-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(ide): add experimental daemon webview path #4267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,8 @@ | |
| "onCommand:qwen-code.openChat", | ||
| "onCommand:qwen-code.focusChat", | ||
| "onCommand:qwen-code.newConversation", | ||
| "onCommand:qwen-code.showLogs" | ||
| "onCommand:qwen-code.showLogs", | ||
| "onCommand:qwen-code.daemonSmoke" | ||
| ], | ||
| "contributes": { | ||
| "jsonValidation": [ | ||
|
|
@@ -126,6 +127,10 @@ | |
| "command": "qwen-code.showLogs", | ||
| "title": "Qwen Code: Show Logs" | ||
| }, | ||
| { | ||
| "command": "qwen-code.daemonSmoke", | ||
| "title": "Qwen Code: Daemon Smoke Test" | ||
| }, | ||
| { | ||
| "command": "qwen-code.copyMessage", | ||
| "title": "%qwen-code.copyMessage.title%" | ||
|
|
@@ -152,6 +157,9 @@ | |
| { | ||
| "command": "qwen-code.auth" | ||
| }, | ||
| { | ||
| "command": "qwen-code.daemonSmoke" | ||
| }, | ||
| { | ||
| "command": "qwen-code.copyMessage", | ||
| "when": "false" | ||
|
|
@@ -247,6 +255,24 @@ | |
| "type": "boolean", | ||
| "default": true, | ||
| "description": "Show notifications with sound when a task completes (at least 20 seconds) or needs your attention, while you are not actively viewing the Qwen Code panel." | ||
| }, | ||
| "qwen-code.daemonUrl": { | ||
| "order": 5, | ||
| "type": "string", | ||
| "default": "", | ||
| "description": "Experimental qwen serve URL used by daemon-backed IDE drafts and the Daemon Smoke Test command." | ||
| }, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] — DeepSeek/deepseek-v4-pro via Qwen Code /review |
||
| "qwen-code.daemonToken": { | ||
| "order": 6, | ||
| "type": "string", | ||
| "default": "", | ||
| "description": "Optional bearer token used by daemon-backed IDE drafts and the Daemon Smoke Test command." | ||
| }, | ||
| "qwen-code.experimentalDaemonIde": { | ||
| "order": 7, | ||
| "type": "boolean", | ||
| "default": false, | ||
| "description": "Experimental: route the IDE webview through a loopback qwen serve daemon instead of spawning a local ACP child process. The daemon owns runtime execution for the same workspace." | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,138 @@ | ||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * @license | ||||||||||||||||||||||||
| * Copyright 2025 Qwen Team | ||||||||||||||||||||||||
| * SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import * as vscode from 'vscode'; | ||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||
| RequestPermissionRequest, | ||||||||||||||||||||||||
| SessionNotification, | ||||||||||||||||||||||||
| } from '@agentclientprotocol/sdk'; | ||||||||||||||||||||||||
| import { DaemonIdeConnection } from '../services/daemonIdeConnection.js'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| type Logger = (message: string) => void; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export const daemonSmokeCommand = 'qwen-code.daemonSmoke'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The Impact: Maintenance burden — two independent copies create a risk of behavioral divergence that is hard to debug.
Suggested change
Then import from the shared module in both — DeepSeek/deepseek-v4-pro via Qwen Code /review |
||||||||||||||||||||||||
| return typeof value === 'object' && value !== null; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getTextContent(value: unknown): string | undefined { | ||||||||||||||||||||||||
| if (!isRecord(value)) { | ||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return typeof value['text'] === 'string' ? value['text'] : undefined; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getSessionUpdateText(data: SessionNotification): string | undefined { | ||||||||||||||||||||||||
| if (!isRecord(data)) { | ||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| const update = data['update']; | ||||||||||||||||||||||||
| if (!isRecord(update)) { | ||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| const sessionUpdate = update['sessionUpdate']; | ||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||
| sessionUpdate !== 'agent_message_chunk' && | ||||||||||||||||||||||||
| sessionUpdate !== 'agent_thought_chunk' | ||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return getTextContent(update['content']); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical]
Suggested change
— DeepSeek/deepseek-v4-pro via Qwen Code /review |
||||||||||||||||||||||||
| async function pickPermissionOption( | ||||||||||||||||||||||||
| request: RequestPermissionRequest, | ||||||||||||||||||||||||
| ): Promise<{ optionId?: string }> { | ||||||||||||||||||||||||
| const options = Array.isArray(request.options) ? request.options : []; | ||||||||||||||||||||||||
| const picked = await vscode.window.showQuickPick( | ||||||||||||||||||||||||
| options.map((option) => ({ | ||||||||||||||||||||||||
| label: option.name ?? option.optionId, | ||||||||||||||||||||||||
| description: option.optionId, | ||||||||||||||||||||||||
| optionId: option.optionId, | ||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| title: `Qwen daemon permission: ${request.toolCall?.kind ?? 'tool'}`, | ||||||||||||||||||||||||
| placeHolder: 'Choose a daemon permission response', | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| return { optionId: picked?.optionId ?? 'cancel' }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export function registerDaemonSmokeCommand( | ||||||||||||||||||||||||
| context: vscode.ExtensionContext, | ||||||||||||||||||||||||
| log: Logger, | ||||||||||||||||||||||||
| outputChannel?: vscode.OutputChannel, | ||||||||||||||||||||||||
| ): void { | ||||||||||||||||||||||||
| context.subscriptions.push( | ||||||||||||||||||||||||
| vscode.commands.registerCommand(daemonSmokeCommand, async () => { | ||||||||||||||||||||||||
| const config = vscode.workspace.getConfiguration(); | ||||||||||||||||||||||||
| const configuredUrl = | ||||||||||||||||||||||||
| config.get<string>('qwen-code.daemonUrl') || 'http://127.0.0.1:4170'; | ||||||||||||||||||||||||
| const baseUrl = await vscode.window.showInputBox({ | ||||||||||||||||||||||||
| title: 'Qwen daemon URL', | ||||||||||||||||||||||||
| value: configuredUrl, | ||||||||||||||||||||||||
| ignoreFocusOut: true, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| if (!baseUrl) { | ||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const prompt = await vscode.window.showInputBox({ | ||||||||||||||||||||||||
| title: 'Qwen daemon smoke prompt', | ||||||||||||||||||||||||
| value: 'Say hello from the daemon IDE wire-up.', | ||||||||||||||||||||||||
| ignoreFocusOut: true, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| if (!prompt) { | ||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The default daemon URL Impact: Brittle defaults — a port change requires hunting down multiple magic strings across the codebase.
Suggested change
Refactor all 4 locations to reference this constant. — DeepSeek/deepseek-v4-pro via Qwen Code /review |
||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const token = | ||||||||||||||||||||||||
| config.get<string>('qwen-code.daemonToken') || | ||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The smoke command reads Impact: (a) Any VS Code extension running in the same process can discover the token via
Suggested change
If interactive smoke testing genuinely needs a fallback, prompt the user with — DeepSeek/deepseek-v4-pro via Qwen Code /review |
||||||||||||||||||||||||
| process.env['QWEN_SERVER_TOKEN']; | ||||||||||||||||||||||||
| const workspaceCwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; | ||||||||||||||||||||||||
| const connection = new DaemonIdeConnection(); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| outputChannel?.show(true); | ||||||||||||||||||||||||
| outputChannel?.appendLine(`[daemon] connecting to ${baseUrl}`); | ||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] This logs the user-entered daemon URL before any validation or redaction. If the user pastes something like — gpt-5.5 via Qwen Code /review |
||||||||||||||||||||||||
| connection.onSessionUpdate = (data) => { | ||||||||||||||||||||||||
| const text = getSessionUpdateText(data); | ||||||||||||||||||||||||
| if (text) { | ||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] The smoke command wires
Suggested change
— DeepSeek/deepseek-v4-pro via Qwen Code /review |
||||||||||||||||||||||||
| outputChannel?.append(text); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| connection.onPermissionRequest = pickPermissionOption; | ||||||||||||||||||||||||
| connection.onEndTurn = (reason) => { | ||||||||||||||||||||||||
| outputChannel?.appendLine(''); | ||||||||||||||||||||||||
| outputChannel?.appendLine(`[daemon] turn ended: ${reason ?? 'ok'}`); | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| connection.onDisconnected = (_code, signal) => { | ||||||||||||||||||||||||
| outputChannel?.appendLine(`[daemon] disconnected: ${signal ?? 'ok'}`); | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| await connection.connect({ | ||||||||||||||||||||||||
| baseUrl, | ||||||||||||||||||||||||
| token, | ||||||||||||||||||||||||
| workspaceCwd, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| outputChannel?.appendLine( | ||||||||||||||||||||||||
| `[daemon] session ${connection.currentSessionId ?? 'unknown'}`, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| await connection.sendPrompt(prompt); | ||||||||||||||||||||||||
| vscode.window.showInformationMessage( | ||||||||||||||||||||||||
| 'Qwen daemon smoke prompt completed.', | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||
| const message = error instanceof Error ? error.message : String(error); | ||||||||||||||||||||||||
| log(`[DaemonSmoke] ${message}`); | ||||||||||||||||||||||||
| vscode.window.showErrorMessage(`Qwen daemon smoke failed: ${message}`); | ||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||
| await connection.disconnect(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The
qwen-code.daemonSmokecommand is contributed and activated unconditionally, so it is visible and runnable even whenqwen-code.experimentalDaemonIdeis disabled. That exposes the draft daemon flow in the normal command surface despite the feature being described as opt-in, and a user can accidentally send a prompt/workspace context to the configured daemon without first enabling the experimental IDE path.Please gate the command behind the same experimental setting before it connects (and ideally hide it from the command palette unless the flag is enabled), showing a clear message when the draft daemon path is disabled.
— gpt-5.5 via Qwen Code /review