fix(extensions): keep skills banner provider-aware
This commit is contained in:
parent
4775d4bc45
commit
19c6144ef5
2 changed files with 114 additions and 2 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue