From 2fd4a1da604614c5eaea0f6a7712f0e6d1b15eec Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Mon, 16 Mar 2026 10:01:27 -0500 Subject: [PATCH] refactor: replace serial prefs wizard with categorized menu (#623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: replace serial prefs wizard with categorized menu The /gsd prefs wizard previously dumped 20+ prompts in sequence, which was overwhelming. This refactors it into a category picker loop where users select from 7 categories (Models, Timeouts, Git, Skills, Budget, Notifications, Advanced), configure only what they need, and return to the menu with updated summaries showing current values at a glance. - Extract 7 category functions from monolithic handlePrefsWizard - Add buildCategorySummaries() for current-value display in menu - Category loop with Save & Exit / Escape to serialize and write - No logic changes to individual prompts — pure structural refactor * fix: narrow ctx.ui.select return type for TypeScript strict mode ctx.ui.select returns string | string[], so startsWith is not available without narrowing. Extract to string with typeof guard before dispatching. --- src/resources/extensions/gsd/commands.ts | 171 +++++++++++++++++++---- 1 file changed, 143 insertions(+), 28 deletions(-) diff --git a/src/resources/extensions/gsd/commands.ts b/src/resources/extensions/gsd/commands.ts index 291198366..713443b0b 100644 --- a/src/resources/extensions/gsd/commands.ts +++ b/src/resources/extensions/gsd/commands.ts @@ -520,17 +520,87 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte // ─── Preferences Wizard ─────────────────────────────────────────────────────── -async function handlePrefsWizard( - ctx: ExtensionCommandContext, - scope: "global" | "project", -): Promise { - const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath(); - const existing = scope === "project" ? loadProjectGSDPreferences() : loadGlobalGSDPreferences(); - const prefs: Record = existing?.preferences ? { ...existing.preferences } : {}; +/** Build short summary strings for each preference category. */ +function buildCategorySummaries(prefs: Record): Record { + // Models + const models = prefs.models as Record | undefined; + let modelsSummary = "(not configured)"; + if (models && Object.keys(models).length > 0) { + const parts = Object.entries(models).map(([phase, model]) => `${phase}: ${model}`); + modelsSummary = parts.join(", "); + } - ctx.ui.notify(`GSD preferences wizard (${scope}) — press Escape at any prompt to skip it.`, "info"); + // Timeouts + const autoSup = prefs.auto_supervisor as Record | undefined; + let timeoutsSummary = "(defaults)"; + if (autoSup && Object.keys(autoSup).length > 0) { + const soft = autoSup.soft_timeout_minutes ?? "20"; + const idle = autoSup.idle_timeout_minutes ?? "10"; + const hard = autoSup.hard_timeout_minutes ?? "30"; + timeoutsSummary = `soft: ${soft}m, idle: ${idle}m, hard: ${hard}m`; + } - // ─── Models ────────────────────────────────────────────────────────────── + // Git + const git = prefs.git as Record | undefined; + let gitSummary = "(defaults)"; + if (git && Object.keys(git).length > 0) { + const branch = git.main_branch ?? "main"; + const push = git.auto_push ? "on" : "off"; + gitSummary = `main: ${branch}, push: ${push}`; + } + + // Skills + const discovery = prefs.skill_discovery as string | undefined; + const uat = prefs.uat_dispatch; + let skillsSummary = "(not configured)"; + if (discovery || uat !== undefined) { + const parts: string[] = []; + if (discovery) parts.push(`discovery: ${discovery}`); + if (uat !== undefined) parts.push(`uat: ${uat}`); + skillsSummary = parts.join(", "); + } + + // Budget + const ceiling = prefs.budget_ceiling; + const enforcement = prefs.budget_enforcement as string | undefined; + let budgetSummary = "(no limit)"; + if (ceiling !== undefined) { + budgetSummary = `$${ceiling}`; + if (enforcement) budgetSummary += ` / ${enforcement}`; + } else if (enforcement) { + budgetSummary = enforcement; + } + + // Notifications + const notif = prefs.notifications as Record | undefined; + let notifSummary = "(defaults)"; + if (notif && Object.keys(notif).length > 0) { + const allKeys = ["enabled", "on_complete", "on_error", "on_budget", "on_milestone", "on_attention"]; + const enabledCount = allKeys.filter(k => notif[k] !== false).length; + notifSummary = `${enabledCount}/${allKeys.length} enabled`; + } + + // Advanced + const uniqueIds = prefs.unique_milestone_ids; + let advancedSummary = "(defaults)"; + if (uniqueIds !== undefined) { + advancedSummary = `unique IDs: ${uniqueIds ? "on" : "off"}`; + } + + return { + models: modelsSummary, + timeouts: timeoutsSummary, + git: gitSummary, + skills: skillsSummary, + budget: budgetSummary, + notifications: notifSummary, + advanced: advancedSummary, + }; +} + +// ─── Category configuration functions ──────────────────────────────────────── + +async function configureModels(ctx: ExtensionCommandContext, prefs: Record): Promise { const modelPhases = ["research", "planning", "execution", "completion"] as const; const models: Record = (prefs.models as Record) ?? {}; @@ -553,7 +623,6 @@ async function handlePrefsWizard( } } } else { - // No authenticated models available — fall back to text input for (const phase of modelPhases) { const current = models[phase] ?? ""; const input = await ctx.ui.input( @@ -573,8 +642,9 @@ async function handlePrefsWizard( if (Object.keys(models).length > 0) { prefs.models = models; } +} - // ─── Auto-supervisor timeouts ──────────────────────────────────────────── +async function configureTimeouts(ctx: ExtensionCommandContext, prefs: Record): Promise { const autoSup: Record = (prefs.auto_supervisor as Record) ?? {}; const timeoutFields = [ { key: "soft_timeout_minutes", label: "Soft timeout (minutes)", defaultVal: "20" }, @@ -603,8 +673,9 @@ async function handlePrefsWizard( if (Object.keys(autoSup).length > 0) { prefs.auto_supervisor = autoSup; } +} - // ─── Git settings ─────────────────────────────────────────────────────── +async function configureGit(ctx: ExtensionCommandContext, prefs: Record): Promise { const git: Record = (prefs.git as Record) ?? {}; // main_branch @@ -705,7 +776,7 @@ async function handlePrefsWizard( git.isolation = isolationChoice; } - // ─── Git commit_docs ──────────────────────────────────────────────────── + // commit_docs const currentCommitDocs = git.commit_docs; const commitDocsChoice = await ctx.ui.select( `Track .gsd/ planning docs in git${currentCommitDocs !== undefined ? ` (current: ${currentCommitDocs})` : ""}:`, @@ -718,8 +789,10 @@ async function handlePrefsWizard( if (Object.keys(git).length > 0) { prefs.git = git; } +} - // ─── Skill discovery mode ─────────────────────────────────────────────── +async function configureSkills(ctx: ExtensionCommandContext, prefs: Record): Promise { + // Skill discovery mode const currentDiscovery = (prefs.skill_discovery as string) ?? ""; const discoveryChoice = await ctx.ui.select( `Skill discovery mode${currentDiscovery ? ` (current: ${currentDiscovery})` : ""}:`, @@ -729,17 +802,18 @@ async function handlePrefsWizard( prefs.skill_discovery = discoveryChoice; } - // ─── Unique milestone IDs ────────────────────────────────────────────── - const currentUnique = prefs.unique_milestone_ids; - const uniqueChoice = await ctx.ui.select( - `Unique milestone IDs${currentUnique !== undefined ? ` (current: ${currentUnique})` : ""}:`, + // UAT dispatch + const currentUat = prefs.uat_dispatch; + const uatChoice = await ctx.ui.select( + `UAT dispatch mode${currentUat !== undefined ? ` (current: ${currentUat})` : " (default: false)"}:`, ["true", "false", "(keep current)"], ); - if (uniqueChoice && uniqueChoice !== "(keep current)") { - prefs.unique_milestone_ids = uniqueChoice === "true"; + if (uatChoice && uatChoice !== "(keep current)") { + prefs.uat_dispatch = uatChoice === "true"; } +} - // ─── Budget & cost control ──────────────────────────────────────────── +async function configureBudget(ctx: ExtensionCommandContext, prefs: Record): Promise { const currentCeiling = prefs.budget_ceiling; const ceilingStr = currentCeiling !== undefined ? String(currentCeiling) : ""; const ceilingInput = await ctx.ui.input( @@ -785,8 +859,9 @@ async function handlePrefsWizard( ctx.ui.notify(`Invalid context pause threshold "${val}" — must be 0-100. Keeping previous value.`, "warning"); } } +} - // ─── Notifications ──────────────────────────────────────────────────── +async function configureNotifications(ctx: ExtensionCommandContext, prefs: Record): Promise { const notif: Record = (prefs.notifications as Record) ?? {}; const notifFields = [ { key: "enabled", label: "Notifications enabled (master toggle)", defaultVal: true }, @@ -811,15 +886,55 @@ async function handlePrefsWizard( if (Object.keys(notif).length > 0) { prefs.notifications = notif; } +} - // ─── UAT dispatch ───────────────────────────────────────────────────── - const currentUat = prefs.uat_dispatch; - const uatChoice = await ctx.ui.select( - `UAT dispatch mode${currentUat !== undefined ? ` (current: ${currentUat})` : " (default: false)"}:`, +async function configureAdvanced(ctx: ExtensionCommandContext, prefs: Record): Promise { + const currentUnique = prefs.unique_milestone_ids; + const uniqueChoice = await ctx.ui.select( + `Unique milestone IDs${currentUnique !== undefined ? ` (current: ${currentUnique})` : ""}:`, ["true", "false", "(keep current)"], ); - if (uatChoice && uatChoice !== "(keep current)") { - prefs.uat_dispatch = uatChoice === "true"; + if (uniqueChoice && uniqueChoice !== "(keep current)") { + prefs.unique_milestone_ids = uniqueChoice === "true"; + } +} + +// ─── Main wizard with category menu ───────────────────────────────────────── + +async function handlePrefsWizard( + ctx: ExtensionCommandContext, + scope: "global" | "project", +): Promise { + const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath(); + const existing = scope === "project" ? loadProjectGSDPreferences() : loadGlobalGSDPreferences(); + const prefs: Record = existing?.preferences ? { ...existing.preferences } : {}; + + ctx.ui.notify(`GSD preferences (${scope}) — pick a category to configure.`, "info"); + + while (true) { + const summaries = buildCategorySummaries(prefs); + const options = [ + `Models ${summaries.models}`, + `Timeouts ${summaries.timeouts}`, + `Git ${summaries.git}`, + `Skills ${summaries.skills}`, + `Budget ${summaries.budget}`, + `Notifications ${summaries.notifications}`, + `Advanced ${summaries.advanced}`, + `── Save & Exit ──`, + ]; + + const raw = await ctx.ui.select("GSD Preferences", options); + const choice = typeof raw === "string" ? raw : ""; + if (!choice || choice.includes("Save & Exit")) break; + + if (choice.startsWith("Models")) await configureModels(ctx, prefs); + else if (choice.startsWith("Timeouts")) await configureTimeouts(ctx, prefs); + else if (choice.startsWith("Git")) await configureGit(ctx, prefs); + else if (choice.startsWith("Skills")) await configureSkills(ctx, prefs); + else if (choice.startsWith("Budget")) await configureBudget(ctx, prefs); + else if (choice.startsWith("Notifications")) await configureNotifications(ctx, prefs); + else if (choice.startsWith("Advanced")) await configureAdvanced(ctx, prefs); } // ─── Serialize to frontmatter ───────────────────────────────────────────