Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 27 additions & 1 deletion packages/vscode-ide-companion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -126,6 +127,10 @@
"command": "qwen-code.showLogs",
"title": "Qwen Code: Show Logs"
},
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The qwen-code.daemonSmoke command is contributed and activated unconditionally, so it is visible and runnable even when qwen-code.experimentalDaemonIde is 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

"command": "qwen-code.daemonSmoke",
"title": "Qwen Code: Daemon Smoke Test"
},
{
"command": "qwen-code.copyMessage",
"title": "%qwen-code.copyMessage.title%"
Expand All @@ -152,6 +157,9 @@
{
"command": "qwen-code.auth"
},
{
"command": "qwen-code.daemonSmoke"
},
{
"command": "qwen-code.copyMessage",
"when": "false"
Expand Down Expand Up @@ -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."
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] qwen-code.daemonToken is stored as a plain "type": "string" configuration value. This means the bearer token is written in cleartext to settings.json, synced across machines via Settings Sync, and could be committed into dotfiles repos. Consider using VS Code's SecretStorage API (context.secrets) instead, or at minimum mark it "secret": true in the configuration contribution.

— 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."
}
}
},
Expand Down
138 changes: 138 additions & 0 deletions packages/vscode-ide-companion/src/commands/daemonSmoke.ts
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> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The isRecord type guard is duplicated verbatim between daemonSmoke.ts and daemonIdeConnection.ts. If the implementation diverges or a bug is found, both copies must be updated separately.

Impact: Maintenance burden — two independent copies create a risk of behavioral divergence that is hard to debug.

Suggested change
function isRecord(value: unknown): value is Record<string, unknown> {
// Extract to a shared utility, e.g. packages/vscode-ide-companion/src/utils/typeGuards.ts
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

Then import from the shared module in both daemonSmoke.ts and daemonIdeConnection.ts.

— 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']);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] getSessionUpdateText() only handles agent_message_chunk and agent_thought_chunk. All other session events (errors, state transitions, session_died) are silently discarded. The user sees "Qwen daemon smoke prompt completed." but the output channel may be empty — impossible to tell if the daemon errored or returned empty content.

Suggested change
const text = getSessionUpdateText(data);
if (text) {
outputChannel?.append(text);
} else if (data.update?.sessionUpdate) {
outputChannel?.appendLine(
`[daemon] event: ${data.update.sessionUpdate}`,
);
}

— 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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The default daemon URL 'http://127.0.0.1:4170' is hardcoded in 4 separate locations: daemonSmoke.ts:89, daemonAcpConnection.ts:68, daemonAcpConnection.ts:133, and WebViewProvider.ts:64. If the default port or address changes, missing any one of these will produce inconsistent connection behavior with no compile error.

Impact: Brittle defaults — a port change requires hunting down multiple magic strings across the codebase.

Suggested change
if (!prompt) {
// Define once in a shared constant, e.g. in daemonIdeConnection.ts or a new constants/daemon.ts
export const DEFAULT_DAEMON_BASE_URL = 'http://127.0.0.1:4170';

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') ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The smoke command reads QWEN_SERVER_TOKEN from the process environment as a fallback for the daemon token. This is inconsistent with the WebViewProvider daemon path (createAgentManagerFromConfiguration) which reads only from qwen-code.daemonToken VS Code config.

Impact: (a) Any VS Code extension running in the same process can discover the token via process.env; (b) users who set QWEN_SERVER_TOKEN only for CLI use may unintentionally leak it to the VS Code smoke test.

Suggested change
config.get<string>('qwen-code.daemonToken') ||
const token = config.get<string>('qwen-code.daemonToken');

If interactive smoke testing genuinely needs a fallback, prompt the user with vscode.window.showInputBox({ password: true }) before connecting instead of reading the environment.

— 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}`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 http://user:pass@127.0.0.1:4170/?token=..., validateDaemonBaseUrl() will reject the embedded credentials later, but the secret has already been persisted in the VS Code output channel. Please validate/parse the URL first and log only a sanitized origin with username, password, query, and hash removed.

— gpt-5.5 via Qwen Code /review

connection.onSessionUpdate = (data) => {
const text = getSessionUpdateText(data);
if (text) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The smoke command wires onSessionUpdate, onPermissionRequest, onEndTurn, and onDisconnected, but does not wire onAskUserQuestion. When the daemon sends an ask_user_question permission request, DaemonIdeConnection falls back to the default handler which returns { optionId: 'cancel' } — the multi-choice prompt is silently cancelled without the user ever seeing it.

Suggested change
if (text) {
connection.onAskUserQuestion = async (request) => {
const picked = await vscode.window.showQuickPick(
(request.options ?? []).map((o) => ({
label: o.label ?? o.optionId,
optionId: o.optionId,
})),
{ title: request.question ?? 'Qwen daemon question' },
);
return { optionId: picked?.optionId ?? 'cancel' };
};

— 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();
}
}),
);
}
6 changes: 6 additions & 0 deletions packages/vscode-ide-companion/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
CHAT_VIEW_ID_SIDEBAR,
CHAT_VIEW_ID_SECONDARY,
} from '../constants/viewIds.js';
import {
daemonSmokeCommand,
registerDaemonSmokeCommand,
} from './daemonSmoke.js';

type Logger = (message: string) => void;

Expand All @@ -23,6 +27,7 @@ export const authCommand = 'qwen-code.auth';
export const focusChatCommand = 'qwen-code.focusChat';
export const newConversationCommand = 'qwen-code.newConversation';
export const showLogsCommand = 'qwen-code.showLogs';
export { daemonSmokeCommand };

/**
* Register all Qwen Code chat-related commands.
Expand Down Expand Up @@ -147,4 +152,5 @@ export function registerNewCommands(
);

context.subscriptions.push(...disposables);
registerDaemonSmokeCommand(context, log, outputChannel);
}
Loading
Loading