@opengeni/react 0.57.0 → 2.2.0-canary.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 +36 -11
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
- package/dist/chunk-5RZN3DD5.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
- package/dist/chunk-UOLJWT5Y.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
- package/dist/chunk-XSA2QAKP.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +10 -3
- package/dist/realtime.js +138 -29
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +48 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +91 -55
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +162 -17
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +200 -37
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +476 -23
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +75 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
* Inbox has no product dismiss API; pass `onDismissIncoming` when the host
|
|
29
29
|
* wants a visible action (dev harness may use a local dummy).
|
|
30
30
|
*
|
|
31
|
-
* Segment switches keep the panel shell mounted and crossfade content
|
|
32
|
-
*
|
|
31
|
+
* Segment switches keep the panel shell mounted and crossfade content. The
|
|
32
|
+
* shell uses one CSS grid-track transition for deliberate open/close actions;
|
|
33
|
+
* live queue reconciliation never feeds measurements back into layout.
|
|
33
34
|
*/
|
|
34
35
|
import type { SessionGoal, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
|
|
35
36
|
import {
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
ArrowDownIcon,
|
|
38
39
|
ArrowUpIcon,
|
|
39
40
|
BotIcon,
|
|
41
|
+
CheckIcon,
|
|
40
42
|
InboxIcon,
|
|
41
43
|
ListOrderedIcon,
|
|
42
44
|
Loader2Icon,
|
|
@@ -49,20 +51,13 @@ import {
|
|
|
49
51
|
ZapIcon,
|
|
50
52
|
} from "lucide-react";
|
|
51
53
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
52
|
-
import {
|
|
53
|
-
useEffect,
|
|
54
|
-
useId,
|
|
55
|
-
useLayoutEffect,
|
|
56
|
-
useMemo,
|
|
57
|
-
useRef,
|
|
58
|
-
useState,
|
|
59
|
-
type ReactNode,
|
|
60
|
-
} from "react";
|
|
54
|
+
import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
|
|
61
55
|
|
|
62
|
-
import type { ComposerState } from "../hooks/use-composer";
|
|
56
|
+
import type { ComposerOptimisticMessage, ComposerState } from "../hooks/use-composer";
|
|
63
57
|
import type { UseGoalResult } from "../hooks/use-goal";
|
|
64
58
|
import type { UseTurnQueueResult } from "../hooks/use-turn-queue";
|
|
65
59
|
import { cn } from "../lib/cn";
|
|
60
|
+
import { formatClockTime } from "../lib/format";
|
|
66
61
|
import { requestQueueDraftEdit } from "./queue-draft-policy";
|
|
67
62
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
68
63
|
|
|
@@ -100,6 +95,7 @@ export type SessionChromeProps = {
|
|
|
100
95
|
|
|
101
96
|
type GoalPillState =
|
|
102
97
|
| "pursuing"
|
|
98
|
+
| "waiting"
|
|
103
99
|
| "scheduled"
|
|
104
100
|
| "blocked"
|
|
105
101
|
| "held"
|
|
@@ -114,6 +110,7 @@ type QueuedTurnPresentation = {
|
|
|
114
110
|
|
|
115
111
|
const GOAL_LABEL: Record<GoalPillState, string> = {
|
|
116
112
|
pursuing: "Pursuing",
|
|
113
|
+
waiting: "Waiting",
|
|
117
114
|
scheduled: "Scheduled",
|
|
118
115
|
blocked: "Blocked",
|
|
119
116
|
held: "Held",
|
|
@@ -122,6 +119,74 @@ const GOAL_LABEL: Record<GoalPillState, string> = {
|
|
|
122
119
|
completed: "Done",
|
|
123
120
|
};
|
|
124
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Short pill suffix per `pausedReason`. `max_auto_continuations` is pacing
|
|
124
|
+
* (new input resumes it); `limits` is budget/admission; `user_pause`/`api` is
|
|
125
|
+
* the human's own override; `agent` is the model declaring it is blocked on a
|
|
126
|
+
* human decision. Unknown or legacy reasons keep the bare "Paused".
|
|
127
|
+
*/
|
|
128
|
+
const GOAL_PAUSED_REASON_SUFFIX: Record<string, string> = {
|
|
129
|
+
max_auto_continuations: "cap",
|
|
130
|
+
limits: "budget",
|
|
131
|
+
user_pause: "manually",
|
|
132
|
+
api: "manually",
|
|
133
|
+
agent: "agent",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const GOAL_PAUSED_REASON_EXPLANATION: Record<string, string> = {
|
|
137
|
+
max_auto_continuations:
|
|
138
|
+
"Paused at the automatic continuation cap. New input (a child result, an agent message, or your prompt) resumes it; you can also resume it here.",
|
|
139
|
+
limits: "Paused because budget or usage limits block another run. Resume once limits allow.",
|
|
140
|
+
user_pause:
|
|
141
|
+
"Paused manually by a person or an API call. Resume to let the goal continue on its own.",
|
|
142
|
+
api: "Paused manually by a person or an API call. Resume to let the goal continue on its own.",
|
|
143
|
+
agent: "Paused by the agent: it is waiting on a human decision before continuing.",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type GoalPillRecord = Pick<SessionGoal, "status" | "pausedReason"> & {
|
|
147
|
+
continuation?: SessionGoal["continuation"] | null | undefined;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Pill label, with the pause reason spelled out: "Paused · cap" / "Paused · manually". */
|
|
151
|
+
export function sessionChromeGoalPillLabel(
|
|
152
|
+
state: GoalPillState,
|
|
153
|
+
record: GoalPillRecord | null | undefined,
|
|
154
|
+
): string {
|
|
155
|
+
if (state !== "paused") return GOAL_LABEL[state];
|
|
156
|
+
const suffix = record?.pausedReason ? GOAL_PAUSED_REASON_SUFFIX[record.pausedReason] : undefined;
|
|
157
|
+
return suffix ? `Paused · ${suffix}` : "Paused";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* One human sentence explaining WHY the goal is not pursuing right now: the
|
|
162
|
+
* pause reason, the agent's own `goal_wait` hold (reason + deadline), or the
|
|
163
|
+
* next idle-backoff check time. Null when the state needs no explanation.
|
|
164
|
+
*/
|
|
165
|
+
export function sessionChromeGoalPillExplanation(
|
|
166
|
+
state: GoalPillState,
|
|
167
|
+
record: GoalPillRecord | null | undefined,
|
|
168
|
+
): string | null {
|
|
169
|
+
const continuation = record?.continuation ?? null;
|
|
170
|
+
if (state === "paused") {
|
|
171
|
+
return record?.pausedReason
|
|
172
|
+
? (GOAL_PAUSED_REASON_EXPLANATION[record.pausedReason] ?? null)
|
|
173
|
+
: null;
|
|
174
|
+
}
|
|
175
|
+
if (state === "scheduled" && continuation?.reason === "backoff_pending") {
|
|
176
|
+
return continuation.nextAttemptAt
|
|
177
|
+
? `Next goal check at ${formatClockTime(continuation.nextAttemptAt)}.`
|
|
178
|
+
: "Next goal check is scheduled.";
|
|
179
|
+
}
|
|
180
|
+
if (state === "held" && continuation?.reason === "held_for_input") {
|
|
181
|
+
const reason = continuation.holdReason?.trim();
|
|
182
|
+
const until = continuation.nextAttemptAt
|
|
183
|
+
? ` until ${formatClockTime(continuation.nextAttemptAt)}`
|
|
184
|
+
: "";
|
|
185
|
+
return `Waiting for input${reason ? `: ${reason}` : ""}${until}. A child result, an agent message, or your prompt wakes it sooner.`;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
125
190
|
function queuedTurnPresentation(turn: SessionTurn): QueuedTurnPresentation {
|
|
126
191
|
const realtimeDelegation = objectValue(turn.metadata.realtimeDelegation);
|
|
127
192
|
const inputTranscript = realtimeDelegation?.inputTranscript;
|
|
@@ -156,12 +221,18 @@ export function sessionChromeGoalPillState(
|
|
|
156
221
|
return continuation.reason === "goal_turn_running"
|
|
157
222
|
? "pursuing"
|
|
158
223
|
: continuation.reason === "human_turn_running"
|
|
159
|
-
? "
|
|
224
|
+
? "waiting"
|
|
160
225
|
: "invariant_broken";
|
|
161
226
|
}
|
|
227
|
+
// `backoff_pending` (idle pacing between consecutive no-input continuations,
|
|
228
|
+
// next evaluation at `nextAttemptAt`) is an ordinary scheduled state.
|
|
162
229
|
if (continuation.state === "scheduled") return "scheduled";
|
|
163
230
|
if (continuation.state === "blocked") {
|
|
164
|
-
|
|
231
|
+
// `held_for_input` is the agent's own goal_wait hold (waiting for child
|
|
232
|
+
// results / external input until a deadline); it shares the Held pill.
|
|
233
|
+
return continuation.reason === "workstream_paused" || continuation.reason === "held_for_input"
|
|
234
|
+
? "held"
|
|
235
|
+
: "blocked";
|
|
165
236
|
}
|
|
166
237
|
return "invariant_broken";
|
|
167
238
|
}
|
|
@@ -199,6 +270,16 @@ function pendingKindLabel(kind: SessionPendingInputPreview["kind"]): string {
|
|
|
199
270
|
switch (kind) {
|
|
200
271
|
case "child_terminal_result":
|
|
201
272
|
return "Child result";
|
|
273
|
+
case "child_requires_action":
|
|
274
|
+
return "Child needs input";
|
|
275
|
+
case "child_requires_action_resolved":
|
|
276
|
+
return "Child unblocked";
|
|
277
|
+
case "child_paused":
|
|
278
|
+
return "Child paused";
|
|
279
|
+
case "child_waiting_capacity":
|
|
280
|
+
return "Child waiting";
|
|
281
|
+
case "child_progress":
|
|
282
|
+
return "Child progress";
|
|
202
283
|
case "agent_steer_instruction":
|
|
203
284
|
return "Steer";
|
|
204
285
|
case "scheduled_occurrence":
|
|
@@ -244,34 +325,29 @@ export function SessionChrome({
|
|
|
244
325
|
const record = goal?.goal ?? null;
|
|
245
326
|
const incoming = queue.pendingInputs;
|
|
246
327
|
const turns = queue.queue;
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
steering: currentSteering,
|
|
272
|
-
queuedTurns: currentTurnId ? turns.filter((turn) => turn.id !== currentTurnId) : turns,
|
|
273
|
-
};
|
|
274
|
-
}, [composerSteering, queue.pendingByTurn, turns]);
|
|
328
|
+
const queueMutationFor = queue.mutationFor;
|
|
329
|
+
const queuedTurns = useMemo(
|
|
330
|
+
() => turns.filter((turn) => !isSteeringTurn(turn) && queueMutationFor(turn.id) !== "steer"),
|
|
331
|
+
[queueMutationFor, turns],
|
|
332
|
+
);
|
|
333
|
+
const queuedTurnIds = useMemo(() => new Set(queuedTurns.map((turn) => turn.id)), [queuedTurns]);
|
|
334
|
+
const optimisticQueued = useMemo(
|
|
335
|
+
() =>
|
|
336
|
+
(composer?.optimisticMessages ?? []).filter(
|
|
337
|
+
(message) =>
|
|
338
|
+
message.delivery === "send" &&
|
|
339
|
+
message.destination === "queue" &&
|
|
340
|
+
(!message.turnId || !queuedTurnIds.has(message.turnId)) &&
|
|
341
|
+
!(
|
|
342
|
+
message.turnId &&
|
|
343
|
+
message.appliedQueueVersion !== null &&
|
|
344
|
+
message.appliedQueueVersion !== undefined &&
|
|
345
|
+
queue.snapshot &&
|
|
346
|
+
queue.snapshot.version >= message.appliedQueueVersion
|
|
347
|
+
),
|
|
348
|
+
),
|
|
349
|
+
[composer?.optimisticMessages, queue.snapshot, queuedTurnIds],
|
|
350
|
+
);
|
|
275
351
|
const stoppingKind =
|
|
276
352
|
composer?.stoppingAttempt ??
|
|
277
353
|
(queue.stoppingPreviousAttempt
|
|
@@ -282,10 +358,7 @@ export function SessionChrome({
|
|
|
282
358
|
const stopping = stoppingKind !== null;
|
|
283
359
|
const canMutateQueue = !readOnly && composer !== undefined;
|
|
284
360
|
|
|
285
|
-
const liveGoal =
|
|
286
|
-
record?.status === "active" &&
|
|
287
|
-
record.continuation?.state === "running" &&
|
|
288
|
-
record.continuation.reason === "goal_turn_running";
|
|
361
|
+
const liveGoal = record?.status === "active";
|
|
289
362
|
const elapsed = useLiveElapsed(
|
|
290
363
|
record?.createdAt,
|
|
291
364
|
Boolean(liveGoal),
|
|
@@ -298,6 +371,8 @@ export function SessionChrome({
|
|
|
298
371
|
id: SessionChromeSignalId;
|
|
299
372
|
label: string;
|
|
300
373
|
detail?: string | undefined;
|
|
374
|
+
/** Hover explanation (why paused / held / when the next check is). */
|
|
375
|
+
title?: string | undefined;
|
|
301
376
|
tone: SessionChromeSignalTone;
|
|
302
377
|
icon: ReactNode;
|
|
303
378
|
}> = [];
|
|
@@ -315,44 +390,43 @@ export function SessionChrome({
|
|
|
315
390
|
icon: <InboxIcon className="size-3" />,
|
|
316
391
|
});
|
|
317
392
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
label: stopping
|
|
322
|
-
? stoppingKind === "current"
|
|
323
|
-
? "Stopping current work…"
|
|
324
|
-
: "Stopping previous work…"
|
|
325
|
-
: "Changing direction…",
|
|
326
|
-
...(steering?.text ? { detail: steering.text } : {}),
|
|
327
|
-
tone: stopping ? "waiting" : "accent",
|
|
328
|
-
icon:
|
|
329
|
-
steering?.phase === "submitting" || stopping ? (
|
|
330
|
-
<Loader2Icon className="size-3 animate-og-spin" />
|
|
331
|
-
) : (
|
|
332
|
-
<ZapIcon className="size-3" />
|
|
333
|
-
),
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
if (queuedTurns.length > 0) {
|
|
393
|
+
const queuedCount = queuedTurns.length + optimisticQueued.length;
|
|
394
|
+
const queueProblem = queue.mutationError ?? queue.error;
|
|
395
|
+
if (queuedCount > 0 || queueProblem) {
|
|
337
396
|
const presentations = queuedTurns.map(queuedTurnPresentation);
|
|
338
397
|
const first = presentations[0];
|
|
339
|
-
const allVoiceRequests =
|
|
398
|
+
const allVoiceRequests =
|
|
399
|
+
optimisticQueued.length === 0 &&
|
|
400
|
+
presentations.length > 0 &&
|
|
401
|
+
presentations.every(({ kind }) => kind === "realtime_voice");
|
|
340
402
|
const onlyVoiceHandoff =
|
|
341
|
-
|
|
403
|
+
optimisticQueued.length === 0 &&
|
|
404
|
+
presentations.length === 1 &&
|
|
405
|
+
first?.kind === "realtime_voice_handoff";
|
|
342
406
|
const voiceOnly = allVoiceRequests || onlyVoiceHandoff;
|
|
343
|
-
const detail =
|
|
407
|
+
const detail = queueProblem
|
|
408
|
+
? queue.mutationError
|
|
409
|
+
? "Action not confirmed"
|
|
410
|
+
: "Queue unavailable"
|
|
411
|
+
: (first?.text ?? optimisticQueued[0]?.text);
|
|
344
412
|
rows.push({
|
|
345
413
|
id: "queue",
|
|
346
|
-
label:
|
|
347
|
-
?
|
|
348
|
-
?
|
|
349
|
-
:
|
|
350
|
-
:
|
|
351
|
-
?
|
|
352
|
-
|
|
414
|
+
label: queueProblem
|
|
415
|
+
? queuedCount > 0
|
|
416
|
+
? `${queuedCount} queued · needs attention`
|
|
417
|
+
: "Queue needs attention"
|
|
418
|
+
: allVoiceRequests
|
|
419
|
+
? queuedCount === 1
|
|
420
|
+
? "Voice request queued"
|
|
421
|
+
: `${queuedCount} voice requests queued`
|
|
422
|
+
: onlyVoiceHandoff
|
|
423
|
+
? "Voice handoff queued"
|
|
424
|
+
: `${queuedCount} queued prompt${queuedCount === 1 ? "" : "s"}`,
|
|
353
425
|
...(detail ? { detail } : {}),
|
|
354
|
-
tone: "neutral",
|
|
355
|
-
icon:
|
|
426
|
+
tone: queueProblem ? "waiting" : "neutral",
|
|
427
|
+
icon: queueProblem ? (
|
|
428
|
+
<TriangleAlertIcon className="size-3" />
|
|
429
|
+
) : voiceOnly ? (
|
|
356
430
|
<AudioLinesIcon className="size-3" />
|
|
357
431
|
) : (
|
|
358
432
|
<ListOrderedIcon className="size-3" />
|
|
@@ -360,11 +434,17 @@ export function SessionChrome({
|
|
|
360
434
|
});
|
|
361
435
|
}
|
|
362
436
|
if (record && goalState) {
|
|
363
|
-
const waiting =
|
|
437
|
+
const waiting =
|
|
438
|
+
goalState === "waiting" ||
|
|
439
|
+
goalState === "blocked" ||
|
|
440
|
+
goalState === "held" ||
|
|
441
|
+
goalState === "paused";
|
|
442
|
+
const explanation = sessionChromeGoalPillExplanation(goalState, record);
|
|
364
443
|
rows.push({
|
|
365
444
|
id: "goal",
|
|
366
|
-
label:
|
|
445
|
+
label: sessionChromeGoalPillLabel(goalState, record),
|
|
367
446
|
detail: elapsed ? `${elapsed} · ${record.text}` : record.text,
|
|
447
|
+
...(explanation ? { title: explanation } : {}),
|
|
368
448
|
tone: waiting
|
|
369
449
|
? "waiting"
|
|
370
450
|
: goalState === "pursuing" || goalState === "scheduled"
|
|
@@ -373,6 +453,8 @@ export function SessionChrome({
|
|
|
373
453
|
icon:
|
|
374
454
|
goalState === "blocked" || goalState === "invariant_broken" ? (
|
|
375
455
|
<TriangleAlertIcon className="size-3" />
|
|
456
|
+
) : goalState === "waiting" ? (
|
|
457
|
+
<Loader2Icon className="size-3 animate-og-spin" />
|
|
376
458
|
) : goalState === "paused" || goalState === "held" ? (
|
|
377
459
|
<PauseIcon className="size-3" />
|
|
378
460
|
) : (
|
|
@@ -396,11 +478,11 @@ export function SessionChrome({
|
|
|
396
478
|
elapsed,
|
|
397
479
|
goalState,
|
|
398
480
|
incoming,
|
|
481
|
+
optimisticQueued,
|
|
482
|
+
queue.error,
|
|
483
|
+
queue.mutationError,
|
|
399
484
|
queuedTurns,
|
|
400
485
|
record,
|
|
401
|
-
steering,
|
|
402
|
-
stopping,
|
|
403
|
-
stoppingKind,
|
|
404
486
|
]);
|
|
405
487
|
|
|
406
488
|
const [activeUncontrolled, setActiveUncontrolled] = useState<SessionChromeSignalId | null>(
|
|
@@ -411,13 +493,24 @@ export function SessionChrome({
|
|
|
411
493
|
if (activeControlled === undefined) setActiveUncontrolled(next);
|
|
412
494
|
onActiveChange?.(next);
|
|
413
495
|
};
|
|
496
|
+
const optimisticQueueKeys = optimisticQueued.map((message) => message.clientEventId).join(",");
|
|
497
|
+
const previousOptimisticQueueKeys = useRef(optimisticQueueKeys);
|
|
498
|
+
const [queueArrivalNonce, setQueueArrivalNonce] = useState(0);
|
|
499
|
+
useEffect(() => {
|
|
500
|
+
const previous = new Set(previousOptimisticQueueKeys.current.split(",").filter(Boolean));
|
|
501
|
+
const arrived = optimisticQueued.some((message) => !previous.has(message.clientEventId));
|
|
502
|
+
previousOptimisticQueueKeys.current = optimisticQueueKeys;
|
|
503
|
+
if (!arrived) return;
|
|
504
|
+
// Queue admission must not move the conversation or open a drawer beneath
|
|
505
|
+
// the pointer. A stable, paint-only receipt on the queue chip communicates
|
|
506
|
+
// destination without participating in layout.
|
|
507
|
+
setQueueArrivalNonce((current) => current + 1);
|
|
508
|
+
}, [optimisticQueueKeys, optimisticQueued]);
|
|
414
509
|
const [replaceDraftFor, setReplaceDraftFor] = useState<string | null>(null);
|
|
415
510
|
|
|
416
511
|
const chipRefs = useRef<Partial<Record<SessionChromeSignalId, HTMLButtonElement | null>>>({});
|
|
417
512
|
const railRef = useRef<HTMLDivElement | null>(null);
|
|
418
|
-
const panelBodyRef = useRef<HTMLDivElement | null>(null);
|
|
419
513
|
const [pill, setPill] = useState({ left: 0, top: 0, width: 0, height: 0, opacity: 0 });
|
|
420
|
-
const [panelHeight, setPanelHeight] = useState(0);
|
|
421
514
|
|
|
422
515
|
const signalIds = signals.map((signal) => signal.id).join(",");
|
|
423
516
|
useEffect(() => {
|
|
@@ -471,28 +564,7 @@ export function SessionChrome({
|
|
|
471
564
|
|
|
472
565
|
const open = active !== null;
|
|
473
566
|
|
|
474
|
-
|
|
475
|
-
if (!open) {
|
|
476
|
-
setPanelHeight(0);
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
const node = panelBodyRef.current;
|
|
480
|
-
if (!node) return;
|
|
481
|
-
setPanelHeight(node.offsetHeight);
|
|
482
|
-
}, [open, active, incoming, queuedTurns, record, goalState, agentsPanel, agentsSignal, steering]);
|
|
483
|
-
|
|
484
|
-
useEffect(() => {
|
|
485
|
-
if (!open) return;
|
|
486
|
-
const node = panelBodyRef.current;
|
|
487
|
-
if (!node || typeof ResizeObserver === "undefined") return;
|
|
488
|
-
const observer = new ResizeObserver(() => {
|
|
489
|
-
setPanelHeight(node.offsetHeight);
|
|
490
|
-
});
|
|
491
|
-
observer.observe(node);
|
|
492
|
-
return () => observer.disconnect();
|
|
493
|
-
}, [open, active]);
|
|
494
|
-
|
|
495
|
-
if (signals.length === 0) return null;
|
|
567
|
+
if (signals.length === 0 && !stopping) return null;
|
|
496
568
|
|
|
497
569
|
const shellDuration = reduceMotion ? 0 : 0.22;
|
|
498
570
|
const crossfadeDuration = reduceMotion ? 0 : 0.18;
|
|
@@ -501,15 +573,16 @@ export function SessionChrome({
|
|
|
501
573
|
const panelBody =
|
|
502
574
|
active === "incoming" ? (
|
|
503
575
|
<IncomingPanel inputs={incoming} onDismiss={onDismissIncoming} />
|
|
504
|
-
) : active === "steering" && (steering || stopping) ? (
|
|
505
|
-
<SteeringPanel
|
|
506
|
-
phase={stopping ? "stopping" : (steering?.phase ?? "accepted")}
|
|
507
|
-
stoppingKind={stoppingKind}
|
|
508
|
-
text={steering?.text}
|
|
509
|
-
/>
|
|
510
576
|
) : active === "queue" ? (
|
|
511
577
|
<QueuePanel
|
|
512
578
|
turns={queuedTurns}
|
|
579
|
+
optimistic={optimisticQueued}
|
|
580
|
+
loadError={queue.error}
|
|
581
|
+
mutationError={queue.mutationError}
|
|
582
|
+
onRefresh={queue.refresh}
|
|
583
|
+
onClearMutationError={queue.clearMutationError}
|
|
584
|
+
onRetryOptimistic={composer?.retryOptimisticMessage}
|
|
585
|
+
onRemoveOptimistic={composer?.removeOptimisticMessage}
|
|
513
586
|
readOnly={!canMutateQueue}
|
|
514
587
|
mutationFor={queue.mutationFor}
|
|
515
588
|
replaceDraftFor={replaceDraftFor}
|
|
@@ -519,10 +592,13 @@ export function SessionChrome({
|
|
|
519
592
|
? () => {
|
|
520
593
|
const turnId = replaceDraftFor;
|
|
521
594
|
setReplaceDraftFor(null);
|
|
522
|
-
void
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
595
|
+
void (async () => {
|
|
596
|
+
const checkedOut = await queue.editTurn(turnId, {
|
|
597
|
+
expectedDraftRevision: composer.draftRevision,
|
|
598
|
+
replaceDraft: true,
|
|
599
|
+
});
|
|
600
|
+
if (checkedOut) composer.applyDraft(checkedOut);
|
|
601
|
+
})();
|
|
526
602
|
}
|
|
527
603
|
: undefined
|
|
528
604
|
}
|
|
@@ -533,10 +609,13 @@ export function SessionChrome({
|
|
|
533
609
|
composer,
|
|
534
610
|
() => setReplaceDraftFor(turn.id),
|
|
535
611
|
() => {
|
|
536
|
-
void
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
612
|
+
void (async () => {
|
|
613
|
+
const checkedOut = await queue.editTurn(turn.id, {
|
|
614
|
+
expectedDraftRevision: composer.draftRevision,
|
|
615
|
+
replaceDraft: false,
|
|
616
|
+
});
|
|
617
|
+
if (checkedOut) composer.applyDraft(checkedOut);
|
|
618
|
+
})();
|
|
540
619
|
},
|
|
541
620
|
);
|
|
542
621
|
}
|
|
@@ -647,6 +726,7 @@ export function SessionChrome({
|
|
|
647
726
|
aria-label={selected ? `Close ${signal.label}` : undefined}
|
|
648
727
|
data-testid={`session-chrome-${signal.id}`}
|
|
649
728
|
data-og-session-chrome-signal={signal.id}
|
|
729
|
+
title={signal.title}
|
|
650
730
|
onClick={() => setActive(selected ? null : signal.id)}
|
|
651
731
|
className={cn(
|
|
652
732
|
"group relative z-[1] inline-flex min-h-[var(--og-session-chrome-chip-min-height)] max-w-full items-center gap-1 rounded-og-md py-1 text-left text-og-xs outline-hidden",
|
|
@@ -660,6 +740,17 @@ export function SessionChrome({
|
|
|
660
740
|
paddingInline: "var(--og-session-chrome-chip-pad-x)",
|
|
661
741
|
}}
|
|
662
742
|
>
|
|
743
|
+
{signal.id === "queue" && queueArrivalNonce > 0 && !reduceMotion ? (
|
|
744
|
+
<motion.span
|
|
745
|
+
key={queueArrivalNonce}
|
|
746
|
+
aria-hidden="true"
|
|
747
|
+
data-testid="session-chrome-queue-arrival"
|
|
748
|
+
className="pointer-events-none absolute inset-0 rounded-og-md bg-og-accent-soft"
|
|
749
|
+
initial={{ opacity: 0 }}
|
|
750
|
+
animate={{ opacity: [0, 0.38, 0] }}
|
|
751
|
+
transition={{ duration: 0.72, times: [0, 0.18, 1], ease }}
|
|
752
|
+
/>
|
|
753
|
+
) : null}
|
|
663
754
|
<span className={cn("shrink-0", toneClass(signal.tone, selected))}>
|
|
664
755
|
{signal.icon}
|
|
665
756
|
</span>
|
|
@@ -686,55 +777,74 @@ export function SessionChrome({
|
|
|
686
777
|
</button>
|
|
687
778
|
);
|
|
688
779
|
})}
|
|
780
|
+
{stopping ? (
|
|
781
|
+
<span
|
|
782
|
+
role="status"
|
|
783
|
+
aria-live="polite"
|
|
784
|
+
className="inline-flex min-h-[var(--og-session-chrome-chip-min-height)] items-center gap-1.5 px-1.5 text-og-xs text-og-fg-muted"
|
|
785
|
+
data-testid="session-chrome-stopping"
|
|
786
|
+
>
|
|
787
|
+
<Loader2Icon
|
|
788
|
+
aria-hidden="true"
|
|
789
|
+
className="size-3 animate-og-spin motion-reduce:animate-none"
|
|
790
|
+
/>
|
|
791
|
+
{stoppingKind === "current" ? "Current work stopping" : "Previous work stopping"}
|
|
792
|
+
</span>
|
|
793
|
+
) : null}
|
|
689
794
|
</div>
|
|
690
795
|
</div>
|
|
691
796
|
|
|
692
|
-
<
|
|
797
|
+
<div
|
|
693
798
|
id={panelId}
|
|
694
799
|
data-og-session-chrome-panel-shell=""
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
800
|
+
aria-hidden={!open}
|
|
801
|
+
inert={!open ? true : undefined}
|
|
802
|
+
className="grid overflow-hidden motion-reduce:transition-none"
|
|
803
|
+
style={{
|
|
804
|
+
gridTemplateRows: open ? "1fr" : "0fr",
|
|
698
805
|
opacity: open ? 1 : 0,
|
|
806
|
+
pointerEvents: open ? "auto" : "none",
|
|
807
|
+
transitionProperty: "grid-template-rows, opacity",
|
|
808
|
+
transitionDuration: "var(--og-session-chrome-duration)",
|
|
809
|
+
transitionTimingFunction: "var(--_og-session-chrome-ease)",
|
|
699
810
|
}}
|
|
700
|
-
transition={{ duration: shellDuration, ease }}
|
|
701
|
-
className="overflow-hidden"
|
|
702
811
|
>
|
|
703
|
-
<div
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
812
|
+
<div className="min-h-0 overflow-hidden">
|
|
813
|
+
<div
|
|
814
|
+
className="relative overflow-y-auto overscroll-contain border-t border-og-border/50"
|
|
815
|
+
style={{
|
|
816
|
+
maxHeight: "var(--og-session-chrome-panel-max-height)",
|
|
817
|
+
paddingInline: "var(--og-session-chrome-panel-pad-x)",
|
|
818
|
+
paddingBlock: "var(--og-session-chrome-panel-pad-y)",
|
|
819
|
+
}}
|
|
820
|
+
>
|
|
821
|
+
<AnimatePresence initial={false}>
|
|
822
|
+
{active && panelBody ? (
|
|
823
|
+
<motion.div
|
|
824
|
+
key={active}
|
|
825
|
+
data-og-session-chrome-panel-frame={active}
|
|
826
|
+
initial={reduceMotion ? false : { opacity: 0 }}
|
|
827
|
+
animate={{ opacity: 1, position: "relative" }}
|
|
828
|
+
exit={
|
|
829
|
+
reduceMotion
|
|
830
|
+
? { opacity: 1, position: "relative" }
|
|
831
|
+
: {
|
|
832
|
+
opacity: 0,
|
|
833
|
+
position: "absolute",
|
|
834
|
+
top: 0,
|
|
835
|
+
left: 0,
|
|
836
|
+
right: 0,
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
transition={{ duration: crossfadeDuration, ease }}
|
|
840
|
+
>
|
|
841
|
+
{panelBody}
|
|
842
|
+
</motion.div>
|
|
843
|
+
) : null}
|
|
844
|
+
</AnimatePresence>
|
|
845
|
+
</div>
|
|
736
846
|
</div>
|
|
737
|
-
</
|
|
847
|
+
</div>
|
|
738
848
|
</div>
|
|
739
849
|
</div>
|
|
740
850
|
</TooltipProvider>
|
|
@@ -788,49 +898,15 @@ function IncomingPanel({
|
|
|
788
898
|
);
|
|
789
899
|
}
|
|
790
900
|
|
|
791
|
-
function SteeringPanel({
|
|
792
|
-
phase,
|
|
793
|
-
stoppingKind,
|
|
794
|
-
text,
|
|
795
|
-
}: {
|
|
796
|
-
phase: "submitting" | "accepted" | "stopping";
|
|
797
|
-
stoppingKind?: "current" | "previous" | null | undefined;
|
|
798
|
-
text?: string | undefined;
|
|
799
|
-
}) {
|
|
800
|
-
return (
|
|
801
|
-
<div
|
|
802
|
-
className="flex items-start gap-2 rounded-og-sm px-1.5 py-1"
|
|
803
|
-
role="status"
|
|
804
|
-
aria-live="polite"
|
|
805
|
-
data-og-session-chrome-panel="steering"
|
|
806
|
-
>
|
|
807
|
-
<span className="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-og-accent-soft text-og-accent">
|
|
808
|
-
{phase === "submitting" || phase === "stopping" ? (
|
|
809
|
-
<Loader2Icon className="size-3 animate-og-spin" aria-hidden="true" />
|
|
810
|
-
) : (
|
|
811
|
-
<ZapIcon className="size-3" aria-hidden="true" />
|
|
812
|
-
)}
|
|
813
|
-
</span>
|
|
814
|
-
<div className="min-w-0">
|
|
815
|
-
{text ? (
|
|
816
|
-
<p className="truncate text-og-xs font-medium leading-4 text-og-fg">{text}</p>
|
|
817
|
-
) : null}
|
|
818
|
-
<p className={cn(text && "mt-0.5", "text-[10px] leading-4 text-og-fg-muted")}>
|
|
819
|
-
{phase === "submitting"
|
|
820
|
-
? "Sending your latest direction…"
|
|
821
|
-
: phase === "stopping"
|
|
822
|
-
? stoppingKind === "previous"
|
|
823
|
-
? "Direction saved. Waiting for the previous command to stop safely."
|
|
824
|
-
: "Waiting for the current command to stop safely."
|
|
825
|
-
: "Direction accepted. The agent will continue from it."}
|
|
826
|
-
</p>
|
|
827
|
-
</div>
|
|
828
|
-
</div>
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
901
|
function QueuePanel({
|
|
833
902
|
turns,
|
|
903
|
+
optimistic,
|
|
904
|
+
loadError,
|
|
905
|
+
mutationError,
|
|
906
|
+
onRefresh,
|
|
907
|
+
onClearMutationError,
|
|
908
|
+
onRetryOptimistic,
|
|
909
|
+
onRemoveOptimistic,
|
|
834
910
|
readOnly,
|
|
835
911
|
mutationFor,
|
|
836
912
|
replaceDraftFor,
|
|
@@ -842,6 +918,13 @@ function QueuePanel({
|
|
|
842
918
|
onMove,
|
|
843
919
|
}: {
|
|
844
920
|
turns: SessionTurn[];
|
|
921
|
+
optimistic: ComposerOptimisticMessage[];
|
|
922
|
+
loadError: Error | null;
|
|
923
|
+
mutationError: Error | null;
|
|
924
|
+
onRefresh: () => Promise<void>;
|
|
925
|
+
onClearMutationError: () => void;
|
|
926
|
+
onRetryOptimistic?: ((clientEventId: string) => void) | undefined;
|
|
927
|
+
onRemoveOptimistic?: ((clientEventId: string) => void) | undefined;
|
|
845
928
|
readOnly: boolean;
|
|
846
929
|
mutationFor: UseTurnQueueResult["mutationFor"];
|
|
847
930
|
replaceDraftFor?: string | null | undefined;
|
|
@@ -852,6 +935,28 @@ function QueuePanel({
|
|
|
852
935
|
onRemove?: ((turnId: string) => void) | undefined;
|
|
853
936
|
onMove?: ((turnId: string, beforeTurnId: string | null) => void) | undefined;
|
|
854
937
|
}) {
|
|
938
|
+
const reduceMotion = useReducedMotion();
|
|
939
|
+
const turnIdsKey = turns.map((turn) => turn.id).join(",");
|
|
940
|
+
const [interactiveTurnIds, setInteractiveTurnIds] = useState<ReadonlySet<string>>(
|
|
941
|
+
() => new Set(),
|
|
942
|
+
);
|
|
943
|
+
useEffect(() => {
|
|
944
|
+
const visibleTurnIds = new Set(turnIdsKey ? turnIdsKey.split(",") : []);
|
|
945
|
+
setInteractiveTurnIds((current) => {
|
|
946
|
+
const retained = new Set([...current].filter((turnId) => visibleTurnIds.has(turnId)));
|
|
947
|
+
return retained.size === current.size ? current : retained;
|
|
948
|
+
});
|
|
949
|
+
if (visibleTurnIds.size === 0) return;
|
|
950
|
+
// An optimistic queue row and its authoritative replacement have different
|
|
951
|
+
// React identities. Do not expose an actionable control during that short
|
|
952
|
+
// handoff: a pointer can otherwise press the outgoing DOM node and release
|
|
953
|
+
// over its replacement, producing a completed-looking click with no event.
|
|
954
|
+
const timer = setTimeout(
|
|
955
|
+
() => setInteractiveTurnIds((current) => new Set([...current, ...visibleTurnIds])),
|
|
956
|
+
reduceMotion ? 0 : 240,
|
|
957
|
+
);
|
|
958
|
+
return () => clearTimeout(timer);
|
|
959
|
+
}, [reduceMotion, turnIdsKey]);
|
|
855
960
|
return (
|
|
856
961
|
<ol
|
|
857
962
|
className="flex flex-col gap-0.5"
|
|
@@ -862,6 +967,7 @@ function QueuePanel({
|
|
|
862
967
|
const presentation = queuedTurnPresentation(turn);
|
|
863
968
|
const voice = presentation.kind !== "prompt";
|
|
864
969
|
const pending = mutationFor(turn.id);
|
|
970
|
+
const settling = !interactiveTurnIds.has(turn.id);
|
|
865
971
|
const beforeUp = index > 0 ? (turns[index - 1]?.id ?? null) : null;
|
|
866
972
|
const beforeDown = index < turns.length - 1 ? (turns[index + 2]?.id ?? null) : null;
|
|
867
973
|
const showActions = !readOnly && (onEdit || onSteer || onRemove || onMove);
|
|
@@ -893,7 +999,7 @@ function QueuePanel({
|
|
|
893
999
|
<IconAction
|
|
894
1000
|
label={`Move queued prompt ${index + 1} up`}
|
|
895
1001
|
tip="Move up"
|
|
896
|
-
disabled={pending !== null || index === 0}
|
|
1002
|
+
disabled={settling || pending !== null || index === 0}
|
|
897
1003
|
onClick={() => onMove(turn.id, beforeUp)}
|
|
898
1004
|
>
|
|
899
1005
|
<ArrowUpIcon className="size-3" />
|
|
@@ -901,7 +1007,7 @@ function QueuePanel({
|
|
|
901
1007
|
<IconAction
|
|
902
1008
|
label={`Move queued prompt ${index + 1} down`}
|
|
903
1009
|
tip="Move down"
|
|
904
|
-
disabled={pending !== null || index >= turns.length - 1}
|
|
1010
|
+
disabled={settling || pending !== null || index >= turns.length - 1}
|
|
905
1011
|
onClick={() => onMove(turn.id, beforeDown)}
|
|
906
1012
|
>
|
|
907
1013
|
<ArrowDownIcon className="size-3" />
|
|
@@ -912,7 +1018,7 @@ function QueuePanel({
|
|
|
912
1018
|
<IconAction
|
|
913
1019
|
label={`Steer queued prompt ${index + 1}`}
|
|
914
1020
|
tip={QUEUE_STEER_TIP}
|
|
915
|
-
disabled={pending !== null}
|
|
1021
|
+
disabled={settling || pending !== null}
|
|
916
1022
|
onClick={() => onSteer(turn.id)}
|
|
917
1023
|
>
|
|
918
1024
|
{pending === "steer" ? (
|
|
@@ -926,7 +1032,7 @@ function QueuePanel({
|
|
|
926
1032
|
<IconAction
|
|
927
1033
|
label={`Edit queued prompt ${index + 1}`}
|
|
928
1034
|
tip={QUEUE_EDIT_TIP}
|
|
929
|
-
disabled={pending !== null}
|
|
1035
|
+
disabled={settling || pending !== null}
|
|
930
1036
|
onClick={() => onEdit(turn)}
|
|
931
1037
|
>
|
|
932
1038
|
{pending === "edit" ? (
|
|
@@ -940,7 +1046,7 @@ function QueuePanel({
|
|
|
940
1046
|
<IconAction
|
|
941
1047
|
label={`Remove queued prompt ${index + 1}`}
|
|
942
1048
|
tip={QUEUE_DELETE_TIP}
|
|
943
|
-
disabled={pending !== null}
|
|
1049
|
+
disabled={settling || pending !== null}
|
|
944
1050
|
onClick={() => onRemove(turn.id)}
|
|
945
1051
|
danger
|
|
946
1052
|
>
|
|
@@ -982,6 +1088,98 @@ function QueuePanel({
|
|
|
982
1088
|
</li>
|
|
983
1089
|
);
|
|
984
1090
|
})}
|
|
1091
|
+
<AnimatePresence initial={false}>
|
|
1092
|
+
{optimistic.map((message, index) => (
|
|
1093
|
+
<motion.li
|
|
1094
|
+
key={message.clientEventId}
|
|
1095
|
+
aria-live="polite"
|
|
1096
|
+
data-optimistic-queue-message={message.clientEventId}
|
|
1097
|
+
initial={reduceMotion ? false : { opacity: 0 }}
|
|
1098
|
+
animate={{ opacity: 1 }}
|
|
1099
|
+
transition={{ duration: reduceMotion ? 0 : 0.12, ease: [0.22, 1, 0.36, 1] }}
|
|
1100
|
+
className={cn(
|
|
1101
|
+
"flex min-h-6 items-center gap-1.5 rounded-og-sm px-1.5 py-1",
|
|
1102
|
+
message.state === "failed" ? "bg-og-danger/8 text-og-danger" : "bg-og-accent-soft/45",
|
|
1103
|
+
)}
|
|
1104
|
+
>
|
|
1105
|
+
<span className="shrink-0 font-og-mono text-[10px] leading-4 text-og-fg-subtle">
|
|
1106
|
+
{turns.length + index + 1}
|
|
1107
|
+
</span>
|
|
1108
|
+
<p className="min-w-0 flex-1 truncate text-og-xs leading-4 text-og-fg">
|
|
1109
|
+
{message.text}
|
|
1110
|
+
</p>
|
|
1111
|
+
<span className="sr-only">
|
|
1112
|
+
{message.state === "failed"
|
|
1113
|
+
? "Not confirmed"
|
|
1114
|
+
: message.state === "sending"
|
|
1115
|
+
? "Placing in queue"
|
|
1116
|
+
: "Queued"}
|
|
1117
|
+
</span>
|
|
1118
|
+
{message.state === "failed" ? (
|
|
1119
|
+
<div className="flex shrink-0 items-center gap-1 text-[10px]">
|
|
1120
|
+
{onRetryOptimistic ? (
|
|
1121
|
+
<button
|
|
1122
|
+
type="button"
|
|
1123
|
+
className="rounded-og-sm px-1.5 py-1 font-medium hover:bg-og-surface-2 pointer-coarse:min-h-11 pointer-coarse:min-w-11"
|
|
1124
|
+
onClick={() => onRetryOptimistic(message.clientEventId)}
|
|
1125
|
+
>
|
|
1126
|
+
Retry
|
|
1127
|
+
</button>
|
|
1128
|
+
) : null}
|
|
1129
|
+
{onRemoveOptimistic ? (
|
|
1130
|
+
<button
|
|
1131
|
+
type="button"
|
|
1132
|
+
aria-label="Dismiss unconfirmed queued prompt"
|
|
1133
|
+
className="rounded-og-sm p-1 text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg pointer-coarse:min-h-11 pointer-coarse:min-w-11"
|
|
1134
|
+
onClick={() => onRemoveOptimistic(message.clientEventId)}
|
|
1135
|
+
>
|
|
1136
|
+
<XIcon className="size-3" />
|
|
1137
|
+
</button>
|
|
1138
|
+
) : null}
|
|
1139
|
+
</div>
|
|
1140
|
+
) : message.state === "sending" ? (
|
|
1141
|
+
<Loader2Icon
|
|
1142
|
+
aria-hidden="true"
|
|
1143
|
+
className="size-3 shrink-0 animate-og-spin text-og-accent motion-reduce:animate-none"
|
|
1144
|
+
/>
|
|
1145
|
+
) : (
|
|
1146
|
+
<CheckIcon aria-hidden="true" className="size-3 shrink-0 text-og-accent" />
|
|
1147
|
+
)}
|
|
1148
|
+
</motion.li>
|
|
1149
|
+
))}
|
|
1150
|
+
</AnimatePresence>
|
|
1151
|
+
{loadError ? (
|
|
1152
|
+
<li
|
|
1153
|
+
role="alert"
|
|
1154
|
+
className="flex items-center gap-1.5 rounded-og-sm bg-og-danger/8 px-1.5 py-1 text-og-xs text-og-danger"
|
|
1155
|
+
>
|
|
1156
|
+
<TriangleAlertIcon className="size-3 shrink-0" />
|
|
1157
|
+
<span className="min-w-0 flex-1">Queue unavailable.</span>
|
|
1158
|
+
<button
|
|
1159
|
+
type="button"
|
|
1160
|
+
className="rounded-og-sm px-1.5 py-1 font-medium hover:bg-og-surface-2 pointer-coarse:min-h-11 pointer-coarse:min-w-11"
|
|
1161
|
+
onClick={() => void onRefresh()}
|
|
1162
|
+
>
|
|
1163
|
+
Retry
|
|
1164
|
+
</button>
|
|
1165
|
+
</li>
|
|
1166
|
+
) : null}
|
|
1167
|
+
{mutationError ? (
|
|
1168
|
+
<li
|
|
1169
|
+
role="alert"
|
|
1170
|
+
className="flex items-center gap-1.5 rounded-og-sm bg-og-danger/8 px-1.5 py-1 text-og-xs text-og-danger"
|
|
1171
|
+
>
|
|
1172
|
+
<TriangleAlertIcon className="size-3 shrink-0" />
|
|
1173
|
+
<span className="min-w-0 flex-1">Not confirmed. Check the queue before retrying.</span>
|
|
1174
|
+
<button
|
|
1175
|
+
type="button"
|
|
1176
|
+
className="rounded-og-sm px-1.5 py-1 font-medium hover:bg-og-surface-2 pointer-coarse:min-h-11 pointer-coarse:min-w-11"
|
|
1177
|
+
onClick={onClearMutationError}
|
|
1178
|
+
>
|
|
1179
|
+
Dismiss
|
|
1180
|
+
</button>
|
|
1181
|
+
</li>
|
|
1182
|
+
) : null}
|
|
985
1183
|
</ol>
|
|
986
1184
|
);
|
|
987
1185
|
}
|
|
@@ -1000,11 +1198,12 @@ function GoalPanel({
|
|
|
1000
1198
|
const record = goal.goal;
|
|
1001
1199
|
if (!record) return null;
|
|
1002
1200
|
const canToggle = !readOnly && (record.status === "active" || record.status === "paused");
|
|
1201
|
+
const explanation = sessionChromeGoalPillExplanation(state, record);
|
|
1003
1202
|
|
|
1004
1203
|
return (
|
|
1005
1204
|
<div className="space-y-1.5" data-og-session-chrome-panel="goal">
|
|
1006
1205
|
<div className="flex flex-wrap items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-og-fg-subtle">
|
|
1007
|
-
<span>{
|
|
1206
|
+
<span>{sessionChromeGoalPillLabel(state, record)}</span>
|
|
1008
1207
|
{elapsed ? (
|
|
1009
1208
|
<span className="tabular-nums normal-case tracking-normal text-og-fg-muted">
|
|
1010
1209
|
· {elapsed}
|
|
@@ -1018,6 +1217,14 @@ function GoalPanel({
|
|
|
1018
1217
|
<span className="font-medium text-og-fg">Done when</span> {record.successCriteria}
|
|
1019
1218
|
</p>
|
|
1020
1219
|
) : null}
|
|
1220
|
+
{explanation ? (
|
|
1221
|
+
<p
|
|
1222
|
+
data-og-session-chrome-goal-explanation
|
|
1223
|
+
className="text-og-xs leading-4 text-og-fg-muted"
|
|
1224
|
+
>
|
|
1225
|
+
{explanation}
|
|
1226
|
+
</p>
|
|
1227
|
+
) : null}
|
|
1021
1228
|
{record.continuation?.lastError ? (
|
|
1022
1229
|
<p className="rounded-og-sm bg-og-status-waiting/10 px-1.5 py-1 text-og-xs leading-4 text-og-status-waiting">
|
|
1023
1230
|
{record.continuation.lastError}
|
|
@@ -1026,7 +1233,7 @@ function GoalPanel({
|
|
|
1026
1233
|
<div className="flex flex-wrap items-center justify-between gap-1.5 pt-0.5">
|
|
1027
1234
|
<div className="flex flex-wrap gap-1 text-[10px] text-og-fg-muted">
|
|
1028
1235
|
<span className="rounded bg-og-surface-3/70 px-1 py-px">
|
|
1029
|
-
{record.autoContinuations}
|
|
1236
|
+
{record.autoContinuations} consecutive unattended continues
|
|
1030
1237
|
</span>
|
|
1031
1238
|
<span className="rounded bg-og-surface-3/70 px-1 py-px">
|
|
1032
1239
|
{record.noProgressStreak} stalled
|