@opengeni/react 3.7.0-canary.0 → 3.7.0-canary.2

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 (67) hide show
  1. package/dist/artifacts.js +24 -5
  2. package/dist/artifacts.js.map +1 -1
  3. package/dist/{browser-viewer-E5DDEFQ4.js → browser-viewer-NTD6UIPR.js} +3 -3
  4. package/dist/{chunk-W2RGYBCV.js → chunk-24M4YBCL.js} +4 -4
  5. package/dist/{chunk-R55HFTT3.js → chunk-3IY6UZI6.js} +4306 -4020
  6. package/dist/chunk-3IY6UZI6.js.map +1 -0
  7. package/dist/chunk-5PQJOCX5.js +902 -0
  8. package/dist/chunk-5PQJOCX5.js.map +1 -0
  9. package/dist/{chunk-FJBOU2TH.js → chunk-MOWYEBCQ.js} +1728 -12
  10. package/dist/chunk-MOWYEBCQ.js.map +1 -0
  11. package/dist/chunk-PAJU5KKL.js +331 -0
  12. package/dist/chunk-PAJU5KKL.js.map +1 -0
  13. package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
  14. package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
  15. package/dist/{chunk-DOK4KZF2.js → chunk-WZIMM3GX.js} +1536 -539
  16. package/dist/chunk-WZIMM3GX.js.map +1 -0
  17. package/dist/{chunk-YVCHAQZY.js → chunk-ZFOFY5ST.js} +27 -349
  18. package/dist/chunk-ZFOFY5ST.js.map +1 -0
  19. package/dist/components/artifacts/published-html-artifact-frame.d.ts +3 -2
  20. package/dist/components/child-session-link.d.ts +5 -0
  21. package/dist/components/session-chrome.d.ts +43 -6
  22. package/dist/components/session-commands-panel.d.ts +6 -0
  23. package/dist/components/session-conversation.d.ts +14 -0
  24. package/dist/components/timeline-anchor.d.ts +21 -0
  25. package/dist/composer.js +2 -3
  26. package/dist/conversation-timeline.d.ts +5 -0
  27. package/dist/index.d.ts +4 -1
  28. package/dist/index.js +716 -914
  29. package/dist/index.js.map +1 -1
  30. package/dist/interaction.js +2 -2
  31. package/dist/lib/format.d.ts +5 -0
  32. package/dist/machines.js +3 -2
  33. package/dist/session-ui.d.ts +3 -0
  34. package/dist/session-ui.js +14 -4
  35. package/dist/session.js +19 -19
  36. package/dist/timeline/types.d.ts +3 -1
  37. package/package.json +2 -2
  38. package/src/components/artifacts/published-html-artifact-frame.tsx +27 -4
  39. package/src/components/child-session-link.tsx +27 -0
  40. package/src/components/composer.tsx +1 -1
  41. package/src/components/machine-input-display.ts +8 -2
  42. package/src/components/message-timeline.tsx +336 -198
  43. package/src/components/session-chrome.tsx +572 -161
  44. package/src/components/session-commands-panel.tsx +113 -0
  45. package/src/components/session-conversation.tsx +141 -0
  46. package/src/components/timeline-anchor.tsx +91 -0
  47. package/src/conversation-timeline.ts +83 -0
  48. package/src/hooks/use-session-background-commands.ts +21 -10
  49. package/src/hooks/use-session-events.ts +36 -14
  50. package/src/hooks/use-session.ts +23 -2
  51. package/src/index.ts +4 -0
  52. package/src/lib/format.ts +32 -0
  53. package/src/session-ui.ts +3 -0
  54. package/src/timeline/projection.ts +6 -12
  55. package/src/timeline/types.ts +4 -0
  56. package/styles/compiled.css +513 -410
  57. package/styles/index.css +23 -0
  58. package/dist/chunk-7EAFGICR.js +0 -302
  59. package/dist/chunk-7EAFGICR.js.map +0 -1
  60. package/dist/chunk-C6WDLHBA.js +0 -2590
  61. package/dist/chunk-C6WDLHBA.js.map +0 -1
  62. package/dist/chunk-DOK4KZF2.js.map +0 -1
  63. package/dist/chunk-FJBOU2TH.js.map +0 -1
  64. package/dist/chunk-R55HFTT3.js.map +0 -1
  65. package/dist/chunk-YVCHAQZY.js.map +0 -1
  66. /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  67. /package/dist/{chunk-W2RGYBCV.js.map → chunk-24M4YBCL.js.map} +0 -0
@@ -1,3 +1,4 @@
1
+ import { ChildSessionLink } from "./child-session-link";
1
2
  /**
2
3
  * SessionChrome — compact merged session signals above the composer.
3
4
  *
@@ -28,12 +29,25 @@
28
29
  * Inbox has no product dismiss API; pass `onDismissIncoming` when the host
29
30
  * wants a visible action (dev harness may use a local dummy).
30
31
  *
32
+ * Queue opens by default when chrome is idle and at least one authoritative
33
+ * prompt is queued. Closing the queue dismisses that session's offer until
34
+ * occupancy drains; a different session on the same chrome instance may
35
+ * offer again. An in-flight optimistic Send never opens the drawer —
36
+ * admission stays a paint-only receipt on the chip so layout does not jump
37
+ * under the pointer.
38
+ *
31
39
  * Segment switches keep the panel shell mounted and crossfade content. The
32
40
  * shell uses one CSS grid-track transition for deliberate open/close actions;
33
41
  * live queue reconciliation never feeds measurements back into layout.
34
42
  */
35
- import type { SessionGoal, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
43
+ import type {
44
+ SessionGoal,
45
+ SessionPendingInputPreview,
46
+ SessionStatus,
47
+ SessionTurn,
48
+ } from "@opengeni/sdk";
36
49
  import {
50
+ ActivityIcon,
37
51
  AudioLinesIcon,
38
52
  ArrowDownIcon,
39
53
  ArrowUpIcon,
@@ -46,9 +60,11 @@ import {
46
60
  PencilIcon,
47
61
  PlayIcon,
48
62
  Trash2Icon,
63
+ TerminalIcon,
49
64
  TriangleAlertIcon,
50
65
  XIcon,
51
- ZapIcon,
66
+ CornerDownRightIcon,
67
+ TargetIcon,
52
68
  } from "lucide-react";
53
69
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
54
70
  import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
@@ -61,7 +77,13 @@ import { formatClockTime } from "../lib/format";
61
77
  import { requestQueueDraftEdit } from "./queue-draft-policy";
62
78
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
63
79
 
64
- export type SessionChromeSignalId = "incoming" | "steering" | "queue" | "goal" | "agents";
80
+ export type SessionChromeSignalId =
81
+ | "incoming"
82
+ | "steering"
83
+ | "queue"
84
+ | "goal"
85
+ | "agents"
86
+ | "commands";
65
87
 
66
88
  export type SessionChromeSignalTone = "neutral" | "accent" | "waiting" | "running";
67
89
 
@@ -72,12 +94,18 @@ export type SessionChromeAgentsSignal = {
72
94
  };
73
95
 
74
96
  export type SessionChromeProps = {
97
+ compact?: boolean;
98
+ /** Authoritative execution status; omitted by older embedding hosts. */
99
+ sessionStatus?: SessionStatus | undefined;
75
100
  queue: UseTurnQueueResult;
76
101
  /** Needed for queue edit → composer checkout. Omit with `readOnly`. */
77
102
  composer?: ComposerState | undefined;
78
103
  goal?: UseGoalResult | null | undefined;
79
104
  /** Expanded agents body (host supplies tree / list). */
80
105
  agentsPanel?: ReactNode;
106
+ /** Active commands only. The host mounts the body only when opened. */
107
+ commandsPanel?: ReactNode;
108
+ commandsCount?: number | undefined;
81
109
  /** Chip summary; when `count > 0` the agents segment appears. */
82
110
  agentsSignal?: SessionChromeAgentsSignal | undefined;
83
111
  /**
@@ -85,6 +113,8 @@ export type SessionChromeProps = {
85
113
  * (and the gallery) may still pass a handler so the action is visible.
86
114
  */
87
115
  onDismissIncoming?: ((inputId: string) => void) | undefined;
116
+ /** Open a typed child update's source through the host's normal authorized route. */
117
+ onOpenSession?: ((sessionId: string) => void) | undefined;
88
118
  readOnly?: boolean | undefined;
89
119
  className?: string | undefined;
90
120
  /** Controlled active segment; omit for uncontrolled. */
@@ -97,6 +127,7 @@ type GoalPillState =
97
127
  | "pursuing"
98
128
  | "waiting"
99
129
  | "scheduled"
130
+ | "session_failed"
100
131
  | "blocked"
101
132
  | "held"
102
133
  | "paused"
@@ -111,8 +142,9 @@ type QueuedTurnPresentation = {
111
142
  const GOAL_LABEL: Record<GoalPillState, string> = {
112
143
  pursuing: "Pursuing",
113
144
  waiting: "Waiting",
114
- scheduled: "Scheduled",
145
+ scheduled: "Waiting",
115
146
  blocked: "Blocked",
147
+ session_failed: "Blocked by session failure",
116
148
  held: "Held",
117
149
  paused: "Paused",
118
150
  invariant_broken: "Needs attention",
@@ -167,15 +199,18 @@ export function sessionChromeGoalPillExplanation(
167
199
  record: GoalPillRecord | null | undefined,
168
200
  ): string | null {
169
201
  const continuation = record?.continuation ?? null;
202
+ if (state === "session_failed") {
203
+ return "Resolve the session failure, then use Continue or send a message to continue this active goal.";
204
+ }
170
205
  if (state === "paused") {
171
206
  return record?.pausedReason
172
207
  ? (GOAL_PAUSED_REASON_EXPLANATION[record.pausedReason] ?? null)
173
208
  : null;
174
209
  }
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.";
210
+ if (state === "scheduled") {
211
+ return continuation?.nextAttemptAt
212
+ ? `Continues at ${formatClockTime(continuation.nextAttemptAt)}.`
213
+ : "Waiting to continue automatically.";
179
214
  }
180
215
  if (state === "held" && continuation?.reason === "held_for_input") {
181
216
  const reason = continuation.holdReason?.trim();
@@ -203,6 +238,82 @@ function isSteeringTurn(turn: SessionTurn): boolean {
203
238
  return turn.metadata.delivery === "steer";
204
239
  }
205
240
 
241
+ function isAuthoritativeQueuedTurn(
242
+ turn: SessionTurn,
243
+ mutationFor: UseTurnQueueResult["mutationFor"],
244
+ ): boolean {
245
+ return !isSteeringTurn(turn) && mutationFor(turn.id) !== "steer";
246
+ }
247
+
248
+ export function countAuthoritativeQueuedTurns(
249
+ turns: readonly SessionTurn[],
250
+ mutationFor: UseTurnQueueResult["mutationFor"],
251
+ ): number {
252
+ return turns.filter((turn) => isAuthoritativeQueuedTurn(turn, mutationFor)).length;
253
+ }
254
+
255
+ function isOptimisticQueuedMessage(
256
+ message: ComposerOptimisticMessage,
257
+ queuedTurnIds: ReadonlySet<string>,
258
+ snapshot: UseTurnQueueResult["snapshot"],
259
+ ): boolean {
260
+ return (
261
+ message.delivery === "send" &&
262
+ message.destination === "queue" &&
263
+ (!message.turnId || !queuedTurnIds.has(message.turnId)) &&
264
+ !(
265
+ message.turnId &&
266
+ message.appliedQueueVersion !== null &&
267
+ message.appliedQueueVersion !== undefined &&
268
+ snapshot &&
269
+ snapshot.version >= message.appliedQueueVersion
270
+ )
271
+ );
272
+ }
273
+
274
+ export function countOptimisticQueuedMessages(
275
+ messages: readonly ComposerOptimisticMessage[] | undefined,
276
+ queuedTurnIds: ReadonlySet<string>,
277
+ snapshot: UseTurnQueueResult["snapshot"],
278
+ ): number {
279
+ return (messages ?? []).filter((message) =>
280
+ isOptimisticQueuedMessage(message, queuedTurnIds, snapshot),
281
+ ).length;
282
+ }
283
+
284
+ /**
285
+ * First uncontrolled segment. An existing authoritative queue opens itself
286
+ * when the host did not pick another default; optimistic-only occupancy
287
+ * stays collapsed so a live Send does not shove a drawer under the pointer.
288
+ */
289
+ export function sessionChromeInitialActive(input: {
290
+ defaultActive: SessionChromeSignalId | null;
291
+ authoritativeQueuedCount: number;
292
+ }): SessionChromeSignalId | null {
293
+ if (input.defaultActive != null) return input.defaultActive;
294
+ return input.authoritativeQueuedCount >= 1 ? "queue" : null;
295
+ }
296
+
297
+ /**
298
+ * Whether idle chrome should offer the queue panel. A dismissed session
299
+ * stays collapsed until occupancy drains. Controlled hosts own this.
300
+ */
301
+ export function sessionChromeShouldOfferQueue(input: {
302
+ controlled: boolean;
303
+ active: SessionChromeSignalId | null;
304
+ activityOpen: boolean;
305
+ authoritativeQueuedCount: number;
306
+ suppressed: boolean;
307
+ }): boolean {
308
+ return (
309
+ !input.controlled &&
310
+ input.active === null &&
311
+ !input.activityOpen &&
312
+ input.authoritativeQueuedCount >= 1 &&
313
+ !input.suppressed
314
+ );
315
+ }
316
+
206
317
  function objectValue(value: unknown): Record<string, unknown> | null {
207
318
  return value !== null && typeof value === "object" && !Array.isArray(value)
208
319
  ? (value as Record<string, unknown>)
@@ -213,9 +324,11 @@ function objectValue(value: unknown): Record<string, unknown> | null {
213
324
  export function sessionChromeGoalPillState(
214
325
  goalStatus: "active" | "paused" | "completed",
215
326
  continuation: SessionGoal["continuation"] | null | undefined,
327
+ sessionStatus?: SessionStatus,
216
328
  ): GoalPillState {
217
329
  if (goalStatus === "completed") return "completed";
218
330
  if (goalStatus === "paused") return "paused";
331
+ if (sessionStatus === "failed") return "session_failed";
219
332
  if (!continuation) return "invariant_broken";
220
333
  if (continuation.state === "running") {
221
334
  return continuation.reason === "goal_turn_running"
@@ -228,6 +341,7 @@ export function sessionChromeGoalPillState(
228
341
  // next evaluation at `nextAttemptAt`) is an ordinary scheduled state.
229
342
  if (continuation.state === "scheduled") return "scheduled";
230
343
  if (continuation.state === "blocked") {
344
+ if (continuation.reason === "human_turn_running") return "waiting";
231
345
  // `held_for_input` is the agent's own wait_for_input hold (waiting for child
232
346
  // results / external input until a deadline); it shares the Held pill.
233
347
  return continuation.reason === "workstream_paused" || continuation.reason === "held_for_input"
@@ -288,6 +402,12 @@ function pendingKindLabel(kind: SessionPendingInputPreview["kind"]): string {
288
402
  return "Goal wake";
289
403
  case "agent_message":
290
404
  return "Update";
405
+ case "background_command_result":
406
+ return "Command result";
407
+ case "session_wait_timeout":
408
+ return "Wait ended";
409
+ case "media_generation_result":
410
+ return "Video result";
291
411
  default:
292
412
  return "Incoming";
293
413
  }
@@ -307,18 +427,26 @@ function toneClass(tone: SessionChromeSignalTone, selected: boolean): string {
307
427
  }
308
428
 
309
429
  export function SessionChrome({
430
+ compact = false,
431
+ sessionStatus,
310
432
  queue,
311
433
  composer,
312
434
  goal,
313
435
  agentsPanel,
436
+ commandsPanel,
437
+ commandsCount = 0,
314
438
  agentsSignal,
315
439
  onDismissIncoming,
440
+ onOpenSession,
316
441
  readOnly = false,
317
442
  className,
318
443
  active: activeControlled,
319
444
  defaultActive = null,
320
445
  onActiveChange,
321
446
  }: SessionChromeProps) {
447
+ const [activityRequested, setActivityOpen] = useState(
448
+ Boolean(compact && defaultActive && ["incoming", "agents", "commands"].includes(defaultActive)),
449
+ );
322
450
  const reactId = useId();
323
451
  const panelId = `og-session-chrome-panel-${reactId}`;
324
452
  const reduceMotion = useReducedMotion();
@@ -327,24 +455,14 @@ export function SessionChrome({
327
455
  const turns = queue.queue;
328
456
  const queueMutationFor = queue.mutationFor;
329
457
  const queuedTurns = useMemo(
330
- () => turns.filter((turn) => !isSteeringTurn(turn) && queueMutationFor(turn.id) !== "steer"),
458
+ () => turns.filter((turn) => isAuthoritativeQueuedTurn(turn, queueMutationFor)),
331
459
  [queueMutationFor, turns],
332
460
  );
333
461
  const queuedTurnIds = useMemo(() => new Set(queuedTurns.map((turn) => turn.id)), [queuedTurns]);
334
462
  const optimisticQueued = useMemo(
335
463
  () =>
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
- ),
464
+ (composer?.optimisticMessages ?? []).filter((message) =>
465
+ isOptimisticQueuedMessage(message, queuedTurnIds, queue.snapshot),
348
466
  ),
349
467
  [composer?.optimisticMessages, queue.snapshot, queuedTurnIds],
350
468
  );
@@ -358,14 +476,51 @@ export function SessionChrome({
358
476
  const stopping = stoppingKind !== null;
359
477
  const canMutateQueue = !readOnly && composer !== undefined;
360
478
 
361
- const liveGoal = record?.status === "active";
362
- const elapsed = useLiveElapsed(
363
- record?.createdAt,
364
- Boolean(liveGoal),
365
- !liveGoal ? record?.updatedAt : null,
479
+ const elapsed = useLiveElapsed(record?.createdAt, Boolean(record));
480
+ const goalState = record
481
+ ? sessionChromeGoalPillState(record.status, record.continuation, sessionStatus)
482
+ : null;
483
+
484
+ const initialAuthoritativeQueuedCount = countAuthoritativeQueuedTurns(
485
+ queue.queue,
486
+ queue.mutationFor,
366
487
  );
367
- const goalState = record ? sessionChromeGoalPillState(record.status, record.continuation) : null;
488
+ const [activeUncontrolled, setActiveUncontrolled] = useState<SessionChromeSignalId | null>(() =>
489
+ sessionChromeInitialActive({
490
+ defaultActive,
491
+ authoritativeQueuedCount: initialAuthoritativeQueuedCount,
492
+ }),
493
+ );
494
+ const active = activeControlled !== undefined ? activeControlled : activeUncontrolled;
495
+ const activityOpen =
496
+ activityRequested ||
497
+ Boolean(compact && active && ["incoming", "agents", "commands"].includes(active));
498
+ const activityVisibleRef = useRef(activityOpen);
499
+ activityVisibleRef.current = activityOpen;
500
+ // Two independent suppressions, both occupancy-scoped:
501
+ // - admission: a live Send while idle must not open a drawer under the pointer
502
+ // - dismiss: closing the queue remembers that session until occupancy drains
503
+ const queueOfferAdmissionSuppressedRef = useRef(
504
+ defaultActive == null &&
505
+ initialAuthoritativeQueuedCount === 0 &&
506
+ countOptimisticQueuedMessages(composer?.optimisticMessages, queuedTurnIds, queue.snapshot) >
507
+ 0,
508
+ );
509
+ const queueOfferDismissedSessionIdsRef = useRef<Set<string>>(new Set());
510
+ const occupancySessionId = queuedTurns[0]?.sessionId ?? null;
511
+ const setActive = (next: SessionChromeSignalId | null) => {
512
+ if (active === "queue" && next === null) {
513
+ if (occupancySessionId) {
514
+ queueOfferDismissedSessionIdsRef.current.add(occupancySessionId);
515
+ } else {
516
+ queueOfferAdmissionSuppressedRef.current = true;
517
+ }
518
+ }
519
+ if (activeControlled === undefined) setActiveUncontrolled(next);
520
+ onActiveChange?.(next);
521
+ };
368
522
 
523
+ const hasCommandsPanel = commandsPanel != null;
369
524
  const signals = useMemo(() => {
370
525
  const rows: Array<{
371
526
  id: SessionChromeSignalId;
@@ -437,6 +592,7 @@ export function SessionChrome({
437
592
  const waiting =
438
593
  goalState === "waiting" ||
439
594
  goalState === "blocked" ||
595
+ goalState === "session_failed" ||
440
596
  goalState === "held" ||
441
597
  goalState === "paused";
442
598
  const explanation = sessionChromeGoalPillExplanation(goalState, record);
@@ -450,16 +606,7 @@ export function SessionChrome({
450
606
  : goalState === "pursuing" || goalState === "scheduled"
451
607
  ? "accent"
452
608
  : "neutral",
453
- icon:
454
- goalState === "blocked" || goalState === "invariant_broken" ? (
455
- <TriangleAlertIcon className="size-3" />
456
- ) : goalState === "waiting" ? (
457
- <Loader2Icon className="size-3 animate-og-spin" />
458
- ) : goalState === "paused" || goalState === "held" ? (
459
- <PauseIcon className="size-3" />
460
- ) : (
461
- <ZapIcon className="size-3" />
462
- ),
609
+ icon: <TargetIcon className="size-3" />,
463
610
  });
464
611
  }
465
612
  if (agentsSignal && agentsSignal.count > 0) {
@@ -472,8 +619,22 @@ export function SessionChrome({
472
619
  icon: <BotIcon className="size-3" />,
473
620
  });
474
621
  }
622
+ if (commandsCount > 0 || (active === "commands" && hasCommandsPanel)) {
623
+ rows.push({
624
+ id: "commands",
625
+ label:
626
+ commandsCount > 0
627
+ ? `${commandsCount} command${commandsCount === 1 ? "" : "s"}`
628
+ : "Commands",
629
+ tone: commandsCount > 0 ? "running" : "neutral",
630
+ icon: <TerminalIcon className="size-3" />,
631
+ });
632
+ }
475
633
  return rows;
476
634
  }, [
635
+ commandsCount,
636
+ hasCommandsPanel,
637
+ active,
477
638
  agentsSignal,
478
639
  elapsed,
479
640
  goalState,
@@ -485,14 +646,6 @@ export function SessionChrome({
485
646
  record,
486
647
  ]);
487
648
 
488
- const [activeUncontrolled, setActiveUncontrolled] = useState<SessionChromeSignalId | null>(
489
- defaultActive,
490
- );
491
- const active = activeControlled !== undefined ? activeControlled : activeUncontrolled;
492
- const setActive = (next: SessionChromeSignalId | null) => {
493
- if (activeControlled === undefined) setActiveUncontrolled(next);
494
- onActiveChange?.(next);
495
- };
496
649
  const optimisticQueueKeys = optimisticQueued.map((message) => message.clientEventId).join(",");
497
650
  const previousOptimisticQueueKeys = useRef(optimisticQueueKeys);
498
651
  const [queueArrivalNonce, setQueueArrivalNonce] = useState(0);
@@ -505,7 +658,45 @@ export function SessionChrome({
505
658
  // the pointer. A stable, paint-only receipt on the queue chip communicates
506
659
  // destination without participating in layout.
507
660
  setQueueArrivalNonce((current) => current + 1);
508
- }, [optimisticQueueKeys, optimisticQueued]);
661
+ if (activeControlled === undefined && active === null) {
662
+ queueOfferAdmissionSuppressedRef.current = true;
663
+ }
664
+ }, [active, activeControlled, optimisticQueueKeys, optimisticQueued]);
665
+ const queueOccupied = queuedTurns.length > 0 || optimisticQueued.length > 0;
666
+ useEffect(() => {
667
+ if (queueOccupied) return;
668
+ // A later wave may offer again. A prior session's dismiss must not stick
669
+ // after this instance has actually gone empty.
670
+ queueOfferAdmissionSuppressedRef.current = false;
671
+ queueOfferDismissedSessionIdsRef.current.clear();
672
+ }, [queueOccupied]);
673
+ const queueOfferSuppressed =
674
+ queueOfferAdmissionSuppressedRef.current ||
675
+ (occupancySessionId != null &&
676
+ queueOfferDismissedSessionIdsRef.current.has(occupancySessionId));
677
+ useEffect(() => {
678
+ if (
679
+ !sessionChromeShouldOfferQueue({
680
+ controlled: activeControlled !== undefined,
681
+ active,
682
+ activityOpen,
683
+ authoritativeQueuedCount: queuedTurns.length,
684
+ suppressed: queueOfferSuppressed,
685
+ })
686
+ ) {
687
+ return;
688
+ }
689
+ if (activeControlled === undefined) setActiveUncontrolled("queue");
690
+ onActiveChange?.("queue");
691
+ }, [
692
+ active,
693
+ activeControlled,
694
+ activityOpen,
695
+ occupancySessionId,
696
+ onActiveChange,
697
+ queueOfferSuppressed,
698
+ queuedTurns.length,
699
+ ]);
509
700
  const [replaceDraftFor, setReplaceDraftFor] = useState<string | null>(null);
510
701
 
511
702
  const chipRefs = useRef<Partial<Record<SessionChromeSignalId, HTMLButtonElement | null>>>({});
@@ -513,6 +704,13 @@ export function SessionChrome({
513
704
  const [pill, setPill] = useState({ left: 0, top: 0, width: 0, height: 0, opacity: 0 });
514
705
 
515
706
  const signalIds = signals.map((signal) => signal.id).join(",");
707
+ useEffect(() => {
708
+ if (
709
+ compact &&
710
+ !signals.some((signal) => ["incoming", "agents", "commands"].includes(signal.id))
711
+ )
712
+ setActivityOpen(false);
713
+ }, [compact, signals]);
516
714
  useEffect(() => {
517
715
  if (active && !signalIds.split(",").includes(active)) {
518
716
  if (activeControlled === undefined) setActiveUncontrolled(null);
@@ -535,7 +733,10 @@ export function SessionChrome({
535
733
  return;
536
734
  }
537
735
  const chip = chipRefs.current[active];
538
- if (!chip) return;
736
+ if (!chip) {
737
+ setPill((prev) => ({ ...prev, opacity: 0 }));
738
+ return;
739
+ }
539
740
  // Measure against the chip's own box so a wrapped multi-row rail never
540
741
  // stretches the highlight into a tall stripe across every signal.
541
742
  const railBox = rail.getBoundingClientRect();
@@ -572,7 +773,11 @@ export function SessionChrome({
572
773
 
573
774
  const panelBody =
574
775
  active === "incoming" ? (
575
- <IncomingPanel inputs={incoming} onDismiss={onDismissIncoming} />
776
+ <IncomingPanel
777
+ inputs={incoming}
778
+ onDismiss={onDismissIncoming}
779
+ onOpenSession={onOpenSession}
780
+ />
576
781
  ) : active === "queue" ? (
577
782
  <QueuePanel
578
783
  turns={queuedTurns}
@@ -645,6 +850,8 @@ export function SessionChrome({
645
850
  />
646
851
  ) : active === "goal" && record && goalState && goal ? (
647
852
  <GoalPanel goal={goal} state={goalState} elapsed={elapsed} readOnly={readOnly} />
853
+ ) : active === "commands" ? (
854
+ <div data-og-session-chrome-panel="commands">{commandsPanel}</div>
648
855
  ) : active === "agents" ? (
649
856
  <div data-og-session-chrome-panel="agents">
650
857
  {agentsPanel ?? <p className="text-og-xs text-og-fg-muted">No agent details.</p>}
@@ -666,15 +873,21 @@ export function SessionChrome({
666
873
  )}
667
874
  style={{
668
875
  borderRadius: "var(--_og-session-chrome-radius)",
669
- background: open
670
- ? "var(--_og-session-chrome-surface-open)"
671
- : "var(--_og-session-chrome-surface)",
672
- borderColor: open
673
- ? "var(--_og-session-chrome-border-open)"
674
- : "var(--_og-session-chrome-border)",
675
- boxShadow: open
676
- ? "var(--og-session-chrome-shadow-open)"
677
- : "var(--og-session-chrome-shadow)",
876
+ background: compact
877
+ ? "transparent"
878
+ : open
879
+ ? "var(--_og-session-chrome-surface-open)"
880
+ : "var(--_og-session-chrome-surface)",
881
+ borderColor: compact
882
+ ? "transparent"
883
+ : open
884
+ ? "var(--_og-session-chrome-border-open)"
885
+ : "var(--_og-session-chrome-border)",
886
+ boxShadow: compact
887
+ ? "none"
888
+ : open
889
+ ? "var(--og-session-chrome-shadow-open)"
890
+ : "var(--og-session-chrome-shadow)",
678
891
  transitionDuration: "var(--og-session-chrome-duration)",
679
892
  transitionTimingFunction: "var(--_og-session-chrome-ease)",
680
893
  }}
@@ -690,9 +903,9 @@ export function SessionChrome({
690
903
  >
691
904
  <div
692
905
  ref={railRef}
693
- className="relative flex flex-wrap items-center"
906
+ className={cn("relative flex items-center", compact ? "flex-nowrap" : "flex-wrap")}
694
907
  style={{
695
- gap: "var(--og-session-chrome-chip-gap)",
908
+ gap: compact ? "10px" : "var(--og-session-chrome-chip-gap)",
696
909
  }}
697
910
  >
698
911
  <motion.div
@@ -712,71 +925,193 @@ export function SessionChrome({
712
925
  }}
713
926
  transition={{ duration: shellDuration, ease }}
714
927
  />
715
- {signals.map((signal) => {
716
- const selected = active === signal.id;
717
- return (
718
- <button
719
- key={signal.id}
720
- type="button"
721
- ref={(node) => {
722
- chipRefs.current[signal.id] = node;
723
- }}
724
- aria-expanded={selected}
725
- aria-controls={panelId}
726
- aria-label={selected ? `Close ${signal.label}` : undefined}
727
- data-testid={`session-chrome-${signal.id}`}
728
- data-og-session-chrome-signal={signal.id}
729
- title={signal.title}
730
- onClick={() => setActive(selected ? null : signal.id)}
731
- className={cn(
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",
733
- // Coarse pointers keep a 44px target (session-pins acceptance).
734
- "pointer-coarse:min-h-11",
735
- "transition-colors duration-150 motion-reduce:transition-none",
736
- "hover:text-og-fg focus-visible:bg-og-surface-3/50",
737
- selected ? "text-og-fg" : "text-og-fg-muted",
738
- )}
739
- style={{
740
- paddingInline: "var(--og-session-chrome-chip-pad-x)",
741
- }}
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}
754
- <span className={cn("shrink-0", toneClass(signal.tone, selected))}>
755
- {signal.icon}
756
- </span>
757
- <span className="shrink-0 font-medium text-og-fg">{signal.label}</span>
758
- {signal.detail ? (
759
- <>
760
- <span aria-hidden className="shrink-0 text-og-fg-subtle/60">
761
- ·
928
+ {signals
929
+ .filter(
930
+ (signal) => !compact || !["incoming", "agents", "commands"].includes(signal.id),
931
+ )
932
+ .map((signal) => {
933
+ const selected = active === signal.id;
934
+ return (
935
+ <div
936
+ key={signal.id}
937
+ role="group"
938
+ aria-label={
939
+ signal.id === "queue"
940
+ ? "Queued messages and actions"
941
+ : signal.id === "goal"
942
+ ? "Goal and actions"
943
+ : signal.label
944
+ }
945
+ className={cn(
946
+ "group/signal relative z-[1] inline-flex min-w-0 max-w-full items-center rounded-og-md",
947
+ compact && "bg-og-surface-2/60 px-0.5",
948
+ compact && selected && "bg-og-surface-3",
949
+ )}
950
+ >
951
+ <button
952
+ type="button"
953
+ ref={(node) => {
954
+ chipRefs.current[signal.id] = node;
955
+ }}
956
+ aria-expanded={selected}
957
+ aria-controls={panelId}
958
+ aria-label={
959
+ compact && signal.id === "goal"
960
+ ? `Goal · ${goalState === "pursuing" ? "Running" : signal.label}`
961
+ : selected
962
+ ? `Close ${signal.label}`
963
+ : undefined
964
+ }
965
+ data-testid={`session-chrome-${signal.id}`}
966
+ data-og-session-chrome-signal={signal.id}
967
+ title={signal.title ?? (compact ? signal.label : undefined)}
968
+ onClick={() => {
969
+ if (compact) setActivityOpen(false);
970
+ setActive(selected ? null : signal.id);
971
+ }}
972
+ className={cn(
973
+ "group relative z-[1] inline-flex min-w-0 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",
974
+ // Coarse pointers keep a 44px target (session-pins acceptance).
975
+ "pointer-coarse:min-h-11",
976
+ "transition-colors duration-150 motion-reduce:transition-none",
977
+ "hover:text-og-fg focus-visible:bg-og-surface-3/50",
978
+ selected ? "text-og-fg" : "text-og-fg-muted",
979
+ )}
980
+ style={{
981
+ paddingInline: "var(--og-session-chrome-chip-pad-x)",
982
+ }}
983
+ >
984
+ {signal.id === "queue" && queueArrivalNonce > 0 && !reduceMotion ? (
985
+ <motion.span
986
+ key={queueArrivalNonce}
987
+ aria-hidden="true"
988
+ data-testid="session-chrome-queue-arrival"
989
+ className="pointer-events-none absolute inset-0 rounded-og-md bg-og-accent-soft"
990
+ initial={{ opacity: 0 }}
991
+ animate={{ opacity: [0, 0.38, 0] }}
992
+ transition={{ duration: 0.72, times: [0, 0.18, 1], ease }}
993
+ />
994
+ ) : null}
995
+ <span className={cn("shrink-0", toneClass(signal.tone, selected))}>
996
+ {signal.icon}
762
997
  </span>
763
- <span className="min-w-0 max-w-[8.5rem] truncate text-og-fg sm:max-w-[12rem]">
764
- {signal.detail}
998
+ <span
999
+ className={cn(
1000
+ "font-medium text-og-fg",
1001
+ compact ? "min-w-0 truncate" : "shrink-0",
1002
+ )}
1003
+ >
1004
+ {compact && signal.id === "goal" ? "Goal · " : null}
1005
+ {compact && signal.id === "goal"
1006
+ ? goalState === "pursuing"
1007
+ ? "Running"
1008
+ : signal.label
1009
+ : compact && signal.id === "queue"
1010
+ ? `${queuedTurns.length + optimisticQueued.length} queued`
1011
+ : signal.label}
765
1012
  </span>
766
- </>
767
- ) : null}
768
- {selected ? (
769
- <span
770
- data-testid="session-chrome-close"
771
- className="ml-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-og-sm text-og-fg-subtle transition-colors group-hover:text-og-fg pointer-coarse:size-5"
772
- aria-hidden
773
- >
774
- <XIcon className="size-3" />
775
- </span>
776
- ) : null}
777
- </button>
778
- );
779
- })}
1013
+ {signal.detail && !compact ? (
1014
+ <>
1015
+ <span aria-hidden className="shrink-0 text-og-fg-subtle/60">
1016
+ ·
1017
+ </span>
1018
+ <span className="min-w-0 max-w-[8.5rem] truncate text-og-fg sm:max-w-[12rem]">
1019
+ {signal.detail}
1020
+ </span>
1021
+ </>
1022
+ ) : null}
1023
+ {selected && !compact ? (
1024
+ <span
1025
+ data-testid="session-chrome-close"
1026
+ className="ml-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded-og-sm text-og-fg-subtle transition-colors group-hover:text-og-fg pointer-coarse:size-5"
1027
+ aria-hidden
1028
+ >
1029
+ <XIcon className="size-3" />
1030
+ </span>
1031
+ ) : null}
1032
+ </button>
1033
+ {signal.id === "goal" && goal && record && !readOnly ? (
1034
+ <div className={cn("flex shrink-0 items-center pr-1", compact && "pl-0.5")}>
1035
+ {record.status === "active" || record.status === "paused" ? (
1036
+ <IconAction
1037
+ label={record.status === "paused" ? "Resume goal" : "Pause goal"}
1038
+ tip={record.status === "paused" ? "Resume goal" : "Pause goal"}
1039
+ disabled={goal.updating}
1040
+ onClick={() =>
1041
+ void (record.status === "paused"
1042
+ ? goal.resume()
1043
+ : goal.pause("Paused from session chrome"))
1044
+ }
1045
+ >
1046
+ {goal.updating ? (
1047
+ <Loader2Icon className="size-3 animate-og-spin" />
1048
+ ) : record.status === "paused" ? (
1049
+ <PlayIcon className="size-3" />
1050
+ ) : (
1051
+ <PauseIcon className="size-3" />
1052
+ )}
1053
+ </IconAction>
1054
+ ) : null}
1055
+ <IconAction
1056
+ label="Clear goal"
1057
+ tip="Clear goal"
1058
+ danger
1059
+ disabled={goal.updating}
1060
+ onClick={() => void goal.deleteGoal()}
1061
+ >
1062
+ <Trash2Icon className="size-3" />
1063
+ </IconAction>
1064
+ </div>
1065
+ ) : signal.id === "queue" && canMutateQueue && queuedTurns[0] ? (
1066
+ <div className={cn("flex shrink-0 items-center pr-1", compact && "pl-0.5")}>
1067
+ <IconAction
1068
+ label="Steer first queued message"
1069
+ text="Steer"
1070
+ tip={QUEUE_STEER_TIP}
1071
+ disabled={
1072
+ queue.mutating || Boolean(queue.mutationFor(queuedTurns[0].id))
1073
+ }
1074
+ onClick={() => void queue.steerTurn(queuedTurns[0]!.id)}
1075
+ >
1076
+ <CornerDownRightIcon className="size-3" />
1077
+ </IconAction>
1078
+ </div>
1079
+ ) : null}
1080
+ </div>
1081
+ );
1082
+ })}
1083
+ {compact &&
1084
+ signals.some((signal) => ["incoming", "agents", "commands"].includes(signal.id)) ? (
1085
+ <button
1086
+ type="button"
1087
+ aria-label="Session activity"
1088
+ title={activityOpen ? "Close activity" : "Inbox, agents and commands"}
1089
+ aria-controls={panelId}
1090
+ aria-expanded={activityOpen}
1091
+ onClick={() => {
1092
+ setActivityOpen(!activityOpen);
1093
+ setActive(
1094
+ activityOpen
1095
+ ? null
1096
+ : (signals.find((signal) =>
1097
+ ["incoming", "agents", "commands"].includes(signal.id),
1098
+ )?.id ?? null),
1099
+ );
1100
+ }}
1101
+ className={cn(
1102
+ "ml-auto inline-flex min-h-8 shrink-0 items-center justify-center gap-1.5 rounded-og-md px-2 text-og-xs transition-colors outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
1103
+ activityOpen
1104
+ ? "bg-og-surface-3 text-og-fg"
1105
+ : "text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg",
1106
+ )}
1107
+ >
1108
+ <ActivityIcon className="size-3.5" />
1109
+ <span className="sr-only">Activity</span>
1110
+ {incoming.length > 0 ? (
1111
+ <span className="size-1.5 rounded-full bg-og-accent" />
1112
+ ) : null}
1113
+ </button>
1114
+ ) : null}
780
1115
  {stopping ? (
781
1116
  <span
782
1117
  role="status"
@@ -794,6 +1129,47 @@ export function SessionChrome({
794
1129
  </div>
795
1130
  </div>
796
1131
 
1132
+ <AnimatePresence initial={false}>
1133
+ {compact && activityOpen ? (
1134
+ <motion.div
1135
+ key="activity-tabs"
1136
+ initial={{ height: 0, opacity: 0 }}
1137
+ animate={{ height: "auto", opacity: 1 }}
1138
+ exit={{ height: 0, opacity: 0 }}
1139
+ transition={{ duration: shellDuration, ease }}
1140
+ style={{ overflow: "hidden" }}
1141
+ >
1142
+ <div className="mt-1 flex items-center gap-1 rounded-t-og-lg bg-og-surface-2/50 px-2 py-1">
1143
+ {signals
1144
+ .filter((signal) => ["incoming", "agents", "commands"].includes(signal.id))
1145
+ .map((signal) => (
1146
+ <button
1147
+ key={signal.id}
1148
+ type="button"
1149
+ aria-expanded={active === signal.id}
1150
+ onClick={() => {
1151
+ if (activityVisibleRef.current) setActive(signal.id);
1152
+ }}
1153
+ className={cn(
1154
+ "inline-flex min-h-9 items-center gap-1.5 rounded-og-md px-2 text-og-xs pointer-coarse:min-h-11",
1155
+ active === signal.id
1156
+ ? "bg-og-surface-3 text-og-fg"
1157
+ : "text-og-fg-muted hover:text-og-fg",
1158
+ )}
1159
+ >
1160
+ {signal.icon}
1161
+ {signal.id === "incoming" ? `${incoming.length} incoming` : signal.label}
1162
+ </button>
1163
+ ))}
1164
+ </div>
1165
+ </motion.div>
1166
+ ) : null}
1167
+ </AnimatePresence>
1168
+ {goal?.mutationError ? (
1169
+ <p role="alert" className="px-3 pb-2 text-og-xs text-og-danger">
1170
+ Goal action not confirmed. {goal.mutationError.message}
1171
+ </p>
1172
+ ) : null}
797
1173
  <div
798
1174
  id={panelId}
799
1175
  data-og-session-chrome-panel-shell=""
@@ -811,7 +1187,14 @@ export function SessionChrome({
811
1187
  >
812
1188
  <div className="min-h-0 overflow-hidden">
813
1189
  <div
814
- className="relative overflow-y-auto overscroll-contain border-t border-og-border/50"
1190
+ className={cn(
1191
+ "relative overflow-y-auto overscroll-contain",
1192
+ compact
1193
+ ? activityOpen
1194
+ ? "rounded-b-og-lg bg-og-surface-2/50"
1195
+ : "mt-1 rounded-og-lg bg-og-surface-2/50"
1196
+ : "border-t border-og-border/50",
1197
+ )}
815
1198
  style={{
816
1199
  maxHeight: "var(--og-session-chrome-panel-max-height)",
817
1200
  paddingInline: "var(--og-session-chrome-panel-pad-x)",
@@ -819,7 +1202,7 @@ export function SessionChrome({
819
1202
  }}
820
1203
  >
821
1204
  <AnimatePresence initial={false}>
822
- {active && panelBody ? (
1205
+ {active && active !== "commands" && panelBody ? (
823
1206
  <motion.div
824
1207
  key={active}
825
1208
  data-og-session-chrome-panel-frame={active}
@@ -842,6 +1225,16 @@ export function SessionChrome({
842
1225
  </motion.div>
843
1226
  ) : null}
844
1227
  </AnimatePresence>
1228
+ {active === "commands" ? (
1229
+ <motion.div
1230
+ key="commands"
1231
+ initial={reduceMotion ? false : { opacity: 0 }}
1232
+ animate={{ opacity: 1 }}
1233
+ transition={{ duration: crossfadeDuration, ease }}
1234
+ >
1235
+ {panelBody}
1236
+ </motion.div>
1237
+ ) : null}
845
1238
  </div>
846
1239
  </div>
847
1240
  </div>
@@ -854,47 +1247,59 @@ export function SessionChrome({
854
1247
  function IncomingPanel({
855
1248
  inputs,
856
1249
  onDismiss,
1250
+ onOpenSession,
857
1251
  }: {
858
1252
  inputs: SessionPendingInputPreview[];
859
1253
  onDismiss?: ((inputId: string) => void) | undefined;
1254
+ onOpenSession?: ((sessionId: string) => void) | undefined;
860
1255
  }) {
861
1256
  return (
862
- <ul
863
- className="flex flex-col gap-0.5"
864
- aria-label="Incoming updates"
865
- data-og-session-chrome-panel="incoming"
866
- >
867
- {inputs.map((input) => (
868
- <li
869
- key={input.id}
870
- className="group flex items-start gap-1.5 rounded-og-sm px-1.5 py-1 transition-colors hover:bg-[var(--_og-session-chrome-row-hover)]"
871
- >
872
- <span
873
- className={cn(
874
- "mt-px shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4",
875
- input.classification === "action_required" || input.classification === "failure"
876
- ? "bg-og-status-waiting/12 text-og-status-waiting"
877
- : "bg-og-surface-3/80 text-og-fg-muted",
878
- )}
1257
+ <div>
1258
+ <p className="mb-2 text-og-xs text-og-fg-subtle">Waiting to be included in an agent turn.</p>
1259
+ <ul
1260
+ className="flex flex-col gap-0.5"
1261
+ aria-label="Incoming updates"
1262
+ data-og-session-chrome-panel="incoming"
1263
+ >
1264
+ {inputs.map((input) => (
1265
+ <li
1266
+ key={input.id}
1267
+ className="group flex items-start gap-1.5 rounded-og-sm px-1.5 py-1 transition-colors hover:bg-[var(--_og-session-chrome-row-hover)]"
879
1268
  >
880
- {pendingKindLabel(input.kind)}
881
- </span>
882
- <p className="min-w-0 flex-1 text-og-xs leading-4 text-og-fg">{input.summary}</p>
883
- {onDismiss ? (
884
- <div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 max-sm:opacity-100">
885
- <IconAction
886
- label={`Dismiss incoming ${pendingKindLabel(input.kind)}`}
887
- tip="Dismiss"
888
- onClick={() => onDismiss(input.id)}
889
- danger
890
- >
891
- <Trash2Icon className="size-3" />
892
- </IconAction>
1269
+ <span
1270
+ className={cn(
1271
+ "mt-px shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4",
1272
+ input.classification === "action_required" || input.classification === "failure"
1273
+ ? "bg-og-status-waiting/12 text-og-status-waiting"
1274
+ : "bg-og-surface-3/80 text-og-fg-muted",
1275
+ )}
1276
+ >
1277
+ {pendingKindLabel(input.kind)}
1278
+ </span>
1279
+ <div className="min-w-0 flex-1">
1280
+ <p className="break-words text-og-xs leading-4 text-og-fg">{input.summary}</p>
1281
+ <ChildSessionLink
1282
+ kind={input.kind}
1283
+ sourceId={input.sourceId}
1284
+ onOpenSession={onOpenSession}
1285
+ />
893
1286
  </div>
894
- ) : null}
895
- </li>
896
- ))}
897
- </ul>
1287
+ {onDismiss ? (
1288
+ <div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 max-sm:opacity-100">
1289
+ <IconAction
1290
+ label={`Dismiss incoming ${pendingKindLabel(input.kind)}`}
1291
+ tip="Dismiss"
1292
+ onClick={() => onDismiss(input.id)}
1293
+ danger
1294
+ >
1295
+ <Trash2Icon className="size-3" />
1296
+ </IconAction>
1297
+ </div>
1298
+ ) : null}
1299
+ </li>
1300
+ ))}
1301
+ </ul>
1302
+ </div>
898
1303
  );
899
1304
  }
900
1305
 
@@ -1017,6 +1422,7 @@ function QueuePanel({
1017
1422
  {onSteer ? (
1018
1423
  <IconAction
1019
1424
  label={`Steer queued prompt ${index + 1}`}
1425
+ text="Steer"
1020
1426
  tip={QUEUE_STEER_TIP}
1021
1427
  disabled={settling || pending !== null}
1022
1428
  onClick={() => onSteer(turn.id)}
@@ -1024,7 +1430,7 @@ function QueuePanel({
1024
1430
  {pending === "steer" ? (
1025
1431
  <Loader2Icon className="size-3 animate-og-spin" />
1026
1432
  ) : (
1027
- <ZapIcon className="size-3" />
1433
+ <CornerDownRightIcon className="size-3" />
1028
1434
  )}
1029
1435
  </IconAction>
1030
1436
  ) : null}
@@ -1206,7 +1612,7 @@ function GoalPanel({
1206
1612
  <span>{sessionChromeGoalPillLabel(state, record)}</span>
1207
1613
  {elapsed ? (
1208
1614
  <span className="tabular-nums normal-case tracking-normal text-og-fg-muted">
1209
- · {elapsed}
1615
+ · Created {elapsed} ago
1210
1616
  </span>
1211
1617
  ) : null}
1212
1618
  <span className="normal-case tracking-normal text-og-fg-muted">· v{record.version}</span>
@@ -1288,6 +1694,7 @@ const QUEUE_EDIT_TIP = "Edit in composer";
1288
1694
  const QUEUE_DELETE_TIP = "Delete this queued prompt";
1289
1695
 
1290
1696
  function IconAction({
1697
+ text,
1291
1698
  label,
1292
1699
  tip,
1293
1700
  onClick,
@@ -1296,6 +1703,7 @@ function IconAction({
1296
1703
  children,
1297
1704
  }: {
1298
1705
  label: string;
1706
+ text?: string;
1299
1707
  tip: ReactNode;
1300
1708
  onClick: () => void;
1301
1709
  disabled?: boolean;
@@ -1311,13 +1719,16 @@ function IconAction({
1311
1719
  disabled={disabled}
1312
1720
  onClick={onClick}
1313
1721
  className={cn(
1314
- "inline-flex size-6 items-center justify-center rounded-og-sm text-og-fg-subtle outline-hidden transition-colors",
1722
+ "inline-flex size-6 items-center justify-center rounded-og-sm outline-hidden transition-colors",
1315
1723
  "hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40",
1316
1724
  "disabled:pointer-events-none disabled:opacity-40 pointer-coarse:size-9",
1725
+ text ? "text-og-fg" : "text-og-fg-subtle",
1726
+ text && "w-auto gap-1 px-1.5 text-og-xs pointer-coarse:w-auto",
1317
1727
  danger && "hover:text-og-danger",
1318
1728
  )}
1319
1729
  >
1320
1730
  {children}
1731
+ {text}
1321
1732
  </button>
1322
1733
  </TooltipTrigger>
1323
1734
  <TooltipContent side="top">{tip}</TooltipContent>