diff --git a/src-tauri/src/backend/app_server.rs b/src-tauri/src/backend/app_server.rs index 659e5d27..a0eb1c8e 100644 --- a/src-tauri/src/backend/app_server.rs +++ b/src-tauri/src/backend/app_server.rs @@ -34,6 +34,19 @@ fn extract_thread_id(value: &Value) -> Option { }) } +fn build_initialize_params(client_version: &str) -> Value { + json!({ + "clientInfo": { + "name": "codex_monitor", + "title": "Codex Monitor", + "version": client_version + }, + "capabilities": { + "experimentalApi": true + } + }) +} + pub(crate) struct WorkspaceSession { pub(crate) entry: WorkspaceEntry, pub(crate) child: Mutex, @@ -332,13 +345,7 @@ pub(crate) async fn spawn_workspace_session( } }); - let init_params = json!({ - "clientInfo": { - "name": "codex_monitor", - "title": "Codex Monitor", - "version": client_version - } - }); + let init_params = build_initialize_params(&client_version); let init_result = timeout( Duration::from_secs(15), session.send_request("initialize", init_params), @@ -372,7 +379,7 @@ pub(crate) async fn spawn_workspace_session( #[cfg(test)] mod tests { - use super::extract_thread_id; + use super::{build_initialize_params, extract_thread_id}; use serde_json::json; #[test] @@ -392,4 +399,16 @@ mod tests { let value = json!({ "params": {} }); assert_eq!(extract_thread_id(&value), None); } + + #[test] + fn build_initialize_params_enables_experimental_api() { + let params = build_initialize_params("1.2.3"); + assert_eq!( + params + .get("capabilities") + .and_then(|caps| caps.get("experimentalApi")) + .and_then(|value| value.as_bool()), + Some(true) + ); + } } diff --git a/src-tauri/src/bin/codex_monitor_daemon.rs b/src-tauri/src/bin/codex_monitor_daemon.rs index 17961cea..7af2b33f 100644 --- a/src-tauri/src/bin/codex_monitor_daemon.rs +++ b/src-tauri/src/bin/codex_monitor_daemon.rs @@ -209,6 +209,7 @@ impl DaemonState { parent_id: String, branch: String, name: Option, + copy_agents_md: bool, client_version: String, ) -> Result { let client_version = client_version.clone(); @@ -216,6 +217,7 @@ impl DaemonState { parent_id, branch, name, + copy_agents_md, &self.data_dir, &self.workspaces, &self.sessions, @@ -914,6 +916,13 @@ fn parse_optional_u32(value: &Value, key: &str) -> Option { } } +fn parse_optional_bool(value: &Value, key: &str) -> Option { + match value { + Value::Object(map) => map.get(key).and_then(|value| value.as_bool()), + _ => None, + } +} + fn parse_optional_string_array(value: &Value, key: &str) -> Option> { match value { Value::Object(map) => map.get(key).and_then(|value| value.as_array()).map(|items| { @@ -989,8 +998,9 @@ async fn handle_rpc_request( let parent_id = parse_string(¶ms, "parentId")?; let branch = parse_string(¶ms, "branch")?; let name = parse_optional_string(¶ms, "name"); + let copy_agents_md = parse_optional_bool(¶ms, "copyAgentsMd").unwrap_or(true); let workspace = state - .add_worktree(parent_id, branch, name, client_version) + .add_worktree(parent_id, branch, name, copy_agents_md, client_version) .await?; serde_json::to_value(workspace).map_err(|err| err.to_string()) } diff --git a/src-tauri/src/shared/workspaces_core.rs b/src-tauri/src/shared/workspaces_core.rs index 2dcba7b9..49a6c7aa 100644 --- a/src-tauri/src/shared/workspaces_core.rs +++ b/src-tauri/src/shared/workspaces_core.rs @@ -17,6 +17,44 @@ use uuid::Uuid; pub(crate) const WORKTREE_SETUP_MARKERS_DIR: &str = "worktree-setup"; pub(crate) const WORKTREE_SETUP_MARKER_EXT: &str = "ran"; +const AGENTS_MD_FILE_NAME: &str = "AGENTS.md"; + +fn copy_agents_md_from_parent_to_worktree( + parent_repo_root: &PathBuf, + worktree_root: &PathBuf, +) -> Result<(), String> { + let source_path = parent_repo_root.join(AGENTS_MD_FILE_NAME); + if !source_path.is_file() { + return Ok(()); + } + + let destination_path = worktree_root.join(AGENTS_MD_FILE_NAME); + if destination_path.is_file() { + return Ok(()); + } + + let temp_path = worktree_root.join(format!("{AGENTS_MD_FILE_NAME}.tmp")); + + std::fs::copy(&source_path, &temp_path).map_err(|err| { + format!( + "Failed to copy {} from {} to {}: {err}", + AGENTS_MD_FILE_NAME, + source_path.display(), + temp_path.display() + ) + })?; + + std::fs::rename(&temp_path, &destination_path).map_err(|err| { + let _ = std::fs::remove_file(&temp_path); + format!( + "Failed to finalize {} copy to {}: {err}", + AGENTS_MD_FILE_NAME, + destination_path.display() + ) + })?; + + Ok(()) +} pub(crate) fn normalize_setup_script(script: Option) -> Option { match script { @@ -248,6 +286,7 @@ pub(crate) async fn add_worktree_core< parent_id: String, branch: String, name: Option, + copy_agents_md: bool, data_dir: &PathBuf, workspaces: &Mutex>, sessions: &Mutex>>, @@ -337,6 +376,17 @@ where .await?; } + if copy_agents_md { + if let Err(error) = copy_agents_md_from_parent_to_worktree(&repo_path, &worktree_path) { + eprintln!( + "add_worktree: optional {} copy failed for {}: {}", + AGENTS_MD_FILE_NAME, + worktree_path.display(), + error + ); + } + } + let entry = WorkspaceEntry { id: Uuid::new_v4().to_string(), name: name.clone().unwrap_or_else(|| branch.clone()), @@ -1118,3 +1168,56 @@ fn sort_workspaces(workspaces: &mut [WorkspaceInfo]) { .then_with(|| a.id.cmp(&b.id)) }); } + +#[cfg(test)] +mod tests { + use super::copy_agents_md_from_parent_to_worktree; + use super::AGENTS_MD_FILE_NAME; + use uuid::Uuid; + + fn make_temp_dir() -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("codex-monitor-{}", Uuid::new_v4())); + std::fs::create_dir_all(&dir).expect("failed to create temp dir"); + dir + } + + #[test] + fn copies_agents_md_when_missing_in_worktree() { + let parent = make_temp_dir(); + let worktree = make_temp_dir(); + let parent_agents = parent.join(AGENTS_MD_FILE_NAME); + let worktree_agents = worktree.join(AGENTS_MD_FILE_NAME); + + std::fs::write(&parent_agents, "parent").expect("failed to write parent AGENTS.md"); + + copy_agents_md_from_parent_to_worktree(&parent, &worktree).expect("copy should succeed"); + + let copied = std::fs::read_to_string(&worktree_agents) + .expect("worktree AGENTS.md should exist after copy"); + assert_eq!(copied, "parent"); + + let _ = std::fs::remove_dir_all(parent); + let _ = std::fs::remove_dir_all(worktree); + } + + #[test] + fn does_not_overwrite_existing_worktree_agents_md() { + let parent = make_temp_dir(); + let worktree = make_temp_dir(); + let parent_agents = parent.join(AGENTS_MD_FILE_NAME); + let worktree_agents = worktree.join(AGENTS_MD_FILE_NAME); + + std::fs::write(&parent_agents, "parent").expect("failed to write parent AGENTS.md"); + std::fs::write(&worktree_agents, "branch-specific") + .expect("failed to write worktree AGENTS.md"); + + copy_agents_md_from_parent_to_worktree(&parent, &worktree).expect("copy should succeed"); + + let retained = std::fs::read_to_string(&worktree_agents) + .expect("worktree AGENTS.md should still exist"); + assert_eq!(retained, "branch-specific"); + + let _ = std::fs::remove_dir_all(parent); + let _ = std::fs::remove_dir_all(worktree); + } +} diff --git a/src-tauri/src/workspaces/commands.rs b/src-tauri/src/workspaces/commands.rs index b2f0b278..14d5ff9d 100644 --- a/src-tauri/src/workspaces/commands.rs +++ b/src-tauri/src/workspaces/commands.rs @@ -285,15 +285,22 @@ pub(crate) async fn add_worktree( parent_id: String, branch: String, name: Option, + copy_agents_md: Option, state: State<'_, AppState>, app: AppHandle, ) -> Result { + let copy_agents_md = copy_agents_md.unwrap_or(true); if remote_backend::is_remote_mode(&*state).await { let response = remote_backend::call_remote( &*state, app, "add_worktree", - json!({ "parentId": parent_id, "branch": branch, "name": name }), + json!({ + "parentId": parent_id, + "branch": branch, + "name": name, + "copyAgentsMd": copy_agents_md + }), ) .await?; return serde_json::from_value(response).map_err(|err| err.to_string()); @@ -308,6 +315,7 @@ pub(crate) async fn add_worktree( parent_id, branch, name, + copy_agents_md, &data_dir, &state.workspaces, &state.sessions, diff --git a/src/App.tsx b/src/App.tsx index 757b50d8..a94f4123 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -67,6 +67,7 @@ import { } from "./features/layout/components/SidebarToggleControls"; import { useAppSettingsController } from "./features/app/hooks/useAppSettingsController"; import { useUpdaterController } from "./features/app/hooks/useUpdaterController"; +import { useResponseRequiredNotificationsController } from "./features/app/hooks/useResponseRequiredNotificationsController"; import { useErrorToasts } from "./features/notifications/hooks/useErrorToasts"; import { useComposerShortcuts } from "./features/composer/hooks/useComposerShortcuts"; import { useComposerMenuActions } from "./features/composer/hooks/useComposerMenuActions"; @@ -421,8 +422,7 @@ function MainApp() { onDebug: addDebugEntry, }); - useComposerShortcuts({ - textareaRef: composerInputRef, + const composerShortcuts = { modelShortcut: appSettings.composerModelShortcut, accessShortcut: appSettings.composerAccessShortcut, reasoningShortcut: appSettings.composerReasoningShortcut, @@ -441,6 +441,16 @@ function MainApp() { selectedEffort, onSelectEffort: setSelectedEffort, reasoningSupported, + }; + + useComposerShortcuts({ + textareaRef: composerInputRef, + ...composerShortcuts, + }); + + useComposerShortcuts({ + textareaRef: workspaceHomeTextareaRef, + ...composerShortcuts, }); useComposerMenuActions({ @@ -703,6 +713,15 @@ function MainApp() { customPrompts: prompts, onMessageActivity: queueGitStatusRefresh }); + + useResponseRequiredNotificationsController({ + systemNotificationsEnabled: appSettings.systemNotificationsEnabled, + approvals, + userInputRequests, + getWorkspaceName, + onDebug: addDebugEntry, + }); + const { activeAccount, accountSwitching, @@ -898,6 +917,7 @@ function MainApp() { cancelPrompt: cancelWorktreePrompt, updateName: updateWorktreeName, updateBranch: updateWorktreeBranch, + updateCopyAgentsMd: updateWorktreeCopyAgentsMd, updateSetupScript: updateWorktreeSetupScript, } = useWorktreePrompt({ addWorktreeAgent, @@ -1046,9 +1066,12 @@ function MainApp() { const activePlan = activeThreadId ? planByThread[activeThreadId] ?? null : null; - const hasActivePlan = Boolean( - activePlan && (activePlan.steps.length > 0 || activePlan.explanation) - ); + const activeThreadProcessing = activeThreadId + ? threadStatusById[activeThreadId]?.isProcessing ?? false + : false; + const hasActivePlan = + Boolean(activePlan && (activePlan.steps.length > 0 || activePlan.explanation)) || + activeThreadProcessing; const showHome = !activeWorkspace; const showWorkspaceHome = Boolean(activeWorkspace && !activeThreadId && !isNewAgentDraftMode); const showComposer = (!isCompact @@ -1925,6 +1948,7 @@ function MainApp() { selectedDiffPath, diffScrollRequestId, onSelectDiff: handleSelectDiff, + diffSource, gitLogEntries, gitLogTotal, gitLogAhead, @@ -2282,6 +2306,7 @@ function MainApp() { worktreePrompt={worktreePrompt} onWorktreePromptNameChange={updateWorktreeName} onWorktreePromptChange={updateWorktreeBranch} + onWorktreePromptCopyAgentsMdChange={updateWorktreeCopyAgentsMd} onWorktreeSetupScriptChange={updateWorktreeSetupScript} onWorktreePromptCancel={cancelWorktreePrompt} onWorktreePromptConfirm={confirmWorktreePrompt} diff --git a/src/features/app/components/AppModals.tsx b/src/features/app/components/AppModals.tsx index 190edab1..e64abddf 100644 --- a/src/features/app/components/AppModals.tsx +++ b/src/features/app/components/AppModals.tsx @@ -6,6 +6,7 @@ import { useRenameThreadPrompt } from "../../threads/hooks/useRenameThreadPrompt import { useClonePrompt } from "../../workspaces/hooks/useClonePrompt"; import { useWorktreePrompt } from "../../workspaces/hooks/useWorktreePrompt"; import type { BranchSwitcherState } from "../../git/hooks/useBranchSwitcher"; +import { useGitBranches } from "../../git/hooks/useGitBranches"; const RenameThreadPrompt = lazy(() => import("../../threads/components/RenameThreadPrompt").then((module) => ({ @@ -42,6 +43,7 @@ type AppModalsProps = { worktreePrompt: WorktreePromptState; onWorktreePromptNameChange: (value: string) => void; onWorktreePromptChange: (value: string) => void; + onWorktreePromptCopyAgentsMdChange: (value: boolean) => void; onWorktreeSetupScriptChange: (value: string) => void; onWorktreePromptCancel: () => void; onWorktreePromptConfirm: () => void; @@ -74,6 +76,7 @@ export const AppModals = memo(function AppModals({ worktreePrompt, onWorktreePromptNameChange, onWorktreePromptChange, + onWorktreePromptCopyAgentsMdChange, onWorktreeSetupScriptChange, onWorktreePromptCancel, onWorktreePromptConfirm, @@ -97,6 +100,10 @@ export const AppModals = memo(function AppModals({ SettingsViewComponent, settingsProps, }: AppModalsProps) { + const { branches: worktreeBranches } = useGitBranches({ + activeWorkspace: worktreePrompt?.workspace ?? null, + }); + return ( <> {renamePrompt && ( @@ -116,6 +123,9 @@ export const AppModals = memo(function AppModals({ workspaceName={worktreePrompt.workspace.name} name={worktreePrompt.name} branch={worktreePrompt.branch} + branchWasEdited={worktreePrompt.branchWasEdited} + branchSuggestions={worktreeBranches} + copyAgentsMd={worktreePrompt.copyAgentsMd} setupScript={worktreePrompt.setupScript} scriptError={worktreePrompt.scriptError} error={worktreePrompt.error} @@ -123,6 +133,7 @@ export const AppModals = memo(function AppModals({ isSavingScript={worktreePrompt.isSavingScript} onNameChange={onWorktreePromptNameChange} onChange={onWorktreePromptChange} + onCopyAgentsMdChange={onWorktreePromptCopyAgentsMdChange} onSetupScriptChange={onWorktreeSetupScriptChange} onCancel={onWorktreePromptCancel} onConfirm={onWorktreePromptConfirm} diff --git a/src/features/app/hooks/useResponseRequiredNotificationsController.ts b/src/features/app/hooks/useResponseRequiredNotificationsController.ts new file mode 100644 index 00000000..905bd5b2 --- /dev/null +++ b/src/features/app/hooks/useResponseRequiredNotificationsController.ts @@ -0,0 +1,31 @@ +import type { ApprovalRequest, DebugEntry, RequestUserInputRequest } from "../../../types"; +import { useWindowFocusState } from "../../layout/hooks/useWindowFocusState"; +import { useAgentResponseRequiredNotifications } from "../../notifications/hooks/useAgentResponseRequiredNotifications"; + +type Params = { + systemNotificationsEnabled: boolean; + approvals: ApprovalRequest[]; + userInputRequests: RequestUserInputRequest[]; + getWorkspaceName?: (workspaceId: string) => string | undefined; + onDebug?: (entry: DebugEntry) => void; +}; + +export function useResponseRequiredNotificationsController({ + systemNotificationsEnabled, + approvals, + userInputRequests, + getWorkspaceName, + onDebug, +}: Params) { + const isWindowFocused = useWindowFocusState(); + + useAgentResponseRequiredNotifications({ + enabled: systemNotificationsEnabled, + isWindowFocused, + approvals, + userInputRequests, + getWorkspaceName, + onDebug, + }); +} + diff --git a/src/features/collaboration/hooks/useCollaborationModes.test.tsx b/src/features/collaboration/hooks/useCollaborationModes.test.tsx index 43830e31..37ee58e3 100644 --- a/src/features/collaboration/hooks/useCollaborationModes.test.tsx +++ b/src/features/collaboration/hooks/useCollaborationModes.test.tsx @@ -32,10 +32,16 @@ const workspaceTwoConnected: WorkspaceInfo = { const makeModesResponse = () => ({ result: { - data: [{ mode: "plan" }, { mode: "code" }], + data: [{ mode: "plan" }, { mode: "default" }], }, }); +const makeModesResponseArrayResult = () => ({ + result: [{ mode: "plan" }, { mode: "default" }], +}); + +const makeModesResponseTopLevelArray = () => [{ mode: "plan" }, { mode: "default" }]; + describe("useCollaborationModes", () => { afterEach(() => { vi.clearAllMocks(); @@ -52,7 +58,7 @@ describe("useCollaborationModes", () => { }, ); - await waitFor(() => expect(result.current.selectedCollaborationModeId).toBe("code")); + await waitFor(() => expect(result.current.selectedCollaborationModeId).toBe("default")); act(() => { result.current.setSelectedCollaborationModeId("plan"); @@ -82,7 +88,7 @@ describe("useCollaborationModes", () => { }, ); - await waitFor(() => expect(result.current.selectedCollaborationModeId).toBe("code")); + await waitFor(() => expect(result.current.selectedCollaborationModeId).toBe("default")); act(() => { result.current.setSelectedCollaborationModeId("plan"); @@ -94,5 +100,34 @@ describe("useCollaborationModes", () => { expect(result.current.selectedCollaborationModeId).toBeNull(); expect(result.current.collaborationModes).toEqual([]); }); -}); + it("accepts alternate response shapes from the backend", async () => { + vi.mocked(getCollaborationModes) + .mockResolvedValueOnce(makeModesResponseArrayResult() as any) + .mockResolvedValueOnce(makeModesResponseTopLevelArray() as any); + + const { result, rerender } = renderHook( + ({ workspace }: { workspace: WorkspaceInfo | null }) => + useCollaborationModes({ activeWorkspace: workspace, enabled: true }), + { + initialProps: { workspace: workspaceOne }, + }, + ); + + await waitFor(() => + expect(result.current.collaborationModes.map((mode) => mode.id)).toEqual([ + "plan", + "default", + ]), + ); + + rerender({ workspace: { ...workspaceOne, id: "workspace-1b" } }); + + await waitFor(() => + expect(result.current.collaborationModes.map((mode) => mode.id)).toEqual([ + "plan", + "default", + ]), + ); + }); +}); diff --git a/src/features/collaboration/hooks/useCollaborationModes.ts b/src/features/collaboration/hooks/useCollaborationModes.ts index b7d060b5..71ab17e7 100644 --- a/src/features/collaboration/hooks/useCollaborationModes.ts +++ b/src/features/collaboration/hooks/useCollaborationModes.ts @@ -5,7 +5,6 @@ import type { WorkspaceInfo, } from "../../../types"; import { getCollaborationModes } from "../../../services/tauri"; -import { formatCollaborationModeLabel } from "../../../utils/collaborationModes"; type UseCollaborationModesOptions = { activeWorkspace: WorkspaceInfo | null; @@ -28,6 +27,35 @@ export function useCollaborationModes({ const workspaceId = activeWorkspace?.id ?? null; const isConnected = Boolean(activeWorkspace?.connected); + const extractModeList = useCallback((response: any): any[] => { + const candidates = [ + response?.result?.data, + response?.result?.modes, + response?.result, + response?.data, + response?.modes, + response, + ]; + for (const candidate of candidates) { + if (Array.isArray(candidate)) { + return candidate; + } + if (candidate && typeof candidate === "object") { + const nested = (candidate as any).data ?? (candidate as any).modes; + if (Array.isArray(nested)) { + return nested; + } + if (nested && typeof nested === "object") { + const deep = (nested as any).data ?? (nested as any).modes; + if (Array.isArray(deep)) { + return deep; + } + } + } + } + return []; + }, []); + const selectedMode = useMemo( () => modes.find((mode) => mode.id === selectedModeId) ?? null, [modes, selectedModeId], @@ -57,18 +85,14 @@ export function useCollaborationModes({ label: "collaborationMode/list response", payload: response, }); - const rawData = response.result?.data ?? response.data ?? []; + const rawData = extractModeList(response); const data: CollaborationModeOption[] = rawData .map((item: any) => { if (!item || typeof item !== "object") { return null; } - const mode = String(item.mode ?? item.name ?? ""); - if (!mode) { - return null; - } - const normalizedMode = mode.trim().toLowerCase(); - if (normalizedMode && normalizedMode !== "plan" && normalizedMode !== "code") { + const modeId = String(item.mode ?? item.name ?? "").trim(); + if (!modeId) { return null; } @@ -89,30 +113,40 @@ export function useCollaborationModes({ 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, - }; + const labelSource = + typeof item.label === "string" && item.label.trim() + ? item.label + : typeof item.name === "string" && item.name.trim() + ? item.name + : modeId; - return { - id: normalizedMode, - label: formatCollaborationModeLabel(labelSource), - mode: normalizedMode, + const option: CollaborationModeOption = { + id: modeId, + label: labelSource, + mode: modeId, model, reasoningEffort: reasoningEffort ? String(reasoningEffort) : null, developerInstructions: developerInstructions ? String(developerInstructions) : null, - value: normalizedValue, + value: item as Record, }; + return option; }) - .filter(Boolean); + .filter((mode): mode is CollaborationModeOption => mode !== null); setModes(data); lastFetchedWorkspaceId.current = workspaceId; const preferredModeId = - data.find((mode) => mode.mode === "code" || mode.id === "code")?.id ?? + data.find( + (mode) => + mode.id.trim().toLowerCase() === "default" || + mode.mode.trim().toLowerCase() === "default", + )?.id ?? + data.find( + (mode) => + mode.id.trim().toLowerCase() === "code" || + mode.mode.trim().toLowerCase() === "code", + )?.id ?? data[0]?.id ?? null; setSelectedModeId((currentSelection) => { @@ -136,7 +170,7 @@ export function useCollaborationModes({ } finally { inFlight.current = false; } - }, [enabled, isConnected, onDebug, workspaceId]); + }, [enabled, extractModeList, isConnected, onDebug, workspaceId]); useEffect(() => { selectedModeIdRef.current = selectedModeId; diff --git a/src/features/composer/components/ComposerMetaBar.tsx b/src/features/composer/components/ComposerMetaBar.tsx index 439feebf..138cb5f6 100644 --- a/src/features/composer/components/ComposerMetaBar.tsx +++ b/src/features/composer/components/ComposerMetaBar.tsx @@ -1,6 +1,5 @@ import type { CSSProperties } from "react"; import type { AccessMode, ThreadTokenUsage } from "../../../types"; -import { formatCollaborationModeLabel } from "../../../utils/collaborationModes"; type ComposerMetaBarProps = { disabled: boolean; @@ -74,7 +73,7 @@ export function ComposerMetaBar({ > {collaborationModes.map((mode) => ( ))} diff --git a/src/features/composer/hooks/useComposerShortcuts.test.tsx b/src/features/composer/hooks/useComposerShortcuts.test.tsx new file mode 100644 index 00000000..e5f73e8d --- /dev/null +++ b/src/features/composer/hooks/useComposerShortcuts.test.tsx @@ -0,0 +1,92 @@ +// @vitest-environment jsdom + +import { render } from "@testing-library/react"; +import { useRef } from "react"; +import { describe, expect, it, vi } from "vitest"; +import { useComposerShortcuts } from "./useComposerShortcuts"; + +function ShortcutHarness(props: { + collaborationShortcut: string | null; + collaborationModes: { id: string; label: string }[]; + selectedCollaborationModeId: string | null; + onSelectCollaborationMode: (id: string | null) => void; +}) { + const textareaRef = useRef(null); + useComposerShortcuts({ + textareaRef, + modelShortcut: null, + accessShortcut: null, + reasoningShortcut: null, + collaborationShortcut: props.collaborationShortcut, + models: [], + collaborationModes: props.collaborationModes, + selectedModelId: null, + onSelectModel: () => {}, + selectedCollaborationModeId: props.selectedCollaborationModeId, + onSelectCollaborationMode: props.onSelectCollaborationMode, + accessMode: "read-only", + onSelectAccessMode: () => {}, + reasoningOptions: [], + selectedEffort: null, + onSelectEffort: () => {}, + reasoningSupported: false, + }); + + return