fix(team): preserve provider model ids and codex slash suggestions
This commit is contained in:
parent
5375eea19f
commit
f819dd0c27
11 changed files with 315 additions and 70 deletions
|
|
@ -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]';
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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<MentionSuggestion[]>(
|
||||
() => buildSlashCommandSuggestions(KNOWN_SLASH_COMMANDS, projectSkills, userSkills),
|
||||
[projectSkills, userSkills]
|
||||
() =>
|
||||
buildSlashCommandSuggestions(
|
||||
getSuggestedSlashCommandsForProvider(leadProviderId),
|
||||
projectSkills,
|
||||
userSkills
|
||||
),
|
||||
[leadProviderId, projectSkills, userSkills]
|
||||
);
|
||||
|
||||
const trimmed = stripEncodedTaskReferenceMetadata(draft.text).trim();
|
||||
|
|
|
|||
|
|
@ -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<AppState, [], [], TeamSlice> = (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<AppState, [], [], TeamSlice> = (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',
|
||||
|
|
|
|||
122
src/renderer/utils/providerSlashCommands.ts
Normal file
122
src/renderer/utils/providerSlashCommands.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
27
test/renderer/utils/providerSlashCommands.test.ts
Normal file
27
test/renderer/utils/providerSlashCommands.test.ts
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -14,10 +14,10 @@ function createSkill(overrides: Partial<SkillCatalogItem>): 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>): 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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue