@opengeni/react 0.26.2 → 0.28.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 (35) hide show
  1. package/README.md +32 -0
  2. package/dist/{chunk-OZDLELJQ.js → chunk-OCHBZ2X7.js} +40 -10
  3. package/dist/chunk-OCHBZ2X7.js.map +1 -0
  4. package/dist/{chunk-NBYNFI5T.js → chunk-PMSGE233.js} +5 -3
  5. package/dist/chunk-PMSGE233.js.map +1 -0
  6. package/dist/{chunk-GQN2QIR2.js → chunk-WAA247AB.js} +178 -87
  7. package/dist/chunk-WAA247AB.js.map +1 -0
  8. package/dist/index.d.ts +7 -34
  9. package/dist/index.js +5 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/{queue-surface-implementation-3UW3MIIR.js → queue-surface-implementation-UQ535OKQ.js} +44 -12
  12. package/dist/queue-surface-implementation-UQ535OKQ.js.map +1 -0
  13. package/dist/{session-LsampPc6.d.ts → session-C3tIXLim.d.ts} +1 -1
  14. package/dist/{session-ui-BPOV-tuY.d.ts → session-ui-B_7cfN5u.d.ts} +81 -5
  15. package/dist/session-ui.d.ts +2 -2
  16. package/dist/session-ui.js +4 -2
  17. package/dist/session.d.ts +2 -2
  18. package/dist/session.js +2 -2
  19. package/dist/{use-turn-queue-aDnQvzVz.d.ts → use-turn-queue-Drm_ypue.d.ts} +7 -3
  20. package/package.json +2 -2
  21. package/src/components/message-timeline.tsx +10 -0
  22. package/src/components/queue-surface-implementation.tsx +79 -11
  23. package/src/components/queue-surface.tsx +5 -5
  24. package/src/hooks/use-turn-queue.ts +8 -0
  25. package/src/index.ts +7 -0
  26. package/src/session-ui.ts +9 -0
  27. package/src/timeline/index.ts +11 -2
  28. package/src/timeline/parsers.ts +3 -31
  29. package/src/timeline/projection.ts +53 -9
  30. package/src/timeline/turn-summary.tsx +243 -56
  31. package/src/timeline/types.ts +1 -1
  32. package/dist/chunk-GQN2QIR2.js.map +0 -1
  33. package/dist/chunk-NBYNFI5T.js.map +0 -1
  34. package/dist/chunk-OZDLELJQ.js.map +0 -1
  35. package/dist/queue-surface-implementation-3UW3MIIR.js.map +0 -1
@@ -46,6 +46,7 @@ import {
46
46
  type TimelineGroup,
47
47
  type TimelineItem,
48
48
  type ToolRegistry,
49
+ type TurnSummaryOptions,
49
50
  type UserMessageItem,
50
51
  type WorkerCompletionItem,
51
52
  TurnSummary,
@@ -97,6 +98,8 @@ export type MessageTimelineProps = {
97
98
  * `createDefaultToolRegistry({ entries })` to add custom tool renderers.
98
99
  */
99
100
  toolRegistry?: ToolRegistry | undefined;
101
+ /** Customize collapsed turn facets for this timeline instance. */
102
+ turnSummary?: TurnSummaryOptions | undefined;
100
103
  /** Follow new events when pinned to the bottom. Defaults to true. */
101
104
  autoFollow?: boolean | undefined;
102
105
  /** Older durable history exists above the current window (see useSessionEvents). */
@@ -127,6 +130,7 @@ export function MessageTimeline({
127
130
  onReconnect,
128
131
  resolveProviderLogo,
129
132
  toolRegistry = defaultToolRegistry,
133
+ turnSummary,
130
134
  autoFollow = true,
131
135
  hasOlder = false,
132
136
  loadingOlder = false,
@@ -414,6 +418,7 @@ export function MessageTimeline({
414
418
  onReconnect={onReconnect}
415
419
  resolveProviderLogo={resolveProviderLogo}
416
420
  toolRegistry={toolRegistry}
421
+ turnSummary={turnSummary}
417
422
  foldLiveCluster={isAgentProgress(groups[index + 1]?.group)}
418
423
  />
419
424
  </div>
@@ -621,6 +626,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
621
626
  onReconnect,
622
627
  resolveProviderLogo,
623
628
  toolRegistry,
629
+ turnSummary,
624
630
  insideTurn = false,
625
631
  foldLiveCluster = false,
626
632
  }: {
@@ -633,6 +639,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
633
639
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
634
640
  resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
635
641
  toolRegistry: ToolRegistry;
642
+ turnSummary?: TurnSummaryOptions | undefined;
636
643
  /** A completed cluster of a still-RUNNING turn (not the live tail) folds
637
644
  behind a neutral chip — the one place activity without an outcome still
638
645
  folds, bounding the DOM of days-long autonomous turns. */
@@ -651,6 +658,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
651
658
  failureText={insideTurn ? undefined : group.failureText}
652
659
  defaultOpen={!insideTurn && group.outcome === "failed" ? true : undefined}
653
660
  bare={insideTurn}
661
+ facets={turnSummary?.facets}
654
662
  >
655
663
  <ActivityRail
656
664
  items={group.items}
@@ -683,6 +691,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
683
691
  onReconnect={onReconnect}
684
692
  resolveProviderLogo={resolveProviderLogo}
685
693
  toolRegistry={toolRegistry}
694
+ turnSummary={turnSummary}
686
695
  insideTurn
687
696
  />
688
697
  ));
@@ -697,6 +706,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
697
706
  durationMs={durationBetween(group.startedAt, group.endedAt)}
698
707
  defaultOpen={!insideTurn && group.outcome === "failed" ? true : undefined}
699
708
  bare={insideTurn}
709
+ facets={turnSummary?.facets}
700
710
  >
701
711
  {insideTurn ? (
702
712
  // A nested turn is already on an ancestor rail — its body just stacks
@@ -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>
@@ -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
@@ -286,6 +286,7 @@ export {
286
286
  TermBlock,
287
287
  Thumbnail,
288
288
  TurnSummary,
289
+ BUILT_IN_TURN_SUMMARY_FACET_IDS,
289
290
  useLightbox,
290
291
  useLightboxOptional,
291
292
  } from "./timeline";
@@ -293,7 +294,13 @@ export type {
293
294
  ActivityDisclosureProps,
294
295
  ActivityRailProps,
295
296
  DisclosureChip,
297
+ BuiltInTurnSummaryFacetId,
296
298
  TurnOutcome,
299
+ TurnSummaryContext,
300
+ TurnSummaryFacet,
301
+ TurnSummaryFacetConfiguration,
302
+ TurnSummaryFacetResult,
303
+ TurnSummaryOptions,
297
304
  TurnSummaryProps,
298
305
  } from "./timeline";
299
306
 
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";
@@ -84,8 +84,17 @@ export { LightboxProvider, useLightbox, useLightboxOptional } from "./screenshot
84
84
  export { DisclosureDefaultsProvider, useForcedDefaultOpen } from "./disclosure-context";
85
85
 
86
86
  // turn-collapse summary chip
87
- export { TurnSummary } from "./turn-summary";
88
- export type { TurnOutcome, TurnSummaryProps } from "./turn-summary";
87
+ export { BUILT_IN_TURN_SUMMARY_FACET_IDS, TurnSummary } from "./turn-summary";
88
+ export type {
89
+ BuiltInTurnSummaryFacetId,
90
+ TurnOutcome,
91
+ TurnSummaryContext,
92
+ TurnSummaryFacet,
93
+ TurnSummaryFacetConfiguration,
94
+ TurnSummaryFacetResult,
95
+ TurnSummaryOptions,
96
+ TurnSummaryProps,
97
+ } from "./turn-summary";
89
98
 
90
99
  // parsers (pure, reusable by custom renderers)
91
100
  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 ------------------------------------- */
@@ -108,14 +108,14 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
108
108
  goalText: childCompletion.goalText,
109
109
  evidence: childCompletion.evidence,
110
110
  pausedReason: childCompletion.pausedReason,
111
- text: typeof payload.text === "string" ? payload.text : "",
111
+ text: stringValue(payload.text),
112
112
  });
113
113
  break;
114
114
  }
115
115
  items.push({
116
116
  kind: "user-message",
117
117
  id: event.id,
118
- text: typeof payload.text === "string" ? payload.text : "",
118
+ text: stringValue(payload.text),
119
119
  resources: resourceRefs(payload.resources),
120
120
  tools: toolRefs(payload.tools),
121
121
  occurredAt: event.occurredAt,
@@ -123,8 +123,23 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
123
123
  break;
124
124
  }
125
125
 
126
+ case "system.update.delivered": {
127
+ const inputs = machineInputMembers(payload.members);
128
+ if (inputs.length === 0) break;
129
+ closeStreamingTail();
130
+ items.push({
131
+ kind: "notice",
132
+ id: event.id,
133
+ tone: "input",
134
+ text: "Input batch",
135
+ details: { label: "Inputs", value: inputs },
136
+ occurredAt: event.occurredAt,
137
+ });
138
+ break;
139
+ }
140
+
126
141
  case "agent.message.delta": {
127
- const text = typeof payload.text === "string" ? payload.text : "";
142
+ const text = stringValue(payload.text);
128
143
  if (!text) {
129
144
  break;
130
145
  }
@@ -146,7 +161,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
146
161
  }
147
162
 
148
163
  case "agent.message.completed": {
149
- const text = typeof payload.text === "string" ? payload.text : "";
164
+ const text = stringValue(payload.text);
150
165
  // Reconcile the most recent same-turn agent message — even when
151
166
  // activity (tool calls, reasoning) landed after its deltas — so the
152
167
  // completed text never duplicates the streamed one.
@@ -454,7 +469,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
454
469
  kind: "auth-needed",
455
470
  id: event.id,
456
471
  turnId,
457
- providerDomain: typeof payload.providerDomain === "string" ? payload.providerDomain : "",
472
+ providerDomain: stringValue(payload.providerDomain),
458
473
  connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null,
459
474
  reason: authNeededReason(payload.reason),
460
475
  scopes: stringList(payload.scopes),
@@ -968,7 +983,12 @@ function foldSettledTurn(groups: TimelineGroup[], turnEnd: TurnEndItem): void {
968
983
  }
969
984
 
970
985
  function isTurnBoundary(group: TimelineGroup | undefined): boolean {
971
- return group?.kind === "turn" || (group?.kind === "item" && group.item.kind === "user-message");
986
+ return (
987
+ group?.kind === "turn" ||
988
+ (group?.kind === "item" &&
989
+ (group.item.kind === "user-message" ||
990
+ (group.item.kind === "notice" && group.item.tone === "input")))
991
+ );
972
992
  }
973
993
 
974
994
  function belongsToDifferentTurn(group: TimelineGroup | undefined, turnId: string | null): boolean {
@@ -1029,6 +1049,30 @@ function asRecord(value: unknown): Record<string, unknown> {
1029
1049
  return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : {};
1030
1050
  }
1031
1051
 
1052
+ function stringValue(value: unknown): string {
1053
+ return typeof value === "string" ? value : "";
1054
+ }
1055
+
1056
+ function machineInputMembers(value: unknown) {
1057
+ return Array.isArray(value)
1058
+ ? value.flatMap((candidate) => {
1059
+ const member = asRecord(candidate);
1060
+ return typeof member.id === "string" &&
1061
+ typeof member.kind === "string" &&
1062
+ typeof member.sourceId === "string"
1063
+ ? [
1064
+ {
1065
+ id: member.id,
1066
+ kind: member.kind.replace("_terminal", "").replaceAll("_", " "),
1067
+ sourceId: member.sourceId,
1068
+ summary: stringValue(member.summary),
1069
+ },
1070
+ ]
1071
+ : [];
1072
+ })
1073
+ : [];
1074
+ }
1075
+
1032
1076
  const SESSION_STATUSES: readonly SessionStatus[] = [
1033
1077
  "queued",
1034
1078
  "running",
@@ -1201,8 +1245,8 @@ function memoryItem(
1201
1245
  id,
1202
1246
  turnId,
1203
1247
  variant: type === "memory.corrected" ? "corrected" : "saved",
1204
- memoryKind: typeof payload.kind === "string" ? payload.kind : "",
1205
- preview: typeof payload.preview === "string" ? payload.preview : "",
1248
+ memoryKind: stringValue(payload.kind),
1249
+ preview: stringValue(payload.preview),
1206
1250
  ...(payload.deduped === true ? { deduped: true } : {}),
1207
1251
  ...(replacementPreview ? { replacementPreview } : {}),
1208
1252
  ...(action ? { action } : {}),
@@ -1245,7 +1289,7 @@ function reasoningText(payload: unknown): string {
1245
1289
  return content
1246
1290
  .map((part) => {
1247
1291
  const text = asRecord(part).text;
1248
- return typeof text === "string" ? text : "";
1292
+ return stringValue(text);
1249
1293
  })
1250
1294
  .join("");
1251
1295
  }