From 8094941385ef1add739bb9e32edd14e9ad7dd1c1 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Mon, 12 Jan 2026 23:07:05 +0530 Subject: [PATCH] feat(opencode): persist dynamic model selection --- apps/ui/src/hooks/use-settings-migration.ts | 10 ++++++++++ apps/ui/src/hooks/use-settings-sync.ts | 1 + apps/ui/src/store/app-store.ts | 19 +++++++------------ libs/types/src/settings.ts | 3 +++ 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index ed236de8..bb86c10c 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -158,6 +158,8 @@ export function parseLocalStorageSettings(): Partial | null { cursorDefaultModel: state.cursorDefaultModel as GlobalSettings['cursorDefaultModel'], enabledOpencodeModels: state.enabledOpencodeModels as GlobalSettings['enabledOpencodeModels'], opencodeDefaultModel: state.opencodeDefaultModel as GlobalSettings['opencodeDefaultModel'], + enabledDynamicModelIds: + state.enabledDynamicModelIds as GlobalSettings['enabledDynamicModelIds'], autoLoadClaudeMd: state.autoLoadClaudeMd as boolean, keyboardShortcuts: state.keyboardShortcuts as GlobalSettings['keyboardShortcuts'], mcpServers: state.mcpServers as GlobalSettings['mcpServers'], @@ -517,6 +519,12 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void { sanitizedEnabledOpencodeModels.push(sanitizedOpencodeDefaultModel); } + const persistedDynamicModelIds = + settings.enabledDynamicModelIds ?? current.enabledDynamicModelIds; + const sanitizedDynamicModelIds = persistedDynamicModelIds.filter( + (modelId) => !modelId.startsWith('amazon-bedrock/') + ); + // Convert ProjectRef[] to Project[] (minimal data, features will be loaded separately) const projects = (settings.projects ?? []).map((ref) => ({ id: ref.id, @@ -562,6 +570,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void { cursorDefaultModel: settings.cursorDefaultModel ?? 'auto', enabledOpencodeModels: sanitizedEnabledOpencodeModels, opencodeDefaultModel: sanitizedOpencodeDefaultModel, + enabledDynamicModelIds: sanitizedDynamicModelIds, autoLoadClaudeMd: settings.autoLoadClaudeMd ?? false, skipSandboxWarning: settings.skipSandboxWarning ?? false, keyboardShortcuts: { @@ -615,6 +624,7 @@ function buildSettingsUpdateFromStore(): Record { enhancementModel: state.enhancementModel, validationModel: state.validationModel, phaseModels: state.phaseModels, + enabledDynamicModelIds: state.enabledDynamicModelIds, autoLoadClaudeMd: state.autoLoadClaudeMd, skipSandboxWarning: state.skipSandboxWarning, keyboardShortcuts: state.keyboardShortcuts, diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index e55ec1b6..a12daf92 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -46,6 +46,7 @@ const SETTINGS_FIELDS_TO_SYNC = [ 'cursorDefaultModel', 'enabledOpencodeModels', 'opencodeDefaultModel', + 'enabledDynamicModelIds', 'autoLoadClaudeMd', 'keyboardShortcuts', 'mcpServers', diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 0490738e..ca415ed6 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -592,7 +592,7 @@ export interface AppState { // Dynamic models are session-only (not persisted) because they're discovered at runtime // from `opencode models` CLI and depend on current provider authentication state dynamicOpencodeModels: ModelDefinition[]; // Dynamically discovered models from OpenCode CLI - enabledDynamicModelIds: string[]; // Which dynamic models are enabled (session-only) + enabledDynamicModelIds: string[]; // Which dynamic models are enabled cachedOpencodeProviders: Array<{ id: string; name: string; @@ -1241,7 +1241,7 @@ const initialState: AppState = { enabledOpencodeModels: getAllOpencodeModelIds(), // All OpenCode models enabled by default opencodeDefaultModel: DEFAULT_OPENCODE_MODEL, // Default to OpenCode free tier dynamicOpencodeModels: [], // Empty until fetched from OpenCode CLI - enabledDynamicModelIds: [], // All dynamic models enabled by default (populated when models are fetched) + enabledDynamicModelIds: [], // Empty until user enables dynamic models cachedOpencodeProviders: [], // Empty until fetched from OpenCode CLI autoLoadClaudeMd: false, // Default to disabled (user must opt-in) skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog) @@ -2041,9 +2041,8 @@ export const useAppStore = create()((set, get) => ({ : state.enabledOpencodeModels.filter((m) => m !== model), })), setDynamicOpencodeModels: (models) => { - // Dynamic models are session-only (not persisted to server) because they depend on - // current CLI authentication state and are re-discovered each session - // When setting dynamic models, auto-enable all of them if enabledDynamicModelIds is empty + // Dynamic models depend on CLI authentication state and are re-discovered each session. + // Persist enabled model IDs, but do not auto-enable new models. const filteredModels = models.filter( (model) => model.provider !== OPENCODE_BEDROCK_PROVIDER_ID && @@ -2051,14 +2050,10 @@ export const useAppStore = create()((set, get) => ({ ); const currentEnabled = get().enabledDynamicModelIds; const newModelIds = filteredModels.map((m) => m.id); + const filteredEnabled = currentEnabled.filter((modelId) => newModelIds.includes(modelId)); - // If no models were previously enabled, enable all new ones - if (currentEnabled.length === 0) { - set({ dynamicOpencodeModels: filteredModels, enabledDynamicModelIds: newModelIds }); - } else { - // Keep existing enabled state, just update the models list - set({ dynamicOpencodeModels: filteredModels }); - } + const nextEnabled = currentEnabled.length === 0 ? [] : filteredEnabled; + set({ dynamicOpencodeModels: filteredModels, enabledDynamicModelIds: nextEnabled }); }, setEnabledDynamicModelIds: (ids) => set({ enabledDynamicModelIds: ids }), toggleDynamicModel: (modelId, enabled) => diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 8ec4ef6c..38402c24 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -401,6 +401,8 @@ export interface GlobalSettings { enabledOpencodeModels?: OpencodeModelId[]; /** Default OpenCode model selection when switching to OpenCode CLI */ opencodeDefaultModel?: OpencodeModelId; + /** Which dynamic OpenCode models are enabled (empty = all discovered) */ + enabledDynamicModelIds?: string[]; // Input Configuration /** User's keyboard shortcut bindings */ @@ -704,6 +706,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { cursorDefaultModel: 'auto', enabledOpencodeModels: getAllOpencodeModelIds(), opencodeDefaultModel: DEFAULT_OPENCODE_MODEL, + enabledDynamicModelIds: [], keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, projects: [], trashedProjects: [],