Merge pull request #457 from deseltrus/fix/skill-diagnostics-ux
fix(ux): improve preferences wizard and skill diagnostics
This commit is contained in:
commit
c0ab967f35
5 changed files with 75 additions and 36 deletions
|
|
@ -33,7 +33,7 @@ export class ExtensionInputComponent extends Container implements Focusable {
|
|||
|
||||
constructor(
|
||||
title: string,
|
||||
_placeholder: string | undefined,
|
||||
placeholder: string | undefined,
|
||||
onSubmit: (value: string) => void,
|
||||
onCancel: () => void,
|
||||
opts?: ExtensionInputOptions,
|
||||
|
|
@ -61,6 +61,9 @@ export class ExtensionInputComponent extends Container implements Focusable {
|
|||
}
|
||||
|
||||
this.input = new Input();
|
||||
if (placeholder) {
|
||||
this.input.placeholder = placeholder;
|
||||
}
|
||||
this.addChild(this.input);
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(`${keyHint("selectConfirm", "submit")} ${keyHint("selectCancel", "cancel")}`, 1, 0));
|
||||
|
|
|
|||
|
|
@ -998,9 +998,20 @@ export class InteractiveMode {
|
|||
if (showDiagnostics) {
|
||||
const skillDiagnostics = skillsResult.diagnostics;
|
||||
if (skillDiagnostics.length > 0) {
|
||||
const warningLines = this.formatDiagnostics(skillDiagnostics, metadata);
|
||||
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const collisionDiags = skillDiagnostics.filter(d => d.type === "collision");
|
||||
const issueDiags = skillDiagnostics.filter(d => d.type !== "collision");
|
||||
|
||||
if (collisionDiags.length > 0) {
|
||||
const collisionLines = this.formatDiagnostics(collisionDiags, metadata);
|
||||
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${collisionLines}`, 0, 0));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
}
|
||||
|
||||
if (issueDiags.length > 0) {
|
||||
const issueLines = this.formatDiagnostics(issueDiags, metadata);
|
||||
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill issues]")}\n${issueLines}`, 0, 0));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
}
|
||||
}
|
||||
|
||||
const promptDiagnostics = promptsResult.diagnostics;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export class Input implements Component, Focusable {
|
|||
private cursor: number = 0; // Cursor position in the value
|
||||
public onSubmit?: (value: string) => void;
|
||||
public onEscape?: () => void;
|
||||
public placeholder: string = "";
|
||||
|
||||
/** Focusable interface - set by TUI when focus changes */
|
||||
focused: boolean = false;
|
||||
|
|
@ -440,6 +441,16 @@ export class Input implements Component, Focusable {
|
|||
return [prompt];
|
||||
}
|
||||
|
||||
// Show placeholder when value is empty
|
||||
if (this.value === "" && this.placeholder) {
|
||||
const placeholderText = this.placeholder.slice(0, availableWidth - 1);
|
||||
const marker = this.focused ? CURSOR_MARKER : "";
|
||||
const cursorChar = "\x1b[7m \x1b[27m"; // inverse space for cursor
|
||||
const dimPlaceholder = `\x1b[2m${placeholderText}\x1b[22m`; // dim text
|
||||
const padding = " ".repeat(Math.max(0, availableWidth - visibleWidth(placeholderText) - 1));
|
||||
return [prompt + marker + cursorChar + dimPlaceholder + padding];
|
||||
}
|
||||
|
||||
let visibleText = "";
|
||||
let cursorDisplay = this.cursor;
|
||||
|
||||
|
|
|
|||
|
|
@ -215,20 +215,16 @@ export async function fireStatusViaCommand(
|
|||
async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
||||
const trimmed = args.trim();
|
||||
|
||||
if (trimmed === "" || trimmed === "global") {
|
||||
if (trimmed === "" || trimmed === "global" || trimmed === "wizard" || trimmed === "setup"
|
||||
|| trimmed === "wizard global" || trimmed === "setup global") {
|
||||
await ensurePreferencesFile(getGlobalGSDPreferencesPath(), ctx, "global");
|
||||
await handlePrefsWizard(ctx, "global");
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "project") {
|
||||
if (trimmed === "project" || trimmed === "wizard project" || trimmed === "setup project") {
|
||||
await ensurePreferencesFile(getProjectGSDPreferencesPath(), ctx, "project");
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "wizard" || trimmed === "setup" || trimmed === "wizard global" || trimmed === "setup global"
|
||||
|| trimmed === "wizard project" || trimmed === "setup project") {
|
||||
const scope = trimmed.includes("project") ? "project" : "global";
|
||||
await handlePrefsWizard(ctx, scope);
|
||||
await handlePrefsWizard(ctx, "project");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -319,22 +315,41 @@ async function handlePrefsWizard(
|
|||
const modelPhases = ["research", "planning", "execution", "completion"] as const;
|
||||
const models: Record<string, string> = (prefs.models as Record<string, string>) ?? {};
|
||||
|
||||
for (const phase of modelPhases) {
|
||||
const current = models[phase] ?? "";
|
||||
const input = await ctx.ui.input(
|
||||
`Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`,
|
||||
current || "e.g. claude-sonnet-4-20250514",
|
||||
);
|
||||
if (input !== null && input !== undefined) {
|
||||
const val = input.trim();
|
||||
if (val) {
|
||||
models[phase] = val;
|
||||
} else if (current) {
|
||||
// User cleared it — remove
|
||||
delete models[phase];
|
||||
const availableModels = ctx.modelRegistry.getAvailable();
|
||||
if (availableModels.length > 0) {
|
||||
const modelOptions = availableModels.map(m => `${m.id} · ${m.provider}`);
|
||||
modelOptions.push("(keep current)", "(clear)");
|
||||
|
||||
for (const phase of modelPhases) {
|
||||
const current = models[phase] ?? "";
|
||||
const title = `Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`;
|
||||
const choice = await ctx.ui.select(title, modelOptions);
|
||||
|
||||
if (choice && choice !== "(keep current)") {
|
||||
if (choice === "(clear)") {
|
||||
delete models[phase];
|
||||
} else {
|
||||
models[phase] = choice.split(" · ")[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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(
|
||||
`Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`,
|
||||
current || "e.g. claude-sonnet-4-20250514",
|
||||
);
|
||||
if (input !== null && input !== undefined) {
|
||||
const val = input.trim();
|
||||
if (val) {
|
||||
models[phase] = val;
|
||||
} else if (current) {
|
||||
delete models[phase];
|
||||
}
|
||||
}
|
||||
}
|
||||
// null/undefined = Escape/skip — keep existing value
|
||||
}
|
||||
if (Object.keys(models).length > 0) {
|
||||
prefs.models = models;
|
||||
|
|
@ -452,8 +467,7 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
|
|||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
lines.push(`${prefix}${key}: []`);
|
||||
return;
|
||||
return; // Omit empty arrays — avoids parse/serialize cycle bug with "[]" strings
|
||||
}
|
||||
lines.push(`${prefix}${key}:`);
|
||||
for (const item of value) {
|
||||
|
|
@ -484,8 +498,7 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
|
|||
if (typeof value === "object") {
|
||||
const entries = Object.entries(value as Record<string, unknown>);
|
||||
if (entries.length === 0) {
|
||||
lines.push(`${prefix}${key}: {}`);
|
||||
return;
|
||||
return; // Omit empty objects — avoids parse/serialize cycle bug with "{}" strings
|
||||
}
|
||||
lines.push(`${prefix}${key}:`);
|
||||
for (const [k, v] of entries) {
|
||||
|
|
@ -538,7 +551,4 @@ async function ensurePreferencesFile(
|
|||
ctx.ui.notify(`Using existing ${scope} GSD skill preferences at ${path}`, "info");
|
||||
}
|
||||
|
||||
await ctx.waitForIdle();
|
||||
await ctx.reload();
|
||||
ctx.ui.notify(`Edit ${path} to update ${scope} GSD skill preferences.`, "info");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -482,16 +482,20 @@ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
|
|||
return root as GSDPreferences;
|
||||
}
|
||||
|
||||
function parseScalar(value: string): string | number | boolean {
|
||||
function parseScalar(value: string): unknown {
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
// Recognize empty array/object literals (with or without surrounding quotes)
|
||||
const unquoted = value.replace(/^['\"]|['\"]$/g, "");
|
||||
if (unquoted === "[]") return [];
|
||||
if (unquoted === "{}") return {};
|
||||
if (/^-?\d+$/.test(value)) {
|
||||
const n = Number(value);
|
||||
// Keep large integers (e.g. Discord channel IDs) as strings to avoid precision loss
|
||||
if (Number.isSafeInteger(n)) return n;
|
||||
return value;
|
||||
}
|
||||
return value.replace(/^['\"]|['\"]$/g, "");
|
||||
return unquoted;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue