* refactor(cli): replace slash command whitelist with capability-based filtering (Phase 1)
## Summary
Replace the hardcoded ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE whitelist with a
unified, capability-based command metadata model. This is Phase 1 of the slash
command architecture refactor described in docs/design/slash-command/.
## Key changes
### New types (types.ts)
- Add ExecutionMode ('interactive' | 'non_interactive' | 'acp')
- Add CommandSource ('builtin-command' | 'bundled-skill' | 'skill-dir-command' |
'plugin-command' | 'mcp-prompt')
- Add CommandType ('prompt' | 'local' | 'local-jsx')
- Extend SlashCommand interface with: source, sourceLabel, commandType,
supportedModes, userInvocable, modelInvocable, argumentHint, whenToUse,
examples (all optional, backward-compatible)
### New module (commandUtils.ts + commandUtils.test.ts)
- getEffectiveSupportedModes(): 3-priority inference
(explicit supportedModes > commandType > CommandKind fallback)
- filterCommandsForMode(): replaces filterCommandsForNonInteractive()
- 18 unit tests
### Whitelist removal (nonInteractiveCliCommands.ts)
- Remove ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE constant
- Remove filterCommandsForNonInteractive() function
- Replace with CommandService.getCommandsForMode(mode)
### CommandService enhancements (CommandService.ts)
- Add getCommandsForMode(mode: ExecutionMode): filters by mode, excludes hidden
- Add getModelInvocableCommands(): reserved for Phase 3 model tool-call use
### Built-in command annotations (41 files)
Annotate every built-in command with commandType:
- commandType='local' + supportedModes all-modes: btw, bug, compress, context,
init, summary (replaces the 6-command whitelist)
- commandType='local' interactive-only: export, memory, plan, insight
- commandType='local-jsx' interactive-only: all remaining ~31 commands
### Loader metadata injection (4 files)
Each loader stamps source/sourceLabel/commandType/modelInvocable on every
command it emits:
- BuiltinCommandLoader: source='builtin-command', modelInvocable=false
- BundledSkillLoader: source='bundled-skill', commandType='prompt',
modelInvocable=true
- command-factory (FileCommandLoader): source per extension/user origin,
commandType='prompt', modelInvocable=!extensionName
- McpPromptLoader: source='mcp-prompt', commandType='prompt', modelInvocable=true
### Bug fix
MCP_PROMPT commands were incorrectly excluded from non-interactive/ACP modes by
the old whitelist logic. commandType='prompt' now correctly allows them in all
modes.
### Session.ts / nonInteractiveHelpers.ts
- ACP session calls getAvailableCommands with explicit 'acp' mode
- Remove allowedBuiltinCommandNames parameter from buildSystemMessage() —
capability filtering is now self-contained in CommandService
* fix test ci
* feat(cli): Phase 2 slash command expansion + ACP fixes + UX improvements
Phase 2.1 - Command mode expansion:
- Extend 13 built-in commands to support non_interactive/acp modes
- A class: export, plan, statusline - supportedModes only
- A+ class: language, copy, restore - add non-interactive branches
- A' class: model, approvalMode - handle dialog paths in non-interactive
- B class: about, stats, insight, docs, clear - full non-interactive branches
- context: format output as readable Markdown instead of raw JSON
- export: use HTML as default format when no subcommand given
Phase 2.2 - SkillTool integration:
- SkillTool now consumes CommandService.getModelInvocableCommands()
Phase 2.3 - Mid-input slash ghost text:
- Replace mid-input dropdown completion with inline ghost text
- Match Claude Code behavior: gray dimmed completion hint in input box
- Tab accepts the ghost text completion
- Add findMidInputSlashCommand() and getBestSlashCommandMatch() utilities
ACP session bug fixes:
- Fix executionMode undefined in interactive mode (slashCommandProcessor)
- Fix slash command output not visible in Zed (use emitAgentMessage)
- Fix newline rendering in Zed (Markdown hard line-break)
- Fix history replay merging consecutive user messages (recordSlashCommand)
- Fix /clear not clearing model context (dynamic chat reference)
* feat: inline complete only for modelInvocable
* fix memory command
* fix: pass 'non_interactive' mode explicitly to getAvailableCommands
- Fix critical bug in nonInteractiveHelpers.ts: loadSlashCommandNames was
calling getAvailableCommands without specifying mode, causing it to default
to 'acp' instead of 'non_interactive'. Commands with supportedModes that
include 'non_interactive' but not 'acp' would be silently excluded.
- Apply the same fix in systemController.ts for the same reason.
- Update test mock to delegate filtering to production filterCommandsForMode()
instead of duplicating the logic inline, preventing divergence.
Fixes review comments by wenshao and tanzhenxin on PR #3283.
* fix: resolve TypeScript type error in nonInteractiveHelpers.test.ts
* fix test ci
* fix mcp prompt in skill manager
* revert pr#3345
* fix test ci
* feat(cli): adapt /insight for non_interactive mode with message return
- non_interactive: run generateStaticInsight() synchronously with no-op
progress callback, return { type: 'message' } with output path
- acp: keep existing stream_messages path with progress streaming
- interactive: unchanged
Add tests for non_interactive success and error paths.
Update phase2-technical-design.md and roadmap.md to reflect the
three-way mode split and clarify that MCP prompts do not need
modelInvocable (they are called via native MCP tool call mechanism).
* fix(cli): ghost text only shown when cursor is at end of slash token
Use strict equality (!==) instead of > in findMidInputSlashCommand so that
ghost text is only computed and Tab-accepted when the cursor sits exactly at
the trailing edge of the partial command token.
Previously, with the cursor inside an already-typed token (e.g. /re|view),
the ghost text suffix would still be shown and pressing Tab would insert it
at the cursor position, producing a duplicated tail. Using strict equality
makes ghost text disappear as soon as the cursor moves inside the token.
Add unit tests for findMidInputSlashCommand covering cursor-at-end,
cursor-inside-token, cursor-past-token, start-of-line, and
no-space-before-slash cases.
* fix(cli): support /model <model-id> in non-interactive and ACP modes
Previously, /model <model-id> (without --fast) fell through to the
non-interactive branch that only returned the current model info and
incorrectly told users to use --fast. Now:
- /model <model-id> → sets the main model via settings + config.setModel()
- /model → shows current model with correct usage hint
- /model --fast <id> → unchanged (sets fast model)
Fixes the inconsistency flagged in PR review: the help text said to use
'/model <model-id>' but the command returned a dialog action which is
unsupported in non-interactive mode.
* fix(cli): declare supportedModes on doctorCommand to enable non-interactive and ACP
The command's action already had non-interactive handling (returns a JSON
message with check results), but without supportedModes declared the
BUILT_IN fallback restricted it to interactive-only so it was never
registered in non_interactive or acp sessions.
* feat(skills): add SkillCommandLoader for user/project/extension skills as slash commands
- New SkillCommandLoader loads user, project, and extension level SKILL.md
files as slash commands (previously only bundled skills were slash-invocable)
- Extension skills follow plugin-command rules: modelInvocable only when
description or whenToUse is present
- User/project skills are always modelInvocable (matching bundled behavior)
- skill-manager now injects extensionName when loading extension-level skills
- Add when_to_use and disable-model-invocation frontmatter support to SKILL.md
and .md command files (SkillConfig, markdown-command-parser, command-factory,
BundledSkillLoader, FileCommandLoader)
- SkillTool filters out skills with disableModelInvocation and includes
whenToUse in the skill description shown to the model
- 16 unit tests for SkillCommandLoader covering all cases
* docs: update phase2 design doc to reflect final decisions on plan/statusline/copy/restore
These four commands are intentionally kept as interactive-only by design:
- /plan and /statusline: tightly coupled with interactive multi-turn UI
- /copy and /restore: clipboard and snapshot restore are inherently interactive
Update design doc classification table, section 4.2, 4.3, 5.2, 5.3,
file change summary, test requirements, behavior analysis table,
and implementation batch descriptions to reflect this decision.
* feat(cli): re-implement slashCommands.disabled denylist based on current refactored code
Adapts the feature originally introduced in pr#3445 to the current
CommandService / Phase-2 refactored code.
Sources (merged, de-duplicated, case-insensitive):
- settings key slashCommands.disabled (string[], UNION merge)
- --disabled-slash-commands CLI flag (comma-separated or repeated)
- QWEN_DISABLED_SLASH_COMMANDS environment variable
Enforcement points:
- CommandService.create() accepts optional disabledNames: ReadonlySet<string>
and removes matching commands post-rename, so disabled commands never appear
in autocomplete, mid-input ghost text, or model-invocable commands list.
- slashCommandProcessor (interactive TUI) passes the denylist to
CommandService.create so disabled commands are absent from dropdown/ghost text.
- nonInteractiveCliCommands.handleSlashCommand() keeps allCommands unfiltered
to distinguish disabled vs unknown; disabled commands return unsupported with
a "disabled by the current configuration" reason (not no_command).
- getAvailableCommands() (ACP) passes the denylist to CommandService.create.
Config plumbing:
- core/Config: ConfigParameters.disabledSlashCommands + getDisabledSlashCommands()
- cli/config: CliArgs.disabledSlashCommands + yargs option + loadCliConfig merge
- settingsSchema: slashCommands.disabled (MergeStrategy.UNION)
- settings.schema.json: regenerated
Tests: 28 pass (CommandService x4, nonInteractiveCliCommands x3 new cases)
* feat(cli): complete slashCommands.disabled coverage from pr#3445
Fill in the three items that were missing from the initial re-implementation:
- packages/cli/src/config/settings.test.ts: add UNION-merge test for
slashCommands.disabled across user and workspace scopes
- packages/cli/src/nonInteractiveCli.test.ts: add getDisabledSlashCommands
mock to the shared mockConfig fixture
- docs/users/configuration/settings.md: add slashCommands section (table +
example + note) and --disabled-slash-commands row in the CLI args table
* fix(cli): match disabled slash commands by alias as well as primary name
The denylist previously only checked cmd.name (the primary/canonical name),
so disabling a command by its alias (e.g. 'about' for the 'status' command)
had no effect. Fix both CommandService.create() and the isDisabled() helper
in nonInteractiveCliCommands.ts to also check altNames.
Also improve the user-facing error message to show the token the user actually
typed (e.g. /about) instead of always showing the primary name (/status).
15 KiB
Slash Command 重构路线图
总体目标
用 Qwen 内部架构风格,交付一个在外部体验上 95% 对齐 Claude Code 的 command 平台,同时修复三模式分裂、命令来源单一、prompt command 无法被模型调用三个核心问题。
核心设计原则
- 每个 Phase 可独立 ship:完成后行为是自洽的,不依赖未来 Phase 才能运行
- Phase 1 是纯基础设施:除修复 MCP_PROMPT 被错误拦截外,不改变任何现有可用命令集
- 行为变化与架构变化分开:Phase 1 做架构,Phase 2 做能力扩展
- 不照搬 Claude Code 内部架构:但对齐用户可感知的能力面
Phase 1:基础设施重建(纯架构,零行为变化)
目标
建立统一的命令元数据模型和跨模式管理机制,为后续所有 Phase 提供底层支撑。
功能点
1.1 扩展 SlashCommand 元数据模型
在现有 SlashCommand 接口上新增以下字段:
来源字段
source: CommandSource:命令来源枚举(builtin-command/bundled-skill/skill-dir-command/plugin-command/mcp-prompt等)sourceLabel?: string:展示用的来源标签(如"Built-in"/"MCP: github-server")
模式能力字段
supportedModes: ExecutionMode[]:声明在哪些运行模式下可用(interactive/non_interactive/acp)
执行类型字段
commandType: CommandType:声明执行类型(prompt/local/local-jsx)
可见性字段
userInvocable: boolean:用户是否可通过 slash command 调用(默认true)modelInvocable: boolean:模型是否可通过 tool call 调用(默认false)
辅助元数据字段(为 Phase 3 预留,Phase 1 仅定义,不使用)
argumentHint?: string:参数提示,如"<model-id>"/"show|list|set"whenToUse?: string:何时调用该命令的说明(供模型使用)examples?: string[]:使用示例
1.2 Loader 填充 source/commandType 字段
每个 Loader 在构建 SlashCommand 时必须填充 source 和 commandType:
| Loader | source | commandType |
|---|---|---|
BuiltinCommandLoader |
builtin-command |
由各命令声明(local / local-jsx) |
BundledSkillLoader |
bundled-skill |
prompt |
FileCommandLoader(用户/项目) |
skill-dir-command |
prompt |
FileCommandLoader(插件) |
plugin-command |
prompt |
McpPromptLoader |
mcp-prompt |
prompt |
1.3 内置命令声明 supportedModes 和 commandType
为所有 built-in 命令显式声明:
commandType:local(无 UI 依赖)或local-jsx(依赖 dialog/React)supportedModes:local类命令声明['interactive', 'non_interactive', 'acp'];local-jsx类命令声明['interactive']
1.4 用 capability-based 过滤替换硬编码白名单
- 删除
ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE常量 - 删除
filterCommandsForNonInteractive函数 - 新增
filterCommandsForMode(commands, mode)函数,基于supportedModes字段过滤 - 新增
getEffectiveSupportedModes(cmd)工具函数(考虑 CommandKind 默认策略) - 修改
handleSlashCommand/getAvailableCommands函数签名,移除allowedBuiltinCommandNames参数
1.5 CommandService 升级为统一 Registry
- 新增
getCommandsForMode(mode: ExecutionMode)方法 - 新增
getModelInvocableCommands()方法(Phase 2/3 使用,Phase 1 提供接口) - 现有
getCommands()保持不变(interactive 使用)
验收标准
SlashCommand接口包含所有新字段,TypeScript 编译通过- 所有 Loader 填充
source和commandType字段 - 所有 built-in 命令声明
commandType和supportedModes ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE已删除,被 capability filter 取代- non-interactive 下可用命令集与重构前完全一致(现有测试不 break)
- MCP prompt commands 在 non-interactive/acp 下可正常执行(修复原有错误限制)
CommandService.getCommandsForMode('non_interactive')返回正确的命令集- 所有现有测试通过
Phase 2:能力扩展(命令整理与 prompt command 模型调用)
目标
基于 Phase 1 的元数据基础,扩展三种模式下的命令可用范围,并打通 prompt command 的模型调用通路。
功能点
2.1 扩展 non-interactive / acp 可用命令集
ACP 语义设计原则
将命令扩展到 ACP/non-interactive 模式前,需遵循以下设计原则:
- 接收方不同:ACP 模式下消息的接收方是 IDE(Zed/VS Code 插件),而非终端用户。消息内容以纯文本或 Markdown 格式为宜,不应包含 terminal 专用的 ANSI 样式。
- 实现策略是增加模式分支,而非替换:正确做法是在命令的
action内部新增模式判断——interactive 路径保持现有 UI 渲染逻辑不变,non_interactive/acp 路径返回适合机器消费的message或submit_prompt。两条路径共存于同一个action函数中。 - 有状态操作需说明语义:在单次非交互调用中(如 CLI
-p参数),/model set、/language set等有状态命令的变更仅在本次 session 内有效,应在命令响应文本中注明。 - 只读 vs 有副作用:只读命令(如
/about、/stats)直接返回当前状态文本;有副作用命令(如/model set、/language set)需在响应中确认操作结果。 - 避免环境相关副作用:打开浏览器(
/docs、/insight)、操作剪贴板(/copy)等依赖图形环境的操作,在 non_interactive/acp 路径下应跳过,改为在响应文本中返回相关 URL 或内容本身。
待扩展命令总览
注:
btw、bug、compress、context、init、summary已在 Phase 1 中扩展到全模式,不在本阶段列表中。
以下 13 个命令将在 Phase 2 中扩展到 non_interactive 和 acp 模式:
A 类:action 已返回 message 或 submit_prompt,只需扩展 supportedModes 并设计 ACP 消息内容
| 命令 | 返回类型 | ACP/non-interactive 处理要点 |
|---|---|---|
/copy |
message |
ACP 下无剪贴板,改为在响应文本中返回内容本身或提示 |
/export |
message |
返回导出文件的完整路径 |
/plan |
submit_prompt |
无需改动,直接扩展模式 |
/restore |
message |
返回恢复操作的结果描述 |
/language |
message |
返回当前语言设置或变更确认文本 |
/statusline |
submit_prompt |
无需改动,直接扩展模式 |
A' 类:有参数时正常执行,无参数时触发 dialog(需增加无参数路径的 non-interactive 处理)
| 命令 | 无参数 interactive 行为 | 无参数 non_interactive/acp 行为 |
|---|---|---|
/model |
打开模型选择 dialog | 返回当前模型名称及说明文本 |
/approval-mode |
打开审批模式 dialog | 返回当前审批模式及说明文本 |
B 类:action 内部使用 context.ui.addItem() 渲染 React 组件,需增加模式分支返回纯文本
| 命令 | interactive 行为 | non_interactive/acp 返回内容 |
|---|---|---|
/about |
渲染版本/配置 React 组件 | 版本号、当前模型、关键配置的纯文本摘要 |
/stats |
渲染 token/费用统计组件 | session 统计数据的纯文本格式 |
/insight |
渲染分析组件 + 打开浏览器 | non_interactive 同步生成返回文件路径;acp 通过 stream_messages 推送进度和结果 |
/docs |
渲染文档入口 + 打开浏览器 | 返回文档 URL,不打开浏览器 |
C 类:特殊处理
| 命令 | interactive 行为 | non_interactive/acp 行为 |
|---|---|---|
/clear |
调用 context.ui.clear() 清空终端显示 |
返回上下文边界标记 message,内容为 "Context cleared. Previous messages are no longer in context." |
2.2 prompt command 模型调用打通
- 在
CommandService(或CommandRegistry)中实现getModelInvocableCommands(),返回所有modelInvocable: true的命令 - 将
BundledSkillLoader、FileCommandLoader(用户/项目命令)加载的命令标记为modelInvocable: true - MCP prompt 不标记为
modelInvocable:MCP prompt 通过独立的 MCP tool call 机制由模型调用,无需经过SkillTool中转 - 改造
SkillTool:从只消费SkillManager.listSkills()改为同时消费CommandService.getModelInvocableCommands() - 构建统一的模型可调用命令描述,注入
SkillTool的 description
2.3 mid-input slash command 检测(基础版)
- 在
InputPrompt中检测光标附近的 slash token(不限于行首) - 检测到 slash token 后通过 inline ghost text 提示最佳匹配命令名(Tab 接受)
- 不包含 dropdown 补全菜单、argument hints、source badge 等(Phase 3 做)
- ghost text 候选集仅限
modelInvocable: true的命令(skill / file command)
验收标准
2.1 命令扩展
- A 类:
/copy、/export、/plan、/restore、/language、/statusline在 non-interactive 和 acp 模式下可正常执行并返回有意义的文本输出 - A' 类:
/model、/approval-mode无参数时在 non-interactive/acp 下返回当前状态文本(不触发 dialog);有参数时执行变更并返回确认文本 - B 类:
/about、/stats、/docs在 non-interactive/acp 下返回纯文本,/docs不打开浏览器;/insight在non_interactive下同步生成并返回文件路径 message,在acp下通过stream_messages推送进度 - C 类:
/clear在 non-interactive/acp 下返回上下文边界标记 message,不调用context.ui.clear() - 所有扩展命令在 interactive 模式下行为与重构前完全一致(无退化)
2.2 模型调用
- 模型在对话中可以通过
SkillTool调用 bundled skill、file command(用户/项目) - MCP prompt 不经过
SkillTool,通过 MCP tool call 机制由模型原生调用 - 模型不可以调用 built-in commands(
userInvocable: true,modelInvocable: false) SkillTool的 description 包含所有modelInvocable命令的描述
2.3 mid-input slash
- mid-input slash:在正文中输入
/后通过 inline ghost text 提示最佳匹配命令(Tab 接受)
Phase 3:体验对齐(补全增强 + Claude Code 命令补齐)
目标
在 Phase 1/2 的元数据和命令能力基础上,补齐补全体验,并补充 Claude Code 中存在而 Qwen Code 缺失的命令。
功能点
3.1 补全体验增强
source badge
- 在补全菜单中展示命令来源标签(
[MCP]已有,扩展为[Skill]、[Custom]等) - 使用
source/sourceLabel字段渲染
argument hint
- 补全菜单中命令名后展示
argumentHint(如set <model-id>) argumentHint由 Phase 1 元数据字段提供
recently used 排序
- 记录用户最近使用的命令(session 级别,无需持久化)
- 在补全排序中给近期使用的命令加权
alias 命中高亮
- 当补全命中
altNames而非主名时,在展示时注明(如help (alias: ?))
冲突策略对齐
- 明确优先级:built-in > bundled/skill-dir > plugin > mcp
- 冲突时将低优先级命令重命名(如
pluginName.commandName)
3.2 mid-input slash command 完整版
- 在 Phase 2 基础版上增加 argument hints 和 source badge 展示
- ghost text 提示(输入
/he时显示/help的淡色提示) - 有效命令 token 高亮(已完成匹配的 slash command 显示不同颜色)
3.3 Help 目录重构
将 /help 从平铺列表改为分组目录:
- Built-in Commands(local + local-jsx,注明 mode)
- Bundled Skills
- Custom Commands(用户/项目 file commands)
- Plugin Commands
- MCP Commands
每条命令展示:名称、argumentHint、description、source、supportedModes 标记
3.4 ACP available commands 元数据增强
在 sendAvailableCommandsUpdate() 中将更多元数据暴露给 ACP 客户端:
argumentHintsourcesupportedModessubcommands(名称列表)modelInvocable
3.5 Claude Code 缺失命令补齐
补充 Qwen Code 当前没有、Claude Code 有且常用的命令:
| 命令 | 类型 | 说明 |
|---|---|---|
/doctor |
local |
环境自检,输出配置/连接/工具状态诊断 |
/release-notes |
local |
展示当前版本的更新日志 |
/cost |
local |
展示当前 session 的 token 消耗和费用估算 |
注:
/review、/commit等任务类命令以 bundled skill 形式提供,不在此列。
验收标准
- 补全菜单展示 source badge(
[MCP]、[Skill]、[Custom]) - 补全菜单展示 argumentHint(如
set <model-id>) - 近期使用的命令在补全列表中优先出现
- alias 命中时在补全项中注明原名
- mid-input slash:ghost text 提示正确渲染
/help输出按来源分组,每条命令展示支持模式标记- ACP available commands 包含
argumentHint、source、subcommands字段 /doctor、/release-notes、/cost三个命令可用/doctor在 non-interactive 模式下可执行(返回message)
各 Phase 依赖关系
Phase 1(元数据 + 统一过滤)
│
├──► Phase 2(能力扩展)
│ │
│ ├──► slash command 子命令拆分
│ └──► prompt command 模型调用(需要 getModelInvocableCommands())
│
└──► Phase 3(体验对齐)
│
├──► source badge(需要 Phase 1 source 字段)
├──► argument hint(需要 Phase 1 argumentHint 字段)
└──► Help 分组(需要 Phase 1 source 字段)
Phase 2 和 Phase 3 不互相依赖,可以并行推进(或根据优先级调换部分子项)。