diff --git a/src-tauri/src/codex.rs b/src-tauri/src/codex.rs index bb82cff76..647374520 100644 --- a/src-tauri/src/codex.rs +++ b/src-tauri/src/codex.rs @@ -261,20 +261,24 @@ pub(crate) async fn send_user_message( .map(remote_backend::normalize_path_for_remote) .collect::>() }); + let mut payload = Map::new(); + payload.insert("workspaceId".to_string(), json!(workspace_id)); + payload.insert("threadId".to_string(), json!(thread_id)); + payload.insert("text".to_string(), json!(text)); + payload.insert("model".to_string(), json!(model)); + payload.insert("effort".to_string(), json!(effort)); + payload.insert("accessMode".to_string(), json!(access_mode)); + payload.insert("images".to_string(), json!(images)); + if let Some(mode) = collaboration_mode { + if !mode.is_null() { + payload.insert("collaborationMode".to_string(), mode); + } + } return remote_backend::call_remote( &*state, app, "send_user_message", - json!({ - "workspaceId": workspace_id, - "threadId": thread_id, - "text": text, - "model": model, - "effort": effort, - "accessMode": access_mode, - "images": images, - "collaborationMode": collaboration_mode, - }), + Value::Object(payload), ) .await; } @@ -329,17 +333,22 @@ pub(crate) async fn send_user_message( return Err("empty user message".to_string()); } - let params = json!({ - "threadId": thread_id, - "input": input, - "cwd": session.entry.path, - "approvalPolicy": approval_policy, - "sandboxPolicy": sandbox_policy, - "model": model, - "effort": effort, - "collaborationMode": collaboration_mode, - }); - session.send_request("turn/start", params).await + let mut params = Map::new(); + params.insert("threadId".to_string(), json!(thread_id)); + params.insert("input".to_string(), json!(input)); + params.insert("cwd".to_string(), json!(session.entry.path)); + params.insert("approvalPolicy".to_string(), json!(approval_policy)); + params.insert("sandboxPolicy".to_string(), json!(sandbox_policy)); + params.insert("model".to_string(), json!(model)); + params.insert("effort".to_string(), json!(effort)); + if let Some(mode) = collaboration_mode { + if !mode.is_null() { + params.insert("collaborationMode".to_string(), mode); + } + } + session + .send_request("turn/start", Value::Object(params)) + .await } #[tauri::command] diff --git a/src/App.tsx b/src/App.tsx index 0b03444b2..e4e07627b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,6 +43,7 @@ import { useGitActions } from "./features/git/hooks/useGitActions"; import { useAutoExitEmptyDiff } from "./features/git/hooks/useAutoExitEmptyDiff"; import { useModels } from "./features/models/hooks/useModels"; import { useCollaborationModes } from "./features/collaboration/hooks/useCollaborationModes"; +import { useCollaborationModeSelection } from "./features/collaboration/hooks/useCollaborationModeSelection"; import { useSkills } from "./features/skills/hooks/useSkills"; import { useCustomPrompts } from "./features/prompts/hooks/useCustomPrompts"; import { useWorkspaceFiles } from "./features/workspaces/hooks/useWorkspaceFiles"; @@ -549,6 +550,17 @@ function MainApp() { setSelectedDiffPath, }); + const { collaborationModePayload } = useCollaborationModeSelection({ + selectedCollaborationMode, + selectedCollaborationModeId, + models, + selectedModelId, + selectedEffort, + resolvedModel, + setSelectedModelId, + setSelectedEffort, + }); + const { setActiveThreadId, activeThreadId, @@ -582,14 +594,14 @@ function MainApp() { startReview, handleApprovalDecision, handleApprovalRemember, - handleUserInputSubmit + handleUserInputSubmit, } = useThreads({ activeWorkspace, onWorkspaceConnected: markWorkspaceConnected, onDebug: addDebugEntry, model: resolvedModel, effort: selectedEffort, - collaborationMode: selectedCollaborationMode?.value ?? null, + collaborationMode: collaborationModePayload, accessMode, steerEnabled: appSettings.experimentalSteerEnabled, customPrompts: prompts, diff --git a/src/features/collaboration/hooks/useCollaborationModeSelection.ts b/src/features/collaboration/hooks/useCollaborationModeSelection.ts new file mode 100644 index 000000000..421647719 --- /dev/null +++ b/src/features/collaboration/hooks/useCollaborationModeSelection.ts @@ -0,0 +1,123 @@ +import { useEffect, useMemo, useRef } from "react"; +import type { CollaborationModeOption, ModelOption } from "../../../types"; + +type UseCollaborationModeSelectionOptions = { + selectedCollaborationMode: CollaborationModeOption | null; + selectedCollaborationModeId: string | null; + models: ModelOption[]; + selectedModelId: string | null; + selectedEffort: string | null; + resolvedModel: string | null; + setSelectedModelId: (id: string | null) => void; + setSelectedEffort: (effort: string | null) => void; +}; + +export function useCollaborationModeSelection({ + selectedCollaborationMode, + selectedCollaborationModeId, + models, + selectedModelId, + selectedEffort, + resolvedModel, + setSelectedModelId, + setSelectedEffort, +}: UseCollaborationModeSelectionOptions) { + const lastAppliedCollaborationModeId = useRef(null); + const lastUserModelId = useRef(null); + const lastUserEffort = useRef(null); + const wasCollaborationModeActive = useRef(false); + + const collaborationModeModelOption = useMemo(() => { + const collaborationModeModel = selectedCollaborationMode?.model ?? null; + if (!collaborationModeModel) { + return null; + } + return ( + models.find((model) => model.model === collaborationModeModel) ?? + models.find((model) => model.id === collaborationModeModel) ?? + null + ); + }, [models, selectedCollaborationMode?.model]); + + useEffect(() => { + if (!selectedCollaborationModeId) { + if (wasCollaborationModeActive.current) { + const restoreModelId = lastUserModelId.current; + if ( + restoreModelId && + restoreModelId !== selectedModelId && + models.some((model) => model.id === restoreModelId) + ) { + setSelectedModelId(restoreModelId); + } + if (lastUserEffort.current !== null && lastUserEffort.current !== selectedEffort) { + setSelectedEffort(lastUserEffort.current); + } + lastUserModelId.current = null; + lastUserEffort.current = null; + wasCollaborationModeActive.current = false; + } + lastAppliedCollaborationModeId.current = null; + return; + } + if (!wasCollaborationModeActive.current) { + lastUserModelId.current = selectedModelId; + lastUserEffort.current = selectedEffort; + wasCollaborationModeActive.current = true; + } + if (selectedCollaborationModeId === lastAppliedCollaborationModeId.current) { + return; + } + const collaborationModeModel = selectedCollaborationMode?.model ?? null; + const nextModelId = collaborationModeModelOption?.id ?? null; + if (nextModelId && nextModelId !== selectedModelId) { + setSelectedModelId(nextModelId); + } + const nextEffort = selectedCollaborationMode?.reasoningEffort ?? null; + if (nextEffort && nextEffort !== selectedEffort) { + setSelectedEffort(nextEffort); + } + if (!collaborationModeModel || nextModelId) { + lastAppliedCollaborationModeId.current = selectedCollaborationModeId; + } + }, [ + collaborationModeModelOption?.id, + models, + selectedCollaborationMode?.model, + selectedCollaborationMode?.reasoningEffort, + selectedCollaborationModeId, + selectedEffort, + selectedModelId, + setSelectedEffort, + setSelectedModelId, + ]); + + const collaborationModePayload = useMemo(() => { + if (!selectedCollaborationModeId || !selectedCollaborationMode) { + return null; + } + + const modeValue = selectedCollaborationMode.mode || selectedCollaborationMode.id; + if (!modeValue) { + return null; + } + + return { + mode: modeValue, + settings: { + model: resolvedModel ?? selectedCollaborationMode.model ?? "", + reasoning_effort: + selectedEffort ?? selectedCollaborationMode.reasoningEffort ?? null, + developer_instructions: + selectedCollaborationMode.developerInstructions ?? null, + }, + }; + }, [ + resolvedModel, + selectedCollaborationMode, + selectedCollaborationModeId, + selectedEffort, + ]); + + return { collaborationModePayload }; +} diff --git a/src/features/collaboration/hooks/useCollaborationModes.ts b/src/features/collaboration/hooks/useCollaborationModes.ts index 544b8c636..f2886e264 100644 --- a/src/features/collaboration/hooks/useCollaborationModes.ts +++ b/src/features/collaboration/hooks/useCollaborationModes.ts @@ -59,32 +59,69 @@ export function useCollaborationModes({ const rawData = response.result?.data ?? response.data ?? []; const data: CollaborationModeOption[] = rawData .map((item: any) => { - const mode = String(item.mode ?? ""); + if (!item || typeof item !== "object") { + return null; + } + const mode = String(item.mode ?? item.name ?? ""); if (!mode) { return null; } - const model = String(item.model ?? ""); - const reasoningEffort = - item.reasoningEffort ?? item.reasoning_effort ?? null; - const developerInstructions = - item.developerInstructions ?? item.developer_instructions ?? null; + const normalizedMode = mode.trim().toLowerCase(); + if (normalizedMode && normalizedMode !== "plan" && normalizedMode !== "code") { + return null; + } + + const settings = + item.settings && typeof item.settings === "object" + ? item.settings + : { + model: item.model ?? null, + reasoning_effort: + item.reasoning_effort ?? item.reasoningEffort ?? null, + developer_instructions: + item.developer_instructions ?? + item.developerInstructions ?? + null, + }; + + const model = String(settings.model ?? ""); + const reasoningEffort = settings.reasoning_effort ?? null; + const developerInstructions = settings.developer_instructions ?? null; + + const labelSource = String(item.name ?? item.label ?? mode); + + const normalizedValue = { + ...(item as Record), + mode: normalizedMode, + }; + return { - id: mode, - label: formatCollaborationModeLabel(mode), - mode, + id: normalizedMode, + label: formatCollaborationModeLabel(labelSource), + mode: normalizedMode, model, reasoningEffort: reasoningEffort ? String(reasoningEffort) : null, developerInstructions: developerInstructions ? String(developerInstructions) : null, - value: item as Record, + value: normalizedValue, }; }) .filter(Boolean); setModes(data); lastFetchedWorkspaceId.current = workspaceId; - if (selectedModeId && !data.some((mode) => mode.id === selectedModeId)) { - setSelectedModeId(null); + const preferredModeId = + data.find((mode) => mode.mode === "code" || mode.id === "code")?.id ?? + data[0]?.id ?? + null; + if (!selectedModeId) { + if (preferredModeId) { + setSelectedModeId(preferredModeId); + } + return; + } + if (!data.some((mode) => mode.id === selectedModeId)) { + setSelectedModeId(preferredModeId); } } catch (error) { onDebug?.({ diff --git a/src/features/composer/components/ComposerMetaBar.tsx b/src/features/composer/components/ComposerMetaBar.tsx index 6cbcf9c2c..48fad3414 100644 --- a/src/features/composer/components/ComposerMetaBar.tsx +++ b/src/features/composer/components/ComposerMetaBar.tsx @@ -33,7 +33,6 @@ export function ComposerMetaBar({ onSelectAccessMode, contextUsage = null, }: ComposerMetaBarProps) { - const isCollaborationOverrideActive = Boolean(selectedCollaborationModeId); const contextWindow = contextUsage?.modelContextWindow ?? null; const lastTokens = contextUsage?.last.totalTokens ?? 0; const totalTokens = contextUsage?.total.totalTokens ?? 0; @@ -71,7 +70,6 @@ export function ComposerMetaBar({ } disabled={disabled} > - {collaborationModes.map((mode) => ( } {models.map((model) => ( @@ -157,7 +155,7 @@ export function ComposerMetaBar({ aria-label="Thinking mode" value={selectedEffort ?? ""} onChange={(event) => onSelectEffort(event.target.value)} - disabled={disabled || isCollaborationOverrideActive} + disabled={disabled} > {reasoningOptions.length === 0 && } {reasoningOptions.map((effort) => ( diff --git a/src/features/settings/components/SettingsView.tsx b/src/features/settings/components/SettingsView.tsx index 41b29b36d..c53ee0156 100644 --- a/src/features/settings/components/SettingsView.tsx +++ b/src/features/settings/components/SettingsView.tsx @@ -2250,7 +2250,7 @@ export function SettingsView({
Collaboration modes
- Enable collaboration mode presets (Default, Plan, Pair programming, Execute). + Enable collaboration mode presets (Code, Plan).