Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/activate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { handleUri } from "./handleUri"
export { registerCommands } from "./registerCommands"
export { registerCodeActions } from "./registerCodeActions"
export { registerTerminalActions } from "./registerTerminalActions"
export { registerPearListener } from "./registerPearListener"
85 changes: 85 additions & 0 deletions src/activate/registerPearListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as vscode from "vscode"
import { ClineProvider } from "../core/webview/ClineProvider"
import { assert } from "../utils/util"

export const getPearaiExtension = async () => {
const pearAiExtension = vscode.extensions.getExtension("pearai.pearai")

assert(!!pearAiExtension, "PearAI extension not found")

if (!pearAiExtension.isActive) {
await pearAiExtension.activate()
}

return pearAiExtension
}

export const registerPearListener = async () => {
// Getting the pear ai extension instance
const pearAiExtension = await getPearaiExtension()

// Access the API directly from exports
if (pearAiExtension.exports) {
pearAiExtension.exports.pearAPI.creatorMode.onDidRequestExecutePlan(async (msg: any) => {
console.dir(`onDidRequestNewTask triggered with: ${JSON.stringify(msg)}`)

// Get the sidebar provider
const sidebarProvider = ClineProvider.getSidebarInstance()

if (sidebarProvider) {
// Focus the sidebar first
await vscode.commands.executeCommand("pearai-roo-cline.SidebarProvider.focus")

// Wait for the view to be ready using a helper function
await ensureViewIsReady(sidebarProvider)

if (msg.creatorModeConfig?.creatorMode) {
// Switch to creator mode
await sidebarProvider.handleModeSwitch("creator")
await sidebarProvider.postStateToWebview()
}
// Navigate to chat view
await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })

// Wait a brief moment for UI to update
await new Promise((resolve) => setTimeout(resolve, 300))

let creatorModeConifig = {
creatorMode: msg.creatorMode,
newProjectType: msg.newProjectType,
newProjectPath: msg.newProjectPath,
}

// Initialize with task
await sidebarProvider.initClineWithTask(msg.plan, undefined, undefined, creatorModeConifig)
}
})
} else {
console.error("⚠️⚠️ PearAI API not available in exports ⚠️⚠️")
}
}

// TODO: decide if this is needed
// Helper function to ensure the webview is ready
async function ensureViewIsReady(provider: ClineProvider): Promise<void> {
// If the view is already launched, we're good to go
if (provider.viewLaunched) {
return
}

// Otherwise, we need to wait for it to initialize
return new Promise((resolve) => {
// Set up a one-time listener for when the view is ready
const disposable = provider.on("clineAdded", () => {
// Clean up the listener
disposable.dispose()
resolve()
})

// Set a timeout just in case
setTimeout(() => {
disposable.dispose()
resolve()
}, 5000)
})
}
13 changes: 11 additions & 2 deletions src/api/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,18 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
case "claude-3-opus-20240229":
case "claude-3-haiku-20240307":
betas.push("prompt-caching-2024-07-31")
// Include prompt_key if newProjectType is set
return {
headers: { "anthropic-beta": betas.join(",") },
authorization: `Bearer ${this.options.apiKey}`,
headers: {
"anthropic-beta": betas.join(","),
prompt_key: this.options.creatorModeConfig?.newProjectType
? String(this.options.creatorModeConfig.newProjectType)
: undefined,
project_path: this.options.creatorModeConfig?.newProjectPath
? String(this.options.creatorModeConfig.newProjectPath)
: undefined,
authorization: `Bearer ${this.options.apiKey}`,
},
}
default:
return undefined
Expand Down
6 changes: 6 additions & 0 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { parseXml } from "../utils/xml"
import { readLines } from "../integrations/misc/read-lines"
import { getWorkspacePath } from "../utils/path"
import { isBinaryFile } from "isbinaryfile"
import { creatorModeConfig } from "../shared/pearaiApi"

type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
Expand Down Expand Up @@ -115,6 +116,7 @@ export type ClineOptions = {
rootTask?: Cline
parentTask?: Cline
taskNumber?: number
creatorModeConfig?: creatorModeConfig
}

export class Cline extends EventEmitter<ClineEvents> {
Expand All @@ -131,6 +133,7 @@ export class Cline extends EventEmitter<ClineEvents> {
private pausedModeSlug: string = defaultModeSlug
private pauseInterval: NodeJS.Timeout | undefined

public creatorModeConfig: creatorModeConfig
readonly apiConfiguration: ApiConfiguration
api: ApiHandler
private urlContentFetcher: UrlContentFetcher
Expand Down Expand Up @@ -192,6 +195,7 @@ export class Cline extends EventEmitter<ClineEvents> {
rootTask,
parentTask,
taskNumber,
creatorModeConfig,
}: ClineOptions) {
super()

Expand Down Expand Up @@ -219,6 +223,8 @@ export class Cline extends EventEmitter<ClineEvents> {
this.enableCheckpoints = enableCheckpoints
this.checkpointStorage = checkpointStorage

this.creatorModeConfig = creatorModeConfig ?? { creatorMode: false }

this.rootTask = rootTask
this.parentTask = parentTask
this.taskNumber = taskNumber ?? -1
Expand Down
55 changes: 50 additions & 5 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import { getUri } from "./getUri"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { getWorkspacePath } from "../../utils/path"
import { PEARAI_URL } from "../../shared/pearaiApi"
import { PEARAI_URL, creatorModeConfig } from "../../shared/pearaiApi"
import { PearAIAgentModelsConfig } from "../../api/providers/pearai/pearai"

/**
Expand Down Expand Up @@ -111,6 +111,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
readonly context: vscode.ExtensionContext,
private readonly outputChannel: vscode.OutputChannel,
private readonly renderContext: "sidebar" | "editor" = "sidebar",
private readonly isCreatorView: boolean = false,
) {
super()

Expand All @@ -137,6 +138,16 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
})
}

public static getSidebarInstance(): ClineProvider | undefined {
const sidebar = Array.from(this.activeInstances).find((instance) => !instance.isCreatorView)

if (!sidebar?.view?.visible) {
vscode.commands.executeCommand("pearai-roo-cline.SidebarProvider.focus")
}

return sidebar
}

// Adds a new Cline instance to clineStack, marking the start of a new task.
// The instance is pushed to the top of the stack (LIFO order).
// When the task is completed, the top instance is removed, reactivating the previous task.
Expand Down Expand Up @@ -477,7 +488,12 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
// when initializing a new task, (not from history but from a tool command new_task) there is no need to remove the previouse task
// since the new task is a sub task of the previous one, and when it finishes it is removed from the stack and the caller is resumed
// in this way we can have a chain of tasks, each one being a sub task of the previous one until the main task is finished
public async initClineWithTask(task?: string, images?: string[], parentTask?: Cline) {
public async initClineWithTask(
task?: string,
images?: string[],
parentTask?: Cline,
creatorModeConfig?: creatorModeConfig,
) {
const {
apiConfiguration,
customModePrompts,
Expand All @@ -490,14 +506,27 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
experiments,
} = await this.getState()

// Update API configuration with creator mode
await this.updateApiConfiguration({
...apiConfiguration,
creatorModeConfig,
})

// Post updated state to webview immediately
await this.postStateToWebview()

const modePrompt = customModePrompts?.[mode] as PromptComponent
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")

const pearaiAgentModels = await this.getPearAIAgentModels()

const cline = new Cline({
provider: this,
apiConfiguration: { ...apiConfiguration, pearaiAgentModels },
apiConfiguration: {
...apiConfiguration,
creatorModeConfig,
pearaiAgentModels,
},
customInstructions: effectiveInstructions,
enableDiff,
enableCheckpoints,
Expand All @@ -509,6 +538,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
parentTask,
taskNumber: this.clineStack.length + 1,
creatorModeConfig,
})

await this.addClineToStack(cline)
Expand Down Expand Up @@ -2160,6 +2190,13 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
// Update mode's default config.
const { mode } = await this.getState()
const currentCline = this.getCurrentCline()

// Preserve creator mode when updating configuration
const updatedConfig = {
...apiConfiguration,
creatorModeConfig: currentCline?.creatorModeConfig,
}

if (mode) {
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
Expand All @@ -2171,7 +2208,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}
}

await this.contextProxy.setApiConfiguration(apiConfiguration)
await this.contextProxy.setApiConfiguration(updatedConfig)

if (this.getCurrentCline()) {
this.getCurrentCline()!.api = buildApiHandler(apiConfiguration)
Expand Down Expand Up @@ -2496,8 +2533,10 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
}

async getStateToPostToWebview() {
const currentCline = this.getCurrentCline()
// Get base state
const {
apiConfiguration,
apiConfiguration: baseApiConfiguration,
lastShownAnnouncementId,
customInstructions,
alwaysAllowReadOnly,
Expand Down Expand Up @@ -2545,6 +2584,12 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
maxReadFileLine,
} = await this.getState()

// Construct API configuration with creator mode
const apiConfiguration = {
...baseApiConfiguration,
creatorModeConfig: currentCline?.creatorModeConfig,
}

const telemetryKey = process.env.POSTHOG_API_KEY
const machineId = vscode.env.machineId
const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
Expand Down
9 changes: 8 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ import { telemetryService } from "./services/telemetry/TelemetryService"
import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
import { API } from "./exports/api"

import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
import {
handleUri,
registerCommands,
registerCodeActions,
registerTerminalActions,
registerPearListener,
} from "./activate"
import { formatLanguage } from "./shared/language"

/**
Expand Down Expand Up @@ -255,6 +261,7 @@ export function activate(context: vscode.ExtensionContext) {

registerCodeActions(context)
registerTerminalActions(context)
registerPearListener()

context.subscriptions.push(
vscode.commands.registerCommand("roo-cline.focus", async (...args: any[]) => {
Expand Down
2 changes: 2 additions & 0 deletions src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface ApiHandlerOptions {
pearaiAgentModels?: PearAIAgentModelsConfig
modelMaxThinkingTokens?: number
fakeAi?: unknown
creatorModeConfig?: creatorModeConfig
}

export type ApiConfiguration = ApiHandlerOptions & {
Expand All @@ -94,6 +95,7 @@ export type ApiConfiguration = ApiHandlerOptions & {

// Import GlobalStateKey type from globalState.ts
import { GlobalStateKey } from "./globalState"
import { creatorModeConfig } from "./pearaiApi"

// Define API configuration keys for dynamic object building.
// TODO: This needs actual type safety; a type error should be thrown if
Expand Down
Empty file added src/shared/creatorMode.ts
Empty file.
7 changes: 7 additions & 0 deletions src/shared/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {

// Main modes configuration as an ordered array
export const modes: readonly ModeConfig[] = [
{
slug: "creator",
name: "Creator",
roleDefinition:
"You are PearAI Agent (Powered by Roo Code / Cline), a creative and systematic software architect focused on turning high-level ideas into actionable plans. Your primary goal is to help users transform their ideas into structured action plans.",
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
},
{
slug: "code",
name: "Code",
Expand Down
6 changes: 6 additions & 0 deletions src/shared/pearaiApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,9 @@ export const allModels: { [key: string]: ModelInfo } = {
// Unbound models (single default model)
[`unbound/${unboundDefaultModelId}`]: unboundDefaultModelInfo,
} as const satisfies Record<string, ModelInfo>

export interface creatorModeConfig {
creatorMode?: boolean // Defaults to false when not set
newProjectType?: string
newProjectPath?: string
}
19 changes: 19 additions & 0 deletions src/utils/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class AssertionError extends Error {
constructor(message: string) {
super(message)
// Adding the stack info to error.
// Inspired by: https://blog.dennisokeeffe.com/blog/2020-08-07-error-tracing-with-sentry-and-es6-classes
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AssertionError)
} else {
this.stack = new Error(message).stack
}
this.name = "AssertionError"
}
}

export function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new AssertionError(message)
}
}
4 changes: 4 additions & 0 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
vscInputBorder,
vscSidebarBorder,
} from "../ui"
import { CreatorModeBar } from "./CreatorModeBar"

interface ChatViewProps {
isHidden: boolean
Expand Down Expand Up @@ -1138,6 +1139,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flexDirection: "column",
overflow: "hidden",
}}>
{apiConfiguration?.creatorModeConfig?.creatorMode === true && (
<CreatorModeBar requestedPlan="YEET" isGenerating={isStreaming} />
)}
{task ? (
<>
<TaskHeader
Expand Down
Loading
Loading