ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GSD 扩展开发:用参数补全实现 Slash 命令子命令模式(/wt 实战剖析)

GSD 扩展开发:用参数补全实现 Slash 命令子命令模式(/wt 实战剖析) 人工智能AI Agent代码智能体Agent 编排CLIAI 应用【免费下载链接】gsd-2A powerful meta-prompting, context engineering and spec-driven development system that enables agents to work for long periods of time autonomously without losing track of the big picture项目地址https://gitcode.com/gh_mirrors/gs/gsd-2点击查看免费下载导读Pi/GSD 并没有一套独立的嵌套 Slash 命令机制——/wt new、/foo delete这类交互体验实际上是由单个命令 参数补全模拟出来的把第一个位置参数当作子命令再用getArgumentCompletions()让第二个参数根据第一个参数动态联想。本文以内置 worktree 扩展的/wt命令为实战样本完整讲解pi.registerCommand(name, options)、getArgumentCompletions(prefix)与handler(args, ctx)三个核心 API 的配合方式读完你将能为自己编写的扩展实现一个命令、多级子命令、动态参数联想的完整能力。心智模型没有嵌套命令只有位置参数Pi 没有类似/wt new或/foo delete的原生嵌套 slash 命令概念。取而代之的是注册一个slash 命令然后把后续输入按位置参数解析一个顶层 slash 命令一个或多个位置参数第一个位置参数充当子命令后续参数根据第一个参数动态补全所以下面的用户可见形态/wt new ls switch merge rm status本质上只是命令wt第一个参数new | ls | switch | merge | rm | status之一这个模式正是内置 worktree 扩展所采用的对应仓库中的 worktree-command.ts。理解这一点后扩展开发者在设计命令时就不需要等待框架提供子命令注册表只需要在补全函数和处理器里统一解析参数即可。三个关键 API 的职责分工整个子命令模式建立在三个 API 之上API签名职责pi.registerCommand(name, options)(name: string, options: OmitRegisteredCommand, name) void注册一个 slash 命令如/wt、/foogetArgumentCompletions(prefix)(argumentPrefix: string) AutocompleteItem[] \| null根据已输入的参数前缀返回补全建议handler(args, ctx)(args: string, ctx: ExtensionCommandContext) Promisevoid执行命令按第一个参数分发逻辑在类型层面三者被统一定义在 types.tsexport interface RegisteredCommand { name: string; description?: string; getArgumentCompletions?: (argumentPrefix: string) AutocompleteItem[] | null; handler: (args: string, ctx: ExtensionCommandContext) Promisevoid; }补全建议项AutocompleteItem定义在 pi-tui/src/autocomplete.tsexport interface AutocompleteItem { value: string; // 选中后回填到输入框的文本 label: string; // 列表中展示的文本 description?: string; // 可选的说明文字 }当扩展调用pi.registerCommand时实际是把{ name, ...options }存入该扩展的命令表见 loader.ts随后交互模式下 interactive-mode.ts 会把扩展命令连同其getArgumentCompletions一起接入自动补全管线用户输入/后即可触发联想。核心模式一份可直接运行的注册代码下面是最简可用的子命令式命令注册示例对应原文档的/foo示例pi.registerCommand(foo, { description: Manage foo items: /foo new|list|delete [name], getArgumentCompletions: (prefix: string) { const subcommands [new, list, delete]; const parts prefix.trim().split(/\s/); // 补全第一个参数/foo subcommand if (parts.length 1) { return subcommands .filter((cmd) cmd.startsWith(parts[0] ?? )) .map((cmd) ({ value: cmd, label: cmd })); } // 补全第二个参数/foo delete name if (parts[0] delete) { const items [alpha, beta, gamma]; const namePrefix parts[1] ?? ; return items .filter((name) name.startsWith(namePrefix)) .map((name) ({ value: delete ${name}, label: name })); } return []; }, handler: async (args, ctx) { const parts args.trim().split(/\s/); const sub parts[0]; const name parts[1]; await ctx.waitForIdle(); if (sub new) { ctx.ui.notify(Create a new foo item, info); return; } if (sub list) { ctx.ui.notify(List foo items, info); return; } if (sub delete) { if (!name) { ctx.ui.notify(Usage: /foo delete name, error); return; } ctx.ui.notify(Deleting ${name}, info); return; } ctx.ui.notify(Usage: /foo new|list|delete [name], info); }, });注意两点细节ctx.waitForIdle()在处理器里先等待 agent 结束当前流式输出避免命令与正在运行的 agent 回合冲突。waitForIdle是ExtensionCommandContext专有的会话控制方法见 types.ts只应在用户主动发起的命令中使用。ctx.ui.notify(message, type)通知类型支持info | warning | error | success见 types.ts。getArgumentCompletions()的行为细节getArgumentCompletions(prefix)收到的prefix是斜杠命令名之后的所有输入。以/foo为例输入/foo后面带空格→prefix 输入/foo de→prefix de输入/foo delete a→prefix delete a也就是说你可以在补全函数里把前缀按空白拆成词再决定下一步该给出什么建议。常见结构只有三步用户还在输入第一个参数 → 展示所有子命令第一个参数命中了某个分支如delete→ 补全下一个参数其他情况 → 返回[]。关键陷阱空前缀的拆分结果这里有一个非常实用、也最容易踩坑的细节.trim().split(/\s/)的结果是[]不是[]。因此必须用parts.length 1而不是parts.length 0来判断正在输入第一个参数const parts prefix.trim().split(/\s/); if (parts.length 1) { // 补全第一个参数 }这样同时覆盖了两种情况命令后完全没有输入prefix 第一个参数只输入了一部分如de动态第二参数补全让后续参数依赖子命令当后续参数取决于前面的子命令时这个模式才真正发挥威力getArgumentCompletions: (prefix) { const parts prefix.trim().split(/\s/); const sub parts[0]; if (parts.length 1) { return [new, list, delete].map((s) ({ value: s, label: s })); } if (sub delete) { const items getCurrentItemsSomehow(); // 实际项目中从状态/磁盘/服务获取 const namePrefix parts[1] ?? ; return items .filter((item) item.startsWith(namePrefix)) .map((item) ({ value: delete ${item}, label: item })); } return []; }这里补全项的value必须携带前缀如delete alpha因为自动补全组件会用选中的 value 替换当前已输入的全部参数部分见 pi-tui/src/autocomplete.ts 的AutocompleteProvider接口契约而label只展示给用户看。这也是/wt switch、/wt merge、/wt rm能实时列出当前 worktree 名称的原理。实战样本仓库内/wt命令的完整实现原文档指向的~/.gsd/agent/extensions/worktree/index.ts在当前仓库中的对应物是 worktree-command.ts。其注册入口worktree-command.ts#L232-L255同时注册了/worktree与/wt两个命令共享同一套补全与处理器export function registerWorktreeCommand(pi: ExtensionAPI): void { ensureWorktreeOriginalCwdFromPath(); pi.registerCommand(worktree, { description: Git worktrees (also /wt): /worktree name | list | merge | remove, getArgumentCompletions: worktreeCompletions, async handler(args: string, ctx: ExtensionCommandContext) { await handleWorktreeCommand(args, ctx, pi, worktree); }, }); // /wt alias — same handler, same completions pi.registerCommand(wt, { description: Alias for /worktree, getArgumentCompletions: worktreeCompletions, async handler(args: string, ctx: ExtensionCommandContext) { await handleWorktreeCommand(args, ctx, pi, wt); }, }); }补全逻辑worktreeCompletions真实的补全函数worktree-command.ts#L54-L96与原文档模式完全一致并增加了一个亮点第一参数阶段同时建议子命令和已有 worktree 名——因为在/wt里直接输入一个名字等价于创建/切换到该 worktreefunction worktreeCompletions(prefix: string) { const parts prefix.trim().split(/\s/); const subcommands [list, merge, remove, switch, create, return]; if (parts.length 1) { const partial parts[0] ?? ; const cmdCompletions subcommands .filter(cmd cmd.startsWith(partial)) .map(cmd ({ value: cmd, label: cmd })); try { const mainBase getWorktreeOriginalCwd() ?? process.cwd(); const existing listWorktrees(mainBase); const nameCompletions existing .filter(wt wt.name.startsWith(partial)) .map(wt ({ value: wt.name, label: wt.name })); return [...cmdCompletions, ...nameCompletions]; } catch { return cmdCompletions; } } if ((parts[0] merge || parts[0] remove || parts[0] switch || parts[0] create) parts.length 2) { const namePrefix parts[1] ?? ; try { const mainBase getWorktreeOriginalCwd() ?? process.cwd(); const existing listWorktrees(mainBase); const nameCompletions existing .filter(wt wt.name.startsWith(namePrefix)) .map(wt ({ value: ${parts[0]} ${wt.name}, label: wt.name })); // remove 额外提供 all 选项 if (parts[0] remove all.startsWith(namePrefix)) { nameCompletions.push({ value: remove all, label: all }); } return nameCompletions; } catch { return []; } } return []; }由此可以验证原文档的说法输入/wt会看到new | ls | switch | merge | rm | status一类的子命令提示当前仓库实际为list | merge | remove | switch | create | return不同版本子命令集合会演进输入/wt switch后则开始联想当前存在的 worktree 名称。另外仓库里还有一份懒加载版的补全实现 worktree-command-bootstrap.ts它在子命令项上补充了description字段让补全列表更可读且只在用户真正执行命令时才动态importExtensionModule加载完整处理器见 worktree-command-bootstrap.ts#L32-L41适合启动性能敏感的扩展。处理器逻辑按第一个参数分发真实的处理器worktreeHandlerworktree-command.ts#L98-L221遵循补全与处理器共享同一命令形状的原则无参数 → 输出完整用法涵盖所有子命令list/return→ 直接执行对应动作switch name/create name→ 若 worktree 已存在则切换否则创建merge [name] [target]→ 解析目标分支先尝试确定性 squash 合并冲突时回退到 LLM 引导合并remove name|all→ 删除前先弹确认框未知输入 → 返回Usage提示甚至给出Did you mean /wt switch ?的纠错。处理器与补全对参数的分词口径必须一致都用trim().split(/\s/)。这一点在Parsing in the Handler一节还会展开。处理器里的参数解析补全逻辑与处理器逻辑必须对命令形状达成一致。处理器侧常用的结构是handler: async (args, ctx) { const parts args.trim().split(/\s/); const sub parts[0]; const rest parts.slice(1); switch (sub) { case new: // 处理 /foo new return; case list: // 处理 /foo list return; case delete: // 处理 /foo delete name return; default: ctx.ui.notify(Usage: /foo new|list|delete, info); return; } }保持解析简单并让分支与补全广告的子命令一一对应。注意真实场景中很多子命令需要校验必需参数后再执行——比如/wt的merge分支会区分无参数时只有处于 worktree 内才合法、第一个参数是 worktree 名、第一个参数是目标分支三种情况见 worktree-command.ts#L150-L182这就是handler 里再次校验的价值。何时使用该模式何时改用独立命令适合单命令 子命令式补全动作归属于同一个清晰领域希望从一个入口获得可发现性子命令感觉像同一族操作后续参数依赖前面的选择。典型例子/wt new|switch|merge|rm|status、/preset save|load|delete、/workflow start|list|abort、/foo new|list|delete。适合拆成独立命令动作在概念上互不相关每个命令需要独立的描述与身份自动补全会变得太深或太臃肿合并后的命令难以记忆或文档化。例如/deploy、/rollback、/handoff更适合各自独立而不是强行塞进一个伞形命令。UX 设计准则顶层子命令保持简短、含义一目了然命名要能在斜杠命令后自然读出来/wt switch读起来顺畅分支层级保持浅一两层通常就够没有任何合理补全时返回空数组回退用法文本与补全结构保持一致description里就写好语法子命令需要必填数据时在 handler 里再次校验。推荐的命令结构清单一个成熟的子命令式命令通常包含四部分description展示顶层语法例如Manage foo items: /foo new|list|delete [name]getArgumentCompletions()负责第一、第二参数的联想handler()按第一个参数分支兜底用法提示针对非法输入输出Usage。description写法示例description: Manage foo items: /foo new|list|delete [name]官方 SDK 文档 building-extensions.md 也给出过同样结构的/deploy与/watch start|stop示例可作为本模式的对照参考。相关阅读与本模式配套阅读的仓库文档11-custom-commands-user-facing-actions.md自定义命令的整体概念与注册流程09-extensionapi-what-you-can-do.mdExtensionAPI 的完整能力面worktree-command.ts本文实战样本的完整源码api-reference.mdSDK API 速查autocomplete.tsTUI 自动补全管线与AutocompleteItem定义。总结如果想让/foo表现得像拥有嵌套子命令只需五步注册一个slash 命令把第一个参数视为子命令实现getArgumentCompletions(prefix)视需要为后续参数提供动态补全value 携带完整前缀在 handler 中基于解析出的第一个参数分支执行。这就是/wt交互体验背后的全部机制。它不需要框架支持子命令注册表只要补全与处理器对参数形状达成一致就能用最少的 API 提供层次清晰、可发现性强的命令体验。赞分享人工智能AI Agent代码智能体Agent 编排CLIAI 应用【免费下载链接】gsd-2A powerful meta-prompting, context engineering and spec-driven development system that enables agents to work for long periods of time autonomously without losing track of the big picture项目地址https://gitcode.com/gh_mirrors/gs/gsd-2点击查看免费下载相关推荐GSD 命令参考手册get-shit-done 全量 Slash 命令语法、Flags 与实战示例GSD 命令参考手册get shit done 全量 Slash 命令语法、Flags 与实战示例 本文是 get shit doneGSD的命令级参考手人工智能AI 应用提示工程开发工具工作流自动化AI Agentast-grep 扩展指令开发用 Rust 实现自定义命令与子命令ast grep 扩展指令开发用 Rust 实现自定义命令与子命令 在日常开发中你是否遇到过需要为代码分析工具添加特定功能的场景ast grep 作为一款开发工具CLI静态分析Lint代码质量PlayIntegrityFix终极指南2025年Root设备完整性修复解决方案详解PlayIntegrityFix终极指南2025年Root设备完整性修复解决方案详解 还在为Google Play商店显示设备未认证而烦恼吗PlayIn移动开发系统编程应用安全上一篇GitHub Desktop 路线图解读从 1.5 到 3.0 的核心功能演进与源码实现下一篇Google Cloud Platform机器学习训练使用What-If Tool分析模型公平性创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表