From 05e2e9d170979c253ac49d9f61d7366865588049 Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Thu, 23 Apr 2026 21:03:26 +0200 Subject: [PATCH] fix(coding-agent): strip trailing `index.js|ts` from extension labels in startup banner (#3596) Strip `index.ts` and `index.js` from extension display paths in both compact and expanded views of the startup banner. This makes the list more readable by removing redundant filename noise. Changes: - Add `formatExtensionDisplayPath()` helper to centralize path formatting - Update `getCompactExtensionLabels()` to strip index files from segments - Update expanded view formatting to use the new helper - Add comprehensive tests for both views Fixes #3549 Fixes #3559 --- .../src/modes/interactive/interactive-mode.ts | 28 +- .../test/interactive-mode-status.test.ts | 240 +++++++++++++++++- 2 files changed, 253 insertions(+), 15 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index cc015ad5..fd38f14d 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -910,6 +910,12 @@ export class InteractiveMode { return result; } + private formatExtensionDisplayPath(path: string): string { + let result = this.formatDisplayPath(path); + result = result.replace(/\/index\.ts$/, "").replace(/\/index\.js$/, ""); + return result; + } + private formatContextPath(p: string): string { const cwd = path.resolve(this.sessionManager.getCwd()); const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p); @@ -1044,11 +1050,18 @@ export class InteractiveMode { private getCompactExtensionLabels(extensions: Array<{ path: string; sourceInfo?: SourceInfo }>): string[] { const nonPackageExtensions = extensions - .map((extension) => ({ - path: extension.path, - sourceInfo: extension.sourceInfo, - segments: this.getCompactDisplayPathSegments(extension.path), - })) + .map((extension) => { + const segments = this.getCompactDisplayPathSegments(extension.path); + const lastSegment = segments[segments.length - 1]; + if (segments.length > 1 && (lastSegment === "index.ts" || lastSegment === "index.js")) { + segments.pop(); + } + return { + path: extension.path, + sourceInfo: extension.sourceInfo, + segments, + }; + }) .filter((extension) => !this.isPackageSource(extension.sourceInfo)); return extensions.map((extension) => { @@ -1373,8 +1386,9 @@ export class InteractiveMode { if (extensions.length > 0) { const groups = this.buildScopeGroups(extensions); const extList = this.formatScopeGroups(groups, { - formatPath: (item) => this.formatDisplayPath(item.path), - formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo), + formatPath: (item) => this.formatExtensionDisplayPath(item.path), + formatPackagePath: (item) => + this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)), }); const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions)); addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading"); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 6da1d7f8..6ac50c08 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -262,6 +262,8 @@ describe("InteractiveMode.showLoadedResources", () => { }, }, formatDisplayPath: (p: string) => (InteractiveMode as any).prototype.formatDisplayPath.call(fakeThis, p), + formatExtensionDisplayPath: (p: string) => + (InteractiveMode as any).prototype.formatExtensionDisplayPath.call(fakeThis, p), formatContextPath: (p: string) => (InteractiveMode as any).prototype.formatContextPath.call(fakeThis, p), getStartupExpansionState: () => (InteractiveMode as any).prototype.getStartupExpansionState.call(fakeThis), buildScopeGroups: () => [], @@ -482,7 +484,7 @@ describe("InteractiveMode.showLoadedResources", () => { expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` "[Extensions] - @scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index/index.ts, pi-markdown-preview, user-index/index.ts"`); + @scope/pi-scoped, answer.ts, cli-extension.ts, HazAT/pi-interactive-subagents, HazAT/pi-interactive-subagents:subagents, local-index, pi-markdown-preview, user-index"`); }); test("adds more parent folders until local extension labels are unique", () => { @@ -528,9 +530,231 @@ describe("InteractiveMode.showLoadedResources", () => { expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` "[Extensions] - alpha/one/index.ts, beta/one/index.ts, gamma/one/index.ts"`); + alpha/one, beta/one, gamma/one"`); }); + test("strips index.ts from local extension label, showing parent dir", () => { + const extensions: ExtensionFixture[] = [ + { + path: "/tmp/extensions/plan-mode/index.ts", + sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.ts", { + source: "local", + scope: "project", + origin: "top-level", + baseDir: "/tmp/extensions", + }), + }, + ]; + + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + extensions, + useRealScopeGroups: true, + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` +"[Extensions] + plan-mode"`); + }); + + test("strips index.js from local extension label, showing parent dir", () => { + const extensions: ExtensionFixture[] = [ + { + path: "/tmp/extensions/plan-mode/index.js", + sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.js", { + source: "local", + scope: "project", + origin: "top-level", + baseDir: "/tmp/extensions", + }), + }, + ]; + + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + extensions, + useRealScopeGroups: true, + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` +"[Extensions] + plan-mode"`); + }); + + test("mixed single-file and subdirectory index.ts extensions strip index.ts", () => { + const extensions: ExtensionFixture[] = [ + { + path: "/tmp/extensions/webfetch.ts", + sourceInfo: createSourceInfo("/tmp/extensions/webfetch.ts", { + source: "local", + scope: "project", + origin: "top-level", + baseDir: "/tmp/extensions", + }), + }, + { + path: "/tmp/extensions/plan-mode/index.ts", + sourceInfo: createSourceInfo("/tmp/extensions/plan-mode/index.ts", { + source: "local", + scope: "project", + origin: "top-level", + baseDir: "/tmp/extensions", + }), + }, + ]; + + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + extensions, + useRealScopeGroups: true, + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` +"[Extensions] + plan-mode, webfetch.ts"`); + }); + + test("multiple index.ts with unique parent dirs need no disambiguation", () => { + const extensions: ExtensionFixture[] = [ + { + path: "/tmp/extensions/foo/index.ts", + sourceInfo: createSourceInfo("/tmp/extensions/foo/index.ts", { + source: "local", + scope: "project", + origin: "top-level", + baseDir: "/tmp/extensions", + }), + }, + { + path: "/tmp/extensions/bar/index.ts", + sourceInfo: createSourceInfo("/tmp/extensions/bar/index.ts", { + source: "local", + scope: "project", + origin: "top-level", + baseDir: "/tmp/extensions", + }), + }, + ]; + + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + extensions, + useRealScopeGroups: true, + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` +"[Extensions] + bar, foo"`); + }); + + test("multiple index.ts with same parent dir name disambiguated with grandparent", () => { + const extensions: ExtensionFixture[] = [ + { + path: "/tmp/alpha/tools/index.ts", + sourceInfo: createSourceInfo("/tmp/alpha/tools/index.ts", { + source: "cli", + scope: "temporary", + origin: "top-level", + baseDir: "/tmp/alpha", + }), + }, + { + path: "/tmp/beta/tools/index.ts", + sourceInfo: createSourceInfo("/tmp/beta/tools/index.ts", { + source: "cli", + scope: "temporary", + origin: "top-level", + baseDir: "/tmp/beta", + }), + }, + ]; + + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + extensions, + useRealScopeGroups: true, + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` +"[Extensions] + alpha/tools, beta/tools"`); + }); + + test("non-index file in subdirectory stays as filename", () => { + const extensions: ExtensionFixture[] = [ + { + path: "/tmp/extensions/my-ext/main.ts", + sourceInfo: createSourceInfo("/tmp/extensions/my-ext/main.ts", { + source: "local", + scope: "project", + origin: "top-level", + baseDir: "/tmp/extensions", + }), + }, + ]; + + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + extensions, + useRealScopeGroups: true, + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` +"[Extensions] + main.ts"`); + }); + + test("package extensions still strip index.ts correctly (regression guard)", () => { + const extensions: ExtensionFixture[] = [ + { + path: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts", + sourceInfo: createSourceInfo("/tmp/project/.pi/npm/node_modules/pi-markdown-preview/extensions/index.ts", { + source: "npm:pi-markdown-preview", + scope: "project", + origin: "package", + baseDir: "/tmp/project/.pi/npm/node_modules/pi-markdown-preview", + }), + }, + ]; + + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + extensions, + useRealScopeGroups: true, + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toMatchInlineSnapshot(` +"[Extensions] + pi-markdown-preview"`); + }); test("captures mixed extension layouts in expanded output", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: false, @@ -547,16 +771,16 @@ describe("InteractiveMode.showLoadedResources", () => { "[Extensions] project /tmp/project/.pi/extensions/answer.ts - /tmp/project/.pi/extensions/local-index/index.ts + /tmp/project/.pi/extensions/local-index git:github.com/HazAT/pi-interactive-subagents - extensions/index.ts - extensions/subagents/index.ts + extensions + extensions/subagents npm:@scope/pi-scoped - extensions/index.ts + extensions npm:pi-markdown-preview - extensions/index.ts + extensions user - /tmp/agent/extensions/user-index/index.ts + /tmp/agent/extensions/user-index path /tmp/temp/cli-extension.ts"`); });