From d7f82e54d1a0a89ee5592712af19737338352114 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 20 May 2026 15:15:42 +0300 Subject: [PATCH] fix(opencode): hydrate summary model catalog --- .../runtime/providerConnectionUi.ts | 18 +- .../store/slices/cliInstallerSlice.ts | 22 ++- .../runtime/providerConnectionUi.test.ts | 15 +- test/renderer/store/cliInstallerSlice.test.ts | 155 ++++++++++++++++++ 4 files changed, 191 insertions(+), 19 deletions(-) diff --git a/src/renderer/components/runtime/providerConnectionUi.ts b/src/renderer/components/runtime/providerConnectionUi.ts index 3b6d49e4..cfe58b58 100644 --- a/src/renderer/components/runtime/providerConnectionUi.ts +++ b/src/renderer/components/runtime/providerConnectionUi.ts @@ -120,21 +120,13 @@ export function isOpenCodeCatalogHydrating( return false; } - if ( - provider.modelCatalogRefreshState === 'ready' || - provider.modelCatalogRefreshState === 'error' - ) { + if (provider.modelCatalogRefreshState === 'error') { return false; } - const hasOnlySummaryFallback = - provider.models.length === 0 || - provider.models.every((model) => model.trim() === 'opencode/big-pickle'); - return ( - hasOnlySummaryFallback && - (provider.modelCatalogRefreshState === 'loading' || - provider.runtimeCapabilities?.modelCatalog?.dynamic === true) + provider.modelCatalogRefreshState === 'loading' || + provider.runtimeCapabilities?.modelCatalog?.dynamic === true ); } @@ -142,7 +134,7 @@ export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): return provider.providerId === 'codex'; } -function getCodexCurrentRuntimeLabel(provider: CliProviderStatus): string { +function getCodexCurrentRuntimeLabel(): string { return CODEX_NATIVE_LABEL; } @@ -213,7 +205,7 @@ export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): s } const prefix = provider.authenticated ? 'Current runtime' : 'Selected runtime'; - return `${prefix}: ${getCodexCurrentRuntimeLabel(provider)}`; + return `${prefix}: ${getCodexCurrentRuntimeLabel()}`; } export function formatProviderStatusText(provider: CliProviderStatus): string { diff --git a/src/renderer/store/slices/cliInstallerSlice.ts b/src/renderer/store/slices/cliInstallerSlice.ts index 9949e853..1d6469df 100644 --- a/src/renderer/store/slices/cliInstallerSlice.ts +++ b/src/renderer/store/slices/cliInstallerSlice.ts @@ -93,6 +93,18 @@ function isModelOnlyFallbackProviderStatus(provider: CliProviderStatus | undefin ); } +function isOpenCodeSummaryOnlyCatalogStatus(provider: CliProviderStatus | undefined): boolean { + if (provider?.providerId !== 'opencode') { + return false; + } + + if (provider.modelCatalog?.providerId === 'opencode' && provider.modelCatalog.models.length > 0) { + return false; + } + + return provider.runtimeCapabilities?.modelCatalog?.dynamic === true; +} + function isHydratedMultimodelProviderStatus(provider: CliProviderStatus | undefined): boolean { if (!provider) { return false; @@ -102,6 +114,10 @@ function isHydratedMultimodelProviderStatus(provider: CliProviderStatus | undefi return false; } + if (isOpenCodeSummaryOnlyCatalogStatus(provider)) { + return false; + } + return !( provider.supported === false && provider.authenticated === false && @@ -131,7 +147,11 @@ function getProviderStatus( } function hasOpenCodeModels(provider: CliProviderStatus | undefined): boolean { - return provider?.providerId === 'opencode' && provider.models.length > 0; + return ( + provider?.providerId === 'opencode' && + provider.models.length > 0 && + !isOpenCodeSummaryOnlyCatalogStatus(provider) + ); } function hasCodexRuntimeReady(provider: CliProviderStatus | undefined): boolean { diff --git a/test/renderer/components/runtime/providerConnectionUi.test.ts b/test/renderer/components/runtime/providerConnectionUi.test.ts index 330c3759..3d55d33f 100644 --- a/test/renderer/components/runtime/providerConnectionUi.test.ts +++ b/test/renderer/components/runtime/providerConnectionUi.test.ts @@ -1,16 +1,15 @@ -import { describe, expect, it } from 'vitest'; - import { formatProviderStatusText, getProviderConnectionModeSummary, getProviderCredentialSummary, getProviderCurrentRuntimeSummary, - isProviderInventoryOnlyFallback, - isOpenCodeCatalogHydrating, isConnectionManagedRuntimeProvider, + isOpenCodeCatalogHydrating, + isProviderInventoryOnlyFallback, shouldShowProviderConnectAction, } from '@renderer/components/runtime/providerConnectionUi'; import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities'; +import { describe, expect, it } from 'vitest'; import type { CliProviderStatus } from '@shared/types'; @@ -259,12 +258,18 @@ describe('providerConnectionUi', () => { ...provider, modelCatalogRefreshState: 'ready', }) - ).toBe(false); + ).toBe(true); expect( isOpenCodeCatalogHydrating({ ...provider, models: ['opencode/big-pickle', 'openrouter/qwen/qwen3-coder-plus'], }) + ).toBe(true); + expect( + isOpenCodeCatalogHydrating({ + ...provider, + modelCatalogRefreshState: 'error', + }) ).toBe(false); }); diff --git a/test/renderer/store/cliInstallerSlice.test.ts b/test/renderer/store/cliInstallerSlice.test.ts index 60cf298f..f7c08bba 100644 --- a/test/renderer/store/cliInstallerSlice.test.ts +++ b/test/renderer/store/cliInstallerSlice.test.ts @@ -237,6 +237,28 @@ describe('cliInstallerSlice', () => { expect(getModelOnlyFallbackProviderIds(status)).toEqual(['opencode']); }); + it('classifies OpenCode summary-only model lists as incomplete until catalog hydration', () => { + const status = createMultimodelStatus([ + createMultimodelProvider({ + providerId: 'opencode', + displayName: 'OpenCode', + authenticated: true, + authMethod: 'opencode_managed', + models: ['opencode/big-pickle'], + backend: { kind: 'opencode-cli', label: 'OpenCode CLI' }, + runtimeCapabilities: { + modelCatalog: { + dynamic: true, + source: 'app-server', + }, + }, + }), + ]); + + expect(getIncompleteMultimodelProviderIds(status)).toEqual(['opencode']); + expect(getModelOnlyFallbackProviderIds(status)).toEqual([]); + }); + it('keeps connection-enriched checking placeholders incomplete until provider hydration finishes', () => { const status = createMultimodelStatus([ createMultimodelProvider({ @@ -1002,6 +1024,139 @@ describe('cliInstallerSlice', () => { backend: { kind: 'opencode-cli', label: 'OpenCode CLI' }, }); }); + + it('refreshes OpenCode when bootstrap metadata has summary-only big-pickle models', async () => { + const mockStatus = createMultimodelStatus([ + createMultimodelProvider({ + providerId: 'anthropic', + displayName: 'Anthropic', + authenticated: true, + authMethod: 'oauth_token', + models: ['claude-sonnet-4-5'], + backend: { kind: 'anthropic', label: 'Anthropic' }, + }), + createMultimodelProvider({ + providerId: 'codex', + displayName: 'Codex', + authenticated: true, + authMethod: 'chatgpt', + models: ['gpt-5.4'], + backend: { kind: 'codex-native', label: 'Codex' }, + }), + createMultimodelProvider({ + providerId: 'opencode', + displayName: 'OpenCode', + authenticated: true, + authMethod: 'opencode_managed', + models: ['opencode/big-pickle'], + backend: { kind: 'opencode-cli', label: 'OpenCode CLI' }, + runtimeCapabilities: { + modelCatalog: { + dynamic: true, + source: 'app-server', + }, + }, + }), + ]); + vi.mocked(api.cliInstaller.getStatus).mockResolvedValue(mockStatus); + vi.mocked(api.cliInstaller.getProviderStatus).mockImplementation((providerId) => { + if (providerId === 'opencode') { + return Promise.resolve( + createMultimodelProvider({ + providerId: 'opencode', + displayName: 'OpenCode', + authenticated: true, + authMethod: 'opencode_managed', + models: [ + 'opencode/big-pickle', + 'openai/gpt-5.4', + 'openrouter/openai/gpt-oss-20b:free', + ], + modelCatalogRefreshState: 'ready', + modelCatalog: { + schemaVersion: 1, + providerId: 'opencode', + source: 'app-server', + status: 'ready', + fetchedAt: '2026-05-20T00:00:00.000Z', + staleAt: '2026-05-20T00: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', + }, + { + id: 'openrouter/openai/gpt-oss-20b:free', + launchModel: 'openrouter/openai/gpt-oss-20b:free', + displayName: 'openrouter/openai/gpt-oss-20b:free', + hidden: false, + supportedReasoningEfforts: [], + defaultReasoningEffort: null, + inputModalities: ['text'], + supportsPersonality: true, + isDefault: false, + upgrade: false, + source: 'app-server', + badgeLabel: 'Free', + }, + ], + diagnostics: { + configReadState: 'ready', + appServerState: 'healthy', + }, + }, + backend: { kind: 'opencode-cli', label: 'OpenCode CLI' }, + runtimeCapabilities: { + modelCatalog: { + dynamic: true, + source: 'app-server', + }, + }, + }) + ); + } + return Promise.reject(new Error(`Unexpected provider status request for ${providerId}`)); + }); + + await useStore.getState().bootstrapCliStatus({ multimodelEnabled: true }); + + expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledTimes(1); + expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledWith('opencode'); + const opencode = useStore + .getState() + .cliStatus?.providers.find((provider) => provider.providerId === 'opencode'); + expect(opencode?.models).toEqual([ + 'opencode/big-pickle', + 'openai/gpt-5.4', + 'openrouter/openai/gpt-oss-20b:free', + ]); + expect(opencode?.modelCatalog?.models).toHaveLength(3); + }); }); describe('installCli', () => {