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
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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"`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user