diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 1b43eb4c1..83a5b2c72 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,40 @@ 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 +726,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 +756,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(