import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { getConfigReport, mutateConfig, type EffectiveBtwSettings } from "./config.ts"; const labels: Array<[keyof EffectiveBtwSettings, string]> = [ ["answerMaxTokens", "Answer cap"], ["refineMaxTokens", "Refine cap"], ["toolAllowlist", "Read-only tools"], ["toolCallBudget", "Tool budget"], ["summaryEnabled", "Summary enabled"], ["summaryTriggerTokens", "Summary trigger/context cap"], ["summaryRetainTokens", "Summary retain"], ["summaryMaxTokens", "Summary output cap"], ["shortcut", "Shortcut"], ]; const numeric = new Set(["answerMaxTokens", "refineMaxTokens", "toolCallBudget", "summaryTriggerTokens", "summaryRetainTokens", "summaryMaxTokens"]); const tools = ["read", "grep", "find", "ls"]; function shown(value: unknown): string { return Array.isArray(value) ? (value.join(", ") || "none") : String(value); } function validNumber(value: string): number | undefined { const n = Number(value); return Number.isSafeInteger(n) && n >= 1 ? n : undefined; } export type SettingsDependencies = { getConfigReport?: typeof getConfigReport; mutateConfig?: typeof mutateConfig; }; /** Dialog-only settings editor; all writes use config's reload-under-lock mutation. */ export async function openSettings(ctx: ExtensionCommandContext, dependencies: SettingsDependencies = {}): Promise { const readConfig = dependencies.getConfigReport ?? getConfigReport; const writeConfig = dependencies.mutateConfig ?? mutateConfig; if (!ctx.hasUI) { ctx.ui.notify("/btw --settings requires interactive UI", "error"); return; } const initial = readConfig(); if (initial.diagnostics.length) ctx.ui.notify(`btw settings: ${initial.diagnostics.join("; ")}`, "warning"); while (true) { const report = readConfig(); const choices = labels.map(([key, label]) => { const field = report.fields[key]!; return `${label}: ${shown(field.value)} (${field.envLocked ? "env · read-only" : field.source})`; }); choices.push("Done"); const picked = await ctx.ui.select("btw settings", choices); if (!picked || picked === "Done") return; const index = choices.indexOf(picked); const [key, label] = labels[index]!; const field = report.fields[key]!; if (field.envLocked) { ctx.ui.notify(`${label} is controlled by ${field.envVar}; remove it to edit this setting`, "info"); continue; } const action = await ctx.ui.select(label, ["Change", ...(field.hasUserOverride ? ["Reset user override"] : []), "Back"]); if (!action || action === "Back") continue; try { if (action === "Reset user override") { await writeConfig(key); } else if (key === "summaryEnabled") { const choice = await ctx.ui.select(label, ["Enable", "Disable", "Back"]); if (choice === "Enable") await writeConfig(key, true); else if (choice === "Disable") await writeConfig(key, false); else continue; } else if (key === "toolAllowlist") { let selected = new Set(report.settings.toolAllowlist); let committed = false; while (true) { const options = tools.map((tool) => `${selected.has(tool) ? "✓" : "○"} ${tool}`); options.push("None", "Done", "Back"); const choice = await ctx.ui.select("Read-only tools", options); if (!choice || choice === "Back") break; if (choice === "Done") { await writeConfig(key, [...selected]); committed = true; break; } if (choice === "None") { selected = new Set(); continue; } const tool = choice.slice(2); selected.has(tool) ? selected.delete(tool) : selected.add(tool); } if (!committed) continue; } else { const input = await ctx.ui.input(`Change ${label}`, shown(field.value)); if (input === undefined) continue; let value: unknown = input; if (numeric.has(key)) { const parsed = validNumber(input); if (parsed === undefined || (key === "summaryTriggerTokens" && parsed < 2)) { ctx.ui.notify("btw: enter a safe integer within this setting's minimum", "error"); continue; } const next = { ...report.settings, [key]: parsed }; if (next.summaryRetainTokens >= next.summaryTriggerTokens) { ctx.ui.notify("btw: summary retain must be less than summary trigger", "error"); continue; } value = parsed; } await writeConfig(key, value); } if (key === "shortcut") { const reload = await ctx.ui.confirm("Reload extensions", "Shortcut changes require /reload. Reload now?"); if (reload) { try { await ctx.reload(); } catch (error) { ctx.ui.notify(`btw settings were saved, but reload failed: ${error instanceof Error ? error.message : String(error)}`, "error"); } return; } ctx.ui.notify("btw: /reload is required for shortcut changes; Pi checks shortcut conflicts on reload", "warning"); } } catch (error) { ctx.ui.notify(`btw settings could not be saved: ${error instanceof Error ? error.message : String(error)}`, "error"); } } }