diff --git a/src/api/index.ts b/src/api/index.ts index bf40b5c2990..e9799644010 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -19,7 +19,7 @@ import { RequestyHandler } from "./providers/requesty" import { PearAiHandler } from "./providers/pearai" export interface SingleCompletionHandler { - completePrompt(prompt: string): Promise + completePrompt(prompt: string, useCreatorMode?: boolean): Promise } export interface ApiHandler { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 4f97d3771ad..81d458e2880 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -48,6 +48,7 @@ import { Cline } from "../Cline" import { openMention } from "../mentions" import { getNonce } from "./getNonce" import { getUri } from "./getUri" +import { PearAiHandler } from "../../api/providers/pearai" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -73,10 +74,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement configManager: ConfigManager customModesManager: CustomModesManager + private proprietaryContext: string[] = [] constructor( readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel, + public viewType: "pearai.roo.agentChat" | "pearai.roo.creatorOverlayView" = "pearai.roo.agentChat", ) { this.outputChannel.appendLine("ClineProvider instantiated") ClineProvider.activeInstances.add(this) @@ -86,6 +89,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() }) + this.listenForPearAiMessages() + // Initialize MCP Hub through the singleton manager McpServerManager.getInstance(this.context, this) .then((hub) => { @@ -127,6 +132,30 @@ export class ClineProvider implements vscode.WebviewViewProvider { McpServerManager.unregisterProvider(this) } + private async listenForPearAiMessages() { + // Getting the pear ai extension instance + const pearaiExtension = vscode.extensions.getExtension("pearai.pearai") + + if (!pearaiExtension) { + console.log("PearAI Extension not found") + return + } + + if (!pearaiExtension.isActive) { + await pearaiExtension.activate() + } + + // Access the API directly from exports + if (pearaiExtension.exports) { + // TODO: SETUP TYPES FOR THE PEAR AI EXPORT + pearaiExtension.exports.pearAPI.creatorMode.onDidRequestExecutePlan((msg: any) => { + console.dir(`onDidRequestNewTask triggered with: ${JSON.stringify(msg)}`) + }) + } else { + console.log("PearAI API not available in exports") + } + } + public static getVisibleInstance(): ClineProvider | undefined { return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) } @@ -317,7 +346,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.outputChannel.appendLine("Webview view resolved") } - public async initClineWithTask(task?: string, images?: string[]) { + public async initClineWithTask(task?: string, images?: string[], proprietaryContext?: ("CreatorMode" | string)[]) { + this.proprietaryContext = proprietaryContext ?? [] + // TODO - MUST DO BEFORE MERGE - WE NEED TO HANDLE THE PROPRIETARY CONTEXT await this.clearTask() const { apiConfiguration, @@ -414,6 +445,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { window.$RefreshReg$ = () => {} window.$RefreshSig$ = () => (type) => type window.__vite_plugin_react_preamble_installed__ = true + window.viewType = "${this.viewType}"; ` @@ -510,6 +542,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { + Roo Code @@ -735,6 +770,68 @@ export class ClineProvider implements vscode.WebviewViewProvider { // initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task await this.initClineWithTask(message.text, message.images) break + case "newCreatorModeTask": + // TODO: Get back the plan from the AI model + // Return the plan to the UI to be edited + const { apiConfiguration } = await this.getState() + // apiConfiguration.apiModelId + // const pearAiModels = usePearAiModels(apiConfiguration) + // const pearAI = vscode.extensions.getExtension("pearai.pearai"); + // if(!pearAI) { + // console.warn("COULDN'T FIND PEAR AI SUBMODULE"); + // } + // pearAI?.exports + // console.log("PEAR AI EXPORTS", pearAI?.exports); + const systemPrompt = ` + Best practices: + For any webapp project, ALWAYS adhere to these rules, unless otherwise specified: + React with TypeScript for the frontend + Tailwind CSS for styling + Shadcn/UI components (via Radix UI) + Supabase for backend/database + React Query for data management + React Router for navigation + Recharts for data visualization + React Hook Form for form handling` + const pearAIClass = new PearAiHandler(apiConfiguration) + let responseText = "" + const messageGenerator = pearAIClass.createMessage(systemPrompt, [ + { + role: "user", + content: message.text, + }, + ]) + + for await (const chunk of messageGenerator) { + if (chunk.type === "text") { + responseText += chunk.text + } + + await this.postMessageToWebview({ + type: "planCreationStream", + text: responseText, + }) + } + await this.postMessageToWebview({ + type: "planCreationSuccess", + text: responseText, + }) + + break + case "creatorModePlannedTaskSubmit": + // TODO: Trigger the "newTask" flow flow, initialising cline with a task + // Go into the planned mode shizz + // vscode.commands.executeCommand("workbench.action.enterCreatorMode") + await this.postMessageToWebview({ + type: "invoke", + invoke: "setChatBoxMessage", + text: message.text, + }) + await this.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" }) + + await this.initClineWithTask(message.text, message.images) + await this.postStateToWebview() + break case "apiConfiguration": if (message.apiConfiguration) { await this.updateApiConfiguration(message.apiConfiguration) @@ -1338,6 +1435,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { try { await this.configManager.saveConfig(message.text, message.apiConfiguration) const listApiConfig = await this.configManager.listConfig() + await this.updateGlobalState("listApiConfigMeta", listApiConfig) } catch (error) { this.outputChannel.appendLine( diff --git a/src/extension.ts b/src/extension.ts index ef8895ee24b..d08d87244c1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -35,7 +35,7 @@ export function activate(context: vscode.ExtensionContext) { context.globalState.update("allowedCommands", defaultCommands) } - const sidebarProvider = new ClineProvider(context, outputChannel) + const sidebarProvider = new ClineProvider(context, outputChannel, "pearai.roo.agentChat") context.subscriptions.push( vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, { @@ -79,6 +79,12 @@ export function activate(context: vscode.ExtensionContext) { }), ) + const creatorOverlay = new ClineProvider(context, outputChannel, "pearai.roo.creatorOverlayView") + + context.subscriptions.push( + vscode.window.registerWebviewViewProvider("pearai.roo.creatorOverlayView", creatorOverlay), + ) + // context.subscriptions.push( // vscode.commands.registerCommand("roo-cline.mcpButtonClicked", () => { // sidebarProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" }) @@ -95,7 +101,7 @@ export function activate(context: vscode.ExtensionContext) { outputChannel.appendLine("Opening Roo Code in new tab") // (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event) // https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts - const tabProvider = new ClineProvider(context, outputChannel) + const tabProvider = new ClineProvider(context, outputChannel, "pearai.roo.agentChat") //const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0)) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index e87edffed16..c5fe43a0c0a 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -45,6 +45,8 @@ export interface ExtensionMessage { | "updateCustomMode" | "deleteCustomMode" | "currentCheckpointUpdated" + | "planCreationStream" + | "planCreationSuccess" text?: string action?: | "chatButtonClicked" @@ -194,6 +196,8 @@ export interface ClineSayTool { | "searchFiles" | "switchMode" | "newTask" + | "newCreatorModeTask" + | "creatorModePlannedTaskSubmit" path?: string diff?: string content?: string diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 6f24415b624..c41f7705dec 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -25,6 +25,8 @@ export interface WebviewMessage { | "alwaysAllowExecute" | "webviewDidLaunch" | "newTask" + | "newCreatorModeTask" + | "creatorModePlannedTaskSubmit" | "askResponse" | "clearTask" | "didShowAnnouncement" @@ -95,6 +97,8 @@ export interface WebviewMessage { | "openPearAiAuth" | "deleteMcpServer" | "maxOpenTabsContext" + | "pearAiHideCreatorLoadingOverlay" + | "pearAiCloseCreatorInterface" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 2739ce988f7..434650c5b86 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -34,6 +34,7 @@ "react-dom": "^18.3.1", "react-markdown": "^9.0.3", "react-remark": "^2.1.0", + "react-router-dom": "^7.3.0", "react-textarea-autosize": "^8.5.3", "react-use": "^17.5.1", "react-virtuoso": "^4.7.13", @@ -6674,6 +6675,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -8661,6 +8668,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -16775,6 +16791,46 @@ } } }, + "node_modules/react-router": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz", + "integrity": "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.3.0.tgz", + "integrity": "sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.3.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -17499,6 +17555,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -18616,6 +18678,12 @@ "dev": true, "license": "0BSD" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 99bae778e13..aa839f59655 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -15,15 +15,15 @@ "build-storybook": "storybook build" }, "dependencies": { + "@headlessui/react": "^2.2.0", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-popover": "^1.1.6", - "@headlessui/react": "^2.2.0", - "@radix-ui/react-select": "^2.1.5", "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-select": "^2.1.5", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", @@ -41,6 +41,7 @@ "react-dom": "^18.3.1", "react-markdown": "^9.0.3", "react-remark": "^2.1.0", + "react-router-dom": "^7.3.0", "react-textarea-autosize": "^8.5.3", "react-use": "^17.5.1", "react-virtuoso": "^4.7.13", diff --git a/webview-ui/src/creatorOverlay/index.tsx b/webview-ui/src/creatorOverlay/index.tsx new file mode 100644 index 00000000000..109c9226107 --- /dev/null +++ b/webview-ui/src/creatorOverlay/index.tsx @@ -0,0 +1,160 @@ +import { vscode } from "@/utils/vscode" +import { useCallback, useEffect, useRef, useState } from "react" +import { useEvent } from "react-use" +import { ExtensionMessage } from "../../../src/shared/ExtensionMessage" +import { RGBWrapper } from "./rgbBackground" +import { PlanEditor } from "./planEditor" +import { InputBox } from "./inputBox" + +/** + * CreatorOverlay component provides a full-screen overlay with an auto-focusing input field + * for capturing user commands or queries. + * + * - This automatically captures keystrokes and redirects them to the input + * - Global keyboard handling: Captures keyboard input even when the textarea isn't focused + * - Automatic text area resizing + * - Escape key closes the overlay + * - Enter submits the request + * - Clicking the background closes the overlay + */ +export const CreatorOverlay = () => { + const [initialMessage, setInitialMessage] = useState("") + const [newProjectPlan, setNewProjectPlan] = useState("") + const [planCreationDone, setPlanCreationDone] = useState(false) + const [isStreaming, setIsStreaming] = useState(false) + const textareaRef = useRef(null) + const isCapturingRef = useRef(false) + + const close = useCallback(() => { + setInitialMessage("") + setNewProjectPlan("") + setPlanCreationDone(false) + setIsStreaming(false) + vscode.postMessage({ + type: "pearAiCloseCreatorInterface", + }) + setInitialMessage("") + }, []) + + const forceFocus = useCallback(() => { + if (!textareaRef.current) return + + try { + textareaRef.current.focus() + textareaRef.current.focus({ preventScroll: false }) + textareaRef.current.scrollIntoView({ behavior: "smooth", block: "center" }) + } catch (e) { + console.error("Focus attempt failed:", e) + } + }, []) + + useEffect(() => { + vscode.postMessage({ + type: "pearAiHideCreatorLoadingOverlay", + }) + + forceFocus() + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + close() + return + } + + if (document.activeElement === textareaRef.current) { + return + } + + if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { + e.preventDefault() + e.stopPropagation() + + forceFocus() + + if (!isCapturingRef.current) { + setInitialMessage((prevText) => prevText + e.key) + isCapturingRef.current = true + + setTimeout(() => { + isCapturingRef.current = false + }, 100) + } + } + } + + window.addEventListener("keydown", handleKeyDown, { capture: true }) + + return () => { + window.removeEventListener("keydown", handleKeyDown, { capture: true }) + } + }, [close, forceFocus]) + + const handleRequest = useCallback(() => { + if (initialMessage.trim()) { + setIsStreaming(true) + vscode.postMessage({ + type: "newCreatorModeTask", + text: initialMessage, + }) + } + }, [initialMessage]) + + const handleMakeIt = useCallback(() => { + if (newProjectPlan.trim()) { + vscode.postMessage({ + type: "creatorModePlannedTaskSubmit", + text: `INITIAL IDEA: ${initialMessage} -- PLAN: ${newProjectPlan}`, + }) + close() + } + }, [newProjectPlan, close]) + + const onMessage = useCallback((e: MessageEvent) => { + const message: ExtensionMessage = e.data + + if (message.type === "planCreationStream" && message.text) { + console.log(`STREAMED TEXT: ${message.text}`) + setNewProjectPlan(message.text) + } else if (message.type === "planCreationSuccess") { + setIsStreaming(false) + setPlanCreationDone(true) + } + }, []) + + useEvent("message", onMessage) + + return ( +
+
e.stopPropagation()} className="justify-center align-middle m-auto w-full max-w-3xl "> + + {/* Stage 1: get the input from the user about what to make */} + { + (!isStreaming && !planCreationDone) && ( + + ) + } + {/* Stage 2: Stream down the plan and display it to the user, let them comment and formulate the plan */} + {(isStreaming || planCreationDone) && ( + <> +
+ + + + )} +
+
+
+ ) +} diff --git a/webview-ui/src/creatorOverlay/inputBox.tsx b/webview-ui/src/creatorOverlay/inputBox.tsx new file mode 100644 index 00000000000..0fb0a2ab344 --- /dev/null +++ b/webview-ui/src/creatorOverlay/inputBox.tsx @@ -0,0 +1,89 @@ +import { Button } from "./ui/button" +import { ArrowTurnDownLeftIcon } from "@heroicons/react/24/outline" +import { EnterIcon } from "@radix-ui/react-icons" +import { FileText, Pencil, Sun } from "lucide-react" +import React, { useCallback, useState } from "react" + +interface InputBoxProps { + textareaRef: React.RefObject + initialMessage: string + setInitialMessage: (value: string) => void + handleRequest: () => void + isDisabled: boolean +} + +export const InputBox: React.FC = ({ + textareaRef, + initialMessage, + setInitialMessage, + handleRequest, + isDisabled, +}) => { + const [makeAPlan, setMakeAPlan] = useState(false); + + const handleTextareaChange = useCallback( + (e: React.ChangeEvent) => { + setInitialMessage(e.target.value) + + const textarea = e.target + textarea.style.height = "36px" + const scrollHeight = textarea.scrollHeight + textarea.style.height = Math.min(scrollHeight, 100) + "px" + }, + [setInitialMessage], + ) + + const handleTextareaKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey && initialMessage.trim()) { + e.preventDefault() + handleRequest() + } + }, + [handleRequest, initialMessage], + ) + + return ( +
+
+