mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
78 lines
3.5 KiB
TypeScript
78 lines
3.5 KiB
TypeScript
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
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";
|
|
|
|
function fallbackText(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
return result.content.find((item) => item.type === "text")?.text ?? "";
|
|
}
|
|
|
|
export function registerAskUserTool(pi: ExtensionAPI): void {
|
|
pi.registerTool({
|
|
name: "ask_user_question",
|
|
label: "Ask User",
|
|
description:
|
|
"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.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 = 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 {
|
|
content: [{ type: "text", text: "User cancelled the questions" }],
|
|
details: result,
|
|
};
|
|
}
|
|
return {
|
|
content: [{ type: "text", text: formatAnswers(result.answers) }],
|
|
details: result,
|
|
};
|
|
},
|
|
|
|
renderCall(args, theme) {
|
|
const questions = Array.isArray(args.questions) ? args.questions as Array<{ id?: unknown; label?: unknown }> : [];
|
|
const labels = questions
|
|
.map((question) => typeof question.label === "string" ? question.label : typeof question.id === "string" ? question.id : "question")
|
|
.join(", ");
|
|
let text = theme.fg("toolTitle", theme.bold("ask user "));
|
|
text += theme.fg("muted", `${questions.length} question${questions.length === 1 ? "" : "s"}`);
|
|
if (labels) text += theme.fg("dim", ` (${labels})`);
|
|
return new Text(text, 0, 0);
|
|
},
|
|
|
|
renderResult(result, _options, theme) {
|
|
const details = result.details as AskUserResult | undefined;
|
|
if (!details) return new Text(fallbackText(result), 0, 0);
|
|
if (details.cancelled) return new Text(theme.fg("warning", "Cancelled"), 0, 0);
|
|
const lines = details.answers.map((answer) => {
|
|
const value = answer.skipped
|
|
? theme.fg("muted", "Skipped")
|
|
: `${answer.custom || answer.type === "text" ? theme.fg("muted", "(wrote) ") : ""}${theme.fg("accent", answer.label)}`;
|
|
return `${theme.fg("success", "✓ ")}${theme.fg("accent", answer.id)}: ${value}`;
|
|
});
|
|
return new Text(lines.join("\n"), 0, 0);
|
|
},
|
|
});
|
|
}
|