Skip to content

Commit 3bb1d78

Browse files
committed
feat: Added human relay function and related message processing initial version
1 parent 931af8f commit 3bb1d78

File tree

12 files changed

+483
-3
lines changed

12 files changed

+483
-3
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,8 @@ docs/_site/
2828

2929
#Logging
3030
logs
31+
.clinerules-architect
32+
.clinerules-ask
33+
.clinerules-code
34+
MemoryBank
35+
.github/copilot-instructions.md

src/activate/registerCommands.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import delay from "delay"
33

44
import { ClineProvider } from "../core/webview/ClineProvider"
55

6+
// Add a global variable to store panel references
7+
let panel: vscode.WebviewPanel | undefined = undefined
8+
9+
// Get the panel function for command access
10+
export function getPanel(): vscode.WebviewPanel | undefined {
11+
return panel
12+
}
13+
14+
// Setting the function of the panel
15+
export function setPanel(newPanel: vscode.WebviewPanel | undefined): void {
16+
panel = newPanel
17+
}
18+
619
export type RegisterCommandOptions = {
720
context: vscode.ExtensionContext
821
outputChannel: vscode.OutputChannel
@@ -15,6 +28,22 @@ export const registerCommands = (options: RegisterCommandOptions) => {
1528
for (const [command, callback] of Object.entries(getCommandsMap(options))) {
1629
context.subscriptions.push(vscode.commands.registerCommand(command, callback))
1730
}
31+
32+
// Human Relay Dialog Command
33+
context.subscriptions.push(
34+
vscode.commands.registerCommand(
35+
"roo-code.showHumanRelayDialog",
36+
(params: { requestId: string; promptText: string }) => {
37+
if (getPanel()) {
38+
getPanel()?.webview.postMessage({
39+
type: "showHumanRelayDialog",
40+
requestId: params.requestId,
41+
promptText: params.promptText,
42+
})
43+
}
44+
},
45+
),
46+
)
1847
}
1948

2049
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => {
@@ -65,20 +94,28 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterComman
6594

6695
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
6796

68-
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
97+
const newPanel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
6998
enableScripts: true,
7099
retainContextWhenHidden: true,
71100
localResourceRoots: [context.extensionUri],
72101
})
73102

103+
// Save panel references
104+
setPanel(newPanel)
105+
74106
// TODO: use better svg icon with light and dark variants (see
75107
// https://stackoverflow.com/questions/58365687/vscode-extension-iconpath).
76-
panel.iconPath = {
108+
newPanel.iconPath = {
77109
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
78110
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
79111
}
80112

81-
await tabProvider.resolveWebviewView(panel)
113+
await tabProvider.resolveWebviewView(newPanel)
114+
115+
// Handle panel closing events
116+
newPanel.onDidDispose(() => {
117+
setPanel(undefined)
118+
})
82119

83120
// Lock the editor group so clicking on files doesn't open them over the panel
84121
await delay(100)

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { VsCodeLmHandler } from "./providers/vscode-lm"
1616
import { ApiStream } from "./transform/stream"
1717
import { UnboundHandler } from "./providers/unbound"
1818
import { RequestyHandler } from "./providers/requesty"
19+
import { HumanRelayHandler } from "./providers/human-relay"
1920

2021
export interface SingleCompletionHandler {
2122
completePrompt(prompt: string): Promise<string>
@@ -59,6 +60,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
5960
return new UnboundHandler(options)
6061
case "requesty":
6162
return new RequestyHandler(options)
63+
case "human-relay":
64+
return new HumanRelayHandler(options)
6265
default:
6366
return new AnthropicHandler(options)
6467
}

src/api/providers/human-relay.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// filepath: e:\Project\Roo-Code\src\api\providers\human-relay.ts
2+
import { Anthropic } from "@anthropic-ai/sdk"
3+
import { ApiHandlerOptions, ModelInfo } from "../../shared/api"
4+
import { ApiHandler, SingleCompletionHandler } from "../index"
5+
import { ApiStream } from "../transform/stream"
6+
import * as vscode from "vscode"
7+
import { ExtensionMessage } from "../../shared/ExtensionMessage"
8+
9+
/**
10+
* Human Relay API processor
11+
* This processor does not directly call the API, but interacts with the model through human operations copy and paste.
12+
*/
13+
export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler {
14+
private options: ApiHandlerOptions
15+
16+
constructor(options: ApiHandlerOptions) {
17+
this.options = options
18+
}
19+
20+
/**
21+
* Create a message processing flow, display a dialog box to request human assistance
22+
* @param systemPrompt System prompt words
23+
* @param messages Message list
24+
*/
25+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
26+
// Get the most recent user message
27+
const latestMessage = messages[messages.length - 1]
28+
29+
if (!latestMessage) {
30+
throw new Error("No message to relay")
31+
}
32+
33+
// If it is the first message, splice the system prompt word with the user message
34+
let promptText = ""
35+
if (messages.length === 1) {
36+
promptText = `${systemPrompt}\n\n${getMessageContent(latestMessage)}`
37+
} else {
38+
promptText = getMessageContent(latestMessage)
39+
}
40+
41+
// Copy to clipboard
42+
await vscode.env.clipboard.writeText(promptText)
43+
44+
// A dialog box pops up to request user action
45+
const response = await showHumanRelayDialog(promptText)
46+
47+
if (!response) {
48+
// The user canceled the operation
49+
throw new Error("Human relay operation cancelled")
50+
}
51+
52+
// Return to the user input reply
53+
yield { type: "text", text: response }
54+
}
55+
56+
/**
57+
* Get model information
58+
*/
59+
getModel(): { id: string; info: ModelInfo } {
60+
// Human relay does not depend on a specific model, here is a default configuration
61+
return {
62+
id: "human-relay",
63+
info: {
64+
maxTokens: 16384,
65+
contextWindow: 100000,
66+
supportsImages: true,
67+
supportsPromptCache: false,
68+
supportsComputerUse: true,
69+
inputPrice: 0,
70+
outputPrice: 0,
71+
description: "Calling web-side AI model through human relay",
72+
},
73+
}
74+
}
75+
76+
/**
77+
* Implementation of a single prompt
78+
* @param prompt Prompt content
79+
*/
80+
async completePrompt(prompt: string): Promise<string> {
81+
// Copy to clipboard
82+
await vscode.env.clipboard.writeText(prompt)
83+
84+
// A dialog box pops up to request user action
85+
const response = await showHumanRelayDialog(prompt)
86+
87+
if (!response) {
88+
throw new Error("Human relay operation cancelled")
89+
}
90+
91+
return response
92+
}
93+
}
94+
95+
/**
96+
* Extract text content from message object
97+
* @param message
98+
*/
99+
function getMessageContent(message: Anthropic.Messages.MessageParam): string {
100+
if (typeof message.content === "string") {
101+
return message.content
102+
} else if (Array.isArray(message.content)) {
103+
return message.content
104+
.filter((item) => item.type === "text")
105+
.map((item) => (item.type === "text" ? item.text : ""))
106+
.join("\n")
107+
}
108+
return ""
109+
}
110+
/**
111+
* Displays the human relay dialog and waits for user response.
112+
* @param promptText The prompt text that needs to be copied.
113+
* @returns The user's input response or undefined (if canceled).
114+
*/
115+
async function showHumanRelayDialog(promptText: string): Promise<string | undefined> {
116+
return new Promise<string | undefined>((resolve) => {
117+
// Create a unique request ID
118+
const requestId = Date.now().toString()
119+
120+
// Register callback to the global callback map
121+
vscode.commands.executeCommand(
122+
"roo-code.registerHumanRelayCallback",
123+
requestId,
124+
(response: string | undefined) => {
125+
resolve(response)
126+
},
127+
)
128+
129+
// Show the WebView dialog
130+
vscode.commands.executeCommand("roo-code.showHumanRelayDialog", {
131+
requestId,
132+
promptText,
133+
})
134+
135+
// Provide a temporary UI in case the WebView fails to load
136+
vscode.window
137+
.showInformationMessage(
138+
"Please paste the copied message to the AI, then copy the response back into the dialog",
139+
{
140+
modal: true,
141+
detail: "The message has been copied to the clipboard. If the dialog does not open, please try using the input box.",
142+
},
143+
"Use Input Box",
144+
)
145+
.then((selection) => {
146+
if (selection === "Use Input Box") {
147+
// Unregister the callback
148+
vscode.commands.executeCommand("roo-code.unregisterHumanRelayCallback", requestId)
149+
150+
vscode.window
151+
.showInputBox({
152+
prompt: "Please paste the AI's response here",
153+
placeHolder: "Paste the AI's response here...",
154+
ignoreFocusOut: true,
155+
})
156+
.then((input) => {
157+
resolve(input || undefined)
158+
})
159+
}
160+
})
161+
})
162+
}

src/core/webview/ClineProvider.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,7 +1522,26 @@ export class ClineProvider implements vscode.WebviewViewProvider {
15221522
// Switch back to default mode after deletion
15231523
await this.updateGlobalState("mode", defaultModeSlug)
15241524
await this.postStateToWebview()
1525+
break
1526+
}
1527+
case "humanRelayResponse":
1528+
if (message.requestId && message.text) {
1529+
vscode.commands.executeCommand("roo-code.handleHumanRelayResponse", {
1530+
requestId: message.requestId,
1531+
text: message.text,
1532+
cancelled: false,
1533+
})
1534+
}
1535+
break
1536+
1537+
case "humanRelayCancel":
1538+
if (message.requestId) {
1539+
vscode.commands.executeCommand("roo-code.handleHumanRelayResponse", {
1540+
requestId: message.requestId,
1541+
cancelled: true,
1542+
})
15251543
}
1544+
break
15261545
}
15271546
},
15281547
null,

src/extension.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ import { McpServerManager } from "./services/mcp/McpServerManager"
1919
let outputChannel: vscode.OutputChannel
2020
let extensionContext: vscode.ExtensionContext
2121

22+
// Callback mapping of human relay response
23+
const humanRelayCallbacks = new Map<string, (response: string | undefined) => void>()
24+
25+
/**
26+
* Register a callback function for human relay response
27+
* @param requestId
28+
* @param callback
29+
*/
30+
export function registerHumanRelayCallback(requestId: string, callback: (response: string | undefined) => void): void {
31+
humanRelayCallbacks.set(requestId, callback)
32+
}
33+
2234
// This method is called when your extension is activated.
2335
// Your extension is activated the very first time the command is executed.
2436
export function activate(context: vscode.ExtensionContext) {
@@ -45,6 +57,30 @@ export function activate(context: vscode.ExtensionContext) {
4557

4658
registerCommands({ context, outputChannel, provider: sidebarProvider })
4759

60+
// Register human relay response processing command
61+
context.subscriptions.push(
62+
vscode.commands.registerCommand(
63+
"roo-code.handleHumanRelayResponse",
64+
(response: { requestId: string; text?: string; cancelled?: boolean }) => {
65+
const callback = humanRelayCallbacks.get(response.requestId)
66+
if (callback) {
67+
if (response.cancelled) {
68+
callback(undefined)
69+
} else {
70+
callback(response.text)
71+
}
72+
humanRelayCallbacks.delete(response.requestId)
73+
}
74+
},
75+
),
76+
)
77+
78+
context.subscriptions.push(
79+
vscode.commands.registerCommand("roo-code.unregisterHumanRelayCallback", (requestId: string) => {
80+
humanRelayCallbacks.delete(requestId)
81+
}),
82+
)
83+
4884
/**
4985
* We use the text document content provider API to show the left side for diff
5086
* view by creating a virtual document for the original content. This makes it

src/shared/ExtensionMessage.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export interface ExtensionMessage {
4545
| "updateCustomMode"
4646
| "deleteCustomMode"
4747
| "currentCheckpointUpdated"
48+
| "showHumanRelayDialog"
49+
| "humanRelayResponse"
50+
| "humanRelayCancel"
4851
text?: string
4952
action?:
5053
| "chatButtonClicked"
@@ -239,4 +242,22 @@ export interface ClineApiReqInfo {
239242
streamingFailedMessage?: string
240243
}
241244

245+
// Human relay related message types
246+
export interface ShowHumanRelayDialogMessage {
247+
type: "showHumanRelayDialog"
248+
requestId: string
249+
promptText: string
250+
}
251+
252+
export interface HumanRelayResponseMessage {
253+
type: "humanRelayResponse"
254+
requestId: string
255+
text: string
256+
}
257+
258+
export interface HumanRelayCancelMessage {
259+
type: "humanRelayCancel"
260+
requestId: string
261+
}
262+
242263
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"

0 commit comments

Comments
 (0)