refactor: replace serial prefs wizard with categorized menu (#623)
* 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.
This commit is contained in:
parent
369bd8aeb9
commit
2fd4a1da60
1 changed files with 143 additions and 28 deletions
|
|
@ -520,17 +520,87 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte
|
|||
|
||||
// ─── Preferences Wizard ───────────────────────────────────────────────────────
|
||||
|
||||
async function handlePrefsWizard(
|
||||
ctx: ExtensionCommandContext,
|
||||
scope: "global" | "project",
|
||||
): Promise<void> {
|
||||
const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath();
|
||||
const existing = scope === "project" ? loadProjectGSDPreferences() : loadGlobalGSDPreferences();
|
||||
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : {};
|
||||
/** Build short summary strings for each preference category. */
|
||||
function buildCategorySummaries(prefs: Record<string, unknown>): Record<string, string> {
|
||||
// Models
|
||||
const models = prefs.models as Record<string, string> | 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<string, unknown> | 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<string, unknown> | 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<string, boolean> | 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<string, unknown>): Promise<void> {
|
||||
const modelPhases = ["research", "planning", "execution", "completion"] as const;
|
||||
const models: Record<string, string> = (prefs.models as Record<string, string>) ?? {};
|
||||
|
||||
|
|
@ -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<string, unknown>): Promise<void> {
|
||||
const autoSup: Record<string, unknown> = (prefs.auto_supervisor as Record<string, unknown>) ?? {};
|
||||
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<string, unknown>): Promise<void> {
|
||||
const git: Record<string, unknown> = (prefs.git as Record<string, unknown>) ?? {};
|
||||
|
||||
// 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<string, unknown>): Promise<void> {
|
||||
// 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<string, unknown>): Promise<void> {
|
||||
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<string, unknown>): Promise<void> {
|
||||
const notif: Record<string, boolean> = (prefs.notifications as Record<string, boolean>) ?? {};
|
||||
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<string, unknown>): Promise<void> {
|
||||
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<void> {
|
||||
const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath();
|
||||
const existing = scope === "project" ? loadProjectGSDPreferences() : loadGlobalGSDPreferences();
|
||||
const prefs: Record<string, unknown> = 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 ───────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue