diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 446998b7..cea184d2 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -858,119 +858,137 @@ export class InteractiveMode { return lines.join("\n"); } - private showLoadedResources(options?: { extensionPaths?: string[]; force?: boolean }): void { - const shouldShow = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup(); - if (!shouldShow) { + private showLoadedResources(options?: { + extensionPaths?: string[]; + 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; } const metadata = this.session.resourceLoader.getPathMetadata(); - const sectionHeader = (name: string, color: ThemeColor = "mdHeading") => theme.fg(color, `[${name}]`); - const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles; - if (contextFiles.length > 0) { - this.chatContainer.addChild(new Spacer(1)); - 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 skillsResult = this.session.resourceLoader.getSkills(); + const promptsResult = this.session.resourceLoader.getPrompts(); + const themesResult = this.session.resourceLoader.getThemes(); - const skills = this.session.resourceLoader.getSkills().skills; - if (skills.length > 0) { - const skillPaths = skills.map((s) => s.filePath); - const groups = this.buildScopeGroups(skillPaths, metadata); - 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)); - } + if (showListing) { + const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles; + if (contextFiles.length > 0) { + this.chatContainer.addChild(new Spacer(1)); + 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 skillDiagnostics = this.session.resourceLoader.getSkills().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 skills = skillsResult.skills; + if (skills.length > 0) { + const skillPaths = skills.map((s) => s.filePath); + const groups = this.buildScopeGroups(skillPaths, metadata); + 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; - if (templates.length > 0) { - const templatePaths = templates.map((t) => t.filePath); - const groups = this.buildScopeGroups(templatePaths, metadata); - const templateByPath = new Map(templates.map((t) => [t.filePath, t])); - const templateList = this.formatScopeGroups(groups, { - formatPath: (p) => { - const template = templateByPath.get(p); - return template ? `/${template.name}` : this.formatDisplayPath(p); - }, - formatPackagePath: (p) => { - const template = templateByPath.get(p); - return template ? `/${template.name}` : this.formatDisplayPath(p); - }, - }); - this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); - } + const templates = this.session.promptTemplates; + if (templates.length > 0) { + const templatePaths = templates.map((t) => t.filePath); + const groups = this.buildScopeGroups(templatePaths, metadata); + const templateByPath = new Map(templates.map((t) => [t.filePath, t])); + const templateList = this.formatScopeGroups(groups, { + formatPath: (p) => { + const template = templateByPath.get(p); + return template ? `/${template.name}` : this.formatDisplayPath(p); + }, + formatPackagePath: (p) => { + const template = templateByPath.get(p); + return template ? `/${template.name}` : this.formatDisplayPath(p); + }, + }); + this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0)); + this.chatContainer.addChild(new Spacer(1)); + } - const promptDiagnostics = this.session.resourceLoader.getPrompts().diagnostics; - 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)); - } + const extensionPaths = options?.extensionPaths ?? []; + if (extensionPaths.length > 0) { + const groups = this.buildScopeGroups(extensionPaths, metadata); + const extList = this.formatScopeGroups(groups, { + 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 ?? []; - if (extensionPaths.length > 0) { - const groups = this.buildScopeGroups(extensionPaths, metadata); - const extList = this.formatScopeGroups(groups, { - 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 extensionDiagnostics: ResourceDiagnostic[] = []; - 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 }); + // Show loaded themes (excluding built-in) + const loadedThemes = themesResult.themes; + 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 commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? []; - extensionDiagnostics.push(...commandDiagnostics); + 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 shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? []; - extensionDiagnostics.push(...shortcutDiagnostics); + const promptDiagnostics = promptsResult.diagnostics; + 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 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 extensionDiagnostics: ResourceDiagnostic[] = []; + 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 }); + } + } - // Show loaded themes (excluding built-in) - const loadedThemes = this.session.resourceLoader.getThemes().themes; - 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 commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? []; + extensionDiagnostics.push(...commandDiagnostics); - const themeDiagnostics = this.session.resourceLoader.getThemes().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)); + const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? []; + extensionDiagnostics.push(...shortcutDiagnostics); + + if (extensionDiagnostics.length > 0) { + 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(); 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(); if (modelsJsonError) { this.showError(`models.json error: ${modelsJsonError}`); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index bab9085e..9c74fb72 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -9,6 +9,10 @@ function renderLastLine(container: Container, width = 120): string { 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", () => { beforeAll(() => { // showStatus uses the global theme instance @@ -55,3 +59,78 @@ describe("InteractiveMode.showStatus", () => { 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]"); + }); +});