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 { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
|
import { getVisibleMultimodelProviders } from '@renderer/utils/multimodelProviderVisibility';
|
||||||
|
import {
|
||||||
|
getCliProviderExtensionCapability,
|
||||||
|
isCliExtensionCapabilityAvailable,
|
||||||
|
} from '@shared/utils/providerExtensionCapabilities';
|
||||||
import {
|
import {
|
||||||
formatSkillRootKind,
|
formatSkillRootKind,
|
||||||
getSkillAudience,
|
getSkillAudience,
|
||||||
|
|
@ -90,6 +95,19 @@ function getSkillStatus(skill: SkillCatalogItem): string {
|
||||||
return 'Ready to use';
|
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 = ({
|
export const SkillsPanel = ({
|
||||||
projectPath,
|
projectPath,
|
||||||
projectLabel,
|
projectLabel,
|
||||||
|
|
@ -131,6 +149,19 @@ export const SkillsPanel = ({
|
||||||
() => isCodexSkillOverlayAvailable(cliStatus),
|
() => isCodexSkillOverlayAvailable(cliStatus),
|
||||||
[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(
|
const codexOnlySkillsCount = useMemo(
|
||||||
() => mergedSkills.filter((skill) => getSkillAudience(skill.rootKind) === 'codex').length,
|
() => mergedSkills.filter((skill) => getSkillAudience(skill.rootKind) === 'codex').length,
|
||||||
[mergedSkills]
|
[mergedSkills]
|
||||||
|
|
@ -252,8 +283,9 @@ export const SkillsPanel = ({
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{cliStatus?.flavor === 'agent_teams_orchestrator' && (
|
{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">
|
<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
|
Shared skills in `.claude`, `.cursor`, and `.agents` are available to{' '}
|
||||||
Codex. Skills stored in `.codex` are only offered to Codex sessions.
|
{skillsAudienceLabel ?? 'the configured runtime'}. Skills stored in `.codex` stay
|
||||||
|
Codex-only when Codex support is available.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="bg-surface-raised/20 rounded-xl border border-border p-4">
|
<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', () => {
|
describe('SkillsPanel', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
|
@ -293,4 +339,38 @@ describe('SkillsPanel', () => {
|
||||||
await Promise.resolve();
|
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