From 9697f6fd5ebe49cc9360dedb9ccb3ea3b0f24ba7 Mon Sep 17 00:00:00 2001 From: Felix NyxJae <18661811993@163.com> Date: Sat, 8 Mar 2025 19:31:21 +0800 Subject: [PATCH 01/19] feat(human-relay): add clipboard monitoring for AI responses - Add automatic detection of AI responses copied from browser - Implement configurable monitoring interval (100-2000ms) - Add duplicate response detection with warning alerts - Update UI to show monitoring status and warnings - Add new configuration options in settings --- src/api/providers/human-relay.ts | 114 +++++++++++++++++- src/shared/WebviewMessage.ts | 2 + src/shared/api.ts | 5 + src/shared/globalState.ts | 2 + webview-ui/src/App.tsx | 13 +- .../human-relay/HumanRelayDialog.tsx | 48 +++++++- .../src/components/settings/ApiOptions.tsx | 34 ++++++ 7 files changed, 211 insertions(+), 7 deletions(-) diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts index b8bd4c28298..684e209aee1 100644 --- a/src/api/providers/human-relay.ts +++ b/src/api/providers/human-relay.ts @@ -1,4 +1,3 @@ -// 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" @@ -46,7 +45,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { await vscode.env.clipboard.writeText(promptText) // A dialog box pops up to request user action - const response = await showHumanRelayDialog(promptText) + const response = await showHumanRelayDialog(promptText, this.options) if (!response) { // The user canceled the operation @@ -86,7 +85,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { await vscode.env.clipboard.writeText(prompt) // A dialog box pops up to request user action - const response = await showHumanRelayDialog(prompt) + const response = await showHumanRelayDialog(prompt, this.options) if (!response) { throw new Error("Human relay operation cancelled") @@ -111,12 +110,50 @@ function getMessageContent(message: Anthropic.Messages.MessageParam): string { } return "" } + +// Elevate lastAIResponse variable to module level to maintain state between multiple calls +let lastAIResponse: string | null = null +let thispromptText: string | null = null +// Add normalized cache to avoid repeatedly processing the same content +let normalizedPrompt: string | null = null +let normalizedLastResponse: string | null = null + +/** + * Normalize string by removing excess whitespace + * @param text Input string + * @returns Normalized string + */ +function normalizeText(text: string | null): string { + if (!text) return "" + // Remove all whitespace and convert to lowercase for case-insensitive comparison + return text.replace(/\s+/g, " ").trim() +} + +/** + * Compare two strings, ignoring whitespace + * @param str1 First string + * @param str2 Second string + * @returns Whether equal + */ +function isTextEqual(str1: string | null, str2: string | null): boolean { + if (str1 === str2) return true // Fast path: same reference + if (!str1 || !str2) return false // One is empty + + return normalizeText(str1) === normalizeText(str2) +} + /** * 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 { +async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOptions): Promise { + // Save initial clipboard content for comparison + const initialClipboardContent = await vscode.env.clipboard.readText() + thispromptText = promptText + // Pre-normalize prompt text to avoid repeated processing during polling + normalizedPrompt = normalizeText(promptText) + return new Promise((resolve) => { // Create a unique request ID const requestId = Date.now().toString() @@ -126,6 +163,11 @@ async function showHumanRelayDialog(promptText: string): Promise { + // Clear clipboard monitoring timer + if (clipboardInterval) { + clearInterval(clipboardInterval) + clipboardInterval = null + } resolve(response) }, ) @@ -135,5 +177,69 @@ async function showHumanRelayDialog(promptText: string): Promise { + try { + // Check if clipboard has changed + const currentClipboardContent = await vscode.env.clipboard.readText() + + if (!currentClipboardContent || !currentClipboardContent.trim()) { + return // Skip empty content + } + + // Normalize current clipboard content to avoid repeated processing + const normalizedClipboard = normalizeText(currentClipboardContent) + + // Validate clipboard content and check for duplicate response + if ( + normalizedClipboard !== normalizeText(initialClipboardContent) && + normalizedClipboard !== normalizedPrompt && + normalizedClipboard !== normalizedLastResponse + ) { + // Update last AI response + lastAIResponse = currentClipboardContent + normalizedLastResponse = normalizedClipboard + + // Clear timer + if (clipboardInterval) { + clearInterval(clipboardInterval) + clipboardInterval = null + } + + // Get current panel + const panel = getPanel() + if (panel) { + // Send close dialog message + panel.webview.postMessage({ type: "closeHumanRelayDialog" }) + } + + // Send response automatically + vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", { + requestId, + text: currentClipboardContent, + }) + } + + // New: Check if the last AI response content was copied + // Use improved comparison method + else if ( + normalizedClipboard === normalizedLastResponse && + normalizedClipboard !== normalizedPrompt + ) { + // Get current panel and send warning message + const panel = getPanel() + panel?.webview.postMessage({ type: "showDuplicateResponseAlert" }) + } + } catch (error) { + console.error("Error monitoring clipboard:", error) + } + }, monitorInterval) + } }) } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 10af6f7a946..6c7a7785e90 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -97,6 +97,8 @@ export interface WebviewMessage { | "maxOpenTabsContext" | "humanRelayResponse" | "humanRelayCancel" + | "closeHumanRelayDialog" + | "showDuplicateResponseAlert" | "browserToolEnabled" | "telemetrySetting" | "showRooIgnoredFiles" diff --git a/src/shared/api.ts b/src/shared/api.ts index 98d595cd036..6ff8897898e 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -73,6 +73,9 @@ export interface ApiHandlerOptions { modelTemperature?: number | null modelMaxTokens?: number modelMaxThinkingTokens?: number + // Human relay specific options + humanRelayMonitorClipboard?: boolean // Whether to monitor clipboard for automatic content sending + humanRelayMonitorInterval?: number // Monitoring interval time (milliseconds) } export type ApiConfiguration = ApiHandlerOptions & { @@ -126,6 +129,8 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [ "modelTemperature", "modelMaxTokens", "modelMaxThinkingTokens", + "humanRelayMonitorClipboard", + "humanRelayMonitorInterval", ] // Models diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index bfd24f42984..470c7891d4c 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -98,6 +98,8 @@ export const GLOBAL_STATE_KEYS = [ "lmStudioDraftModelId", "telemetrySetting", "showRooIgnoredFiles", + "humanRelayMonitorClipboard", + "humanRelayMonitorInterval", ] as const // Derive the type from the array - creates a union of string literals diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index b537b9298e3..6a3b21d78c5 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -25,8 +25,15 @@ const tabsByMessageAction: Partial { - const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } = - useExtensionState() + const { + didHydrateState, + showWelcome, + shouldShowAnnouncement, + telemetrySetting, + telemetryKey, + machineId, + apiConfiguration, + } = useExtensionState() const [showAnnouncement, setShowAnnouncement] = useState(false) const [tab, setTab] = useState("chat") const settingsRef = useRef(null) @@ -139,6 +146,8 @@ const App = () => { onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))} onSubmit={handleHumanRelaySubmit} onCancel={handleHumanRelayCancel} + monitorClipboard={apiConfiguration?.humanRelayMonitorClipboard} + monitorInterval={apiConfiguration?.humanRelayMonitorInterval} /> ) diff --git a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx index 61d4cbe2132..58f3f8411e7 100644 --- a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx +++ b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx @@ -3,7 +3,8 @@ 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" +import { AlertTriangle, Check, Copy, X } from "lucide-react" +import { ProgressIndicator } from "../chat/ChatRow" interface HumanRelayDialogProps { isOpen: boolean @@ -12,6 +13,8 @@ interface HumanRelayDialogProps { promptText: string onSubmit: (requestId: string, text: string) => void onCancel: (requestId: string) => void + monitorClipboard?: boolean + monitorInterval?: number } /** @@ -25,10 +28,13 @@ export const HumanRelayDialog: React.FC = ({ promptText, onSubmit, onCancel, + monitorClipboard = false, + monitorInterval = 500, }) => { const [response, setResponse] = React.useState("") const { copy } = useClipboard() const [isCopyClicked, setIsCopyClicked] = React.useState(false) + const [showDuplicateWarning, setShowDuplicateWarning] = React.useState(false) // Listen to isOpen changes, clear the input box when the dialog box is opened React.useEffect(() => { @@ -36,8 +42,30 @@ export const HumanRelayDialog: React.FC = ({ setResponse("") setIsCopyClicked(false) } + setShowDuplicateWarning(false) }, [isOpen]) + React.useEffect(() => { + // Handle messages from extension + const messageHandler = (event: MessageEvent) => { + const message = event.data + if (message.type === "closeHumanRelayDialog") { + onClose() + } + // Handle duplicate response warning + else if (message.type === "showDuplicateResponseAlert") { + // Show warning + setShowDuplicateWarning(true) + } + } + + window.addEventListener("message", messageHandler) + + return () => { + window.removeEventListener("message", messageHandler) + } + }, [onClose]) + // Copy to clipboard and show a success message const handleCopy = () => { copy(promptText) @@ -85,6 +113,24 @@ export const HumanRelayDialog: React.FC = ({ {isCopyClicked &&
Copied to clipboard
} + {monitorClipboard && ( + <> + {showDuplicateWarning && ( +
+ + + It seems you copied the AI's response from the last interaction instead of the + current task. Please check your interaction with the web AI. + +
+ )} + +
+ + Monitoring clipboard for changes, interval: {monitorInterval}ms +
+ + )}
Please enter the AI's response:
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index c5a02dc1173..032565cedd8 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1194,6 +1194,40 @@ const ApiOptions = ({ automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude), then copy the AI's reply back to the dialog box and click the confirm button.
+ {/* Auto clipboard monitoring */} +
+ + Enable clipboard monitoring + +
+ Automatically detect when you copy the AI's response from the browser +
+
+ {apiConfiguration?.humanRelayMonitorClipboard && ( +
+ + { + const target = e.target as HTMLInputElement + return parseInt(target.value) + })} + className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background" + /> + + {apiConfiguration?.humanRelayMonitorInterval || 500} ms + +
+ How frequently to check for clipboard changes (100-2000ms) +
+
+ )} )} From d1b34c7f75103172659d5eb6b2845a5d9639fff6 Mon Sep 17 00:00:00 2001 From: Felix NyxJae <18661811993@163.com> Date: Sat, 8 Mar 2025 19:53:13 +0800 Subject: [PATCH 02/19] Fixed: Remove unnecessary variables --- src/api/providers/human-relay.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts index 684e209aee1..e92b1982811 100644 --- a/src/api/providers/human-relay.ts +++ b/src/api/providers/human-relay.ts @@ -113,7 +113,6 @@ function getMessageContent(message: Anthropic.Messages.MessageParam): string { // Elevate lastAIResponse variable to module level to maintain state between multiple calls let lastAIResponse: string | null = null -let thispromptText: string | null = null // Add normalized cache to avoid repeatedly processing the same content let normalizedPrompt: string | null = null let normalizedLastResponse: string | null = null @@ -150,7 +149,6 @@ function isTextEqual(str1: string | null, str2: string | null): boolean { async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOptions): Promise { // Save initial clipboard content for comparison const initialClipboardContent = await vscode.env.clipboard.readText() - thispromptText = promptText // Pre-normalize prompt text to avoid repeated processing during polling normalizedPrompt = normalizeText(promptText) From a9b50ad123ffc73d963fa2f7ad1e284a31a4a1ca Mon Sep 17 00:00:00 2001 From: Felix NyxJae <18661811993@163.com> Date: Tue, 11 Mar 2025 15:52:17 +0800 Subject: [PATCH 03/19] feat: Optimize Human-relay UI prompts and add format validation for copied content --- src/api/providers/human-relay.ts | 87 ++++++++++--------- src/shared/WebviewMessage.ts | 2 +- .../human-relay/HumanRelayDialog.tsx | 10 +-- 3 files changed, 50 insertions(+), 49 deletions(-) diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts index e92b1982811..4e3fdbc120c 100644 --- a/src/api/providers/human-relay.ts +++ b/src/api/providers/human-relay.ts @@ -8,7 +8,7 @@ import { getPanel } from "../../activate/registerCommands" // Import the getPane /** * Human Relay API processor - * This processor does not directly call the API, but interacts with the model through human operations copy and paste. + * This processor does not directly call the API, but interacts with the model through human operations like copy and paste. */ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions @@ -16,13 +16,14 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { constructor(options: ApiHandlerOptions) { this.options = options } + countTokens(content: Array): Promise { return Promise.resolve(0) } /** * Create a message processing flow, display a dialog box to request human assistance - * @param systemPrompt System prompt words + * @param systemPrompt System prompt text * @param messages Message list */ async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { @@ -33,7 +34,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { throw new Error("No message to relay") } - // If it is the first message, splice the system prompt word with the user message + // If it is the first message, concatenate the system prompt with the user message let promptText = "" if (messages.length === 1) { promptText = `${systemPrompt}\n\n${getMessageContent(latestMessage)}` @@ -44,7 +45,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { // Copy to clipboard await vscode.env.clipboard.writeText(promptText) - // A dialog box pops up to request user action + // Display a dialog box to request user action const response = await showHumanRelayDialog(promptText, this.options) if (!response) { @@ -52,7 +53,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { throw new Error("Human relay operation cancelled") } - // Return to the user input reply + // Return the user-provided response yield { type: "text", text: response } } @@ -60,7 +61,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { * Get model information */ getModel(): { id: string; info: ModelInfo } { - // Human relay does not depend on a specific model, here is a default configuration + // Human relay does not depend on a specific model; here is a default configuration return { id: "human-relay", info: { @@ -77,14 +78,14 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { } /** - * Implementation of a single prompt + * Implementation of a single prompt completion * @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 + // Display a dialog box to request user action const response = await showHumanRelayDialog(prompt, this.options) if (!response) { @@ -96,7 +97,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { } /** - * Extract text content from message object + * Extract text content from a message object * @param message */ function getMessageContent(message: Anthropic.Messages.MessageParam): string { @@ -124,7 +125,7 @@ let normalizedLastResponse: string | null = null */ function normalizeText(text: string | null): string { if (!text) return "" - // Remove all whitespace and convert to lowercase for case-insensitive comparison + // Remove all excess whitespace and convert to lowercase for case-insensitive comparison return text.replace(/\s+/g, " ").trim() } @@ -132,7 +133,7 @@ function normalizeText(text: string | null): string { * Compare two strings, ignoring whitespace * @param str1 First string * @param str2 Second string - * @returns Whether equal + * @returns Whether they are equal */ function isTextEqual(str1: string | null, str2: string | null): boolean { if (str1 === str2) return true // Fast path: same reference @@ -147,8 +148,6 @@ function isTextEqual(str1: string | null, str2: string | null): boolean { * @returns The user's input response or undefined (if canceled). */ async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOptions): Promise { - // Save initial clipboard content for comparison - const initialClipboardContent = await vscode.env.clipboard.readText() // Pre-normalize prompt text to avoid repeated processing during polling normalizedPrompt = normalizeText(promptText) @@ -184,23 +183,33 @@ async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOpti clipboardInterval = setInterval(async () => { try { - // Check if clipboard has changed const currentClipboardContent = await vscode.env.clipboard.readText() if (!currentClipboardContent || !currentClipboardContent.trim()) { - return // Skip empty content + return } - // Normalize current clipboard content to avoid repeated processing const normalizedClipboard = normalizeText(currentClipboardContent) + const panel = getPanel() - // Validate clipboard content and check for duplicate response - if ( - normalizedClipboard !== normalizeText(initialClipboardContent) && - normalizedClipboard !== normalizedPrompt && - normalizedClipboard !== normalizedLastResponse - ) { - // Update last AI response + // Check if it’s a duplicate response + if (normalizedClipboard === normalizedLastResponse) { + panel?.webview.postMessage({ + type: "showHumanRelayResponseAlert", + text: "It seems you copied the AI's response from the last interaction instead of the current task. Please check your interaction with the web AI", + }) + return + } + if (!containsValidTags(currentClipboardContent)) { + panel?.webview.postMessage({ + type: "showHumanRelayResponseAlert", + text: "The AI's response does not seem to meet the RooCode format requirements. Please check your interaction with the web AI.", + }) + return + } + + // Process new valid response + if (normalizedClipboard !== normalizedPrompt) { lastAIResponse = currentClipboardContent normalizedLastResponse = normalizedClipboard @@ -210,30 +219,13 @@ async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOpti clipboardInterval = null } - // Get current panel - const panel = getPanel() - if (panel) { - // Send close dialog message - panel.webview.postMessage({ type: "closeHumanRelayDialog" }) - } - - // Send response automatically + // Close dialog and send response + panel?.webview.postMessage({ type: "closeHumanRelayDialog" }) vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", { requestId, text: currentClipboardContent, }) } - - // New: Check if the last AI response content was copied - // Use improved comparison method - else if ( - normalizedClipboard === normalizedLastResponse && - normalizedClipboard !== normalizedPrompt - ) { - // Get current panel and send warning message - const panel = getPanel() - panel?.webview.postMessage({ type: "showDuplicateResponseAlert" }) - } } catch (error) { console.error("Error monitoring clipboard:", error) } @@ -241,3 +233,14 @@ async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOpti } }) } + +/** + * Validate if the content contains any tag in format + * @param content The content to validate + * @returns Whether the content contains a valid tag format + */ +function containsValidTags(content: string): boolean { + // Use a regular expression to match tags in format + const tagPattern = /<[^>]+>/ + return tagPattern.test(content) +} diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index cd20ae5b0ec..2aa434d9f35 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -101,7 +101,7 @@ export interface WebviewMessage { | "humanRelayResponse" | "humanRelayCancel" | "closeHumanRelayDialog" - | "showDuplicateResponseAlert" + | "showHumanRelayResponseAlert" | "browserToolEnabled" | "telemetrySetting" | "showRooIgnoredFiles" diff --git a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx index 58f3f8411e7..32d1c53539d 100644 --- a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx +++ b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx @@ -35,6 +35,7 @@ export const HumanRelayDialog: React.FC = ({ const { copy } = useClipboard() const [isCopyClicked, setIsCopyClicked] = React.useState(false) const [showDuplicateWarning, setShowDuplicateWarning] = React.useState(false) + const [warningMessage, setWarningMessage] = React.useState("") // Listen to isOpen changes, clear the input box when the dialog box is opened React.useEffect(() => { @@ -53,8 +54,8 @@ export const HumanRelayDialog: React.FC = ({ onClose() } // Handle duplicate response warning - else if (message.type === "showDuplicateResponseAlert") { - // Show warning + else if (message.type === "showHumanRelayResponseAlert") { + setWarningMessage(message.text) setShowDuplicateWarning(true) } } @@ -118,10 +119,7 @@ export const HumanRelayDialog: React.FC = ({ {showDuplicateWarning && (
- - It seems you copied the AI's response from the last interaction instead of the - current task. Please check your interaction with the web AI. - + {warningMessage}
)} From d7256fc4886798b88f1346da4a2d4327f8250548 Mon Sep 17 00:00:00 2001 From: Felix NyxJae <18661811993@163.com> Date: Tue, 11 Mar 2025 17:28:41 +0800 Subject: [PATCH 04/19] feat: Add toggle options for clipboard monitoring of human relay providers, allowing for more freedom to start and stop when used --- src/api/providers/human-relay.ts | 210 ++++++++++-------- src/shared/WebviewMessage.ts | 1 + .../human-relay/HumanRelayDialog.tsx | 69 ++++-- .../src/components/settings/ApiOptions.tsx | 18 +- 4 files changed, 167 insertions(+), 131 deletions(-) diff --git a/src/api/providers/human-relay.ts b/src/api/providers/human-relay.ts index 4e3fdbc120c..7b28c0c4bbf 100644 --- a/src/api/providers/human-relay.ts +++ b/src/api/providers/human-relay.ts @@ -4,7 +4,7 @@ import { ApiHandler, SingleCompletionHandler } from "../index" import { ApiStream } from "../transform/stream" import * as vscode from "vscode" import { ExtensionMessage } from "../../shared/ExtensionMessage" -import { getPanel } from "../../activate/registerCommands" // Import the getPanel function +import { getPanel } from "../../activate/registerCommands" /** * Human Relay API processor @@ -23,18 +23,15 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { /** * Create a message processing flow, display a dialog box to request human assistance - * @param systemPrompt System prompt text - * @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, concatenate the system prompt with the user message + // Concatenate system prompt with user message if this is the first message let promptText = "" if (messages.length === 1) { promptText = `${systemPrompt}\n\n${getMessageContent(latestMessage)}` @@ -49,11 +46,9 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { const response = await showHumanRelayDialog(promptText, this.options) if (!response) { - // The user canceled the operation throw new Error("Human relay operation cancelled") } - // Return the user-provided response yield { type: "text", text: response } } @@ -61,7 +56,6 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { * 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: { @@ -79,13 +73,10 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { /** * Implementation of a single prompt completion - * @param prompt Prompt content */ async completePrompt(prompt: string): Promise { - // Copy to clipboard await vscode.env.clipboard.writeText(prompt) - // Display a dialog box to request user action const response = await showHumanRelayDialog(prompt, this.options) if (!response) { @@ -98,7 +89,6 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler { /** * Extract text content from a message object - * @param message */ function getMessageContent(message: Anthropic.Messages.MessageParam): string { if (typeof message.content === "string") { @@ -112,135 +102,165 @@ function getMessageContent(message: Anthropic.Messages.MessageParam): string { return "" } -// Elevate lastAIResponse variable to module level to maintain state between multiple calls +// Global variables let lastAIResponse: string | null = null -// Add normalized cache to avoid repeatedly processing the same content let normalizedPrompt: string | null = null let normalizedLastResponse: string | null = null +let globalClipboardInterval: NodeJS.Timeout | null = null +let lastMonitorOptions: ApiHandlerOptions | undefined /** - * Normalize string by removing excess whitespace - * @param text Input string - * @returns Normalized string + * Normalize text by removing extra spaces */ function normalizeText(text: string | null): string { if (!text) return "" - // Remove all excess whitespace and convert to lowercase for case-insensitive comparison return text.replace(/\s+/g, " ").trim() } /** - * Compare two strings, ignoring whitespace - * @param str1 First string - * @param str2 Second string - * @returns Whether they are equal + * Compare if two strings are equal (ignoring whitespace differences) */ function isTextEqual(str1: string | null, str2: string | null): boolean { - if (str1 === str2) return true // Fast path: same reference - if (!str1 || !str2) return false // One is empty - + if (str1 === str2) return true + if (!str1 || !str2) return false return normalizeText(str1) === normalizeText(str2) } /** - * 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). + * Stop clipboard monitoring + */ +function stopClipboardMonitoring() { + if (globalClipboardInterval) { + clearInterval(globalClipboardInterval) + globalClipboardInterval = null + } +} + +// Store clipboard content when monitoring starts +let clipboardContentAtMonitorStart: string | null = null + +/** + * Start clipboard monitoring + */ +async function startClipboardMonitoring(requestId: string, options?: ApiHandlerOptions) { + // Stop any existing monitoring + stopClipboardMonitoring() + + // Start new monitoring + if (options?.humanRelayMonitorClipboard) { + const monitorInterval = Math.min(Math.max(100, options?.humanRelayMonitorInterval ?? 500), 2000) + + // Read clipboard content at startup + clipboardContentAtMonitorStart = await vscode.env.clipboard.readText() + + globalClipboardInterval = setInterval(async () => { + try { + const currentClipboardContent = await vscode.env.clipboard.readText() + + if (!currentClipboardContent || !currentClipboardContent.trim()) { + return + } + + const normalizedClipboard = normalizeText(currentClipboardContent) + const normalizedInitialClipboard = normalizeText(clipboardContentAtMonitorStart) + + // Skip if current clipboard content matches content at start + if (normalizedClipboard === normalizedInitialClipboard) { + return + } + + const panel = getPanel() + + // Check if response is duplicate + if (normalizedClipboard === normalizedLastResponse) { + panel?.webview.postMessage({ + type: "showHumanRelayResponseAlert", + text: "It seems you copied the AI's response from the last interaction instead of the current task. Please check your interaction with the web AI", + }) + return + } + if (!containsValidTags(currentClipboardContent)) { + panel?.webview.postMessage({ + type: "showHumanRelayResponseAlert", + text: "The AI's response does not seem to meet the RooCode format requirements. Please check your interaction with the web AI.", + }) + return + } + + // Process valid new response + if (normalizedClipboard !== normalizedPrompt) { + lastAIResponse = currentClipboardContent + normalizedLastResponse = normalizedClipboard + + // Clear timer + stopClipboardMonitoring() + + // Close dialog and send response + panel?.webview.postMessage({ type: "closeHumanRelayDialog" }) + vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", { + requestId, + text: currentClipboardContent, + }) + } + } catch (error) { + console.error("Error monitoring clipboard:", error) + } + }, monitorInterval) + } +} + +/** + * Display human relay dialog and wait for user response */ async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOptions): Promise { - // Pre-normalize prompt text to avoid repeated processing during polling normalizedPrompt = normalizeText(promptText) + lastMonitorOptions = options return new Promise((resolve) => { - // Create a unique request ID + // Create unique request ID const requestId = Date.now().toString() - // Register a global callback function + // Register global callback function vscode.commands.executeCommand( "roo-cline.registerHumanRelayCallback", requestId, (response: string | undefined) => { - // Clear clipboard monitoring timer - if (clipboardInterval) { - clearInterval(clipboardInterval) - clipboardInterval = null - } + stopClipboardMonitoring() resolve(response) }, ) - // Open the dialog box directly using the current panel + // Get panel and register message handler + const panel = getPanel() + if (panel) { + panel.webview.onDidReceiveMessage((message) => { + if (message.type === "toggleHumanRelayMonitor" && message.requestId === requestId) { + if (message.bool) { + startClipboardMonitoring(requestId, lastMonitorOptions) + } else { + stopClipboardMonitoring() + } + } + }) + } + + // Open dialog vscode.commands.executeCommand("roo-cline.showHumanRelayDialog", { requestId, promptText, }) - // If clipboard monitoring is enabled, start polling for clipboard changes - let clipboardInterval: NodeJS.Timeout | null = null - + // Start polling clipboard changes if enabled if (options?.humanRelayMonitorClipboard) { - const monitorInterval = Math.min(Math.max(100, options?.humanRelayMonitorInterval ?? 500), 2000) - - clipboardInterval = setInterval(async () => { - try { - const currentClipboardContent = await vscode.env.clipboard.readText() - - if (!currentClipboardContent || !currentClipboardContent.trim()) { - return - } - - const normalizedClipboard = normalizeText(currentClipboardContent) - const panel = getPanel() - - // Check if it’s a duplicate response - if (normalizedClipboard === normalizedLastResponse) { - panel?.webview.postMessage({ - type: "showHumanRelayResponseAlert", - text: "It seems you copied the AI's response from the last interaction instead of the current task. Please check your interaction with the web AI", - }) - return - } - if (!containsValidTags(currentClipboardContent)) { - panel?.webview.postMessage({ - type: "showHumanRelayResponseAlert", - text: "The AI's response does not seem to meet the RooCode format requirements. Please check your interaction with the web AI.", - }) - return - } - - // Process new valid response - if (normalizedClipboard !== normalizedPrompt) { - lastAIResponse = currentClipboardContent - normalizedLastResponse = normalizedClipboard - - // Clear timer - if (clipboardInterval) { - clearInterval(clipboardInterval) - clipboardInterval = null - } - - // Close dialog and send response - panel?.webview.postMessage({ type: "closeHumanRelayDialog" }) - vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", { - requestId, - text: currentClipboardContent, - }) - } - } catch (error) { - console.error("Error monitoring clipboard:", error) - } - }, monitorInterval) + startClipboardMonitoring(requestId, options) } }) } /** - * Validate if the content contains any tag in format - * @param content The content to validate - * @returns Whether the content contains a valid tag format + * Validate if content contains any tags in format */ function containsValidTags(content: string): boolean { - // Use a regular expression to match tags in format const tagPattern = /<[^>]+>/ return tagPattern.test(content) } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 2aa434d9f35..6266a863946 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -15,6 +15,7 @@ export interface WebviewMessage { | "saveApiConfiguration" | "upsertApiConfiguration" | "deleteApiConfiguration" + | "toggleHumanRelayMonitor" | "loadApiConfiguration" | "renameApiConfiguration" | "getListApiConfiguration" diff --git a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx index 32d1c53539d..748a5711027 100644 --- a/webview-ui/src/components/human-relay/HumanRelayDialog.tsx +++ b/webview-ui/src/components/human-relay/HumanRelayDialog.tsx @@ -3,8 +3,10 @@ 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 { AlertTriangle, Check, Copy, X } from "lucide-react" +import { AlertTriangle, Check, Copy, Power, X } from "lucide-react" +import { useState as useReactState } from "react" import { ProgressIndicator } from "../chat/ChatRow" +import { vscode } from "../../utils/vscode" interface HumanRelayDialogProps { isOpen: boolean @@ -15,6 +17,7 @@ interface HumanRelayDialogProps { onCancel: (requestId: string) => void monitorClipboard?: boolean monitorInterval?: number + onToggleMonitor?: (enabled: boolean) => void } /** @@ -30,21 +33,41 @@ export const HumanRelayDialog: React.FC = ({ onCancel, monitorClipboard = false, monitorInterval = 500, + onToggleMonitor, }) => { const [response, setResponse] = React.useState("") const { copy } = useClipboard() const [isCopyClicked, setIsCopyClicked] = React.useState(false) const [showDuplicateWarning, setShowDuplicateWarning] = React.useState(false) const [warningMessage, setWarningMessage] = React.useState("") + const [isMonitoring, setIsMonitoring] = useReactState(monitorClipboard) - // Listen to isOpen changes, clear the input box when the dialog box is opened + // Clear input when dialog opens React.useEffect(() => { if (isOpen) { setResponse("") setIsCopyClicked(false) + setIsMonitoring(monitorClipboard) } setShowDuplicateWarning(false) - }, [isOpen]) + }, [isOpen, monitorClipboard, setIsMonitoring]) + + // Handle monitor toggle + const handleToggleMonitor = () => { + const newState = !isMonitoring + setIsMonitoring(newState) + + // Send message to backend to control clipboard monitoring + vscode.postMessage({ + type: "toggleHumanRelayMonitor", + bool: newState, + requestId: requestId, + }) + + if (onToggleMonitor) { + onToggleMonitor(newState) + } + } React.useEffect(() => { // Handle messages from extension @@ -67,7 +90,7 @@ export const HumanRelayDialog: React.FC = ({ } }, [onClose]) - // Copy to clipboard and show a success message + // Copy to clipboard and show success message const handleCopy = () => { copy(promptText) setIsCopyClicked(true) @@ -76,7 +99,7 @@ export const HumanRelayDialog: React.FC = ({ }, 2000) } - // Submit the response + // Submit response const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (response.trim()) { @@ -85,7 +108,7 @@ export const HumanRelayDialog: React.FC = ({ } } - // Cancel the operation + // Cancel operation const handleCancel = () => { onCancel(requestId) onClose() @@ -114,22 +137,28 @@ export const HumanRelayDialog: React.FC = ({ {isCopyClicked &&
Copied to clipboard
} - {monitorClipboard && ( - <> - {showDuplicateWarning && ( -
- - {warningMessage} -
- )} - -
- - Monitoring clipboard for changes, interval: {monitorInterval}ms -
- + {showDuplicateWarning && ( +
+ + {warningMessage} +
)} +
+
+ {isMonitoring && } + Clipboard Monitoring: {isMonitoring ? "Enabled" : "Disabled"} +
+ +
+
Please enter the AI's response: