/** * settings-menu.ts — Settings panel for /tasks → Settings. * * Two-level select/input navigation (pick a setting, then pick/enter a value), * using only ui.select / ui.input — these work in both TUI and RPC modes, * unlike ui.custom (component factories), which RPC mode does not support. */ import { saveTasksConfig, type TasksConfig } from "../tasks-config.js"; // ── Types ─────────────────────────────────────────────────────────────────── export type SettingsUI = { select( title: string, options: string[], opts?: { timeout?: number }, ): Promise; input( title: string, placeholder?: string, opts?: { timeout?: number }, ): Promise; }; type SettingDef = { id: keyof TasksConfig; label: string; hint: string; kind: "select" | "input"; values?: string[]; current: () => string; apply: (value: string) => boolean; }; const BACK = "← Back"; // ── Settings panel ────────────────────────────────────────────────────────── export async function openSettingsMenu( ui: SettingsUI, cfg: TasksConfig, onBack: () => Promise, clearDelayTurns: number, cwd = process.cwd(), ): Promise { const definitions: SettingDef[] = [ { id: "taskScope", label: "Task storage", hint: "memory | session | project — takes effect on next session start", kind: "select", values: ["memory", "session", "project"], current: () => cfg.taskScope ?? "project", apply: (v) => { cfg.taskScope = v as "memory" | "session" | "project"; return true; }, }, { id: "autoCascade", label: "Auto-cascade agent tasks", hint: "ON: pending agent tasks start once dependencies complete", kind: "select", values: ["on", "off"], current: () => (cfg.autoCascade ?? false) ? "on" : "off", apply: (v) => { cfg.autoCascade = v === "on"; return true; }, }, { id: "showAll", label: "Show all tasks in widget", hint: "ON: ignore the visible limit", kind: "select", values: ["on", "off"], current: () => (cfg.showAll ?? false) ? "on" : "off", apply: (v) => { cfg.showAll = v === "on"; return true; }, }, { id: "maxVisible", label: "Max visible tasks in widget", hint: "Caps widget task lines when 'Show all' is off", kind: "input", current: () => String(cfg.maxVisible ?? 10), apply: (v) => { const n = Number(v); if (!Number.isInteger(n) || n <= 0) return false; cfg.maxVisible = n; return true; }, }, { id: "sortOrder", label: "Widget sort order", hint: "status | id | recent | oldest", kind: "select", values: ["id", "status", "recent", "oldest"], current: () => cfg.sortOrder ?? "id", apply: (v) => { cfg.sortOrder = v as TasksConfig["sortOrder"]; return true; }, }, { id: "hiddenAt", label: "Hidden tasks position", hint: "bottom: truncate from end | top: truncate from start", kind: "select", values: ["bottom", "top"], current: () => cfg.hiddenAt ?? "bottom", apply: (v) => { cfg.hiddenAt = v as "top" | "bottom"; return true; }, }, { id: "autoClearCompleted", label: "Auto-clear completed tasks", hint: `never | on_list_complete | on_task_complete — clears lag ~${clearDelayTurns} turns`, kind: "select", values: ["never", "on_list_complete", "on_task_complete"], current: () => cfg.autoClearCompleted ?? "on_list_complete", apply: (v) => { cfg.autoClearCompleted = v as TasksConfig["autoClearCompleted"]; return true; }, }, ]; const edit = async (def: SettingDef): Promise => { let value: string | undefined; if (def.kind === "input") { value = await ui.input(def.label, def.current()); } else { value = await ui.select(`${def.label} — ${def.hint}`, [...(def.values ?? []), BACK]); } if (!value || value === BACK) return; if (def.apply(value)) saveTasksConfig(cfg, cwd); }; // Loop until the user backs out — each edit persists immediately. for (;;) { const choice = await ui.select( "⚙ Task Settings", definitions.map(d => `${d.label} (${d.current()})`).concat(BACK), ); if (!choice || choice === BACK) return onBack(); const def = definitions.find(d => `${d.label} (${d.current()})` === choice); if (def) await edit(def); } }