From 845c4de31e6e4b85e50583ae4c2bdcd1d8756292 Mon Sep 17 00:00:00 2001 From: Ivan Borinschi Date: Wed, 4 Feb 2026 10:26:01 +0200 Subject: [PATCH 1/4] Add show in Finder for worktree menu --- src/features/app/components/WorktreeCard.tsx | 4 +- .../app/components/WorktreeSection.tsx | 2 +- .../app/hooks/useSidebarMenus.test.tsx | 101 ++++++++++++++++++ src/features/app/hooks/useSidebarMenus.ts | 34 +++++- 4 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 src/features/app/hooks/useSidebarMenus.test.tsx diff --git a/src/features/app/components/WorktreeCard.tsx b/src/features/app/components/WorktreeCard.tsx index 853a3fbb..5406ccb8 100644 --- a/src/features/app/components/WorktreeCard.tsx +++ b/src/features/app/components/WorktreeCard.tsx @@ -7,7 +7,7 @@ type WorktreeCardProps = { isActive: boolean; isDeleting?: boolean; onSelectWorkspace: (id: string) => void; - onShowWorktreeMenu: (event: MouseEvent, workspaceId: string) => void; + onShowWorktreeMenu: (event: MouseEvent, worktree: WorkspaceInfo) => void; onToggleWorkspaceCollapse: (workspaceId: string, collapsed: boolean) => void; onConnectWorkspace: (workspace: WorkspaceInfo) => void; children?: React.ReactNode; @@ -41,7 +41,7 @@ export function WorktreeCard({ }} onContextMenu={(event) => { if (!isDeleting) { - onShowWorktreeMenu(event, worktree.id); + onShowWorktreeMenu(event, worktree); } }} onKeyDown={(event) => { diff --git a/src/features/app/components/WorktreeSection.tsx b/src/features/app/components/WorktreeSection.tsx index aea8655a..51695e6f 100644 --- a/src/features/app/components/WorktreeSection.tsx +++ b/src/features/app/components/WorktreeSection.tsx @@ -48,7 +48,7 @@ type WorktreeSectionProps = { threadId: string, canPin: boolean, ) => void; - onShowWorktreeMenu: (event: MouseEvent, workspaceId: string) => void; + onShowWorktreeMenu: (event: MouseEvent, worktree: WorkspaceInfo) => void; onToggleExpanded: (workspaceId: string) => void; onLoadOlderThreads: (workspaceId: string) => void; }; diff --git a/src/features/app/hooks/useSidebarMenus.test.tsx b/src/features/app/hooks/useSidebarMenus.test.tsx new file mode 100644 index 00000000..01189fa1 --- /dev/null +++ b/src/features/app/hooks/useSidebarMenus.test.tsx @@ -0,0 +1,101 @@ +/** @vitest-environment jsdom */ +import type { MouseEvent as ReactMouseEvent } from "react"; +import { renderHook } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import type { WorkspaceInfo } from "../../../types"; +import { useSidebarMenus } from "./useSidebarMenus"; + +const menuNew = vi.hoisted(() => + vi.fn(async ({ items }) => ({ popup: vi.fn(), items })), +); +const menuItemNew = vi.hoisted(() => vi.fn(async (options) => options)); + +vi.mock("@tauri-apps/api/menu", () => ({ + Menu: { new: menuNew }, + MenuItem: { new: menuItemNew }, +})); + +vi.mock("@tauri-apps/api/window", () => ({ + getCurrentWindow: () => ({ scaleFactor: () => 1 }), +})); + +vi.mock("@tauri-apps/api/dpi", () => ({ + LogicalPosition: class LogicalPosition { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + }, +})); + +const revealItemInDir = vi.hoisted(() => vi.fn()); + +vi.mock("@tauri-apps/plugin-opener", () => ({ + revealItemInDir: (...args: unknown[]) => revealItemInDir(...args), +})); + +vi.mock("../../../services/toasts", () => ({ + pushErrorToast: vi.fn(), +})); + +describe("useSidebarMenus", () => { + it("adds a show in finder option for worktrees", async () => { + const onDeleteThread = vi.fn(); + const onSyncThread = vi.fn(); + const onPinThread = vi.fn(); + const onUnpinThread = vi.fn(); + const isThreadPinned = vi.fn(() => false); + const onRenameThread = vi.fn(); + const onReloadWorkspaceThreads = vi.fn(); + const onDeleteWorkspace = vi.fn(); + const onDeleteWorktree = vi.fn(); + + const { result } = renderHook(() => + useSidebarMenus({ + onDeleteThread, + onSyncThread, + onPinThread, + onUnpinThread, + isThreadPinned, + onRenameThread, + onReloadWorkspaceThreads, + onDeleteWorkspace, + onDeleteWorktree, + }), + ); + + const worktree: WorkspaceInfo = { + id: "worktree-1", + name: "feature/test", + path: "/tmp/worktree-1", + kind: "worktree", + connected: true, + settings: { + sidebarCollapsed: false, + worktreeSetupScript: "", + }, + worktree: { branch: "feature/test" }, + }; + + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + clientX: 12, + clientY: 34, + } as unknown as ReactMouseEvent; + + await result.current.showWorktreeMenu(event, worktree); + + const menuArgs = menuNew.mock.calls[0]?.[0]; + const revealItem = menuArgs.items.find( + (item: { text: string }) => item.text === "Show in Finder", + ); + + expect(revealItem).toBeDefined(); + await revealItem.action(); + expect(revealItemInDir).toHaveBeenCalledWith("/tmp/worktree-1"); + }); +}); diff --git a/src/features/app/hooks/useSidebarMenus.ts b/src/features/app/hooks/useSidebarMenus.ts index 97e0bc37..2e391ebb 100644 --- a/src/features/app/hooks/useSidebarMenus.ts +++ b/src/features/app/hooks/useSidebarMenus.ts @@ -2,6 +2,10 @@ import { useCallback, type MouseEvent } from "react"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { LogicalPosition } from "@tauri-apps/api/dpi"; import { getCurrentWindow } from "@tauri-apps/api/window"; +import { revealItemInDir } from "@tauri-apps/plugin-opener"; + +import type { WorkspaceInfo } from "../../../types"; +import { pushErrorToast } from "../../../services/toasts"; type SidebarMenuHandlers = { onDeleteThread: (workspaceId: string, threadId: string) => void; @@ -110,18 +114,40 @@ export function useSidebarMenus({ ); const showWorktreeMenu = useCallback( - async (event: MouseEvent, workspaceId: string) => { + async (event: MouseEvent, worktree: WorkspaceInfo) => { event.preventDefault(); event.stopPropagation(); const reloadItem = await MenuItem.new({ text: "Reload threads", - action: () => onReloadWorkspaceThreads(workspaceId), + action: () => onReloadWorkspaceThreads(worktree.id), + }); + const revealItem = await MenuItem.new({ + text: "Show in Finder", + action: async () => { + if (!worktree.path) { + return; + } + try { + await revealItemInDir(worktree.path); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + pushErrorToast({ + title: "Couldn't show worktree in Finder", + message, + }); + console.warn("Failed to reveal worktree", { + message, + workspaceId: worktree.id, + path: worktree.path, + }); + } + }, }); const deleteItem = await MenuItem.new({ text: "Delete worktree", - action: () => onDeleteWorktree(workspaceId), + action: () => onDeleteWorktree(worktree.id), }); - const menu = await Menu.new({ items: [reloadItem, deleteItem] }); + const menu = await Menu.new({ items: [reloadItem, revealItem, deleteItem] }); const window = getCurrentWindow(); const position = new LogicalPosition(event.clientX, event.clientY); await menu.popup(position, window); From 63cac9377c6048db82aef3ed41d44ddd0e57e4e8 Mon Sep 17 00:00:00 2001 From: Ivan Borinschi Date: Wed, 4 Feb 2026 10:39:19 +0200 Subject: [PATCH 2/4] Add toggle for worktree Finder menu --- src-tauri/src/types.rs | 11 ++++ src/App.tsx | 1 + src/features/app/components/Sidebar.test.tsx | 1 + src/features/app/components/Sidebar.tsx | 3 + .../app/hooks/useSidebarMenus.test.tsx | 47 ++++++++++++++++ src/features/app/hooks/useSidebarMenus.ts | 56 +++++++++++-------- src/features/layout/hooks/useLayoutNodes.tsx | 2 + .../settings/components/SettingsView.test.tsx | 1 + .../settings/components/SettingsView.tsx | 24 ++++++++ src/features/settings/hooks/useAppSettings.ts | 1 + src/types.ts | 1 + 11 files changed, 124 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 791450ce..632b3fd2 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -431,6 +431,11 @@ pub(crate) struct AppSettings { rename = "systemNotificationsEnabled" )] pub(crate) system_notifications_enabled: bool, + #[serde( + default = "default_worktree_show_in_finder_enabled", + rename = "worktreeShowInFinderEnabled" + )] + pub(crate) worktree_show_in_finder_enabled: bool, #[serde( default = "default_experimental_collab_enabled", rename = "experimentalCollabEnabled" @@ -541,6 +546,10 @@ fn default_usage_show_remaining() -> bool { false } +fn default_worktree_show_in_finder_enabled() -> bool { + false +} + fn default_ui_font_family() -> String { "\"SF Pro Text\", \"SF Pro Display\", -apple-system, \"Helvetica Neue\", sans-serif" .to_string() @@ -813,6 +822,7 @@ impl Default for AppSettings { code_font_size: default_code_font_size(), notification_sounds_enabled: true, system_notifications_enabled: true, + worktree_show_in_finder_enabled: default_worktree_show_in_finder_enabled(), preload_git_diffs: default_preload_git_diffs(), git_diff_ignore_whitespace_changes: default_git_diff_ignore_whitespace_changes(), experimental_collab_enabled: false, @@ -906,6 +916,7 @@ mod tests { settings.cycle_workspace_prev_shortcut.as_deref(), Some("cmd+shift+up") ); + assert!(!settings.worktree_show_in_finder_enabled); assert!(settings.last_composer_model_id.is_none()); assert!(settings.last_composer_reasoning_effort.is_none()); assert!((settings.ui_scale - 1.0).abs() < f64::EPSILON); diff --git a/src/App.tsx b/src/App.tsx index 8f3cb33b..bc330ab4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1742,6 +1742,7 @@ function MainApp() { activeRateLimits, usageShowRemaining: appSettings.usageShowRemaining, accountInfo: activeAccount, + showWorktreeInFinder: appSettings.worktreeShowInFinderEnabled, onSwitchAccount: handleSwitchAccount, onCancelSwitchAccount: handleCancelSwitchAccount, accountSwitching, diff --git a/src/features/app/components/Sidebar.test.tsx b/src/features/app/components/Sidebar.test.tsx index d4904c75..d8fee315 100644 --- a/src/features/app/components/Sidebar.test.tsx +++ b/src/features/app/components/Sidebar.test.tsx @@ -21,6 +21,7 @@ const baseProps = { accountRateLimits: null, usageShowRemaining: false, accountInfo: null, + showWorktreeInFinder: false, onSwitchAccount: vi.fn(), onCancelSwitchAccount: vi.fn(), accountSwitching: false, diff --git a/src/features/app/components/Sidebar.tsx b/src/features/app/components/Sidebar.tsx index be17ed7e..002159c9 100644 --- a/src/features/app/components/Sidebar.tsx +++ b/src/features/app/components/Sidebar.tsx @@ -57,6 +57,7 @@ type SidebarProps = { accountRateLimits: RateLimitSnapshot | null; usageShowRemaining: boolean; accountInfo: AccountSnapshot | null; + showWorktreeInFinder: boolean; onSwitchAccount: () => void; onCancelSwitchAccount: () => void; accountSwitching: boolean; @@ -110,6 +111,7 @@ export function Sidebar({ accountRateLimits, usageShowRemaining, accountInfo, + showWorktreeInFinder, onSwitchAccount, onCancelSwitchAccount, accountSwitching, @@ -171,6 +173,7 @@ export function Sidebar({ onReloadWorkspaceThreads, onDeleteWorkspace, onDeleteWorktree, + showWorktreeInFinder, }); const { sessionPercent, diff --git a/src/features/app/hooks/useSidebarMenus.test.tsx b/src/features/app/hooks/useSidebarMenus.test.tsx index 01189fa1..ee45902e 100644 --- a/src/features/app/hooks/useSidebarMenus.test.tsx +++ b/src/features/app/hooks/useSidebarMenus.test.tsx @@ -64,6 +64,7 @@ describe("useSidebarMenus", () => { onReloadWorkspaceThreads, onDeleteWorkspace, onDeleteWorktree, + showWorktreeInFinder: true, }), ); @@ -98,4 +99,50 @@ describe("useSidebarMenus", () => { await revealItem.action(); expect(revealItemInDir).toHaveBeenCalledWith("/tmp/worktree-1"); }); + + it("omits show in finder when disabled", async () => { + const { result } = renderHook(() => + useSidebarMenus({ + onDeleteThread: vi.fn(), + onSyncThread: vi.fn(), + onPinThread: vi.fn(), + onUnpinThread: vi.fn(), + isThreadPinned: vi.fn(() => false), + onRenameThread: vi.fn(), + onReloadWorkspaceThreads: vi.fn(), + onDeleteWorkspace: vi.fn(), + onDeleteWorktree: vi.fn(), + showWorktreeInFinder: false, + }), + ); + + const worktree: WorkspaceInfo = { + id: "worktree-2", + name: "feature/nope", + path: "/tmp/worktree-2", + kind: "worktree", + connected: true, + settings: { + sidebarCollapsed: false, + worktreeSetupScript: "", + }, + worktree: { branch: "feature/nope" }, + }; + + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + clientX: 22, + clientY: 44, + } as unknown as ReactMouseEvent; + + await result.current.showWorktreeMenu(event, worktree); + + const menuArgs = menuNew.mock.calls[1]?.[0]; + const revealItem = menuArgs.items.find( + (item: { text: string }) => item.text === "Show in Finder", + ); + + expect(revealItem).toBeUndefined(); + }); }); diff --git a/src/features/app/hooks/useSidebarMenus.ts b/src/features/app/hooks/useSidebarMenus.ts index 2e391ebb..91d1f503 100644 --- a/src/features/app/hooks/useSidebarMenus.ts +++ b/src/features/app/hooks/useSidebarMenus.ts @@ -17,6 +17,7 @@ type SidebarMenuHandlers = { onReloadWorkspaceThreads: (workspaceId: string) => void; onDeleteWorkspace: (workspaceId: string) => void; onDeleteWorktree: (workspaceId: string) => void; + showWorktreeInFinder: boolean; }; export function useSidebarMenus({ @@ -29,6 +30,7 @@ export function useSidebarMenus({ onReloadWorkspaceThreads, onDeleteWorkspace, onDeleteWorktree, + showWorktreeInFinder, }: SidebarMenuHandlers) { const showThreadMenu = useCallback( async ( @@ -121,38 +123,44 @@ export function useSidebarMenus({ text: "Reload threads", action: () => onReloadWorkspaceThreads(worktree.id), }); - const revealItem = await MenuItem.new({ - text: "Show in Finder", - action: async () => { - if (!worktree.path) { - return; - } - try { - await revealItemInDir(worktree.path); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - pushErrorToast({ - title: "Couldn't show worktree in Finder", - message, - }); - console.warn("Failed to reveal worktree", { - message, - workspaceId: worktree.id, - path: worktree.path, - }); - } - }, - }); const deleteItem = await MenuItem.new({ text: "Delete worktree", action: () => onDeleteWorktree(worktree.id), }); - const menu = await Menu.new({ items: [reloadItem, revealItem, deleteItem] }); + const items = [reloadItem]; + if (showWorktreeInFinder) { + items.push( + await MenuItem.new({ + text: "Show in Finder", + action: async () => { + if (!worktree.path) { + return; + } + try { + await revealItemInDir(worktree.path); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + pushErrorToast({ + title: "Couldn't show worktree in Finder", + message, + }); + console.warn("Failed to reveal worktree", { + message, + workspaceId: worktree.id, + path: worktree.path, + }); + } + }, + }), + ); + } + items.push(deleteItem); + const menu = await Menu.new({ items }); const window = getCurrentWindow(); const position = new LogicalPosition(event.clientX, event.clientY); await menu.popup(position, window); }, - [onReloadWorkspaceThreads, onDeleteWorktree], + [onReloadWorkspaceThreads, onDeleteWorktree, showWorktreeInFinder], ); return { showThreadMenu, showWorkspaceMenu, showWorktreeMenu }; diff --git a/src/features/layout/hooks/useLayoutNodes.tsx b/src/features/layout/hooks/useLayoutNodes.tsx index 4eed4439..78ec2f9d 100644 --- a/src/features/layout/hooks/useLayoutNodes.tsx +++ b/src/features/layout/hooks/useLayoutNodes.tsx @@ -118,6 +118,7 @@ type LayoutNodesOptions = { activeRateLimits: RateLimitSnapshot | null; usageShowRemaining: boolean; accountInfo: AccountSnapshot | null; + showWorktreeInFinder: boolean; onSwitchAccount: () => void; onCancelSwitchAccount: () => void; accountSwitching: boolean; @@ -489,6 +490,7 @@ export function useLayoutNodes(options: LayoutNodesOptions): LayoutNodesResult { accountRateLimits={options.activeRateLimits} usageShowRemaining={options.usageShowRemaining} accountInfo={options.accountInfo} + showWorktreeInFinder={options.showWorktreeInFinder} onSwitchAccount={options.onSwitchAccount} onCancelSwitchAccount={options.onCancelSwitchAccount} accountSwitching={options.accountSwitching} diff --git a/src/features/settings/components/SettingsView.test.tsx b/src/features/settings/components/SettingsView.test.tsx index ba21683c..1a539f23 100644 --- a/src/features/settings/components/SettingsView.test.tsx +++ b/src/features/settings/components/SettingsView.test.tsx @@ -55,6 +55,7 @@ const baseSettings: AppSettings = { codeFontSize: 11, notificationSoundsEnabled: true, systemNotificationsEnabled: true, + worktreeShowInFinderEnabled: false, preloadGitDiffs: true, gitDiffIgnoreWhitespaceChanges: false, experimentalCollabEnabled: false, diff --git a/src/features/settings/components/SettingsView.tsx b/src/features/settings/components/SettingsView.tsx index 762f9ae3..51072aff 100644 --- a/src/features/settings/components/SettingsView.tsx +++ b/src/features/settings/components/SettingsView.tsx @@ -3228,6 +3228,30 @@ export function SettingsView({ +
+
+
Show worktree in Finder
+
+ Adds a “Show in Finder” option to the worktree menu (macOS only). +
+
+ +
Personality
diff --git a/src/features/settings/hooks/useAppSettings.ts b/src/features/settings/hooks/useAppSettings.ts index 5fc483f2..081322c9 100644 --- a/src/features/settings/hooks/useAppSettings.ts +++ b/src/features/settings/hooks/useAppSettings.ts @@ -56,6 +56,7 @@ const defaultSettings: AppSettings = { codeFontSize: CODE_FONT_SIZE_DEFAULT, notificationSoundsEnabled: true, systemNotificationsEnabled: true, + worktreeShowInFinderEnabled: false, preloadGitDiffs: true, gitDiffIgnoreWhitespaceChanges: false, experimentalCollabEnabled: false, diff --git a/src/types.ts b/src/types.ts index 3eb3d800..0a0adec5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -175,6 +175,7 @@ export type AppSettings = { codeFontSize: number; notificationSoundsEnabled: boolean; systemNotificationsEnabled: boolean; + worktreeShowInFinderEnabled: boolean; preloadGitDiffs: boolean; gitDiffIgnoreWhitespaceChanges: boolean; experimentalCollabEnabled: boolean; From 4c664f7c4f5ba5f72357dae1b7ba02e26942dfc8 Mon Sep 17 00:00:00 2001 From: Ivan Borinschi Date: Wed, 4 Feb 2026 10:47:59 +0200 Subject: [PATCH 3/4] Revert "Add toggle for worktree Finder menu" This reverts commit 63cac9377c6048db82aef3ed41d44ddd0e57e4e8. --- src-tauri/src/types.rs | 11 ---- src/App.tsx | 1 - src/features/app/components/Sidebar.test.tsx | 1 - src/features/app/components/Sidebar.tsx | 3 - .../app/hooks/useSidebarMenus.test.tsx | 47 ---------------- src/features/app/hooks/useSidebarMenus.ts | 56 ++++++++----------- src/features/layout/hooks/useLayoutNodes.tsx | 2 - .../settings/components/SettingsView.test.tsx | 1 - .../settings/components/SettingsView.tsx | 24 -------- src/features/settings/hooks/useAppSettings.ts | 1 - src/types.ts | 1 - 11 files changed, 24 insertions(+), 124 deletions(-) diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 632b3fd2..791450ce 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -431,11 +431,6 @@ pub(crate) struct AppSettings { rename = "systemNotificationsEnabled" )] pub(crate) system_notifications_enabled: bool, - #[serde( - default = "default_worktree_show_in_finder_enabled", - rename = "worktreeShowInFinderEnabled" - )] - pub(crate) worktree_show_in_finder_enabled: bool, #[serde( default = "default_experimental_collab_enabled", rename = "experimentalCollabEnabled" @@ -546,10 +541,6 @@ fn default_usage_show_remaining() -> bool { false } -fn default_worktree_show_in_finder_enabled() -> bool { - false -} - fn default_ui_font_family() -> String { "\"SF Pro Text\", \"SF Pro Display\", -apple-system, \"Helvetica Neue\", sans-serif" .to_string() @@ -822,7 +813,6 @@ impl Default for AppSettings { code_font_size: default_code_font_size(), notification_sounds_enabled: true, system_notifications_enabled: true, - worktree_show_in_finder_enabled: default_worktree_show_in_finder_enabled(), preload_git_diffs: default_preload_git_diffs(), git_diff_ignore_whitespace_changes: default_git_diff_ignore_whitespace_changes(), experimental_collab_enabled: false, @@ -916,7 +906,6 @@ mod tests { settings.cycle_workspace_prev_shortcut.as_deref(), Some("cmd+shift+up") ); - assert!(!settings.worktree_show_in_finder_enabled); assert!(settings.last_composer_model_id.is_none()); assert!(settings.last_composer_reasoning_effort.is_none()); assert!((settings.ui_scale - 1.0).abs() < f64::EPSILON); diff --git a/src/App.tsx b/src/App.tsx index bc330ab4..8f3cb33b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1742,7 +1742,6 @@ function MainApp() { activeRateLimits, usageShowRemaining: appSettings.usageShowRemaining, accountInfo: activeAccount, - showWorktreeInFinder: appSettings.worktreeShowInFinderEnabled, onSwitchAccount: handleSwitchAccount, onCancelSwitchAccount: handleCancelSwitchAccount, accountSwitching, diff --git a/src/features/app/components/Sidebar.test.tsx b/src/features/app/components/Sidebar.test.tsx index d8fee315..d4904c75 100644 --- a/src/features/app/components/Sidebar.test.tsx +++ b/src/features/app/components/Sidebar.test.tsx @@ -21,7 +21,6 @@ const baseProps = { accountRateLimits: null, usageShowRemaining: false, accountInfo: null, - showWorktreeInFinder: false, onSwitchAccount: vi.fn(), onCancelSwitchAccount: vi.fn(), accountSwitching: false, diff --git a/src/features/app/components/Sidebar.tsx b/src/features/app/components/Sidebar.tsx index 002159c9..be17ed7e 100644 --- a/src/features/app/components/Sidebar.tsx +++ b/src/features/app/components/Sidebar.tsx @@ -57,7 +57,6 @@ type SidebarProps = { accountRateLimits: RateLimitSnapshot | null; usageShowRemaining: boolean; accountInfo: AccountSnapshot | null; - showWorktreeInFinder: boolean; onSwitchAccount: () => void; onCancelSwitchAccount: () => void; accountSwitching: boolean; @@ -111,7 +110,6 @@ export function Sidebar({ accountRateLimits, usageShowRemaining, accountInfo, - showWorktreeInFinder, onSwitchAccount, onCancelSwitchAccount, accountSwitching, @@ -173,7 +171,6 @@ export function Sidebar({ onReloadWorkspaceThreads, onDeleteWorkspace, onDeleteWorktree, - showWorktreeInFinder, }); const { sessionPercent, diff --git a/src/features/app/hooks/useSidebarMenus.test.tsx b/src/features/app/hooks/useSidebarMenus.test.tsx index ee45902e..01189fa1 100644 --- a/src/features/app/hooks/useSidebarMenus.test.tsx +++ b/src/features/app/hooks/useSidebarMenus.test.tsx @@ -64,7 +64,6 @@ describe("useSidebarMenus", () => { onReloadWorkspaceThreads, onDeleteWorkspace, onDeleteWorktree, - showWorktreeInFinder: true, }), ); @@ -99,50 +98,4 @@ describe("useSidebarMenus", () => { await revealItem.action(); expect(revealItemInDir).toHaveBeenCalledWith("/tmp/worktree-1"); }); - - it("omits show in finder when disabled", async () => { - const { result } = renderHook(() => - useSidebarMenus({ - onDeleteThread: vi.fn(), - onSyncThread: vi.fn(), - onPinThread: vi.fn(), - onUnpinThread: vi.fn(), - isThreadPinned: vi.fn(() => false), - onRenameThread: vi.fn(), - onReloadWorkspaceThreads: vi.fn(), - onDeleteWorkspace: vi.fn(), - onDeleteWorktree: vi.fn(), - showWorktreeInFinder: false, - }), - ); - - const worktree: WorkspaceInfo = { - id: "worktree-2", - name: "feature/nope", - path: "/tmp/worktree-2", - kind: "worktree", - connected: true, - settings: { - sidebarCollapsed: false, - worktreeSetupScript: "", - }, - worktree: { branch: "feature/nope" }, - }; - - const event = { - preventDefault: vi.fn(), - stopPropagation: vi.fn(), - clientX: 22, - clientY: 44, - } as unknown as ReactMouseEvent; - - await result.current.showWorktreeMenu(event, worktree); - - const menuArgs = menuNew.mock.calls[1]?.[0]; - const revealItem = menuArgs.items.find( - (item: { text: string }) => item.text === "Show in Finder", - ); - - expect(revealItem).toBeUndefined(); - }); }); diff --git a/src/features/app/hooks/useSidebarMenus.ts b/src/features/app/hooks/useSidebarMenus.ts index 91d1f503..2e391ebb 100644 --- a/src/features/app/hooks/useSidebarMenus.ts +++ b/src/features/app/hooks/useSidebarMenus.ts @@ -17,7 +17,6 @@ type SidebarMenuHandlers = { onReloadWorkspaceThreads: (workspaceId: string) => void; onDeleteWorkspace: (workspaceId: string) => void; onDeleteWorktree: (workspaceId: string) => void; - showWorktreeInFinder: boolean; }; export function useSidebarMenus({ @@ -30,7 +29,6 @@ export function useSidebarMenus({ onReloadWorkspaceThreads, onDeleteWorkspace, onDeleteWorktree, - showWorktreeInFinder, }: SidebarMenuHandlers) { const showThreadMenu = useCallback( async ( @@ -123,44 +121,38 @@ export function useSidebarMenus({ text: "Reload threads", action: () => onReloadWorkspaceThreads(worktree.id), }); + const revealItem = await MenuItem.new({ + text: "Show in Finder", + action: async () => { + if (!worktree.path) { + return; + } + try { + await revealItemInDir(worktree.path); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + pushErrorToast({ + title: "Couldn't show worktree in Finder", + message, + }); + console.warn("Failed to reveal worktree", { + message, + workspaceId: worktree.id, + path: worktree.path, + }); + } + }, + }); const deleteItem = await MenuItem.new({ text: "Delete worktree", action: () => onDeleteWorktree(worktree.id), }); - const items = [reloadItem]; - if (showWorktreeInFinder) { - items.push( - await MenuItem.new({ - text: "Show in Finder", - action: async () => { - if (!worktree.path) { - return; - } - try { - await revealItemInDir(worktree.path); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - pushErrorToast({ - title: "Couldn't show worktree in Finder", - message, - }); - console.warn("Failed to reveal worktree", { - message, - workspaceId: worktree.id, - path: worktree.path, - }); - } - }, - }), - ); - } - items.push(deleteItem); - const menu = await Menu.new({ items }); + const menu = await Menu.new({ items: [reloadItem, revealItem, deleteItem] }); const window = getCurrentWindow(); const position = new LogicalPosition(event.clientX, event.clientY); await menu.popup(position, window); }, - [onReloadWorkspaceThreads, onDeleteWorktree, showWorktreeInFinder], + [onReloadWorkspaceThreads, onDeleteWorktree], ); return { showThreadMenu, showWorkspaceMenu, showWorktreeMenu }; diff --git a/src/features/layout/hooks/useLayoutNodes.tsx b/src/features/layout/hooks/useLayoutNodes.tsx index 78ec2f9d..4eed4439 100644 --- a/src/features/layout/hooks/useLayoutNodes.tsx +++ b/src/features/layout/hooks/useLayoutNodes.tsx @@ -118,7 +118,6 @@ type LayoutNodesOptions = { activeRateLimits: RateLimitSnapshot | null; usageShowRemaining: boolean; accountInfo: AccountSnapshot | null; - showWorktreeInFinder: boolean; onSwitchAccount: () => void; onCancelSwitchAccount: () => void; accountSwitching: boolean; @@ -490,7 +489,6 @@ export function useLayoutNodes(options: LayoutNodesOptions): LayoutNodesResult { accountRateLimits={options.activeRateLimits} usageShowRemaining={options.usageShowRemaining} accountInfo={options.accountInfo} - showWorktreeInFinder={options.showWorktreeInFinder} onSwitchAccount={options.onSwitchAccount} onCancelSwitchAccount={options.onCancelSwitchAccount} accountSwitching={options.accountSwitching} diff --git a/src/features/settings/components/SettingsView.test.tsx b/src/features/settings/components/SettingsView.test.tsx index 1a539f23..ba21683c 100644 --- a/src/features/settings/components/SettingsView.test.tsx +++ b/src/features/settings/components/SettingsView.test.tsx @@ -55,7 +55,6 @@ const baseSettings: AppSettings = { codeFontSize: 11, notificationSoundsEnabled: true, systemNotificationsEnabled: true, - worktreeShowInFinderEnabled: false, preloadGitDiffs: true, gitDiffIgnoreWhitespaceChanges: false, experimentalCollabEnabled: false, diff --git a/src/features/settings/components/SettingsView.tsx b/src/features/settings/components/SettingsView.tsx index 51072aff..762f9ae3 100644 --- a/src/features/settings/components/SettingsView.tsx +++ b/src/features/settings/components/SettingsView.tsx @@ -3228,30 +3228,6 @@ export function SettingsView({
-
-
-
Show worktree in Finder
-
- Adds a “Show in Finder” option to the worktree menu (macOS only). -
-
- -
Personality
diff --git a/src/features/settings/hooks/useAppSettings.ts b/src/features/settings/hooks/useAppSettings.ts index 081322c9..5fc483f2 100644 --- a/src/features/settings/hooks/useAppSettings.ts +++ b/src/features/settings/hooks/useAppSettings.ts @@ -56,7 +56,6 @@ const defaultSettings: AppSettings = { codeFontSize: CODE_FONT_SIZE_DEFAULT, notificationSoundsEnabled: true, systemNotificationsEnabled: true, - worktreeShowInFinderEnabled: false, preloadGitDiffs: true, gitDiffIgnoreWhitespaceChanges: false, experimentalCollabEnabled: false, diff --git a/src/types.ts b/src/types.ts index 0a0adec5..3eb3d800 100644 --- a/src/types.ts +++ b/src/types.ts @@ -175,7 +175,6 @@ export type AppSettings = { codeFontSize: number; notificationSoundsEnabled: boolean; systemNotificationsEnabled: boolean; - worktreeShowInFinderEnabled: boolean; preloadGitDiffs: boolean; gitDiffIgnoreWhitespaceChanges: boolean; experimentalCollabEnabled: boolean; From 8bb0a62452645155e461850fe39a37a73e04a404 Mon Sep 17 00:00:00 2001 From: Ivan Borinschi Date: Wed, 4 Feb 2026 10:49:42 +0200 Subject: [PATCH 4/4] Lazy-load opener for Finder reveal --- src/features/app/hooks/useSidebarMenus.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/app/hooks/useSidebarMenus.ts b/src/features/app/hooks/useSidebarMenus.ts index 2e391ebb..ff4f1eba 100644 --- a/src/features/app/hooks/useSidebarMenus.ts +++ b/src/features/app/hooks/useSidebarMenus.ts @@ -2,7 +2,6 @@ import { useCallback, type MouseEvent } from "react"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { LogicalPosition } from "@tauri-apps/api/dpi"; import { getCurrentWindow } from "@tauri-apps/api/window"; -import { revealItemInDir } from "@tauri-apps/plugin-opener"; import type { WorkspaceInfo } from "../../../types"; import { pushErrorToast } from "../../../services/toasts"; @@ -128,6 +127,9 @@ export function useSidebarMenus({ return; } try { + const { revealItemInDir } = await import( + "@tauri-apps/plugin-opener" + ); await revealItemInDir(worktree.path); } catch (error) { const message = error instanceof Error ? error.message : String(error);