1import type { EnvironmentId, RunId, ThreadId } from "@t3tools/contracts";
2
3export const REMOVE_QUEUED_MESSAGE_ACCESSIBILITY_LABEL = "Remove queued message";
4
5export interface ThreadQueueRowControls {
6 readonly canDismiss: boolean;
7 readonly canEdit: boolean;
8 readonly canMoveDown: boolean;
9 readonly canMoveUp: boolean;
10 readonly canSteer: boolean;
11 readonly dismissAccessibilityLabel: string;
12 readonly displayText: string;
13 readonly isEditing: boolean;
14}
15
16export function resolveThreadQueueRowControls(input: {
17 readonly busy: boolean;
18 readonly canPromoteToSteer: boolean;
19 readonly canReorder: boolean;
20 readonly index: number;
21 /** This row's message is already open in the composer. */
22 readonly isEditing?: boolean;
23 readonly queuedCount: number;
24 readonly text: string;
25}): ThreadQueueRowControls {
26 const mutationEnabled = !input.busy;
27 const isEditing = input.isEditing === true;
28
29 return {
30 canDismiss: !input.busy,
31 // Re-opening the row already in the composer would reload it and throw
32 // away whatever has been typed since.
33 canEdit: mutationEnabled && !isEditing,
34 canMoveDown: mutationEnabled && input.canReorder && input.index < input.queuedCount - 1,
35 canMoveUp: mutationEnabled && input.canReorder && input.index > 0,
36 canSteer: mutationEnabled && input.canPromoteToSteer && !isEditing,
37 dismissAccessibilityLabel: REMOVE_QUEUED_MESSAGE_ACCESSIBILITY_LABEL,
38 displayText: input.text,
39 isEditing,
40 };
41}
42
43export function buildCancelQueuedRunCommand(input: {
44 readonly environmentId: EnvironmentId;
45 readonly runId: RunId;
46 readonly threadId: ThreadId;
47}): {
48 readonly environmentId: EnvironmentId;
49 readonly input: {
50 readonly runId: RunId;
51 readonly threadId: ThreadId;
52 };
53} {
54 return {
55 environmentId: input.environmentId,
56 input: {
57 runId: input.runId,
58 threadId: input.threadId,
59 },
60 };
61}
62
63/** Find the insertion anchor using the rows' original, untransformed layout. */
64export function resolveQueueDragBeforeRunId(
65 rows: ReadonlyArray<{ id: RunId; y?: number; height?: number }>,
66 runId: RunId,
67 translationY: number,
68): RunId | null | undefined {
69 const sourceIndex = rows.findIndex((row) => row.id === runId);
70 const source = rows[sourceIndex];
71 if (!source || rows.some((row) => row.y === undefined || row.height === undefined)) return;
72 const center = source.y! + source.height! / 2 + translationY;
73 const remaining = rows.filter((row) => row.id !== runId);
74 return remaining.find((row) => center < row.y! + row.height! / 2)?.id ?? null;
75}
76
77/** Return the insertion anchor after a drag, or undefined when the order is unchanged. */
78export function resolveQueueDropBeforeRunId(
79 rows: ReadonlyArray<{ id: RunId; y?: number; height?: number }>,
80 runId: RunId,
81 translationY: number,
82): RunId | null | undefined {
83 const before = resolveQueueDragBeforeRunId(rows, runId, translationY);
84 if (before === undefined) return;
85 const sourceIndex = rows.findIndex((row) => row.id === runId);
86 return before === (rows[sourceIndex + 1]?.id ?? null) ? undefined : before;
87}