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} -

-
-