Skip to content

Commit 9697f6f

Browse files
committed
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
1 parent 9fcf69c commit 9697f6f

File tree

7 files changed

+211
-7
lines changed

7 files changed

+211
-7
lines changed

src/api/providers/human-relay.ts

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// filepath: e:\Project\Roo-Code\src\api\providers\human-relay.ts
21
import { Anthropic } from "@anthropic-ai/sdk"
32
import { ApiHandlerOptions, ModelInfo } from "../../shared/api"
43
import { ApiHandler, SingleCompletionHandler } from "../index"
@@ -46,7 +45,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler {
4645
await vscode.env.clipboard.writeText(promptText)
4746

4847
// A dialog box pops up to request user action
49-
const response = await showHumanRelayDialog(promptText)
48+
const response = await showHumanRelayDialog(promptText, this.options)
5049

5150
if (!response) {
5251
// The user canceled the operation
@@ -86,7 +85,7 @@ export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler {
8685
await vscode.env.clipboard.writeText(prompt)
8786

8887
// A dialog box pops up to request user action
89-
const response = await showHumanRelayDialog(prompt)
88+
const response = await showHumanRelayDialog(prompt, this.options)
9089

9190
if (!response) {
9291
throw new Error("Human relay operation cancelled")
@@ -111,12 +110,50 @@ function getMessageContent(message: Anthropic.Messages.MessageParam): string {
111110
}
112111
return ""
113112
}
113+
114+
// Elevate lastAIResponse variable to module level to maintain state between multiple calls
115+
let lastAIResponse: string | null = null
116+
let thispromptText: string | null = null
117+
// Add normalized cache to avoid repeatedly processing the same content
118+
let normalizedPrompt: string | null = null
119+
let normalizedLastResponse: string | null = null
120+
121+
/**
122+
* Normalize string by removing excess whitespace
123+
* @param text Input string
124+
* @returns Normalized string
125+
*/
126+
function normalizeText(text: string | null): string {
127+
if (!text) return ""
128+
// Remove all whitespace and convert to lowercase for case-insensitive comparison
129+
return text.replace(/\s+/g, " ").trim()
130+
}
131+
132+
/**
133+
* Compare two strings, ignoring whitespace
134+
* @param str1 First string
135+
* @param str2 Second string
136+
* @returns Whether equal
137+
*/
138+
function isTextEqual(str1: string | null, str2: string | null): boolean {
139+
if (str1 === str2) return true // Fast path: same reference
140+
if (!str1 || !str2) return false // One is empty
141+
142+
return normalizeText(str1) === normalizeText(str2)
143+
}
144+
114145
/**
115146
* Displays the human relay dialog and waits for user response.
116147
* @param promptText The prompt text that needs to be copied.
117148
* @returns The user's input response or undefined (if canceled).
118149
*/
119-
async function showHumanRelayDialog(promptText: string): Promise<string | undefined> {
150+
async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOptions): Promise<string | undefined> {
151+
// Save initial clipboard content for comparison
152+
const initialClipboardContent = await vscode.env.clipboard.readText()
153+
thispromptText = promptText
154+
// Pre-normalize prompt text to avoid repeated processing during polling
155+
normalizedPrompt = normalizeText(promptText)
156+
120157
return new Promise<string | undefined>((resolve) => {
121158
// Create a unique request ID
122159
const requestId = Date.now().toString()
@@ -126,6 +163,11 @@ async function showHumanRelayDialog(promptText: string): Promise<string | undefi
126163
"roo-cline.registerHumanRelayCallback",
127164
requestId,
128165
(response: string | undefined) => {
166+
// Clear clipboard monitoring timer
167+
if (clipboardInterval) {
168+
clearInterval(clipboardInterval)
169+
clipboardInterval = null
170+
}
129171
resolve(response)
130172
},
131173
)
@@ -135,5 +177,69 @@ async function showHumanRelayDialog(promptText: string): Promise<string | undefi
135177
requestId,
136178
promptText,
137179
})
180+
181+
// If clipboard monitoring is enabled, start polling for clipboard changes
182+
let clipboardInterval: NodeJS.Timeout | null = null
183+
184+
if (options?.humanRelayMonitorClipboard) {
185+
const monitorInterval = Math.min(Math.max(100, options?.humanRelayMonitorInterval ?? 500), 2000)
186+
187+
clipboardInterval = setInterval(async () => {
188+
try {
189+
// Check if clipboard has changed
190+
const currentClipboardContent = await vscode.env.clipboard.readText()
191+
192+
if (!currentClipboardContent || !currentClipboardContent.trim()) {
193+
return // Skip empty content
194+
}
195+
196+
// Normalize current clipboard content to avoid repeated processing
197+
const normalizedClipboard = normalizeText(currentClipboardContent)
198+
199+
// Validate clipboard content and check for duplicate response
200+
if (
201+
normalizedClipboard !== normalizeText(initialClipboardContent) &&
202+
normalizedClipboard !== normalizedPrompt &&
203+
normalizedClipboard !== normalizedLastResponse
204+
) {
205+
// Update last AI response
206+
lastAIResponse = currentClipboardContent
207+
normalizedLastResponse = normalizedClipboard
208+
209+
// Clear timer
210+
if (clipboardInterval) {
211+
clearInterval(clipboardInterval)
212+
clipboardInterval = null
213+
}
214+
215+
// Get current panel
216+
const panel = getPanel()
217+
if (panel) {
218+
// Send close dialog message
219+
panel.webview.postMessage({ type: "closeHumanRelayDialog" })
220+
}
221+
222+
// Send response automatically
223+
vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", {
224+
requestId,
225+
text: currentClipboardContent,
226+
})
227+
}
228+
229+
// New: Check if the last AI response content was copied
230+
// Use improved comparison method
231+
else if (
232+
normalizedClipboard === normalizedLastResponse &&
233+
normalizedClipboard !== normalizedPrompt
234+
) {
235+
// Get current panel and send warning message
236+
const panel = getPanel()
237+
panel?.webview.postMessage({ type: "showDuplicateResponseAlert" })
238+
}
239+
} catch (error) {
240+
console.error("Error monitoring clipboard:", error)
241+
}
242+
}, monitorInterval)
243+
}
138244
})
139245
}

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export interface WebviewMessage {
9797
| "maxOpenTabsContext"
9898
| "humanRelayResponse"
9999
| "humanRelayCancel"
100+
| "closeHumanRelayDialog"
101+
| "showDuplicateResponseAlert"
100102
| "browserToolEnabled"
101103
| "telemetrySetting"
102104
| "showRooIgnoredFiles"

src/shared/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export interface ApiHandlerOptions {
7373
modelTemperature?: number | null
7474
modelMaxTokens?: number
7575
modelMaxThinkingTokens?: number
76+
// Human relay specific options
77+
humanRelayMonitorClipboard?: boolean // Whether to monitor clipboard for automatic content sending
78+
humanRelayMonitorInterval?: number // Monitoring interval time (milliseconds)
7679
}
7780

7881
export type ApiConfiguration = ApiHandlerOptions & {
@@ -126,6 +129,8 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
126129
"modelTemperature",
127130
"modelMaxTokens",
128131
"modelMaxThinkingTokens",
132+
"humanRelayMonitorClipboard",
133+
"humanRelayMonitorInterval",
129134
]
130135

131136
// Models

src/shared/globalState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export const GLOBAL_STATE_KEYS = [
9898
"lmStudioDraftModelId",
9999
"telemetrySetting",
100100
"showRooIgnoredFiles",
101+
"humanRelayMonitorClipboard",
102+
"humanRelayMonitorInterval",
101103
] as const
102104

103105
// Derive the type from the array - creates a union of string literals

webview-ui/src/App.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,15 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
2525
historyButtonClicked: "history",
2626
}
2727
const App = () => {
28-
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
29-
useExtensionState()
28+
const {
29+
didHydrateState,
30+
showWelcome,
31+
shouldShowAnnouncement,
32+
telemetrySetting,
33+
telemetryKey,
34+
machineId,
35+
apiConfiguration,
36+
} = useExtensionState()
3037
const [showAnnouncement, setShowAnnouncement] = useState(false)
3138
const [tab, setTab] = useState<Tab>("chat")
3239
const settingsRef = useRef<SettingsViewRef>(null)
@@ -139,6 +146,8 @@ const App = () => {
139146
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
140147
onSubmit={handleHumanRelaySubmit}
141148
onCancel={handleHumanRelayCancel}
149+
monitorClipboard={apiConfiguration?.humanRelayMonitorClipboard}
150+
monitorInterval={apiConfiguration?.humanRelayMonitorInterval}
142151
/>
143152
</>
144153
)

webview-ui/src/components/human-relay/HumanRelayDialog.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { Button } from "../ui/button"
33
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"
44
import { Textarea } from "../ui/textarea"
55
import { useClipboard } from "../ui/hooks"
6-
import { Check, Copy, X } from "lucide-react"
6+
import { AlertTriangle, Check, Copy, X } from "lucide-react"
7+
import { ProgressIndicator } from "../chat/ChatRow"
78

89
interface HumanRelayDialogProps {
910
isOpen: boolean
@@ -12,6 +13,8 @@ interface HumanRelayDialogProps {
1213
promptText: string
1314
onSubmit: (requestId: string, text: string) => void
1415
onCancel: (requestId: string) => void
16+
monitorClipboard?: boolean
17+
monitorInterval?: number
1518
}
1619

1720
/**
@@ -25,19 +28,44 @@ export const HumanRelayDialog: React.FC<HumanRelayDialogProps> = ({
2528
promptText,
2629
onSubmit,
2730
onCancel,
31+
monitorClipboard = false,
32+
monitorInterval = 500,
2833
}) => {
2934
const [response, setResponse] = React.useState("")
3035
const { copy } = useClipboard()
3136
const [isCopyClicked, setIsCopyClicked] = React.useState(false)
37+
const [showDuplicateWarning, setShowDuplicateWarning] = React.useState(false)
3238

3339
// Listen to isOpen changes, clear the input box when the dialog box is opened
3440
React.useEffect(() => {
3541
if (isOpen) {
3642
setResponse("")
3743
setIsCopyClicked(false)
3844
}
45+
setShowDuplicateWarning(false)
3946
}, [isOpen])
4047

48+
React.useEffect(() => {
49+
// Handle messages from extension
50+
const messageHandler = (event: MessageEvent) => {
51+
const message = event.data
52+
if (message.type === "closeHumanRelayDialog") {
53+
onClose()
54+
}
55+
// Handle duplicate response warning
56+
else if (message.type === "showDuplicateResponseAlert") {
57+
// Show warning
58+
setShowDuplicateWarning(true)
59+
}
60+
}
61+
62+
window.addEventListener("message", messageHandler)
63+
64+
return () => {
65+
window.removeEventListener("message", messageHandler)
66+
}
67+
}, [onClose])
68+
4169
// Copy to clipboard and show a success message
4270
const handleCopy = () => {
4371
copy(promptText)
@@ -85,6 +113,24 @@ export const HumanRelayDialog: React.FC<HumanRelayDialogProps> = ({
85113
</div>
86114

87115
{isCopyClicked && <div className="text-sm text-emerald-500 font-medium">Copied to clipboard</div>}
116+
{monitorClipboard && (
117+
<>
118+
{showDuplicateWarning && (
119+
<div className="flex items-center gap-2 text-sm p-2 rounded-md bg-amber-100 dark:bg-amber-900 border border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-300">
120+
<AlertTriangle className="h-4 w-4 text-amber-500" />
121+
<span className="font-medium">
122+
It seems you copied the AI's response from the last interaction instead of the
123+
current task. Please check your interaction with the web AI.
124+
</span>
125+
</div>
126+
)}
127+
128+
<div className="flex items-center gap-2 text-sm text-vscode-descriptionForeground">
129+
<ProgressIndicator />
130+
<span>Monitoring clipboard for changes, interval: {monitorInterval}ms</span>
131+
</div>
132+
</>
133+
)}
88134

89135
<div>
90136
<div className="mb-2 font-medium">Please enter the AI's response:</div>

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,40 @@ const ApiOptions = ({
11941194
automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude), then
11951195
copy the AI's reply back to the dialog box and click the confirm button.
11961196
</div>
1197+
{/* Auto clipboard monitoring */}
1198+
<div className="mt-4">
1199+
<Checkbox
1200+
checked={apiConfiguration?.humanRelayMonitorClipboard ?? false}
1201+
onChange={handleInputChange("humanRelayMonitorClipboard", noTransform)}>
1202+
<span className="font-medium">Enable clipboard monitoring</span>
1203+
</Checkbox>
1204+
<div className="text-sm text-vscode-descriptionForeground ml-6">
1205+
Automatically detect when you copy the AI's response from the browser
1206+
</div>
1207+
</div>
1208+
{apiConfiguration?.humanRelayMonitorClipboard && (
1209+
<div className="mt-2">
1210+
<label className="font-medium">Monitor interval (ms)</label>
1211+
<input
1212+
type="range"
1213+
min="100"
1214+
max="2000"
1215+
step="100"
1216+
value={apiConfiguration?.humanRelayMonitorInterval || 500}
1217+
onChange={handleInputChange("humanRelayMonitorInterval", (e) => {
1218+
const target = e.target as HTMLInputElement
1219+
return parseInt(target.value)
1220+
})}
1221+
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
1222+
/>
1223+
<span style={{ minWidth: "45px", textAlign: "left" }}>
1224+
{apiConfiguration?.humanRelayMonitorInterval || 500} ms
1225+
</span>
1226+
<div className="text-sm text-vscode-descriptionForeground">
1227+
How frequently to check for clipboard changes (100-2000ms)
1228+
</div>
1229+
</div>
1230+
)}
11971231
</>
11981232
)}
11991233

0 commit comments

Comments
 (0)