fix(opencode): render catalog-backed provider badges

This commit is contained in:
777genius 2026-05-20 17:39:56 +03:00
parent c88a11c1d8
commit c632208dba
4 changed files with 283 additions and 6 deletions

View file

@ -53,6 +53,7 @@ import { isMultimodelRuntimeStatus } from '@renderer/utils/multimodelProviderVis
import { resolveProjectPathById } from '@renderer/utils/projectLookup';
import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
import { getRuntimeDisplayName as getHumanRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName';
import { getVisibleTeamProviderModels } from '@renderer/utils/teamModelCatalog';
import {
AlertTriangle,
CheckCircle,
@ -836,12 +837,17 @@ const InstalledBanner = ({
const modelCatalogLoading =
provider.modelCatalogRefreshState === 'loading' ||
isOpenCodeCatalogHydrating(provider);
const hasProviderModels =
provider.providerId === 'opencode'
? getVisibleTeamProviderModels(provider.providerId, provider.models, provider)
.length > 0
: provider.models.length > 0;
const hasDetailContent = Boolean(
(provider.backend?.label && !runtimeSummary) ||
runtimeSummary ||
connectionModeSummary ||
credentialSummary ||
provider.models.length === 0 ||
!hasProviderModels ||
modelCatalogLoading
);
@ -905,7 +911,7 @@ const InstalledBanner = ({
{connectionModeSummary ? <span>{connectionModeSummary}</span> : null}
{credentialSummary ? <span>{credentialSummary}</span> : null}
{modelCatalogLoading ? <span>Loading models...</span> : null}
{provider.models.length === 0 && !modelCatalogLoading && (
{!hasProviderModels && !modelCatalogLoading && (
<span>Models unavailable for this runtime build</span>
)}
</div>
@ -1087,7 +1093,7 @@ const InstalledBanner = ({
</button>
</div>
</div>
{!showSkeleton && !modelCatalogLoading && provider.models.length > 0 && (
{!showSkeleton && !modelCatalogLoading && hasProviderModels && (
<div className="col-span-2">
<ProviderModelBadges
providerId={provider.providerId}

View file

@ -41,6 +41,7 @@ import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze';
import { resolveProjectPathById } from '@renderer/utils/projectLookup';
import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
import { getRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName';
import { getVisibleTeamProviderModels } from '@renderer/utils/teamModelCatalog';
import {
AlertTriangle,
CheckCircle,
@ -491,6 +492,14 @@ export const CliStatusSection = (): React.JSX.Element | null => {
const modelCatalogLoading =
provider.modelCatalogRefreshState === 'loading' ||
isOpenCodeCatalogHydrating(provider);
const hasProviderModels =
provider.providerId === 'opencode'
? getVisibleTeamProviderModels(
provider.providerId,
provider.models,
provider
).length > 0
: provider.models.length > 0;
const connectionModeSummary = getProviderConnectionModeSummary(provider);
const credentialSummary = getProviderCredentialSummary(provider);
const disconnectAction = getProviderDisconnectAction(provider);
@ -499,7 +508,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
runtimeSummary ||
connectionModeSummary ||
credentialSummary ||
provider.models.length === 0 ||
!hasProviderModels ||
modelCatalogLoading
);
@ -553,7 +562,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
) : null}
{credentialSummary ? <span>{credentialSummary}</span> : null}
{modelCatalogLoading ? <span>Loading models...</span> : null}
{provider.models.length === 0 && !modelCatalogLoading && (
{!hasProviderModels && !modelCatalogLoading && (
<span>Models unavailable for this runtime build</span>
)}
</div>
@ -612,7 +621,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
</div>
{!effectiveShowSkeleton &&
!modelCatalogLoading &&
provider.models.length > 0 && (
hasProviderModels && (
<div className="col-span-2">
<ProviderModelBadges
providerId={provider.providerId}

View file

@ -807,6 +807,192 @@ describe('CLI status visibility during completed install state', () => {
});
});
it('shows OpenCode catalog models on the dashboard when provider models are empty', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';
storeState.cliStatus = createInstalledCliStatus({
flavor: 'agent_teams_orchestrator',
displayName: 'Multimodel runtime',
supportsSelfUpdate: false,
showVersionDetails: false,
showBinaryPath: false,
authLoggedIn: true,
providers: [
{
providerId: 'opencode',
displayName: 'OpenCode (200+ models)',
supported: true,
authenticated: true,
authMethod: 'opencode_managed',
verificationState: 'verified',
statusMessage: 'Ready',
models: [],
canLoginFromUi: false,
capabilities: {
teamLaunch: true,
oneShot: false,
},
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
modelCatalog: {
schemaVersion: 1,
providerId: 'opencode',
source: 'app-server',
status: 'ready',
fetchedAt: '2026-05-12T00:00:00.000Z',
staleAt: '2026-05-12T00:10:00.000Z',
defaultModelId: 'opencode/big-pickle',
defaultLaunchModel: 'opencode/big-pickle',
models: [
{
id: 'opencode/big-pickle',
launchModel: 'opencode/big-pickle',
displayName: 'opencode/big-pickle',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: true,
upgrade: false,
source: 'app-server',
badgeLabel: 'Free',
},
{
id: 'openai/gpt-5.4',
launchModel: 'openai/gpt-5.4',
displayName: 'openai/gpt-5.4',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: false,
upgrade: false,
source: 'app-server',
badgeLabel: null,
},
],
diagnostics: {
configReadState: 'ready',
appServerState: 'healthy',
},
},
},
],
});
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(React.createElement(CliStatusBanner));
await Promise.resolve();
});
expect(host.textContent).toContain('big-pickle');
expect(host.textContent).toContain('GPT-5.4');
expect(host.textContent).not.toContain('Models unavailable for this runtime build');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('shows OpenCode catalog models in settings when provider models are empty', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';
storeState.cliStatus = createInstalledCliStatus({
flavor: 'agent_teams_orchestrator',
displayName: 'Multimodel runtime',
supportsSelfUpdate: false,
showVersionDetails: false,
showBinaryPath: false,
authLoggedIn: true,
providers: [
{
providerId: 'opencode',
displayName: 'OpenCode (200+ models)',
supported: true,
authenticated: true,
authMethod: 'opencode_managed',
verificationState: 'verified',
statusMessage: 'Ready',
models: [],
canLoginFromUi: false,
capabilities: {
teamLaunch: true,
oneShot: false,
},
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
modelCatalog: {
schemaVersion: 1,
providerId: 'opencode',
source: 'app-server',
status: 'ready',
fetchedAt: '2026-05-12T00:00:00.000Z',
staleAt: '2026-05-12T00:10:00.000Z',
defaultModelId: 'opencode/big-pickle',
defaultLaunchModel: 'opencode/big-pickle',
models: [
{
id: 'opencode/big-pickle',
launchModel: 'opencode/big-pickle',
displayName: 'opencode/big-pickle',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: true,
upgrade: false,
source: 'app-server',
badgeLabel: 'Free',
},
{
id: 'openai/gpt-5.4',
launchModel: 'openai/gpt-5.4',
displayName: 'openai/gpt-5.4',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: false,
upgrade: false,
source: 'app-server',
badgeLabel: null,
},
],
diagnostics: {
configReadState: 'ready',
appServerState: 'healthy',
},
},
},
],
});
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(React.createElement(CliStatusSection));
await Promise.resolve();
});
expect(host.textContent).toContain('big-pickle');
expect(host.textContent).toContain('GPT-5.4');
expect(host.textContent).not.toContain('Models unavailable for this runtime build');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('preserves dashboard runtime backend refresh errors for the manage dialog', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';

View file

@ -171,6 +171,82 @@ describe('ProviderModelBadges', () => {
expect(host.textContent?.match(/Free/g)).toHaveLength(1);
});
it('uses the OpenCode catalog when provider models are summary-only', () => {
const host = render(
<ProviderModelBadges
providerId="opencode"
models={['opencode/big-pickle']}
providerStatus={{
providerId: 'opencode',
authMethod: 'opencode_managed',
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
modelCatalog: {
schemaVersion: 1,
providerId: 'opencode',
source: 'app-server',
status: 'ready',
fetchedAt: '2026-05-12T00:00:00.000Z',
staleAt: '2026-05-12T00:10:00.000Z',
defaultModelId: 'opencode/big-pickle',
defaultLaunchModel: 'opencode/big-pickle',
models: [
{
id: 'opencode/big-pickle',
launchModel: 'opencode/big-pickle',
displayName: 'opencode/big-pickle',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: true,
upgrade: false,
source: 'app-server',
badgeLabel: 'Free',
},
{
id: 'openai/gpt-5.4',
launchModel: 'openai/gpt-5.4',
displayName: 'openai/gpt-5.4',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: false,
upgrade: false,
source: 'app-server',
badgeLabel: null,
},
{
id: 'openrouter/hidden-model',
launchModel: 'openrouter/hidden-model',
displayName: 'openrouter/hidden-model',
hidden: true,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text'],
supportsPersonality: true,
isDefault: false,
upgrade: false,
source: 'app-server',
badgeLabel: null,
},
],
diagnostics: {
configReadState: 'ready',
appServerState: 'healthy',
},
},
}}
/>
);
expect(host.textContent).toContain('big-pickle');
expect(host.textContent).toContain('GPT-5.4');
expect(host.textContent).not.toContain('hidden-model');
});
it('renders OpenCode free badges from metadata when badgeLabel is absent', () => {
const host = render(
<ProviderModelBadges