| |
| |
| |
| |
| |
|
|
| import { |
| coreEvents, |
| CoreEvent, |
| type SlashCommandConflictsPayload, |
| type SlashCommandConflict, |
| } from '@google/gemini-cli-core'; |
| import { CommandKind } from '../ui/commands/types.js'; |
|
|
| |
| |
| |
| |
| |
| |
| export class SlashCommandConflictHandler { |
| private notifiedConflicts = new Set<string>(); |
| private pendingConflicts: SlashCommandConflict[] = []; |
| private flushTimeout: ReturnType<typeof setTimeout> | null = null; |
|
|
| constructor() { |
| this.handleConflicts = this.handleConflicts.bind(this); |
| } |
|
|
| start() { |
| coreEvents.on(CoreEvent.SlashCommandConflicts, this.handleConflicts); |
| } |
|
|
| stop() { |
| coreEvents.off(CoreEvent.SlashCommandConflicts, this.handleConflicts); |
| if (this.flushTimeout) { |
| clearTimeout(this.flushTimeout); |
| this.flushTimeout = null; |
| } |
| } |
|
|
| private handleConflicts(payload: SlashCommandConflictsPayload) { |
| const newConflicts = payload.conflicts.filter((c) => { |
| |
| const sourceId = |
| c.loserExtensionName || c.loserMcpServerName || c.loserKind; |
| const key = `${c.name}:${sourceId}:${c.renamedTo}`; |
| if (this.notifiedConflicts.has(key)) { |
| return false; |
| } |
| this.notifiedConflicts.add(key); |
| return true; |
| }); |
|
|
| if (newConflicts.length > 0) { |
| this.pendingConflicts.push(...newConflicts); |
| this.scheduleFlush(); |
| } |
| } |
|
|
| private scheduleFlush() { |
| if (this.flushTimeout) { |
| clearTimeout(this.flushTimeout); |
| } |
| |
| this.flushTimeout = setTimeout(() => this.flush(), 500); |
| } |
|
|
| private flush() { |
| this.flushTimeout = null; |
| const conflicts = [...this.pendingConflicts]; |
| this.pendingConflicts = []; |
|
|
| if (conflicts.length === 0) { |
| return; |
| } |
|
|
| |
| const grouped = new Map<string, SlashCommandConflict[]>(); |
| for (const c of conflicts) { |
| const list = grouped.get(c.name) ?? []; |
| list.push(c); |
| grouped.set(c.name, list); |
| } |
|
|
| for (const [name, commandConflicts] of grouped) { |
| if (commandConflicts.length > 1) { |
| this.emitGroupedFeedback(name, commandConflicts); |
| } else { |
| this.emitSingleFeedback(commandConflicts[0]); |
| } |
| } |
| } |
|
|
| |
| |
| |
| private emitGroupedFeedback( |
| name: string, |
| conflicts: SlashCommandConflict[], |
| ): void { |
| const messages = conflicts |
| .map((c) => { |
| const source = this.getSourceDescription( |
| c.loserExtensionName, |
| c.loserKind, |
| c.loserMcpServerName, |
| ); |
| return `- ${this.capitalize(source)} '/${c.name}' was renamed to '/${c.renamedTo}'`; |
| }) |
| .join('\n'); |
|
|
| coreEvents.emitFeedback( |
| 'info', |
| `Conflicts detected for command '/${name}':\n${messages}`, |
| ); |
| } |
|
|
| |
| |
| |
| private emitSingleFeedback(c: SlashCommandConflict): void { |
| const loserSource = this.getSourceDescription( |
| c.loserExtensionName, |
| c.loserKind, |
| c.loserMcpServerName, |
| ); |
| const winnerSource = this.getSourceDescription( |
| c.winnerExtensionName, |
| c.winnerKind, |
| c.winnerMcpServerName, |
| ); |
|
|
| coreEvents.emitFeedback( |
| 'info', |
| `${this.capitalize(loserSource)} '/${c.name}' was renamed to '/${c.renamedTo}' because it conflicts with ${winnerSource}.`, |
| ); |
| } |
|
|
| private capitalize(s: string): string { |
| return s.charAt(0).toUpperCase() + s.slice(1); |
| } |
|
|
| |
| |
| |
| private getSourceDescription( |
| extensionName?: string, |
| kind?: string, |
| mcpServerName?: string, |
| ): string { |
| switch (kind) { |
| case CommandKind.EXTENSION_FILE: |
| return extensionName |
| ? `extension '${extensionName}' command` |
| : 'extension command'; |
| case CommandKind.SKILL: |
| return extensionName |
| ? `extension '${extensionName}' skill` |
| : 'skill command'; |
| case CommandKind.MCP_PROMPT: |
| return mcpServerName |
| ? `MCP server '${mcpServerName}' command` |
| : 'MCP server command'; |
| case CommandKind.USER_FILE: |
| return 'user command'; |
| case CommandKind.WORKSPACE_FILE: |
| return 'workspace command'; |
| case CommandKind.BUILT_IN: |
| return 'built-in command'; |
| default: |
| return 'existing command'; |
| } |
| } |
| } |
|
|