docs/design/slash-command/phase3-technical-design.md
Phase 3 在 Phase 1/2 已落地的命令元数据、跨模式过滤和 prompt command 模型调用基础上,补齐用户可感知的 slash command 体验:
/help 从当前不可用的命令堆砌重构为 Claude Code 风格的分 tab、清晰、美观的帮助面板available_commands_update 的命令元数据/doctor 不重复实现;/release-notes 不纳入本阶段SlashCommand、CommandService、handleSlashCommand、useSlashCompletion 和 Help 组件,不新建 CommandDescriptor / CommandExecutor / ModeAdapter。commandType:当前实现已删除 Phase 1 早期设计中的 commandType 字段,Phase 3 不重新引入该字段。availableCommands[].name、description、input 三个已有字段保持不变;新增元数据放在兼容字段或 _meta 中,避免破坏已有 ACP 客户端。packages/cli/src/ui/commands/types.ts 当前 SlashCommand 已包含:
source?: CommandSourcesourceLabel?: stringsupportedModes?: ExecutionMode[]userInvocable?: booleanmodelInvocable?: booleanargumentHint?: stringwhenToUse?: stringexamples?: string[]CommandSource 当前支持:
export type CommandSource =
| 'builtin-command'
| 'bundled-skill'
| 'skill-dir-command'
| 'plugin-command'
| 'mcp-prompt';
各 Loader 当前已填充的展示信息:
| Loader | source | sourceLabel | argumentHint | modelInvocable |
|---|---|---|---|---|
BuiltinCommandLoader | builtin-command | Built-in | 多数未声明 | false |
BundledSkillLoader | bundled-skill | Skill | 来自 skill | !disableModelInvocation |
FileCommandLoader / command-factory | skill-dir-command / plugin-command | Custom / Plugin: <extensionName> | 来自 frontmatter | 用户/项目默认 true;插件需 description/whenToUse |
SkillCommandLoader | skill-dir-command / plugin-command | User / Project / Extension: <name> | 来自 skill | 用户/项目默认 true;插件需 description/whenToUse |
McpPromptLoader | mcp-prompt | MCP: <serverName> | 未生成 | 当前未显式设置 modelInvocable |
注意:Phase 1 路线图曾要求 MCP prompt
modelInvocable: true,但当前实现没有显式设置。Phase 3 不改变 MCP prompt 的模型调用路径;MCP prompt 仍通过 MCP 原生机制调用,不通过SkillTool中转。
| 能力 | 当前状态 | 关键文件 |
|---|---|---|
| mid-input slash 基础 ghost text | 已部分实现,仅对 modelInvocable 命令做前缀补全 | ui/utils/commandUtils.ts、ui/hooks/useCommandCompletion.tsx |
| line-start 命令 argument ghost text | 已部分实现,命令完全匹配且无 args 时展示 argumentHint | ui/hooks/useCommandCompletion.tsx |
| alias 参与匹配 | 已实现匹配与排序,但展示总是显示全部 alias,不区分命中 alias | ui/hooks/useSlashCompletion.ts |
| source badge | 仅 MCP 展示 [MCP] | ui/components/SuggestionsDisplay.tsx、ui/components/Help.tsx |
/help | 当前实现视为未完成:虽有分组尝试,但仍是命令堆砌,不具备 Claude Code 风格的分 tab、清晰可读帮助面板体验 | ui/components/Help.tsx |
ACP argumentHint | 已映射到 availableCommands[].input.hint | acp-integration/session/Session.ts |
| ACP source/supportedModes/subcommands/modelInvocable | 未暴露 | acp-integration/session/Session.ts |
| 冲突处理 | extension 命令冲突时已重命名为 extensionName.commandName,非 extension 同名为后加载覆盖前加载 | services/CommandService.ts |
/doctor | 已实现,支持 interactive / non_interactive / acp | ui/commands/doctorCommand.ts、utils/doctorChecks.ts |
参考 /Users/mochi/code/claude-code 源码:
src/types/command.ts:命令模型包含 argumentHint、whenToUse、aliases、loadedFrom、kind、immediate、isSensitive、userFacingName、supportsNonInteractive 等展示/能力字段。src/utils/suggestions/commandSuggestions.ts:补全排序同时考虑精确命中、alias 命中、prefix、fuzzy、skill usage;alias 命中时只展示用户实际命中的 alias。src/utils/suggestions/commandSuggestions.ts:mid-input slash 使用 findMidInputSlashCommand()、getBestCommandMatch() 和 findSlashCommandPositions() 支持 ghost text 与高亮。src/components/HelpV2/Commands.tsx:Help V2 是可浏览的命令目录,展示描述时会附带来源信息。src/commands.ts:Claude Code 内置 /doctor、/release-notes 等命令,Qwen Code 当前已实现 /doctor;本阶段不实现 /release-notes。Phase 3 采用“体验对齐,不复制架构”的方式借鉴上述点。
| 文件 | 变更内容 |
|---|---|
packages/cli/src/ui/components/SuggestionsDisplay.tsx | 扩展 Suggestion 类型,展示 source badge、argumentHint、aliasHit |
packages/cli/src/ui/hooks/useSlashCompletion.ts | 生成增强补全项;排序接入 recently used;保留 alias 命中信息 |
packages/cli/src/ui/hooks/useCommandCompletion.tsx | mid-input ghost text 复用增强匹配;输出 argument/source 元数据供 UI 展示 |
packages/cli/src/ui/utils/commandUtils.ts | 增加 slash token 高亮辅助函数,或扩展现有函数返回命令有效性 |
packages/cli/src/ui/components/InputPrompt.tsx | 渲染有效 slash command token 高亮;保留 Tab 接受 ghost text |
packages/cli/src/ui/components/Help.tsx | 重构为 Claude Code 风格的分 tab 帮助面板,避免命令堆砌 |
packages/cli/src/ui/commands/helpCommand.ts | 如需 non-interactive/acp 帮助文本,扩展 action;否则仅保持 interactive UI |
packages/cli/src/acp-integration/session/Session.ts | 在 ACP update 中暴露增强元数据 |
packages/cli/src/ui/commands/*Command.ts | 为常用 built-in 命令补充 argumentHint |
建议新增 packages/cli/src/services/commandMetadata.ts,集中处理 Help、Completion、ACP 共同需要的展示逻辑:
export function getCommandSourceBadge(cmd: SlashCommand): string | null;
export function getCommandSourceGroup(cmd: SlashCommand): CommandSourceGroup;
export function formatSupportedModes(cmd: SlashCommand): string;
export function getCommandDisplayName(cmd: SlashCommand): string;
export function getCommandSubcommandNames(cmd: SlashCommand): string[];
不建议把这些展示函数放入 Loader,避免 Loader 承担 UI 逻辑。
Suggestion 数据结构当前:
export interface Suggestion {
label: string;
value: string;
description?: string;
matchedIndex?: number;
commandKind?: CommandKind;
}
建议扩展为:
export interface Suggestion {
label: string;
value: string;
description?: string;
matchedIndex?: number;
commandKind?: CommandKind;
// Phase 3
source?: CommandSource;
sourceLabel?: string;
sourceBadge?: string;
argumentHint?: string;
matchedAlias?: string;
supportedModes?: ExecutionMode[];
modelInvocable?: boolean;
}
mode !== 'slash' 的文件补全、reverse search 不需要填充这些字段。
当前 SuggestionsDisplay 只对 CommandKind.MCP_PROMPT 追加 [MCP]。Phase 3 改为使用 source / sourceLabel 统一生成 badge:
| source / sourceLabel | badge |
|---|---|
builtin-command | [Built-in](可选:默认不展示,降低噪音) |
bundled-skill / Skill | [Skill] |
skill-dir-command / User | [User] |
skill-dir-command / Project | [Project] |
skill-dir-command / Custom | [Custom] |
plugin-command / Plugin: x | [Plugin] 或 [Plugin: x] |
plugin-command / Extension: x | [Extension] 或 [Extension: x] |
mcp-prompt | [MCP] |
推荐实现:
function getCommandSourceBadge(cmd: SlashCommand): string | null {
switch (cmd.source) {
case 'bundled-skill':
return '[Skill]';
case 'skill-dir-command':
return cmd.sourceLabel === 'User'
? '[User]'
: cmd.sourceLabel === 'Project'
? '[Project]'
: '[Custom]';
case 'plugin-command':
return '[Plugin]';
case 'mcp-prompt':
return '[MCP]';
case 'builtin-command':
default:
return null;
}
}
是否展示
[Built-in]由 UI 可读性决定。Help 中必须展示 Built-in 分组;补全菜单中可以省略 built-in badge,只对非内置来源展示 badge。
补全菜单中命令名后追加灰色 argumentHint:
/model <model-id> Switch model
/export md|html|json|jsonl Export current session
/review [pr-number] [--comment] [Skill] Review changed code
实现建议:
useSlashCompletion 在 finalSuggestions 中填充 argumentHint: cmd.argumentHintSuggestionsDisplay 在 label 后以 theme.text.secondary 渲染 argumentHintcommandColumnWidth 计算包含 label + hint + badge,避免描述列错位argumentHint需要先为常用 built-in 命令补充 argumentHint。建议首批:
| 命令 | argumentHint |
| ---------------- | ----------------------- | ------------------ | -------- | ------------- | ------- |
| /model | [--fast] [<model-id>] |
| /approval-mode | <mode> |
| /language | ui | output <language> |
| /export | md | html | json | jsonl [path] |
| /memory | show | add | refresh |
| /mcp | desc | nodesc | schema | auth | noauth |
| /stats | [model | tools] |
| /docs | 空或不设置 |
| /doctor | 空或不设置 |
在 useSlashCommandProcessor 或 AppContainer 中维护 session 级最近使用状态:
type RecentSlashCommand = {
name: string;
usedAt: number;
count: number;
};
建议以 Map<string, RecentSlashCommand> 存储,key 使用最终命令名(即冲突处理后的 cmd.name)。
在 useSlashCommandProcessor.handleSlashCommand 成功解析到 commandToExecute 后记录使用:
commandToExecute.name 记录当前 compareRankedCommandMatches() 排序顺序是:
Phase 3 插入 recentScore:
return (
right.matchStrength - left.matchStrength ||
right.completionPriority - left.completionPriority ||
right.recentScore - left.recentScore ||
right.score - left.score ||
left.start - right.start ||
left.itemLength - right.itemLength ||
left.originalIndex - right.originalIndex
);
recentScore 建议:
const RECENT_DECAY_MS = 10 * 60 * 1000;
const recentScore = count * 10 + Math.max(0, 10 - ageMs / RECENT_DECAY_MS);
当 query 为空(用户只输入 /)时,recently used 命令置顶;当 query 非空时,只在同等匹配强度下加权,避免近期命令压过明显更精确的命令。
当前 alias 已参与 AsyncFzf 和 prefix fallback,但 formatSlashCommandLabel() 总是显示所有 alias:
help (?)
compress (summarize)
Phase 3 改为:
help (alias: ?)Suggestion.matchedAlias 由匹配阶段写入实现要点:
function findMatchedAlias(
cmd: SlashCommand,
query: string,
): string | undefined {
return cmd.altNames?.find((alt) =>
alt.toLowerCase().startsWith(query.toLowerCase()),
);
}
在 FZF 结果中,如果 result.item 来自 altNames,可直接将其作为 matchedAlias;prefix fallback 中同理。
当前 findMidInputSlashCommand() 仅识别“由空白分隔的 /xxx token”,且要求 cursor 位于 token 末尾;getBestSlashCommandMatch() 只在 modelInvocable 命令中做字母序 prefix 匹配。
这符合 Phase 2 基础版目标,但 Phase 3 需要补齐展示与高亮。
保留当前策略:mid-input slash 只提示 modelInvocable 命令,因为正文中的内置命令不会作为 slash command 执行。
增强点:
useSlashCompletion 的排序规则(至少考虑 completionPriority 和 recently used)export type BestSlashCommandMatch = {
suffix: string;
fullCommand: string;
command: SlashCommand;
sourceBadge?: string;
argumentHint?: string;
};
由于 ghost text 位置空间有限,不建议把 badge 和 hint 直接塞入 ghost text 主体。建议展示规则:
please /rev 显示 iewargumentHint 时,在 cursor 后显示淡色参数提示,例如 /review [pr-number] [--comment]借鉴 Claude Code findSlashCommandPositions(),在 InputPrompt.renderLineWithHighlighting() 中对正文里的有效 slash command token 着色。
建议新增工具函数:
export type SlashCommandToken = {
start: number;
end: number;
commandName: string;
valid: boolean;
};
export function findSlashCommandTokens(
text: string,
commands: readonly SlashCommand[],
): SlashCommandToken[];
规则:
/[a-zA-Z][a-zA-Z0-9:_-]*modelInvocable 命令为 valid/usr/bin 误标为命令Help.tsx 当前输出:
Commands:[MCP] 说明问题:
argumentHintsupportedModesmodelInvocable按 source / sourceLabel 分组:
source === 'builtin-command'source === 'bundled-skill'source === 'skill-dir-command',包含 Custom / User / Projectsource === 'plugin-command',包含 Plugin:* / Extension:*source === 'mcp-prompt'每组内部按命令名排序;hidden 命令不展示。
格式建议:
/model [--fast] [<model-id>] Switch model
source: Built-in modes: interactive, non_interactive, acp
/review [pr-number] [--comment] Review changed code
source: Skill modes: interactive, non_interactive, acp model: yes
为避免 Help 过宽,建议压缩为单行:
/review [pr-number] [--comment] [Skill] [all] [model] - Review changed code
mode badge 建议:
| supportedModes | badge |
|---|---|
interactive only | [interactive] |
interactive, non_interactive, acp | [all] |
non_interactive, acp | [headless] |
| 其他组合 | [i] [ni] [acp] |
/help 是否扩展到 headless路线图只要求 /help 输出按来源分组,没有明确要求 non-interactive/acp。当前 /help 是 supportedModes: ['interactive']。
Phase 3 建议新增 headless 路径,但作为独立子任务:
supportedModes 改为 all modesHistoryItemHelpmessage如果 scope 需要收敛,可先只重构 interactive Help 组件,headless /help 延后。
Session.sendAvailableCommandsUpdate() 当前将 SlashCommand[] 映射为:
{
name: cmd.name,
description: cmd.description,
input: cmd.argumentHint ? { hint: cmd.argumentHint } : null,
}
其中 argumentHint 已通过 input.hint 暴露。
ACP protocol 的 AvailableCommand 类型如果不能直接增加字段,使用 _meta 保持兼容:
const availableCommands: AvailableCommand[] = slashCommands.map((cmd) => ({
name: cmd.name,
description: cmd.description,
input: cmd.argumentHint ? { hint: cmd.argumentHint } : null,
_meta: {
argumentHint: cmd.argumentHint,
source: cmd.source,
sourceLabel: cmd.sourceLabel,
supportedModes: cmd.supportedModes ?? getEffectiveSupportedModes(cmd),
subcommands: cmd.subCommands
?.filter((sub) => !sub.hidden)
.map((sub) => sub.name),
modelInvocable: cmd.modelInvocable === true,
},
}));
如果 AvailableCommand 类型允许扩展字段,则优先输出为一等字段:
{
name,
description,
input,
argumentHint,
source,
supportedModes,
subcommands,
modelInvocable,
}
但仍建议保留 _meta 镜像一段时间,便于旧客户端渐进迁移。
验收标准只要求 subcommands 名称列表。首期输出一级子命令即可:
subcommands: cmd.subCommands?.map((sub) => sub.name) ?? [];
后续如果 ACP 客户端需要多级树,可扩展为:
type AcpSubcommandMeta = {
name: string;
description?: string;
argumentHint?: string;
subcommands?: AcpSubcommandMeta[];
};
/doctor:已实现,不重复实现当前 doctorCommand 已存在:
packages/cli/src/ui/commands/doctorCommand.tsBuiltinCommandLoader['interactive', 'non_interactive', 'acp']HistoryItemDoctormessagepackages/cli/src/utils/doctorChecks.tsPhase 3 只需在 Help 和补全中为 /doctor 正确展示来源、mode;如需优化,可将 headless JSON 改为更适合人读的 Markdown,但这不是必需项。
/release-notes:不纳入本阶段/release-notes 不再作为 Phase 3 需求。本阶段不新增命令、不注册 built-in、不编写相关测试,避免引入无明确产品需求的命令表面。
当前 CommandService 冲突策略:
extensionName.commandNameextensionName.commandName1Phase 3 不改变执行语义,只在 Help/Completion 中清晰展示最终名称和来源。
建议补充测试确保:
[Plugin] badge路线图中“built-in > bundled/skill-dir > plugin > mcp”的优先级,与当前实现“非 extension 后加载覆盖前加载”不完全一致。Phase 3 文档以当前
CommandService源码为准,不在本阶段改冲突语义;如需严格调整优先级,应作为单独 Phase 处理,避免改变已有用户/项目命令覆盖行为。
更新或新增:
packages/cli/src/ui/hooks/useSlashCompletion.test.tspackages/cli/src/ui/hooks/useCommandCompletion.test.tspackages/cli/src/ui/components/SuggestionsDisplay.test.tsx(如当前无文件则新增)覆盖:
/ 时近期命令排在前面;输入明确 query 时精确命中优先? 展示 help (alias: ?),输入 he 不展示 alias 命中提示/rev 提示 modelInvocable /review 后缀/sta 不提示 /stats(除非未来设计允许内嵌 built-in 执行)更新:packages/cli/src/ui/components/Help.test.tsx
覆盖:
argumentHint、source badge、mode badge、model badge 正确出现更新:packages/cli/src/acp-integration/session/Session.test.ts
覆盖:
availableCommands[].input.hint 保持现有行为argumentHint、source、sourceLabel、supportedModes、subcommands、modelInvocableargumentHint 的命令 input: null 保持兼容getAvailableCommands(config, signal, 'acp') 调用保持不变本阶段不新增 /release-notes 或其他 built-in 命令,因此不需要新增命令测试。仅保留 /doctor 既有回归测试。
Phase 3 同时修改 TUI 补全、slash command 执行、ACP command metadata,单元测试不能覆盖完整用户路径。E2E 验证分三类进行:
npm run build && npm run bundle,后续使用 node dist/cli.js 验证本地实现。available_commands_update 元数据。npm run build && npm run bundle
Interactive 场景建议使用独立临时目录,避免污染当前仓库:
tmux new-session -d -s qwen-slash-phase3 -x 200 -y 50 \
"cd /tmp/qwen-slash-phase3 && /Users/mochi/code/qwen-code-test/dist/cli.js --approval-mode yolo"
sleep 3
发送输入时拆分文本和回车,避免 TUI 吞掉提交:
tmux send-keys -t qwen-slash-phase3 "/help"
sleep 0.5
tmux send-keys -t qwen-slash-phase3 Enter
捕获输出:
tmux capture-pane -t qwen-slash-phase3 -p -S -100
清理:
tmux kill-session -t qwen-slash-phase3
| 场景 | 模式 | 步骤 | 预期结果 |
|---|---|---|---|
| 补全 source badge | interactive/tmux | 输入 /,观察补全菜单 | skill/custom/plugin/MCP 命令展示对应 source badge;built-in 可不展示 badge |
| 补全 argument hint | interactive/tmux | 输入 /model、/export | 命令名后展示 argumentHint;无参数命令不展示噪声 hint |
| recently used 排序 | interactive/tmux | 先执行 /help,再输入 / | /help 在同等匹配条件下优先出现;精确 query 仍优先匹配 query |
| alias 命中展示 | interactive/tmux | 输入 /? | 补全项展示 help (alias: ?);输入 /he 时不误显示 alias 命中 |
| mid-input ghost text | interactive/tmux | 在正文中输入 please /rev | 出现 /review 的 ghost text 后缀,Tab 可接受 |
| mid-input token 高亮 | interactive/tmux | 输入包含 /review 的正文 | 有效 model-invocable slash token 使用命令高亮;路径如 /usr/bin 不被高亮为命令 |
| Help 分组目录 | interactive/tmux | 执行 /help | 输出包含 Built-in Commands、Bundled Skills、Custom Commands、Plugin Commands、MCP Commands 分组;每条命令展示 source/mode/hint |
/doctor headless 回归 | headless/json | 执行 node dist/cli.js "/doctor" --approval-mode yolo --output-format json 2>/dev/null | 返回 message,不触发 TUI-only 组件错误 |
| ACP metadata | integration | 运行 ACP session 并触发 available_commands_update | 每个 command 保留 name、description、input.hint,并包含 argumentHint、source、supportedModes、subcommands、modelInvocable |
/release-notes 不纳入本阶段;headless 回归仅保留 /doctor 等既有命令验证。
按 AGENTS.md,优先运行单文件测试:
cd packages/cli && npx vitest run src/ui/hooks/useSlashCompletion.test.ts
cd packages/cli && npx vitest run src/ui/hooks/useCommandCompletion.test.ts
cd packages/cli && npx vitest run src/ui/components/Help.test.tsx
cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts
最终验证:
npm run build && npm run typecheck
npm run build && npm run bundle
[MCP]、[Skill]、[Custom]、[Plugin])argumentHint/ 时优先出现alias: <alias>,非 alias 命中不噪声展示/review 这类 model-invocable 命令时 ghost text 正确提示/help 按来源分组展示命令argumentHint、description、source、supportedModes 标记available_commands_update 继续包含 name、description、input.hintargumentHint、source、supportedModes、subcommands、modelInvocable/doctor 仍可用,且 non-interactive 返回 message/release-notes,文档、测试和验收标准中均不再要求该命令以下内容不纳入 Phase 3:
SkillTool 的模型调用协议Suggestion 和 SuggestionsDisplay,风险低、反馈直观。argumentHint:让已有 ghost text 和 ACP input.hint 立即受益。useSlashCompletion 引入 recent score,补测试。matchedAlias。Session.sendAvailableCommandsUpdate(),保持 _meta 兼容。