@opengeni/react 0.12.0 → 0.15.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.
- package/README.md +20 -14
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +1040 -363
- package/dist/index.js +10938 -6281
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +20 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +41 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +417 -69
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +93 -15
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +28 -19
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +21 -3
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +193 -225
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +957 -0
- package/src/components/session-status.tsx +22 -2
- package/src/components/workbench-changes.tsx +585 -0
- package/src/components/workspace-dock.tsx +213 -59
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +414 -63
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +50 -23
- package/src/hooks/use-session-events.ts +50 -21
- package/src/hooks/use-session-lineage.ts +128 -0
- package/src/hooks/use-session.ts +53 -30
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +202 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +133 -23
- package/src/lib/format.ts +3 -3
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +29 -3
- package/src/provider.tsx +195 -6
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +87 -16
- package/src/timeline/projection.ts +412 -87
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/index.css +22 -0
- package/styles/tokens.css +2 -2
- package/dist/chunk-5TIXRCSI.js +0 -1291
- package/dist/chunk-5TIXRCSI.js.map +0 -1
|
@@ -0,0 +1,578 @@
|
|
|
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";
|
|
33
|
+
|
|
34
|
+
import { DropdownMenu } from "radix-ui";
|
|
35
|
+
import type { ComposerState } from "../hooks/use-composer";
|
|
36
|
+
import type { QueueMutationKind, UseTurnQueueResult } from "../hooks/use-turn-queue";
|
|
37
|
+
|
|
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;
|
|
120
|
+
|
|
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;
|
|
131
|
+
|
|
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
|
+
}
|
|
145
|
+
|
|
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
|
+
);
|
|
159
|
+
|
|
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
|
+
);
|
|
181
|
+
|
|
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
|
+
);
|
|
198
|
+
|
|
199
|
+
if (count === 0 && !queue.error && !queue.mutationError) return null;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div
|
|
203
|
+
className="mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
|
|
204
|
+
data-testid="queue-surface"
|
|
205
|
+
>
|
|
206
|
+
<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" : ""}`}
|
|
215
|
+
/>
|
|
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>
|
|
356
|
+
</div>
|
|
357
|
+
</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>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
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
|
+
}
|