From 16c1e4b47d05f5adb6c6bea62dfa6f5124e748e7 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 20 May 2026 15:31:20 +0300 Subject: [PATCH] fix(opencode): settle empty catalog hydration --- .../runtime/providerConnectionUi.ts | 2 +- .../store/slices/cliInstallerSlice.ts | 6 +- .../runtime/providerConnectionUi.test.ts | 20 +++++++ test/renderer/store/cliInstallerSlice.test.ts | 59 +++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/runtime/providerConnectionUi.ts b/src/renderer/components/runtime/providerConnectionUi.ts index cfe58b58..643122af 100644 --- a/src/renderer/components/runtime/providerConnectionUi.ts +++ b/src/renderer/components/runtime/providerConnectionUi.ts @@ -116,7 +116,7 @@ export function isOpenCodeCatalogHydrating( return false; } - if (provider.modelCatalog?.providerId === 'opencode' && provider.modelCatalog.models.length > 0) { + if (provider.modelCatalog?.providerId === 'opencode') { return false; } diff --git a/src/renderer/store/slices/cliInstallerSlice.ts b/src/renderer/store/slices/cliInstallerSlice.ts index 1d6469df..94fb1995 100644 --- a/src/renderer/store/slices/cliInstallerSlice.ts +++ b/src/renderer/store/slices/cliInstallerSlice.ts @@ -98,7 +98,11 @@ function isOpenCodeSummaryOnlyCatalogStatus(provider: CliProviderStatus | undefi return false; } - if (provider.modelCatalog?.providerId === 'opencode' && provider.modelCatalog.models.length > 0) { + if (provider.modelCatalog?.providerId === 'opencode') { + return false; + } + + if (provider.modelCatalogRefreshState === 'error') { return false; } diff --git a/test/renderer/components/runtime/providerConnectionUi.test.ts b/test/renderer/components/runtime/providerConnectionUi.test.ts index 3d55d33f..6847eddf 100644 --- a/test/renderer/components/runtime/providerConnectionUi.test.ts +++ b/test/renderer/components/runtime/providerConnectionUi.test.ts @@ -271,6 +271,26 @@ describe('providerConnectionUi', () => { modelCatalogRefreshState: 'error', }) ).toBe(false); + expect( + isOpenCodeCatalogHydrating({ + ...provider, + 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: null, + defaultLaunchModel: null, + models: [], + diagnostics: { + configReadState: 'ready', + appServerState: 'healthy', + }, + }, + }) + ).toBe(false); }); it('does not describe Anthropic API key mode as subscription connected when the key is missing', () => { diff --git a/test/renderer/store/cliInstallerSlice.test.ts b/test/renderer/store/cliInstallerSlice.test.ts index f7c08bba..3be8043c 100644 --- a/test/renderer/store/cliInstallerSlice.test.ts +++ b/test/renderer/store/cliInstallerSlice.test.ts @@ -259,6 +259,65 @@ describe('cliInstallerSlice', () => { expect(getModelOnlyFallbackProviderIds(status)).toEqual([]); }); + it('treats an empty OpenCode model catalog as hydrated', () => { + const status = createMultimodelStatus([ + createMultimodelProvider({ + providerId: 'opencode', + displayName: 'OpenCode', + authenticated: false, + authMethod: null, + models: [], + backend: { kind: 'opencode-cli', label: 'OpenCode CLI' }, + 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: null, + defaultLaunchModel: null, + models: [], + diagnostics: { + configReadState: 'ready', + appServerState: 'healthy', + }, + }, + runtimeCapabilities: { + modelCatalog: { + dynamic: true, + source: 'app-server', + }, + }, + }), + ]); + + expect(getIncompleteMultimodelProviderIds(status)).toEqual([]); + }); + + it('does not keep OpenCode catalog errors marked as incomplete', () => { + const status = createMultimodelStatus([ + createMultimodelProvider({ + providerId: 'opencode', + displayName: 'OpenCode', + verificationState: 'error', + statusMessage: 'Catalog hydration failed', + models: [], + modelCatalog: null, + modelCatalogRefreshState: 'error', + runtimeCapabilities: { + modelCatalog: { + dynamic: true, + source: 'app-server', + }, + }, + }), + ]); + + expect(getIncompleteMultimodelProviderIds(status)).toEqual([]); + }); + it('keeps connection-enriched checking placeholders incomplete until provider hydration finishes', () => { const status = createMultimodelStatus([ createMultimodelProvider({