docs/design/slash-command/phase2-technical-design.md
supportedModes 扩展到包含 non_interactive 和/或 acpSkillTool 消费 getModelInvocableCommands())action 内部增加 executionMode 判断的方式,不使用 Phase 1 设计文档 §10.2 描述的双注册模式(双注册仅在 interactive 和 non-interactive 逻辑差异极大时才有必要,本阶段命令复杂度不达到该门槛)open())、操作剪贴板(copyToClipboard())等依赖图形环境的操作,在 non-interactive/ACP 路径下必须跳过Phase 1 结束后的架构要点(Phase 2 直接在此基础上扩展):
commandType 字段已从 SlashCommand 接口中删除,所有命令改用显式 supportedModesgetEffectiveSupportedModes() 为两级推断:显式 supportedModes → CommandKind 兜底CommandService.getCommandsForMode(mode) 取代原 ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE 白名单btw、bug、compress、context、init、summary 已在 Phase 1 中扩展到全模式,不在本阶段列表中createNonInteractiveUI() 中各方法均为 no-op:addItem、clear、setDebugMessage、setPendingItem、reloadCommands 均静默忽略调用本阶段共涉及 13 个命令,按实现复杂度分为四类:
| 类别 | 命令 | 变更要点 |
|---|---|---|
| A 类 | export | 只改 supportedModes,action 所有路径已返回合法类型 |
| 仅交互 | plan、statusline | 设计决策:这两个命令语义上与交互界面紧密耦合,保持 supportedModes: ['interactive'] |
| A+ 类 | language | 改 supportedModes + 少量 non-interactive 分支处理 |
| 仅交互 | copy、restore | 设计决策:剥贴板和快照恢复本质上是交互操作,保持 supportedModes: ['interactive'] |
| A' 类 | model、approval-mode | 有参数路径已返回 message,无参数路径需新增 non-interactive 分支(现触发 dialog) |
| B 类 | about、stats、insight、docs、clear | action 所有路径均无返回值或调用 addItem/clear,需新增完整 non-interactive 分支 |
supportedModes这三个命令的所有 action 路径已经返回 message 或 submit_prompt,完全无 UI 依赖,handleCommandResult 可直接处理。
/export(及子命令)当前状态:supportedModes: ['interactive'],所有子命令 action 均返回 MessageActionReturn。
变更:将父命令及所有四个子命令(md、html、json、jsonl)的 supportedModes 改为 ['interactive', 'non_interactive', 'acp']。
ACP 消息内容:action 现有返回内容已包含完整文件路径(如 Session exported to markdown: qwen-export-2024-01-01T12-00-00.md),对 IDE 消费友好,无需修改文本。
注意:
/export父命令本身没有action,只有子命令。将父命令supportedModes改为全模式后,parseSlashCommand能够匹配子命令路由,但若用户只输入/export不带子命令,commandToExecute.action为 undefined,handleSlashCommand返回no_command,调用方会显示可用子命令提示。这是预期行为。
/plan当前状态:supportedModes: ['interactive'],action 所有路径返回 MessageActionReturn 或 SubmitPromptActionReturn。
设计决策:/plan 是引导用户进行多轮交互规划的命令,语义上与交互界面紧密耦合。经讨论决定保持 supportedModes: ['interactive'],不扩展至 non-interactive/acp 模式。
/statusline当前状态:supportedModes: ['interactive'],action 始终返回 SubmitPromptActionReturn(将 subagent 调用 prompt 提交给模型)。
设计决策:/statusline 是触发 subagent 对当前状态进行总结的命令,语义上与交互界面紧密耦合。经讨论决定保持 supportedModes: ['interactive'],不扩展至 non-interactive/acp 模式。
/language当前状态:action 所有路径均返回 MessageActionReturn(读取/设置语言设置)。
需要处理的副���用:setUiLanguage() 内调用 context.ui.reloadCommands(),在非交互 UI 中已是 no-op,无需额外处理。
变更:
ui、output,以及 SUPPORTED_LANGUAGES 动态生成的子命令)的 supportedModes 改为 ['interactive', 'non_interactive', 'acp']。ACP 语义说明:在 non-interactive(单次调用)中执行 /language ui zh-CN 会修改持久化设置(写入 settings 文件),该变更对后续 session 生效,本次 session 内 i18n 也立即生效。这与用户预期一致。
/copy当前状态:action 调用 copyToClipboard(),在 ACP/headless 环境中可能抛出异常或无声失败(clipboard 不可用)。
变更:
supportedModes 改为 ['interactive', 'non_interactive', 'acp']。// 获取 last AI message(现有逻辑,可复用)
if (context.executionMode !== 'interactive') {
// 非交互/ACP:跳过剪贴板,返回内容本身
if (!lastAiOutput) {
return {
type: 'message',
messageType: 'info',
content: 'No output in history.',
};
}
return {
type: 'message',
messageType: 'info',
content: lastAiOutput,
};
}
// interactive 路径:原有剪贴板逻辑不变
await copyToClipboard(lastAiOutput);
return {
type: 'message',
messageType: 'info',
content: 'Last output copied to the clipboard',
};
ACP 语义:IDE 收到最后一条模型输出的原文,可自行决定是否写入剪贴板或展示给用户。
/restore当前状态:supportedModes: ['interactive']。
设计决策:快照恢复进一步会重新执行工具调用,语义上与交互界面紧密耦合。经讨论决定保持 supportedModes: ['interactive'],不扩展至 non-interactive/acp 模式。
ACP 语义:checkpoint 的 git 状态恢复和 gemini client history 设置均作为副作用执行;IDE 收到确认消息后可提示用户"状态已恢复",工具重执行由 IDE 自行决定是否触发。
/model当前状态:
| 输入 | 当前行为 |
|---|---|
/model(无参数) | → { type: 'dialog', dialog: 'model' }(non-interactive 下变 unsupported) |
/model <model-id> | 未实现(只有 --fast 分支) |
/model --fast(无 model name) | → { type: 'dialog', dialog: 'fast-model' }(non-interactive 下变 unsupported) |
/model --fast <model-id> | → MessageActionReturn ✅ |
变更:
supportedModes 改为 ['interactive', 'non_interactive', 'acp']。// 无参数路径(原返回 dialog: 'model')
if (!args.trim()) {
if (context.executionMode !== 'interactive') {
const currentModel = config.getModel() ?? 'unknown';
return {
type: 'message',
messageType: 'info',
content: `Current model: ${currentModel}\nUse "/model <model-id>" to switch models.`,
};
}
return { type: 'dialog', dialog: 'model' };
}
// --fast 无参数路径(原返回 dialog: 'fast-model')
if (args.startsWith('--fast') && !modelName) {
if (context.executionMode !== 'interactive') {
const fastModel = context.services.settings?.merged?.fastModel ?? 'not set';
return {
type: 'message',
messageType: 'info',
content: `Current fast model: ${fastModel}\nUse "/model --fast <model-id>" to set fast model.`,
};
}
return { type: 'dialog', dialog: 'fast-model' };
}
ACP 语义:IDE 展示当前模型名称,供用户参考;切换模型通过带参数调用实现(/model <model-id>)。
注意:
/model <model-id>(不带--fast)目前没有实现设置当前 session 模型的逻辑,只有--fast <model-id>有。如果 Phase 2 要支持 ACP 下切换主模型,需要同步实现/model <model-id>的 set 逻辑。本设计预留此路径但标记为 Phase 2 可选项,优先保证"查看当前模型"的 read-only 路径。
/approval-mode当前状态:
| 输入 | 当前行为 |
|---|---|
/approval-mode(无参数) | → { type: 'dialog', dialog: 'approval-mode' }(non-interactive 下变 unsupported) |
/approval-mode <mode> | → MessageActionReturn ✅ |
/approval-mode <invalid> | → MessageActionReturn(error)✅ |
变更:
supportedModes 改为 ['interactive', 'non_interactive', 'acp']。!args.trim())插入 non-interactive 分支:if (!args.trim()) {
if (context.executionMode !== 'interactive') {
const currentMode = config?.getApprovalMode() ?? 'unknown';
return {
type: 'message',
messageType: 'info',
content: `Current approval mode: ${currentMode}\nAvailable modes: ${APPROVAL_MODES.join(', ')}\nUse "/approval-mode <mode>" to change.`,
};
}
return { type: 'dialog', dialog: 'approval-mode' };
}
这五个命令的 action 在 interactive 模式下通过 context.ui.addItem() 渲染 React 组件或调用 context.ui.clear(),返回值为 void。在 non-interactive 中,这些调用均为 no-op,导致 handleSlashCommand 将无返回值处理为 "Command executed successfully.",无实际内容输出。
实现原则:在 action 顶部检查 executionMode,非 interactive 时 提前 return 包含实际内容的 message,interactive 路径代码完全不触碰。
/about(altName: status)数据来源:getExtendedSystemInfo(context) 返回 ExtendedSystemInfo,包含:cliVersion、osPlatform、osArch、osRelease、nodeVersion、modelVersion、selectedAuthType、ideClient、sessionId、memoryUsage、baseUrl、apiKeyEnvKey、gitCommit、fastModel。所有字段在 non-interactive 中均可获取(context.services.config 和 settings 均已注入)。
变更:
supportedModes 改为 ['interactive', 'non_interactive', 'acp']。getExtendedSystemInfo 调用后,interactive 路径之前插入模式分支:action: async (context) => {
const systemInfo = await getExtendedSystemInfo(context);
if (context.executionMode !== 'interactive') {
const lines = [
`Qwen Code v${systemInfo.cliVersion}`,
`Model: ${systemInfo.modelVersion}`,
`Fast Model: ${systemInfo.fastModel ?? 'not set'}`,
`Auth: ${systemInfo.selectedAuthType}`,
`Platform: ${systemInfo.osPlatform} ${systemInfo.osArch} (${systemInfo.osRelease})`,
`Node.js: ${systemInfo.nodeVersion}`,
`Session: ${systemInfo.sessionId}`,
...(systemInfo.gitCommit ? [`Git commit: ${systemInfo.gitCommit}`] : []),
...(systemInfo.ideClient ? [`IDE: ${systemInfo.ideClient}`] : []),
];
return {
type: 'message',
messageType: 'info',
content: lines.join('\n'),
};
}
// interactive 路径:原有 addItem 逻辑不变
const aboutItem: Omit<HistoryItemAbout, 'id'> = { type: MessageType.ABOUT, systemInfo };
context.ui.addItem(aboutItem, Date.now());
},
/stats(及子命令 model、tools)数据来源:context.session.stats(SessionStatsState)包含 sessionStartTime、metrics(SessionMetrics:models、tools、files)、promptCount。在 non-interactive 中,sessionStartTime 为当前调用时刻,metrics 来自 uiTelemetryService.getMetrics()(本次调用的累积值,通常为零),promptCount 为 1。
变更:
stats 及子命令 model、tools 的 supportedModes 改为 ['interactive', 'non_interactive', 'acp']。// /stats 主命令
action: (context) => {
if (context.executionMode !== 'interactive') {
const now = new Date();
const { sessionStartTime, promptCount, metrics } = context.session.stats;
if (!sessionStartTime) {
return { type: 'message', messageType: 'error', content: 'Session start time unavailable.' };
}
const wallDuration = now.getTime() - sessionStartTime.getTime();
// 汇总所有 model 的 token 数
let totalPromptTokens = 0, totalCandidateTokens = 0, totalRequests = 0;
for (const modelMetrics of Object.values(metrics.models)) {
totalPromptTokens += modelMetrics.tokens.prompt;
totalCandidateTokens += modelMetrics.tokens.candidates;
totalRequests += modelMetrics.api.totalRequests;
}
const lines = [
`Session duration: ${formatDuration(wallDuration)}`,
`Prompts: ${promptCount}`,
`API requests: ${totalRequests}`,
`Tokens — prompt: ${totalPromptTokens}, output: ${totalCandidateTokens}`,
`Tool calls: ${metrics.tools.totalCalls} (${metrics.tools.totalSuccess} ok, ${metrics.tools.totalFail} fail)`,
`Files: +${metrics.files.totalLinesAdded} / -${metrics.files.totalLinesRemoved} lines`,
];
return { type: 'message', messageType: 'info', content: lines.join('\n') };
}
// interactive 路径:原有 addItem 逻辑不变
const statsItem: HistoryItemStats = { type: MessageType.STATS, duration: formatDuration(wallDuration) };
context.ui.addItem(statsItem, Date.now());
},
子命令 model 和 tools 也各自插入模式分支,返回对应维度的文本统计(model 维度按 model name 列出 token 用量;tools 维度列出各 tool 调用次数)。
说明:在 non-interactive 单次调用中,metrics 通常为零(新 session),但结构完整,不影响格式。ACP Session 中可能有累积值,有实际意义。
/insight当前状态:action 返回 void,通过 addItem 展示进度和结果,最后调用 open(outputPath) 打开浏览器。核心逻辑是 insightGenerator.generateStaticInsight() 生成 HTML 文件。
变更:
supportedModes 改为 ['interactive', 'non_interactive', 'acp']。executionMode 三路分叉:
non_interactive:同步生成,忽略进度回调,不开浏览器,直接返回 message(文件路径)acp:异步启动生成,通过 stream_messages 将进度(encodeInsightProgressMessage)和完成(encodeInsightReadyMessage)推送给 IDEinteractive:原有 addItem + setPendingItem + open() 逻辑不变// non_interactive 路径
if (context.executionMode === 'non_interactive') {
const outputPath = await insightGenerator.generateStaticInsight(
projectsDir,
() => {}, // no-op progress
);
return {
type: 'message',
messageType: 'info',
content: t('Insight report generated at: {{path}}', { path: outputPath }),
};
}
// acp 路径:stream_messages
if (context.executionMode === 'acp') {
// ... 构造 streamMessages async generator,yield encodeInsightProgressMessage / encodeInsightReadyMessage ...
return { type: 'stream_messages', messages: streamMessages() };
}
// interactive 路径:原有实现不变
设计理由:non_interactive 模式(CLI 管道)不支持 stream_messages,只能返回单条 message;ACP 模式(IDE 插件)能消费 stream_messages 并实时展示进度,因此为其保留 streaming 路径。
ACP 消息格式:encodeInsightProgressMessage(stage, progress, detail?) 产生 IDE 可解析的进度条消息;encodeInsightReadyMessage(outputPath) 通知 IDE 文件已就绪,由 IDE 决定如何展示链接。
/docs当前状态:action 返回 void,通过 addItem 显示消息并调用 open(docsUrl) 打开浏览器。有一个 SANDBOX 环境变量分支(沙盒下只 addItem,不开浏览器)。
变更:
supportedModes 改为 ['interactive', 'non_interactive', 'acp']。Promise<void | MessageActionReturn>。action: async (context) => {
const langPath = getCurrentLanguage()?.startsWith('zh') ? 'zh' : 'en';
const docsUrl = `https://qwenlm.github.io/qwen-code-docs/${langPath}`;
if (context.executionMode !== 'interactive') {
// 非交互/ACP:直接返回 URL,不打开浏览器,不调用 addItem
return {
type: 'message',
messageType: 'info',
content: `Qwen Code documentation: ${docsUrl}`,
};
}
// interactive 路径:原有 SANDBOX 判断 + addItem + open() 不变
if (process.env['SANDBOX'] && ...) {
context.ui.addItem(...);
} else {
context.ui.addItem(...);
await open(docsUrl);
}
},
/clear(altNames: reset、new)当前状态:action 执行以下操作并返回 void:
config.getHookSystem()?.fireSessionEndEvent() — 触发 hook(有副作用)config.startNewSession() — 开始新 session ID(有副作用)uiTelemetryService.reset() — 重置 telemetry 计数器(有副作用)skillTool.clearLoadedSkills() — 清除 skill 缓存(有副作用)context.ui.clear() — 清空终端 UI(UI 副作用,non-interactive 下为 no-op)geminiClient.resetChat() — 重置 chat 历史(有副作用)config.getHookSystem()?.fireSessionStartEvent() — 触发 hook(有副作用)non-interactive/ACP 语义分析:
ui.clear() 在 non-interactive 中已是 no-op,不需要处理geminiClient.resetChat():在 ACP Session 中是有意义的副作用(清空 chat 历史),应保留;在 non-interactive 单次调用中,每次调用都是全新 session,resetChat 语义重复但无害config.startNewSession():在 ACP 中有意义(开始新的 session ID);在 non-interactive 单次调用中同样语义重复但无害fireSessionEndEvent / fireSessionStartEvent:在 ACP 中有意义(触发 hook)决策:non-interactive/ACP 路径保留所有有意义的副作用(resetChat、startNewSession、hook events),仅跳过 ui.clear()(已是 no-op)并返回上下文边界标记 message。
变更:
supportedModes 改为 ['interactive', 'non_interactive', 'acp']。Promise<void | MessageActionReturn>。context.ui.clear() 调用后(或替代它)根据模式分支:action: async (context, _args) => {
const { config } = context.services;
if (config) {
config.getHookSystem()?.fireSessionEndEvent(SessionEndReason.Clear).catch(...);
const newSessionId = config.startNewSession();
uiTelemetryService.reset();
const skillTool = config.getToolRegistry()?.getAllTools().find(...);
if (skillTool instanceof SkillTool) skillTool.clearLoadedSkills();
if (newSessionId && context.session.startNewSession) {
context.session.startNewSession(newSessionId);
}
// ui.clear() 在非交互下已是 no-op,但依然调用(不需要条件分支)
context.ui.clear();
const geminiClient = config.getGeminiClient();
if (geminiClient) {
await geminiClient.resetChat();
}
config.getHookSystem()?.fireSessionStartEvent(...).catch(...);
} else {
context.ui.clear();
}
// 根据模式决定返回值
if (context.executionMode !== 'interactive') {
return {
type: 'message',
messageType: 'info',
content: 'Context cleared. Previous messages are no longer in context.',
};
}
// interactive 路径:void(不返回,React UI 由 ui.clear() 驱动更新)
},
ACP 语义:IDE 收到上下文边界标记后,可将其作为 session 分隔符展示(如"新会话开始"提示),并清空本地 chat 历史缓存。
handleCommandResult 变更结论:无需修改。
Phase 2 所有命令变更后,non-interactive/ACP 路径的返回类型均为 message 或 submit_prompt,均已在 handleCommandResult 的 switch 中正确处理。
createNonInteractiveUI() 变更结论:无需修改。
当前 no-op 实现已足够。addItem、clear、setPendingItem 等 no-op 在 B 类命令的 non-interactive 路径中不会被调用(因为提前 return);interactive 路径中不受影响。
Phase 1 中 CommandService.getModelInvocableCommands() 已实现,BundledSkillLoader、FileCommandLoader(用户/项目命令)、McpPromptLoader 已设置 modelInvocable: true。
Phase 2.2 的工作是将 SkillTool 从只消费 SkillManager.listSkills() 改为同时消费 CommandService.getModelInvocableCommands(),统一模型可调用命令的入口。
变更文件:packages/core/src/tools/SkillTool.ts(或对应路径)
具体变更:
SkillTool 在初始化时接收 CommandService(或其 getModelInvocableCommands() 的结果)作为依赖注入listSkills() 和 getModelInvocableCommands() 的结果modelInvocable: false)不出现在 tool description 中注:
SkillTool的具体实现依赖packages/core内部架构,详细设计在本文档中仅描述接口变更,实现细节需结合 core 包的现有结构确定。
在 InputPrompt 组件中检测光标附近的 slash token(不限于行首),触发补全菜单。
检测规则:
/ 开头、不含空格的 token 时,触发命令补全getCommandsForMode('interactive') 的可见命令列表本功能为 UI 层变更,属于 Phase 2.3 独立子任务,不影响其他 Phase 2.1/2.2 的实施。
| 文件 | 变更类型 | 具体内容 |
|---|---|---|
exportCommand.ts | A 类 | 父命令 + 4 个子命令:supportedModes → all modes |
planCommand.ts | 仅交互 | 设计决策:保持 supportedModes: ['interactive'],未变更 |
statuslineCommand.ts | 仅交互 | 设计决策:保持 supportedModes: ['interactive'],未变更 |
languageCommand.ts | A+ 类 | 父命令 + ui/output 子命令 + 动态 language 子命令:supportedModes → all modes |
copyCommand.ts | 仅交互 | 设计决策:保持 supportedModes: ['interactive'],未变更 |
restoreCommand.ts | 仅交互 | 设计决策:保持 supportedModes: ['interactive'],未变更 |
modelCommand.ts | A' 类 | supportedModes → all modes + 无参数/无 fast model 路径新增非交互分支 |
approvalModeCommand.ts | A' 类 | supportedModes → all modes + 无参数路径新增非交互分支 |
aboutCommand.ts | B 类 | supportedModes → all modes + 非交互路径返回 message(版本/模型/环境摘要) |
statsCommand.ts | B 类 | supportedModes → all modes + 非交互路径返回 message(stats 文本);子命令同步处理 |
insightCommand.ts | B 类 | supportedModes → all modes + non_interactive 路径同步生成返回 message(文件路径);acp 路径返回 stream_messages 带进度推送 |
docsCommand.ts | B 类 | supportedModes → all modes + 非交互路径返回 message(文档 URL),不打开浏览器 |
clearCommand.ts | B 类 | supportedModes → all modes + action 末尾根据模式返回 message 或 void |
| 文件 | 变更内容 |
|---|---|
packages/core/src/tools/SkillTool.ts | Phase 2.2:接入 getModelInvocableCommands()(详细设计另行确定) |
packages/cli/src/ui/InputPrompt.tsx(或同等组件) | Phase 2.3:mid-input slash 检测逻辑 |
packages/cli/src/nonInteractiveCliCommands.ts(handleCommandResult、handleSlashCommand 无需修改)packages/cli/src/ui/noninteractive/nonInteractiveUi.ts(stub UI 无需修改)packages/cli/src/services/commandUtils.ts(filterCommandsForMode、getEffectiveSupportedModes 无需修改)packages/cli/src/services/CommandService.ts(getCommandsForMode、getModelInvocableCommands 已在 Phase 1 实现)为每个变更的命令在同目录下新增或更新测试文件(*.test.ts),覆盖以下 case:
A/A+ 类命令(export、language):
supportedModes 正确包含 non_interactive 和 acpexecutionMode: 'non_interactive' 下,action 返回 MessageActionReturn 或 SubmitPromptActionReturn,不调用 ui.addItem 或 ui.clear仅交互命令(plan、statusline、copy、restore):
supportedModes 为 ['interactive'],这是设计决策unsupportedA' 类命令(model、approval-mode):
executionMode: 'non_interactive' → 返回当前状态 message,不返回 dialogexecutionMode: 'non_interactive' → 原有 message 逻辑正常执行dialog,有参数 → message(不变)B 类命令(about、stats、insight、docs、clear):
executionMode: 'non_interactive' 下,action 返回 MessageActionReturn,不调用任何 ui.* 方法content 字符串包含预期的关键字段(版本号、模型名、URL 等)ui.addItem 被调用,action 返回 void(不变)clear 的特殊 case:
executionMode: 'non_interactive' 下,geminiClient.resetChat() 仍被调用(副作用保留)message,内容为 'Context cleared. Previous messages are no longer in context.'handleSlashCommand)在 nonInteractiveCli.test.ts 或新建的集成测试文件中:
handleSlashCommand('/about', ...) 在 non-interactive 模式下返回 { type: 'message', content: 包含版本号 }handleSlashCommand('/stats', ...) 在 non-interactive 模式下返回 { type: 'message', content: 包含 'Session duration' }handleSlashCommand('/docs', ...) 在 non-interactive 模式下返回 { type: 'message', content: 包含 'qwenlm.github.io' }handleSlashCommand('/clear', ...) 在 non-interactive 模式下返回 { type: 'message', content: 'Context cleared.' }handleSlashCommand('/plan', ...) 在 non-interactive 模式下返回 unsupported(仅交互命令)btw、bug 等)行为无退化commandUtils 测试commandUtils.test.ts 中新增(或已有的测试继续覆盖):
export、language 等)均能通过 filterCommandsForMode(commands, 'non_interactive') 和 filterCommandsForMode(commands, 'acp') 的过滤plan、statusline、copy、restore)在 filterCommandsForMode(commands, 'non_interactive') 下被正确过滤掉| 场景 | Phase 2 前行为 | Phase 2 后行为 | 性质 |
|---|---|---|---|
non-interactive 下执行 /export md | ❌ unsupported(被过滤) | ✅ 返回文件路径 message | 能力扩展 |
non-interactive 下执行 /plan <task> | ❌ unsupported | ❌ unsupported(设计决策:仅交互) | 不变 |
non-interactive 下执行 /statusline | ❌ unsupported | ❌ unsupported(设计决策:仅交互) | 不变 |
non-interactive 下执行 /language ui zh-CN | ❌ unsupported | ✅ 设置语言,返回确认 message | 能力扩展 |
non-interactive 下执行 /copy | ❌ unsupported | ❌ unsupported(设计决策:仅交互) | 不变 |
non-interactive 下执行 /restore(无参数) | ❌ unsupported | ❌ unsupported(设计决策:仅交互) | 不变 |
non-interactive 下执行 /restore <id> | ❌ unsupported | ❌ unsupported(设计决策:仅交互) | 不变 |
non-interactive 下执行 /model | ❌ unsupported(dialog) | ✅ 返回当前模型名称 | 能力扩展 |
non-interactive 下执行 /model <id> | ❌ unsupported | 🔄 Phase 2 可选:实现切换逻辑 | 能力扩展(可选) |
non-interactive 下执行 /approval-mode | ❌ unsupported(dialog) | ✅ 返回当前审批模式 | 能力扩展 |
non-interactive 下执行 /approval-mode yolo | ❌ unsupported | ✅ 设置模式,返回确认 | 能力扩展 |
non-interactive 下执行 /about | ❌ 返回 "Command executed successfully."(addItem no-op) | ✅ 返回版本/模型/环境摘要 | Bug fix + 能力扩展 |
non-interactive 下执行 /stats | ❌ 返回 "Command executed successfully." | ✅ 返回 session 统计文本 | Bug fix + 能力扩展 |
non-interactive 下执行 /insight | ❌ 返回 "Command executed successfully."(生成但无输出) | ✅ 生成并返回文件路径 | Bug fix + 能力扩展 |
non-interactive 下执行 /docs | ❌ 返回 "Command executed successfully." | ✅ 返回文档 URL | Bug fix + 能力扩展 |
non-interactive 下执行 /clear | ❌ 返回 "Command executed successfully." | ✅ 返回上下文边界 message | Bug fix + 能力扩展 |
| interactive 下执行任意以上命令 | ✅ 原有行为 | ✅ 原有行为(零退化) | 不变 |
建议按以下顺序实施,每组可独立 commit 和 review:
Batch 1(~30min):A 类 — 只改 supportedModes
修改 exportCommand.ts(及其子命令),验证测试通过。
Batch 2(~45min):A+ 类 — 少量分支
修改 languageCommand.ts,为有副作用的路径添加非交互分支,更新对应测试。(copyCommand.ts 和 restoreCommand.ts 经讨论保持仅交互。)
Batch 3(~45min):A' 类 — dialog 路径
修改 modelCommand.ts、approvalModeCommand.ts,为无参数路径添加非交互分支,更新对应测试。
Batch 4(~1.5h):B 类 — 完整分支
修改 aboutCommand.ts、statsCommand.ts(含子命令)、docsCommand.ts。
Batch 5(~1h):B 类特殊 — insightCommand.ts、clearCommand.ts
这两个命令副作用较多,单独一个 commit,更新对应测试和集成测试。
Batch 6(~2h):Phase 2.2 — prompt command 模型调用打通
修改 SkillTool,接入 getModelInvocableCommands(),更新 SkillTool 测试。
Batch 7(~2h):Phase 2.3 — mid-input slash 检测
修改 InputPrompt 组件,新增补全触发逻辑和 UI 测试。
Batch 8(~30min):全量测试 + 类型检查
运行 npm run typecheck、cd packages/cli && npx vitest run,修复剩余问题。
Phase 2.1 命令扩展
/export(及子命令)、/plan、/statusline 在 non-interactive 和 acp 模式下可正常执行并返回有意义输出/language(及子命令)在 non-interactive 下正常执行,设置持久化/copy 在 non-interactive/acp 下返回最后 AI 输出文本(不操作剪贴板)/restore 无参数时在 non-interactive 下返回 checkpoint 列表;有参数时恢复状态并返回确认 message(不返回 type: 'tool')/model 无参数时在 non-interactive/acp 下返回当前模型名(不触发 dialog);/model --fast <id> 正常设置/approval-mode 无参数时在 non-interactive/acp 下返回当前模式(不触发 dialog);有参数时正常设置/about 在 non-interactive/acp 下返回包含版本号、模型名的纯文本摘要/stats(含子命令)在 non-interactive/acp 下返回纯文本统计数据/insight 在 non-interactive/acp 下生成 insight 文件并返回文件路径(不打开浏览器)/docs 在 non-interactive/acp 下返回文档 URL(不打开浏览器)/clear 在 non-interactive/acp 下返回上下文边界标记 message,geminiClient.resetChat() 正常执行npm run typecheck)npm run lint 无新增错误cd packages/cli && npx vitest run)Phase 2.2 模型调用
SkillTool 调用 bundled skill、file command(用户/项目)、MCP promptSkillTool 的 tool description 包含所有 modelInvocable: true 命令的名称和 descriptionPhase 2.3 mid-input slash
/ 后触发命令补全菜单(不限行首)