Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9697f6f
feat(human-relay): add clipboard monitoring for AI responses
NyxJae Mar 8, 2025
d1b34c7
Fixed: Remove unnecessary variables
NyxJae Mar 8, 2025
b1553bb
Merge branch 'RooVetGit:main' into human-relay
NyxJae Mar 10, 2025
717477a
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 11, 2025
a9b50ad
feat: Optimize Human-relay UI prompts and add format validation for c…
NyxJae Mar 11, 2025
d7256fc
feat: Add toggle options for clipboard monitoring of human relay prov…
NyxJae Mar 11, 2025
75d1477
feat: Optimize clipboard monitoring logic
NyxJae Mar 11, 2025
004f8e3
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 12, 2025
c90d4d0
Merge branch 'RooVetGit:main' into human-relay
NyxJae Mar 13, 2025
1d1cf54
feat: Add human relay monitoring related configuration keys
NyxJae Mar 13, 2025
2ed74f9
fix: Add VSCode progress ring component to HumanRelayDialog instead o…
NyxJae Mar 13, 2025
fb1550c
Merge branch 'RooVetGit:main' into human-relay
NyxJae Mar 14, 2025
161480b
feat: Add commands and shortcut keys to send clipboard content to hum…
NyxJae Mar 14, 2025
6dbaf15
Doc:Fix spelling errors in comments
NyxJae Mar 14, 2025
dd5e65d
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 15, 2025
17a97a7
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 17, 2025
a1a2380
fix: Update human-computer interaction warning information to use req…
NyxJae Mar 17, 2025
c1d68a2
feat(i8n): Add multilingual support for manual relay dialog text
NyxJae Mar 17, 2025
9daca17
fix(i18n): Correct the expression of "web page AI" in Chinese prompt …
NyxJae Mar 17, 2025
0987f63
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 18, 2025
fee34c4
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 18, 2025
23c2154
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 18, 2025
ab7847f
Add Vietnamese (vi) translation for humanRelay.json
NyxJae Mar 18, 2025
4c3100f
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 19, 2025
d8f612d
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 20, 2025
7eb42d0
fix:Added human relay monitoring clipboard and monitoring interval co…
NyxJae Mar 20, 2025
bb7ac3e
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 23, 2025
5f51d80
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Mar 31, 2025
385ebf7
fix:Supplement Human Relay related configurations
NyxJae Mar 31, 2025
51f63a0
fix: Fix Human-relay international translation
NyxJae Mar 31, 2025
107d16e
fix: Optimize the display of Human-relay Dialog
NyxJae Mar 31, 2025
5d19c97
fix(test): add missing CodeActionKind mock in vscode.js
NyxJae Mar 31, 2025
7b3bfca
Merge branch 'main' of github.com:NyxJae/Roo-Code into human-relay
NyxJae Apr 1, 2025
e60355f
Merge branch 'main' into human-relay
NyxJae Apr 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 108 additions & 4 deletions src/api/providers/human-relay.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -111,12 +110,48 @@ 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
// 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<string | undefined> {
async function showHumanRelayDialog(promptText: string, options?: ApiHandlerOptions): Promise<string | undefined> {
// 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)

return new Promise<string | undefined>((resolve) => {
// Create a unique request ID
const requestId = Date.now().toString()
Expand All @@ -126,6 +161,11 @@ async function showHumanRelayDialog(promptText: string): Promise<string | undefi
"roo-cline.registerHumanRelayCallback",
requestId,
(response: string | undefined) => {
// Clear clipboard monitoring timer
if (clipboardInterval) {
clearInterval(clipboardInterval)
clipboardInterval = null
}
resolve(response)
},
)
Expand All @@ -135,5 +175,69 @@ async function showHumanRelayDialog(promptText: string): Promise<string | undefi
requestId,
promptText,
})

// If clipboard monitoring is enabled, start polling for clipboard changes
let clipboardInterval: NodeJS.Timeout | null = null

if (options?.humanRelayMonitorClipboard) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure unit/integration tests cover the new clipboard monitoring functionality, including duplicate response detection.

const monitorInterval = Math.min(Math.max(100, options?.humanRelayMonitorInterval ?? 500), 2000)

clipboardInterval = setInterval(async () => {
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)
}
})
}
2 changes: 2 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export interface WebviewMessage {
| "maxOpenTabsContext"
| "humanRelayResponse"
| "humanRelayCancel"
| "closeHumanRelayDialog"
| "showDuplicateResponseAlert"
| "browserToolEnabled"
| "telemetrySetting"
| "showRooIgnoredFiles"
Expand Down
5 changes: 5 additions & 0 deletions src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -126,6 +129,8 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
"modelTemperature",
"modelMaxTokens",
"modelMaxThinkingTokens",
"humanRelayMonitorClipboard",
"humanRelayMonitorInterval",
]

// Models
Expand Down
2 changes: 2 additions & 0 deletions src/shared/globalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
historyButtonClicked: "history",
}
const App = () => {
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<Tab>("chat")
const settingsRef = useRef<SettingsViewRef>(null)
Expand Down Expand Up @@ -139,6 +146,8 @@ const App = () => {
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
onSubmit={handleHumanRelaySubmit}
onCancel={handleHumanRelayCancel}
monitorClipboard={apiConfiguration?.humanRelayMonitorClipboard}
monitorInterval={apiConfiguration?.humanRelayMonitorInterval}
/>
</>
)
Expand Down
48 changes: 47 additions & 1 deletion webview-ui/src/components/human-relay/HumanRelayDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,6 +13,8 @@ interface HumanRelayDialogProps {
promptText: string
onSubmit: (requestId: string, text: string) => void
onCancel: (requestId: string) => void
monitorClipboard?: boolean
monitorInterval?: number
}

/**
Expand All @@ -25,19 +28,44 @@ export const HumanRelayDialog: React.FC<HumanRelayDialogProps> = ({
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(() => {
if (isOpen) {
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)
Expand Down Expand Up @@ -85,6 +113,24 @@ export const HumanRelayDialog: React.FC<HumanRelayDialogProps> = ({
</div>

{isCopyClicked && <div className="text-sm text-emerald-500 font-medium">Copied to clipboard</div>}
{monitorClipboard && (
<>
{showDuplicateWarning && (
<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">
<AlertTriangle className="h-4 w-4 text-amber-500" />
<span className="font-medium">
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.
</span>
</div>
)}

<div className="flex items-center gap-2 text-sm text-vscode-descriptionForeground">
<ProgressIndicator />
<span>Monitoring clipboard for changes, interval: {monitorInterval}ms</span>
</div>
</>
)}

<div>
<div className="mb-2 font-medium">Please enter the AI's response:</div>
Expand Down
34 changes: 34 additions & 0 deletions webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</div>
{/* Auto clipboard monitoring */}
<div className="mt-4">
<Checkbox
checked={apiConfiguration?.humanRelayMonitorClipboard ?? false}
onChange={handleInputChange("humanRelayMonitorClipboard", noTransform)}>
<span className="font-medium">Enable clipboard monitoring</span>
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground ml-6">
Automatically detect when you copy the AI's response from the browser
</div>
</div>
{apiConfiguration?.humanRelayMonitorClipboard && (
<div className="mt-2">
<label className="font-medium">Monitor interval (ms)</label>
<input
type="range"
min="100"
max="2000"
step="100"
value={apiConfiguration?.humanRelayMonitorInterval || 500}
onChange={handleInputChange("humanRelayMonitorInterval", (e) => {
const target = e.target as HTMLInputElement
return parseInt(target.value)
})}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ minWidth: "45px", textAlign: "left" }}>
{apiConfiguration?.humanRelayMonitorInterval || 500} ms
</span>
<div className="text-sm text-vscode-descriptionForeground">
How frequently to check for clipboard changes (100-2000ms)
</div>
</div>
)}
</>
)}

Expand Down
Loading