From 3bb1d78c17f8caf6d57fb0ebd541c7ea87d9c3e8 Mon Sep 17 00:00:00 2001 From: Felix NyxJae <18661811993@163.com> Date: Thu, 27 Feb 2025 18:08:05 +0800 Subject: [PATCH 01/10] feat: Added human relay function and related message processing initial version --- .gitignore | 5 + src/activate/registerCommands.ts | 43 ++++- src/api/index.ts | 3 + src/api/providers/human-relay.ts | 162 ++++++++++++++++++ src/core/webview/ClineProvider.ts | 19 ++ src/extension.ts | 36 ++++ src/shared/ExtensionMessage.ts | 21 +++ src/shared/WebviewMessage.ts | 14 ++ src/shared/api.ts | 1 + webview-ui/src/App.tsx | 52 ++++++ .../human-relay/HumanRelayDialog.tsx | 105 ++++++++++++ .../src/components/settings/ApiOptions.tsx | 25 +++ 12 files changed, 483 insertions(+), 3 deletions(-) create mode 100644 src/api/providers/human-relay.ts create mode 100644 webview-ui/src/components/human-relay/HumanRelayDialog.tsx diff --git a/.gitignore b/.gitignore index 211d06aa19..bdae7b5b26 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,8 @@ docs/_site/ #Logging logs +.clinerules-architect +.clinerules-ask +.clinerules-code +MemoryBank +.github/copilot-instructions.md diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 69e257e7a5..8cc895f291 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -3,6 +3,19 @@ import delay from "delay" import { ClineProvider } from "../core/webview/ClineProvider" +// Add a global variable to store panel references +let panel: vscode.WebviewPanel | undefined = undefined + +// Get the panel function for command access +export function getPanel(): vscode.WebviewPanel | undefined { + return panel +} + +// Setting the function of the panel +export function setPanel(newPanel: vscode.WebviewPanel | undefined): void { + panel = newPanel +} + export type RegisterCommandOptions = { context: vscode.ExtensionContext outputChannel: vscode.OutputChannel @@ -15,6 +28,22 @@ export const registerCommands = (options: RegisterCommandOptions) => { for (const [command, callback] of Object.entries(getCommandsMap(options))) { context.subscriptions.push(vscode.commands.registerCommand(command, callback)) } + + // Human Relay Dialog Command + context.subscriptions.push( + vscode.commands.registerCommand( + "roo-code.showHumanRelayDialog", + (params: { requestId: string; promptText: string }) => { + if (getPanel()) { + getPanel()?.webview.postMessage({ + type: "showHumanRelayDialog", + requestId: params.requestId, + promptText: params.promptText, + }) + } + }, + ), + ) } const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => { @@ -65,20 +94,28 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit { + setPanel(undefined) + }) // Lock the editor group so clicking on files doesn't open them over the panel await delay(100) diff --git a/src/api/index.ts b/src/api/index.ts index f68c9acd1f..85572632ec 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -16,6 +16,7 @@ import { VsCodeLmHandler } from "./providers/vscode-lm" import { ApiStream } from "./transform/stream" import { UnboundHandler } from "./providers/unbound" import { RequestyHandler } from "./providers/requesty" +import { HumanRelayHandler } from "./providers/human-relay" export interface SingleCompletionHandler { completePrompt(prompt: string): Promise @@ -59,6 +60,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new UnboundHandler(options) case "requesty": return new RequestyHandler(options) + case "human-relay": + return new HumanRelayHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts new file mode 100644 index 0000000000..8454a7c9af --- /dev/null +++ b/src/api/providers/human-relay.ts @@ -0,0 +1,162 @@ +// filepath: e:\Project\Roo-Code\src\api\providers\human-relay.ts +import { Anthropic } from "@anthropic-ai/sdk" +import { ApiHandlerOptions, ModelInfo } from "../../shared/api" +import { ApiHandler, SingleCompletionHandler } from "../index" +import { ApiStream } from "../transform/stream" +import * as vscode from "vscode" +import { ExtensionMessage } from "../../shared/ExtensionMessage" + +/** + * Human Relay API processor + * This processor does not directly call the API, but interacts with the model through human operations copy and paste. + */ +export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { + private options: ApiHandlerOptions + + constructor(options: ApiHandlerOptions) { + this.options = options + } + + /** + * Create a message processing flow, display a dialog box to request human assistance + * @param systemPrompt System prompt words + * @param messages Message list + */ + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + // Get the most recent user message + const latestMessage = messages[messages.length - 1] + + if (!latestMessage) { + throw new Error("No message to relay") + } + + // If it is the first message, splice the system prompt word with the user message + let promptText = "" + if (messages.length === 1) { + promptText = `${systemPrompt}\n\n${getMessageContent(latestMessage)}` + } else { + promptText = getMessageContent(latestMessage) + } + + // Copy to clipboard + await vscode.env.clipboard.writeText(promptText) + + // A dialog box pops up to request user action + const response = await showHumanRelayDialog(promptText) + + if (!response) { + // The user canceled the operation + throw new Error("Human relay operation cancelled") + } + + // Return to the user input reply + yield { type: "text", text: response } + } + + /** + * Get model information + */ + getModel(): { id: string; info: ModelInfo } { + // Human relay does not depend on a specific model, here is a default configuration + return { + id: "human-relay", + info: { + maxTokens: 16384, + contextWindow: 100000, + supportsImages: true, + supportsPromptCache: false, + supportsComputerUse: true, + inputPrice: 0, + outputPrice: 0, + description: "Calling web-side AI model through human relay", + }, + } + } + + /** + * Implementation of a single prompt + * @param prompt Prompt content + */ + async completePrompt(prompt: string): Promise { + // Copy to clipboard + await vscode.env.clipboard.writeText(prompt) + + // A dialog box pops up to request user action + const response = await showHumanRelayDialog(prompt) + + if (!response) { + throw new Error("Human relay operation cancelled") + } + + return response + } +} + +/** + * Extract text content from message object + * @param message + */ +function getMessageContent(message: Anthropic.Messages.MessageParam): string { + if (typeof message.content === "string") { + return message.content + } else if (Array.isArray(message.content)) { + return message.content + .filter((item) => item.type === "text") + .map((item) => (item.type === "text" ? item.text : "")) + .join("\n") + } + return "" +} +/** + * Displays the human relay dialog and waits for user response. + * @param promptText The prompt text that needs to be copied. + * @returns The user's input response or undefined (if canceled). + */ +async function showHumanRelayDialog(promptText: string): Promise { + return new Promise((resolve) => { + // Create a unique request ID + const requestId = Date.now().toString() + + // Register callback to the global callback map + vscode.commands.executeCommand( + "roo-code.registerHumanRelayCallback", + requestId, + (response: string | undefined) => { + resolve(response) + }, + ) + + // Show the WebView dialog + vscode.commands.executeCommand("roo-code.showHumanRelayDialog", { + requestId, + promptText, + }) + + // Provide a temporary UI in case the WebView fails to load + vscode.window + .showInformationMessage( + "Please paste the copied message to the AI, then copy the response back into the dialog", + { + modal: true, + detail: "The message has been copied to the clipboard. If the dialog does not open, please try using the input box.", + }, + "Use Input Box", + ) + .then((selection) => { + if (selection === "Use Input Box") { + // Unregister the callback + vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId) + + vscode.window + .showInputBox({ + prompt: "Please paste the AI's response here", + placeHolder: "Paste the AI's response here...", + ignoreFocusOut: true, + }) + .then((input) => { + resolve(input || undefined) + }) + } + }) + }) +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 5e6170e2ee..1c2ffea550 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1522,7 +1522,26 @@ export class ClineProvider implements vscode.WebviewViewProvider { // Switch back to default mode after deletion await this.updateGlobalState("mode", defaultModeSlug) await this.postStateToWebview() + break } + case "humanRelayResponse": + if (message.requestId && message.text) { + vscode.commands.executeCommand("roo-code.handleHumanRelayResponse", { + requestId: message.requestId, + text: message.text, + cancelled: false, + }) + } + break + + case "humanRelayCancel": + if (message.requestId) { + vscode.commands.executeCommand("roo-code.handleHumanRelayResponse", { + requestId: message.requestId, + cancelled: true, + }) + } + break } }, null, diff --git a/src/extension.ts b/src/extension.ts index a05afa4651..3b148a41ac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,6 +19,18 @@ import { McpServerManager } from "./services/mcp/McpServerManager" let outputChannel: vscode.OutputChannel let extensionContext: vscode.ExtensionContext +// Callback mapping of human relay response +const humanRelayCallbacks = new Map void>() + +/** + * Register a callback function for human relay response + * @param requestId + * @param callback + */ +export function registerHumanRelayCallback(requestId: string, callback: (response: string | undefined) => void): void { + humanRelayCallbacks.set(requestId, callback) +} + // This method is called when your extension is activated. // Your extension is activated the very first time the command is executed. export function activate(context: vscode.ExtensionContext) { @@ -45,6 +57,30 @@ export function activate(context: vscode.ExtensionContext) { registerCommands({ context, outputChannel, provider: sidebarProvider }) + // Register human relay response processing command + context.subscriptions.push( + vscode.commands.registerCommand( + "roo-code.handleHumanRelayResponse", + (response: { requestId: string; text?: string; cancelled?: boolean }) => { + const callback = humanRelayCallbacks.get(response.requestId) + if (callback) { + if (response.cancelled) { + callback(undefined) + } else { + callback(response.text) + } + humanRelayCallbacks.delete(response.requestId) + } + }, + ), + ) + + context.subscriptions.push( + vscode.commands.registerCommand("roo-code.unregisterHumanRelayCallback", (requestId: string) => { + humanRelayCallbacks.delete(requestId) + }), + ) + /** * We use the text document content provider API to show the left side for diff * view by creating a virtual document for the original content. This makes it diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index e87edffed1..16a18043f5 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -45,6 +45,9 @@ export interface ExtensionMessage { | "updateCustomMode" | "deleteCustomMode" | "currentCheckpointUpdated" + | "showHumanRelayDialog" + | "humanRelayResponse" + | "humanRelayCancel" text?: string action?: | "chatButtonClicked" @@ -239,4 +242,22 @@ export interface ClineApiReqInfo { streamingFailedMessage?: string } +// Human relay related message types +export interface ShowHumanRelayDialogMessage { + type: "showHumanRelayDialog" + requestId: string + promptText: string +} + +export interface HumanRelayResponseMessage { + type: "humanRelayResponse" + requestId: string + text: string +} + +export interface HumanRelayCancelMessage { + type: "humanRelayCancel" + requestId: string +} + export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index fde7442cc1..2b0c68f7be 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -94,6 +94,8 @@ export interface WebviewMessage { | "checkpointRestore" | "deleteMcpServer" | "maxOpenTabsContext" + | "HumanRelayResponseMessage" + | "HumanRelayCancelMessage" text?: string disabled?: boolean askResponse?: ClineAskResponse @@ -119,6 +121,18 @@ export interface WebviewMessage { source?: "global" | "project" } +// Human relay related message types +export interface HumanRelayResponseMessage { + type: "humanRelayResponse" + requestId: string + text: string +} + +export interface HumanRelayCancelMessage { + type: "humanRelayCancel" + requestId: string +} + export const checkoutDiffPayloadSchema = z.object({ ts: z.number(), commitHash: z.string(), diff --git a/src/shared/api.ts b/src/shared/api.ts index e7e4c54db6..68b2f87c45 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -16,6 +16,7 @@ export type ApiProvider = | "mistral" | "unbound" | "requesty" + | "human-relay" export interface ApiHandlerOptions { apiModelId?: string diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 3ae441cd52..5909a3eaef 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -11,6 +11,7 @@ import SettingsView, { SettingsViewRef } from "./components/settings/SettingsVie import WelcomeView from "./components/welcome/WelcomeView" import McpView from "./components/mcp/McpView" import PromptsView from "./components/prompts/PromptsView" +import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" type Tab = "settings" | "history" | "mcp" | "prompts" | "chat" @@ -28,6 +29,17 @@ const App = () => { const [tab, setTab] = useState("chat") const settingsRef = useRef(null) + // Human Relay Dialog Status + const [humanRelayDialogState, setHumanRelayDialogState] = useState<{ + isOpen: boolean + requestId: string + promptText: string + }>({ + isOpen: false, + requestId: "", + promptText: "", + }) + const switchTab = useCallback((newTab: Tab) => { if (settingsRef.current?.checkUnsaveChanges) { settingsRef.current.checkUnsaveChanges(() => setTab(newTab)) @@ -47,10 +59,36 @@ const App = () => { switchTab(newTab) } } + + // Processing displays human relay dialog messages + if (message.type === "showHumanRelayDialog" && message.requestId && message.promptText) { + setHumanRelayDialogState({ + isOpen: true, + requestId: message.requestId, + promptText: message.promptText, + }) + } }, [switchTab], ) + // Processing Human Relay Dialog Submission + const handleHumanRelaySubmit = (requestId: string, text: string) => { + vscode.postMessage({ + type: "humanRelayResponse", + requestId, + text, + }) + } + + // Handle Human Relay dialog box cancel + const handleHumanRelayCancel = (requestId: string) => { + vscode.postMessage({ + type: "humanRelayCancel", + requestId, + }) + } + useEvent("message", onMessage) useEffect(() => { @@ -60,6 +98,11 @@ const App = () => { } }, [shouldShowAnnouncement]) + // Tell Extension that we are ready to receive messages + useEffect(() => { + vscode.postMessage({ type: "webviewDidLaunch" }) + }, []) + if (!didHydrateState) { return null } @@ -80,6 +123,15 @@ const App = () => { hideAnnouncement={() => setShowAnnouncement(false)} showHistoryView={() => switchTab("history")} /> + {/* Human Relay Dialog */} + setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))} + onSubmit={handleHumanRelaySubmit} + onCancel={handleHumanRelayCancel} + /> ) } diff --git a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx new file mode 100644 index 0000000000..ea306d11d7 --- /dev/null +++ b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx @@ -0,0 +1,105 @@ +import * as React from "react" +import { Button } from "../ui/button" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog" +import { Textarea } from "../ui/textarea" +import { useClipboard } from "../ui/hooks" +import { Check, Copy, X } from "lucide-react" + +interface HumanRelayDialogProps { + isOpen: boolean + onClose: () => void + requestId: string + promptText: string + onSubmit: (requestId: string, text: string) => void + onCancel: (requestId: string) => void +} + +/** + * Human Relay Dialog Component + * Displays the prompt text that needs to be copied and provides an input box for the user to paste the AI's response. + */ +export const HumanRelayDialog: React.FC = ({ + isOpen, + onClose, + requestId, + promptText, + onSubmit, + onCancel, +}) => { + const [response, setResponse] = React.useState("") + const { onCopy } = useClipboard(promptText) + const [isCopyClicked, setIsCopyClicked] = React.useState(false) + + // Copy to clipboard and show a success message + const handleCopy = () => { + onCopy() + setIsCopyClicked(true) + setTimeout(() => { + setIsCopyClicked(false) + }, 2000) + } + + // Submit the response + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (response.trim()) { + onSubmit(requestId, response) + onClose() + } + } + + // Cancel the operation + const handleCancel = () => { + onCancel(requestId) + onClose() + } + + return ( + !open && handleCancel()}> + + + Human Relay - Please Help Copy and Paste Information + + Please copy the text below to the web AI, then paste the AI's response into the input box below. + + + +
+
+