fix(coding-agent): respect quietStartup on /reload while keeping diagnostics (fixes #1336)

This commit is contained in:
Mario Zechner 2026-02-06 18:30:25 +01:00
parent e9f94ba6c3
commit fe6f4d3a9d
2 changed files with 197 additions and 96 deletions

View file

@ -858,119 +858,137 @@ export class InteractiveMode {
return lines.join("\n"); return lines.join("\n");
} }
private showLoadedResources(options?: { extensionPaths?: string[]; force?: boolean }): void { private showLoadedResources(options?: {
const shouldShow = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup(); extensionPaths?: string[];
if (!shouldShow) { force?: boolean;
showDiagnosticsWhenQuiet?: boolean;
}): void {
const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
if (!showListing && !showDiagnostics) {
return; return;
} }
const metadata = this.session.resourceLoader.getPathMetadata(); const metadata = this.session.resourceLoader.getPathMetadata();
const sectionHeader = (name: string, color: ThemeColor = "mdHeading") => theme.fg(color, `[${name}]`); const sectionHeader = (name: string, color: ThemeColor = "mdHeading") => theme.fg(color, `[${name}]`);
const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles; const skillsResult = this.session.resourceLoader.getSkills();
if (contextFiles.length > 0) { const promptsResult = this.session.resourceLoader.getPrompts();
this.chatContainer.addChild(new Spacer(1)); const themesResult = this.session.resourceLoader.getThemes();
const contextList = contextFiles.map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`)).join("\n");
this.chatContainer.addChild(new Text(`${sectionHeader("Context")}\n${contextList}`, 0, 0));
this.chatContainer.addChild(new Spacer(1));
}
const skills = this.session.resourceLoader.getSkills().skills; if (showListing) {
if (skills.length > 0) { const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
const skillPaths = skills.map((s) => s.filePath); if (contextFiles.length > 0) {
const groups = this.buildScopeGroups(skillPaths, metadata); this.chatContainer.addChild(new Spacer(1));
const skillList = this.formatScopeGroups(groups, { const contextList = contextFiles
formatPath: (p) => this.formatDisplayPath(p), .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
formatPackagePath: (p, source) => this.getShortPath(p, source), .join("\n");
}); this.chatContainer.addChild(new Text(`${sectionHeader("Context")}\n${contextList}`, 0, 0));
this.chatContainer.addChild(new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0)); this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Spacer(1)); }
}
const skillDiagnostics = this.session.resourceLoader.getSkills().diagnostics; const skills = skillsResult.skills;
if (skillDiagnostics.length > 0) { if (skills.length > 0) {
const warningLines = this.formatDiagnostics(skillDiagnostics, metadata); const skillPaths = skills.map((s) => s.filePath);
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0)); const groups = this.buildScopeGroups(skillPaths, metadata);
this.chatContainer.addChild(new Spacer(1)); const skillList = this.formatScopeGroups(groups, {
} formatPath: (p) => this.formatDisplayPath(p),
formatPackagePath: (p, source) => this.getShortPath(p, source),
});
this.chatContainer.addChild(new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0));
this.chatContainer.addChild(new Spacer(1));
}
const templates = this.session.promptTemplates; const templates = this.session.promptTemplates;
if (templates.length > 0) { if (templates.length > 0) {
const templatePaths = templates.map((t) => t.filePath); const templatePaths = templates.map((t) => t.filePath);
const groups = this.buildScopeGroups(templatePaths, metadata); const groups = this.buildScopeGroups(templatePaths, metadata);
const templateByPath = new Map(templates.map((t) => [t.filePath, t])); const templateByPath = new Map(templates.map((t) => [t.filePath, t]));
const templateList = this.formatScopeGroups(groups, { const templateList = this.formatScopeGroups(groups, {
formatPath: (p) => { formatPath: (p) => {
const template = templateByPath.get(p); const template = templateByPath.get(p);
return template ? `/${template.name}` : this.formatDisplayPath(p); return template ? `/${template.name}` : this.formatDisplayPath(p);
}, },
formatPackagePath: (p) => { formatPackagePath: (p) => {
const template = templateByPath.get(p); const template = templateByPath.get(p);
return template ? `/${template.name}` : this.formatDisplayPath(p); return template ? `/${template.name}` : this.formatDisplayPath(p);
}, },
}); });
this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0)); this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0));
this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Spacer(1));
} }
const promptDiagnostics = this.session.resourceLoader.getPrompts().diagnostics; const extensionPaths = options?.extensionPaths ?? [];
if (promptDiagnostics.length > 0) { if (extensionPaths.length > 0) {
const warningLines = this.formatDiagnostics(promptDiagnostics, metadata); const groups = this.buildScopeGroups(extensionPaths, metadata);
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0)); const extList = this.formatScopeGroups(groups, {
this.chatContainer.addChild(new Spacer(1)); formatPath: (p) => this.formatDisplayPath(p),
} formatPackagePath: (p, source) => this.getShortPath(p, source),
});
this.chatContainer.addChild(new Text(`${sectionHeader("Extensions", "mdHeading")}\n${extList}`, 0, 0));
this.chatContainer.addChild(new Spacer(1));
}
const extensionPaths = options?.extensionPaths ?? []; // Show loaded themes (excluding built-in)
if (extensionPaths.length > 0) { const loadedThemes = themesResult.themes;
const groups = this.buildScopeGroups(extensionPaths, metadata); const customThemes = loadedThemes.filter((t) => t.sourcePath);
const extList = this.formatScopeGroups(groups, { if (customThemes.length > 0) {
formatPath: (p) => this.formatDisplayPath(p), const themePaths = customThemes.map((t) => t.sourcePath!);
formatPackagePath: (p, source) => this.getShortPath(p, source), const groups = this.buildScopeGroups(themePaths, metadata);
}); const themeList = this.formatScopeGroups(groups, {
this.chatContainer.addChild(new Text(`${sectionHeader("Extensions", "mdHeading")}\n${extList}`, 0, 0)); formatPath: (p) => this.formatDisplayPath(p),
this.chatContainer.addChild(new Spacer(1)); formatPackagePath: (p, source) => this.getShortPath(p, source),
} });
this.chatContainer.addChild(new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0));
const extensionDiagnostics: ResourceDiagnostic[] = []; this.chatContainer.addChild(new Spacer(1));
const extensionErrors = this.session.resourceLoader.getExtensions().errors;
if (extensionErrors.length > 0) {
for (const error of extensionErrors) {
extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
} }
} }
const commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? []; if (showDiagnostics) {
extensionDiagnostics.push(...commandDiagnostics); 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 shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? []; const promptDiagnostics = promptsResult.diagnostics;
extensionDiagnostics.push(...shortcutDiagnostics); if (promptDiagnostics.length > 0) {
const warningLines = this.formatDiagnostics(promptDiagnostics, metadata);
this.chatContainer.addChild(
new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0),
);
this.chatContainer.addChild(new Spacer(1));
}
if (extensionDiagnostics.length > 0) { const extensionDiagnostics: ResourceDiagnostic[] = [];
const warningLines = this.formatDiagnostics(extensionDiagnostics, metadata); const extensionErrors = this.session.resourceLoader.getExtensions().errors;
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0)); if (extensionErrors.length > 0) {
this.chatContainer.addChild(new Spacer(1)); for (const error of extensionErrors) {
} extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
}
}
// Show loaded themes (excluding built-in) const commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? [];
const loadedThemes = this.session.resourceLoader.getThemes().themes; extensionDiagnostics.push(...commandDiagnostics);
const customThemes = loadedThemes.filter((t) => t.sourcePath);
if (customThemes.length > 0) {
const themePaths = customThemes.map((t) => t.sourcePath!);
const groups = this.buildScopeGroups(themePaths, metadata);
const themeList = this.formatScopeGroups(groups, {
formatPath: (p) => this.formatDisplayPath(p),
formatPackagePath: (p, source) => this.getShortPath(p, source),
});
this.chatContainer.addChild(new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0));
this.chatContainer.addChild(new Spacer(1));
}
const themeDiagnostics = this.session.resourceLoader.getThemes().diagnostics; const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];
if (themeDiagnostics.length > 0) { extensionDiagnostics.push(...shortcutDiagnostics);
const warningLines = this.formatDiagnostics(themeDiagnostics, metadata);
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0)); if (extensionDiagnostics.length > 0) {
this.chatContainer.addChild(new Spacer(1)); const warningLines = this.formatDiagnostics(extensionDiagnostics, metadata);
this.chatContainer.addChild(
new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0),
);
this.chatContainer.addChild(new Spacer(1));
}
const themeDiagnostics = themesResult.diagnostics;
if (themeDiagnostics.length > 0) {
const warningLines = this.formatDiagnostics(themeDiagnostics, metadata);
this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
this.chatContainer.addChild(new Spacer(1));
}
} }
} }
@ -3728,7 +3746,11 @@ export class InteractiveMode {
} }
this.rebuildChatFromMessages(); this.rebuildChatFromMessages();
dismissLoader(this.editor as Component); dismissLoader(this.editor as Component);
this.showLoadedResources({ extensionPaths: runner?.getExtensionPaths() ?? [], force: true }); this.showLoadedResources({
extensionPaths: runner?.getExtensionPaths() ?? [],
force: false,
showDiagnosticsWhenQuiet: true,
});
const modelsJsonError = this.session.modelRegistry.getError(); const modelsJsonError = this.session.modelRegistry.getError();
if (modelsJsonError) { if (modelsJsonError) {
this.showError(`models.json error: ${modelsJsonError}`); this.showError(`models.json error: ${modelsJsonError}`);

View file

@ -9,6 +9,10 @@ function renderLastLine(container: Container, width = 120): string {
return last.render(width).join("\n"); return last.render(width).join("\n");
} }
function renderAll(container: Container, width = 120): string {
return container.children.flatMap((child) => child.render(width)).join("\n");
}
describe("InteractiveMode.showStatus", () => { describe("InteractiveMode.showStatus", () => {
beforeAll(() => { beforeAll(() => {
// showStatus uses the global theme instance // showStatus uses the global theme instance
@ -55,3 +59,78 @@ describe("InteractiveMode.showStatus", () => {
expect(renderLastLine(fakeThis.chatContainer)).toContain("STATUS_TWO"); expect(renderLastLine(fakeThis.chatContainer)).toContain("STATUS_TWO");
}); });
}); });
describe("InteractiveMode.showLoadedResources", () => {
beforeAll(() => {
initTheme("dark");
});
function createShowLoadedResourcesThis(options: {
quietStartup: boolean;
verbose?: boolean;
skills?: Array<{ filePath: string }>;
skillDiagnostics?: Array<{ type: "warning" | "error" | "collision"; message: string }>;
}) {
const fakeThis: any = {
options: { verbose: options.verbose ?? false },
chatContainer: new Container(),
settingsManager: {
getQuietStartup: () => options.quietStartup,
},
session: {
promptTemplates: [],
extensionRunner: undefined,
resourceLoader: {
getPathMetadata: () => new Map(),
getAgentsFiles: () => ({ agentsFiles: [] }),
getSkills: () => ({
skills: options.skills ?? [],
diagnostics: options.skillDiagnostics ?? [],
}),
getPrompts: () => ({ prompts: [], diagnostics: [] }),
getExtensions: () => ({ errors: [] }),
getThemes: () => ({ themes: [], diagnostics: [] }),
},
},
formatDisplayPath: (p: string) => p,
buildScopeGroups: () => [],
formatScopeGroups: () => "resource-list",
getShortPath: (p: string) => p,
formatDiagnostics: () => "diagnostics",
};
return fakeThis;
}
test("does not show verbose listing on quiet startup during reload", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: true,
skills: [{ filePath: "/tmp/skill/SKILL.md" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
extensionPaths: ["/tmp/ext/index.ts"],
force: false,
showDiagnosticsWhenQuiet: true,
});
expect(fakeThis.chatContainer.children).toHaveLength(0);
});
test("still shows diagnostics on quiet startup when requested", () => {
const fakeThis = createShowLoadedResourcesThis({
quietStartup: true,
skills: [{ filePath: "/tmp/skill/SKILL.md" }],
skillDiagnostics: [{ type: "warning", message: "duplicate skill name" }],
});
(InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, {
force: false,
showDiagnosticsWhenQuiet: true,
});
const output = renderAll(fakeThis.chatContainer);
expect(output).toContain("[Skill conflicts]");
expect(output).not.toContain("[Skills]");
});
});