fix(extensions): keep skills banner provider-aware

This commit is contained in:
777genius 2026-04-17 20:57:59 +03:00
parent 4775d4bc45
commit 19c6144ef5
2 changed files with 114 additions and 2 deletions

View file

@ -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 = ({
<div className="flex flex-col gap-4">
{cliStatus?.flavor === 'agent_teams_orchestrator' && (
<div className="rounded-md border border-blue-500/30 bg-blue-500/5 px-4 py-3 text-sm text-blue-300">
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.
</div>
)}
<div className="bg-surface-raised/20 rounded-xl border border-border p-4">

View file

@ -168,6 +168,52 @@ function makeUserSkill(): SkillCatalogItem {
};
}
function makeMultimodelStatus(
overrides?: Partial<CliInstallationStatus>
): 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();
});
});
});