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

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 (64) 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-DOK4KZF2.js → chunk-5IXTJTK3.js} +1322 -392
  6. package/dist/chunk-5IXTJTK3.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-R55HFTT3.js → chunk-INGB2N5R.js} +4305 -4019
  10. package/dist/chunk-INGB2N5R.js.map +1 -0
  11. package/dist/{chunk-FJBOU2TH.js → chunk-MOWYEBCQ.js} +1728 -12
  12. package/dist/chunk-MOWYEBCQ.js.map +1 -0
  13. package/dist/chunk-PAJU5KKL.js +331 -0
  14. package/dist/chunk-PAJU5KKL.js.map +1 -0
  15. package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
  16. package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
  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/session-chrome.d.ts +37 -4
  21. package/dist/components/session-commands-panel.d.ts +6 -0
  22. package/dist/components/session-conversation.d.ts +14 -0
  23. package/dist/components/timeline-anchor.d.ts +21 -0
  24. package/dist/composer.js +2 -3
  25. package/dist/conversation-timeline.d.ts +5 -0
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.js +716 -914
  28. package/dist/index.js.map +1 -1
  29. package/dist/interaction.js +2 -2
  30. package/dist/lib/format.d.ts +5 -0
  31. package/dist/machines.js +3 -2
  32. package/dist/session-ui.d.ts +3 -0
  33. package/dist/session-ui.js +14 -4
  34. package/dist/session.js +19 -19
  35. package/dist/timeline/types.d.ts +3 -1
  36. package/package.json +2 -2
  37. package/src/components/artifacts/published-html-artifact-frame.tsx +27 -4
  38. package/src/components/machine-input-display.ts +6 -0
  39. package/src/components/message-timeline.tsx +320 -197
  40. package/src/components/session-chrome.tsx +498 -125
  41. package/src/components/session-commands-panel.tsx +113 -0
  42. package/src/components/session-conversation.tsx +141 -0
  43. package/src/components/timeline-anchor.tsx +91 -0
  44. package/src/conversation-timeline.ts +83 -0
  45. package/src/hooks/use-session-background-commands.ts +21 -10
  46. package/src/hooks/use-session-events.ts +36 -14
  47. package/src/hooks/use-session.ts +23 -2
  48. package/src/index.ts +4 -0
  49. package/src/lib/format.ts +32 -0
  50. package/src/session-ui.ts +3 -0
  51. package/src/timeline/projection.ts +6 -12
  52. package/src/timeline/types.ts +4 -0
  53. package/styles/compiled.css +495 -411
  54. package/styles/index.css +23 -0
  55. package/dist/chunk-7EAFGICR.js +0 -302
  56. package/dist/chunk-7EAFGICR.js.map +0 -1
  57. package/dist/chunk-C6WDLHBA.js +0 -2590
  58. package/dist/chunk-C6WDLHBA.js.map +0 -1
  59. package/dist/chunk-DOK4KZF2.js.map +0 -1
  60. package/dist/chunk-FJBOU2TH.js.map +0 -1
  61. package/dist/chunk-R55HFTT3.js.map +0 -1
  62. package/dist/chunk-YVCHAQZY.js.map +0 -1
  63. /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  64. /package/dist/{chunk-W2RGYBCV.js.map → chunk-24M4YBCL.js.map} +0 -0
@@ -28,12 +28,20 @@
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
+ * Queue opens by default when chrome is idle and at least one authoritative
32
+ * prompt is queued. Closing the queue dismisses that session's offer until
33
+ * occupancy drains; a different session on the same chrome instance may
34
+ * offer again. An in-flight optimistic Send never opens the drawer —
35
+ * admission stays a paint-only receipt on the chip so layout does not jump
36
+ * under the pointer.
37
+ *
31
38
  * Segment switches keep the panel shell mounted and crossfade content. The
32
39
  * shell uses one CSS grid-track transition for deliberate open/close actions;
33
40
  * live queue reconciliation never feeds measurements back into layout.
34
41
  */
35
42
  import type { SessionGoal, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
36
43
  import {
44
+ ActivityIcon,
37
45
  AudioLinesIcon,
38
46
  ArrowDownIcon,
39
47
  ArrowUpIcon,
@@ -46,9 +54,11 @@ import {
46
54
  PencilIcon,
47
55
  PlayIcon,
48
56
  Trash2Icon,
57
+ TerminalIcon,
49
58
  TriangleAlertIcon,
50
59
  XIcon,
51
- ZapIcon,
60
+ CornerDownRightIcon,
61
+ TargetIcon,
52
62
  } from "lucide-react";
53
63
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
54
64
  import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
@@ -61,7 +71,13 @@ import { formatClockTime } from "../lib/format";
61
71
  import { requestQueueDraftEdit } from "./queue-draft-policy";
62
72
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
63
73
 
64
- export type SessionChromeSignalId = "incoming" | "steering" | "queue" | "goal" | "agents";
74
+ export type SessionChromeSignalId =
75
+ | "incoming"
76
+ | "steering"
77
+ | "queue"
78
+ | "goal"
79
+ | "agents"
80
+ | "commands";
65
81
 
66
82
  export type SessionChromeSignalTone = "neutral" | "accent" | "waiting" | "running";
67
83
 
@@ -72,12 +88,16 @@ export type SessionChromeAgentsSignal = {
72
88
  };
73
89
 
74
90
  export type SessionChromeProps = {
91
+ compact?: boolean;
75
92
  queue: UseTurnQueueResult;
76
93
  /** Needed for queue edit → composer checkout. Omit with `readOnly`. */
77
94
  composer?: ComposerState | undefined;
78
95
  goal?: UseGoalResult | null | undefined;
79
96
  /** Expanded agents body (host supplies tree / list). */
80
97
  agentsPanel?: ReactNode;
98
+ /** Active commands only. The host mounts the body only when opened. */
99
+ commandsPanel?: ReactNode;
100
+ commandsCount?: number | undefined;
81
101
  /** Chip summary; when `count > 0` the agents segment appears. */
82
102
  agentsSignal?: SessionChromeAgentsSignal | undefined;
83
103
  /**
@@ -111,7 +131,7 @@ type QueuedTurnPresentation = {
111
131
  const GOAL_LABEL: Record<GoalPillState, string> = {
112
132
  pursuing: "Pursuing",
113
133
  waiting: "Waiting",
114
- scheduled: "Scheduled",
134
+ scheduled: "Waiting",
115
135
  blocked: "Blocked",
116
136
  held: "Held",
117
137
  paused: "Paused",
@@ -172,10 +192,10 @@ export function sessionChromeGoalPillExplanation(
172
192
  ? (GOAL_PAUSED_REASON_EXPLANATION[record.pausedReason] ?? null)
173
193
  : null;
174
194
  }
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.";
195
+ if (state === "scheduled") {
196
+ return continuation?.nextAttemptAt
197
+ ? `Continues at ${formatClockTime(continuation.nextAttemptAt)}.`
198
+ : "Waiting to continue automatically.";
179
199
  }
180
200
  if (state === "held" && continuation?.reason === "held_for_input") {
181
201
  const reason = continuation.holdReason?.trim();
@@ -203,6 +223,82 @@ function isSteeringTurn(turn: SessionTurn): boolean {
203
223
  return turn.metadata.delivery === "steer";
204
224
  }
205
225
 
226
+ function isAuthoritativeQueuedTurn(
227
+ turn: SessionTurn,
228
+ mutationFor: UseTurnQueueResult["mutationFor"],
229
+ ): boolean {
230
+ return !isSteeringTurn(turn) && mutationFor(turn.id) !== "steer";
231
+ }
232
+
233
+ export function countAuthoritativeQueuedTurns(
234
+ turns: readonly SessionTurn[],
235
+ mutationFor: UseTurnQueueResult["mutationFor"],
236
+ ): number {
237
+ return turns.filter((turn) => isAuthoritativeQueuedTurn(turn, mutationFor)).length;
238
+ }
239
+
240
+ function isOptimisticQueuedMessage(
241
+ message: ComposerOptimisticMessage,
242
+ queuedTurnIds: ReadonlySet<string>,
243
+ snapshot: UseTurnQueueResult["snapshot"],
244
+ ): boolean {
245
+ return (
246
+ message.delivery === "send" &&
247
+ message.destination === "queue" &&
248
+ (!message.turnId || !queuedTurnIds.has(message.turnId)) &&
249
+ !(
250
+ message.turnId &&
251
+ message.appliedQueueVersion !== null &&
252
+ message.appliedQueueVersion !== undefined &&
253
+ snapshot &&
254
+ snapshot.version >= message.appliedQueueVersion
255
+ )
256
+ );
257
+ }
258
+
259
+ export function countOptimisticQueuedMessages(
260
+ messages: readonly ComposerOptimisticMessage[] | undefined,
261
+ queuedTurnIds: ReadonlySet<string>,
262
+ snapshot: UseTurnQueueResult["snapshot"],
263
+ ): number {
264
+ return (messages ?? []).filter((message) =>
265
+ isOptimisticQueuedMessage(message, queuedTurnIds, snapshot),
266
+ ).length;
267
+ }
268
+
269
+ /**
270
+ * First uncontrolled segment. An existing authoritative queue opens itself
271
+ * when the host did not pick another default; optimistic-only occupancy
272
+ * stays collapsed so a live Send does not shove a drawer under the pointer.
273
+ */
274
+ export function sessionChromeInitialActive(input: {
275
+ defaultActive: SessionChromeSignalId | null;
276
+ authoritativeQueuedCount: number;
277
+ }): SessionChromeSignalId | null {
278
+ if (input.defaultActive != null) return input.defaultActive;
279
+ return input.authoritativeQueuedCount >= 1 ? "queue" : null;
280
+ }
281
+
282
+ /**
283
+ * Whether idle chrome should offer the queue panel. A dismissed session
284
+ * stays collapsed until occupancy drains. Controlled hosts own this.
285
+ */
286
+ export function sessionChromeShouldOfferQueue(input: {
287
+ controlled: boolean;
288
+ active: SessionChromeSignalId | null;
289
+ activityOpen: boolean;
290
+ authoritativeQueuedCount: number;
291
+ suppressed: boolean;
292
+ }): boolean {
293
+ return (
294
+ !input.controlled &&
295
+ input.active === null &&
296
+ !input.activityOpen &&
297
+ input.authoritativeQueuedCount >= 1 &&
298
+ !input.suppressed
299
+ );
300
+ }
301
+
206
302
  function objectValue(value: unknown): Record<string, unknown> | null {
207
303
  return value !== null && typeof value === "object" && !Array.isArray(value)
208
304
  ? (value as Record<string, unknown>)
@@ -228,6 +324,7 @@ export function sessionChromeGoalPillState(
228
324
  // next evaluation at `nextAttemptAt`) is an ordinary scheduled state.
229
325
  if (continuation.state === "scheduled") return "scheduled";
230
326
  if (continuation.state === "blocked") {
327
+ if (continuation.reason === "human_turn_running") return "waiting";
231
328
  // `held_for_input` is the agent's own wait_for_input hold (waiting for child
232
329
  // results / external input until a deadline); it shares the Held pill.
233
330
  return continuation.reason === "workstream_paused" || continuation.reason === "held_for_input"
@@ -288,6 +385,12 @@ function pendingKindLabel(kind: SessionPendingInputPreview["kind"]): string {
288
385
  return "Goal wake";
289
386
  case "agent_message":
290
387
  return "Update";
388
+ case "background_command_result":
389
+ return "Command result";
390
+ case "session_wait_timeout":
391
+ return "Wait ended";
392
+ case "media_generation_result":
393
+ return "Video result";
291
394
  default:
292
395
  return "Incoming";
293
396
  }
@@ -307,10 +410,13 @@ function toneClass(tone: SessionChromeSignalTone, selected: boolean): string {
307
410
  }
308
411
 
309
412
  export function SessionChrome({
413
+ compact = false,
310
414
  queue,
311
415
  composer,
312
416
  goal,
313
417
  agentsPanel,
418
+ commandsPanel,
419
+ commandsCount = 0,
314
420
  agentsSignal,
315
421
  onDismissIncoming,
316
422
  readOnly = false,
@@ -319,6 +425,9 @@ export function SessionChrome({
319
425
  defaultActive = null,
320
426
  onActiveChange,
321
427
  }: SessionChromeProps) {
428
+ const [activityRequested, setActivityOpen] = useState(
429
+ Boolean(compact && defaultActive && ["incoming", "agents", "commands"].includes(defaultActive)),
430
+ );
322
431
  const reactId = useId();
323
432
  const panelId = `og-session-chrome-panel-${reactId}`;
324
433
  const reduceMotion = useReducedMotion();
@@ -327,24 +436,14 @@ export function SessionChrome({
327
436
  const turns = queue.queue;
328
437
  const queueMutationFor = queue.mutationFor;
329
438
  const queuedTurns = useMemo(
330
- () => turns.filter((turn) => !isSteeringTurn(turn) && queueMutationFor(turn.id) !== "steer"),
439
+ () => turns.filter((turn) => isAuthoritativeQueuedTurn(turn, queueMutationFor)),
331
440
  [queueMutationFor, turns],
332
441
  );
333
442
  const queuedTurnIds = useMemo(() => new Set(queuedTurns.map((turn) => turn.id)), [queuedTurns]);
334
443
  const optimisticQueued = useMemo(
335
444
  () =>
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
- ),
445
+ (composer?.optimisticMessages ?? []).filter((message) =>
446
+ isOptimisticQueuedMessage(message, queuedTurnIds, queue.snapshot),
348
447
  ),
349
448
  [composer?.optimisticMessages, queue.snapshot, queuedTurnIds],
350
449
  );
@@ -358,14 +457,49 @@ export function SessionChrome({
358
457
  const stopping = stoppingKind !== null;
359
458
  const canMutateQueue = !readOnly && composer !== undefined;
360
459
 
361
- const liveGoal = record?.status === "active";
362
- const elapsed = useLiveElapsed(
363
- record?.createdAt,
364
- Boolean(liveGoal),
365
- !liveGoal ? record?.updatedAt : null,
366
- );
460
+ const elapsed = useLiveElapsed(record?.createdAt, Boolean(record));
367
461
  const goalState = record ? sessionChromeGoalPillState(record.status, record.continuation) : null;
368
462
 
463
+ const initialAuthoritativeQueuedCount = countAuthoritativeQueuedTurns(
464
+ queue.queue,
465
+ queue.mutationFor,
466
+ );
467
+ const [activeUncontrolled, setActiveUncontrolled] = useState<SessionChromeSignalId | null>(() =>
468
+ sessionChromeInitialActive({
469
+ defaultActive,
470
+ authoritativeQueuedCount: initialAuthoritativeQueuedCount,
471
+ }),
472
+ );
473
+ const active = activeControlled !== undefined ? activeControlled : activeUncontrolled;
474
+ const activityOpen =
475
+ activityRequested ||
476
+ Boolean(compact && active && ["incoming", "agents", "commands"].includes(active));
477
+ const activityVisibleRef = useRef(activityOpen);
478
+ activityVisibleRef.current = activityOpen;
479
+ // Two independent suppressions, both occupancy-scoped:
480
+ // - admission: a live Send while idle must not open a drawer under the pointer
481
+ // - dismiss: closing the queue remembers that session until occupancy drains
482
+ const queueOfferAdmissionSuppressedRef = useRef(
483
+ defaultActive == null &&
484
+ initialAuthoritativeQueuedCount === 0 &&
485
+ countOptimisticQueuedMessages(composer?.optimisticMessages, queuedTurnIds, queue.snapshot) >
486
+ 0,
487
+ );
488
+ const queueOfferDismissedSessionIdsRef = useRef<Set<string>>(new Set());
489
+ const occupancySessionId = queuedTurns[0]?.sessionId ?? null;
490
+ const setActive = (next: SessionChromeSignalId | null) => {
491
+ if (active === "queue" && next === null) {
492
+ if (occupancySessionId) {
493
+ queueOfferDismissedSessionIdsRef.current.add(occupancySessionId);
494
+ } else {
495
+ queueOfferAdmissionSuppressedRef.current = true;
496
+ }
497
+ }
498
+ if (activeControlled === undefined) setActiveUncontrolled(next);
499
+ onActiveChange?.(next);
500
+ };
501
+
502
+ const hasCommandsPanel = commandsPanel != null;
369
503
  const signals = useMemo(() => {
370
504
  const rows: Array<{
371
505
  id: SessionChromeSignalId;
@@ -450,16 +584,7 @@ export function SessionChrome({
450
584
  : goalState === "pursuing" || goalState === "scheduled"
451
585
  ? "accent"
452
586
  : "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
- ),
587
+ icon: <TargetIcon className="size-3" />,
463
588
  });
464
589
  }
465
590
  if (agentsSignal && agentsSignal.count > 0) {
@@ -472,8 +597,22 @@ export function SessionChrome({
472
597
  icon: <BotIcon className="size-3" />,
473
598
  });
474
599
  }
600
+ if (commandsCount > 0 || (active === "commands" && hasCommandsPanel)) {
601
+ rows.push({
602
+ id: "commands",
603
+ label:
604
+ commandsCount > 0
605
+ ? `${commandsCount} command${commandsCount === 1 ? "" : "s"}`
606
+ : "Commands",
607
+ tone: commandsCount > 0 ? "running" : "neutral",
608
+ icon: <TerminalIcon className="size-3" />,
609
+ });
610
+ }
475
611
  return rows;
476
612
  }, [
613
+ commandsCount,
614
+ hasCommandsPanel,
615
+ active,
477
616
  agentsSignal,
478
617
  elapsed,
479
618
  goalState,
@@ -485,14 +624,6 @@ export function SessionChrome({
485
624
  record,
486
625
  ]);
487
626
 
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
627
  const optimisticQueueKeys = optimisticQueued.map((message) => message.clientEventId).join(",");
497
628
  const previousOptimisticQueueKeys = useRef(optimisticQueueKeys);
498
629
  const [queueArrivalNonce, setQueueArrivalNonce] = useState(0);
@@ -505,7 +636,45 @@ export function SessionChrome({
505
636
  // the pointer. A stable, paint-only receipt on the queue chip communicates
506
637
  // destination without participating in layout.
507
638
  setQueueArrivalNonce((current) => current + 1);
508
- }, [optimisticQueueKeys, optimisticQueued]);
639
+ if (activeControlled === undefined && active === null) {
640
+ queueOfferAdmissionSuppressedRef.current = true;
641
+ }
642
+ }, [active, activeControlled, optimisticQueueKeys, optimisticQueued]);
643
+ const queueOccupied = queuedTurns.length > 0 || optimisticQueued.length > 0;
644
+ useEffect(() => {
645
+ if (queueOccupied) return;
646
+ // A later wave may offer again. A prior session's dismiss must not stick
647
+ // after this instance has actually gone empty.
648
+ queueOfferAdmissionSuppressedRef.current = false;
649
+ queueOfferDismissedSessionIdsRef.current.clear();
650
+ }, [queueOccupied]);
651
+ const queueOfferSuppressed =
652
+ queueOfferAdmissionSuppressedRef.current ||
653
+ (occupancySessionId != null &&
654
+ queueOfferDismissedSessionIdsRef.current.has(occupancySessionId));
655
+ useEffect(() => {
656
+ if (
657
+ !sessionChromeShouldOfferQueue({
658
+ controlled: activeControlled !== undefined,
659
+ active,
660
+ activityOpen,
661
+ authoritativeQueuedCount: queuedTurns.length,
662
+ suppressed: queueOfferSuppressed,
663
+ })
664
+ ) {
665
+ return;
666
+ }
667
+ if (activeControlled === undefined) setActiveUncontrolled("queue");
668
+ onActiveChange?.("queue");
669
+ }, [
670
+ active,
671
+ activeControlled,
672
+ activityOpen,
673
+ occupancySessionId,
674
+ onActiveChange,
675
+ queueOfferSuppressed,
676
+ queuedTurns.length,
677
+ ]);
509
678
  const [replaceDraftFor, setReplaceDraftFor] = useState<string | null>(null);
510
679
 
511
680
  const chipRefs = useRef<Partial<Record<SessionChromeSignalId, HTMLButtonElement | null>>>({});
@@ -513,6 +682,13 @@ export function SessionChrome({
513
682
  const [pill, setPill] = useState({ left: 0, top: 0, width: 0, height: 0, opacity: 0 });
514
683
 
515
684
  const signalIds = signals.map((signal) => signal.id).join(",");
685
+ useEffect(() => {
686
+ if (
687
+ compact &&
688
+ !signals.some((signal) => ["incoming", "agents", "commands"].includes(signal.id))
689
+ )
690
+ setActivityOpen(false);
691
+ }, [compact, signals]);
516
692
  useEffect(() => {
517
693
  if (active && !signalIds.split(",").includes(active)) {
518
694
  if (activeControlled === undefined) setActiveUncontrolled(null);
@@ -535,7 +711,10 @@ export function SessionChrome({
535
711
  return;
536
712
  }
537
713
  const chip = chipRefs.current[active];
538
- if (!chip) return;
714
+ if (!chip) {
715
+ setPill((prev) => ({ ...prev, opacity: 0 }));
716
+ return;
717
+ }
539
718
  // Measure against the chip's own box so a wrapped multi-row rail never
540
719
  // stretches the highlight into a tall stripe across every signal.
541
720
  const railBox = rail.getBoundingClientRect();
@@ -645,6 +824,8 @@ export function SessionChrome({
645
824
  />
646
825
  ) : active === "goal" && record && goalState && goal ? (
647
826
  <GoalPanel goal={goal} state={goalState} elapsed={elapsed} readOnly={readOnly} />
827
+ ) : active === "commands" ? (
828
+ <div data-og-session-chrome-panel="commands">{commandsPanel}</div>
648
829
  ) : active === "agents" ? (
649
830
  <div data-og-session-chrome-panel="agents">
650
831
  {agentsPanel ?? <p className="text-og-xs text-og-fg-muted">No agent details.</p>}
@@ -666,15 +847,21 @@ export function SessionChrome({
666
847
  )}
667
848
  style={{
668
849
  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)",
850
+ background: compact
851
+ ? "transparent"
852
+ : open
853
+ ? "var(--_og-session-chrome-surface-open)"
854
+ : "var(--_og-session-chrome-surface)",
855
+ borderColor: compact
856
+ ? "transparent"
857
+ : open
858
+ ? "var(--_og-session-chrome-border-open)"
859
+ : "var(--_og-session-chrome-border)",
860
+ boxShadow: compact
861
+ ? "none"
862
+ : open
863
+ ? "var(--og-session-chrome-shadow-open)"
864
+ : "var(--og-session-chrome-shadow)",
678
865
  transitionDuration: "var(--og-session-chrome-duration)",
679
866
  transitionTimingFunction: "var(--_og-session-chrome-ease)",
680
867
  }}
@@ -690,9 +877,9 @@ export function SessionChrome({
690
877
  >
691
878
  <div
692
879
  ref={railRef}
693
- className="relative flex flex-wrap items-center"
880
+ className={cn("relative flex items-center", compact ? "flex-nowrap" : "flex-wrap")}
694
881
  style={{
695
- gap: "var(--og-session-chrome-chip-gap)",
882
+ gap: compact ? "10px" : "var(--og-session-chrome-chip-gap)",
696
883
  }}
697
884
  >
698
885
  <motion.div
@@ -712,71 +899,193 @@ export function SessionChrome({
712
899
  }}
713
900
  transition={{ duration: shellDuration, ease }}
714
901
  />
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
- ·
902
+ {signals
903
+ .filter(
904
+ (signal) => !compact || !["incoming", "agents", "commands"].includes(signal.id),
905
+ )
906
+ .map((signal) => {
907
+ const selected = active === signal.id;
908
+ return (
909
+ <div
910
+ key={signal.id}
911
+ role="group"
912
+ aria-label={
913
+ signal.id === "queue"
914
+ ? "Queued messages and actions"
915
+ : signal.id === "goal"
916
+ ? "Goal and actions"
917
+ : signal.label
918
+ }
919
+ className={cn(
920
+ "group/signal relative z-[1] inline-flex min-w-0 max-w-full items-center rounded-og-md",
921
+ compact && "bg-og-surface-2/60 px-0.5",
922
+ compact && selected && "bg-og-surface-3",
923
+ )}
924
+ >
925
+ <button
926
+ type="button"
927
+ ref={(node) => {
928
+ chipRefs.current[signal.id] = node;
929
+ }}
930
+ aria-expanded={selected}
931
+ aria-controls={panelId}
932
+ aria-label={
933
+ compact && signal.id === "goal"
934
+ ? `Goal · ${goalState === "pursuing" ? "Running" : signal.label}`
935
+ : selected
936
+ ? `Close ${signal.label}`
937
+ : undefined
938
+ }
939
+ data-testid={`session-chrome-${signal.id}`}
940
+ data-og-session-chrome-signal={signal.id}
941
+ title={signal.title ?? (compact ? signal.label : undefined)}
942
+ onClick={() => {
943
+ if (compact) setActivityOpen(false);
944
+ setActive(selected ? null : signal.id);
945
+ }}
946
+ className={cn(
947
+ "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",
948
+ // Coarse pointers keep a 44px target (session-pins acceptance).
949
+ "pointer-coarse:min-h-11",
950
+ "transition-colors duration-150 motion-reduce:transition-none",
951
+ "hover:text-og-fg focus-visible:bg-og-surface-3/50",
952
+ selected ? "text-og-fg" : "text-og-fg-muted",
953
+ )}
954
+ style={{
955
+ paddingInline: "var(--og-session-chrome-chip-pad-x)",
956
+ }}
957
+ >
958
+ {signal.id === "queue" && queueArrivalNonce > 0 && !reduceMotion ? (
959
+ <motion.span
960
+ key={queueArrivalNonce}
961
+ aria-hidden="true"
962
+ data-testid="session-chrome-queue-arrival"
963
+ className="pointer-events-none absolute inset-0 rounded-og-md bg-og-accent-soft"
964
+ initial={{ opacity: 0 }}
965
+ animate={{ opacity: [0, 0.38, 0] }}
966
+ transition={{ duration: 0.72, times: [0, 0.18, 1], ease }}
967
+ />
968
+ ) : null}
969
+ <span className={cn("shrink-0", toneClass(signal.tone, selected))}>
970
+ {signal.icon}
762
971
  </span>
763
- <span className="min-w-0 max-w-[8.5rem] truncate text-og-fg sm:max-w-[12rem]">
764
- {signal.detail}
972
+ <span
973
+ className={cn(
974
+ "font-medium text-og-fg",
975
+ compact ? "min-w-0 truncate" : "shrink-0",
976
+ )}
977
+ >
978
+ {compact && signal.id === "goal" ? "Goal · " : null}
979
+ {compact && signal.id === "goal"
980
+ ? goalState === "pursuing"
981
+ ? "Running"
982
+ : signal.label
983
+ : compact && signal.id === "queue"
984
+ ? `${queuedTurns.length + optimisticQueued.length} queued`
985
+ : signal.label}
765
986
  </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
- })}
987
+ {signal.detail && !compact ? (
988
+ <>
989
+ <span aria-hidden className="shrink-0 text-og-fg-subtle/60">
990
+ ·
991
+ </span>
992
+ <span className="min-w-0 max-w-[8.5rem] truncate text-og-fg sm:max-w-[12rem]">
993
+ {signal.detail}
994
+ </span>
995
+ </>
996
+ ) : null}
997
+ {selected && !compact ? (
998
+ <span
999
+ data-testid="session-chrome-close"
1000
+ 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"
1001
+ aria-hidden
1002
+ >
1003
+ <XIcon className="size-3" />
1004
+ </span>
1005
+ ) : null}
1006
+ </button>
1007
+ {signal.id === "goal" && goal && record && !readOnly ? (
1008
+ <div className={cn("flex shrink-0 items-center pr-1", compact && "pl-0.5")}>
1009
+ {record.status === "active" || record.status === "paused" ? (
1010
+ <IconAction
1011
+ label={record.status === "paused" ? "Resume goal" : "Pause goal"}
1012
+ tip={record.status === "paused" ? "Resume goal" : "Pause goal"}
1013
+ disabled={goal.updating}
1014
+ onClick={() =>
1015
+ void (record.status === "paused"
1016
+ ? goal.resume()
1017
+ : goal.pause("Paused from session chrome"))
1018
+ }
1019
+ >
1020
+ {goal.updating ? (
1021
+ <Loader2Icon className="size-3 animate-og-spin" />
1022
+ ) : record.status === "paused" ? (
1023
+ <PlayIcon className="size-3" />
1024
+ ) : (
1025
+ <PauseIcon className="size-3" />
1026
+ )}
1027
+ </IconAction>
1028
+ ) : null}
1029
+ <IconAction
1030
+ label="Clear goal"
1031
+ tip="Clear goal"
1032
+ danger
1033
+ disabled={goal.updating}
1034
+ onClick={() => void goal.deleteGoal()}
1035
+ >
1036
+ <Trash2Icon className="size-3" />
1037
+ </IconAction>
1038
+ </div>
1039
+ ) : signal.id === "queue" && canMutateQueue && queuedTurns[0] ? (
1040
+ <div className={cn("flex shrink-0 items-center pr-1", compact && "pl-0.5")}>
1041
+ <IconAction
1042
+ label="Steer first queued message"
1043
+ text="Steer"
1044
+ tip={QUEUE_STEER_TIP}
1045
+ disabled={
1046
+ queue.mutating || Boolean(queue.mutationFor(queuedTurns[0].id))
1047
+ }
1048
+ onClick={() => void queue.steerTurn(queuedTurns[0]!.id)}
1049
+ >
1050
+ <CornerDownRightIcon className="size-3" />
1051
+ </IconAction>
1052
+ </div>
1053
+ ) : null}
1054
+ </div>
1055
+ );
1056
+ })}
1057
+ {compact &&
1058
+ signals.some((signal) => ["incoming", "agents", "commands"].includes(signal.id)) ? (
1059
+ <button
1060
+ type="button"
1061
+ aria-label="Session activity"
1062
+ title={activityOpen ? "Close activity" : "Inbox, agents and commands"}
1063
+ aria-controls={panelId}
1064
+ aria-expanded={activityOpen}
1065
+ onClick={() => {
1066
+ setActivityOpen(!activityOpen);
1067
+ setActive(
1068
+ activityOpen
1069
+ ? null
1070
+ : (signals.find((signal) =>
1071
+ ["incoming", "agents", "commands"].includes(signal.id),
1072
+ )?.id ?? null),
1073
+ );
1074
+ }}
1075
+ className={cn(
1076
+ "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",
1077
+ activityOpen
1078
+ ? "bg-og-surface-3 text-og-fg"
1079
+ : "text-og-fg-muted hover:bg-og-surface-2 hover:text-og-fg",
1080
+ )}
1081
+ >
1082
+ <ActivityIcon className="size-3.5" />
1083
+ <span className="sr-only">Activity</span>
1084
+ {incoming.length > 0 ? (
1085
+ <span className="size-1.5 rounded-full bg-og-accent" />
1086
+ ) : null}
1087
+ </button>
1088
+ ) : null}
780
1089
  {stopping ? (
781
1090
  <span
782
1091
  role="status"
@@ -794,6 +1103,47 @@ export function SessionChrome({
794
1103
  </div>
795
1104
  </div>
796
1105
 
1106
+ <AnimatePresence initial={false}>
1107
+ {compact && activityOpen ? (
1108
+ <motion.div
1109
+ key="activity-tabs"
1110
+ initial={{ height: 0, opacity: 0 }}
1111
+ animate={{ height: "auto", opacity: 1 }}
1112
+ exit={{ height: 0, opacity: 0 }}
1113
+ transition={{ duration: shellDuration, ease }}
1114
+ style={{ overflow: "hidden" }}
1115
+ >
1116
+ <div className="mt-1 flex items-center gap-1 rounded-t-og-lg bg-og-surface-2/50 px-2 py-1">
1117
+ {signals
1118
+ .filter((signal) => ["incoming", "agents", "commands"].includes(signal.id))
1119
+ .map((signal) => (
1120
+ <button
1121
+ key={signal.id}
1122
+ type="button"
1123
+ aria-expanded={active === signal.id}
1124
+ onClick={() => {
1125
+ if (activityVisibleRef.current) setActive(signal.id);
1126
+ }}
1127
+ className={cn(
1128
+ "inline-flex min-h-9 items-center gap-1.5 rounded-og-md px-2 text-og-xs pointer-coarse:min-h-11",
1129
+ active === signal.id
1130
+ ? "bg-og-surface-3 text-og-fg"
1131
+ : "text-og-fg-muted hover:text-og-fg",
1132
+ )}
1133
+ >
1134
+ {signal.icon}
1135
+ {signal.id === "incoming" ? `${incoming.length} incoming` : signal.label}
1136
+ </button>
1137
+ ))}
1138
+ </div>
1139
+ </motion.div>
1140
+ ) : null}
1141
+ </AnimatePresence>
1142
+ {goal?.mutationError ? (
1143
+ <p role="alert" className="px-3 pb-2 text-og-xs text-og-danger">
1144
+ Goal action not confirmed. {goal.mutationError.message}
1145
+ </p>
1146
+ ) : null}
797
1147
  <div
798
1148
  id={panelId}
799
1149
  data-og-session-chrome-panel-shell=""
@@ -811,7 +1161,14 @@ export function SessionChrome({
811
1161
  >
812
1162
  <div className="min-h-0 overflow-hidden">
813
1163
  <div
814
- className="relative overflow-y-auto overscroll-contain border-t border-og-border/50"
1164
+ className={cn(
1165
+ "relative overflow-y-auto overscroll-contain",
1166
+ compact
1167
+ ? activityOpen
1168
+ ? "rounded-b-og-lg bg-og-surface-2/50"
1169
+ : "mt-1 rounded-og-lg bg-og-surface-2/50"
1170
+ : "border-t border-og-border/50",
1171
+ )}
815
1172
  style={{
816
1173
  maxHeight: "var(--og-session-chrome-panel-max-height)",
817
1174
  paddingInline: "var(--og-session-chrome-panel-pad-x)",
@@ -819,7 +1176,7 @@ export function SessionChrome({
819
1176
  }}
820
1177
  >
821
1178
  <AnimatePresence initial={false}>
822
- {active && panelBody ? (
1179
+ {active && active !== "commands" && panelBody ? (
823
1180
  <motion.div
824
1181
  key={active}
825
1182
  data-og-session-chrome-panel-frame={active}
@@ -842,6 +1199,16 @@ export function SessionChrome({
842
1199
  </motion.div>
843
1200
  ) : null}
844
1201
  </AnimatePresence>
1202
+ {active === "commands" ? (
1203
+ <motion.div
1204
+ key="commands"
1205
+ initial={reduceMotion ? false : { opacity: 0 }}
1206
+ animate={{ opacity: 1 }}
1207
+ transition={{ duration: crossfadeDuration, ease }}
1208
+ >
1209
+ {panelBody}
1210
+ </motion.div>
1211
+ ) : null}
845
1212
  </div>
846
1213
  </div>
847
1214
  </div>
@@ -1017,6 +1384,7 @@ function QueuePanel({
1017
1384
  {onSteer ? (
1018
1385
  <IconAction
1019
1386
  label={`Steer queued prompt ${index + 1}`}
1387
+ text="Steer"
1020
1388
  tip={QUEUE_STEER_TIP}
1021
1389
  disabled={settling || pending !== null}
1022
1390
  onClick={() => onSteer(turn.id)}
@@ -1024,7 +1392,7 @@ function QueuePanel({
1024
1392
  {pending === "steer" ? (
1025
1393
  <Loader2Icon className="size-3 animate-og-spin" />
1026
1394
  ) : (
1027
- <ZapIcon className="size-3" />
1395
+ <CornerDownRightIcon className="size-3" />
1028
1396
  )}
1029
1397
  </IconAction>
1030
1398
  ) : null}
@@ -1206,7 +1574,7 @@ function GoalPanel({
1206
1574
  <span>{sessionChromeGoalPillLabel(state, record)}</span>
1207
1575
  {elapsed ? (
1208
1576
  <span className="tabular-nums normal-case tracking-normal text-og-fg-muted">
1209
- · {elapsed}
1577
+ · Created {elapsed} ago
1210
1578
  </span>
1211
1579
  ) : null}
1212
1580
  <span className="normal-case tracking-normal text-og-fg-muted">· v{record.version}</span>
@@ -1288,6 +1656,7 @@ const QUEUE_EDIT_TIP = "Edit in composer";
1288
1656
  const QUEUE_DELETE_TIP = "Delete this queued prompt";
1289
1657
 
1290
1658
  function IconAction({
1659
+ text,
1291
1660
  label,
1292
1661
  tip,
1293
1662
  onClick,
@@ -1296,6 +1665,7 @@ function IconAction({
1296
1665
  children,
1297
1666
  }: {
1298
1667
  label: string;
1668
+ text?: string;
1299
1669
  tip: ReactNode;
1300
1670
  onClick: () => void;
1301
1671
  disabled?: boolean;
@@ -1311,13 +1681,16 @@ function IconAction({
1311
1681
  disabled={disabled}
1312
1682
  onClick={onClick}
1313
1683
  className={cn(
1314
- "inline-flex size-6 items-center justify-center rounded-og-sm text-og-fg-subtle outline-hidden transition-colors",
1684
+ "inline-flex size-6 items-center justify-center rounded-og-sm outline-hidden transition-colors",
1315
1685
  "hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40",
1316
1686
  "disabled:pointer-events-none disabled:opacity-40 pointer-coarse:size-9",
1687
+ text ? "text-og-fg" : "text-og-fg-subtle",
1688
+ text && "w-auto gap-1 px-1.5 text-og-xs pointer-coarse:w-auto",
1317
1689
  danger && "hover:text-og-danger",
1318
1690
  )}
1319
1691
  >
1320
1692
  {children}
1693
+ {text}
1321
1694
  </button>
1322
1695
  </TooltipTrigger>
1323
1696
  <TooltipContent side="top">{tip}</TooltipContent>