From f819dd0c27c48434537b2c0aaca52713fb4b2c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D0=B8=D1=8F?= Date: Tue, 14 Apr 2026 14:56:29 +0300 Subject: [PATCH] fix(team): preserve provider model ids and codex slash suggestions --- .../team/dialogs/TeamModelSelector.tsx | 9 +- .../team/dialogs/launchDialogPrefill.ts | 15 ++- .../team/messages/MessageComposer.tsx | 22 +++- src/renderer/store/slices/teamSlice.ts | 81 ++++++------ src/renderer/utils/providerSlashCommands.ts | 122 ++++++++++++++++++ src/renderer/utils/skillCommandSuggestions.ts | 5 +- src/renderer/utils/teamModelContext.ts | 24 ++++ .../components/team/TeamModelSelector.test.ts | 1 + .../team/dialogs/launchDialogPrefill.test.ts | 29 ++++- .../utils/providerSlashCommands.test.ts | 27 ++++ .../utils/skillCommandSuggestions.test.ts | 50 +++++-- 11 files changed, 315 insertions(+), 70 deletions(-) create mode 100644 src/renderer/utils/providerSlashCommands.ts create mode 100644 test/renderer/utils/providerSlashCommands.test.ts diff --git a/src/renderer/components/team/dialogs/TeamModelSelector.tsx b/src/renderer/components/team/dialogs/TeamModelSelector.tsx index 2afa54b6..c0175dbf 100644 --- a/src/renderer/components/team/dialogs/TeamModelSelector.tsx +++ b/src/renderer/components/team/dialogs/TeamModelSelector.tsx @@ -16,7 +16,6 @@ import { GEMINI_UI_DISABLED_REASON, isGeminiUiFrozen, } from '@renderer/utils/geminiUiFreeze'; -import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext'; import { doesTeamModelCarryProviderBrand, getProviderScopedTeamModelLabel, @@ -27,6 +26,7 @@ import { normalizeTeamModelForUi, TEAM_MODEL_UI_DISABLED_BADGE_LABEL, } from '@renderer/utils/teamModelCatalog'; +import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext'; import { Info } from 'lucide-react'; export { getProviderScopedTeamModelLabel } from '@renderer/utils/teamModelCatalog'; @@ -100,8 +100,11 @@ export function computeEffectiveTeamModel( limitContext: boolean, providerId: 'anthropic' | 'codex' | 'gemini' = 'anthropic' ): string | undefined { - const base = stripTrailingOneMillionSuffixes(selectedModel); - if (providerId !== 'anthropic') return base; + if (providerId !== 'anthropic') { + return selectedModel.trim() || undefined; + } + + const base = extractProviderScopedBaseModel(selectedModel, providerId); if (limitContext) return base; if (base === 'haiku') return base; return base ? `${base}[1m]` : 'opus[1m]'; diff --git a/src/renderer/components/team/dialogs/launchDialogPrefill.ts b/src/renderer/components/team/dialogs/launchDialogPrefill.ts index c359b14b..5651a207 100644 --- a/src/renderer/components/team/dialogs/launchDialogPrefill.ts +++ b/src/renderer/components/team/dialogs/launchDialogPrefill.ts @@ -1,6 +1,6 @@ import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze'; -import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext'; import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability'; +import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext'; import { isLeadMember } from '@shared/utils/leadDetection'; import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; @@ -31,12 +31,15 @@ interface LaunchDialogPrefillResult { limitContext: boolean; } -function normalizeModelCandidate(model: string | undefined): string { +function normalizeModelCandidate( + model: string | undefined, + providerId: TeamProviderId | undefined +): string { const trimmed = model?.trim() ?? ''; if (!trimmed || trimmed === 'default' || trimmed === '__default__') { return ''; } - return stripTrailingOneMillionSuffixes(trimmed) ?? ''; + return extractProviderScopedBaseModel(trimmed, providerId) ?? ''; } function canReuseModelForSelectedProvider( @@ -74,15 +77,15 @@ export function resolveLaunchDialogPrefill({ const modelCandidates = [ { providerId: currentLeadProviderId, - model: normalizeModelCandidate(currentLead?.model), + model: normalizeModelCandidate(currentLead?.model, currentLeadProviderId), }, { providerId: savedRequestProviderId, - model: normalizeModelCandidate(savedRequest?.model), + model: normalizeModelCandidate(savedRequest?.model, savedRequestProviderId), }, { providerId: previousLaunchProviderId, - model: normalizeModelCandidate(previousLaunchParams?.model), + model: normalizeModelCandidate(previousLaunchParams?.model, previousLaunchProviderId), }, ]; diff --git a/src/renderer/components/team/messages/MessageComposer.tsx b/src/renderer/components/team/messages/MessageComposer.tsx index 7f5003ea..eff3ceea 100644 --- a/src/renderer/components/team/messages/MessageComposer.tsx +++ b/src/renderer/components/team/messages/MessageComposer.tsx @@ -19,6 +19,7 @@ import { serializeChipsWithText } from '@renderer/types/inlineChip'; import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { nameColorSet } from '@renderer/utils/projectColor'; +import { getSuggestedSlashCommandsForProvider } from '@renderer/utils/providerSlashCommands'; import { buildSlashCommandSuggestions } from '@renderer/utils/skillCommandSuggestions'; import { extractTaskRefsFromText, @@ -26,7 +27,11 @@ import { } from '@renderer/utils/taskReferenceUtils'; import { MAX_TEXT_LENGTH } from '@shared/constants'; import { isLeadMember } from '@shared/utils/leadDetection'; -import { KNOWN_SLASH_COMMANDS, parseStandaloneSlashCommand } from '@shared/utils/slashCommands'; +import { parseStandaloneSlashCommand } from '@shared/utils/slashCommands'; +import { + inferTeamProviderIdFromModel, + normalizeOptionalTeamProviderId, +} from '@shared/utils/teamProvider'; import { AlertCircle, Check, ChevronDown, Mic, Paperclip, Search, Send } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; @@ -206,6 +211,12 @@ export const MessageComposer = ({ })), [members, colorMap] ); + const leadProviderId = useMemo(() => { + const lead = members.find((member) => isLeadMember(member)); + return ( + normalizeOptionalTeamProviderId(lead?.providerId) ?? inferTeamProviderIdFromModel(lead?.model) + ); + }, [members]); const { suggestions: teamMentionSuggestions } = useTeamSuggestions(teamName); const { suggestions: taskSuggestions } = useTaskSuggestions(teamName); @@ -222,8 +233,13 @@ export const MessageComposer = ({ }, [fetchSkillsCatalog, projectPath]); const slashCommandSuggestions = useMemo( - () => buildSlashCommandSuggestions(KNOWN_SLASH_COMMANDS, projectSkills, userSkills), - [projectSkills, userSkills] + () => + buildSlashCommandSuggestions( + getSuggestedSlashCommandsForProvider(leadProviderId), + projectSkills, + userSkills + ), + [leadProviderId, projectSkills, userSkills] ); const trimmed = stripEncodedTaskReferenceMetadata(draft.text).trim(); diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index a0b8f8b2..91dc5d1e 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -6,16 +6,51 @@ import { canDisplayTaskChangesForOptions, type TaskChangeRequestOptions, } from '@renderer/utils/taskChangeRequest'; -import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext'; +import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext'; import { IpcError, unwrapIpc } from '@renderer/utils/unwrapIpc'; import { stripAgentBlocks } from '@shared/constants/agentBlocks'; +import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team'; import { createLogger } from '@shared/utils/logger'; import { getTaskKanbanColumn } from '@shared/utils/reviewState'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { getWorktreeNavigationState } from '../utils/stateResetHelpers'; +import type { AppState } from '../types'; +import type { AppConfig } from '@renderer/types/data'; import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode'; +import type { + ActiveToolCall, + AddMemberRequest, + AddTaskCommentRequest, + CreateTaskRequest, + CrossTeamSendRequest, + EffortLevel, + GlobalTask, + InboxMessage, + KanbanColumnId, + LeadActivityState, + LeadContextUsage, + MemberSpawnStatusEntry, + MemberSpawnStatusesSnapshot, + PersistedTeamLaunchSummary, + SendMessageRequest, + SendMessageResult, + TaskChangePresenceState, + TaskComment, + TeamCreateRequest, + TeamData, + TeamLaunchRequest, + TeamProviderId, + TeamProvisioningProgress, + TeamSummary, + TeamTask, + TeamTaskStatus, + ToolApprovalRequest, + ToolApprovalSettings, + UpdateKanbanPatch, +} from '@shared/types'; +import type { StateCreator } from 'zustand'; const logger = createLogger('teamSlice'); @@ -484,42 +519,6 @@ async function pollProvisioningStatus( } } -import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team'; - -import type { AppState } from '../types'; -import type { AppConfig } from '@renderer/types/data'; -import type { - ActiveToolCall, - AddMemberRequest, - AddTaskCommentRequest, - CreateTaskRequest, - CrossTeamSendRequest, - EffortLevel, - GlobalTask, - InboxMessage, - KanbanColumnId, - LeadActivityState, - LeadContextUsage, - MemberSpawnStatusEntry, - MemberSpawnStatusesSnapshot, - PersistedTeamLaunchSummary, - SendMessageRequest, - SendMessageResult, - TaskChangePresenceState, - TaskComment, - TeamCreateRequest, - TeamData, - TeamLaunchRequest, - TeamProvisioningProgress, - TeamSummary, - TeamTask, - TeamTaskStatus, - ToolApprovalRequest, - ToolApprovalSettings, - UpdateKanbanPatch, -} from '@shared/types'; -import type { StateCreator } from 'zustand'; - // --- Clarification notification tracking --- // Native OS notifications for new inbox messages are handled in main process // (main/index.ts → notifyNewInboxMessages). This renderer-side tracking only @@ -1220,8 +1219,8 @@ function saveLaunchParams(teamName: string, params: TeamLaunchParams): void { * Extract the base model name from the raw model string sent to CLI. * E.g. 'opus[1m]' → 'opus', 'sonnet' → 'sonnet', undefined → undefined. */ -function extractBaseModel(raw?: string): string | undefined { - return stripTrailingOneMillionSuffixes(raw); +function extractBaseModel(raw?: string, providerId?: TeamProviderId): string | undefined { + return extractProviderScopedBaseModel(raw, providerId); } const TOOL_APPROVAL_PREFIX = 'team:toolApprovalSettings:'; @@ -2587,7 +2586,7 @@ export const createTeamSlice: StateCreator = (set, const response = await unwrapIpc('team:create', () => api.teams.createTeam(request)); // Persist per-team launch params (model, effort, limit context) - const baseModel = extractBaseModel(request.model); + const baseModel = extractBaseModel(request.model, request.providerId); const params: TeamLaunchParams = { providerId: request.providerId ?? 'anthropic', model: baseModel || 'default', @@ -2767,7 +2766,7 @@ export const createTeamSlice: StateCreator = (set, const response = await unwrapIpc('team:launch', () => api.teams.launchTeam(request)); // Persist per-team launch params (model, effort, limit context) - const baseModel = extractBaseModel(request.model); + const baseModel = extractBaseModel(request.model, request.providerId); const params: TeamLaunchParams = { providerId: request.providerId ?? 'anthropic', model: baseModel || 'default', diff --git a/src/renderer/utils/providerSlashCommands.ts b/src/renderer/utils/providerSlashCommands.ts new file mode 100644 index 00000000..d7cc66af --- /dev/null +++ b/src/renderer/utils/providerSlashCommands.ts @@ -0,0 +1,122 @@ +import { KNOWN_SLASH_COMMANDS } from '@shared/utils/slashCommands'; + +import type { TeamProviderId } from '@shared/types'; +import type { KnownSlashCommandDefinition } from '@shared/utils/slashCommands'; + +const CODEX_SLASH_COMMAND_SUGGESTIONS: readonly KnownSlashCommandDefinition[] = [ + { + name: 'model', + command: '/model', + description: 'Choose the active model for this session.', + }, + { + name: 'fast', + command: '/fast', + description: 'Toggle Fast mode on or off.', + }, + { + name: 'permissions', + command: '/permissions', + description: 'Adjust approval requirements for tools and commands.', + }, + { + name: 'plan', + command: '/plan', + description: 'Switch to plan mode with an optional prompt.', + }, + { + name: 'review', + command: '/review', + description: 'Ask Codex to review the current working tree.', + }, + { + name: 'diff', + command: '/diff', + description: 'Show the current Git diff, including untracked files.', + }, + { + name: 'status', + command: '/status', + description: 'Show session configuration and token usage.', + }, + { + name: 'mcp', + command: '/mcp', + description: 'List configured MCP tools for this session.', + }, + { + name: 'mention', + command: '/mention', + description: 'Attach a file or folder to the conversation.', + }, + { + name: 'apps', + command: '/apps', + description: 'Browse available apps and connectors.', + }, + { + name: 'plugins', + command: '/plugins', + description: 'Browse and manage installed plugins.', + }, + { + name: 'agent', + command: '/agent', + description: 'Switch to another agent thread.', + }, + { + name: 'personality', + command: '/personality', + description: 'Change Codex response style for the current thread.', + }, + { + name: 'compact', + command: '/compact', + description: 'Summarize the conversation to free tokens.', + }, + { + name: 'clear', + command: '/clear', + description: 'Clear the terminal and start a fresh chat.', + }, + { + name: 'new', + command: '/new', + description: 'Start a new conversation in the current session.', + }, + { + name: 'copy', + command: '/copy', + description: 'Copy the latest completed Codex output.', + }, + { + name: 'fork', + command: '/fork', + description: 'Fork the current conversation into a new thread.', + }, + { + name: 'resume', + command: '/resume', + description: 'Resume a previous conversation.', + }, + { + name: 'quit', + command: '/quit', + description: 'Exit the CLI.', + }, + { + name: 'exit', + command: '/exit', + description: 'Exit the CLI.', + }, +] as const; + +export function getSuggestedSlashCommandsForProvider( + providerId?: TeamProviderId +): readonly KnownSlashCommandDefinition[] { + if (providerId === 'codex') { + return CODEX_SLASH_COMMAND_SUGGESTIONS; + } + + return KNOWN_SLASH_COMMANDS; +} diff --git a/src/renderer/utils/skillCommandSuggestions.ts b/src/renderer/utils/skillCommandSuggestions.ts index 201cffea..370df4e9 100644 --- a/src/renderer/utils/skillCommandSuggestions.ts +++ b/src/renderer/utils/skillCommandSuggestions.ts @@ -1,4 +1,4 @@ -import { getKnownSlashCommand, isSupportedSlashCommandName } from '@shared/utils/slashCommands'; +import { isSupportedSlashCommandName } from '@shared/utils/slashCommands'; import type { MentionSuggestion } from '@renderer/types/mention'; import type { SkillCatalogItem } from '@shared/types/extensions'; @@ -9,6 +9,7 @@ export function buildSlashCommandSuggestions( projectSkills: readonly SkillCatalogItem[], userSkills: readonly SkillCatalogItem[] ): MentionSuggestion[] { + const builtInNames = new Set(builtIns.map((command) => command.name.trim().toLowerCase())); const builtInSuggestions: MentionSuggestion[] = builtIns.map((command) => ({ id: `command:${command.name}`, name: command.name, @@ -26,7 +27,7 @@ export function buildSlashCommandSuggestions( !skill.isValid || !normalizedFolderName || !isSupportedSlashCommandName(normalizedFolderName) || - getKnownSlashCommand(normalizedFolderName) !== null || + builtInNames.has(normalizedFolderName) || seenSkillNames.has(normalizedFolderName) ) { continue; diff --git a/src/renderer/utils/teamModelContext.ts b/src/renderer/utils/teamModelContext.ts index 7229cff0..706dc691 100644 --- a/src/renderer/utils/teamModelContext.ts +++ b/src/renderer/utils/teamModelContext.ts @@ -1,3 +1,7 @@ +import { inferTeamProviderIdFromModel } from '@shared/utils/teamProvider'; + +import type { TeamProviderId } from '@shared/types'; + export function stripTrailingOneMillionSuffixes(model: string | undefined): string | undefined { const trimmed = model?.trim(); if (!trimmed) { @@ -6,3 +10,23 @@ export function stripTrailingOneMillionSuffixes(model: string | undefined): stri return trimmed.replace(/(?:\[1m\])+$/, '') || undefined; } + +export function extractProviderScopedBaseModel( + model: string | undefined, + providerId?: TeamProviderId +): string | undefined { + const trimmed = model?.trim(); + if (!trimmed) { + return undefined; + } + + const effectiveProviderId = + providerId ?? + inferTeamProviderIdFromModel(trimmed) ?? + inferTeamProviderIdFromModel(stripTrailingOneMillionSuffixes(trimmed)); + if (effectiveProviderId !== 'anthropic') { + return trimmed; + } + + return stripTrailingOneMillionSuffixes(trimmed); +} diff --git a/test/renderer/components/team/TeamModelSelector.test.ts b/test/renderer/components/team/TeamModelSelector.test.ts index 5e7f2a47..f441903a 100644 --- a/test/renderer/components/team/TeamModelSelector.test.ts +++ b/test/renderer/components/team/TeamModelSelector.test.ts @@ -68,5 +68,6 @@ describe('computeEffectiveTeamModel', () => { it('returns non-anthropic models as-is', () => { expect(computeEffectiveTeamModel('gpt-5.4', false, 'codex')).toBe('gpt-5.4'); + expect(computeEffectiveTeamModel('custom-model[1m]', false, 'codex')).toBe('custom-model[1m]'); }); }); diff --git a/test/renderer/components/team/dialogs/launchDialogPrefill.test.ts b/test/renderer/components/team/dialogs/launchDialogPrefill.test.ts index b91c03f1..35ca0b67 100644 --- a/test/renderer/components/team/dialogs/launchDialogPrefill.test.ts +++ b/test/renderer/components/team/dialogs/launchDialogPrefill.test.ts @@ -66,7 +66,7 @@ describe('resolveLaunchDialogPrefill', () => { const savedRequest = { teamName: 'vector-room-2', - cwd: '/tmp/project', + cwd: '/Users/test/project', providerId: 'anthropic', model: 'haiku', effort: 'low', @@ -181,4 +181,31 @@ describe('resolveLaunchDialogPrefill', () => { limitContext: true, }); }); + + it('preserves literal [1m] suffixes for non-anthropic providers', () => { + const result = resolveLaunchDialogPrefill({ + members: [], + savedRequest: null, + previousLaunchParams: { + providerId: 'codex', + model: 'custom-model[1m]', + effort: 'medium', + }, + multimodelEnabled: true, + storedProviderId: 'anthropic', + storedEffort: 'medium', + storedLimitContext: false, + getStoredModel: createStoredModelGetter({ + anthropic: 'haiku', + codex: 'gpt-5.4', + }), + }); + + expect(result).toEqual({ + providerId: 'codex', + model: 'custom-model[1m]', + effort: 'medium', + limitContext: false, + }); + }); }); diff --git a/test/renderer/utils/providerSlashCommands.test.ts b/test/renderer/utils/providerSlashCommands.test.ts new file mode 100644 index 00000000..0934de33 --- /dev/null +++ b/test/renderer/utils/providerSlashCommands.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; + +import { getSuggestedSlashCommandsForProvider } from '@renderer/utils/providerSlashCommands'; + +describe('getSuggestedSlashCommandsForProvider', () => { + it('returns Codex-specific command suggestions without Anthropic-only entries', () => { + const commands = getSuggestedSlashCommandsForProvider('codex').map( + (command) => command.command + ); + + expect(commands).toContain('/permissions'); + expect(commands).toContain('/agent'); + expect(commands).toContain('/review'); + expect(commands).not.toContain('/effort'); + expect(commands).not.toContain('/usage'); + }); + + it('falls back to the default curated list for Anthropic-like providers', () => { + const commands = getSuggestedSlashCommandsForProvider('anthropic').map( + (command) => command.command + ); + + expect(commands).toContain('/effort'); + expect(commands).toContain('/usage'); + expect(commands).not.toContain('/permissions'); + }); +}); diff --git a/test/renderer/utils/skillCommandSuggestions.test.ts b/test/renderer/utils/skillCommandSuggestions.test.ts index e7e2a1d2..528a2f2d 100644 --- a/test/renderer/utils/skillCommandSuggestions.test.ts +++ b/test/renderer/utils/skillCommandSuggestions.test.ts @@ -14,10 +14,10 @@ function createSkill(overrides: Partial): SkillCatalogItem { folderName: overrides.folderName ?? 'skill-name', scope: overrides.scope ?? 'project', rootKind: overrides.rootKind ?? 'claude', - projectRoot: overrides.projectRoot ?? '/tmp/project', - discoveryRoot: overrides.discoveryRoot ?? '/tmp/project/.claude/skills', - skillDir: overrides.skillDir ?? '/tmp/project/.claude/skills/skill-name', - skillFile: overrides.skillFile ?? '/tmp/project/.claude/skills/skill-name/SKILL.md', + projectRoot: overrides.projectRoot ?? '/Users/test/project', + discoveryRoot: overrides.discoveryRoot ?? '/Users/test/project/.claude/skills', + skillDir: overrides.skillDir ?? '/Users/test/project/.claude/skills/skill-name', + skillFile: overrides.skillFile ?? '/Users/test/project/.claude/skills/skill-name/SKILL.md', metadata: overrides.metadata ?? {}, invocationMode: overrides.invocationMode ?? 'manual-only', flags: overrides.flags ?? { hasScripts: false, hasReferences: false, hasAssets: false }, @@ -29,18 +29,22 @@ function createSkill(overrides: Partial): SkillCatalogItem { describe('buildSlashCommandSuggestions', () => { it('keeps built-ins and adds valid skills in a separate suggestion type', () => { - const suggestions = buildSlashCommandSuggestions(KNOWN_SLASH_COMMANDS, [ - createSkill({ id: 'project-skill', folderName: 'review-skill', scope: 'project' }), - ], []); + const suggestions = buildSlashCommandSuggestions( + KNOWN_SLASH_COMMANDS, + [createSkill({ id: 'project-skill', folderName: 'review-skill', scope: 'project' })], + [] + ); expect(suggestions[0]?.type).toBe('command'); expect(suggestions.some((suggestion) => suggestion.type === 'skill')).toBe(true); - expect(suggestions.find((suggestion) => suggestion.id === 'skill:project-skill')).toMatchObject({ - name: 'review-skill', - command: '/review-skill', - subtitle: 'Project skill', - type: 'skill', - }); + expect(suggestions.find((suggestion) => suggestion.id === 'skill:project-skill')).toMatchObject( + { + name: 'review-skill', + command: '/review-skill', + subtitle: 'Project skill', + type: 'skill', + } + ); }); it('filters slash-unsafe names and built-in collisions', () => { @@ -64,9 +68,27 @@ describe('buildSlashCommandSuggestions', () => { [createSkill({ id: 'user', folderName: 'shared-skill', scope: 'user' })] ); - expect(suggestions.filter((suggestion) => suggestion.command === '/shared-skill')).toHaveLength(1); + expect(suggestions.filter((suggestion) => suggestion.command === '/shared-skill')).toHaveLength( + 1 + ); expect(suggestions.find((suggestion) => suggestion.command === '/shared-skill')?.id).toBe( 'skill:project' ); }); + + it('uses the provided built-in set when filtering skill collisions', () => { + const suggestions = buildSlashCommandSuggestions( + [ + { + name: 'custom-cmd', + command: '/custom-cmd', + description: 'Custom command', + }, + ], + [createSkill({ id: 'collision', folderName: 'custom-cmd' })], + [] + ); + + expect(suggestions.find((suggestion) => suggestion.id === 'skill:collision')).toBeUndefined(); + }); });