diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c541c2f3f..948d5e4e7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,6 +46,8 @@ pub fn run() { let check_updates_item = MenuItemBuilder::with_id("check_for_updates", "Check for Updates...") .build(handle)?; + let settings_item = + MenuItemBuilder::with_id("file_open_settings", "Settings...").build(handle)?; let app_menu = Submenu::with_items( handle, app_name.clone(), @@ -53,6 +55,7 @@ pub fn run() { &[ &about_item, &check_updates_item, + &settings_item, &PredefinedMenuItem::separator(handle)?, &PredefinedMenuItem::services(handle, None)?, &PredefinedMenuItem::separator(handle)?, @@ -63,6 +66,17 @@ pub fn run() { ], )?; + let new_agent_item = + MenuItemBuilder::with_id("file_new_agent", "New Agent").build(handle)?; + let new_worktree_agent_item = + MenuItemBuilder::with_id("file_new_worktree_agent", "New Worktree Agent") + .build(handle)?; + let new_clone_agent_item = + MenuItemBuilder::with_id("file_new_clone_agent", "New Clone Agent") + .build(handle)?; + let add_workspace_item = + MenuItemBuilder::with_id("file_add_workspace", "Add Workspace...").build(handle)?; + #[cfg(target_os = "linux")] let file_menu = { let close_window_item = @@ -72,7 +86,16 @@ pub fn run() { handle, "File", true, - &[&close_window_item, &quit_item], + &[ + &new_agent_item, + &new_worktree_agent_item, + &new_clone_agent_item, + &PredefinedMenuItem::separator(handle)?, + &add_workspace_item, + &PredefinedMenuItem::separator(handle)?, + &close_window_item, + &quit_item, + ], )? }; #[cfg(not(target_os = "linux"))] @@ -81,6 +104,12 @@ pub fn run() { "File", true, &[ + &new_agent_item, + &new_worktree_agent_item, + &new_clone_agent_item, + &PredefinedMenuItem::separator(handle)?, + &add_workspace_item, + &PredefinedMenuItem::separator(handle)?, &PredefinedMenuItem::close_window(handle, None)?, #[cfg(not(target_os = "macos"))] &PredefinedMenuItem::quit(handle, None)?, @@ -194,6 +223,21 @@ pub fn run() { "check_for_updates" => { let _ = app.emit("updater-check", ()); } + "file_new_agent" => { + let _ = app.emit("menu-new-agent", ()); + } + "file_new_worktree_agent" => { + let _ = app.emit("menu-new-worktree-agent", ()); + } + "file_new_clone_agent" => { + let _ = app.emit("menu-new-clone-agent", ()); + } + "file_add_workspace" => { + let _ = app.emit("menu-add-workspace", ()); + } + "file_open_settings" => { + let _ = app.emit("menu-open-settings", ()); + } "file_close_window" | "window_close" => { if let Some(window) = app.get_webview_window("main") { let _ = window.close(); diff --git a/src/App.tsx b/src/App.tsx index ef62966d7..e88ad40f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -94,7 +94,14 @@ import { playNotificationSound } from "./utils/notificationSounds"; import { pickWorkspacePath, } from "./services/tauri"; -import { subscribeUpdaterCheck } from "./services/events"; +import { + subscribeMenuAddWorkspace, + subscribeMenuNewAgent, + subscribeMenuNewCloneAgent, + subscribeMenuNewWorktreeAgent, + subscribeMenuOpenSettings, + subscribeUpdaterCheck, +} from "./services/events"; import type { AccessMode, GitHubPullRequest, @@ -1039,16 +1046,7 @@ function MainApp() { listThreadsForWorkspace }); - useNewAgentShortcut({ - isEnabled: Boolean(activeWorkspace), - onTrigger: () => { - if (activeWorkspace) { - void handleAddAgent(activeWorkspace); - } - }, - }); - - async function handleAddWorkspace() { + const handleAddWorkspace = useCallback(async () => { try { const workspace = await addWorkspace(); if (workspace) { @@ -1068,34 +1066,56 @@ function MainApp() { }); alert(`Failed to add workspace.\n\n${message}`); } - } + }, [addDebugEntry, addWorkspace, isCompact, setActiveTab, setActiveThreadId]); + const handleAddAgent = useCallback( + async (workspace: (typeof workspaces)[number]) => { + exitDiffView(); + selectWorkspace(workspace.id); + if (!workspace.connected) { + await connectWorkspace(workspace); + } + await startThreadForWorkspace(workspace.id); + if (isCompact) { + setActiveTab("codex"); + } + // Focus the composer input after creating the agent + setTimeout(() => composerInputRef.current?.focus(), 0); + }, + [ + connectWorkspace, + exitDiffView, + isCompact, + selectWorkspace, + setActiveTab, + startThreadForWorkspace, + ], + ); - async function handleAddAgent(workspace: (typeof workspaces)[number]) { - exitDiffView(); - selectWorkspace(workspace.id); - if (!workspace.connected) { - await connectWorkspace(workspace); - } - await startThreadForWorkspace(workspace.id); - if (isCompact) { - setActiveTab("codex"); - } - // Focus the composer input after creating the agent - setTimeout(() => composerInputRef.current?.focus(), 0); - } + const handleAddWorktreeAgent = useCallback( + async (workspace: (typeof workspaces)[number]) => { + exitDiffView(); + openWorktreePrompt(workspace); + }, + [exitDiffView, openWorktreePrompt], + ); - async function handleAddWorktreeAgent( - workspace: (typeof workspaces)[number] - ) { - exitDiffView(); - openWorktreePrompt(workspace); - } + const handleAddCloneAgent = useCallback( + async (workspace: (typeof workspaces)[number]) => { + exitDiffView(); + openClonePrompt(workspace); + }, + [exitDiffView, openClonePrompt], + ); - async function handleAddCloneAgent(workspace: (typeof workspaces)[number]) { - exitDiffView(); - openClonePrompt(workspace); - } + useNewAgentShortcut({ + isEnabled: Boolean(activeWorkspace), + onTrigger: () => { + if (activeWorkspace) { + void handleAddAgent(activeWorkspace); + } + }, + }); function handleSelectDiff(path: string) { setSelectedDiffPath(path); @@ -1187,6 +1207,87 @@ function MainApp() { [], ); + useEffect(() => { + let unlistenNewAgent: (() => void) | null = null; + let unlistenNewWorktree: (() => void) | null = null; + let unlistenNewClone: (() => void) | null = null; + let unlistenAddWorkspace: (() => void) | null = null; + let unlistenOpenSettings: (() => void) | null = null; + const baseWorkspace = activeParentWorkspace ?? activeWorkspace; + + subscribeMenuNewAgent(() => { + if (activeWorkspace) { + void handleAddAgent(activeWorkspace); + } + }) + .then((handler) => { + unlistenNewAgent = handler; + }) + .catch(() => {}); + + subscribeMenuNewWorktreeAgent(() => { + if (baseWorkspace) { + void handleAddWorktreeAgent(baseWorkspace); + } + }) + .then((handler) => { + unlistenNewWorktree = handler; + }) + .catch(() => {}); + + subscribeMenuNewCloneAgent(() => { + if (baseWorkspace) { + void handleAddCloneAgent(baseWorkspace); + } + }) + .then((handler) => { + unlistenNewClone = handler; + }) + .catch(() => {}); + + subscribeMenuAddWorkspace(() => { + void handleAddWorkspace(); + }) + .then((handler) => { + unlistenAddWorkspace = handler; + }) + .catch(() => {}); + + subscribeMenuOpenSettings(() => { + handleOpenSettings(); + }) + .then((handler) => { + unlistenOpenSettings = handler; + }) + .catch(() => {}); + + return () => { + if (unlistenNewAgent) { + unlistenNewAgent(); + } + if (unlistenNewWorktree) { + unlistenNewWorktree(); + } + if (unlistenNewClone) { + unlistenNewClone(); + } + if (unlistenAddWorkspace) { + unlistenAddWorkspace(); + } + if (unlistenOpenSettings) { + unlistenOpenSettings(); + } + }; + }, [ + activeParentWorkspace, + activeWorkspace, + handleAddAgent, + handleAddCloneAgent, + handleAddWorkspace, + handleAddWorktreeAgent, + handleOpenSettings, + ]); + const orderValue = (entry: WorkspaceInfo) => typeof entry.settings.sortOrder === "number" ? entry.settings.sortOrder diff --git a/src/services/events.ts b/src/services/events.ts index 98a593bfa..2d6679ba6 100644 --- a/src/services/events.ts +++ b/src/services/events.ts @@ -48,3 +48,43 @@ export async function subscribeUpdaterCheck( onEvent(); }); } + +export async function subscribeMenuNewAgent( + onEvent: () => void, +): Promise { + return listen("menu-new-agent", () => { + onEvent(); + }); +} + +export async function subscribeMenuNewWorktreeAgent( + onEvent: () => void, +): Promise { + return listen("menu-new-worktree-agent", () => { + onEvent(); + }); +} + +export async function subscribeMenuNewCloneAgent( + onEvent: () => void, +): Promise { + return listen("menu-new-clone-agent", () => { + onEvent(); + }); +} + +export async function subscribeMenuAddWorkspace( + onEvent: () => void, +): Promise { + return listen("menu-add-workspace", () => { + onEvent(); + }); +} + +export async function subscribeMenuOpenSettings( + onEvent: () => void, +): Promise { + return listen("menu-open-settings", () => { + onEvent(); + }); +}