diff --git a/AGENTS.md b/AGENTS.md index e647131..fd41507 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ - `chrome_snapshot`、`chrome_find` 以及带 `includeSnapshot=true` 的交互工具必须把完整结构化快照写入当前项目 `.pi/chrome-context/` 下的 Session 临时文件,工具结果只返回相对路径与有界摘要;模型必须立即用 `ctx_execute_file` 提取下一步所需信息,不得用 `read` / `cat` 把原始 JSON 重新塞入上下文。每个 Session/目标只保留最新快照,页面交互后旧快照视为失效,Session shutdown、撤销或授权过期时清理。 - Hashline 默认以 2 字符行哈希覆盖内置 `read` / `edit`,`grep` 和 `replaceText` 均关闭;它保证锚定编辑而不负责压缩,大文件上下文节省必须来自先定位/隔离分析、再做最小范围 `offset/limit` 读取。 - 工具路由规则不得替换 Pi 默认系统提示词:代码结构、调用关系和待修改 symbol 优先 CodeGraph,定义/引用/类型/诊断优先 LSP;工作区内大文件探索、分析和总结优先 `ctx_execute_file`,日志、构建与不可预测命令输出优先 Context Mode;字面搜索先用 FFF `find` 收敛文件位置,再在收敛路径内用 `grep`/`multi_grep` 获取行号;真正修改前才用小范围 Hashline `read` 获取新鲜锚点并用锚定 `edit`。在线搜索按场景只先选一个服务:广泛发现、新闻和候选来源用 Tavily,官方/技术/论文和高确定性来源用 Exa,中文、站点限定和日期筛选用 Keenable;只有必要时才跨服务复核。遇到大量结果、截断或上限时继续缩小范围,不得靠提高 limit 或倾倒全部结果解决。 -- `ask_user_question` 只用于模型继续任务所必需的用户决定、偏好、确认或澄清;不得重复询问直接用户消息中已有的信息。工具仅支持交互式 TUI、顺序执行和最多 8 题;取消或 abort 必须返回 `cancelled: true`,不得伪造答案,且 `/qna` 不属于该扩展。其成功 `details.answers` 必须保留 `question` / `answer` 字段,以匹配 AutoReview 已有的可信结构化用户交互边界。 +- `ask_user_question` 只用于模型继续任务所必需的用户决定、偏好、确认或澄清;不得重复询问直接用户消息中已有的信息。工具支持交互式 TUI 与具备 Pi extension UI bridge 的 RPC 客户端、顺序执行和最多 8 题;RPC 必须使用标准 `select` / `input` 对话框逐题收集,JSON/print 等无 UI 模式保持拒绝。取消或 abort 必须返回 `cancelled: true`,不得伪造答案,且 `/qna` 不属于该扩展。其成功 `details.answers` 必须保留 `question` / `answer` 字段,以匹配 AutoReview 已有的可信结构化用户交互边界。 - Tool Search 默认常驻核心 `read` / `write` / `edit` / `bash` / `grep` / `find`、`codegraph_explore`、`lsp_diagnostics`、`ctx_execute` / `ctx_execute_file` / `ctx_batch_execute` 与 `tool_search`;Windows 上内置 `powershell` 必须与 `bash` 同属固定本地 Shell 核心工具,非 Windows 平台必须从目录中过滤。首次创建完整默认 `toolSearch` 段时必须通过唯一同目录临时文件以权限 `600` 原子写入并记录 `bundleDefaultsVersion: 2`,已有自定义段只补缺失运行时字段且不得伪造迁移来源标记。其余组合包工具必须优先使用 `pi-tool-search/extensions/bundle-groups.ts` 中提交的权威工作流分组;精确已有分组必须直接激活,不得等待无关未知工具的模型增强。默认最多 5 个动态组、模型组最多 8 个工具、动态工具总数最多 28;`groups` 可原子激活最多 5 个精确组,完整请求超限时不得改变当前状态;加载或组内调用更新 LRU,超限先移除最久未用的非请求组。自然语言查询必须优先精确工具名和已提交 workflow alias,弱匹配或近似同分只能返回候选,不得任意激活;`/tool-search-status` 必须显示当前容量、LRU 顺序与最近淘汰原因。 - `pi-chrome` 在未授权时不注册 Chrome 工具;显式授权后出现的 21 个 `chrome_*` 工具必须由 Tool Search 分入 `chrome-navigation`、`chrome-interaction`、`chrome-debugging`,不得落入 unknown-tools 模型增强路径。授权撤销或过期时必须停用全部 21 个工具,包括 `chrome_find` / `chrome_inspect`;Tool Search 重新激活 schema 不能替代每次执行时的授权校验。 - 标准组合包工具全部命中预置目录时 Tool Search 不得调用模型、创建用户缓存或发送隐藏 schema。只有存在未识别第三方工具时才允许惰性模型增强;生成结果必须保留全部预置 assignment,模型元数据不得替代真实名称、参数或 schema。缓存必须位于 agent 目录且权限 `600`;模型失败必须继续使用预置目录 + 确定性未知工具分组。`groupOverrides` 优先级高于预置分组。 @@ -250,9 +250,9 @@ ## `pi-ask-user` 开发约定 -- 扩展入口是 `pi-ask-user/index.ts`,schema、归一化、TUI 与工具注册位于 `pi-ask-user/src/`,纯 helper 测试保留在 `pi-ask-user/test/`。 +- 扩展入口是 `pi-ask-user/index.ts`,schema、归一化、TUI、RPC extension UI bridge 与工具注册位于 `pi-ask-user/src/`,纯 helper 测试保留在 `pi-ask-user/test/`。 - 工具名必须保持 `ask_user_question`,并由 Tool Search 的独立 `user-interaction` 组按需加载;根加载顺序必须位于 `pi-tool-search` 之前。 -- 参数必须有界并在运行时复核 question id、题型、选项和值的有效性;TUI 必须顺序执行、监听 abort、按 width 失效缓存,并把外层 `Focusable` 状态传播给内嵌 Editor。 +- 参数必须有界并在运行时复核 question id、题型、选项和值的有效性;TUI 必须顺序执行、监听 abort、按 width 失效缓存,并把外层 `Focusable` 状态传播给内嵌 Editor;RPC 必须只依赖 Pi 标准 `select` / `input` extension UI、逐题执行、传递 abort signal,并把 dismiss/abort 归一化为取消。 - 成功结果的 `details` 必须保持 `version`、`cancelled: false` 与非空 `answers`,每个 answer 必须提供原始 `question` 和用户 `answer`;取消结果不得成为权限授权证据。修改结果结构时同步验证 `pi-permission-auto-review` transcript 解析。 - 同步上游示例时从明确 commit 移植并保留 `LICENSE`、`UPSTREAM.md` 与本地整合差异;不得恢复 `/qna`、重复的单题工具或多个扩展入口。 - 修改后至少运行包内 `npm test` / `npm run check`、Tool Search 的 typecheck/test/build、AutoReview transcript 测试、根扩展联合加载和 packed tarball 隔离安装验证。 diff --git a/README.md b/README.md index 6666d32..16a2344 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - 本仓库维护的 `pi-minimal-footer`:基于 `@ogulcancelik/pi-minimal-footer@0.1.10`,用紧凑的上下文仪表和订阅用量条替换默认 footer,并在 Codex Fast 模式开启时显示 `Fast on`。 - 本仓库维护的 `pi-notify`:基于 `@smoose/pi-notify@0.1.1`,使用 Kitty OSC 99 在 Pi 完全 settled 或权限系统即将请求人工确认时通知,并支持点击精确聚焦来源 Kitty 窗口。 - 本仓库维护的 `pi-condense`:从官方 `v2.9.1` 源码导入,把已完成的工具调用批次总结为可恢复的短摘要,并通过 `context_tree_query` 按需取回原始输出;默认开启,瞬时摘要故障默认最多重试 2 次。 -- 本仓库维护的 `pi-ask-user`:提供模型主动调用的 `ask_user_question`,用一个顺序 TUI 同时支持单题、多题、选择题、文本题、自定义回答与提交前复核。 +- 本仓库维护的 `pi-ask-user`:提供模型主动调用的 `ask_user_question`;TUI 中保留单题、多题、选择、文本、自定义回答与提交前复核,Paseo 等兼容 RPC 客户端通过标准 Pi extension UI 逐题交互。 - 本仓库维护的 `pi-tool-search`:从完整工具定义生成并缓存经过校验的工作流分组,默认以最多 5 个动态组、28 个动态工具的 LRU 策略加载原始完整 schema,并支持原子批量激活。 - 本仓库维护的 `pi-chrome`:从上游 `0.15.46` 源码导入,通过显式会话授权接入用户当前已登录的 Chrome Profile;大体积页面快照写入 Session 临时文件并交给 Context Mode 隔离分析,21 个 `chrome_*` 工具仍由 Tool Search 按工作流加载。 - 本仓库维护的 `pi-permission-auto-review`:作为 `pi-permission-system` authorizer,使用 Codex Guardian 风格策略自动复核 `ask` 请求。 @@ -129,7 +129,7 @@ CodeGraph 本体不由组合包安装。需要使用 CodeGraph 的机器应自 ### 模型主动提问 -`pi-ask-user/` 把 Pi 上游的单题与多题示例合并为唯一的 `ask_user_question` 工具;不提供 `/qna` 或其他用户命令。模型只有在继续任务确实缺少用户决定、偏好、确认或澄清时才应加载 `user-interaction` 组并调用它,不得重复询问直接消息中已有的信息。工具仅在交互式 TUI 中运行,最多一次提交 8 个问题,支持选择、自由文本、显式跳过、返回修改和最终复核;取消结果不会作为 AutoReview 的授权证据。 +`pi-ask-user/` 把 Pi 上游的单题与多题示例合并为唯一的 `ask_user_question` 工具;不提供 `/qna` 或其他用户命令。模型只有在继续任务确实缺少用户决定、偏好、确认或澄清时才应加载 `user-interaction` 组并调用它,不得重复询问直接消息中已有的信息。工具最多一次提交 8 个问题:交互式 TUI 支持选择、自由文本、显式跳过、返回修改和最终复核;Paseo 等具备 Pi extension UI bridge 的 RPC 客户端使用逐题 `select` / `input` 对话框。取消结果不会作为 AutoReview 的授权证据,JSON/print 等无交互 UI 模式保持拒绝执行。 ### 工具与搜索路由 diff --git a/pi-ask-user/README.md b/pi-ask-user/README.md index cc8b4cb..9cad457 100644 --- a/pi-ask-user/README.md +++ b/pi-ask-user/README.md @@ -4,16 +4,17 @@ A locally maintained Pi extension that consolidates the upstream `question.ts` a ## Tool -The extension registers `ask_user_question`. The model can present one to eight questions in one sequential TUI interaction: +The extension registers `ask_user_question`. The model can present one to eight questions through an interactive Pi UI: - `select`: one to eight stable `{ value, label, description? }` choices, always followed by a free-form choice that the model cannot disable; -- `text`: a free-form Editor answer; +- `text`: a free-form answer; - `required: false`: lets the user explicitly skip the question; -- multiple questions: progress tabs plus a final review page. +- TUI mode: one sequential custom interaction with progress tabs and a final review page; +- RPC mode: standard Pi `select` / `input` dialogs shown one question at a time by compatible clients such as Paseo. The model should call the tool only when a missing decision, preference, or clarification is needed to continue. It should not repeat questions already answered in the user's direct messages. -## Controls +## TUI controls - `↑` / `↓`: move through choices; - `Enter`: select, edit, or submit; @@ -22,6 +23,8 @@ The model should call the tool only when a missing decision, preference, or clar The outer component propagates focus to the embedded Editor for IME cursor positioning. +RPC clients use their native dialog controls. Dismissing a dialog or aborting the tool returns `cancelled: true`; required blank text is requested again, while optional blank text is recorded as an explicit skip. + ## Result contract Successful results include readable text and structured `details`: @@ -48,7 +51,7 @@ Successful results include readable text and structured `details`: The tool name and the `question` / `answer` fields intentionally match `pi-permission-auto-review`'s trusted structured user-interaction envelope. Cancelled results are never treated as authorization evidence. -The tool requires Pi's interactive TUI mode. RPC, JSON, and print modes receive a tool error rather than an invented answer. +The tool requires dialog-capable interactive UI. TUI mode uses the custom questionnaire, RPC mode uses Pi's standard extension UI bridge, and JSON/print modes receive a tool error rather than an invented answer. ## Development diff --git a/pi-ask-user/UPSTREAM.md b/pi-ask-user/UPSTREAM.md index 2d0179d..7a8fdb0 100644 --- a/pi-ask-user/UPSTREAM.md +++ b/pi-ask-user/UPSTREAM.md @@ -7,6 +7,6 @@ Initial reference snapshot: `dcd461925db2edf69a43c8135db1180d418afd54` (`main`, inspected 2026-08-24). -The upstream `qna.ts` command is intentionally not included: this package only supports model-initiated questions. Local changes consolidate single and multi-question flows into one tool, add text questions, keep a user-controlled free-form escape hatch on every select question, validation, bounded schemas, cancellation/abort handling, width-aware rendering, and IME focus propagation. +The upstream `qna.ts` command is intentionally not included: this package only supports model-initiated questions. Local changes consolidate single and multi-question flows into one tool, add text questions, keep a user-controlled free-form escape hatch on every select question, validation, bounded schemas, cancellation/abort handling, width-aware rendering, IME focus propagation, and a standard Pi extension-UI fallback for dialog-capable RPC clients. The copied MIT license and original copyright notice are retained in `LICENSE`. diff --git a/pi-ask-user/src/rpc.ts b/pi-ask-user/src/rpc.ts new file mode 100644 index 0000000..c34d681 --- /dev/null +++ b/pi-ask-user/src/rpc.ts @@ -0,0 +1,142 @@ +import type { NormalizedQuestion, QuestionOptionInput, UserAnswer, AskUserResult } from "./types.ts"; + +interface RpcDialogOptions { + signal?: AbortSignal; +} + +export interface RpcQuestionUI { + select(title: string, options: string[], opts?: RpcDialogOptions): Promise; + input(title: string, placeholder?: string, opts?: RpcDialogOptions): Promise; + notify(message: string, type?: "info" | "warning" | "error"): void; +} + +type RpcChoice = + | { kind: "option"; display: string; option: QuestionOptionInput } + | { kind: "custom"; display: string } + | { kind: "skip"; display: string }; + +type TextResponse = + | { kind: "answer"; text: string } + | { kind: "skipped" } + | { kind: "cancelled" }; + +function dialogTitle(question: NormalizedQuestion, index: number, total: number): string { + const progress = total > 1 ? `[${index + 1}/${total}] ` : ""; + return `${progress}${question.label}: ${question.prompt}`; +} + +function choicesFor(question: NormalizedQuestion): RpcChoice[] { + const choices: RpcChoice[] = question.options.map((option, index) => ({ + kind: "option", + display: `${index + 1}. ${option.label}${option.description ? ` — ${option.description}` : ""}`, + option, + })); + choices.push({ kind: "custom", display: "Type something." }); + if (!question.required) choices.push({ kind: "skip", display: "Skip this question." }); + return choices; +} + +function saveTextAnswer(question: NormalizedQuestion, text: string, custom: boolean): UserAnswer { + return { + id: question.id, + question: question.prompt, + type: question.type, + answer: text, + value: text, + label: text, + custom, + skipped: false, + }; +} + +function saveSkippedAnswer(question: NormalizedQuestion): UserAnswer { + return { + id: question.id, + question: question.prompt, + type: question.type, + answer: "", + value: "", + label: "Skipped", + custom: false, + skipped: true, + }; +} + +function cancelledResult(questions: NormalizedQuestion[], answers: UserAnswer[]): AskUserResult { + return { version: 1, cancelled: true, questions, answers }; +} + +async function requestText( + ui: RpcQuestionUI, + title: string, + required: boolean, + placeholder: string, + signal?: AbortSignal, +): Promise { + while (true) { + if (signal?.aborted) return { kind: "cancelled" }; + const response = await ui.input(title, placeholder, { signal }); + if (signal?.aborted || response === undefined) return { kind: "cancelled" }; + + const text = response.trim(); + if (text) return { kind: "answer", text }; + if (!required) return { kind: "skipped" }; + ui.notify("An answer is required.", "warning"); + } +} + +export async function runRpcQuestions( + ui: RpcQuestionUI, + questions: NormalizedQuestion[], + signal?: AbortSignal, +): Promise { + const answers: UserAnswer[] = []; + + for (const [index, question] of questions.entries()) { + if (signal?.aborted) return cancelledResult(questions, answers); + const title = dialogTitle(question, index, questions.length); + + if (question.type === "text") { + const response = await requestText( + ui, + title, + question.required, + question.required ? "Type your answer" : "Type an answer, or leave blank to skip", + signal, + ); + if (response.kind === "cancelled") return cancelledResult(questions, answers); + answers.push(response.kind === "skipped" ? saveSkippedAnswer(question) : saveTextAnswer(question, response.text, false)); + continue; + } + + const choices = choicesFor(question); + const selected = await ui.select(title, choices.map((choice) => choice.display), { signal }); + if (signal?.aborted || selected === undefined) return cancelledResult(questions, answers); + const choice = choices.find((item) => item.display === selected); + if (!choice) throw new Error("RPC UI returned an unknown choice"); + + if (choice.kind === "skip") { + answers.push(saveSkippedAnswer(question)); + continue; + } + if (choice.kind === "custom") { + const response = await requestText(ui, `${title} — custom answer`, true, "Type a different answer", signal); + if (response.kind !== "answer") return cancelledResult(questions, answers); + answers.push(saveTextAnswer(question, response.text, true)); + continue; + } + + answers.push({ + id: question.id, + question: question.prompt, + type: question.type, + answer: choice.option.label, + value: choice.option.value, + label: choice.option.label, + custom: false, + skipped: false, + }); + } + + return { version: 1, cancelled: false, questions, answers }; +} diff --git a/pi-ask-user/src/tool.ts b/pi-ask-user/src/tool.ts index a95b6dd..381c4e2 100644 --- a/pi-ask-user/src/tool.ts +++ b/pi-ask-user/src/tool.ts @@ -3,6 +3,7 @@ import { Text } from "@earendil-works/pi-tui"; import { formatAnswers, normalizeQuestions } from "./normalize.ts"; import { AskUserParamsSchema } from "./schema.ts"; +import { runRpcQuestions } from "./rpc.ts"; import type { AskUserResult, QuestionInput } from "./types.ts"; import { AskUserView } from "./view.ts"; @@ -15,23 +16,27 @@ export function registerAskUserTool(pi: ExtensionAPI): void { name: "ask_user_question", label: "Ask User", description: - "Ask the user one or more structured questions in an interactive TUI when their input is genuinely needed to continue. Supports choices, free-form text, optional custom answers, review, and cancellation. Do not ask for information already stated by the user.", + "Ask the user one or more structured questions in an interactive UI when their input is genuinely needed to continue. Supports TUI and RPC clients, choices, free-form text, optional custom answers, and cancellation. Do not ask for information already stated by the user.", promptSnippet: "Use ask_user_question to pause and collect missing user decisions instead of guessing", parameters: AskUserParamsSchema, executionMode: "sequential", async execute(_toolCallId, params, signal, _onUpdate, ctx) { const questions = normalizeQuestions(params.questions as QuestionInput[]); - if (ctx.mode !== "tui") throw new Error("ask_user_question requires interactive TUI mode"); + if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) { + throw new Error("ask_user_question requires interactive TUI or RPC UI mode"); + } if (signal?.aborted) { const result: AskUserResult = { version: 1, cancelled: true, questions, answers: [] }; return { content: [{ type: "text", text: "User interaction was cancelled" }], details: result }; } - const result = await ctx.ui.custom((tui, theme, _keybindings, done) => - new AskUserView(tui, theme, questions, done, signal), - ); + const result = ctx.mode === "tui" + ? await ctx.ui.custom((tui, theme, _keybindings, done) => + new AskUserView(tui, theme, questions, done, signal), + ) + : await runRpcQuestions(ctx.ui, questions, signal); if (result.cancelled) { return { diff --git a/pi-ask-user/test/rpc.test.ts b/pi-ask-user/test/rpc.test.ts new file mode 100644 index 0000000..542d1d0 --- /dev/null +++ b/pi-ask-user/test/rpc.test.ts @@ -0,0 +1,124 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { normalizeQuestions } from "../src/normalize.ts"; +import { runRpcQuestions, type RpcQuestionUI } from "../src/rpc.ts"; + +function mockUi(responses: { + select?: Array; + input?: Array; +}): RpcQuestionUI & { + calls: Array<{ kind: "select" | "input"; title: string; options?: string[] }>; + notifications: Array<{ message: string; type?: "info" | "warning" | "error" }>; +} { + const selectResponses = [...(responses.select ?? [])]; + const inputResponses = [...(responses.input ?? [])]; + const calls: Array<{ kind: "select" | "input"; title: string; options?: string[] }> = []; + const notifications: Array<{ message: string; type?: "info" | "warning" | "error" }> = []; + return { + calls, + notifications, + async select(title, options) { + calls.push({ kind: "select", title, options }); + assert.ok(selectResponses.length > 0, "unexpected select dialog"); + return selectResponses.shift(); + }, + async input(title) { + calls.push({ kind: "input", title }); + assert.ok(inputResponses.length > 0, "unexpected input dialog"); + return inputResponses.shift(); + }, + notify(message, type) { + notifications.push({ message, type }); + }, + }; +} + +test("asks RPC questions sequentially and preserves stable option values", async () => { + const questions = normalizeQuestions([ + { + id: "language", + label: "Language", + prompt: "Which language?", + type: "select", + options: [ + { value: "ts", label: "TypeScript", description: "Strict typing" }, + { value: "js", label: "JavaScript" }, + ], + }, + { id: "notes", prompt: "Anything else?", type: "text", required: false }, + ]); + const ui = mockUi({ select: ["2. JavaScript"], input: [" "] }); + + const result = await runRpcQuestions(ui, questions); + + assert.equal(result.cancelled, false); + assert.deepEqual(result.answers, [ + { + id: "language", + question: "Which language?", + type: "select", + answer: "JavaScript", + value: "js", + label: "JavaScript", + custom: false, + skipped: false, + }, + { + id: "notes", + question: "Anything else?", + type: "text", + answer: "", + value: "", + label: "Skipped", + custom: false, + skipped: true, + }, + ]); + assert.match(ui.calls[0]?.title ?? "", /^\[1\/2\] Language:/); + assert.ok(ui.calls[0]?.options?.includes("1. TypeScript — Strict typing")); +}); + +test("supports custom RPC answers and repeats required blank input", async () => { + const questions = normalizeQuestions([{ + id: "other", + prompt: "Choose or write one", + type: "select", + options: [{ value: "known", label: "Known" }], + }]); + const ui = mockUi({ select: ["Type something."], input: [" ", " Custom value "] }); + + const result = await runRpcQuestions(ui, questions); + + assert.equal(result.cancelled, false); + assert.equal(result.answers[0]?.answer, "Custom value"); + assert.equal(result.answers[0]?.custom, true); + assert.deepEqual(ui.notifications, [{ message: "An answer is required.", type: "warning" }]); +}); + +test("returns cancelled with partial answers when an RPC dialog is dismissed", async () => { + const questions = normalizeQuestions([ + { id: "name", prompt: "Name?", type: "text" }, + { id: "confirm", prompt: "Continue?", type: "select", options: [{ value: "yes", label: "Yes" }] }, + ]); + const ui = mockUi({ input: ["Ada"], select: [undefined] }); + + const result = await runRpcQuestions(ui, questions); + + assert.equal(result.cancelled, true); + assert.equal(result.answers.length, 1); + assert.equal(result.answers[0]?.answer, "Ada"); +}); + +test("does not open RPC dialogs after abort", async () => { + const questions = normalizeQuestions([{ id: "name", prompt: "Name?", type: "text" }]); + const ui = mockUi({}); + const controller = new AbortController(); + controller.abort(); + + const result = await runRpcQuestions(ui, questions, controller.signal); + + assert.equal(result.cancelled, true); + assert.deepEqual(result.answers, []); + assert.deepEqual(ui.calls, []); +}); diff --git a/pi-tool-search/README.md b/pi-tool-search/README.md index 2069532..bc287b2 100644 --- a/pi-tool-search/README.md +++ b/pi-tool-search/README.md @@ -42,7 +42,7 @@ Run `/tool-search-status` to inspect the current catalog source, pinned count, d | `memory-recall` | Official Hippo recall/context/status and condensed-output recovery | | `memory-management` | Official Hippo remember/outcome tools | | `subagents` | Launch agents, collect background results, and steer running agents | -| `user-interaction` | `ask_user_question` model-initiated TUI questions | +| `user-interaction` | `ask_user_question` model-initiated TUI/RPC questions | | `chrome-navigation` | Chrome bridge setup, tabs, navigation, snapshots/find/inspect, waits, and screenshots | | `chrome-interaction` | Chrome click/type/fill/key/hover/drag/tap/scroll input | | `chrome-debugging` | Chrome page evaluation, console/network diagnostics, response retrieval, and file upload |