From 494da1315c0fb5da5bc199b3e05ffe1dc660a6cd Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 19 Jan 2026 03:42:34 +0200 Subject: [PATCH] refactoring: switched to gpt-5-nano for commit message generation Remove commitMessageModel from persisted settings and defaults Eliminate UI controls and state for commitMessageModel in GitSettings and config store Update backend and persistence layers to stop reading or writing commitMessageModel --- .../desktop/src-tauri/src/commands/git.rs | 95 ++++++++++++++----- .../src-tauri/src/commands/settings.rs | 9 -- .../sections/openchamber/GitSettings.tsx | 77 +-------------- packages/ui/src/hooks/useChatScrollManager.ts | 2 +- packages/ui/src/lib/desktop.ts | 1 - packages/ui/src/lib/persistence.ts | 8 -- packages/ui/src/stores/useConfigStore.ts | 15 --- packages/web/server/index.js | 95 +++++++++++-------- 8 files changed, 129 insertions(+), 173 deletions(-) diff --git a/packages/desktop/src-tauri/src/commands/git.rs b/packages/desktop/src-tauri/src/commands/git.rs index 6a9ee58b..521c1ac7 100644 --- a/packages/desktop/src-tauri/src/commands/git.rs +++ b/packages/desktop/src-tauri/src/commands/git.rs @@ -15,6 +15,44 @@ use tokio::fs; use tokio::io::AsyncReadExt; use tokio::process::Command; +fn extract_json_object(value: &str) -> Option { + let trimmed = value.trim(); + if trimmed.is_empty() { + return None; + } + + let mut start = match trimmed.find('{') { + Some(index) => index, + None => return None, + }; + + while start < trimmed.len() { + let mut end = match trimmed[start..].find('}') { + Some(index) => start + index, + None => break, + }; + + loop { + let candidate = &trimmed[start..=end]; + if serde_json::from_str::(candidate).is_ok() { + return Some(candidate.to_string()); + } + + end = match trimmed[end + 1..].find('}') { + Some(index) => end + 1 + index, + None => break, + }; + } + + start = match trimmed[start + 1..].find('{') { + Some(index) => start + 1 + index, + None => break, + }; + } + + None +} + const GIT_IDENTITY_STORAGE_FILE: &str = "git-identities.json"; const GIT_FILE_DIFF_TIMEOUT_MS: u64 = 15_000; const GIT_LS_REMOTE_TIMEOUT_MS: u64 = 5_000; @@ -2225,32 +2263,17 @@ 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 - }; + let model = "gpt-5-nano"; // 3. Call API let client = Client::new(); let res = client - .post("https://opencode.ai/zen/v1/chat/completions") + .post("https://opencode.ai/zen/v1/responses") .json(&serde_json::json!({ "model": model, - "messages": [{ "role": "user", "content": prompt }], - "max_tokens": 3000, + "input": [{ "role": "user", "content": prompt }], + "max_output_tokens": 1000, "stream": false, "reasoning": { "effort": "low" @@ -2265,8 +2288,12 @@ Diff summary: } let body: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; - let raw_content = body["choices"][0]["message"]["content"] - .as_str() + let raw_content = body["output"] + .as_array() + .and_then(|items| items.iter().find(|item| item["type"] == "message")) + .and_then(|item| item["content"].as_array()) + .and_then(|content| content.iter().find(|entry| entry["type"] == "output_text")) + .and_then(|entry| entry["text"].as_str()) .unwrap_or("") .trim(); @@ -2278,8 +2305,28 @@ Diff summary: .trim_end_matches("```") .trim(); - let message: GeneratedCommitMessage = - serde_json::from_str(cleaned).map_err(|e| format!("Failed to parse AI response: {}", e))?; + let extracted = extract_json_object(cleaned); - Ok(CommitMessageResponse { message }) + let mut last_error: Option = None; + + if let Some(candidate) = extracted.as_deref() { + if candidate.starts_with('{') || candidate.starts_with('[') { + match serde_json::from_str::(candidate) { + Ok(message) => return Ok(CommitMessageResponse { message }), + Err(err) => last_error = Some(err.to_string()), + } + } + } + + if cleaned.starts_with('{') || cleaned.starts_with('[') { + match serde_json::from_str::(cleaned) { + Ok(message) => return Ok(CommitMessageResponse { message }), + Err(err) => last_error = Some(err.to_string()), + } + } + + Err(format!( + "Failed to parse AI response: {}", + last_error.unwrap_or_else(|| "unknown error".to_string()) + )) } diff --git a/packages/desktop/src-tauri/src/commands/settings.rs b/packages/desktop/src-tauri/src/commands/settings.rs index 4f93215b..85bdded9 100644 --- a/packages/desktop/src-tauri/src/commands/settings.rs +++ b/packages/desktop/src-tauri/src/commands/settings.rs @@ -265,15 +265,6 @@ fn sanitize_settings_update(payload: &Value) -> Value { result_obj.insert("defaultGitIdentityId".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("gitmojiEnabled") { result_obj.insert("gitmojiEnabled".to_string(), json!(b)); diff --git a/packages/ui/src/components/sections/openchamber/GitSettings.tsx b/packages/ui/src/components/sections/openchamber/GitSettings.tsx index 0dce615f..630108b0 100644 --- a/packages/ui/src/components/sections/openchamber/GitSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/GitSettings.tsx @@ -1,64 +1,23 @@ 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 settingsGitmojiEnabled = useConfigStore((state) => state.settingsGitmojiEnabled); const setSettingsGitmojiEnabled = useConfigStore((state) => state.setSettingsGitmojiEnabled); - 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; gitmojiEnabled?: boolean } | null = null; + let data: { gitmojiEnabled?: boolean } | null = null; // 1. Desktop runtime (Tauri) if (isDesktopRuntime()) { @@ -72,7 +31,6 @@ export const GitSettings: React.FC = () => { const settings = result?.settings; if (settings) { data = { - commitMessageModel: typeof settings.commitMessageModel === 'string' ? settings.commitMessageModel : undefined, gitmojiEnabled: typeof (settings as Record).gitmojiEnabled === 'boolean' ? ((settings as Record).gitmojiEnabled as boolean) : undefined, @@ -96,10 +54,6 @@ export const GitSettings: React.FC = () => { } if (data) { - const model = typeof data.commitMessageModel === 'string' && data.commitMessageModel.trim().length > 0 - ? data.commitMessageModel.trim() - : undefined; - setSettingsCommitMessageModel(model); if (typeof data.gitmojiEnabled === 'boolean') { setSettingsGitmojiEnabled(data.gitmojiEnabled); } @@ -112,20 +66,7 @@ export const GitSettings: React.FC = () => { } }; loadSettings(); - }, [setSettingsCommitMessageModel, setSettingsGitmojiEnabled]); - - 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]); + }, [setSettingsGitmojiEnabled]); const handleGitmojiChange = React.useCallback(async (event: React.ChangeEvent) => { const enabled = event.target.checked; @@ -160,20 +101,6 @@ export const GitSettings: React.FC = () => {
-
- Model for generation - -

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

-
-