From 958406fe6ceac716a13a77ab7a581504be7d1cb6 Mon Sep 17 00:00:00 2001 From: Acorn221 Date: Mon, 17 Mar 2025 14:35:56 +0000 Subject: [PATCH 01/10] feat: creator overlay --- src/core/webview/ClineProvider.ts | 11 ++ src/extension.ts | 10 +- src/shared/WebviewMessage.ts | 2 + webview-ui/package.json | 1 + webview-ui/src/creatorOverlay/index.tsx | 154 ++++++++++++++++++++++++ webview-ui/src/index.tsx | 4 +- webview-ui/src/router.tsx | 29 +++++ webview-ui/src/vite-env.d.ts | 8 ++ 8 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 webview-ui/src/creatorOverlay/index.tsx create mode 100644 webview-ui/src/router.tsx diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 4f97d3771ad..7ba3d01956f 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -77,6 +77,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { 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) @@ -414,6 +415,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 +512,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { + Roo Code @@ -1542,6 +1547,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { ), ) break + case "pearAiCloseCreatorInterface": + await vscode.commands.executeCommand("workbench.action.closeCreatorView") + break + case "pearAiHideCreatorLoadingOverlay": + await vscode.commands.executeCommand("workbench.action.hideCreatorLoadingOverlay") + break } }, null, 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/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 6f24415b624..a05f924428a 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -95,6 +95,8 @@ export interface WebviewMessage { | "openPearAiAuth" | "deleteMcpServer" | "maxOpenTabsContext" + | "pearAiHideCreatorLoadingOverlay" + | "pearAiCloseCreatorInterface" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/webview-ui/package.json b/webview-ui/package.json index 99bae778e13..5a778a52c29 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -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..c02ad13b5c2 --- /dev/null +++ b/webview-ui/src/creatorOverlay/index.tsx @@ -0,0 +1,154 @@ +import { vscode } from "@/utils/vscode" +import { EnterIcon } from "@radix-ui/react-icons" +import { Sun } from "lucide-react" +import { useCallback, useEffect, useRef, useState } from "react" + +/** + * 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 [text, setText] = useState("") + const textareaRef = useRef(null) + const isCapturingRef = useRef(false) + + const close = useCallback(() => { + vscode.postMessage({ + type: "pearAiCloseCreatorInterface", + }) + setText("") + }, []) + + 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) { + setText((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 (text.trim()) { + vscode.postMessage({ + type: "newTask", + text, + }) + close() + } + }, [text, close]) + + const handleTextareaChange = useCallback((e: React.ChangeEvent) => { + setText(e.target.value) + + const textarea = e.target + textarea.style.height = "36px" + const scrollHeight = textarea.scrollHeight + textarea.style.height = Math.min(scrollHeight, 100) + "px" + }, []) + + const handleTextareaKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey && text.trim()) { + e.preventDefault() + handleRequest() + } + }, + [handleRequest, text], + ) + + return ( +
+
e.stopPropagation()}> +
+
+
+
+ +
+
+