/** * tool-picker.ts — Categorized tool selection for agent configuration. * * Presents tool categories (Read-only, Edit, Execution, MCP, Other) * with batch toggle via ctx.ui.* primitives. */ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent"; /** Known tool categories and their members. */ const READ_ONLY_TOOLS = ["read", "bash", "grep", "find", "ls"]; const EDIT_TOOLS = ["edit", "write"]; const EXECUTION_TOOLS: string[] = []; const ALL_CATEGORIZED = new Set([...READ_ONLY_TOOLS, ...EDIT_TOOLS, ...EXECUTION_TOOLS]); /** * Show categorized tool picker. * Returns an array of tool names for a custom subset, * undefined for "all tools" (no restriction), * or null if cancelled. */ export async function showToolPicker( ctx: ExtensionCommandContext, currentTools?: string[], ): Promise { const choice = await ctx.ui.select("Tool selection", [ "All tools (no restriction)", "Read-only (read, bash, grep, find, ls)", "Read + Edit", "Edit only (edit, write)", "Custom... (choose individually)", ]); if (!choice) return null; if (choice.startsWith("All tools")) return undefined; if (choice.startsWith("Read-only")) return [...READ_ONLY_TOOLS]; if (choice.startsWith("Read + Edit")) return [...READ_ONLY_TOOLS, ...EDIT_TOOLS]; if (choice.startsWith("Edit only")) return [...EDIT_TOOLS]; // Custom mode — step through category toggles return promptCustomTools(ctx, currentTools); } /** * Walk through tool categories asking confirm for each. */ async function promptCustomTools( ctx: ExtensionCommandContext, currentTools?: string[], ): Promise { const current = new Set(currentTools ?? []); const selected = new Set(current); // Read-only tools const wantRead = await ctx.ui.confirm( "Read-only tools", currentHasAny(current, READ_ONLY_TOOLS) ? "Include read-only tools (read, bash, grep, find, ls)? Currently selected." : "Include read-only tools (read, bash, grep, find, ls)?", ); if (wantRead === undefined) return null; if (wantRead) { for (const t of READ_ONLY_TOOLS) selected.add(t); } else { for (const t of READ_ONLY_TOOLS) selected.delete(t); } // Edit tools const wantEdit = await ctx.ui.confirm( "Edit tools", currentHasAny(current, EDIT_TOOLS) ? "Include edit tools (edit, write)? Currently selected." : "Include edit tools (edit, write)?", ); if (wantEdit === undefined) return null; if (wantEdit) { for (const t of EDIT_TOOLS) selected.add(t); } else { for (const t of EDIT_TOOLS) selected.delete(t); } // MCP / Other tools const currentMCP = [...current].filter(t => !ALL_CATEGORIZED.has(t)); const wantMCP = await ctx.ui.confirm( "Other / MCP tools", currentMCP.length > 0 ? `Include other tools (MCP, etc.)? Currently: ${currentMCP.join(", ")}` : "Include other tools (MCP extensions, etc.)?", ); if (wantMCP === undefined) return null; if (wantMCP) { const mcpNames = await ctx.ui.input( "Tool names (comma-separated)", currentMCP.join(", "), ); if (mcpNames === undefined) return null; if (mcpNames.trim()) { for (const t of mcpNames.split(",").map(s => s.trim()).filter(Boolean)) { selected.add(t); } } } else { const toRemove: string[] = []; for (const t of selected) { if (!ALL_CATEGORIZED.has(t)) toRemove.push(t); } for (const t of toRemove) selected.delete(t); } const result = [...selected].sort(); return result.length > 0 ? result : undefined; } function currentHasAny(current: Set, tools: string[]): boolean { return tools.some(t => current.has(t)); }