Skip to content

Commit 08a449e

Browse files
authored
Merge pull request #1267 from NyxJae/human-relay
Human relay
2 parents 7e4761f + f4441e3 commit 08a449e

File tree

11 files changed

+502
-3
lines changed

11 files changed

+502
-3
lines changed

src/activate/registerCommands.ts

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

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

6+
// Store panel references in both modes
7+
let sidebarPanel: vscode.WebviewView | undefined = undefined
8+
let tabPanel: vscode.WebviewPanel | undefined = undefined
9+
10+
/**
11+
* Get the currently active panel
12+
* @returns WebviewPanel或WebviewView
13+
*/
14+
export function getPanel(): vscode.WebviewPanel | vscode.WebviewView | undefined {
15+
return tabPanel || sidebarPanel
16+
}
17+
18+
/**
19+
* Set panel references
20+
*/
21+
export function setPanel(
22+
newPanel: vscode.WebviewPanel | vscode.WebviewView | undefined,
23+
type: "sidebar" | "tab",
24+
): void {
25+
if (type === "sidebar") {
26+
sidebarPanel = newPanel as vscode.WebviewView
27+
tabPanel = undefined
28+
} else {
29+
tabPanel = newPanel as vscode.WebviewPanel
30+
sidebarPanel = undefined
31+
}
32+
}
33+
634
export type RegisterCommandOptions = {
735
context: vscode.ExtensionContext
836
outputChannel: vscode.OutputChannel
@@ -15,6 +43,22 @@ export const registerCommands = (options: RegisterCommandOptions) => {
1543
for (const [command, callback] of Object.entries(getCommandsMap(options))) {
1644
context.subscriptions.push(vscode.commands.registerCommand(command, callback))
1745
}
46+
47+
// Human Relay Dialog Command
48+
context.subscriptions.push(
49+
vscode.commands.registerCommand(
50+
"roo-cline.showHumanRelayDialog",
51+
(params: { requestId: string; promptText: string }) => {
52+
if (getPanel()) {
53+
getPanel()?.webview.postMessage({
54+
type: "showHumanRelayDialog",
55+
requestId: params.requestId,
56+
promptText: params.promptText,
57+
})
58+
}
59+
},
60+
),
61+
)
1862
}
1963

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

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

68-
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
112+
const newPanel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
69113
enableScripts: true,
70114
retainContextWhenHidden: true,
71115
localResourceRoots: [context.extensionUri],
72116
})
73117

118+
// Save as tab type panel
119+
setPanel(newPanel, "tab")
120+
74121
// TODO: use better svg icon with light and dark variants (see
75122
// https://stackoverflow.com/questions/58365687/vscode-extension-iconpath).
76-
panel.iconPath = {
123+
newPanel.iconPath = {
77124
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
78125
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
79126
}
80127

81-
await tabProvider.resolveWebviewView(panel)
128+
await tabProvider.resolveWebviewView(newPanel)
129+
130+
// Handle panel closing events
131+
newPanel.onDidDispose(() => {
132+
setPanel(undefined, "tab")
133+
})
82134

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

src/api/index.ts

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

2324
export interface SingleCompletionHandler {
2425
completePrompt(prompt: string): Promise<string>
@@ -72,6 +73,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
7273
return new UnboundHandler(options)
7374
case "requesty":
7475
return new RequestyHandler(options)
76+
case "human-relay":
77+
return new HumanRelayHandler(options)
7578
default:
7679
return new AnthropicHandler(options)
7780
}

src/api/providers/human-relay.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
import { getPanel } from "../../activate/registerCommands" // Import the getPanel function
9+
10+
/**
11+
* Human Relay API processor
12+
* This processor does not directly call the API, but interacts with the model through human operations copy and paste.
13+
*/
14+
export class HumanRelayHandler implements ApiHandler, SingleCompletionHandler {
15+
private options: ApiHandlerOptions
16+
17+
constructor(options: ApiHandlerOptions) {
18+
this.options = options
19+
}
20+
countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
21+
return Promise.resolve(0)
22+
}
23+
24+
/**
25+
* Create a message processing flow, display a dialog box to request human assistance
26+
* @param systemPrompt System prompt words
27+
* @param messages Message list
28+
*/
29+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
30+
// Get the most recent user message
31+
const latestMessage = messages[messages.length - 1]
32+
33+
if (!latestMessage) {
34+
throw new Error("No message to relay")
35+
}
36+
37+
// If it is the first message, splice the system prompt word with the user message
38+
let promptText = ""
39+
if (messages.length === 1) {
40+
promptText = `${systemPrompt}\n\n${getMessageContent(latestMessage)}`
41+
} else {
42+
promptText = getMessageContent(latestMessage)
43+
}
44+
45+
// Copy to clipboard
46+
await vscode.env.clipboard.writeText(promptText)
47+
48+
// A dialog box pops up to request user action
49+
const response = await showHumanRelayDialog(promptText)
50+
51+
if (!response) {
52+
// The user canceled the operation
53+
throw new Error("Human relay operation cancelled")
54+
}
55+
56+
// Return to the user input reply
57+
yield { type: "text", text: response }
58+
}
59+
60+
/**
61+
* Get model information
62+
*/
63+
getModel(): { id: string; info: ModelInfo } {
64+
// Human relay does not depend on a specific model, here is a default configuration
65+
return {
66+
id: "human-relay",
67+
info: {
68+
maxTokens: 16384,
69+
contextWindow: 100000,
70+
supportsImages: true,
71+
supportsPromptCache: false,
72+
supportsComputerUse: true,
73+
inputPrice: 0,
74+
outputPrice: 0,
75+
description: "Calling web-side AI model through human relay",
76+
},
77+
}
78+
}
79+
80+
/**
81+
* Implementation of a single prompt
82+
* @param prompt Prompt content
83+
*/
84+
async completePrompt(prompt: string): Promise<string> {
85+
// Copy to clipboard
86+
await vscode.env.clipboard.writeText(prompt)
87+
88+
// A dialog box pops up to request user action
89+
const response = await showHumanRelayDialog(prompt)
90+
91+
if (!response) {
92+
throw new Error("Human relay operation cancelled")
93+
}
94+
95+
return response
96+
}
97+
}
98+
99+
/**
100+
* Extract text content from message object
101+
* @param message
102+
*/
103+
function getMessageContent(message: Anthropic.Messages.MessageParam): string {
104+
if (typeof message.content === "string") {
105+
return message.content
106+
} else if (Array.isArray(message.content)) {
107+
return message.content
108+
.filter((item) => item.type === "text")
109+
.map((item) => (item.type === "text" ? item.text : ""))
110+
.join("\n")
111+
}
112+
return ""
113+
}
114+
/**
115+
* Displays the human relay dialog and waits for user response.
116+
* @param promptText The prompt text that needs to be copied.
117+
* @returns The user's input response or undefined (if canceled).
118+
*/
119+
async function showHumanRelayDialog(promptText: string): Promise<string | undefined> {
120+
return new Promise<string | undefined>((resolve) => {
121+
// Create a unique request ID
122+
const requestId = Date.now().toString()
123+
124+
// Register a global callback function
125+
vscode.commands.executeCommand(
126+
"roo-cline.registerHumanRelayCallback",
127+
requestId,
128+
(response: string | undefined) => {
129+
resolve(response)
130+
},
131+
)
132+
133+
// Open the dialog box directly using the current panel
134+
vscode.commands.executeCommand("roo-cline.showHumanRelayDialog", {
135+
requestId,
136+
promptText,
137+
})
138+
})
139+
}

src/core/webview/ClineProvider.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import pWaitFor from "p-wait-for"
77
import * as path from "path"
88
import * as vscode from "vscode"
99
import simpleGit from "simple-git"
10+
import { setPanel } from "../../activate/registerCommands"
1011

1112
import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api"
1213
import { CheckpointStorage } from "../../shared/checkpoints"
@@ -237,6 +238,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
237238
this.outputChannel.appendLine("Resolving webview view")
238239
this.view = webviewView
239240

241+
// Set panel reference according to webview type
242+
if ("onDidChangeViewState" in webviewView) {
243+
// Tag page type
244+
setPanel(webviewView, "tab")
245+
} else if ("onDidChangeVisibility" in webviewView) {
246+
// Sidebar Type
247+
setPanel(webviewView, "sidebar")
248+
}
249+
240250
// Initialize sound enabled state
241251
this.getState().then(({ soundEnabled }) => {
242252
setSoundEnabled(soundEnabled ?? false)
@@ -1558,6 +1568,25 @@ export class ClineProvider implements vscode.WebviewViewProvider {
15581568
await this.updateGlobalState("mode", defaultModeSlug)
15591569
await this.postStateToWebview()
15601570
}
1571+
break
1572+
case "humanRelayResponse":
1573+
if (message.requestId && message.text) {
1574+
vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", {
1575+
requestId: message.requestId,
1576+
text: message.text,
1577+
cancelled: false,
1578+
})
1579+
}
1580+
break
1581+
1582+
case "humanRelayCancel":
1583+
if (message.requestId) {
1584+
vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", {
1585+
requestId: message.requestId,
1586+
cancelled: true,
1587+
})
1588+
}
1589+
break
15611590
}
15621591
},
15631592
null,

src/extension.ts

Lines changed: 46 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,40 @@ export function activate(context: vscode.ExtensionContext) {
4557

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

60+
// Register human relay callback registration command
61+
context.subscriptions.push(
62+
vscode.commands.registerCommand(
63+
"roo-cline.registerHumanRelayCallback",
64+
(requestId: string, callback: (response: string | undefined) => void) => {
65+
registerHumanRelayCallback(requestId, callback)
66+
},
67+
),
68+
)
69+
70+
// Register human relay response processing command
71+
context.subscriptions.push(
72+
vscode.commands.registerCommand(
73+
"roo-cline.handleHumanRelayResponse",
74+
(response: { requestId: string; text?: string; cancelled?: boolean }) => {
75+
const callback = humanRelayCallbacks.get(response.requestId)
76+
if (callback) {
77+
if (response.cancelled) {
78+
callback(undefined)
79+
} else {
80+
callback(response.text)
81+
}
82+
humanRelayCallbacks.delete(response.requestId)
83+
}
84+
},
85+
),
86+
)
87+
88+
context.subscriptions.push(
89+
vscode.commands.registerCommand("roo-cline.unregisterHumanRelayCallback", (requestId: string) => {
90+
humanRelayCallbacks.delete(requestId)
91+
}),
92+
)
93+
4894
/**
4995
* We use the text document content provider API to show the left side for diff
5096
* 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
@@ -46,6 +46,9 @@ export interface ExtensionMessage {
4646
| "updateCustomMode"
4747
| "deleteCustomMode"
4848
| "currentCheckpointUpdated"
49+
| "showHumanRelayDialog"
50+
| "humanRelayResponse"
51+
| "humanRelayCancel"
4952
| "browserToolEnabled"
5053
text?: string
5154
action?:
@@ -243,4 +246,22 @@ export interface ClineApiReqInfo {
243246
streamingFailedMessage?: string
244247
}
245248

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

0 commit comments

Comments
 (0)