fix(coding-agent): filter out commands conflict with builtins

This commit is contained in:
haoqixu 2026-02-03 01:23:12 +08:00
parent df5b0f76c0
commit 29e2997f41
3 changed files with 57 additions and 8 deletions

View file

@ -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<string>): 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) {

View file

@ -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);

View file

@ -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", () => {