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" | "pin" | "unpin" | "settle" | "unsettle" | "snooze" | `snooze:${string}` | "unsnooze" | "rename" | "regenerate-title" | "mark-unread" | "copy-path" | "copy-branch" | "delete"; export interface ThreadActionMenuState { readonly branch: string | null; readonly isPinned: boolean; readonly isSettled: boolean; readonly isSnoozed: boolean; readonly canSnoozeNow: boolean; readonly isRegeneratingTitle: 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}`, }, ] : []), ...(state.supports.pinning ? [ state.isPinned ? { id: "unpin" as const, label: "Unpin thread" } : { id: "pin" as const, label: "Pin thread" }, ] : []), // 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" } : { id: "settle" as const, label: "Settle thread" }, ] : []), ...(state.supports.snooze ? [ state.isSnoozed ? { id: "unsnooze" as const, label: "Wake thread" } : { id: "snooze" as const, label: "Snooze", disabled: !state.canSnoozeNow, children: state.snoozePresets.map((preset) => ({ id: `snooze:${preset.id}` as const, label: `${preset.label} (${preset.whenLabel})`, })), }, ] : []), { id: "rename", label: "Rename thread" }, ...(state.supports.titleRegeneration ? [ { id: "regenerate-title" as const, label: state.isRegeneratingTitle ? "Regenerating…" : "Regenerate title", disabled: state.isRegeneratingTitle, }, ] : []), { 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: "delete", label: "Delete", destructive: true, icon: "trash" }, ];}
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"
| "pin"
| "unpin"
| "settle"
| "unsettle"
| "snooze"
| `snooze:${string}`
| "unsnooze"
| "rename"
| "regenerate-title"
| "mark-unread"
| "copy-path"
| "copy-branch"
| "delete";
export interface ThreadActionMenuState {
readonly branch: string | null;
readonly isPinned: boolean;
readonly isSettled: boolean;
readonly isSnoozed: boolean;
readonly canSnoozeNow: boolean;
readonly isRegeneratingTitle: 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}`,
},
]
: []),
...(state.supports.pinning
? [
state.isPinned
? { id: "unpin" as const, label: "Unpin thread" }
: { id: "pin" as const, label: "Pin thread" },
]
: []),
// 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" }
: { id: "settle" as const, label: "Settle thread" },
]
: []),
...(state.supports.snooze
? [
state.isSnoozed
? { id: "unsnooze" as const, label: "Wake thread" }
: {
id: "snooze" as const,
label: "Snooze",
disabled: !state.canSnoozeNow,
children: state.snoozePresets.map((preset) => ({
id: `snooze:${preset.id}` as const,
label: `${preset.label} (${preset.whenLabel})`,
})),
},
]
: []),
{ id: "rename", label: "Rename thread" },
...(state.supports.titleRegeneration
? [
{
id: "regenerate-title" as const,
label: state.isRegeneratingTitle ? "Regenerating…" : "Regenerate title",
disabled: state.isRegeneratingTitle,
},
]
: []),
{ 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: "delete", label: "Delete", destructive: true, icon: "trash" },
];
}