From d64aded8a9b2515e61f4b3e34481966724cc31c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=A6=E5=A5=87?= Date: Mon, 18 May 2026 10:58:45 +0800 Subject: [PATCH 1/3] feat(tui): add daemon wire-up harness --- package-lock.json | 1 + packages/cli/package.json | 1 + packages/cli/src/commands/daemon-tui.ts | 240 ++++++++++++++++++++++++ packages/cli/src/config/config.ts | 8 +- 4 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/commands/daemon-tui.ts diff --git a/package-lock.json b/package-lock.json index 25cb88f01e0..31ceffd59f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17283,6 +17283,7 @@ "@modelcontextprotocol/sdk": "^1.25.1", "@qwen-code/channel-base": "file:../channels/base", "@qwen-code/channel-dingtalk": "file:../channels/dingtalk", + "@qwen-code/sdk": "file:../sdk-typescript", "@qwen-code/channel-telegram": "file:../channels/telegram", "@qwen-code/channel-weixin": "file:../channels/weixin", "@qwen-code/qwen-code-core": "file:../core", diff --git a/packages/cli/package.json b/packages/cli/package.json index b6b331b1aed..633c1d9abb5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -46,6 +46,7 @@ "@modelcontextprotocol/sdk": "^1.25.1", "@qwen-code/channel-base": "file:../channels/base", "@qwen-code/channel-dingtalk": "file:../channels/dingtalk", + "@qwen-code/sdk": "file:../sdk-typescript", "@qwen-code/channel-telegram": "file:../channels/telegram", "@qwen-code/channel-weixin": "file:../channels/weixin", "@qwen-code/qwen-code-core": "file:../core", diff --git a/packages/cli/src/commands/daemon-tui.ts b/packages/cli/src/commands/daemon-tui.ts new file mode 100644 index 00000000000..43d9f271d3b --- /dev/null +++ b/packages/cli/src/commands/daemon-tui.ts @@ -0,0 +1,240 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CommandModule } from 'yargs'; +import { createInterface } from 'node:readline/promises'; +import { stdin as input, stdout as output } from 'node:process'; +import { writeStderrLine } from '../utils/stdioHelpers.js'; +import type { + DaemonTuiUpdate, + DaemonTuiSessionClient, +} from '../ui/daemon/DaemonTuiAdapter.js'; + +interface DaemonTuiArgs { + 'daemon-url': string; + token?: string; + workspace?: string; + model?: string; + 'session-id'?: string; + 'session-scope'?: 'single' | 'thread'; + prompt?: string; +} + +function writeLine(line = ''): void { + output.write(`${line}\n`); +} + +function formatHistoryItem(item: unknown): string { + if (!item || typeof item !== 'object') { + return String(item); + } + const record = item as Record; + const type = typeof record['type'] === 'string' ? record['type'] : 'history'; + const text = + typeof record['text'] === 'string' + ? record['text'] + : JSON.stringify(record, null, 2); + return `[${type}] ${text}`; +} + +function printDaemonUpdate(update: DaemonTuiUpdate): void { + switch (update.type) { + case 'history': + writeLine(formatHistoryItem(update.item)); + break; + case 'tool_group_update': + writeLine('[tool]'); + for (const tool of update.item.tools) { + writeLine(` - ${tool.name}: ${tool.status}`); + if (tool.resultDisplay !== undefined) { + writeLine(` ${JSON.stringify(tool.resultDisplay)}`); + } + } + break; + case 'permission_request': + writeLine(`[permission] ${update.requestId}`); + writeLine( + ` tool: ${update.request.toolCall.kind} (${update.request.toolCall.toolCallId})`, + ); + for (const option of update.request.options) { + writeLine(` - ${option.optionId}: ${option.name ?? option.optionId}`); + } + writeLine(` approve with: /approve ${update.requestId} `); + writeLine(` reject with: /reject ${update.requestId}`); + break; + case 'permission_resolved': + writeLine( + `[permission_resolved] ${update.requestId} ${JSON.stringify( + update.outcome, + )}`, + ); + break; + case 'model_switched': + writeLine(`[model] ${update.modelId}`); + break; + case 'disconnected': + writeLine(`[disconnected] ${update.reason}`); + break; + default: { + const neverUpdate: never = update; + writeLine(JSON.stringify(neverUpdate)); + } + } +} + +async function createDaemonTuiSession( + argv: DaemonTuiArgs, +): Promise { + const { DaemonClient, DaemonSessionClient } = await import('@qwen-code/sdk'); + const client = new DaemonClient({ + baseUrl: argv['daemon-url'], + token: argv.token ?? process.env['QWEN_SERVER_TOKEN'], + }); + const workspaceCwd = argv.workspace ?? process.cwd(); + if (argv['session-id']) { + return await DaemonSessionClient.load(client, argv['session-id'], { + workspaceCwd, + }); + } + return await DaemonSessionClient.createOrAttach(client, { + workspaceCwd, + ...(argv.model ? { modelServiceId: argv.model } : {}), + ...(argv['session-scope'] ? { sessionScope: argv['session-scope'] } : {}), + }); +} + +async function runPrompt( + session: DaemonTuiSessionClient, + prompt: string, +): Promise { + const { DaemonTuiAdapter } = await import('../ui/daemon/DaemonTuiAdapter.js'); + const adapter = new DaemonTuiAdapter({ + session, + onUpdate: printDaemonUpdate, + }); + await adapter.start(); + try { + await adapter.sendPrompt(prompt); + } finally { + await adapter.stop(); + } +} + +async function runInteractive(session: DaemonTuiSessionClient): Promise { + const { DaemonTuiAdapter } = await import('../ui/daemon/DaemonTuiAdapter.js'); + const adapter = new DaemonTuiAdapter({ + session, + onUpdate: printDaemonUpdate, + }); + await adapter.start(); + const rl = createInterface({ input, output }); + try { + writeLine( + `Connected to daemon session ${session.sessionId} (${session.workspaceCwd})`, + ); + writeLine( + 'Commands: /quit, /cancel, /model , /approve