diff --git a/packages/desktop/src-tauri/src/commands/git.rs b/packages/desktop/src-tauri/src/commands/git.rs index c3b94cc2..186a324f 100644 --- a/packages/desktop/src-tauri/src/commands/git.rs +++ b/packages/desktop/src-tauri/src/commands/git.rs @@ -2089,13 +2089,30 @@ Diff summary: {}"#, diff_summaries ); + + let settings = state.settings().load().await.unwrap_or(serde_json::Value::Null); + let raw_model = settings.get("commitMessageModel") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let model_candidate = raw_model + .split('/') + .last() + .unwrap_or(raw_model) + .trim(); + + let model = if model_candidate.is_empty() { + "big-pickle" + } else { + model_candidate + }; // 3. Call API let client = Client::new(); let res = client .post("https://opencode.ai/zen/v1/chat/completions") .json(&serde_json::json!({ - "model": "big-pickle", + "model": model, "messages": [{ "role": "user", "content": prompt }], "max_tokens": 3000, "stream": false, diff --git a/packages/desktop/src-tauri/src/commands/settings.rs b/packages/desktop/src-tauri/src/commands/settings.rs index e9c416e2..85213347 100644 --- a/packages/desktop/src-tauri/src/commands/settings.rs +++ b/packages/desktop/src-tauri/src/commands/settings.rs @@ -257,6 +257,14 @@ fn sanitize_settings_update(payload: &Value) -> Value { result_obj.insert("defaultAgent".to_string(), json!(trimmed)); } } + if let Some(Value::String(s)) = obj.get("commitMessageModel") { + let trimmed = s.trim(); + if trimmed.is_empty() { + result_obj.insert("commitMessageModel".to_string(), Value::Null); + } else { + result_obj.insert("commitMessageModel".to_string(), json!(trimmed)); + } + } // Boolean fields if let Some(Value::Bool(b)) = obj.get("useSystemTheme") { diff --git a/packages/ui/src/components/sections/agents/ModelSelector.tsx b/packages/ui/src/components/sections/agents/ModelSelector.tsx index 73b9004c..68962d2d 100644 --- a/packages/ui/src/components/sections/agents/ModelSelector.tsx +++ b/packages/ui/src/components/sections/agents/ModelSelector.tsx @@ -25,6 +25,7 @@ interface ModelSelectorProps { modelId: string; onChange: (providerId: string, modelId: string) => void; className?: string; + allowedProviderIds?: string[]; } const COMPACT_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', { @@ -49,7 +50,8 @@ export const ModelSelector: React.FC = ({ providerId, modelId, onChange, - className + className, + allowedProviderIds }) => { const { providers, modelsMetadata } = useConfigStore(); const isMobile = useUIStore(state => state.isMobile); @@ -65,6 +67,20 @@ export const ModelSelector: React.FC = ({ const [selectedIndex, setSelectedIndex] = React.useState(0); const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); + const allowedProviderSet = React.useMemo(() => { + if (!Array.isArray(allowedProviderIds) || allowedProviderIds.length === 0) { + return null; + } + return new Set(allowedProviderIds); + }, [allowedProviderIds]); + + const visibleProviders = React.useMemo(() => { + if (!allowedProviderSet) { + return providers; + } + return providers.filter((provider) => allowedProviderSet.has(String(provider.id))); + }, [providers, allowedProviderSet]); + const closeMobilePanel = () => setIsMobilePanelOpen(false); const toggleMobileProviderExpansion = (provId: string) => { setExpandedMobileProviders(prev => { @@ -188,6 +204,9 @@ export const ModelSelector: React.FC = ({ // Filter data for desktop dropdown const filteredFavorites = favoriteModelsList.filter(({ model, providerID }) => { + if (allowedProviderSet && !allowedProviderSet.has(providerID)) { + return false; + } const provider = providers.find(p => p.id === providerID); const providerName = provider?.name || providerID; const modelName = getModelDisplayName(model); @@ -195,13 +214,16 @@ export const ModelSelector: React.FC = ({ }); const filteredRecents = recentModelsList.filter(({ model, providerID }) => { + if (allowedProviderSet && !allowedProviderSet.has(providerID)) { + return false; + } const provider = providers.find(p => p.id === providerID); const providerName = provider?.name || providerID; const modelName = getModelDisplayName(model); return filterByQuery(modelName, providerName); }); - const filteredProviders = providers + const filteredProviders = visibleProviders .map((provider) => { const providerModels = Array.isArray(provider.models) ? provider.models : []; const filteredModels = providerModels.filter((model: ProviderModel) => { @@ -332,7 +354,7 @@ export const ModelSelector: React.FC = ({ )} - {providers.map((provider) => { + {visibleProviders.map((provider) => { const providerModels = Array.isArray(provider.models) ? provider.models : []; if (providerModels.length === 0) return null; diff --git a/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx b/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx index 68c18c52..be425235 100644 --- a/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/DefaultsSettings.tsx @@ -10,6 +10,33 @@ import { useConfigStore } from '@/stores/useConfigStore'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { getModifierLabel } from '@/lib/utils'; +const FALLBACK_PROVIDER_ID = 'opencode'; +const FALLBACK_MODEL_ID = 'big-pickle'; + +const getDisplayModel = ( + storedModel: string | undefined, + providers: Array<{ id: string; models: Array<{ id: string }> }> +): { providerId: string; modelId: string } => { + if (storedModel) { + const parts = storedModel.split('/'); + if (parts.length === 2 && parts[0] && parts[1]) { + return { providerId: parts[0], modelId: parts[1] }; + } + } + + const fallbackProvider = providers.find(p => p.id === FALLBACK_PROVIDER_ID); + if (fallbackProvider?.models.some(m => m.id === FALLBACK_MODEL_ID)) { + return { providerId: FALLBACK_PROVIDER_ID, modelId: FALLBACK_MODEL_ID }; + } + + const firstProvider = providers[0]; + if (firstProvider?.models[0]) { + return { providerId: firstProvider.id, modelId: firstProvider.models[0].id }; + } + + return { providerId: '', modelId: '' }; +}; + export const DefaultsSettings: React.FC = () => { const setProvider = useConfigStore((state) => state.setProvider); const setModel = useConfigStore((state) => state.setModel); @@ -27,13 +54,9 @@ export const DefaultsSettings: React.FC = () => { const [defaultAgent, setDefaultAgent] = React.useState(); const [isLoading, setIsLoading] = React.useState(true); - // Parse "provider/model" string into separate parts const parsedModel = React.useMemo(() => { - if (!defaultModel) return { providerId: '', modelId: '' }; - const parts = defaultModel.split('/'); - if (parts.length !== 2) return { providerId: '', modelId: '' }; - return { providerId: parts[0] || '', modelId: parts[1] || '' }; - }, [defaultModel]); + return getDisplayModel(defaultModel, providers); + }, [defaultModel, providers]); // Load current settings React.useEffect(() => { @@ -180,6 +203,7 @@ export const DefaultsSettings: React.FC = () => { }, [setAgent, setSettingsDefaultAgent]); const availableVariants = React.useMemo(() => { + if (!parsedModel.providerId || !parsedModel.modelId) return []; const provider = providers.find((p) => p.id === parsedModel.providerId); const model = provider?.models.find((m: Record) => (m as { id?: string }).id === parsedModel.modelId) as | { variants?: Record } @@ -274,16 +298,16 @@ export const DefaultsSettings: React.FC = () => { - {(defaultModel || defaultAgent) && ( + {(parsedModel.providerId || defaultAgent) && (
New sessions will start with:{' '} - {defaultModel && ( + {parsedModel.providerId && ( - {defaultModel} + {parsedModel.providerId}/{parsedModel.modelId} {supportsVariants ? ` (${defaultVariant ?? 'default'})` : ''} )} - {defaultModel && defaultAgent && ' / '} + {parsedModel.providerId && defaultAgent && ' / '} {defaultAgent && {defaultAgent}}
)} diff --git a/packages/ui/src/components/sections/openchamber/GitSettings.tsx b/packages/ui/src/components/sections/openchamber/GitSettings.tsx new file mode 100644 index 00000000..072617ac --- /dev/null +++ b/packages/ui/src/components/sections/openchamber/GitSettings.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { RiInformationLine } from '@remixicon/react'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { ModelSelector } from '@/components/sections/agents/ModelSelector'; +import { updateDesktopSettings } from '@/lib/persistence'; +import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop'; +import { useConfigStore } from '@/stores/useConfigStore'; +import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; + +const FALLBACK_PROVIDER_ID = 'opencode'; +const FALLBACK_MODEL_ID = 'big-pickle'; + +const getDisplayModel = ( + storedModel: string | undefined, + providers: Array<{ id: string; models: Array<{ id: string }> }> +): { providerId: string; modelId: string } => { + if (storedModel) { + const parts = storedModel.split('/'); + if (parts.length === 2 && parts[0] && parts[1]) { + return { providerId: parts[0], modelId: parts[1] }; + } + } + + const fallbackProvider = providers.find(p => p.id === FALLBACK_PROVIDER_ID); + if (fallbackProvider?.models.some(m => m.id === FALLBACK_MODEL_ID)) { + return { providerId: FALLBACK_PROVIDER_ID, modelId: FALLBACK_MODEL_ID }; + } + + const firstProvider = providers[0]; + if (firstProvider?.models[0]) { + return { providerId: firstProvider.id, modelId: firstProvider.models[0].id }; + } + + return { providerId: '', modelId: '' }; +}; + +export const GitSettings: React.FC = () => { + const settingsCommitMessageModel = useConfigStore((state) => state.settingsCommitMessageModel); + const setSettingsCommitMessageModel = useConfigStore((state) => state.setSettingsCommitMessageModel); + const providers = useConfigStore((state) => state.providers); + + const [isLoading, setIsLoading] = React.useState(true); + + const opencodeProviders = React.useMemo(() => { + return providers.filter((provider) => provider.id === FALLBACK_PROVIDER_ID); + }, [providers]); + + const parsedModel = React.useMemo(() => { + const effectiveStoredModel = settingsCommitMessageModel?.startsWith(`${FALLBACK_PROVIDER_ID}/`) + ? settingsCommitMessageModel + : undefined; + return getDisplayModel(effectiveStoredModel, opencodeProviders); + }, [settingsCommitMessageModel, opencodeProviders]); + + // Load current settings + React.useEffect(() => { + const loadSettings = async () => { + try { + let data: { commitMessageModel?: string } | null = null; + + // 1. Desktop runtime (Tauri) + if (isDesktopRuntime()) { + data = await getDesktopSettings(); + } else { + // 2. Runtime settings API (VSCode) + const runtimeSettings = getRegisteredRuntimeAPIs()?.settings; + if (runtimeSettings) { + try { + const result = await runtimeSettings.load(); + const settings = result?.settings; + if (settings) { + data = { + commitMessageModel: typeof settings.commitMessageModel === 'string' ? settings.commitMessageModel : undefined, + }; + } + } catch { + // Fall through to fetch + } + } + + // 3. Fetch API (Web) + if (!data) { + const response = await fetch('/api/config/settings', { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + if (response.ok) { + data = await response.json(); + } + } + } + + if (data) { + const model = typeof data.commitMessageModel === 'string' && data.commitMessageModel.trim().length > 0 + ? data.commitMessageModel.trim() + : undefined; + setSettingsCommitMessageModel(model); + } + } catch (error) { + console.warn('Failed to load git settings:', error); + } finally { + setIsLoading(false); + } + }; + loadSettings(); + }, [setSettingsCommitMessageModel]); + + const handleModelChange = React.useCallback(async (providerId: string, modelId: string) => { + const newValue = providerId && modelId ? `${providerId}/${modelId}` : undefined; + setSettingsCommitMessageModel(newValue); + + try { + await updateDesktopSettings({ + commitMessageModel: newValue ?? '', + }); + } catch (error) { + console.warn('Failed to save commit message model:', error); + } + }, [setSettingsCommitMessageModel]); + + if (isLoading) { + return null; + } + + return ( +
+
+
+

Commit Messages

+ + + + + + Configure how commit messages are generated. + + +
+
+ +
+
+ + +

+ This model will be used to analyze diffs and suggest commit messages. + {!settingsCommitMessageModel && <> Default: opencode/big-pickle} +

+
+
+
+ ); +}; diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx index 56b7542a..80a17664 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx @@ -3,6 +3,7 @@ import { OpenChamberVisualSettings } from './OpenChamberVisualSettings'; import { AboutSettings } from './AboutSettings'; import { SessionRetentionSettings } from './SessionRetentionSettings'; import { DefaultsSettings } from './DefaultsSettings'; +import { GitSettings } from './GitSettings'; import { WorktreeSectionContent } from './WorktreeSectionContent'; import { NotificationSettings } from './NotificationSettings'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; @@ -52,8 +53,8 @@ export const OpenChamberPage: React.FC = ({ section }) => return ; case 'sessions': return ; - case 'worktree': - return ; + case 'git': + return ; case 'notifications': return ; default: @@ -94,6 +95,24 @@ const SessionsSectionContent: React.FC = () => { ); }; +// Git section: Commit message model, Worktree settings +const GitSectionContent: React.FC = () => { + return ( +
+ +
+
+

Worktree

+

+ Configure worktree branch defaults and manage existing worktrees. +

+
+ +
+
+ ); +}; + // Notifications section: Native browser notifications const NotificationSectionContent: React.FC = () => { return ; diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberSidebar.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberSidebar.tsx index fec6b035..e10f263a 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberSidebar.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberSidebar.tsx @@ -5,7 +5,7 @@ import { isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; import { AboutSettings } from './AboutSettings'; import { cn } from '@/lib/utils'; -export type OpenChamberSection = 'visual' | 'chat' | 'sessions' | 'worktree' | 'notifications'; +export type OpenChamberSection = 'visual' | 'chat' | 'sessions' | 'git' | 'notifications'; interface OpenChamberSidebarProps { selectedSection: OpenChamberSection; @@ -35,9 +35,9 @@ const OPENCHAMBER_SECTION_GROUPS: SectionGroup[] = [ items: ['Defaults', 'Retention'], }, { - id: 'worktree', - label: 'Worktree', - items: ['Branch', 'Setup'], + id: 'git', + label: 'Git', + items: ['Commit Messages', 'Worktree'], }, { id: 'notifications', diff --git a/packages/ui/src/components/sections/openchamber/WorktreeSectionContent.tsx b/packages/ui/src/components/sections/openchamber/WorktreeSectionContent.tsx index 58efd7e0..7fb62d43 100644 --- a/packages/ui/src/components/sections/openchamber/WorktreeSectionContent.tsx +++ b/packages/ui/src/components/sections/openchamber/WorktreeSectionContent.tsx @@ -326,40 +326,25 @@ export const WorktreeSectionContent: React.FC = () => { if (!projectPath) { return ( -
-
-

Worktree settings

-

- Select a project to configure worktree defaults. -

-
-
+

+ Select a project to configure worktree defaults. +

); } if (isLoadingGit) { return ( -
-
-

Worktree settings

-

- Loading... -

-
-
+

+ Loading... +

); } if (isGitRepo === false) { return ( -
-
-

Worktree settings

-

- Worktree settings are only available for Git repositories. -

-
-
+

+ Worktree settings are only available for Git repositories. +

); } diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index 019ba68d..2a4c15b4 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -58,6 +58,7 @@ export type DesktopSettings = { defaultAgent?: string; autoCreateWorktree?: boolean; queueModeEnabled?: boolean; + commitMessageModel?: string; // format: "provider/model" // User-added skills catalogs (persisted to ~/.config/openchamber/settings.json) skillCatalogs?: SkillCatalogConfig[]; diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index c0ed0b29..33240001 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -47,6 +47,11 @@ const persistToLocalStorage = (settings: DesktopSettings) => { } else { localStorage.removeItem('pinnedDirectories'); } + if (settings.commitMessageModel) { + localStorage.setItem('commitMessageModel', settings.commitMessageModel); + } else { + localStorage.removeItem('commitMessageModel'); + } }; type PersistApi = { @@ -261,6 +266,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { if (typeof candidate.autoCreateWorktree === 'boolean') { result.autoCreateWorktree = candidate.autoCreateWorktree; } + if (typeof candidate.commitMessageModel === 'string' && candidate.commitMessageModel.length > 0) { + result.commitMessageModel = candidate.commitMessageModel; + } if (typeof candidate.queueModeEnabled === 'boolean') { result.queueModeEnabled = candidate.queueModeEnabled; } diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index 5c4ccafb..2b73e427 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -25,6 +25,7 @@ interface OpenChamberDefaults { defaultVariant?: string; defaultAgent?: string; autoCreateWorktree?: boolean; + commitMessageModel?: string; } const fetchOpenChamberDefaults = async (): Promise => { @@ -37,6 +38,7 @@ const fetchOpenChamberDefaults = async (): Promise => { defaultVariant: settings?.defaultVariant, defaultAgent: settings?.defaultAgent, autoCreateWorktree: settings?.autoCreateWorktree, + commitMessageModel: settings?.commitMessageModel, }; } @@ -50,12 +52,14 @@ const fetchOpenChamberDefaults = async (): Promise => { const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : ''; const defaultVariant = typeof data?.defaultVariant === 'string' ? data.defaultVariant.trim() : ''; const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : ''; + const commitMessageModel = typeof data?.commitMessageModel === 'string' ? data.commitMessageModel.trim() : ''; return { defaultModel: defaultModel.length > 0 ? defaultModel : undefined, defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined, defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined, autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined, + commitMessageModel: commitMessageModel.length > 0 ? commitMessageModel : undefined, }; } } catch { @@ -75,12 +79,14 @@ const fetchOpenChamberDefaults = async (): Promise => { const defaultModel = typeof data?.defaultModel === 'string' ? data.defaultModel.trim() : ''; const defaultVariant = typeof data?.defaultVariant === 'string' ? data.defaultVariant.trim() : ''; const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : ''; + const commitMessageModel = typeof data?.commitMessageModel === 'string' ? data.commitMessageModel.trim() : ''; return { defaultModel: defaultModel.length > 0 ? defaultModel : undefined, defaultVariant: defaultVariant.length > 0 ? defaultVariant : undefined, defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined, autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined, + commitMessageModel: commitMessageModel.length > 0 ? commitMessageModel : undefined, }; } catch { return {}; @@ -372,6 +378,7 @@ interface ConfigStore { settingsDefaultVariant: string | undefined; settingsDefaultAgent: string | undefined; settingsAutoCreateWorktree: boolean; + settingsCommitMessageModel: string | undefined; // format: "provider/model" activateDirectory: (directory: string | null | undefined) => Promise; @@ -388,6 +395,7 @@ interface ConfigStore { setSettingsDefaultVariant: (variant: string | undefined) => void; setSettingsDefaultAgent: (agent: string | undefined) => void; setSettingsAutoCreateWorktree: (enabled: boolean) => void; + setSettingsCommitMessageModel: (model: string | undefined) => void; saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void; getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null; checkConnection: () => Promise; @@ -431,6 +439,7 @@ export const useConfigStore = create()( settingsDefaultVariant: undefined, settingsDefaultAgent: undefined, settingsAutoCreateWorktree: false, + settingsCommitMessageModel: undefined, activateDirectory: async (directory) => { const directoryKey = toDirectoryKey(directory); @@ -871,10 +880,12 @@ export const useConfigStore = create()( const nextState: Partial = { settingsDefaultModel: openChamberDefaults.defaultModel, - settingsDefaultVariant: openChamberDefaults.defaultVariant, - settingsDefaultAgent: openChamberDefaults.defaultAgent, - settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false, - directoryScoped: { + settingsDefaultVariant: openChamberDefaults.defaultVariant, + settingsDefaultAgent: openChamberDefaults.defaultAgent, + settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false, + settingsCommitMessageModel: openChamberDefaults.commitMessageModel, + directoryScoped: { + ...state.directoryScoped, [directoryKey]: nextSnapshot, }, @@ -1292,6 +1303,10 @@ export const useConfigStore = create()( set({ settingsAutoCreateWorktree: enabled }); }, + setSettingsCommitMessageModel: (model) => { + set({ settingsCommitMessageModel: model }); + }, + checkConnection: async () => { const maxAttempts = 5; let attempt = 0; diff --git a/packages/web/server/index.js b/packages/web/server/index.js index 45a519b6..9daad147 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -611,6 +611,10 @@ const sanitizeSettingsUpdate = (payload) => { if (typeof candidate.autoCreateWorktree === 'boolean') { result.autoCreateWorktree = candidate.autoCreateWorktree; } + if (typeof candidate.commitMessageModel === 'string') { + const trimmed = candidate.commitMessageModel.trim(); + result.commitMessageModel = trimmed.length > 0 ? trimmed : undefined; + } const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs); if (skillCatalogs) { @@ -3501,6 +3505,15 @@ async function main(options = {}) { .join('\n\n'); const prompt = `You are drafting git commit notes for this codebase. Respond in JSON of the shape {"subject": string, "highlights": string[]} (ONLY the JSON in response, no markdown wrappers or anything except JSON) with these rules:\n- subject follows our convention: type[optional-scope]: summary (examples: "feat: add diff virtualization", "fix(chat): restore enter key handling")\n- allowed types: feat, fix, chore, style, refactor, perf, docs, test, build, ci (choose the best match or fallback to chore)\n- summary must be imperative, concise, <= 70 characters, no trailing punctuation\n- scope is optional; include only when obvious from filenames/folders; do not invent scopes\n- focus on the most impactful user-facing change; if multiple capabilities ship together, align the subject with the dominant theme and use highlights to cover the other major outcomes\n- highlights array should contain 2-3 plain sentences (<= 90 chars each) that describe distinct features or UI changes users will notice (e.g. "Add per-file revert action in Changes list"). Avoid subjective benefit statements, marketing tone, repeating the subject, or referencing helper function names. Highlight additions such as new controls/buttons, new actions (e.g. revert), or stored state changes explicitly. Skip highlights if fewer than two meaningful points exist.\n- text must be plain (no markdown bullets); each highlight should start with an uppercase verb\n\nDiff summary:\n${diffSummaries}`; + + const settings = await readSettingsFromDiskMigrated(); + const rawModel = typeof settings.commitMessageModel === 'string' ? settings.commitMessageModel.trim() : ''; + const model = (() => { + if (!rawModel) return 'big-pickle'; + const parts = rawModel.split('/').filter(Boolean); + const candidate = parts.length > 1 ? parts[parts.length - 1] : parts[0]; + return candidate || 'big-pickle'; + })(); const completionTimeout = createTimeoutSignal(LONG_REQUEST_TIMEOUT_MS); let response; @@ -3509,7 +3522,7 @@ async function main(options = {}) { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - model: 'big-pickle', + model, messages: [{ role: 'user', content: prompt }], max_tokens: 3000, stream: false,