@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.
Files changed (174) hide show
  1. package/README.md +36 -11
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
  8. package/dist/chunk-5RZN3DD5.js.map +1 -0
  9. package/dist/chunk-5YK2ZDIG.js +116 -0
  10. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  11. package/dist/chunk-H5YGYNXS.js +329 -0
  12. package/dist/chunk-H5YGYNXS.js.map +1 -0
  13. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  14. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  15. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  16. package/dist/chunk-JOAVA2TW.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
  24. package/dist/chunk-UOLJWT5Y.js.map +1 -0
  25. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  26. package/dist/chunk-W6NFJXSA.js.map +1 -0
  27. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  28. package/dist/chunk-X4A66EWZ.js.map +1 -0
  29. package/dist/chunk-X4JAFD5R.js +3907 -0
  30. package/dist/chunk-X4JAFD5R.js.map +1 -0
  31. package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
  32. package/dist/chunk-XSA2QAKP.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +10 -3
  83. package/dist/realtime.js +138 -29
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +48 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +91 -55
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +162 -17
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +200 -37
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +476 -23
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +75 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /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 while
32
- * animating measured height no `mode="wait"` unmount flash.
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
- ? "blocked"
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
- return continuation.reason === "workstream_paused" ? "held" : "blocked";
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 composerSteering = composer?.steering ?? null;
248
- const { steering, queuedTurns } = useMemo(() => {
249
- const pendingQueueSteer =
250
- turns.find((turn) => queue.pendingByTurn[turn.id] === "steer") ?? null;
251
- const durableQueueSteer =
252
- !pendingQueueSteer && turns[0] && isSteeringTurn(turns[0]) ? turns[0] : null;
253
- const currentSteering =
254
- composerSteering?.phase === "submitting"
255
- ? composerSteering
256
- : pendingQueueSteer
257
- ? {
258
- phase: "submitting" as const,
259
- text: queuedTurnPresentation(pendingQueueSteer).text,
260
- turnId: pendingQueueSteer.id,
261
- }
262
- : durableQueueSteer
263
- ? {
264
- phase: "accepted" as const,
265
- text: queuedTurnPresentation(durableQueueSteer).text,
266
- turnId: durableQueueSteer.id,
267
- }
268
- : composerSteering;
269
- const currentTurnId = currentSteering?.turnId ?? null;
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
- if (steering || stopping) {
319
- rows.push({
320
- id: "steering",
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 = presentations.every(({ kind }) => kind === "realtime_voice");
398
+ const allVoiceRequests =
399
+ optimisticQueued.length === 0 &&
400
+ presentations.length > 0 &&
401
+ presentations.every(({ kind }) => kind === "realtime_voice");
340
402
  const onlyVoiceHandoff =
341
- presentations.length === 1 && first?.kind === "realtime_voice_handoff";
403
+ optimisticQueued.length === 0 &&
404
+ presentations.length === 1 &&
405
+ first?.kind === "realtime_voice_handoff";
342
406
  const voiceOnly = allVoiceRequests || onlyVoiceHandoff;
343
- const detail = first?.text;
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: allVoiceRequests
347
- ? queuedTurns.length === 1
348
- ? "Voice request queued"
349
- : `${queuedTurns.length} voice requests queued`
350
- : onlyVoiceHandoff
351
- ? "Voice handoff queued"
352
- : `${queuedTurns.length} queued prompt${queuedTurns.length === 1 ? "" : "s"}`,
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: voiceOnly ? (
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 = goalState === "blocked" || goalState === "held" || goalState === "paused";
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: GOAL_LABEL[goalState],
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
- useLayoutEffect(() => {
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 queue.editTurn(turnId, {
523
- expectedDraftRevision: composer.draftRevision,
524
- replaceDraft: true,
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 queue.editTurn(turn.id, {
537
- expectedDraftRevision: composer.draftRevision,
538
- replaceDraft: false,
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
- <motion.div
797
+ <div
693
798
  id={panelId}
694
799
  data-og-session-chrome-panel-shell=""
695
- initial={false}
696
- animate={{
697
- height: open ? panelHeight : 0,
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
- ref={panelBodyRef}
705
- className="relative overflow-y-auto overscroll-contain border-t border-og-border/50"
706
- style={{
707
- maxHeight: "var(--og-session-chrome-panel-max-height)",
708
- paddingInline: "var(--og-session-chrome-panel-pad-x)",
709
- paddingBlock: "var(--og-session-chrome-panel-pad-y)",
710
- }}
711
- >
712
- <AnimatePresence initial={false}>
713
- {active && panelBody ? (
714
- <motion.div
715
- key={active}
716
- data-og-session-chrome-panel-frame={active}
717
- initial={reduceMotion ? false : { opacity: 0 }}
718
- animate={{ opacity: 1, position: "relative" }}
719
- exit={
720
- reduceMotion
721
- ? { opacity: 1, position: "relative" }
722
- : {
723
- opacity: 0,
724
- position: "absolute",
725
- top: 0,
726
- left: 0,
727
- right: 0,
728
- }
729
- }
730
- transition={{ duration: crossfadeDuration, ease }}
731
- >
732
- {panelBody}
733
- </motion.div>
734
- ) : null}
735
- </AnimatePresence>
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
- </motion.div>
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>{GOAL_LABEL[state]}</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} auto-continues
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