Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
scopeThreadRef,
scopedThreadKey,
} from "@t3tools/client-runtime/environment";
import type { ScopedThreadRef } from "@t3tools/contracts";
import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts";
import type { TimestampFormat } from "@t3tools/contracts/settings";
import {
AlarmClockIcon,
Expand Down Expand Up @@ -1631,6 +1631,25 @@ export default function Sidebar() {
);
},
});
const { copyToClipboard: copyThreadId } = useCopyToClipboard<{ threadId: ThreadId }>({
target: "thread ID",
onCopy: ({ threadId }) => {
toastManager.add({
type: "success",
title: "Thread ID copied",
description: threadId,
});
},
onError: (error) => {
toastManager.add(
stackedThreadToast({
type: "error",
title: "Failed to copy thread ID",
description: error instanceof Error ? error.message : "An error occurred.",
}),
);
},
});
const [projectScopeMenuOpen, setProjectScopeMenuOpen] = useState(false);
const newThreadContext = useHandleNewThread();
const openAddProjectCommandPalette = useCallback(
Expand Down Expand Up @@ -3007,6 +3026,9 @@ export default function Sidebar() {
copyBranchToClipboard(thread.branch, { branch: thread.branch });
}
return;
case "copy-thread-id":
copyThreadId(thread.id, { threadId: thread.id });
return;
case "delete": {
if (confirmThreadDelete) {
const confirmed = await settlePromise(() =>
Expand Down Expand Up @@ -3049,6 +3071,7 @@ export default function Sidebar() {
confirmThreadDelete,
copyBranchToClipboard,
copyPathToClipboard,
copyThreadId,
deleteThread,
handleMultiSelectContextMenu,
markThreadUnread,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/threadActionMenu.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("buildThreadActionMenuItems", () => {
...baseState,
supports: { settlement: false, snooze: false, pinning: false, titleRegeneration: false },
}),
).toEqual(["rename", "mark-unread", "copy-path", "delete"]);
).toEqual(["rename", "mark-unread", "copy-path", "copy-thread-id", "delete"]);
});

it("includes branch items only for threads with a branch", () => {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/threadActionMenu.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ThreadActionMenuId =
| "mark-unread"
| "copy-path"
| "copy-branch"
| "copy-thread-id"
| "delete";

export interface ThreadActionMenuState {
Expand Down Expand Up @@ -100,6 +101,7 @@ export function buildThreadActionMenuItems(
{ id: "mark-unread", label: "Mark unread" },
{ id: "copy-path", label: "Copy path", icon: "copy" },
...(state.branch ? [{ id: "copy-branch" as const, label: "Copy branch", icon: "copy" }] : []),
{ id: "copy-thread-id", label: "Copy thread ID", icon: "copy" },
{ id: "delete", label: "Delete", destructive: true, icon: "trash" },
];
}
13 changes: 12 additions & 1 deletion apps/web/src/hooks/useThreadActionMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
effectiveSnoozed,
type ChangeRequestStateLike,
} from "@t3tools/client-runtime/state/thread-settled";
import type { ScopedThreadRef } from "@t3tools/contracts";
import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts";
import { useCallback } from "react";

import { resolveSnoozePresets, snoozeWakeDescription } from "../components/Sidebar.snooze";
Expand Down Expand Up @@ -95,6 +95,13 @@ export function useThreadActionMenu(input: {
},
onError: (error) => failureToast("Failed to copy branch", error),
});
const { copyToClipboard: copyThreadId } = useCopyToClipboard<{ threadId: ThreadId }>({
target: "thread ID",
onCopy: ({ threadId }) => {
toastManager.add({ type: "success", title: "Thread ID copied", description: threadId });
},
onError: (error) => failureToast("Failed to copy thread ID", error),
});

const openMenu = useCallback(
(position: { x: number; y: number }) => {
Expand Down Expand Up @@ -242,6 +249,9 @@ export function useThreadActionMenu(input: {
copyBranchToClipboard(thread.branch, { branch: thread.branch });
}
return;
case "copy-thread-id":
copyThreadId(threadRef.threadId, { threadId: threadRef.threadId });
return;
case "delete": {
if (confirmThreadDelete) {
const confirmed = await settlePromise(() =>
Expand Down Expand Up @@ -279,6 +289,7 @@ export function useThreadActionMenu(input: {
confirmThreadDelete,
copyBranchToClipboard,
copyPathToClipboard,
copyThreadId,
deleteThread,
handleNewThread,
markThreadUnread,
Expand Down
Loading