跳到主内容
创建时间:最后更新:作者:白川

为扩展添加交互和结果展示

用选择对话框、状态和结果组件展示检查过程,并为非交互模式保留可用入口。

为扩展添加交互和结果展示

工具返回了正确 JSON,终端里却不容易看出检查了哪个文件。展示层可以把相同结果整理成通知、状态或面板,但不能成为第二份业务实现。

这篇为检查功能增加文件选择和摘要展示。业务结果仍由 inspect 生成;模型调用仍使用工具返回值,用户选择则走命令入口。

本文示例固定使用 @earendil-works/pi-coding-agent@1.5.0,源码位于仓库 columns/coding-agent/examples/extension-development/。示例是独立包,先在该目录安装依赖;不需要修改专栏站点依赖。下面的命令均从这个示例目录运行。

对话与终端能力分开判断

ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
import { allowedFiles, inspect } from "../src/inspect.js"
import { registerInspection } from "../src/register-inspection.js"

export default function (pi: ExtensionAPI) {
  registerInspection(pi)
  pi.registerCommand("lesson-pick", {
    description: "选择教学文件并展示检查结果",
    handler: async (_args, ctx) => {
      if (!ctx.hasUI) {
        throw new Error("当前模式没有对话 UI,请改用 lesson_inspect 工具")
      }
      const file = await ctx.ui.select("选择文件", [...allowedFiles])
      if (!file) {
        return
      }
      if (ctx.mode === "tui") {
        ctx.ui.setStatus("lesson", "正在检查")
      }
      try {
        const report = await inspect(ctx.cwd, file)
        if (ctx.mode === "tui") {
          ctx.ui.setWidget("lesson", [
            `文件:${report.file}`,
            `声明:${report.names.join(", ") || "无匹配"}`,
          ])
        }
        ctx.ui.notify(JSON.stringify(report), "info")
      } finally {
        if (ctx.mode === "tui") {
          ctx.ui.setStatus("lesson", undefined)
        }
      }
    },
  })
}

ctx.hasUI 表示宿主提供交互 UI,包含 TUI 和支持相关请求的 RPC 场景;它不等于“当前一定是终端”。状态栏和 widget 在这里额外检查 ctx.mode === 'tui',避免把终端展示方法用于不支持的模式。

bash
npx pi --extension ./extensions/ui.ts

输入 /lesson-pick,选择 fixtures/sample.ts 后应显示 greet。取消选择时直接返回,不生成检查结果。制造文件缺失后再次选择,错误应由宿主呈现,同时 finally 清除正在检查的状态。

一份结果,按使用方展示

展示方式适合表达是否替代工具结果
通知
一次完成或错误提示
否
状态栏
正在执行、等待输入
否
widget
当前文件和摘要
否
工具自定义渲染
工具调用与结果的紧凑/展开视图
否
自定义消息或条目渲染
带类型的历史记录
否

需要自定义工具渲染时,使用 SDK 的 renderCall、renderResult 及组件接口,根据展开状态决定展示摘要还是详细内容;不要把大段源码固定输出到每次工具结果里。渲染器读 details,机器调用读 structuredContent,两者仍来源于同一个报告。

本例先用内置选择框与文本 widget,减少组件依赖。添加复杂 TUI 组件后,应单独验证键盘、窄屏、折叠和主题表现,不能用 TypeScript 编译通过替代交互检查。

无 UI 的路径也必须明确

print 或 JSON 模式不能弹出选择框,调用 /lesson-pick 会明确报错并提示工具入口。自动化调用应直接给工具参数,不等待用户输入。RPC 客户端需要实现宿主对应的 UI 请求与响应,不能仅凭 hasUI 就假设所有前端都画出了选择框。

扩展代码不要直接 console.log 写进宿主 stdout。那里可能承载 JSON 事件或 RPC 协议,一句调试文本就能破坏客户端解析。独立的审计脚本可以输出 JSON,因为脚本拥有自己的标准输出。

展示状态也有生命周期

finally 处理的是本次命令结束;切换会话、重载扩展或退出宿主时,长期 widget 和监听器还需要显式清理。本例 widget 保存最近一次展示,重新检查会覆盖;生产实现应按会话决定是否保留,并在 session_shutdown 清除相应 UI。

检查 UI 时至少走成功、取消、业务失败和无 UI 四条路径。只有成功路径可用,不能说明命令适合自动化,也不能证明状态没有残留。

依据与版本

代码以已发布的 1.5.0 包导出、类型声明和加载器为准。在线主分支会继续变化,遇到接口差异时先核对版本。