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(