diff --git a/src-tauri/src/bin/codex_monitor_daemon.rs b/src-tauri/src/bin/codex_monitor_daemon.rs index 24e8176a4..d0a6d6a2e 100644 --- a/src-tauri/src/bin/codex_monitor_daemon.rs +++ b/src-tauri/src/bin/codex_monitor_daemon.rs @@ -1685,6 +1685,52 @@ mod tests { }); } + #[test] + fn rpc_open_workspace_in_invalid_args_is_rejected() { + run_async_test(async { + let tmp = make_temp_dir("rpc-open-workspace-in-invalid-args"); + let state = test_state(&tmp); + + let err = rpc::handle_rpc_request( + &state, + "open_workspace_in", + json!({ + "path": "/tmp/repo", + "args": [1, 2], + }), + "daemon-test".to_string(), + ) + .await + .expect_err("invalid args should be rejected"); + + assert_eq!(err, "invalid `args`"); + let _ = std::fs::remove_dir_all(&tmp); + }); + } + + #[test] + fn rpc_list_git_roots_invalid_depth_is_rejected() { + run_async_test(async { + let tmp = make_temp_dir("rpc-list-git-roots-invalid-depth"); + let state = test_state(&tmp); + + let err = rpc::handle_rpc_request( + &state, + "list_git_roots", + json!({ + "workspaceId": "ws-invalid-depth", + "depth": "10", + }), + "daemon-test".to_string(), + ) + .await + .expect_err("invalid depth should be rejected"); + + assert_eq!(err, "invalid `depth`"); + let _ = std::fs::remove_dir_all(&tmp); + }); + } + #[test] fn rpc_daemon_info_reports_identity() { run_async_test(async { diff --git a/src-tauri/src/bin/codex_monitor_daemon/rpc.rs b/src-tauri/src/bin/codex_monitor_daemon/rpc.rs index f7dc160e2..091710d8a 100644 --- a/src-tauri/src/bin/codex_monitor_daemon/rpc.rs +++ b/src-tauri/src/bin/codex_monitor_daemon/rpc.rs @@ -85,16 +85,22 @@ pub(super) fn parse_optional_string(value: &Value, key: &str) -> Option } } -pub(super) fn parse_optional_u32(value: &Value, key: &str) -> Option { +pub(super) fn parse_optional_u32(value: &Value, key: &str) -> Result, String> { match value { - Value::Object(map) => map.get(key).and_then(|value| value.as_u64()).and_then(|v| { - if v > u32::MAX as u64 { - None - } else { - Some(v as u32) + Value::Object(map) => match map.get(key) { + None => Ok(None), + Some(value) => { + let raw = value + .as_u64() + .filter(|value| *value <= u32::MAX as u64) + .map(|value| value as u32); + match raw { + Some(value) => Ok(Some(value)), + None => Err(format!("invalid `{key}`")), + } } - }), - _ => None, + }, + _ => Err(format!("invalid `{key}`")), } } @@ -105,23 +111,33 @@ pub(super) fn parse_optional_bool(value: &Value, key: &str) -> Option { } } -pub(super) fn parse_optional_string_array(value: &Value, key: &str) -> Option> { +pub(super) fn parse_optional_string_array( + value: &Value, + key: &str, +) -> Result>, String> { match value { - Value::Object(map) => map - .get(key) - .and_then(|value| value.as_array()) - .map(|items| { - items + Value::Object(map) => match map.get(key) { + None => Ok(None), + Some(value) => { + let Some(items) = value.as_array() else { + return Err(format!("invalid `{key}`")); + }; + let parsed_items = items .iter() - .filter_map(|item| item.as_str().map(|value| value.to_string())) - .collect::>() - }), - _ => None, + .map(|item| item.as_str().map(|value| value.to_string())) + .collect::>>(); + match parsed_items { + Some(items) => Ok(Some(items)), + None => Err(format!("invalid `{key}`")), + } + } + }, + _ => Err(format!("invalid `{key}`")), } } pub(super) fn parse_string_array(value: &Value, key: &str) -> Result, String> { - parse_optional_string_array(value, key).ok_or_else(|| format!("missing `{key}`")) + parse_optional_string_array(value, key)?.ok_or_else(|| format!("missing `{key}`")) } pub(super) fn parse_optional_value(value: &Value, key: &str) -> Option { @@ -184,3 +200,51 @@ pub(super) fn spawn_rpc_response_task( } }); } + +#[cfg(test)] +mod tests { + use serde_json::json; + + #[test] + fn parse_optional_u32_rejects_non_numeric_values() { + let err = super::parse_optional_u32(&json!({ "limit": "20" }), "limit") + .expect_err("limit should be invalid"); + assert_eq!(err, "invalid `limit`"); + } + + #[test] + fn parse_optional_u32_rejects_overflow_values() { + let err = super::parse_optional_u32(&json!({ "limit": 4294967296u64 }), "limit") + .expect_err("limit should overflow u32"); + assert_eq!(err, "invalid `limit`"); + } + + #[test] + fn parse_optional_u32_allows_missing_value() { + let value = super::parse_optional_u32(&json!({ "depth": 5 }), "limit") + .expect("parse should succeed"); + assert!(value.is_none()); + } + + #[test] + fn parse_optional_string_array_rejects_non_array_values() { + let err = super::parse_optional_string_array(&json!({ "images": "banner.png" }), "images") + .expect_err("images should be an array"); + assert_eq!(err, "invalid `images`"); + } + + #[test] + fn parse_optional_string_array_rejects_mixed_type_items() { + let err = + super::parse_optional_string_array(&json!({ "images": ["image.png", 5] }), "images") + .expect_err("images should only contain strings"); + assert_eq!(err, "invalid `images`"); + } + + #[test] + fn parse_optional_string_array_allows_missing_value() { + let value = super::parse_optional_string_array(&json!({ "images": ["image.png"] }), "args") + .expect("parse should succeed"); + assert!(value.is_none()); + } +} diff --git a/src-tauri/src/bin/codex_monitor_daemon/rpc/codex.rs b/src-tauri/src/bin/codex_monitor_daemon/rpc/codex.rs index 7a5411a1d..efbf6bd6f 100644 --- a/src-tauri/src/bin/codex_monitor_daemon/rpc/codex.rs +++ b/src-tauri/src/bin/codex_monitor_daemon/rpc/codex.rs @@ -87,7 +87,10 @@ pub(super) async fn try_handle( Err(err) => return Some(Err(err)), }; let cursor = parse_optional_string(params, "cursor"); - let limit = parse_optional_u32(params, "limit"); + let limit = match parse_optional_u32(params, "limit") { + Ok(value) => value, + Err(err) => return Some(Err(err)), + }; let sort_key = parse_optional_string(params, "sortKey"); let cwd = parse_optional_string(params, "cwd"); Some( @@ -102,7 +105,10 @@ pub(super) async fn try_handle( Err(err) => return Some(Err(err)), }; let cursor = parse_optional_string(params, "cursor"); - let limit = parse_optional_u32(params, "limit"); + let limit = match parse_optional_u32(params, "limit") { + Ok(value) => value, + Err(err) => return Some(Err(err)), + }; Some( state .list_mcp_server_status(workspace_id, cursor, limit) @@ -162,7 +168,10 @@ pub(super) async fn try_handle( let model = parse_optional_string(params, "model"); let effort = parse_optional_string(params, "effort"); let access_mode = parse_optional_string(params, "accessMode"); - let images = parse_optional_string_array(params, "images"); + let images = match parse_optional_string_array(params, "images") { + Ok(value) => value, + Err(err) => return Some(Err(err)), + }; let app_mentions = parse_optional_value(params, "appMentions") .and_then(|value| value.as_array().cloned()); let collaboration_mode = parse_optional_value(params, "collaborationMode"); @@ -214,7 +223,10 @@ pub(super) async fn try_handle( Ok(value) => value, Err(err) => return Some(Err(err)), }; - let images = parse_optional_string_array(params, "images"); + let images = match parse_optional_string_array(params, "images") { + Ok(value) => value, + Err(err) => return Some(Err(err)), + }; let app_mentions = parse_optional_value(params, "appMentions") .and_then(|value| value.as_array().cloned()); Some( @@ -261,8 +273,15 @@ pub(super) async fn try_handle( Err(err) => return Some(Err(err)), }; let cursor = parse_optional_string(params, "cursor"); - let limit = parse_optional_u32(params, "limit"); - Some(state.experimental_feature_list(workspace_id, cursor, limit).await) + let limit = match parse_optional_u32(params, "limit") { + Ok(value) => value, + Err(err) => return Some(Err(err)), + }; + Some( + state + .experimental_feature_list(workspace_id, cursor, limit) + .await, + ) } "collaboration_mode_list" => { let workspace_id = match parse_string(params, "workspaceId") { @@ -410,7 +429,10 @@ pub(super) async fn try_handle( Err(err) => return Some(Err(err)), }; let cursor = parse_optional_string(params, "cursor"); - let limit = parse_optional_u32(params, "limit"); + let limit = match parse_optional_u32(params, "limit") { + Ok(value) => value, + Err(err) => return Some(Err(err)), + }; let thread_id = parse_optional_string(params, "threadId"); Some( state diff --git a/src-tauri/src/bin/codex_monitor_daemon/rpc/git.rs b/src-tauri/src/bin/codex_monitor_daemon/rpc/git.rs index 388819b37..53832e462 100644 --- a/src-tauri/src/bin/codex_monitor_daemon/rpc/git.rs +++ b/src-tauri/src/bin/codex_monitor_daemon/rpc/git.rs @@ -50,7 +50,10 @@ pub(super) async fn try_handle( Ok(value) => value, Err(err) => return Some(Err(err)), }; - let depth = parse_optional_u32(params, "depth").map(|value| value as usize); + let depth = match parse_optional_u32(params, "depth") { + Ok(value) => value.map(|value| value as usize), + Err(err) => return Some(Err(err)), + }; let roots = match state.list_git_roots(workspace_id, depth).await { Ok(value) => value, Err(err) => return Some(Err(err)), @@ -73,7 +76,10 @@ pub(super) async fn try_handle( Ok(value) => value, Err(err) => return Some(Err(err)), }; - let limit = parse_optional_u32(params, "limit").map(|value| value as usize); + let limit = match parse_optional_u32(params, "limit") { + Ok(value) => value.map(|value| value as usize), + Err(err) => return Some(Err(err)), + }; let log = match state.get_git_log(workspace_id, limit).await { Ok(value) => value, Err(err) => return Some(Err(err)), diff --git a/src-tauri/src/bin/codex_monitor_daemon/rpc/workspace.rs b/src-tauri/src/bin/codex_monitor_daemon/rpc/workspace.rs index 66e8ea9d0..57e1c38e0 100644 --- a/src-tauri/src/bin/codex_monitor_daemon/rpc/workspace.rs +++ b/src-tauri/src/bin/codex_monitor_daemon/rpc/workspace.rs @@ -382,7 +382,10 @@ pub(super) async fn try_handle( }; let app = parse_optional_string(params, "app"); let command = parse_optional_string(params, "command"); - let args = parse_optional_string_array(params, "args").unwrap_or_default(); + let args = match parse_optional_string_array(params, "args") { + Ok(value) => value.unwrap_or_default(), + Err(err) => return Some(Err(err)), + }; Some( state .open_workspace_in(path, app, args, command) @@ -402,7 +405,10 @@ pub(super) async fn try_handle( Some(serde_json::to_value(icon).map_err(|err| err.to_string())) } "local_usage_snapshot" => { - let days = parse_optional_u32(params, "days"); + let days = match parse_optional_u32(params, "days") { + Ok(value) => value, + Err(err) => return Some(Err(err)), + }; let workspace_path = parse_optional_string(params, "workspacePath"); let snapshot = match state.local_usage_snapshot(days, workspace_path).await { Ok(value) => value, diff --git a/src-tauri/src/shared/workspaces_core/connect.rs b/src-tauri/src/shared/workspaces_core/connect.rs index e59c5d122..cff7c3f36 100644 --- a/src-tauri/src/shared/workspaces_core/connect.rs +++ b/src-tauri/src/shared/workspaces_core/connect.rs @@ -24,6 +24,13 @@ where F: Fn(WorkspaceEntry, Option, Option, Option) -> Fut, Fut: Future, String>>, { + { + let sessions = sessions.lock().await; + if sessions.contains_key(&workspace_id) { + return Ok(()); + } + } + let (entry, parent_entry) = resolve_entry_and_parent(workspaces, &workspace_id).await?; let (default_bin, codex_args) = { let settings = app_settings.lock().await; @@ -34,7 +41,15 @@ where }; let codex_home = resolve_workspace_codex_home(&entry, parent_entry.as_ref()); let session = spawn_session(entry.clone(), default_bin, codex_args, codex_home).await?; - sessions.lock().await.insert(entry.id, session); + { + let mut sessions = sessions.lock().await; + if sessions.contains_key(&entry.id) { + let mut child = session.child.lock().await; + kill_child_process_tree(&mut child).await; + return Ok(()); + } + sessions.insert(entry.id, session); + } Ok(()) } diff --git a/src/features/workspaces/hooks/useWorkspaceRestore.test.tsx b/src/features/workspaces/hooks/useWorkspaceRestore.test.tsx new file mode 100644 index 000000000..130a63d46 --- /dev/null +++ b/src/features/workspaces/hooks/useWorkspaceRestore.test.tsx @@ -0,0 +1,103 @@ +// @vitest-environment jsdom +import { act, renderHook } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { WorkspaceInfo } from "../../../types"; +import { useWorkspaceRestore } from "./useWorkspaceRestore"; + +const workspace: WorkspaceInfo = { + id: "ws-restore", + name: "Restore Workspace", + path: "/tmp/restore", + connected: false, + kind: "main", + parentId: null, + worktree: null, + settings: { sidebarCollapsed: false }, +}; + +const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); + +describe("useWorkspaceRestore", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("retries restore after a transient failure", async () => { + const connectWorkspace = vi.fn().mockResolvedValue(undefined); + const listThreadsForWorkspace = vi + .fn() + .mockRejectedValueOnce(new Error("temporary RPC error")) + .mockResolvedValueOnce(undefined); + + const { rerender } = renderHook( + (props: { + hasLoaded: boolean; + workspaces: WorkspaceInfo[]; + connectWorkspace: (workspace: WorkspaceInfo) => Promise; + listThreadsForWorkspace: ( + workspace: WorkspaceInfo, + options?: { preserveState?: boolean }, + ) => Promise; + }) => useWorkspaceRestore(props), + { + initialProps: { + hasLoaded: true, + workspaces: [workspace], + connectWorkspace, + listThreadsForWorkspace, + }, + }, + ); + + await act(async () => { + await flush(); + }); + + expect(connectWorkspace).toHaveBeenCalledTimes(1); + expect(listThreadsForWorkspace).toHaveBeenCalledTimes(1); + + rerender({ + hasLoaded: true, + workspaces: [workspace], + connectWorkspace, + listThreadsForWorkspace, + }); + + await act(async () => { + await flush(); + }); + + expect(connectWorkspace).toHaveBeenCalledTimes(2); + expect(listThreadsForWorkspace).toHaveBeenCalledTimes(2); + }); + + it("does not retry after a successful restore", async () => { + const connectWorkspace = vi.fn().mockResolvedValue(undefined); + const listThreadsForWorkspace = vi.fn().mockResolvedValue(undefined); + + const { rerender } = renderHook( + () => + useWorkspaceRestore({ + hasLoaded: true, + workspaces: [workspace], + connectWorkspace, + listThreadsForWorkspace, + }), + { + initialProps: undefined, + }, + ); + + await act(async () => { + await flush(); + }); + + rerender(); + await act(async () => { + await flush(); + }); + + expect(connectWorkspace).toHaveBeenCalledTimes(1); + expect(listThreadsForWorkspace).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/features/workspaces/hooks/useWorkspaceRestore.ts b/src/features/workspaces/hooks/useWorkspaceRestore.ts index ced09d167..7240ce1ec 100644 --- a/src/features/workspaces/hooks/useWorkspaceRestore.ts +++ b/src/features/workspaces/hooks/useWorkspaceRestore.ts @@ -27,13 +27,13 @@ export function useWorkspaceRestore({ if (restoredWorkspaces.current.has(workspace.id)) { return; } - restoredWorkspaces.current.add(workspace.id); void (async () => { try { if (!workspace.connected) { await connectWorkspace(workspace); } await listThreadsForWorkspace(workspace); + restoredWorkspaces.current.add(workspace.id); } catch { // Silent: connection errors show in debug panel. } diff --git a/src/services/tauri.test.ts b/src/services/tauri.test.ts index adbc53a34..3f60fd988 100644 --- a/src/services/tauri.test.ts +++ b/src/services/tauri.test.ts @@ -5,6 +5,7 @@ import * as notification from "@tauri-apps/plugin-notification"; import { exportMarkdownFile, addWorkspace, + connectWorkspace, compactThread, createGitHubRepo, fetchGit, @@ -190,7 +191,20 @@ describe("tauri invoke wrappers", () => { ); await expect(listWorkspaces()).resolves.toEqual([]); - expect(invokeMock).toHaveBeenCalledWith("list_workspaces"); + expect(invokeMock).toHaveBeenCalledWith("list_workspaces", undefined); + }); + + it("throws a clearer error for non-Tauri environments", async () => { + const invokeMock = vi.mocked(invoke); + invokeMock.mockRejectedValueOnce( + new TypeError("Cannot read properties of undefined (reading 'invoke')"), + ); + + await expect(connectWorkspace("ws-1")).rejects.toThrowError( + "Tauri invoke bridge is unavailable. Open this page in the Tauri app.", + ); + + expect(invokeMock).toHaveBeenCalledWith("connect_workspace", { id: "ws-1" }); }); it("applies default limit for git log", async () => { @@ -377,11 +391,14 @@ describe("tauri invoke wrappers", () => { await tailscaleDaemonStop(); await tailscaleDaemonStatus(); - expect(invokeMock).toHaveBeenCalledWith("tailscale_status"); - expect(invokeMock).toHaveBeenCalledWith("tailscale_daemon_command_preview"); - expect(invokeMock).toHaveBeenCalledWith("tailscale_daemon_start"); - expect(invokeMock).toHaveBeenCalledWith("tailscale_daemon_stop"); - expect(invokeMock).toHaveBeenCalledWith("tailscale_daemon_status"); + expect(invokeMock).toHaveBeenCalledWith("tailscale_status", undefined); + expect(invokeMock).toHaveBeenCalledWith( + "tailscale_daemon_command_preview", + undefined, + ); + expect(invokeMock).toHaveBeenCalledWith("tailscale_daemon_start", undefined); + expect(invokeMock).toHaveBeenCalledWith("tailscale_daemon_stop", undefined); + expect(invokeMock).toHaveBeenCalledWith("tailscale_daemon_status", undefined); }); it("reads agent.md for a workspace", async () => { @@ -476,7 +493,7 @@ describe("tauri invoke wrappers", () => { await getAgentsSettings(); - expect(invokeMock).toHaveBeenCalledWith("get_agents_settings"); + expect(invokeMock).toHaveBeenCalledWith("get_agents_settings", undefined); }); it("updates core agents settings", async () => { @@ -824,7 +841,7 @@ describe("tauri invoke wrappers", () => { await sendNotification("Dev", "Fallback"); - expect(invokeMock).toHaveBeenCalledWith("is_macos_debug_build"); + expect(invokeMock).toHaveBeenCalledWith("is_macos_debug_build", undefined); expect(invokeMock).toHaveBeenCalledWith("send_notification_fallback", { title: "Dev", body: "Fallback", diff --git a/src/services/tauri.ts b/src/services/tauri.ts index 728b0438b..caae242f7 100644 --- a/src/services/tauri.ts +++ b/src/services/tauri.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { invoke as invokeInTauri } from "@tauri-apps/api/core"; import { open, save } from "@tauri-apps/plugin-dialog"; import type { Options as NotificationOptions } from "@tauri-apps/plugin-notification"; import type { @@ -35,6 +35,39 @@ function isMissingTauriInvokeError(error: unknown) { ); } +const MISSING_TAURI_INVOKE_ERROR = + "Tauri invoke bridge is unavailable. Open this page in the Tauri app."; + +async function invokeCommand(command: string, params?: Record): Promise { + try { + return await invokeInTauri(command, params); + } catch (error) { + if (isMissingTauriInvokeError(error)) { + throw new Error(MISSING_TAURI_INVOKE_ERROR); + } + throw error; + } +} + +async function invokeCommandWithFallback( + command: string, + params: Record | undefined, + fallback: T, +): Promise { + try { + return await invokeCommand(command, params); + } catch (error) { + if (error instanceof Error && error.message === MISSING_TAURI_INVOKE_ERROR) { + console.warn( + "Tauri invoke bridge unavailable; returning a fallback value for", + command, + ); + return fallback; + } + throw error; + } +} + export async function pickWorkspacePath(): Promise { const selection = await open({ directory: true, multiple: false }); if (!selection || Array.isArray(selection)) { @@ -84,26 +117,16 @@ export async function exportMarkdownFile( if (!selection) { return null; } - await invoke("write_text_file", { path: selection, content }); + await invokeCommand("write_text_file", { path: selection, content }); return selection; } export async function listWorkspaces(): Promise { - try { - return await invoke("list_workspaces"); - } catch (error) { - if (isMissingTauriInvokeError(error)) { - // In non-Tauri environments (e.g., Electron/web previews), the invoke - // bridge may be missing. Treat this as "no workspaces" instead of crashing. - console.warn("Tauri invoke bridge unavailable; returning empty workspaces list."); - return []; - } - throw error; - } + return invokeCommandWithFallback("list_workspaces", undefined, []); } export async function getCodexConfigPath(): Promise { - return invoke("get_codex_config_path"); + return invokeCommand("get_codex_config_path"); } export type TextFileResponse = { @@ -167,7 +190,7 @@ async function fileRead( kind: FileKind, workspaceId?: string, ): Promise { - return invoke("file_read", { scope, kind, workspaceId }); + return invokeCommand("file_read", { scope, kind, workspaceId }); } async function fileWrite( @@ -176,7 +199,7 @@ async function fileWrite( content: string, workspaceId?: string, ): Promise { - return invoke("file_write", { scope, kind, workspaceId, content }); + return invokeCommand("file_write", { scope, kind, workspaceId, content }); } export async function readGlobalAgentsMd(): Promise { @@ -196,40 +219,40 @@ export async function writeGlobalCodexConfigToml(content: string): Promise } export async function getAgentsSettings(): Promise { - return invoke("get_agents_settings"); + return invokeCommand("get_agents_settings"); } export async function setAgentsCoreSettings( input: SetAgentsCoreInput, ): Promise { - return invoke("set_agents_core_settings", { input }); + return invokeCommand("set_agents_core_settings", { input }); } export async function createAgent(input: CreateAgentInput): Promise { - return invoke("create_agent", { input }); + return invokeCommand("create_agent", { input }); } export async function updateAgent(input: UpdateAgentInput): Promise { - return invoke("update_agent", { input }); + return invokeCommand("update_agent", { input }); } export async function deleteAgent(input: DeleteAgentInput): Promise { - return invoke("delete_agent", { input }); + return invokeCommand("delete_agent", { input }); } export async function readAgentConfigToml(agentName: string): Promise { - return invoke("read_agent_config_toml", { agentName }); + return invokeCommand("read_agent_config_toml", { agentName }); } export async function writeAgentConfigToml( agentName: string, content: string, ): Promise { - return invoke("write_agent_config_toml", { agentName, content }); + return invokeCommand("write_agent_config_toml", { agentName, content }); } export async function getConfigModel(workspaceId: string): Promise { - const response = await invoke<{ model?: string | null }>("get_config_model", { + const response = await invokeCommand<{ model?: string | null }>("get_config_model", { workspaceId, }); const model = response?.model; @@ -244,7 +267,7 @@ export async function addWorkspace( path: string, codex_bin: string | null, ): Promise { - return invoke("add_workspace", { path, codex_bin }); + return invokeCommand("add_workspace", { path, codex_bin }); } export async function addWorkspaceFromGitUrl( @@ -253,7 +276,7 @@ export async function addWorkspaceFromGitUrl( targetFolderName: string | null, codexBin: string | null, ): Promise { - return invoke("add_workspace_from_git_url", { + return invokeCommand("add_workspace_from_git_url", { url, destinationPath, targetFolderName, @@ -262,7 +285,7 @@ export async function addWorkspaceFromGitUrl( } export async function isWorkspacePathDir(path: string): Promise { - return invoke("is_workspace_path_dir", { path }); + return invokeCommand("is_workspace_path_dir", { path }); } export async function addClone( @@ -270,7 +293,7 @@ export async function addClone( copiesFolder: string, copyName: string, ): Promise { - return invoke("add_clone", { + return invokeCommand("add_clone", { sourceWorkspaceId, copiesFolder, copyName, @@ -283,7 +306,7 @@ export async function addWorktree( name: string | null, copyAgentsMd = true, ): Promise { - return invoke("add_worktree", { parentId, branch, name, copyAgentsMd }); + return invokeCommand("add_worktree", { parentId, branch, name, copyAgentsMd }); } export type WorktreeSetupStatus = { @@ -294,40 +317,40 @@ export type WorktreeSetupStatus = { export async function getWorktreeSetupStatus( workspaceId: string, ): Promise { - return invoke("worktree_setup_status", { workspaceId }); + return invokeCommand("worktree_setup_status", { workspaceId }); } export async function markWorktreeSetupRan(workspaceId: string): Promise { - return invoke("worktree_setup_mark_ran", { workspaceId }); + return invokeCommand("worktree_setup_mark_ran", { workspaceId }); } export async function updateWorkspaceSettings( id: string, settings: WorkspaceSettings, ): Promise { - return invoke("update_workspace_settings", { id, settings }); + return invokeCommand("update_workspace_settings", { id, settings }); } export async function updateWorkspaceCodexBin( id: string, codex_bin: string | null, ): Promise { - return invoke("update_workspace_codex_bin", { id, codex_bin }); + return invokeCommand("update_workspace_codex_bin", { id, codex_bin }); } export async function removeWorkspace(id: string): Promise { - return invoke("remove_workspace", { id }); + return invokeCommand("remove_workspace", { id }); } export async function removeWorktree(id: string): Promise { - return invoke("remove_worktree", { id }); + return invokeCommand("remove_worktree", { id }); } export async function renameWorktree( id: string, branch: string, ): Promise { - return invoke("rename_worktree", { id, branch }); + return invokeCommand("rename_worktree", { id, branch }); } export async function renameWorktreeUpstream( @@ -335,11 +358,11 @@ export async function renameWorktreeUpstream( oldBranch: string, newBranch: string, ): Promise { - return invoke("rename_worktree_upstream", { id, oldBranch, newBranch }); + return invokeCommand("rename_worktree_upstream", { id, oldBranch, newBranch }); } export async function applyWorktreeChanges(workspaceId: string): Promise { - return invoke("apply_worktree_changes", { workspaceId }); + return invokeCommand("apply_worktree_changes", { workspaceId }); } export async function openWorkspaceIn( @@ -350,7 +373,7 @@ export async function openWorkspaceIn( args?: string[]; }, ): Promise { - return invoke("open_workspace_in", { + return invokeCommand("open_workspace_in", { path, app: options.appName ?? null, command: options.command ?? null, @@ -359,33 +382,33 @@ export async function openWorkspaceIn( } export async function getOpenAppIcon(appName: string): Promise { - return invoke("get_open_app_icon", { appName }); + return invokeCommand("get_open_app_icon", { appName }); } export async function connectWorkspace(id: string): Promise { - return invoke("connect_workspace", { id }); + return invokeCommand("connect_workspace", { id }); } export async function setWorkspaceRuntimeCodexArgs( workspaceId: string, codexArgs: string | null, ): Promise<{ appliedCodexArgs: string | null; respawned: boolean }> { - return invoke("set_workspace_runtime_codex_args", { + return invokeCommand("set_workspace_runtime_codex_args", { workspaceId, codexArgs, }); } export async function startThread(workspaceId: string) { - return invoke("start_thread", { workspaceId }); + return invokeCommand("start_thread", { workspaceId }); } export async function forkThread(workspaceId: string, threadId: string) { - return invoke("fork_thread", { workspaceId, threadId }); + return invokeCommand("fork_thread", { workspaceId, threadId }); } export async function compactThread(workspaceId: string, threadId: string) { - return invoke("compact_thread", { workspaceId, threadId }); + return invokeCommand("compact_thread", { workspaceId, threadId }); } export async function sendUserMessage( @@ -416,7 +439,7 @@ export async function sendUserMessage( if (options?.appMentions && options.appMentions.length > 0) { payload.appMentions = options.appMentions; } - return invoke("send_user_message", payload); + return invokeCommand("send_user_message", payload); } export async function interruptTurn( @@ -424,7 +447,7 @@ export async function interruptTurn( threadId: string, turnId: string, ) { - return invoke("turn_interrupt", { workspaceId, threadId, turnId }); + return invokeCommand("turn_interrupt", { workspaceId, threadId, turnId }); } export async function steerTurn( @@ -445,7 +468,7 @@ export async function steerTurn( if (appMentions && appMentions.length > 0) { payload.appMentions = appMentions; } - return invoke("turn_steer", payload); + return invokeCommand("turn_steer", payload); } export async function startReview( @@ -458,7 +481,7 @@ export async function startReview( if (delivery) { payload.delivery = delivery; } - return invoke("start_review", payload); + return invokeCommand("start_review", payload); } export async function respondToServerRequest( @@ -466,7 +489,7 @@ export async function respondToServerRequest( requestId: number | string, decision: "accept" | "decline", ) { - return invoke("respond_to_server_request", { + return invokeCommand("respond_to_server_request", { workspaceId, requestId, result: { decision }, @@ -478,7 +501,7 @@ export async function respondToUserInputRequest( requestId: number | string, answers: Record, ) { - return invoke("respond_to_server_request", { + return invokeCommand("respond_to_server_request", { workspaceId, requestId, result: { answers }, @@ -489,7 +512,7 @@ export async function rememberApprovalRule( workspaceId: string, command: string[], ) { - return invoke("remember_approval_rule", { workspaceId, command }); + return invokeCommand("remember_approval_rule", { workspaceId, command }); } export async function getGitStatus(workspace_id: string): Promise<{ @@ -500,7 +523,7 @@ export async function getGitStatus(workspace_id: string): Promise<{ totalAdditions: number; totalDeletions: number; }> { - return invoke("get_git_status", { workspaceId: workspace_id }); + return invokeCommand("get_git_status", { workspaceId: workspace_id }); } export type InitGitRepoResponse = @@ -513,7 +536,7 @@ export async function initGitRepo( branch: string, force = false, ): Promise { - return invoke("init_git_repo", { workspaceId, branch, force }); + return invokeCommand("init_git_repo", { workspaceId, branch, force }); } export type CreateGitHubRepoResponse = @@ -532,7 +555,7 @@ export async function createGitHubRepo( visibility: "private" | "public", branch?: string | null, ): Promise { - return invoke("create_github_repo", { + return invokeCommand("create_github_repo", { workspaceId, repo, visibility, @@ -544,93 +567,93 @@ export async function listGitRoots( workspace_id: string, depth: number, ): Promise { - return invoke("list_git_roots", { workspaceId: workspace_id, depth }); + return invokeCommand("list_git_roots", { workspaceId: workspace_id, depth }); } export async function getGitDiffs( workspace_id: string, ): Promise { - return invoke("get_git_diffs", { workspaceId: workspace_id }); + return invokeCommand("get_git_diffs", { workspaceId: workspace_id }); } export async function getGitLog( workspace_id: string, limit = 40, ): Promise { - return invoke("get_git_log", { workspaceId: workspace_id, limit }); + return invokeCommand("get_git_log", { workspaceId: workspace_id, limit }); } export async function getGitCommitDiff( workspace_id: string, sha: string, ): Promise { - return invoke("get_git_commit_diff", { workspaceId: workspace_id, sha }); + return invokeCommand("get_git_commit_diff", { workspaceId: workspace_id, sha }); } export async function getGitRemote(workspace_id: string): Promise { - return invoke("get_git_remote", { workspaceId: workspace_id }); + return invokeCommand("get_git_remote", { workspaceId: workspace_id }); } export async function stageGitFile(workspaceId: string, path: string) { - return invoke("stage_git_file", { workspaceId, path }); + return invokeCommand("stage_git_file", { workspaceId, path }); } export async function stageGitAll(workspaceId: string): Promise { - return invoke("stage_git_all", { workspaceId }); + return invokeCommand("stage_git_all", { workspaceId }); } export async function unstageGitFile(workspaceId: string, path: string) { - return invoke("unstage_git_file", { workspaceId, path }); + return invokeCommand("unstage_git_file", { workspaceId, path }); } export async function revertGitFile(workspaceId: string, path: string) { - return invoke("revert_git_file", { workspaceId, path }); + return invokeCommand("revert_git_file", { workspaceId, path }); } export async function revertGitAll(workspaceId: string) { - return invoke("revert_git_all", { workspaceId }); + return invokeCommand("revert_git_all", { workspaceId }); } export async function commitGit( workspaceId: string, message: string, ): Promise { - return invoke("commit_git", { workspaceId, message }); + return invokeCommand("commit_git", { workspaceId, message }); } export async function pushGit(workspaceId: string): Promise { - return invoke("push_git", { workspaceId }); + return invokeCommand("push_git", { workspaceId }); } export async function pullGit(workspaceId: string): Promise { - return invoke("pull_git", { workspaceId }); + return invokeCommand("pull_git", { workspaceId }); } export async function fetchGit(workspaceId: string): Promise { - return invoke("fetch_git", { workspaceId }); + return invokeCommand("fetch_git", { workspaceId }); } export async function syncGit(workspaceId: string): Promise { - return invoke("sync_git", { workspaceId }); + return invokeCommand("sync_git", { workspaceId }); } export async function getGitHubIssues( workspace_id: string, ): Promise { - return invoke("get_github_issues", { workspaceId: workspace_id }); + return invokeCommand("get_github_issues", { workspaceId: workspace_id }); } export async function getGitHubPullRequests( workspace_id: string, ): Promise { - return invoke("get_github_pull_requests", { workspaceId: workspace_id }); + return invokeCommand("get_github_pull_requests", { workspaceId: workspace_id }); } export async function getGitHubPullRequestDiff( workspace_id: string, prNumber: number, ): Promise { - return invoke("get_github_pull_request_diff", { + return invokeCommand("get_github_pull_request_diff", { workspaceId: workspace_id, prNumber, }); @@ -640,7 +663,7 @@ export async function getGitHubPullRequestComments( workspace_id: string, prNumber: number, ): Promise { - return invoke("get_github_pull_request_comments", { + return invokeCommand("get_github_pull_request_comments", { workspaceId: workspace_id, prNumber, }); @@ -650,7 +673,7 @@ export async function checkoutGitHubPullRequest( workspace_id: string, prNumber: number, ): Promise { - return invoke("checkout_github_pull_request", { + return invokeCommand("checkout_github_pull_request", { workspaceId: workspace_id, prNumber, }); @@ -664,11 +687,11 @@ export async function localUsageSnapshot( if (workspacePath) { payload.workspacePath = workspacePath; } - return invoke("local_usage_snapshot", payload); + return invokeCommand("local_usage_snapshot", payload); } export async function getModelList(workspaceId: string) { - return invoke("model_list", { workspaceId }); + return invokeCommand("model_list", { workspaceId }); } export async function getExperimentalFeatureList( @@ -676,50 +699,50 @@ export async function getExperimentalFeatureList( cursor?: string | null, limit?: number | null, ) { - return invoke("experimental_feature_list", { workspaceId, cursor, limit }); + return invokeCommand("experimental_feature_list", { workspaceId, cursor, limit }); } export async function setCodexFeatureFlag( featureKey: string, enabled: boolean, ): Promise { - return invoke("set_codex_feature_flag", { featureKey, enabled }); + return invokeCommand("set_codex_feature_flag", { featureKey, enabled }); } export async function generateRunMetadata(workspaceId: string, prompt: string) { - return invoke<{ title: string; worktreeName: string }>("generate_run_metadata", { + return invokeCommand<{ title: string; worktreeName: string }>("generate_run_metadata", { workspaceId, prompt, }); } export async function getCollaborationModes(workspaceId: string) { - return invoke("collaboration_mode_list", { workspaceId }); + return invokeCommand("collaboration_mode_list", { workspaceId }); } export async function getAccountRateLimits(workspaceId: string) { - return invoke("account_rate_limits", { workspaceId }); + return invokeCommand("account_rate_limits", { workspaceId }); } export async function getAccountInfo(workspaceId: string) { - return invoke("account_read", { workspaceId }); + return invokeCommand("account_read", { workspaceId }); } export async function runCodexLogin(workspaceId: string) { - return invoke<{ loginId: string; authUrl: string; raw?: unknown }>("codex_login", { + return invokeCommand<{ loginId: string; authUrl: string; raw?: unknown }>("codex_login", { workspaceId, }); } export async function cancelCodexLogin(workspaceId: string) { - return invoke<{ canceled: boolean; status?: string; raw?: unknown }>( + return invokeCommand<{ canceled: boolean; status?: string; raw?: unknown }>( "codex_login_cancel", { workspaceId }, ); } export async function getSkillsList(workspaceId: string) { - return invoke("skills_list", { workspaceId }); + return invokeCommand("skills_list", { workspaceId }); } export async function getAppsList( @@ -728,19 +751,19 @@ export async function getAppsList( limit?: number | null, threadId?: string | null, ) { - return invoke("apps_list", { workspaceId, cursor, limit, threadId }); + return invokeCommand("apps_list", { workspaceId, cursor, limit, threadId }); } export async function getPromptsList(workspaceId: string) { - return invoke("prompts_list", { workspaceId }); + return invokeCommand("prompts_list", { workspaceId }); } export async function getWorkspacePromptsDir(workspaceId: string) { - return invoke("prompts_workspace_dir", { workspaceId }); + return invokeCommand("prompts_workspace_dir", { workspaceId }); } export async function getGlobalPromptsDir(workspaceId: string) { - return invoke("prompts_global_dir", { workspaceId }); + return invokeCommand("prompts_global_dir", { workspaceId }); } export async function createPrompt( @@ -753,7 +776,7 @@ export async function createPrompt( content: string; }, ) { - return invoke("prompts_create", { + return invokeCommand("prompts_create", { workspaceId, scope: data.scope, name: data.name, @@ -773,7 +796,7 @@ export async function updatePrompt( content: string; }, ) { - return invoke("prompts_update", { + return invokeCommand("prompts_update", { workspaceId, path: data.path, name: data.name, @@ -784,14 +807,14 @@ export async function updatePrompt( } export async function deletePrompt(workspaceId: string, path: string) { - return invoke("prompts_delete", { workspaceId, path }); + return invokeCommand("prompts_delete", { workspaceId, path }); } export async function movePrompt( workspaceId: string, data: { path: string; scope: "workspace" | "global" }, ) { - return invoke("prompts_move", { + return invokeCommand("prompts_move", { workspaceId, path: data.path, scope: data.scope, @@ -799,35 +822,35 @@ export async function movePrompt( } export async function getAppSettings(): Promise { - return invoke("get_app_settings"); + return invokeCommand("get_app_settings"); } export async function isMobileRuntime(): Promise { - return invoke("is_mobile_runtime"); + return invokeCommand("is_mobile_runtime"); } export async function updateAppSettings(settings: AppSettings): Promise { - return invoke("update_app_settings", { settings }); + return invokeCommand("update_app_settings", { settings }); } export async function tailscaleStatus(): Promise { - return invoke("tailscale_status"); + return invokeCommand("tailscale_status"); } export async function tailscaleDaemonCommandPreview(): Promise { - return invoke("tailscale_daemon_command_preview"); + return invokeCommand("tailscale_daemon_command_preview"); } export async function tailscaleDaemonStart(): Promise { - return invoke("tailscale_daemon_start"); + return invokeCommand("tailscale_daemon_start"); } export async function tailscaleDaemonStop(): Promise { - return invoke("tailscale_daemon_stop"); + return invokeCommand("tailscale_daemon_stop"); } export async function tailscaleDaemonStatus(): Promise { - return invoke("tailscale_daemon_status"); + return invokeCommand("tailscale_daemon_status"); } type MenuAcceleratorUpdate = { @@ -838,32 +861,32 @@ type MenuAcceleratorUpdate = { export async function setMenuAccelerators( updates: MenuAcceleratorUpdate[], ): Promise { - return invoke("menu_set_accelerators", { updates }); + return invokeCommand("menu_set_accelerators", { updates }); } export async function runCodexDoctor( codexBin: string | null, codexArgs: string | null, ): Promise { - return invoke("codex_doctor", { codexBin, codexArgs }); + return invokeCommand("codex_doctor", { codexBin, codexArgs }); } export async function runCodexUpdate( codexBin: string | null, codexArgs: string | null, ): Promise { - return invoke("codex_update", { codexBin, codexArgs }); + return invokeCommand("codex_update", { codexBin, codexArgs }); } export async function getWorkspaceFiles(workspaceId: string) { - return invoke("list_workspace_files", { workspaceId }); + return invokeCommand("list_workspace_files", { workspaceId }); } export async function readWorkspaceFile( workspaceId: string, path: string, ): Promise<{ content: string; truncated: boolean }> { - return invoke<{ content: string; truncated: boolean }>("read_workspace_file", { + return invokeCommand<{ content: string; truncated: boolean }>("read_workspace_file", { workspaceId, path, }); @@ -878,15 +901,15 @@ export async function writeAgentMd(workspaceId: string, content: string): Promis } export async function listGitBranches(workspaceId: string) { - return invoke("list_git_branches", { workspaceId }); + return invokeCommand("list_git_branches", { workspaceId }); } export async function checkoutGitBranch(workspaceId: string, name: string) { - return invoke("checkout_git_branch", { workspaceId, name }); + return invokeCommand("checkout_git_branch", { workspaceId, name }); } export async function createGitBranch(workspaceId: string, name: string) { - return invoke("create_git_branch", { workspaceId, name }); + return invokeCommand("create_git_branch", { workspaceId, name }); } function withModelId(modelId?: string | null) { @@ -896,7 +919,7 @@ function withModelId(modelId?: string | null) { export async function getDictationModelStatus( modelId?: string | null, ): Promise { - return invoke( + return invokeCommand( "dictation_model_status", withModelId(modelId), ); @@ -905,7 +928,7 @@ export async function getDictationModelStatus( export async function downloadDictationModel( modelId?: string | null, ): Promise { - return invoke( + return invokeCommand( "dictation_download_model", withModelId(modelId), ); @@ -914,7 +937,7 @@ export async function downloadDictationModel( export async function cancelDictationDownload( modelId?: string | null, ): Promise { - return invoke( + return invokeCommand( "dictation_cancel_download", withModelId(modelId), ); @@ -923,7 +946,7 @@ export async function cancelDictationDownload( export async function removeDictationModel( modelId?: string | null, ): Promise { - return invoke( + return invokeCommand( "dictation_remove_model", withModelId(modelId), ); @@ -932,19 +955,19 @@ export async function removeDictationModel( export async function startDictation( preferredLanguage: string | null, ): Promise { - return invoke("dictation_start", { preferredLanguage }); + return invokeCommand("dictation_start", { preferredLanguage }); } export async function requestDictationPermission(): Promise { - return invoke("dictation_request_permission"); + return invokeCommand("dictation_request_permission"); } export async function stopDictation(): Promise { - return invoke("dictation_stop"); + return invokeCommand("dictation_stop"); } export async function cancelDictation(): Promise { - return invoke("dictation_cancel"); + return invokeCommand("dictation_cancel"); } export async function openTerminalSession( @@ -953,7 +976,7 @@ export async function openTerminalSession( cols: number, rows: number, ): Promise<{ id: string }> { - return invoke("terminal_open", { workspaceId, terminalId, cols, rows }); + return invokeCommand("terminal_open", { workspaceId, terminalId, cols, rows }); } export async function writeTerminalSession( @@ -961,7 +984,7 @@ export async function writeTerminalSession( terminalId: string, data: string, ): Promise { - return invoke("terminal_write", { workspaceId, terminalId, data }); + return invokeCommand("terminal_write", { workspaceId, terminalId, data }); } export async function resizeTerminalSession( @@ -970,14 +993,14 @@ export async function resizeTerminalSession( cols: number, rows: number, ): Promise { - return invoke("terminal_resize", { workspaceId, terminalId, cols, rows }); + return invokeCommand("terminal_resize", { workspaceId, terminalId, cols, rows }); } export async function closeTerminalSession( workspaceId: string, terminalId: string, ): Promise { - return invoke("terminal_close", { workspaceId, terminalId }); + return invokeCommand("terminal_close", { workspaceId, terminalId }); } export async function listThreads( @@ -987,7 +1010,7 @@ export async function listThreads( sortKey?: "created_at" | "updated_at" | null, cwd?: string | null, ) { - return invoke("list_threads", { workspaceId, cursor, limit, sortKey, cwd }); + return invokeCommand("list_threads", { workspaceId, cursor, limit, sortKey, cwd }); } export async function listMcpServerStatus( @@ -995,23 +1018,23 @@ export async function listMcpServerStatus( cursor?: string | null, limit?: number | null, ) { - return invoke("list_mcp_server_status", { workspaceId, cursor, limit }); + return invokeCommand("list_mcp_server_status", { workspaceId, cursor, limit }); } export async function resumeThread(workspaceId: string, threadId: string) { - return invoke("resume_thread", { workspaceId, threadId }); + return invokeCommand("resume_thread", { workspaceId, threadId }); } export async function threadLiveSubscribe(workspaceId: string, threadId: string) { - return invoke("thread_live_subscribe", { workspaceId, threadId }); + return invokeCommand("thread_live_subscribe", { workspaceId, threadId }); } export async function threadLiveUnsubscribe(workspaceId: string, threadId: string) { - return invoke("thread_live_unsubscribe", { workspaceId, threadId }); + return invokeCommand("thread_live_unsubscribe", { workspaceId, threadId }); } export async function archiveThread(workspaceId: string, threadId: string) { - return invoke("archive_thread", { workspaceId, threadId }); + return invokeCommand("archive_thread", { workspaceId, threadId }); } export async function setThreadName( @@ -1019,14 +1042,14 @@ export async function setThreadName( threadId: string, name: string, ) { - return invoke("set_thread_name", { workspaceId, threadId, name }); + return invokeCommand("set_thread_name", { workspaceId, threadId, name }); } export async function generateCommitMessage( workspaceId: string, commitMessageModelId: string | null, ): Promise { - return invoke("generate_commit_message", { workspaceId, commitMessageModelId }); + return invokeCommand("generate_commit_message", { workspaceId, commitMessageModelId }); } export type GeneratedAgentConfiguration = { @@ -1038,7 +1061,7 @@ export async function generateAgentDescription( workspaceId: string, description: string, ): Promise { - return invoke("generate_agent_description", { workspaceId, description }); + return invokeCommand("generate_agent_description", { workspaceId, description }); } export async function sendNotification( @@ -1053,12 +1076,12 @@ export async function sendNotification( extra?: Record; }, ): Promise { - const macosDebugBuild = await invoke("is_macos_debug_build").catch( + const macosDebugBuild = await invokeCommand("is_macos_debug_build").catch( () => false, ); const attemptFallback = async () => { try { - await invoke("send_notification_fallback", { title, body }); + await invokeCommand("send_notification_fallback", { title, body }); return true; } catch (error) { console.warn("Notification fallback failed.", { error });