diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index b7904727..ea46303c 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -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 ? {connectionModeSummary} : null} {credentialSummary ? {credentialSummary} : null} {modelCatalogLoading ? Loading models... : null} - {provider.models.length === 0 && !modelCatalogLoading && ( + {!hasProviderModels && !modelCatalogLoading && ( Models unavailable for this runtime build )} @@ -1087,7 +1093,7 @@ const InstalledBanner = ({ - {!showSkeleton && !modelCatalogLoading && provider.models.length > 0 && ( + {!showSkeleton && !modelCatalogLoading && hasProviderModels && (
{ 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 ? {credentialSummary} : null} {modelCatalogLoading ? Loading models... : null} - {provider.models.length === 0 && !modelCatalogLoading && ( + {!hasProviderModels && !modelCatalogLoading && ( Models unavailable for this runtime build )}
@@ -612,7 +621,7 @@ export const CliStatusSection = (): React.JSX.Element | null => { {!effectiveShowSkeleton && !modelCatalogLoading && - provider.models.length > 0 && ( + hasProviderModels && (
{ }); }); + 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'; diff --git a/test/renderer/components/runtime/ProviderModelBadges.test.tsx b/test/renderer/components/runtime/ProviderModelBadges.test.tsx index 159a5add..3366ffa7 100644 --- a/test/renderer/components/runtime/ProviderModelBadges.test.tsx +++ b/test/renderer/components/runtime/ProviderModelBadges.test.tsx @@ -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( + + ); + + 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(