From 46fc3726ae819c4277c933d3a681f66bb74f8b58 Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Mon, 16 Mar 2026 12:17:55 +1100 Subject: [PATCH 1/2] Add thread context menu action to copy workspace path - Add `Copy Path` to the thread context menu in the sidebar - Resolve thread path from `worktreePath` with project cwd fallback - Show success/error toasts for copy and unavailable path cases --- apps/web/src/components/Sidebar.tsx | 49 +++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 1b43eb4c1..ea7005c9e 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -561,7 +561,6 @@ export default function Sidebar() { if (!api) return; const thread = threads.find((t) => t.id === threadId); if (!thread) return; - const threadProject = projects.find((project) => project.id === thread.projectId); // When bulk-deleting, exclude the other threads being deleted so // getOrphanedWorktreePathForThread correctly detects that no surviving @@ -665,7 +664,7 @@ export default function Sidebar() { ], ); - const { copyToClipboard } = useCopyToClipboard<{ threadId: ThreadId }>({ + const { copyToClipboard: copyThreadIdToClipboard } = useCopyToClipboard<{ threadId: ThreadId }>({ onCopy: (ctx) => { toastManager.add({ type: "success", @@ -681,21 +680,39 @@ export default function Sidebar() { }); }, }); + const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({ + onCopy: (ctx) => { + toastManager.add({ + type: "success", + title: "Path copied", + description: ctx.path, + }); + }, + onError: (error) => { + toastManager.add({ + type: "error", + title: "Failed to copy path", + description: error instanceof Error ? error.message : "An error occurred.", + }); + }, + }); const handleThreadContextMenu = useCallback( async (threadId: ThreadId, position: { x: number; y: number }) => { const api = readNativeApi(); if (!api) return; + const thread = threads.find((t) => t.id === threadId); + if (!thread) return; + const threadWorkspacePath = thread.worktreePath ?? projectCwdById.get(thread.projectId) ?? null; const clicked = await api.contextMenu.show( [ { id: "rename", label: "Rename thread" }, { id: "mark-unread", label: "Mark unread" }, + { id: "copy-path", label: "Copy Path" }, { id: "copy-thread-id", label: "Copy Thread ID" }, { id: "delete", label: "Delete", destructive: true }, ], position, ); - const thread = threads.find((t) => t.id === threadId); - if (!thread) return; if (clicked === "rename") { setRenamingThreadId(threadId); @@ -708,8 +725,20 @@ export default function Sidebar() { markThreadUnread(threadId); return; } + if (clicked === "copy-path") { + if (!threadWorkspacePath) { + toastManager.add({ + type: "error", + title: "Path unavailable", + description: "This thread does not have a workspace path to copy.", + }); + return; + } + copyPathToClipboard(threadWorkspacePath, { path: threadWorkspacePath }); + return; + } if (clicked === "copy-thread-id") { - copyToClipboard(threadId, { threadId }); + copyThreadIdToClipboard(threadId, { threadId }); return; } if (clicked !== "delete") return; @@ -726,7 +755,15 @@ export default function Sidebar() { } await deleteThread(threadId); }, - [appSettings.confirmThreadDelete, copyToClipboard, deleteThread, markThreadUnread, threads], + [ + appSettings.confirmThreadDelete, + copyPathToClipboard, + copyThreadIdToClipboard, + deleteThread, + markThreadUnread, + projectCwdById, + threads, + ], ); const handleMultiSelectContextMenu = useCallback( From 26123af5d20f5e28a0bdb1ad244646028309ba02 Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Mon, 16 Mar 2026 12:53:14 +1100 Subject: [PATCH 2/2] style: format sidebar copy path change --- apps/web/src/components/Sidebar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index ea7005c9e..83a5b2c72 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -702,7 +702,8 @@ export default function Sidebar() { if (!api) return; const thread = threads.find((t) => t.id === threadId); if (!thread) return; - const threadWorkspacePath = thread.worktreePath ?? projectCwdById.get(thread.projectId) ?? null; + const threadWorkspacePath = + thread.worktreePath ?? projectCwdById.get(thread.projectId) ?? null; const clicked = await api.contextMenu.show( [ { id: "rename", label: "Rename thread" },