authorgravatar for 105842516+jamesx0416@users.noreply.github.comJames <105842516+jamesx0416@users.noreply.github.com> 2026-03-17 16:34:24+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-16 22:34:24-07:00
logd430a0624b570561a4d8eedc7c277fb6c31c46e1
tree8c03798daf26270dfec3c319b33c8f6f7ccfd950
parent765c1dc9e418f4ce0dbeb42c154d7ecb65a09fb6
signature Signed by PGP key B5690EEEBB952194

Add copy workspace path button to thread action menu (#1128)


1 files changed, 44 insertions(+), 6 deletions(-)

apps/web/src/components/Sidebar.tsx+44-6
......@@ -561,7 +561,6 @@ export default function Sidebar() {
561561 if (!api) return;
562562 const thread = threads.find((t) => t.id === threadId);
563563 if (!thread) return;
564
565564 const threadProject = projects.find((project) => project.id === thread.projectId);
566565 // When bulk-deleting, exclude the other threads being deleted so
567566 // getOrphanedWorktreePathForThread correctly detects that no surviving
......@@ -665,7 +664,7 @@ export default function Sidebar() {
665664 ],
666665 );
667666
668 const { copyToClipboard } = useCopyToClipboard<{ threadId: ThreadId }>({
667 const { copyToClipboard: copyThreadIdToClipboard } = useCopyToClipboard<{ threadId: ThreadId }>({
669668 onCopy: (ctx) => {
670669 toastManager.add({
671670 type: "success",
......@@ -681,21 +680,40 @@ export default function Sidebar() {
681680 });
682681 },
683682 });
683 const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({
684 onCopy: (ctx) => {
685 toastManager.add({
686 type: "success",
687 title: "Path copied",
688 description: ctx.path,
689 });
690 },
691 onError: (error) => {
692 toastManager.add({
693 type: "error",
694 title: "Failed to copy path",
695 description: error instanceof Error ? error.message : "An error occurred.",
696 });
697 },
698 });
684699 const handleThreadContextMenu = useCallback(
685700 async (threadId: ThreadId, position: { x: number; y: number }) => {
686701 const api = readNativeApi();
687702 if (!api) return;
703 const thread = threads.find((t) => t.id === threadId);
704 if (!thread) return;
705 const threadWorkspacePath =
706 thread.worktreePath ?? projectCwdById.get(thread.projectId) ?? null;
688707 const clicked = await api.contextMenu.show(
689708 [
690709 { id: "rename", label: "Rename thread" },
691710 { id: "mark-unread", label: "Mark unread" },
711 { id: "copy-path", label: "Copy Path" },
692712 { id: "copy-thread-id", label: "Copy Thread ID" },
693713 { id: "delete", label: "Delete", destructive: true },
694714 ],
695715 position,
696716 );
697 const thread = threads.find((t) => t.id === threadId);
698 if (!thread) return;
699717
700718 if (clicked === "rename") {
701719 setRenamingThreadId(threadId);
......@@ -708,8 +726,20 @@ export default function Sidebar() {
708726 markThreadUnread(threadId);
709727 return;
710728 }
729 if (clicked === "copy-path") {
730 if (!threadWorkspacePath) {
731 toastManager.add({
732 type: "error",
733 title: "Path unavailable",
734 description: "This thread does not have a workspace path to copy.",
735 });
736 return;
737 }
738 copyPathToClipboard(threadWorkspacePath, { path: threadWorkspacePath });
739 return;
740 }
711741 if (clicked === "copy-thread-id") {
712 copyToClipboard(threadId, { threadId });
742 copyThreadIdToClipboard(threadId, { threadId });
713743 return;
714744 }
715745 if (clicked !== "delete") return;
......@@ -726,7 +756,15 @@ export default function Sidebar() {
726756 }
727757 await deleteThread(threadId);
728758 },
729 [appSettings.confirmThreadDelete, copyToClipboard, deleteThread, markThreadUnread, threads],
759 [
760 appSettings.confirmThreadDelete,
761 copyPathToClipboard,
762 copyThreadIdToClipboard,
763 deleteThread,
764 markThreadUnread,
765 projectCwdById,
766 threads,
767 ],
730768 );
731769
732770 const handleMultiSelectContextMenu = useCallback(