143 lines · 4.5 KB · TypeScript
RawDownload
import type { ContextMenuItem } from "@t3tools/contracts";import type { SnoozePreset } from "@t3tools/client-runtime/state/thread-settled"; /** * Ids for the per-thread action menu. Snooze presets are dispatched as * `snooze:<presetId>` so the union stays closed while the preset list * remains data-driven. */export type ThreadActionMenuId =  | "new-thread-on-branch"  | "project-settings"  | "pin"  | "unpin"  | "settle"  | "unsettle"  | "snooze"  | `snooze:${string}`  | "unsnooze"  | "rename"  | "regenerate-title"  | "mark-unread"  | "copy"  | "copy-path"  | "copy-branch"  | "copy-thread-id"  | "archive"  | "delete"; export interface ThreadActionMenuState {  readonly branch: string | null;  readonly isPinned: boolean;  readonly isSettled: boolean;  readonly isSnoozed: boolean;  readonly canSnoozeNow: boolean;  readonly isRegeneratingTitle: boolean;  /** Archive rejects a thread with an active turn, so disable it here rather than let the action fail. */  readonly isRunning: boolean;  readonly supports: {    readonly settlement: boolean;    readonly snooze: boolean;    readonly pinning: boolean;    readonly titleRegeneration: boolean;  };  readonly snoozePresets: ReadonlyArray<SnoozePreset>;} /** * Single source for the per-thread action menu: the sidebar row's right-click * menu and the chat header menu both render exactly this list, so labels, * ordering, and capability gating cannot drift between the two surfaces. */export function buildThreadActionMenuItems(  state: ThreadActionMenuState,): ReadonlyArray<ContextMenuItem<ThreadActionMenuId>> {  return [    ...(state.branch      ? [          {            id: "new-thread-on-branch" as const,            label: `New thread on ${state.branch}`,            icon: "message-square-plus",          },        ]      : []),    ...(state.supports.pinning      ? [          state.isPinned            ? { id: "unpin" as const, label: "Unpin thread", icon: "pin-off" }            : { id: "pin" as const, label: "Pin thread", icon: "pin" },        ]      : []),    // Both lifecycle actions stay available on pinned threads: settling    // clears the pin ("done" beats "keep on top"), and snoozing hides the    // card until wake with the pin intact.    ...(state.supports.settlement      ? [          state.isSettled            ? { id: "unsettle" as const, label: "Un-settle thread", icon: "circle-check" }            : { id: "settle" as const, label: "Settle thread", icon: "circle-check" },        ]      : []),    ...(state.supports.snooze      ? [          state.isSnoozed            ? { id: "unsnooze" as const, label: "Wake thread", icon: "clock" }            : {                id: "snooze" as const,                label: "Snooze",                icon: "clock",                disabled: !state.canSnoozeNow,                children: state.snoozePresets.map((preset) => ({                  id: `snooze:${preset.id}` as const,                  label: `${preset.label} (${preset.whenLabel})`,                })),              },        ]      : []),    { id: "rename", label: "Rename thread", icon: "pencil", separatorBefore: true },    ...(state.supports.titleRegeneration      ? [          {            id: "regenerate-title" as const,            label: state.isRegeneratingTitle ? "Regenerating…" : "Regenerate title",            icon: "refresh-cw",            disabled: state.isRegeneratingTitle,          },        ]      : []),    { id: "mark-unread", label: "Mark unread", icon: "mail-open" },    {      id: "copy",      label: "Copy",      icon: "copy",      separatorBefore: true,      children: [        { id: "copy-path", label: "Path", icon: "folder" },        ...(state.branch          ? [{ id: "copy-branch" as const, label: "Branch", icon: "git-branch" }]          : []),        { id: "copy-thread-id", label: "Thread ID", icon: "hash" },      ],    },    { id: "project-settings", label: "Project settings", icon: "settings" },    // Archive removes the thread from the sidebar while keeping its    // conversation under Settings > Archived threads — distinct from Settle    // (stays visible in the Settled shelf) and Delete (clears history for    // good), so it sits beside Delete without borrowing its destructive    // styling.    {      id: "archive",      label: "Archive thread",      icon: "archive",      disabled: state.isRunning,      separatorBefore: true,    },    {      id: "delete",      label: "Delete",      destructive: true,      icon: "trash",    },  ];}