@opengeni/react 0.15.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +110 -10
  2. package/dist/{chunk-TOJR776I.js → chunk-6BXBGJ3B.js} +45 -369
  3. package/dist/chunk-6BXBGJ3B.js.map +1 -0
  4. package/dist/chunk-6UT5OC3P.js +2133 -0
  5. package/dist/chunk-6UT5OC3P.js.map +1 -0
  6. package/dist/chunk-EBTT3MYN.js +92 -0
  7. package/dist/chunk-EBTT3MYN.js.map +1 -0
  8. package/dist/chunk-HHFA4AFX.js +425 -0
  9. package/dist/chunk-HHFA4AFX.js.map +1 -0
  10. package/dist/chunk-LAZ2A743.js +2393 -0
  11. package/dist/chunk-LAZ2A743.js.map +1 -0
  12. package/dist/chunk-MRSECXRP.js +563 -0
  13. package/dist/chunk-MRSECXRP.js.map +1 -0
  14. package/dist/chunk-TK7G6XLT.js +18 -0
  15. package/dist/chunk-TK7G6XLT.js.map +1 -0
  16. package/dist/composer-DvUaa5ki.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +1288 -1785
  21. package/dist/index.js +7015 -8806
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +3 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
  30. package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
  31. package/dist/session-Gd4iVPYw.d.ts +425 -0
  32. package/dist/session-context-DIFFlXKc.d.ts +52 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +50 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-BCfm4mzX.d.ts +112 -0
  37. package/package.json +20 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +40 -0
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +94 -993
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +1 -1
  51. package/src/components/machines-dashboard.tsx +3 -3
  52. package/src/components/message-timeline.tsx +15 -7
  53. package/src/components/model-picker.tsx +12 -3
  54. package/src/components/pierre-diff.tsx +32 -6
  55. package/src/components/queue-surface-implementation.tsx +1041 -0
  56. package/src/components/queue-surface-state.tsx +94 -0
  57. package/src/components/queue-surface.tsx +41 -559
  58. package/src/components/sandbox-files.tsx +138 -14
  59. package/src/components/sandbox-terminal.tsx +8 -2
  60. package/src/components/sandbox-workspace.tsx +339 -119
  61. package/src/components/workbench-changes.tsx +84 -33
  62. package/src/components/workspace-dock.tsx +188 -18
  63. package/src/composer.ts +60 -0
  64. package/src/hooks/internal.ts +115 -34
  65. package/src/hooks/use-composer.ts +335 -104
  66. package/src/hooks/use-human-input.ts +131 -0
  67. package/src/hooks/use-machine-chip.ts +2 -2
  68. package/src/hooks/use-machines.ts +27 -11
  69. package/src/hooks/use-sandbox-files.ts +310 -62
  70. package/src/hooks/use-sandbox-git.ts +154 -40
  71. package/src/hooks/use-sandbox-terminal.ts +28 -6
  72. package/src/hooks/use-session-capabilities.ts +38 -9
  73. package/src/hooks/use-session-control.ts +37 -8
  74. package/src/hooks/use-session-events.ts +522 -48
  75. package/src/hooks/use-slash-commands.ts +38 -38
  76. package/src/hooks/use-transcription.ts +757 -0
  77. package/src/hooks/use-turn-queue.ts +134 -66
  78. package/src/hooks/use-windowed-sections.ts +2 -2
  79. package/src/hooks/use-workspace-capture.ts +146 -40
  80. package/src/hooks/use-workspace-edit.ts +50 -14
  81. package/src/hooks/use-workspace-sessions.ts +3 -0
  82. package/src/human-input.ts +72 -0
  83. package/src/index.ts +76 -6
  84. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  85. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  86. package/src/lib/use-portal-token-style.ts +51 -0
  87. package/src/lib/use-unicode-fonts.ts +57 -0
  88. package/src/provider.tsx +4 -69
  89. package/src/session-context.ts +135 -0
  90. package/src/session.ts +79 -0
  91. package/src/timeline/parsers.ts +35 -0
  92. package/src/timeline/projection.ts +12 -4
  93. package/src/timeline/tool-renderers.tsx +41 -0
  94. package/src/timeline/turn-summary.tsx +2 -2
  95. package/src/timeline/types.ts +16 -8
  96. package/styles/index.css +56 -0
  97. package/styles/index.d.ts +2 -0
  98. package/styles/tokens.css +11 -6
  99. package/styles/tokens.d.ts +2 -0
  100. package/dist/chunk-TOJR776I.js.map +0 -1
  101. package/dist/machines-BpdwuQcD.d.ts +0 -449
@@ -1,578 +1,60 @@
1
- import {
2
- DndContext,
3
- DragOverlay,
4
- PointerSensor,
5
- closestCenter,
6
- useSensor,
7
- useSensors,
8
- type DragEndEvent,
9
- type DragStartEvent,
10
- type Modifier,
11
- } from "@dnd-kit/core";
12
- import {
13
- SortableContext,
14
- arrayMove,
15
- useSortable,
16
- verticalListSortingStrategy,
17
- } from "@dnd-kit/sortable";
18
- import { CSS } from "@dnd-kit/utilities";
19
- import type { SessionTurn } from "@opengeni/sdk";
20
- import {
21
- ArrowDownToLineIcon,
22
- ArrowUpToLineIcon,
23
- ChevronDownIcon,
24
- EllipsisIcon,
25
- GripVerticalIcon,
26
- Loader2Icon,
27
- PencilIcon,
28
- RotateCwIcon,
29
- Trash2Icon,
30
- ZapIcon,
31
- } from "lucide-react";
32
- import { useCallback, useMemo, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
1
+ import { Loader2Icon } from "lucide-react";
2
+ import { lazy, Suspense, type ComponentType } from "react";
33
3
 
34
- import { DropdownMenu } from "radix-ui";
35
- import type { ComposerState } from "../hooks/use-composer";
36
- import type { QueueMutationKind, UseTurnQueueResult } from "../hooks/use-turn-queue";
4
+ import type { QueueSurfaceProps } from "./queue-surface-implementation";
5
+ import { EmptyQueueStateSurface } from "./queue-surface-state";
37
6
 
38
- /** The sole human prompt queue: compact above Goal, Agents, and composer. */
39
- export type QueueSurfaceProps =
40
- | {
41
- queue: UseTurnQueueResult;
42
- composer: ComposerState;
43
- readOnly?: false | undefined;
44
- }
45
- | {
46
- queue: UseTurnQueueResult;
47
- composer?: undefined;
48
- readOnly: true;
49
- };
50
-
51
- export function QueueSurface({ queue, composer, readOnly = false }: QueueSurfaceProps) {
52
- const [open, setOpen] = useState(false);
53
- const [replaceDraftFor, setReplaceDraftFor] = useState<string | null>(null);
54
- const [announcement, setAnnouncement] = useState("");
55
- const [draggedTurnId, setDraggedTurnId] = useState<string | null>(null);
56
- const [keyboardDrag, setKeyboardDrag] = useState<{
57
- turnId: string;
58
- projectedIndex: number;
59
- } | null>(null);
60
- const count = queue.queue.length;
61
- const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
62
-
63
- const displayedQueue = useMemo(() => {
64
- if (!keyboardDrag) return queue.queue;
65
- const oldIndex = queue.queue.findIndex((turn) => turn.id === keyboardDrag.turnId);
66
- if (oldIndex < 0) return queue.queue;
67
- return arrayMove(queue.queue, oldIndex, keyboardDrag.projectedIndex);
68
- }, [keyboardDrag, queue.queue]);
69
-
70
- const ids = useMemo(() => displayedQueue.map((turn) => turn.id), [displayedQueue]);
71
- const moveToIndex = useCallback(
72
- async (turnId: string, nextIndex: number): Promise<void> => {
73
- const oldIndex = queue.queue.findIndex((turn) => turn.id === turnId);
74
- if (oldIndex < 0) return;
75
- const boundedIndex = Math.max(0, Math.min(nextIndex, queue.queue.length - 1));
76
- if (oldIndex === boundedIndex) return;
77
- const ordered = arrayMove(queue.queue, oldIndex, boundedIndex);
78
- const beforeTurnId = ordered[boundedIndex + 1]?.id ?? null;
79
- const moved = await queue.moveTurn(turnId, beforeTurnId);
80
- setAnnouncement(
81
- moved
82
- ? `Queued prompt moved to position ${boundedIndex + 1}.`
83
- : "The queue changed before that prompt could be moved. Refreshed server order.",
84
- );
85
- if (moved) focusQueueTurn(turnId);
86
- },
87
- [queue],
88
- );
89
-
90
- const onDragEnd = useCallback(
91
- (event: DragEndEvent) => {
92
- setDraggedTurnId(null);
93
- const activeId = String(event.active.id);
94
- const overId = event.over ? String(event.over.id) : null;
95
- if (!overId || activeId === overId) {
96
- setAnnouncement("Queued prompt returned to its original position.");
97
- return;
98
- }
99
- const nextIndex = queue.queue.findIndex((turn) => turn.id === overId);
100
- if (nextIndex >= 0) void moveToIndex(activeId, nextIndex);
101
- },
102
- [moveToIndex, queue.queue],
103
- );
104
-
105
- const onDragStart = useCallback(
106
- (event: DragStartEvent) => {
107
- setKeyboardDrag(null);
108
- const turnId = String(event.active.id);
109
- const position = queue.queue.findIndex((turn) => turn.id === turnId) + 1;
110
- setDraggedTurnId(turnId);
111
- setAnnouncement(`Dragging queued prompt ${position} of ${queue.queue.length}.`);
112
- },
113
- [queue.queue],
114
- );
115
-
116
- const onHandleKeyDown = useCallback(
117
- (event: ReactKeyboardEvent<HTMLButtonElement>, turnId: string) => {
118
- const canonicalIndex = queue.queue.findIndex((turn) => turn.id === turnId);
119
- if (canonicalIndex < 0) return;
7
+ export type { QueueSurfaceProps } from "./queue-surface-implementation";
120
8
 
121
- if (!keyboardDrag) {
122
- if (event.key !== " ") return;
123
- event.preventDefault();
124
- setKeyboardDrag({ turnId, projectedIndex: canonicalIndex });
125
- setAnnouncement(
126
- `Lifted queued prompt ${canonicalIndex + 1} of ${count}. Use arrow keys to move it, then press Space to drop.`,
127
- );
128
- return;
129
- }
130
- if (keyboardDrag.turnId !== turnId) return;
9
+ const loadQueueSurface = () => import("./queue-surface-implementation");
10
+ type QueueSurfaceModule = { QueueSurface: ComponentType<QueueSurfaceProps> };
11
+ type QueueSurfaceLoader = () => Promise<QueueSurfaceModule>;
131
12
 
132
- if (event.key === "Escape") {
133
- event.preventDefault();
134
- setKeyboardDrag(null);
135
- setAnnouncement("Queue reorder cancelled.");
136
- return;
137
- }
138
- if (event.key === " ") {
139
- event.preventDefault();
140
- const targetIndex = keyboardDrag.projectedIndex;
141
- setAnnouncement(`Moving queued prompt to position ${targetIndex + 1}.`);
142
- void moveToIndex(turnId, targetIndex).finally(() => setKeyboardDrag(null));
143
- return;
144
- }
13
+ /** The sole human prompt queue: compact above Goal, Agents, and composer. */
14
+ export const QueueSurface = createQueueSurface(loadQueueSurface);
145
15
 
146
- const direction = event.key === "ArrowUp" ? -1 : event.key === "ArrowDown" ? 1 : 0;
147
- const edge = event.key === "Home" ? 0 : event.key === "End" ? count - 1 : null;
148
- if (direction === 0 && edge === null) return;
149
- event.preventDefault();
150
- const projectedIndex = Math.max(
151
- 0,
152
- Math.min(edge ?? keyboardDrag.projectedIndex + direction, count - 1),
153
- );
154
- setKeyboardDrag({ turnId, projectedIndex });
155
- setAnnouncement(`Queued prompt projected to position ${projectedIndex + 1} of ${count}.`);
156
- },
157
- [count, keyboardDrag, moveToIndex, queue.queue],
158
- );
16
+ /** @internal Deterministic Suspense seam for the QueueSurface regression suite. */
17
+ export function createQueueSurfaceForTest(loadImplementation: QueueSurfaceLoader) {
18
+ return createQueueSurface(loadImplementation);
19
+ }
159
20
 
160
- const edit = useCallback(
161
- async (turn: SessionTurn, replaceDraft: boolean) => {
162
- if (!composer || readOnly) return;
163
- const restored = await queue.editTurn(turn.id, {
164
- expectedDraftRevision: composer.draftRevision,
165
- replaceDraft,
166
- });
167
- if (!restored) return;
168
- composer.applyDraft(restored);
169
- setReplaceDraftFor(null);
170
- setAnnouncement("Queued prompt moved back to the composer for editing.");
171
- window.requestAnimationFrame(() => {
172
- const input = document.querySelector<HTMLTextAreaElement>(
173
- 'textarea[aria-label="Message the agent"]',
174
- );
175
- input?.scrollIntoView({ block: "nearest", behavior: "smooth" });
176
- input?.focus();
177
- });
178
- },
179
- [composer, queue, readOnly],
180
- );
21
+ function createQueueSurface(loadImplementation: QueueSurfaceLoader) {
22
+ const LazyQueueSurface = lazy(async () => ({
23
+ default: (await loadImplementation()).QueueSurface,
24
+ }));
181
25
 
182
- const requestEdit = useCallback(
183
- (turn: SessionTurn) => {
184
- if (!composer || readOnly) return;
185
- const draftDirty =
186
- composer.value.length > 0 ||
187
- composer.restoredResources.length > 0 ||
188
- (composer.draft?.tools.length ?? 0) > 0 ||
189
- (composer.draft?.sourceTurnId !== null && composer.draft?.sourceTurnId !== undefined);
190
- if (draftDirty) {
191
- setReplaceDraftFor(turn.id);
192
- } else {
193
- void edit(turn, false);
194
- }
195
- },
196
- [composer, edit, readOnly],
197
- );
26
+ return function QueueSurfaceBoundary(props: QueueSurfaceProps) {
27
+ const { queue } = props;
28
+ if (queue.queue.length === 0) {
29
+ if (!queue.stoppingPreviousAttempt && !queue.error && !queue.mutationError) return null;
30
+ return <EmptyQueueStateSurface queue={queue} />;
31
+ }
198
32
 
199
- if (count === 0 && !queue.error && !queue.mutationError) return null;
33
+ return (
34
+ <Suspense fallback={<QueueSurfaceFallback count={queue.queue.length} />}>
35
+ <LazyQueueSurface {...props} />
36
+ </Suspense>
37
+ );
38
+ };
39
+ }
200
40
 
41
+ function QueueSurfaceFallback({ count }: { count: number }) {
201
42
  return (
202
43
  <div
44
+ aria-live="polite"
203
45
  className="mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
204
- data-testid="queue-surface"
46
+ data-testid="queue-surface-loading"
47
+ role="status"
205
48
  >
206
49
  <div className="overflow-hidden rounded-lg border border-border bg-surface/80 shadow-sm">
207
- <button
208
- type="button"
209
- className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left outline-none transition-colors hover:bg-surface-2/60 focus-visible:bg-surface-2/60 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 pointer-coarse:min-h-11"
210
- onClick={() => setOpen((value) => !value)}
211
- aria-expanded={open}
212
- >
213
- <ChevronDownIcon
214
- className={`size-3.5 shrink-0 text-fg-subtle transition-transform ${open ? "rotate-180" : ""}`}
50
+ <div className="flex items-center gap-2 px-3 py-2 text-xs text-fg-muted pointer-coarse:min-h-[44px]">
51
+ <Loader2Icon
52
+ aria-hidden="true"
53
+ className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none"
215
54
  />
216
- <span className="text-xs font-medium text-fg">
217
- {count} queued prompt{count === 1 ? "" : "s"}
218
- </span>
219
- {readOnly ? (
220
- <span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-2xs text-fg-subtle">
221
- Read-only
222
- </span>
223
- ) : null}
224
- {!open && count > 0 ? (
225
- <span className="min-w-0 flex-1 truncate text-xs text-fg-muted">
226
- {queue.queue[0]?.prompt}
227
- </span>
228
- ) : null}
229
- {queue.loading ? <Loader2Icon className="ml-auto size-3.5 animate-spin" /> : null}
230
- </button>
231
-
232
- {open && count > 0 && readOnly ? (
233
- <ol className="divide-y divide-border border-t border-border" aria-label="Queued prompts">
234
- {queue.queue.map((turn, index) => (
235
- <ReadOnlyQueueRow key={turn.id} turn={turn} index={index} />
236
- ))}
237
- </ol>
238
- ) : null}
239
-
240
- {open && count > 0 && !readOnly ? (
241
- <DndContext
242
- sensors={sensors}
243
- collisionDetection={closestCenter}
244
- modifiers={[verticalOnly]}
245
- onDragStart={onDragStart}
246
- onDragCancel={() => {
247
- setDraggedTurnId(null);
248
- setAnnouncement("Queue reorder cancelled.");
249
- }}
250
- onDragEnd={onDragEnd}
251
- >
252
- <SortableContext items={ids} strategy={verticalListSortingStrategy}>
253
- <ol
254
- className="divide-y divide-border border-t border-border"
255
- aria-label="Queued prompts"
256
- >
257
- {displayedQueue.map((turn, index) => (
258
- <SortableQueueRow
259
- key={turn.id}
260
- turn={turn}
261
- index={index}
262
- count={count}
263
- pending={queue.mutationFor(turn.id)}
264
- confirmingReplace={replaceDraftFor === turn.id}
265
- keyboardDragging={keyboardDrag?.turnId === turn.id}
266
- onHandleKeyDown={(event) => onHandleKeyDown(event, turn.id)}
267
- onMove={(nextIndex) => void moveToIndex(turn.id, nextIndex)}
268
- onEdit={() => requestEdit(turn)}
269
- onConfirmReplace={() => void edit(turn, true)}
270
- onCancelReplace={() => setReplaceDraftFor(null)}
271
- onSteer={() => {
272
- void queue.steerTurn(turn.id).then((steered) => {
273
- setAnnouncement(
274
- steered
275
- ? "Queued prompt is now the next direction."
276
- : "That prompt changed before it could be steered.",
277
- );
278
- if (steered) focusAfterQueueRemoval(index);
279
- });
280
- }}
281
- onDelete={() => {
282
- void queue.removeTurn(turn.id).then((removed) => {
283
- setAnnouncement(
284
- removed
285
- ? "Queued prompt deleted."
286
- : "That prompt changed before it could be deleted.",
287
- );
288
- if (removed) focusAfterQueueRemoval(index);
289
- });
290
- }}
291
- />
292
- ))}
293
- </ol>
294
- </SortableContext>
295
- <DragOverlay modifiers={[verticalOnly]}>
296
- {draggedTurnId ? (
297
- <div className="max-w-xl rounded-md border border-brand/40 bg-surface px-3 py-2 text-xs text-fg shadow-lg">
298
- {queue.queue.find((turn) => turn.id === draggedTurnId)?.prompt}
299
- </div>
300
- ) : null}
301
- </DragOverlay>
302
- </DndContext>
303
- ) : null}
304
-
305
- {queue.error || queue.mutationError ? (
306
- <div className="border-t border-border p-2">
307
- <div
308
- role="alert"
309
- className="flex items-center gap-2 rounded-md bg-status-failed/10 px-2 py-1.5 text-xs text-status-failed"
310
- >
311
- <span className="min-w-0 flex-1">
312
- {(queue.mutationError ?? queue.error)?.message}
313
- </span>
314
- <button
315
- type="button"
316
- onClick={() => {
317
- queue.clearMutationError();
318
- void queue.refresh();
319
- }}
320
- aria-label="Dismiss queue error and retry"
321
- title="Retry loading the queue"
322
- className="inline-flex size-7 items-center justify-center rounded-md outline-none transition-colors hover:bg-surface-3 focus-visible:ring-2 focus-visible:ring-ring/40 pointer-coarse:size-11"
323
- >
324
- <RotateCwIcon className="size-3.5" />
325
- </button>
326
- </div>
327
- </div>
328
- ) : null}
329
- </div>
330
- <p className="sr-only" aria-live="polite" aria-atomic="true">
331
- {announcement}
332
- </p>
333
- </div>
334
- );
335
- }
336
-
337
- function ReadOnlyQueueRow({ turn, index }: { turn: SessionTurn; index: number }) {
338
- return (
339
- <li className="flex min-w-0 items-start gap-2 bg-surface px-3 py-2">
340
- <span className="mt-1 shrink-0 font-mono text-2xs text-fg-subtle">{index + 1}</span>
341
- <div className="min-w-0 flex-1">
342
- <p className="whitespace-pre-wrap break-words text-xs leading-5 text-fg">{turn.prompt}</p>
343
- <div className="mt-1 flex flex-wrap gap-x-2 gap-y-0.5 text-2xs text-fg-subtle">
344
- {turn.resources.length > 0 ? (
345
- <span>
346
- {turn.resources.length} resource{turn.resources.length === 1 ? "" : "s"}
347
- </span>
348
- ) : null}
349
- {turn.tools.length > 0 ? (
350
- <span>
351
- {turn.tools.length} tool{turn.tools.length === 1 ? "" : "s"}
352
- </span>
353
- ) : null}
354
- <span>{turn.model}</span>
355
- <span>{turn.reasoningEffort}</span>
55
+ Loading {count} queued prompt{count === 1 ? "" : "s"}…
356
56
  </div>
357
57
  </div>
358
- </li>
359
- );
360
- }
361
-
362
- function SortableQueueRow({
363
- turn,
364
- index,
365
- count,
366
- pending,
367
- confirmingReplace,
368
- keyboardDragging,
369
- onHandleKeyDown,
370
- onMove,
371
- onEdit,
372
- onConfirmReplace,
373
- onCancelReplace,
374
- onSteer,
375
- onDelete,
376
- }: {
377
- turn: SessionTurn;
378
- index: number;
379
- count: number;
380
- pending: QueueMutationKind | null;
381
- confirmingReplace: boolean;
382
- keyboardDragging: boolean;
383
- onHandleKeyDown: (event: ReactKeyboardEvent<HTMLButtonElement>) => void;
384
- onMove: (index: number) => void;
385
- onEdit: () => void;
386
- onConfirmReplace: () => void;
387
- onCancelReplace: () => void;
388
- onSteer: () => void;
389
- onDelete: () => void;
390
- }) {
391
- const sortable = useSortable({
392
- id: turn.id,
393
- disabled: pending !== null || keyboardDragging,
394
- });
395
- return (
396
- <li
397
- data-queue-turn-id={turn.id}
398
- ref={sortable.setNodeRef}
399
- style={{
400
- transform: CSS.Transform.toString(sortable.transform),
401
- transition: sortable.transition,
402
- }}
403
- className={`bg-surface ${sortable.isDragging || keyboardDragging ? "relative z-10 shadow-lg ring-1 ring-brand/40" : ""}`}
404
- >
405
- <div className="flex min-w-0 items-start gap-1.5 px-2 py-2 sm:gap-2 sm:px-3">
406
- <button
407
- data-queue-handle
408
- ref={sortable.setActivatorNodeRef}
409
- type="button"
410
- {...sortable.attributes}
411
- {...sortable.listeners}
412
- onKeyDown={onHandleKeyDown}
413
- disabled={pending !== null}
414
- className="mt-0.5 inline-flex size-7 shrink-0 touch-none items-center justify-center rounded-md text-fg-subtle hover:bg-surface-2 hover:text-fg focus-visible:ring-2 focus-visible:ring-ring/40 pointer-coarse:size-11"
415
- aria-label={`Reorder queued prompt ${index + 1}`}
416
- title="Drag to reorder. Press Space, arrows, then Space to drop."
417
- >
418
- <GripVerticalIcon className="size-3.5" />
419
- </button>
420
- <span className="mt-1 shrink-0 font-mono text-2xs text-fg-subtle">{index + 1}</span>
421
- <div className="min-w-0 flex-1">
422
- <p className="whitespace-pre-wrap break-words text-xs leading-5 text-fg">{turn.prompt}</p>
423
- <div className="mt-1 flex flex-wrap gap-x-2 gap-y-0.5 text-2xs text-fg-subtle">
424
- {turn.resources.length > 0 ? (
425
- <span>
426
- {turn.resources.length} resource{turn.resources.length === 1 ? "" : "s"}
427
- </span>
428
- ) : null}
429
- {turn.tools.length > 0 ? (
430
- <span>
431
- {turn.tools.length} tool{turn.tools.length === 1 ? "" : "s"}
432
- </span>
433
- ) : null}
434
- <span>{turn.model}</span>
435
- <span>{turn.reasoningEffort}</span>
436
- </div>
437
- </div>
438
- <button
439
- type="button"
440
- disabled={pending !== null}
441
- onClick={onSteer}
442
- aria-label={`Steer queued prompt ${index + 1}`}
443
- title="Make this the next direction"
444
- className="inline-flex h-7 shrink-0 items-center justify-center gap-1 rounded-md px-2 text-xs font-medium outline-none transition-colors hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 pointer-coarse:min-h-11"
445
- >
446
- {pending === "steer" ? (
447
- <Loader2Icon className="size-3.5 animate-spin" />
448
- ) : (
449
- <ZapIcon className="size-3.5" />
450
- )}
451
- <span className="hidden sm:inline">Steer</span>
452
- </button>
453
- <button
454
- type="button"
455
- disabled={pending !== null}
456
- onClick={onDelete}
457
- aria-label={`Delete queued prompt ${index + 1}`}
458
- title="Delete this queued prompt"
459
- className="inline-flex size-7 shrink-0 items-center justify-center rounded-md outline-none transition-colors hover:bg-surface-2 hover:text-status-failed focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 pointer-coarse:size-11"
460
- >
461
- {pending === "delete" ? (
462
- <Loader2Icon className="size-3.5 animate-spin" />
463
- ) : (
464
- <Trash2Icon className="size-3.5" />
465
- )}
466
- </button>
467
- <DropdownMenu.Root>
468
- <DropdownMenu.Trigger asChild>
469
- <button
470
- type="button"
471
- disabled={pending !== null}
472
- aria-label={`More actions for queued prompt ${index + 1}`}
473
- className="inline-flex size-7 shrink-0 items-center justify-center rounded-md outline-none transition-colors hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 pointer-coarse:size-11"
474
- >
475
- {pending && pending !== "steer" && pending !== "delete" ? (
476
- <Loader2Icon className="size-3.5 animate-spin" />
477
- ) : (
478
- <EllipsisIcon className="size-3.5" />
479
- )}
480
- </button>
481
- </DropdownMenu.Trigger>
482
- <DropdownMenu.Portal>
483
- <DropdownMenu.Content
484
- align="end"
485
- sideOffset={4}
486
- className="z-50 w-48 rounded-md border border-border bg-surface p-1 text-xs text-fg shadow-lg"
487
- >
488
- <DropdownMenu.Item
489
- className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 outline-none focus:bg-surface-2"
490
- onSelect={onEdit}
491
- >
492
- <PencilIcon className="size-3.5" /> Edit in composer
493
- </DropdownMenu.Item>
494
- <DropdownMenu.Separator className="my-1 h-px bg-border" />
495
- <DropdownMenu.Item
496
- className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 outline-none focus:bg-surface-2 data-[disabled]:opacity-50"
497
- disabled={index === 0}
498
- onSelect={() => onMove(0)}
499
- >
500
- <ArrowUpToLineIcon className="size-3.5" /> Move to top
501
- </DropdownMenu.Item>
502
- <DropdownMenu.Item
503
- className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 outline-none focus:bg-surface-2 data-[disabled]:opacity-50"
504
- disabled={index === 0}
505
- onSelect={() => onMove(index - 1)}
506
- >
507
- Move up
508
- </DropdownMenu.Item>
509
- <DropdownMenu.Item
510
- className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 outline-none focus:bg-surface-2 data-[disabled]:opacity-50"
511
- disabled={index === count - 1}
512
- onSelect={() => onMove(index + 1)}
513
- >
514
- Move down
515
- </DropdownMenu.Item>
516
- <DropdownMenu.Item
517
- className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 outline-none focus:bg-surface-2 data-[disabled]:opacity-50"
518
- disabled={index === count - 1}
519
- onSelect={() => onMove(count - 1)}
520
- >
521
- <ArrowDownToLineIcon className="size-3.5" /> Move to bottom
522
- </DropdownMenu.Item>
523
- </DropdownMenu.Content>
524
- </DropdownMenu.Portal>
525
- </DropdownMenu.Root>
526
- </div>
527
- {confirmingReplace ? (
528
- <div className="mx-3 mb-2 rounded-md border border-status-waiting/30 bg-status-waiting/10 p-2 text-xs text-fg">
529
- <p>Your composer already has a draft. Replace it with this queued prompt?</p>
530
- <p className="mt-0.5 text-fg-muted">
531
- The current draft will be permanently discarded; this queued prompt is preserved until
532
- you confirm.
533
- </p>
534
- <div className="mt-2 flex justify-end gap-1.5">
535
- <button
536
- type="button"
537
- className="rounded-md px-2 py-1 font-medium hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring/40"
538
- onClick={onCancelReplace}
539
- >
540
- Keep current draft
541
- </button>
542
- <button
543
- type="button"
544
- className="rounded-md bg-brand px-2 py-1 font-medium text-white hover:bg-brand/90 focus-visible:ring-2 focus-visible:ring-ring/40"
545
- onClick={onConfirmReplace}
546
- >
547
- Replace and edit
548
- </button>
549
- </div>
550
- </div>
551
- ) : null}
552
- </li>
58
+ </div>
553
59
  );
554
60
  }
555
-
556
- const verticalOnly: Modifier = ({ transform }) => ({ ...transform, x: 0 });
557
-
558
- function focusQueueTurn(turnId: string): void {
559
- window.requestAnimationFrame(() => {
560
- document
561
- .querySelector<HTMLElement>(`[data-queue-turn-id="${turnId}"] [data-queue-handle]`)
562
- ?.focus();
563
- });
564
- }
565
-
566
- function focusAfterQueueRemoval(previousIndex: number): void {
567
- window.requestAnimationFrame(() => {
568
- const handles = document.querySelectorAll<HTMLElement>("[data-queue-handle]");
569
- const nearest = handles[Math.min(previousIndex, Math.max(0, handles.length - 1))];
570
- if (nearest) {
571
- nearest.focus();
572
- return;
573
- }
574
- document
575
- .querySelector<HTMLTextAreaElement>('textarea[aria-label="Message the agent"]')
576
- ?.focus();
577
- });
578
- }