diff --git a/package-lock.json b/package-lock.json index 90534cf55e8..af7193c12b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12640,7 +12640,6 @@ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "chalk": "^2.4.1", diff --git a/src/activate/index.ts b/src/activate/index.ts index 658bf467f7a..2fff46a5f04 100644 --- a/src/activate/index.ts +++ b/src/activate/index.ts @@ -2,3 +2,4 @@ export { handleUri } from "./handleUri" export { registerCommands } from "./registerCommands" export { registerCodeActions } from "./registerCodeActions" export { registerTerminalActions } from "./registerTerminalActions" +export { registerPearListener } from "./registerPearListener" diff --git a/src/activate/registerPearListener.ts b/src/activate/registerPearListener.ts new file mode 100644 index 00000000000..e40a2494b89 --- /dev/null +++ b/src/activate/registerPearListener.ts @@ -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 { + // 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) + }) +} diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index a1773d8c52b..de021d85f09 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -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 diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 6cb26fbbc61..b3e22c8e27b 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -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 type UserContent = Array @@ -115,6 +116,7 @@ export type ClineOptions = { rootTask?: Cline parentTask?: Cline taskNumber?: number + creatorModeConfig?: creatorModeConfig } export class Cline extends EventEmitter { @@ -131,6 +133,7 @@ export class Cline extends EventEmitter { private pausedModeSlug: string = defaultModeSlug private pauseInterval: NodeJS.Timeout | undefined + public creatorModeConfig: creatorModeConfig readonly apiConfiguration: ApiConfiguration api: ApiHandler private urlContentFetcher: UrlContentFetcher @@ -192,6 +195,7 @@ export class Cline extends EventEmitter { rootTask, parentTask, taskNumber, + creatorModeConfig, }: ClineOptions) { super() @@ -219,6 +223,8 @@ export class Cline extends EventEmitter { this.enableCheckpoints = enableCheckpoints this.checkpointStorage = checkpointStorage + this.creatorModeConfig = creatorModeConfig ?? { creatorMode: false } + this.rootTask = rootTask this.parentTask = parentTask this.taskNumber = taskNumber ?? -1 diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a113472d734..4164ed6d658 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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" /** @@ -111,6 +111,7 @@ export class ClineProvider extends EventEmitter implements readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel, private readonly renderContext: "sidebar" | "editor" = "sidebar", + private readonly isCreatorView: boolean = false, ) { super() @@ -137,6 +138,16 @@ export class ClineProvider extends EventEmitter 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. @@ -477,7 +488,12 @@ export class ClineProvider extends EventEmitter 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, @@ -490,6 +506,15 @@ export class ClineProvider extends EventEmitter 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") @@ -497,7 +522,11 @@ export class ClineProvider extends EventEmitter implements const cline = new Cline({ provider: this, - apiConfiguration: { ...apiConfiguration, pearaiAgentModels }, + apiConfiguration: { + ...apiConfiguration, + creatorModeConfig, + pearaiAgentModels, + }, customInstructions: effectiveInstructions, enableDiff, enableCheckpoints, @@ -509,6 +538,7 @@ export class ClineProvider extends EventEmitter implements rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, parentTask, taskNumber: this.clineStack.length + 1, + creatorModeConfig, }) await this.addClineToStack(cline) @@ -2160,6 +2190,13 @@ export class ClineProvider extends EventEmitter 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") @@ -2171,7 +2208,7 @@ export class ClineProvider extends EventEmitter implements } } - await this.contextProxy.setApiConfiguration(apiConfiguration) + await this.contextProxy.setApiConfiguration(updatedConfig) if (this.getCurrentCline()) { this.getCurrentCline()!.api = buildApiHandler(apiConfiguration) @@ -2496,8 +2533,10 @@ export class ClineProvider extends EventEmitter implements } async getStateToPostToWebview() { + const currentCline = this.getCurrentCline() + // Get base state const { - apiConfiguration, + apiConfiguration: baseApiConfiguration, lastShownAnnouncementId, customInstructions, alwaysAllowReadOnly, @@ -2545,6 +2584,12 @@ export class ClineProvider extends EventEmitter 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("allowedCommands") || [] diff --git a/src/extension.ts b/src/extension.ts index d26101a9289..d462df85a65 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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" /** @@ -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[]) => { diff --git a/src/shared/api.ts b/src/shared/api.ts index 4f4e7aebe34..3a8b9127898 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -85,6 +85,7 @@ export interface ApiHandlerOptions { pearaiAgentModels?: PearAIAgentModelsConfig modelMaxThinkingTokens?: number fakeAi?: unknown + creatorModeConfig?: creatorModeConfig } export type ApiConfiguration = ApiHandlerOptions & { @@ -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 diff --git a/src/shared/creatorMode.ts b/src/shared/creatorMode.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 374958f7434..51403fc07fa 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -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", diff --git a/src/shared/pearaiApi.ts b/src/shared/pearaiApi.ts index 2ff45a276df..6c0ddbde20e 100644 --- a/src/shared/pearaiApi.ts +++ b/src/shared/pearaiApi.ts @@ -130,3 +130,9 @@ export const allModels: { [key: string]: ModelInfo } = { // Unbound models (single default model) [`unbound/${unboundDefaultModelId}`]: unboundDefaultModelInfo, } as const satisfies Record + +export interface creatorModeConfig { + creatorMode?: boolean // Defaults to false when not set + newProjectType?: string + newProjectPath?: string +} diff --git a/src/utils/util.ts b/src/utils/util.ts new file mode 100644 index 00000000000..54e5cbc8f04 --- /dev/null +++ b/src/utils/util.ts @@ -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) + } +} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 9380a5807f8..bab4afde721 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -44,6 +44,7 @@ import { vscInputBorder, vscSidebarBorder, } from "../ui" +import { CreatorModeBar } from "./CreatorModeBar" interface ChatViewProps { isHidden: boolean @@ -1138,6 +1139,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie flexDirection: "column", overflow: "hidden", }}> + {apiConfiguration?.creatorModeConfig?.creatorMode === true && ( + + )} {task ? ( <> void + nextCallback?: () => void + className?: string +} +// from: https://vscode.dev/github/trypear/pearai-submodule/blob/acorn/253-submodule-api-fixed/gui/src/pages/creator/ui/planningBar.tsx#L15-L50 +// TODO: UI LIBRARY COMPONENT SHARING SHIZZ HERE! + +export const CreatorModeBar: FC = ({ + isGenerating, + requestedPlan, + playCallback, + nextCallback, + className, +}) => { + return ( +
+ {isGenerating &&
} +
+
+
+
+
Planning
+
+
{requestedPlan}
+
+
+ +
+
+ + + +
+ + {/* */} +
+
+ ) +} diff --git a/webview-ui/src/components/chat/button/index.tsx b/webview-ui/src/components/chat/button/index.tsx new file mode 100644 index 00000000000..a5e043aa6e8 --- /dev/null +++ b/webview-ui/src/components/chat/button/index.tsx @@ -0,0 +1,116 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +// FROM: https://vscode.dev/github/trypear/pearai-submodule/blob/acorn/253-submodule-api-fixed/gui/src/pages/creator/ui/button/index.tsx#L1-L121 +// TODO: UI LIBRARY COMPONENT SHARING SHIZZ HERE! +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 border-none whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#a1a1aa] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-[#18181b] text-[#fafafa] shadow hover:bg-[#27272a]", + destructive: "bg-[#ef4444] text-[#fafafa] shadow-sm hover:bg-[#dc2626]", + outline: "border border-[#e4e4e7] bg-[#ffffff] shadow-sm hover:bg-[#f4f4f5] hover:text-[#18181b]", + secondary: "bg-[#f4f4f5] text-[#18181b] hover:bg-[#e4e4e7]", + ghost: "hover:bg-[#f4f4f5] hover:text-[#18181b]", + link: "text-[#18181b] underline-offset-4 hover:underline", + }, + size: { + // default: "h-9 px-4 py-2", + default: "h-7 rounded-md px-2 text-md", + sm: "h-6 rounded-md px-2 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + toggled: { + true: "", + }, + }, + compoundVariants: [ + { + variant: "default", + toggled: true, + className: " bg-[#3030ad] text-[#0B84FF] hover:bg-[#3a3ad2]", // bg-[#27272a] text-[#fafafa] + }, + { + variant: "destructive", + toggled: true, + className: "bg-[#dc2626] text-[#fafafa]", + }, + { + variant: "outline", + toggled: true, + className: "bg-[#f4f4f5] text-[#18181b] border-[#a1a1aa]", + }, + { + variant: "secondary", + toggled: true, + // className: "bg-[#e4e4e7] text-[#18181b]" + className: "bg-[#E3EFFF] text-[#4388F8] hover:bg-[#D1E3FF]", + }, + { + variant: "ghost", + toggled: true, + className: "bg-[#f4f4f5] text-[#18181b]", + }, + { + variant: "link", + toggled: true, + className: "text-[#18181b] underline", + }, + ], + defaultVariants: { + variant: "default", + size: "default", + toggled: false, + }, + }, +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean + onToggle?: (toggled: boolean) => void +} + +const Button = React.forwardRef( + ( + { className, variant, size, toggled: initialToggled = false, asChild = false, onToggle, onClick, ...props }, + ref, + ) => { + const Comp = asChild ? Slot : "button" + const [toggled, setToggled] = React.useState(initialToggled) + + const handleClick = (event: React.MouseEvent) => { + if (onToggle) { + const newToggled = !toggled + setToggled(newToggled) + onToggle(newToggled) + } + + onClick?.(event) + } + + return ( + + ) + }, +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index fa00a72f38e..ad59df9516a 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1607,8 +1607,8 @@ export function normalizeApiConfiguration( if (modelId === "pearai-model" && models[modelId].underlyingModelUpdated) { let modelInfo = models[modelId].underlyingModelUpdated selectedModelInfo = { - contextWindow: modelInfo.contextWindow || 4096, // provide default or actual value - supportsPromptCache: modelInfo.supportsPromptCaching || false, // provide default or actual value + contextWindow: modelInfo.contextWindow || 4096, + supportsPromptCache: modelInfo.supportsPromptCaching || false, ...modelInfo, } } else { @@ -1619,7 +1619,13 @@ export function normalizeApiConfiguration( selectedModelInfo = models[defaultId] } - return { selectedProvider: provider, selectedModelId, selectedModelInfo } + // Preserve all original configuration fields while updating model-specific ones + return { + selectedProvider: provider, + selectedModelId, + selectedModelInfo, + ...apiConfiguration, + } } switch (provider) { @@ -1705,8 +1711,16 @@ export function normalizeApiConfiguration( } case "pearai": { // Always use the models from the hook which are fetched when provider is selected - let query = pearAiModelsQuery - return getProviderData(pearAiModelsQuery || {}, pearAiDefaultModelId) + const { selectedProvider, selectedModelId, selectedModelInfo } = getProviderData( + pearAiModelsQuery || {}, + pearAiDefaultModelId, + ) + return { + selectedProvider, + selectedModelId, + selectedModelInfo, + creatorMode: apiConfiguration?.creatorModeConfig?.creatorMode, + } } default: return getProviderData(anthropicModels, anthropicDefaultModelId)