@opengeni/react 0.27.0 → 0.29.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 (43) hide show
  1. package/README.md +32 -0
  2. package/dist/{chunk-4F2YLLHP.js → chunk-AVLMIWYO.js} +3 -12
  3. package/dist/chunk-AVLMIWYO.js.map +1 -0
  4. package/dist/{chunk-NBYNFI5T.js → chunk-CSJBF3QJ.js} +5 -3
  5. package/dist/chunk-CSJBF3QJ.js.map +1 -0
  6. package/dist/{chunk-XVVQP7A4.js → chunk-IPVOVOW6.js} +2 -2
  7. package/dist/{chunk-OZDLELJQ.js → chunk-OKXCECLG.js} +55 -10
  8. package/dist/chunk-OKXCECLG.js.map +1 -0
  9. package/dist/{chunk-GQN2QIR2.js → chunk-ZMNPFZWL.js} +238 -87
  10. package/dist/chunk-ZMNPFZWL.js.map +1 -0
  11. package/dist/composer.js +2 -2
  12. package/dist/index.d.ts +7 -34
  13. package/dist/index.js +7 -5
  14. package/dist/index.js.map +1 -1
  15. package/dist/{queue-surface-implementation-3UW3MIIR.js → queue-surface-implementation-UQ535OKQ.js} +44 -12
  16. package/dist/queue-surface-implementation-UQ535OKQ.js.map +1 -0
  17. package/dist/{session-LsampPc6.d.ts → session-DaONt-dY.d.ts} +1 -1
  18. package/dist/{session-ui-BPOV-tuY.d.ts → session-ui-B5_roA95.d.ts} +81 -5
  19. package/dist/session-ui.d.ts +2 -2
  20. package/dist/session-ui.js +4 -2
  21. package/dist/session.d.ts +2 -2
  22. package/dist/session.js +3 -3
  23. package/dist/{use-turn-queue-aDnQvzVz.d.ts → use-turn-queue-D_1a8W_l.d.ts} +26 -4
  24. package/package.json +2 -2
  25. package/src/components/message-timeline.tsx +88 -0
  26. package/src/components/queue-surface-implementation.tsx +79 -11
  27. package/src/components/queue-surface.tsx +5 -5
  28. package/src/hooks/use-composer.ts +1 -15
  29. package/src/hooks/use-turn-queue.ts +8 -0
  30. package/src/index.ts +9 -0
  31. package/src/session-ui.ts +9 -0
  32. package/src/session.ts +2 -0
  33. package/src/timeline/index.ts +13 -2
  34. package/src/timeline/parsers.ts +3 -31
  35. package/src/timeline/projection.ts +73 -9
  36. package/src/timeline/turn-summary.tsx +243 -56
  37. package/src/timeline/types.ts +27 -1
  38. package/dist/chunk-4F2YLLHP.js.map +0 -1
  39. package/dist/chunk-GQN2QIR2.js.map +0 -1
  40. package/dist/chunk-NBYNFI5T.js.map +0 -1
  41. package/dist/chunk-OZDLELJQ.js.map +0 -1
  42. package/dist/queue-surface-implementation-3UW3MIIR.js.map +0 -1
  43. /package/dist/{chunk-XVVQP7A4.js.map → chunk-IPVOVOW6.js.map} +0 -0
@@ -42,10 +42,12 @@ import {
42
42
  type AgentMessageItem,
43
43
  type AuthNeededItem,
44
44
  type GoalItem,
45
+ type MachineInputBatchItem,
45
46
  type NoticeItem,
46
47
  type TimelineGroup,
47
48
  type TimelineItem,
48
49
  type ToolRegistry,
50
+ type TurnSummaryOptions,
49
51
  type UserMessageItem,
50
52
  type WorkerCompletionItem,
51
53
  TurnSummary,
@@ -97,6 +99,8 @@ export type MessageTimelineProps = {
97
99
  * `createDefaultToolRegistry({ entries })` to add custom tool renderers.
98
100
  */
99
101
  toolRegistry?: ToolRegistry | undefined;
102
+ /** Customize collapsed turn facets for this timeline instance. */
103
+ turnSummary?: TurnSummaryOptions | undefined;
100
104
  /** Follow new events when pinned to the bottom. Defaults to true. */
101
105
  autoFollow?: boolean | undefined;
102
106
  /** Older durable history exists above the current window (see useSessionEvents). */
@@ -127,6 +131,7 @@ export function MessageTimeline({
127
131
  onReconnect,
128
132
  resolveProviderLogo,
129
133
  toolRegistry = defaultToolRegistry,
134
+ turnSummary,
130
135
  autoFollow = true,
131
136
  hasOlder = false,
132
137
  loadingOlder = false,
@@ -414,6 +419,7 @@ export function MessageTimeline({
414
419
  onReconnect={onReconnect}
415
420
  resolveProviderLogo={resolveProviderLogo}
416
421
  toolRegistry={toolRegistry}
422
+ turnSummary={turnSummary}
417
423
  foldLiveCluster={isAgentProgress(groups[index + 1]?.group)}
418
424
  />
419
425
  </div>
@@ -621,6 +627,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
621
627
  onReconnect,
622
628
  resolveProviderLogo,
623
629
  toolRegistry,
630
+ turnSummary,
624
631
  insideTurn = false,
625
632
  foldLiveCluster = false,
626
633
  }: {
@@ -633,6 +640,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
633
640
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
634
641
  resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
635
642
  toolRegistry: ToolRegistry;
643
+ turnSummary?: TurnSummaryOptions | undefined;
636
644
  /** A completed cluster of a still-RUNNING turn (not the live tail) folds
637
645
  behind a neutral chip — the one place activity without an outcome still
638
646
  folds, bounding the DOM of days-long autonomous turns. */
@@ -651,6 +659,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
651
659
  failureText={insideTurn ? undefined : group.failureText}
652
660
  defaultOpen={!insideTurn && group.outcome === "failed" ? true : undefined}
653
661
  bare={insideTurn}
662
+ facets={turnSummary?.facets}
654
663
  >
655
664
  <ActivityRail
656
665
  items={group.items}
@@ -683,6 +692,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
683
692
  onReconnect={onReconnect}
684
693
  resolveProviderLogo={resolveProviderLogo}
685
694
  toolRegistry={toolRegistry}
695
+ turnSummary={turnSummary}
686
696
  insideTurn
687
697
  />
688
698
  ));
@@ -697,6 +707,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
697
707
  durationMs={durationBetween(group.startedAt, group.endedAt)}
698
708
  defaultOpen={!insideTurn && group.outcome === "failed" ? true : undefined}
699
709
  bare={insideTurn}
710
+ facets={turnSummary?.facets}
700
711
  >
701
712
  {insideTurn ? (
702
713
  // A nested turn is already on an ancestor rail — its body just stacks
@@ -833,6 +844,8 @@ export function TimelineRow({
833
844
  return <SessionStatusRow item={item} />;
834
845
  case "goal":
835
846
  return <GoalRow item={item} />;
847
+ case "machine-input-batch":
848
+ return <MachineInputBatchRow item={item} />;
836
849
  case "notice":
837
850
  return <NoticeRow item={item} />;
838
851
  case "auth-needed":
@@ -1155,6 +1168,81 @@ function GoalRow({ item }: { item: GoalItem }) {
1155
1168
  );
1156
1169
  }
1157
1170
 
1171
+ const MACHINE_INPUT_META: Record<MachineInputBatchItem["members"][number]["kind"], string> = {
1172
+ scheduled_occurrence: "Scheduled update",
1173
+ goal_continuation: "Goal continued",
1174
+ agent_message: "Agent update",
1175
+ agent_steer_instruction: "Agent direction",
1176
+ child_terminal_result: "Agent finished",
1177
+ };
1178
+
1179
+ function MachineInputBatchRow({ item }: { item: MachineInputBatchItem }) {
1180
+ const enter = useEntranceAnimation();
1181
+ const visible = item.members.slice(0, 3);
1182
+ return (
1183
+ <div
1184
+ className={cn(
1185
+ enter && "animate-og-enter",
1186
+ "rounded-og-md border border-og-border/70 bg-og-surface-1/55 px-3 py-2.5",
1187
+ )}
1188
+ >
1189
+ {item.members.length > 1 && (
1190
+ <div className="mb-2 text-xs font-medium text-og-fg-subtle">
1191
+ {item.members.length} updates joined this turn
1192
+ </div>
1193
+ )}
1194
+ <div className="space-y-2">
1195
+ {visible.map((member) => (
1196
+ <MachineInputRow key={member.id} member={member} />
1197
+ ))}
1198
+ </div>
1199
+ {item.members.length > visible.length && (
1200
+ <details className="mt-2 pl-8 text-xs text-og-fg-muted">
1201
+ <summary className="cursor-pointer select-none hover:text-og-fg">
1202
+ Show {item.members.length - visible.length} more
1203
+ </summary>
1204
+ <div className="mt-2 space-y-2">
1205
+ {item.members.slice(visible.length).map((member) => (
1206
+ <MachineInputRow key={member.id} member={member} />
1207
+ ))}
1208
+ </div>
1209
+ </details>
1210
+ )}
1211
+ </div>
1212
+ );
1213
+ }
1214
+
1215
+ function MachineInputRow({ member }: { member: MachineInputBatchItem["members"][number] }) {
1216
+ const source = readableMachineInputSource(member.sourceId);
1217
+ return (
1218
+ <div className="flex min-w-0 items-start gap-2.5">
1219
+ <span className="mt-2 size-1.5 shrink-0 rounded-full bg-og-fg-subtle" aria-hidden />
1220
+ <div className="min-w-0 flex-1">
1221
+ <span className="text-xs font-medium text-og-fg-muted">
1222
+ {MACHINE_INPUT_META[member.kind]}
1223
+ </span>
1224
+ {source && <span className="ml-1.5 text-xs text-og-fg-subtle">from {source}</span>}
1225
+ {member.summary && (
1226
+ <p className="mt-0.5 whitespace-pre-wrap break-words text-sm leading-5 text-og-fg">
1227
+ {truncate(cleanMachineInputSummary(member.summary), 320)}
1228
+ </p>
1229
+ )}
1230
+ </div>
1231
+ </div>
1232
+ );
1233
+ }
1234
+
1235
+ function readableMachineInputSource(sourceId: string): string | null {
1236
+ const value = sourceId.trim();
1237
+ if (!value || /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(value)) return null;
1238
+ if (/^(goal|schedule|system):/i.test(value)) return null;
1239
+ return value.replaceAll("_", " ");
1240
+ }
1241
+
1242
+ function cleanMachineInputSummary(summary: string): string {
1243
+ return summary.replace(/^\[[A-Z][A-Z _-]*(?:\s+\d+\/\d+)?\]\s*/, "").trim();
1244
+ }
1245
+
1158
1246
  function NoticeRow({ item }: { item: NoticeItem }) {
1159
1247
  const enter = useEntranceAnimation();
1160
1248
  const tone =
@@ -16,7 +16,7 @@ import {
16
16
  verticalListSortingStrategy,
17
17
  } from "@dnd-kit/sortable";
18
18
  import { CSS } from "@dnd-kit/utilities";
19
- import type { SessionTurn } from "@opengeni/sdk";
19
+ import type { SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
20
20
  import {
21
21
  ArrowDownToLineIcon,
22
22
  ArrowUpToLineIcon,
@@ -43,7 +43,7 @@ import type { QueueMutationKind, UseTurnQueueResult } from "../hooks/use-turn-qu
43
43
  import { requestQueueDraftEdit } from "./queue-draft-policy";
44
44
  import { QueueErrorAlert, QueueStoppingStatus } from "./queue-surface-state";
45
45
 
46
- /** The sole human prompt queue: compact above Goal, Agents, and composer. */
46
+ /** The sole pending-input surface: compact above Goal, Agents, and composer. */
47
47
  type QueueSurfaceCommonProps = {
48
48
  /** Focus the composer owned by this queue after checkout/removal. */
49
49
  onRequestComposerFocus?: (() => void) | undefined;
@@ -85,10 +85,21 @@ export function QueueSurface({
85
85
  projectedIndex: number;
86
86
  } | null>(null);
87
87
  const count = queue.queue.length;
88
+ const pendingInputCount = queue.pendingInputs.length;
89
+ const totalCount = count + pendingInputCount;
90
+ const attachedInputIds = queue.pendingInputAttachment?.inputIds ?? [];
91
+ const attachedInputs = queue.pendingInputs.filter((input) => attachedInputIds.includes(input.id));
92
+ const standaloneInputs = queue.pendingInputs.filter(
93
+ (input) => !attachedInputIds.includes(input.id),
94
+ );
88
95
  const canEditInComposer = queueComposerCheckoutEnabled(composer, readOnly);
89
96
  const collapsedPreview = useMemo(
90
- () => queuePromptPreview(queue.queue[0]?.prompt ?? "", QUEUE_COLLAPSED_PREVIEW_CHARACTERS),
91
- [queue.queue],
97
+ () =>
98
+ queuePromptPreview(
99
+ queue.queue[0]?.prompt ?? queue.pendingInputs[0]?.summary ?? "",
100
+ QUEUE_COLLAPSED_PREVIEW_CHARACTERS,
101
+ ),
102
+ [queue.pendingInputs, queue.queue],
92
103
  );
93
104
  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
94
105
 
@@ -227,7 +238,7 @@ export function QueueSurface({
227
238
  [canEditInComposer, composer, edit],
228
239
  );
229
240
 
230
- if (count === 0 && !queue.stoppingPreviousAttempt && !queue.error && !queue.mutationError)
241
+ if (totalCount === 0 && !queue.stoppingPreviousAttempt && !queue.error && !queue.mutationError)
231
242
  return null;
232
243
 
233
244
  return (
@@ -242,23 +253,31 @@ export function QueueSurface({
242
253
  type="button"
243
254
  className="flex w-full min-w-0 flex-wrap items-center gap-2 px-3 py-2 text-left outline-none transition-colors hover:bg-surface-2/60 focus-visible:bg-surface-2/60 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 pointer-coarse:min-h-[44px]"
244
255
  onClick={() => setOpen((value) => !value)}
245
- aria-description={!open && count > 0 ? collapsedPreview.summary : undefined}
256
+ aria-description={!open && totalCount > 0 ? collapsedPreview.summary : undefined}
246
257
  aria-expanded={open}
247
- aria-label={`${count} queued prompt${count === 1 ? "" : "s"}${readOnly ? " Read-only" : ""}`}
258
+ aria-label={`${count} queued prompt${count === 1 ? "" : "s"}${
259
+ pendingInputCount > 0
260
+ ? ` and ${pendingInputCount} pending machine input${pendingInputCount === 1 ? "" : "s"}`
261
+ : ""
262
+ }${readOnly ? " Read-only" : ""}`}
248
263
  >
249
264
  <ChevronDownIcon
250
265
  aria-hidden="true"
251
266
  className={`size-3.5 shrink-0 text-fg-subtle transition-transform ${open ? "rotate-180" : ""}`}
252
267
  />
253
268
  <span className="text-xs font-medium text-fg">
254
- {count} queued prompt{count === 1 ? "" : "s"}
269
+ {count > 0 ? `${count} queued prompt${count === 1 ? "" : "s"}` : null}
270
+ {count > 0 && pendingInputCount > 0 ? " · " : null}
271
+ {pendingInputCount > 0
272
+ ? `${pendingInputCount} incoming update${pendingInputCount === 1 ? "" : "s"}`
273
+ : null}
255
274
  </span>
256
275
  {readOnly ? (
257
276
  <span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-2xs text-fg-subtle">
258
277
  Read-only
259
278
  </span>
260
279
  ) : null}
261
- {!open && count > 0 ? (
280
+ {!open && totalCount > 0 ? (
262
281
  <span
263
282
  aria-hidden="true"
264
283
  className={`max-w-full truncate text-fg-muted ${
@@ -275,6 +294,10 @@ export function QueueSurface({
275
294
  {queue.loading ? <Loader2Icon className="ml-auto size-3.5 animate-spin" /> : null}
276
295
  </button>
277
296
 
297
+ {open && standaloneInputs.length > 0 ? (
298
+ <PendingMachineInputs inputs={standaloneInputs} />
299
+ ) : null}
300
+
278
301
  {open && count > 0 && readOnly ? (
279
302
  <ol
280
303
  className="max-h-[min(30rem,60dvh)] divide-y divide-border overflow-y-auto overscroll-contain border-t border-border"
@@ -286,6 +309,9 @@ export function QueueSurface({
286
309
  key={turn.id}
287
310
  turn={turn}
288
311
  index={index}
312
+ attachedInputs={
313
+ turn.id === queue.pendingInputAttachment?.turnId ? attachedInputs : []
314
+ }
289
315
  onDisclosureChange={(expanded) =>
290
316
  setAnnouncement(
291
317
  `Full content for queued prompt ${index + 1} ${expanded ? "shown" : "hidden"}.`,
@@ -320,6 +346,9 @@ export function QueueSurface({
320
346
  turn={turn}
321
347
  index={index}
322
348
  count={count}
349
+ attachedInputs={
350
+ turn.id === queue.pendingInputAttachment?.turnId ? attachedInputs : []
351
+ }
323
352
  pending={queue.mutationFor(turn.id)}
324
353
  confirmingReplace={replaceDraftFor === turn.id}
325
354
  keyboardDragging={keyboardDrag?.turnId === turn.id}
@@ -396,6 +425,35 @@ export function QueueSurface({
396
425
  );
397
426
  }
398
427
 
428
+ function PendingMachineInputs({
429
+ inputs,
430
+ attached = false,
431
+ }: {
432
+ inputs: SessionPendingInputPreview[];
433
+ attached?: boolean;
434
+ }) {
435
+ return (
436
+ <section className="border-t border-border px-3 py-2">
437
+ <p className="text-2xs font-medium text-fg-subtle">
438
+ {attached
439
+ ? `${inputs.length} update${inputs.length === 1 ? "" : "s"} will join this prompt`
440
+ : "Incoming updates waiting to run"}
441
+ </p>
442
+ <ol className="mt-1 space-y-1">
443
+ {inputs.map((input) => (
444
+ <li key={input.id} className="line-clamp-3 break-words text-xs text-fg" dir="auto">
445
+ <strong className="capitalize text-fg-muted">
446
+ {input.kind.replace("_terminal", "").replaceAll("_", " ")}
447
+ </strong>{" "}
448
+ · {input.sourceId}
449
+ {input.summary ? ` — ${input.summary}` : ""}
450
+ </li>
451
+ ))}
452
+ </ol>
453
+ </section>
454
+ );
455
+ }
456
+
399
457
  const QUEUE_ROW_PREVIEW_CHARACTERS = 360;
400
458
  const QUEUE_COLLAPSED_PREVIEW_CHARACTERS = 180;
401
459
  const QUEUE_PREVIEW_GRAPHEME_CONTEXT_CHARACTERS = 32;
@@ -735,7 +793,7 @@ function QueuePrompt({
735
793
  aria-controls={fullContentId}
736
794
  aria-expanded={expanded}
737
795
  aria-label={`${expanded ? "Hide" : "Show"} full content for queued prompt ${index + 1}`}
738
- className="mt-1 inline-flex min-h-7 min-w-0 max-w-full items-center gap-1 whitespace-normal rounded-md text-left text-2xs font-medium text-fg-muted outline-none transition-colors hover:text-fg focus-visible:ring-2 focus-visible:ring-ring/40 pointer-coarse:min-h-[44px]"
796
+ className="mt-1 inline-flex min-h-7 min-w-0 max-w-full items-center gap-1 whitespace-normal rounded-md text-left text-2xs font-medium text-fg outline-none focus-visible:ring-2 focus-visible:ring-ring/40 pointer-coarse:min-h-[44px]"
739
797
  data-testid={`queue-prompt-disclosure-${index + 1}`}
740
798
  onClick={() => {
741
799
  const next = !expanded;
@@ -769,10 +827,12 @@ function QueuePrompt({
769
827
  function ReadOnlyQueueRow({
770
828
  turn,
771
829
  index,
830
+ attachedInputs,
772
831
  onDisclosureChange,
773
832
  }: {
774
833
  turn: SessionTurn;
775
834
  index: number;
835
+ attachedInputs: SessionPendingInputPreview[];
776
836
  onDisclosureChange: (expanded: boolean) => void;
777
837
  }) {
778
838
  return (
@@ -780,6 +840,9 @@ function ReadOnlyQueueRow({
780
840
  <span className="mt-1 shrink-0 font-mono text-2xs text-fg-subtle">{index + 1}</span>
781
841
  <div className="min-w-0 flex-1">
782
842
  <QueuePrompt prompt={turn.prompt} index={index} onDisclosureChange={onDisclosureChange} />
843
+ {attachedInputs.length > 0 ? (
844
+ <PendingMachineInputs inputs={attachedInputs} attached />
845
+ ) : null}
783
846
  <div className="mt-1 flex min-w-0 items-center gap-2 overflow-hidden whitespace-nowrap text-2xs text-fg-subtle">
784
847
  {turn.resources.length > 0 ? (
785
848
  <span className="shrink-0">
@@ -803,6 +866,7 @@ function SortableQueueRow({
803
866
  turn,
804
867
  index,
805
868
  count,
869
+ attachedInputs,
806
870
  pending,
807
871
  confirmingReplace,
808
872
  keyboardDragging,
@@ -818,6 +882,7 @@ function SortableQueueRow({
818
882
  turn: SessionTurn;
819
883
  index: number;
820
884
  count: number;
885
+ attachedInputs: SessionPendingInputPreview[];
821
886
  pending: QueueMutationKind | null;
822
887
  confirmingReplace: boolean;
823
888
  keyboardDragging: boolean;
@@ -864,6 +929,9 @@ function SortableQueueRow({
864
929
  </span>
865
930
  <div className="col-span-full row-start-2 min-w-0 sm:col-span-1 sm:col-start-3 sm:row-start-1">
866
931
  <QueuePrompt prompt={turn.prompt} index={index} onDisclosureChange={onDisclosureChange} />
932
+ {attachedInputs.length > 0 ? (
933
+ <PendingMachineInputs inputs={attachedInputs} attached />
934
+ ) : null}
867
935
  <div className="mt-1 flex min-w-0 items-center gap-2 overflow-hidden whitespace-nowrap text-2xs text-fg-subtle">
868
936
  {turn.resources.length > 0 ? (
869
937
  <span className="shrink-0">
@@ -886,7 +954,7 @@ function SortableQueueRow({
886
954
  onClick={onSteer}
887
955
  aria-label={`Steer queued prompt ${index + 1}`}
888
956
  title="Make this the next direction"
889
- className="inline-flex h-7 shrink-0 items-center justify-center gap-1 rounded-md px-2 text-xs font-medium outline-none transition-colors hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 pointer-coarse:min-h-[44px] pointer-coarse:min-w-[44px]"
957
+ className="inline-flex h-7 shrink-0 items-center justify-center gap-1 rounded-md px-2 text-xs font-medium text-fg outline-none transition-[background-color] hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 pointer-coarse:min-h-[44px] pointer-coarse:min-w-[44px]"
890
958
  >
891
959
  {pending === "steer" ? (
892
960
  <Loader2Icon className="size-3.5 animate-spin" />
@@ -11,7 +11,7 @@ const loadQueueSurface = () => import("./queue-surface-implementation");
11
11
  type QueueSurfaceModule = { QueueSurface: ComponentType<QueueSurfaceProps> };
12
12
  type QueueSurfaceLoader = () => Promise<QueueSurfaceModule>;
13
13
 
14
- /** The sole human prompt queue: compact above Goal, Agents, and composer. */
14
+ /** The sole pending-input surface: compact above Goal, Agents, and composer. */
15
15
  export const QueueSurface = createQueueSurface(loadQueueSurface);
16
16
 
17
17
  /** @internal Deterministic Suspense seam for the QueueSurface regression suite. */
@@ -26,20 +26,20 @@ function createQueueSurface(loadImplementation: QueueSurfaceLoader) {
26
26
 
27
27
  return function QueueSurfaceBoundary(props: QueueSurfaceProps) {
28
28
  const { queue } = props;
29
- if (queue.queue.length === 0) {
29
+ if (queue.queue.length + queue.pendingInputs.length === 0) {
30
30
  if (!queue.stoppingPreviousAttempt && !queue.error && !queue.mutationError) return null;
31
31
  return <EmptyQueueStateSurface queue={queue} />;
32
32
  }
33
33
 
34
34
  return (
35
- <Suspense fallback={<QueueSurfaceFallback count={queue.queue.length} />}>
35
+ <Suspense fallback={<QueueSurfaceFallback />}>
36
36
  <LazyQueueSurface {...props} />
37
37
  </Suspense>
38
38
  );
39
39
  };
40
40
  }
41
41
 
42
- function QueueSurfaceFallback({ count }: { count: number }) {
42
+ function QueueSurfaceFallback() {
43
43
  return (
44
44
  <div
45
45
  aria-live="polite"
@@ -53,7 +53,7 @@ function QueueSurfaceFallback({ count }: { count: number }) {
53
53
  aria-hidden="true"
54
54
  className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none"
55
55
  />
56
- Loading {count} queued prompt{count === 1 ? "" : "s"}
56
+ Loading inputs
57
57
  </div>
58
58
  </div>
59
59
  </div>
@@ -773,14 +773,8 @@ export function useComposer(
773
773
  // The wire contract requires non-empty text (z.string().min(1)) and the
774
774
  // worker rejects whitespace-only text; a file-only message therefore
775
775
  // carries a minimal default so the attachments still get delivered.
776
- const acknowledgedDraft = draftRef.current;
777
- const sendExtras =
778
- acknowledgedDraft?.toolsProvided === true &&
779
- !Object.prototype.hasOwnProperty.call(extras, "tools")
780
- ? { ...extras, tools: acknowledgedDraft.tools }
781
- : extras;
782
776
  pendingClientEventId.current ??= generateClientEventId();
783
- const input = composeSendInput(sendText, pendingClientEventId.current, sendExtras, {
777
+ const input = composeSendInput(sendText, pendingClientEventId.current, extras, {
784
778
  ...(options.effectiveControl?.controlEtag
785
779
  ? { controlEtag: options.effectiveControl.controlEtag }
786
780
  : {}),
@@ -1033,13 +1027,10 @@ export function useComposer(
1033
1027
  const hasDraftContent = useCallback((): boolean => {
1034
1028
  const current = draftRef.current;
1035
1029
  const extras = resolveSendExtras(sendExtrasRef.current);
1036
- const toolsProvidedByHost = Object.prototype.hasOwnProperty.call(extras, "tools");
1037
- const tools = toolsProvidedByHost ? (extras.tools ?? []) : (current?.tools ?? []);
1038
1030
  return (
1039
1031
  valueRef.current.length > 0 ||
1040
1032
  restoredResourcesRef.current.length > 0 ||
1041
1033
  (extras.resources?.length ?? 0) > 0 ||
1042
- tools.length > 0 ||
1043
1034
  (current?.sourceTurnId !== null && current?.sourceTurnId !== undefined)
1044
1035
  );
1045
1036
  }, []);
@@ -1201,8 +1192,6 @@ function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
1201
1192
  expectedRevision: draft.revision,
1202
1193
  text: draft.text,
1203
1194
  resources: draft.resources,
1204
- tools: draft.tools,
1205
- toolsProvided: draft.toolsProvided,
1206
1195
  model: draft.model,
1207
1196
  reasoningEffort: draft.reasoningEffort,
1208
1197
  };
@@ -1214,13 +1203,10 @@ function composerDraftPayload(
1214
1203
  restoredResources: ResourceRef[],
1215
1204
  extras: ComposerSendExtras,
1216
1205
  ): SaveComposerDraftRequest {
1217
- const toolsProvidedByHost = Object.prototype.hasOwnProperty.call(extras, "tools");
1218
1206
  return {
1219
1207
  expectedRevision: base.revision,
1220
1208
  text,
1221
1209
  resources: mergeResources(restoredResources, extras.resources ?? []),
1222
- tools: toolsProvidedByHost ? (extras.tools ?? []) : base.tools,
1223
- toolsProvided: toolsProvidedByHost ? true : base.toolsProvided,
1224
1210
  model: extras.model ?? base.model,
1225
1211
  reasoningEffort: extras.reasoningEffort ?? base.reasoningEffort,
1226
1212
  };
@@ -4,6 +4,7 @@ import type {
4
4
  SessionEvent,
5
5
  SessionQueueMutationResponse,
6
6
  SessionQueueSnapshot,
7
+ SessionPendingInputPreview,
7
8
  SessionTurn,
8
9
  } from "@opengeni/sdk";
9
10
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
@@ -20,6 +21,7 @@ export function isTurnQueueEvent(event: Pick<SessionEvent, "type">): boolean {
20
21
  event.type.startsWith("turn.") ||
21
22
  event.type.startsWith("session.queue.") ||
22
23
  event.type.startsWith("session.control.") ||
24
+ event.type.startsWith("system.update.") ||
23
25
  event.type.startsWith("workspace.inference.")
24
26
  );
25
27
  }
@@ -47,6 +49,10 @@ export type UseTurnQueueResult = {
47
49
  snapshot: SessionQueueSnapshot | null;
48
50
  /** Human/API prompts exactly in server execution order. Never client-sorted. */
49
51
  queue: SessionTurn[];
52
+ /** Canonical pending machine inputs. Events only trigger an authoritative refresh. */
53
+ pendingInputs: SessionPendingInputPreview[];
54
+ /** Exact pending members projected to join an already-waiting prompt. */
55
+ pendingInputAttachment: SessionQueueSnapshot["pendingInputAttachment"];
50
56
  effectiveControl: EffectiveSessionControl | null;
51
57
  /** The latest interrupted attempt has not yet durably proved physical quiescence. */
52
58
  stoppingPreviousAttempt: boolean;
@@ -324,6 +330,8 @@ export function useTurnQueue(
324
330
  return {
325
331
  snapshot: visibleSnapshot,
326
332
  queue: visibleSnapshot?.items ?? [],
333
+ pendingInputs: visibleSnapshot?.pendingInputs ?? [],
334
+ pendingInputAttachment: visibleSnapshot?.pendingInputAttachment ?? null,
327
335
  effectiveControl: visibleSnapshot?.effectiveControl ?? null,
328
336
  stoppingPreviousAttempt: visibleSnapshot?.stoppingPreviousAttempt ?? false,
329
337
  loading: identityMatches ? loading : enabled,
package/src/index.ts CHANGED
@@ -245,6 +245,8 @@ export type {
245
245
  AgentMessageItem,
246
246
  AuthNeededItem,
247
247
  GoalItem,
248
+ MachineInputBatchItem,
249
+ MachineInputMember,
248
250
  NoticeItem,
249
251
  ReasoningItem,
250
252
  SandboxItem,
@@ -286,6 +288,7 @@ export {
286
288
  TermBlock,
287
289
  Thumbnail,
288
290
  TurnSummary,
291
+ BUILT_IN_TURN_SUMMARY_FACET_IDS,
289
292
  useLightbox,
290
293
  useLightboxOptional,
291
294
  } from "./timeline";
@@ -293,7 +296,13 @@ export type {
293
296
  ActivityDisclosureProps,
294
297
  ActivityRailProps,
295
298
  DisclosureChip,
299
+ BuiltInTurnSummaryFacetId,
296
300
  TurnOutcome,
301
+ TurnSummaryContext,
302
+ TurnSummaryFacet,
303
+ TurnSummaryFacetConfiguration,
304
+ TurnSummaryFacetResult,
305
+ TurnSummaryOptions,
297
306
  TurnSummaryProps,
298
307
  } from "./timeline";
299
308
 
package/src/session-ui.ts CHANGED
@@ -9,5 +9,14 @@ export type {
9
9
  } from "./components/human-input-form";
10
10
  export { MessageTimeline, TimelineRow } from "./components/message-timeline";
11
11
  export type { MessageTimelineProps } from "./components/message-timeline";
12
+ export { BUILT_IN_TURN_SUMMARY_FACET_IDS } from "./timeline/turn-summary";
13
+ export type {
14
+ BuiltInTurnSummaryFacetId,
15
+ TurnSummaryContext,
16
+ TurnSummaryFacet,
17
+ TurnSummaryFacetConfiguration,
18
+ TurnSummaryFacetResult,
19
+ TurnSummaryOptions,
20
+ } from "./timeline/turn-summary";
12
21
  export { QueueSurface } from "./components/queue-surface";
13
22
  export type { QueueSurfaceProps } from "./components/queue-surface";
package/src/session.ts CHANGED
@@ -96,6 +96,8 @@ export type {
96
96
  AgentMessageItem,
97
97
  AuthNeededItem,
98
98
  GoalItem,
99
+ MachineInputBatchItem,
100
+ MachineInputMember,
99
101
  MemoryItem,
100
102
  NoticeItem,
101
103
  ReasoningItem,
@@ -33,6 +33,8 @@ export type {
33
33
  FleetDecisionItem,
34
34
  FleetDecisionScoreItem,
35
35
  GoalItem,
36
+ MachineInputBatchItem,
37
+ MachineInputMember,
36
38
  MemoryItem,
37
39
  NoticeItem,
38
40
  ReasoningItem,
@@ -84,8 +86,17 @@ export { LightboxProvider, useLightbox, useLightboxOptional } from "./screenshot
84
86
  export { DisclosureDefaultsProvider, useForcedDefaultOpen } from "./disclosure-context";
85
87
 
86
88
  // turn-collapse summary chip
87
- export { TurnSummary } from "./turn-summary";
88
- export type { TurnOutcome, TurnSummaryProps } from "./turn-summary";
89
+ export { BUILT_IN_TURN_SUMMARY_FACET_IDS, TurnSummary } from "./turn-summary";
90
+ export type {
91
+ BuiltInTurnSummaryFacetId,
92
+ TurnOutcome,
93
+ TurnSummaryContext,
94
+ TurnSummaryFacet,
95
+ TurnSummaryFacetConfiguration,
96
+ TurnSummaryFacetResult,
97
+ TurnSummaryOptions,
98
+ TurnSummaryProps,
99
+ } from "./turn-summary";
89
100
 
90
101
  // parsers (pure, reusable by custom renderers)
91
102
  export {
@@ -1,4 +1,4 @@
1
- import type { GitFileDiff } from "@opengeni/sdk";
1
+ import { normalizeMcpOutput, type GitFileDiff } from "@opengeni/sdk";
2
2
  import { tryParseJson } from "../lib/format";
3
3
 
4
4
  /* ----------------------------------------------------------------------------
@@ -288,36 +288,8 @@ export function unwrapMcpOutput(output: unknown): {
288
288
  text: string;
289
289
  isError: boolean;
290
290
  } {
291
- // Managed MCP events persist a normalized single text part as
292
- // `{ type: "text", text }`; accept it alongside the SDK-native content array.
293
- if (
294
- output &&
295
- typeof output === "object" &&
296
- (output as { type?: unknown }).type === "text" &&
297
- typeof (output as { text?: unknown }).text === "string"
298
- ) {
299
- const record = output as { text: string; isError?: unknown };
300
- return { text: record.text, isError: Boolean(record.isError) };
301
- }
302
- if (output && typeof output === "object" && "content" in output) {
303
- const record = output as { content?: unknown; isError?: unknown };
304
- const isError = Boolean(record.isError);
305
- if (Array.isArray(record.content)) {
306
- const textPart = record.content.find(
307
- (part): part is { type: string; text: string } =>
308
- !!part && typeof part === "object" && (part as { type?: unknown }).type === "text",
309
- );
310
- return {
311
- text: textPart ? String(textPart.text) : JSON.stringify(output),
312
- isError,
313
- };
314
- }
315
- return { text: JSON.stringify(output), isError };
316
- }
317
- return {
318
- text: typeof output === "string" ? output : output == null ? "" : JSON.stringify(output),
319
- isError: false,
320
- };
291
+ const normalized = normalizeMcpOutput(output);
292
+ return { text: normalized.text, isError: normalized.isError };
321
293
  }
322
294
 
323
295
  /* --- computer-use screenshot extraction ------------------------------------- */