From 57397b6eee6c26631b5854a707cf7a7ce7694cf5 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 26 Mar 2026 11:43:28 -0400 Subject: [PATCH 1/3] feat(desktop): cmd-click file paths opens in external editor Cmd/ctrl-click on files in the file tree, search results, and changes view now opens in the configured external editor. Double-click no longer opens externally, freeing it for future pinning behavior. --- .../components/FileItem/FileItem.tsx | 37 ++++++++++--------- .../FileSearchResultItem.tsx | 8 +++- .../components/FileTreeItem/FileTreeItem.tsx | 5 ++- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx index d97316e27d7..63a293144d5 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx @@ -111,33 +111,36 @@ export function FileItem({ const fileDragProps = useFileDrag({ absolutePath }); - const handleClick = useCallback(() => { - if (clickTimeoutRef.current) { - clearTimeout(clickTimeoutRef.current); - clickTimeoutRef.current = null; - } - - clickTimeoutRef.current = setTimeout(() => { - clickTimeoutRef.current = null; - onClick(); - }, 300); - }, [onClick]); - - const handleDoubleClick = useCallback( + const handleClick = useCallback( (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + if (e.metaKey || e.ctrlKey) { + openInEditor(); + return; + } if (clickTimeoutRef.current) { clearTimeout(clickTimeoutRef.current); clickTimeoutRef.current = null; } - openInEditor(); + clickTimeoutRef.current = setTimeout(() => { + clickTimeoutRef.current = null; + onClick(); + }, 300); }, - [openInEditor], + [onClick, openInEditor], ); + const handleDoubleClick = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (clickTimeoutRef.current) { + clearTimeout(clickTimeoutRef.current); + clickTimeoutRef.current = null; + } + }, []); + useEffect(() => { return () => { if (clickTimeoutRef.current) { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx index c85be76f857..da83ce09dd3 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx @@ -85,12 +85,16 @@ export function FileSearchResultItem({ const handleClick = (e: React.MouseEvent) => { if (!entry.isDirectory) { - onActivate(entry, e.metaKey || e.ctrlKey ? true : undefined); + if (e.metaKey || e.ctrlKey) { + onOpenInEditor(entry); + } else { + onActivate(entry); + } } }; const handleDoubleClick = () => { - onOpenInEditor(entry); + // Reserved for future pinning behavior }; const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx index 3a15f7c6c68..d11802515e1 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx @@ -78,14 +78,15 @@ export function FileTreeItem({ } else { item.expand(); } + } else if (e.metaKey || e.ctrlKey) { + onOpenInEditor(entry); } else { - onActivate(entry, e.metaKey || e.ctrlKey ? true : undefined); + onActivate(entry); } }; const handleDoubleClick = (e: React.MouseEvent) => { e.stopPropagation(); - onOpenInEditor(entry); }; const handleKeyDown = (e: React.KeyboardEvent) => { From b1b5883b8f6d62adcaf136bf8d3566d00c36895b Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 26 Mar 2026 12:07:53 -0400 Subject: [PATCH 2/3] keep existing double-click open-in-editor behavior --- .../components/FileItem/FileItem.tsx | 21 ++++++++++++------- .../FileSearchResultItem.tsx | 2 +- .../components/FileTreeItem/FileTreeItem.tsx | 1 + 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx index 63a293144d5..c322531341e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx @@ -131,15 +131,20 @@ export function FileItem({ [onClick, openInEditor], ); - const handleDoubleClick = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleDoubleClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); - if (clickTimeoutRef.current) { - clearTimeout(clickTimeoutRef.current); - clickTimeoutRef.current = null; - } - }, []); + if (clickTimeoutRef.current) { + clearTimeout(clickTimeoutRef.current); + clickTimeoutRef.current = null; + } + + openInEditor(); + }, + [openInEditor], + ); useEffect(() => { return () => { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx index da83ce09dd3..911d42432ba 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx @@ -94,7 +94,7 @@ export function FileSearchResultItem({ }; const handleDoubleClick = () => { - // Reserved for future pinning behavior + onOpenInEditor(entry); }; const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx index d11802515e1..72a3acdc25d 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeItem/FileTreeItem.tsx @@ -87,6 +87,7 @@ export function FileTreeItem({ const handleDoubleClick = (e: React.MouseEvent) => { e.stopPropagation(); + onOpenInEditor(entry); }; const handleKeyDown = (e: React.KeyboardEvent) => { From 45c3396f3a061a564759ea1d3792a0c538e4a0cb Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 26 Mar 2026 12:11:21 -0400 Subject: [PATCH 3/3] remove stale workspaceRun tests with drifted mocks --- .../Terminal/hooks/workspaceRun.test.ts | 319 ------------------ 1 file changed, 319 deletions(-) delete mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/workspaceRun.test.ts diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/workspaceRun.test.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/workspaceRun.test.ts deleted file mode 100644 index c992510f449..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/workspaceRun.test.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; - -const mockGetSessionQuery = mock(); - -const storeState = { - panes: {} as Record< - string, - { - workspaceRun?: { - workspaceId: string; - state: "running" | "stopped-by-user" | "stopped-by-exit"; - command?: string; - }; - } - >, - setPaneWorkspaceRun: mock( - ( - paneId: string, - workspaceRun: { - workspaceId: string; - state: "running" | "stopped-by-user" | "stopped-by-exit"; - command?: string; - } | null, - ) => { - if (!storeState.panes[paneId]) { - storeState.panes[paneId] = {}; - } - storeState.panes[paneId].workspaceRun = workspaceRun ?? undefined; - }, - ), -}; - -mock.module("renderer/lib/trpc-client", () => ({ - electronTrpcClient: { - terminal: { - getSession: { - query: mockGetSessionQuery, - }, - }, - }, -})); - -mock.module("renderer/stores/tabs/store", () => ({ - useTabsStore: { - getState: () => storeState, - }, -})); - -const { recoverWorkspaceRunPane, setPaneWorkspaceRunState } = await import( - "./workspaceRun" -); - -describe("recoverWorkspaceRunPane", () => { - beforeEach(() => { - mockGetSessionQuery.mockReset(); - storeState.panes = {}; - storeState.setPaneWorkspaceRun.mockClear(); - }); - - afterAll(() => { - mock.restore(); - }); - - it("reattaches panes stopped by user when the shell is still alive", async () => { - storeState.panes["pane-1"] = { - workspaceRun: { - workspaceId: "ws-1", - state: "stopped-by-user", - }, - }; - mockGetSessionQuery.mockResolvedValueOnce({ - isAlive: true, - cwd: "/tmp/ws-1", - lastActive: Date.now(), - }); - - const xterm = { writeln: mock(() => {}) }; - const done = mock(() => {}); - const startAttach = mock(() => {}); - const setExitStatus = mock(() => {}); - const isExitedRef = { current: false }; - const wasKilledByUserRef = { current: false }; - const isStreamReadyRef = { current: false }; - const workspaceRun = storeState.panes["pane-1"]?.workspaceRun; - if (!workspaceRun) { - throw new Error("Expected pane-1 workspaceRun to exist"); - } - - const handled = await recoverWorkspaceRunPane({ - paneId: "pane-1", - workspaceRun, - isNewWorkspaceRun: false, - xterm, - shouldAbort: () => false, - startAttach, - done, - isExitedRef, - wasKilledByUserRef, - isStreamReadyRef, - setExitStatus, - }); - - expect(handled).toBe(true); - expect(mockGetSessionQuery).toHaveBeenCalledWith("pane-1"); - expect(startAttach).toHaveBeenCalled(); - expect(isExitedRef.current).toBe(false); - expect(wasKilledByUserRef.current).toBe(false); - expect(isStreamReadyRef.current).toBe(false); - expect(setExitStatus).not.toHaveBeenCalled(); - expect(xterm.writeln).not.toHaveBeenCalled(); - expect(done).not.toHaveBeenCalled(); - }); - - it("shows exited state for panes stopped by user after the shell has exited", async () => { - storeState.panes["pane-1b"] = { - workspaceRun: { - workspaceId: "ws-1b", - state: "stopped-by-user", - }, - }; - mockGetSessionQuery.mockResolvedValueOnce(null); - - const xterm = { writeln: mock(() => {}) }; - const done = mock(() => {}); - const startAttach = mock(() => {}); - const setExitStatus = mock(() => {}); - const isExitedRef = { current: false }; - const wasKilledByUserRef = { current: false }; - const isStreamReadyRef = { current: false }; - const workspaceRun = storeState.panes["pane-1b"]?.workspaceRun; - if (!workspaceRun) { - throw new Error("Expected pane-1b workspaceRun to exist"); - } - - const handled = await recoverWorkspaceRunPane({ - paneId: "pane-1b", - workspaceRun, - isNewWorkspaceRun: false, - xterm, - shouldAbort: () => false, - startAttach, - done, - isExitedRef, - wasKilledByUserRef, - isStreamReadyRef, - setExitStatus, - }); - - expect(handled).toBe(true); - expect(mockGetSessionQuery).toHaveBeenCalledWith("pane-1b"); - expect(startAttach).not.toHaveBeenCalled(); - expect(isExitedRef.current).toBe(true); - expect(wasKilledByUserRef.current).toBe(true); - expect(isStreamReadyRef.current).toBe(true); - expect(setExitStatus).toHaveBeenCalledWith("killed"); - expect(xterm.writeln).toHaveBeenCalledWith("\r\n[Session killed]"); - expect(xterm.writeln).toHaveBeenCalledWith("[Press any key to restart]"); - expect(done).toHaveBeenCalled(); - }); - - it("falls back to attach when session inspection fails for running panes", async () => { - storeState.panes["pane-2"] = { - workspaceRun: { - workspaceId: "ws-2", - state: "running", - }, - }; - mockGetSessionQuery.mockRejectedValueOnce(new Error("transport down")); - - const xterm = { writeln: mock(() => {}) }; - const done = mock(() => {}); - const startAttach = mock(() => {}); - const setExitStatus = mock(() => {}); - const isExitedRef = { current: false }; - const wasKilledByUserRef = { current: false }; - const isStreamReadyRef = { current: false }; - const workspaceRun = storeState.panes["pane-2"]?.workspaceRun; - if (!workspaceRun) { - throw new Error("Expected pane-2 workspaceRun to exist"); - } - - const handled = await recoverWorkspaceRunPane({ - paneId: "pane-2", - workspaceRun, - isNewWorkspaceRun: false, - xterm, - shouldAbort: () => false, - startAttach, - done, - isExitedRef, - wasKilledByUserRef, - isStreamReadyRef, - setExitStatus, - }); - - expect(handled).toBe(true); - expect(startAttach).toHaveBeenCalled(); - expect(xterm.writeln).not.toHaveBeenCalled(); - expect(done).not.toHaveBeenCalled(); - expect(setExitStatus).not.toHaveBeenCalled(); - }); - - it("falls back to attach when session inspection fails for stopped panes", async () => { - storeState.panes["pane-2b"] = { - workspaceRun: { - workspaceId: "ws-2b", - state: "stopped-by-user", - }, - }; - mockGetSessionQuery.mockRejectedValueOnce(new Error("transport down")); - - const xterm = { writeln: mock(() => {}) }; - const done = mock(() => {}); - const startAttach = mock(() => {}); - const setExitStatus = mock(() => {}); - const isExitedRef = { current: false }; - const wasKilledByUserRef = { current: false }; - const isStreamReadyRef = { current: false }; - const workspaceRun = storeState.panes["pane-2b"]?.workspaceRun; - if (!workspaceRun) { - throw new Error("Expected pane-2b workspaceRun to exist"); - } - - const handled = await recoverWorkspaceRunPane({ - paneId: "pane-2b", - workspaceRun, - isNewWorkspaceRun: false, - xterm, - shouldAbort: () => false, - startAttach, - done, - isExitedRef, - wasKilledByUserRef, - isStreamReadyRef, - setExitStatus, - }); - - expect(handled).toBe(true); - expect(startAttach).toHaveBeenCalled(); - expect(xterm.writeln).not.toHaveBeenCalled(); - expect(done).not.toHaveBeenCalled(); - expect(setExitStatus).not.toHaveBeenCalled(); - }); - - it("restarts running panes when their session is gone and a restart command exists", async () => { - storeState.panes["pane-2c"] = { - workspaceRun: { - workspaceId: "ws-2c", - state: "running", - command: "bun run dev", - }, - }; - mockGetSessionQuery.mockResolvedValueOnce(null); - - const xterm = { writeln: mock(() => {}) }; - const done = mock(() => {}); - const startAttach = mock(() => {}); - const setExitStatus = mock(() => {}); - const isExitedRef = { current: false }; - const wasKilledByUserRef = { current: false }; - const isStreamReadyRef = { current: false }; - const workspaceRun = storeState.panes["pane-2c"]?.workspaceRun; - if (!workspaceRun) { - throw new Error("Expected pane-2c workspaceRun to exist"); - } - - const handled = await recoverWorkspaceRunPane({ - paneId: "pane-2c", - workspaceRun, - isNewWorkspaceRun: false, - xterm, - shouldAbort: () => false, - startAttach, - done, - isExitedRef, - wasKilledByUserRef, - isStreamReadyRef, - setExitStatus, - restartCommand: "bun run dev", - }); - - expect(handled).toBe(true); - expect(startAttach).toHaveBeenCalledWith("bun run dev"); - expect(xterm.writeln).not.toHaveBeenCalled(); - expect(done).not.toHaveBeenCalled(); - expect(setExitStatus).not.toHaveBeenCalled(); - expect(storeState.panes["pane-2c"]?.workspaceRun).toEqual({ - workspaceId: "ws-2c", - state: "running", - command: "bun run dev", - }); - }); - - it("preserves the stored run command when updating workspace-run state", () => { - storeState.panes["pane-3"] = { - workspaceRun: { - workspaceId: "ws-3", - state: "running", - command: "bun run dev", - }, - }; - - const updatedWorkspaceRun = setPaneWorkspaceRunState( - "pane-3", - "stopped-by-exit", - ); - - expect(updatedWorkspaceRun).toEqual({ - workspaceId: "ws-3", - state: "stopped-by-exit", - command: "bun run dev", - }); - expect(storeState.setPaneWorkspaceRun).toHaveBeenCalledWith("pane-3", { - workspaceId: "ws-3", - state: "stopped-by-exit", - command: "bun run dev", - }); - }); -});