authorgravatar for 73941998+UtkarshUsername@users.noreply.github.comUtkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> 2026-08-11 18:32:02+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-11 18:32:02+05:30
log65b005f1e4bfccb6a404b3b1e5bfa363d534ac2a
tree98f4a16a8df3dc1eaffa53eb8c4ba80a3cad906f
parent752acbf6549e6dd422ac8d8ed11c0fa02577965b
signature Signed by PGP key B5690EEEBB952194

feat(web): add Copy Thread ID to the sidebar and chat header thread context menu (#5574)

Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>

4 files changed, 37 insertions(+), 3 deletions(-)

apps/web/src/components/Sidebar.tsx+23-1
...@@ -29,7 +29,7 @@ import {...@@ -29,7 +29,7 @@ import {
29 scopeThreadRef,29 scopeThreadRef,
30 scopedThreadKey,30 scopedThreadKey,
31} from "@t3tools/client-runtime/environment";31} from "@t3tools/client-runtime/environment";
32import type { ScopedThreadRef } from "@t3tools/contracts";32import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts";
33import type { TimestampFormat } from "@t3tools/contracts/settings";33import type { TimestampFormat } from "@t3tools/contracts/settings";
34import {34import {
35 AlarmClockIcon,35 AlarmClockIcon,
...@@ -1647,6 +1647,24 @@ export default function Sidebar() {...@@ -1647,6 +1647,24 @@ export default function Sidebar() {
1647 );1647 );
1648 },1648 },
1649 });1649 });
1650 const { copyToClipboard: copyThreadIdToClipboard } = useCopyToClipboard<{ threadId: ThreadId }>({
1651 onCopy: ({ threadId }) => {
1652 toastManager.add({
1653 type: "success",
1654 title: "Thread ID copied",
1655 description: threadId,
1656 });
1657 },
1658 onError: (error) => {
1659 toastManager.add(
1660 stackedThreadToast({
1661 type: "error",
1662 title: "Failed to copy thread ID",
1663 description: error instanceof Error ? error.message : "An error occurred.",
1664 }),
1665 );
1666 },
1667 });
1650 const [projectScopeMenuOpen, setProjectScopeMenuOpen] = useState(false);1668 const [projectScopeMenuOpen, setProjectScopeMenuOpen] = useState(false);
1651 const newThreadContext = useHandleNewThread();1669 const newThreadContext = useHandleNewThread();
1652 const openAddProjectCommandPalette = useCallback(1670 const openAddProjectCommandPalette = useCallback(
...@@ -3037,6 +3055,9 @@ export default function Sidebar() {...@@ -3037,6 +3055,9 @@ export default function Sidebar() {
3037 copyBranchToClipboard(thread.branch, { branch: thread.branch });3055 copyBranchToClipboard(thread.branch, { branch: thread.branch });
3038 }3056 }
3039 return;3057 return;
3058 case "copy-thread-id":
3059 copyThreadIdToClipboard(thread.id, { threadId: thread.id });
3060 return;
3040 case "delete": {3061 case "delete": {
3041 if (confirmThreadDelete) {3062 if (confirmThreadDelete) {
3042 const confirmed = await settlePromise(() =>3063 const confirmed = await settlePromise(() =>
...@@ -3079,6 +3100,7 @@ export default function Sidebar() {...@@ -3079,6 +3100,7 @@ export default function Sidebar() {
3079 confirmThreadDelete,3100 confirmThreadDelete,
3080 copyBranchToClipboard,3101 copyBranchToClipboard,
3081 copyPathToClipboard,3102 copyPathToClipboard,
3103 copyThreadIdToClipboard,
3082 deleteThread,3104 deleteThread,
3083 handleMultiSelectContextMenu,3105 handleMultiSelectContextMenu,
3084 markThreadUnread,3106 markThreadUnread,
apps/web/src/components/threadActionMenu.logic.test.ts+1-1
...@@ -26,7 +26,7 @@ describe("buildThreadActionMenuItems", () => {...@@ -26,7 +26,7 @@ describe("buildThreadActionMenuItems", () => {
26 ...baseState,26 ...baseState,
27 supports: { settlement: false, snooze: false, pinning: false, titleRegeneration: false },27 supports: { settlement: false, snooze: false, pinning: false, titleRegeneration: false },
28 }),28 }),
29 ).toEqual(["rename", "mark-unread", "copy-path", "delete"]);29 ).toEqual(["rename", "mark-unread", "copy-path", "copy-thread-id", "delete"]);
30 });30 });
3131
32 it("includes branch items only for threads with a branch", () => {32 it("includes branch items only for threads with a branch", () => {
apps/web/src/components/threadActionMenu.logic.ts+2
...@@ -20,6 +20,7 @@ export type ThreadActionMenuId =...@@ -20,6 +20,7 @@ export type ThreadActionMenuId =
20 | "mark-unread"20 | "mark-unread"
21 | "copy-path"21 | "copy-path"
22 | "copy-branch"22 | "copy-branch"
23 | "copy-thread-id"
23 | "delete";24 | "delete";
2425
25export interface ThreadActionMenuState {26export interface ThreadActionMenuState {
...@@ -100,6 +101,7 @@ export function buildThreadActionMenuItems(...@@ -100,6 +101,7 @@ export function buildThreadActionMenuItems(
100 { id: "mark-unread", label: "Mark unread" },101 { id: "mark-unread", label: "Mark unread" },
101 { id: "copy-path", label: "Copy path", icon: "copy" },102 { id: "copy-path", label: "Copy path", icon: "copy" },
102 ...(state.branch ? [{ id: "copy-branch" as const, label: "Copy branch", icon: "copy" }] : []),103 ...(state.branch ? [{ id: "copy-branch" as const, label: "Copy branch", icon: "copy" }] : []),
104 { id: "copy-thread-id", label: "Copy thread ID", icon: "copy" },
103 { id: "delete", label: "Delete", destructive: true, icon: "trash" },105 { id: "delete", label: "Delete", destructive: true, icon: "trash" },
104 ];106 ];
105}107}
apps/web/src/hooks/useThreadActionMenu.ts+11-1
...@@ -11,7 +11,7 @@ import {...@@ -11,7 +11,7 @@ import {
11 effectiveSnoozed,11 effectiveSnoozed,
12 type ChangeRequestStateLike,12 type ChangeRequestStateLike,
13} from "@t3tools/client-runtime/state/thread-settled";13} from "@t3tools/client-runtime/state/thread-settled";
14import type { ScopedThreadRef } from "@t3tools/contracts";14import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts";
15import { useCallback } from "react";15import { useCallback } from "react";
1616
17import { resolveSnoozePresets, snoozeWakeDescription } from "../components/Sidebar.snooze";17import { resolveSnoozePresets, snoozeWakeDescription } from "../components/Sidebar.snooze";
...@@ -95,6 +95,12 @@ export function useThreadActionMenu(input: {...@@ -95,6 +95,12 @@ export function useThreadActionMenu(input: {
95 },95 },
96 onError: (error) => failureToast("Failed to copy branch", error),96 onError: (error) => failureToast("Failed to copy branch", error),
97 });97 });
98 const { copyToClipboard: copyThreadIdToClipboard } = useCopyToClipboard<{ threadId: ThreadId }>({
99 onCopy: ({ threadId }) => {
100 toastManager.add({ type: "success", title: "Thread ID copied", description: threadId });
101 },
102 onError: (error) => failureToast("Failed to copy thread ID", error),
103 });
98104
99 const openMenu = useCallback(105 const openMenu = useCallback(
100 (position: { x: number; y: number }) => {106 (position: { x: number; y: number }) => {
...@@ -242,6 +248,9 @@ export function useThreadActionMenu(input: {...@@ -242,6 +248,9 @@ export function useThreadActionMenu(input: {
242 copyBranchToClipboard(thread.branch, { branch: thread.branch });248 copyBranchToClipboard(thread.branch, { branch: thread.branch });
243 }249 }
244 return;250 return;
251 case "copy-thread-id":
252 copyThreadIdToClipboard(thread.id, { threadId: thread.id });
253 return;
245 case "delete": {254 case "delete": {
246 if (confirmThreadDelete) {255 if (confirmThreadDelete) {
247 const confirmed = await settlePromise(() =>256 const confirmed = await settlePromise(() =>
...@@ -279,6 +288,7 @@ export function useThreadActionMenu(input: {...@@ -279,6 +288,7 @@ export function useThreadActionMenu(input: {
279 confirmThreadDelete,288 confirmThreadDelete,
280 copyBranchToClipboard,289 copyBranchToClipboard,
281 copyPathToClipboard,290 copyPathToClipboard,
291 copyThreadIdToClipboard,
282 deleteThread,292 deleteThread,
283 handleNewThread,293 handleNewThread,
284 markThreadUnread,294 markThreadUnread,