Commit b1ae5ca3b9f4
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/ask-user.ts
@@ -2,15 +2,18 @@
* Ask User Tool
*
* Allows the AI to ask the user a question and wait for their response.
- * Similar to Claude Code's "ask" tool. Supports both free-form text input
- * and optional multiple-choice suggestions.
+ * Supports free-form text input, single-choice suggestions, and
+ * multi-select checkboxes.
*
* Usage by the AI:
* ask_user({ question: "Which database should I use?" })
* ask_user({ question: "How should I handle errors?", suggestions: ["retry", "fail fast", "log and continue"] })
+ * ask_user({ question: "Which features?", suggestions: ["auth", "logging", "caching"], multiSelect: true })
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent";
+import { Container, type SettingItem, SettingsList, Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
export default function (pi: ExtensionAPI) {
@@ -23,6 +26,7 @@ export default function (pi: ExtensionAPI) {
"Use ask_user when you need user input, clarification, or a decision before proceeding.",
"Prefer ask_user over guessing when the choice significantly affects the outcome.",
"Keep questions concise and specific. Provide suggestions when there are obvious options.",
+ "Use multiSelect: true when the user may want to pick more than one option from the suggestions.",
],
parameters: Type.Object({
question: Type.String({ description: "The question to ask the user" }),
@@ -32,6 +36,12 @@ export default function (pi: ExtensionAPI) {
"Optional list of suggested answers. User can pick one or type their own response.",
}),
),
+ multiSelect: Type.Optional(
+ Type.Boolean({
+ description:
+ "If true, user can select multiple suggestions (checkbox-style). Requires suggestions to be provided.",
+ }),
+ ),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -47,19 +57,80 @@ export default function (pi: ExtensionAPI) {
};
}
- const { question, suggestions } = params;
+ const { question, suggestions, multiSelect } = params;
const hasSuggestions = suggestions && suggestions.length > 0;
let answer: string | undefined;
- if (hasSuggestions) {
- // Show suggestions as selectable options + free-form input
+ if (multiSelect && hasSuggestions) {
+ // Multi-select: checkbox-style toggles using SettingsList
+ const selected = new Set<string>();
+
+ const result = await ctx.ui.custom<string[] | null>((tui, theme, _kb, done) => {
+ const container = new Container();
+
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
+ container.addChild(new Text(theme.fg("accent", theme.bold(question)), 1, 0));
+ container.addChild(new Text(theme.fg("dim", "Space/Enter toggle • Tab confirm • Esc cancel"), 1, 0));
+
+ const items: SettingItem[] = suggestions.map((s) => ({
+ id: s,
+ label: s,
+ currentValue: "☐",
+ values: ["☐", "☑"],
+ }));
+
+ const settingsList = new SettingsList(
+ items,
+ Math.min(items.length + 2, 15),
+ getSettingsListTheme(),
+ (id, newValue) => {
+ if (newValue === "☑") {
+ selected.add(id);
+ } else {
+ selected.delete(id);
+ }
+ },
+ () => done(null), // Esc → cancel
+ );
+ container.addChild(settingsList);
+
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
+
+ return {
+ render: (w) => container.render(w),
+ invalidate: () => container.invalidate(),
+ handleInput: (data) => {
+ // Tab = confirm selections
+ if (data === "\t") {
+ done(Array.from(selected));
+ return;
+ }
+ settingsList.handleInput(data);
+ tui.requestRender();
+ },
+ };
+ });
+
+ if (result === null || result.length === 0) {
+ return {
+ content: [{ type: "text", text: "User cancelled — did not select any options." }],
+ details: { question, suggestions, multiSelect: true, answer: null, selected: [] },
+ };
+ }
+
+ answer = result.join(", ");
+ return {
+ content: [{ type: "text", text: `User selected: ${answer}` }],
+ details: { question, suggestions, multiSelect: true, answer, selected: result },
+ };
+ } else if (hasSuggestions) {
+ // Single-select: show suggestions as selectable options + free-form input
const freeFormOption = "✎ Type a different answer…";
const options = [...suggestions, freeFormOption];
const choice = await ctx.ui.select(question, options);
if (choice === undefined) {
- // User cancelled
return {
content: [{ type: "text", text: "User cancelled — did not answer the question." }],
details: { question, suggestions, answer: null },
@@ -67,7 +138,6 @@ export default function (pi: ExtensionAPI) {
}
if (choice === freeFormOption) {
- // User chose to type their own answer
answer = await ctx.ui.input(question);
} else {
answer = choice;