diff --git a/src/renderer/components/extensions/skills/SkillsPanel.tsx b/src/renderer/components/extensions/skills/SkillsPanel.tsx index ecdeed73..e6bf795a 100644 --- a/src/renderer/components/extensions/skills/SkillsPanel.tsx +++ b/src/renderer/components/extensions/skills/SkillsPanel.tsx @@ -6,6 +6,11 @@ import { Button } from '@renderer/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; +import { getVisibleMultimodelProviders } from '@renderer/utils/multimodelProviderVisibility'; +import { + getCliProviderExtensionCapability, + isCliExtensionCapabilityAvailable, +} from '@shared/utils/providerExtensionCapabilities'; import { formatSkillRootKind, getSkillAudience, @@ -90,6 +95,19 @@ function getSkillStatus(skill: SkillCatalogItem): string { return 'Ready to use'; } +function formatRuntimeAudienceLabel(providerNames: readonly string[]): string { + if (providerNames.length === 0) { + return 'the configured runtime'; + } + if (providerNames.length === 1) { + return providerNames[0]!; + } + if (providerNames.length === 2) { + return `${providerNames[0]} and ${providerNames[1]}`; + } + return `${providerNames.slice(0, -1).join(', ')}, and ${providerNames.at(-1)}`; +} + export const SkillsPanel = ({ projectPath, projectLabel, @@ -131,6 +149,19 @@ export const SkillsPanel = ({ () => isCodexSkillOverlayAvailable(cliStatus), [cliStatus] ); + const skillsAudienceLabel = useMemo(() => { + if (cliStatus?.flavor !== 'agent_teams_orchestrator') { + return null; + } + + const providerNames = getVisibleMultimodelProviders(cliStatus.providers ?? []) + .filter((provider) => + isCliExtensionCapabilityAvailable(getCliProviderExtensionCapability(provider, 'skills')) + ) + .map((provider) => provider.displayName); + + return formatRuntimeAudienceLabel(providerNames); + }, [cliStatus]); const codexOnlySkillsCount = useMemo( () => mergedSkills.filter((skill) => getSkillAudience(skill.rootKind) === 'codex').length, [mergedSkills] @@ -252,8 +283,9 @@ export const SkillsPanel = ({
{cliStatus?.flavor === 'agent_teams_orchestrator' && (
- Shared skills in `.claude`, `.cursor`, and `.agents` are available to both Anthropic and - Codex. Skills stored in `.codex` are only offered to Codex sessions. + Shared skills in `.claude`, `.cursor`, and `.agents` are available to{' '} + {skillsAudienceLabel ?? 'the configured runtime'}. Skills stored in `.codex` stay + Codex-only when Codex support is available.
)}
diff --git a/test/renderer/components/extensions/skills/SkillsPanel.test.ts b/test/renderer/components/extensions/skills/SkillsPanel.test.ts index d8864a79..557794cb 100644 --- a/test/renderer/components/extensions/skills/SkillsPanel.test.ts +++ b/test/renderer/components/extensions/skills/SkillsPanel.test.ts @@ -168,6 +168,52 @@ function makeUserSkill(): SkillCatalogItem { }; } +function makeMultimodelStatus( + overrides?: Partial +): CliInstallationStatus { + return { + flavor: 'agent_teams_orchestrator', + displayName: 'Multimodel runtime', + supportsSelfUpdate: false, + showVersionDetails: true, + showBinaryPath: true, + installed: true, + installedVersion: '1.0.0', + binaryPath: '/usr/local/bin/agent-teams', + latestVersion: '1.0.0', + updateAvailable: false, + authLoggedIn: false, + authStatusChecking: false, + authMethod: null, + providers: [ + { + providerId: 'anthropic', + displayName: 'Anthropic', + supported: true, + authenticated: true, + authMethod: 'oauth', + verificationState: 'verified', + statusMessage: 'Connected', + models: [], + canLoginFromUi: true, + capabilities: { + teamLaunch: true, + oneShot: true, + extensions: { + plugins: { status: 'supported', ownership: 'provider', reason: null }, + mcp: { status: 'supported', ownership: 'shared', reason: null }, + skills: { status: 'supported', ownership: 'shared', reason: null }, + apiKeys: { status: 'supported', ownership: 'shared', reason: null }, + }, + }, + connection: null, + backend: null, + }, + ], + ...overrides, + }; +} + describe('SkillsPanel', () => { beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); @@ -293,4 +339,38 @@ describe('SkillsPanel', () => { await Promise.resolve(); }); }); + + it('uses a runtime-aware shared skills banner when codex is unavailable', async () => { + storeState.cliStatus = makeMultimodelStatus(); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(SkillsPanel, { + projectPath: '/tmp/project-a', + projectLabel: 'Project A', + skillsSearchQuery: '', + setSkillsSearchQuery: vi.fn(), + skillsSort: 'name-asc', + setSkillsSort: vi.fn(), + selectedSkillId: null, + setSelectedSkillId: vi.fn(), + }) + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(host.textContent).toContain( + 'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic.' + ); + expect(host.textContent).not.toContain('available to both Anthropic and Codex'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); });