diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index bcdd9fa5..a813b70e 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -170,6 +170,7 @@ export class ExtensionRunner { private switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false }); private shutdownHandler: ShutdownHandler = () => {}; private shortcutDiagnostics: ResourceDiagnostic[] = []; + private commandDiagnostics: ResourceDiagnostic[] = []; constructor( extensions: Extension[], @@ -371,16 +372,31 @@ export class ExtensionRunner { return undefined; } - getRegisteredCommands(): RegisteredCommand[] { + getRegisteredCommands(reserved?: Set): RegisteredCommand[] { + this.commandDiagnostics = []; + const commands: RegisteredCommand[] = []; for (const ext of this.extensions) { for (const command of ext.commands.values()) { + if (reserved?.has(command.name)) { + const message = `Extension command '${command.name}' from ${ext.path} conflicts with built-in commands. Skipping.`; + this.commandDiagnostics.push({ type: "warning", message, path: ext.path }); + if (!this.hasUI()) { + console.warn(message); + } + continue; + } + commands.push(command); } } return commands; } + getCommandDiagnostics(): ResourceDiagnostic[] { + return this.commandDiagnostics; + } + getRegisteredCommandsWithPaths(): Array<{ command: RegisteredCommand; extensionPath: string }> { const result: Array<{ command: RegisteredCommand; extensionPath: string }> = []; for (const ext of this.extensions) { diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 300a7516..91451d1b 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -347,13 +347,14 @@ export class InteractiveMode { })); // Convert extension commands to SlashCommand format - const extensionCommands: SlashCommand[] = (this.session.extensionRunner?.getRegisteredCommands() ?? []).map( - (cmd) => ({ - name: cmd.name, - description: cmd.description ?? "(extension command)", - getArgumentCompletions: cmd.getArgumentCompletions, - }), - ); + const builtinCommandNames = new Set(slashCommands.map((c) => c.name)); + const extensionCommands: SlashCommand[] = ( + this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? [] + ).map((cmd) => ({ + name: cmd.name, + description: cmd.description ?? "(extension command)", + getArgumentCompletions: cmd.getArgumentCompletions, + })); // Build skill commands from session.skills (if enabled) this.skillCommands.clear(); @@ -959,6 +960,9 @@ export class InteractiveMode { } } + const commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? []; + extensionDiagnostics.push(...commandDiagnostics); + const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? []; extensionDiagnostics.push(...shortcutDiagnostics); diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index 6e139ff1..e9747c1d 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -279,6 +279,35 @@ describe("ExtensionRunner", () => { const missing = runner.getCommand("not-exists"); expect(missing).toBeUndefined(); }); + + it("filters out commands conflict with reseved", async () => { + const cmdCode = (name: string) => ` + export default function(pi) { + pi.registerCommand("${name}", { + description: "Test command", + handler: async () => {}, + }); + } + `; + fs.writeFileSync(path.join(extensionsDir, "cmd-a.ts"), cmdCode("cmd-a")); + fs.writeFileSync(path.join(extensionsDir, "cmd-b.ts"), cmdCode("cmd-b")); + + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const result = await discoverAndLoadExtensions([], tempDir, tempDir); + const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry); + const commands = runner.getRegisteredCommands(new Set(["cmd-a"])); + const diagnostics = runner.getCommandDiagnostics(); + + expect(commands.length).toBe(1); + expect(commands.map((c) => c.name).sort()).toEqual(["cmd-b"]); + + expect(diagnostics.length).toBe(1); + expect(diagnostics[0].path).toEqual(path.join(extensionsDir, "cmd-a.ts")); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("conflicts with built-in command")); + warnSpy.mockRestore(); + }); }); describe("error handling", () => {