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 {
2929 scopeThreadRef,
3030 scopedThreadKey,
3131} from "@t3tools/client-runtime/environment";
32import type { ScopedThreadRef } from "@t3tools/contracts";
32import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts";
3333import type { TimestampFormat } from "@t3tools/contracts/settings";
3434import {
3535 AlarmClockIcon,
......@@ -1647,6 +1647,24 @@ export default function Sidebar() {
16471647 );
16481648 },
16491649 });
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 });
16501668 const [projectScopeMenuOpen, setProjectScopeMenuOpen] = useState(false);
16511669 const newThreadContext = useHandleNewThread();
16521670 const openAddProjectCommandPalette = useCallback(
......@@ -3037,6 +3055,9 @@ export default function Sidebar() {
30373055 copyBranchToClipboard(thread.branch, { branch: thread.branch });
30383056 }
30393057 return;
3058 case "copy-thread-id":
3059 copyThreadIdToClipboard(thread.id, { threadId: thread.id });
3060 return;
30403061 case "delete": {
30413062 if (confirmThreadDelete) {
30423063 const confirmed = await settlePromise(() =>
......@@ -3079,6 +3100,7 @@ export default function Sidebar() {
30793100 confirmThreadDelete,
30803101 copyBranchToClipboard,
30813102 copyPathToClipboard,
3103 copyThreadIdToClipboard,
30823104 deleteThread,
30833105 handleMultiSelectContextMenu,
30843106 markThreadUnread,
apps/web/src/components/threadActionMenu.logic.test.ts+1-1
......@@ -26,7 +26,7 @@ describe("buildThreadActionMenuItems", () => {
2626 ...baseState,
2727 supports: { settlement: false, snooze: false, pinning: false, titleRegeneration: false },
2828 }),
29 ).toEqual(["rename", "mark-unread", "copy-path", "delete"]);
29 ).toEqual(["rename", "mark-unread", "copy-path", "copy-thread-id", "delete"]);
3030 });
3131
3232 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 =
2020 | "mark-unread"
2121 | "copy-path"
2222 | "copy-branch"
23 | "copy-thread-id"
2324 | "delete";
2425
2526export interface ThreadActionMenuState {
......@@ -100,6 +101,7 @@ export function buildThreadActionMenuItems(
100101 { id: "mark-unread", label: "Mark unread" },
101102 { id: "copy-path", label: "Copy path", icon: "copy" },
102103 ...(state.branch ? [{ id: "copy-branch" as const, label: "Copy branch", icon: "copy" }] : []),
104 { id: "copy-thread-id", label: "Copy thread ID", icon: "copy" },
103105 { id: "delete", label: "Delete", destructive: true, icon: "trash" },
104106 ];
105107}
apps/web/src/hooks/useThreadActionMenu.ts+11-1
......@@ -11,7 +11,7 @@ import {
1111 effectiveSnoozed,
1212 type ChangeRequestStateLike,
1313} from "@t3tools/client-runtime/state/thread-settled";
14import type { ScopedThreadRef } from "@t3tools/contracts";
14import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts";
1515import { useCallback } from "react";
1616
1717import { resolveSnoozePresets, snoozeWakeDescription } from "../components/Sidebar.snooze";
......@@ -95,6 +95,12 @@ export function useThreadActionMenu(input: {
9595 },
9696 onError: (error) => failureToast("Failed to copy branch", error),
9797 });
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
99105 const openMenu = useCallback(
100106 (position: { x: number; y: number }) => {
......@@ -242,6 +248,9 @@ export function useThreadActionMenu(input: {
242248 copyBranchToClipboard(thread.branch, { branch: thread.branch });
243249 }
244250 return;
251 case "copy-thread-id":
252 copyThreadIdToClipboard(thread.id, { threadId: thread.id });
253 return;
245254 case "delete": {
246255 if (confirmThreadDelete) {
247256 const confirmed = await settlePromise(() =>
......@@ -279,6 +288,7 @@ export function useThreadActionMenu(input: {
279288 confirmThreadDelete,
280289 copyBranchToClipboard,
281290 copyPathToClipboard,
291 copyThreadIdToClipboard,
282292 deleteThread,
283293 handleNewThread,
284294 markThreadUnread,