mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat(pi-ask-user): support RPC UI dialogs
This commit is contained in:
@@ -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 隔离安装验证。
|
||||
|
||||
@@ -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 模式保持拒绝执行。
|
||||
|
||||
### 工具与搜索路由
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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<string | undefined>;
|
||||
input(title: string, placeholder?: string, opts?: RpcDialogOptions): Promise<string | undefined>;
|
||||
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<TextResponse> {
|
||||
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<AskUserResult> {
|
||||
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 };
|
||||
}
|
||||
@@ -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<AskUserResult>((tui, theme, _keybindings, done) =>
|
||||
const result = ctx.mode === "tui"
|
||||
? await ctx.ui.custom<AskUserResult>((tui, theme, _keybindings, done) =>
|
||||
new AskUserView(tui, theme, questions, done, signal),
|
||||
);
|
||||
)
|
||||
: await runRpcQuestions(ctx.ui, questions, signal);
|
||||
|
||||
if (result.cancelled) {
|
||||
return {
|
||||
|
||||
@@ -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<string | undefined>;
|
||||
input?: Array<string | undefined>;
|
||||
}): 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, []);
|
||||
});
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user