@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.
- package/README.md +32 -0
- package/dist/{chunk-OZDLELJQ.js → chunk-OCHBZ2X7.js} +40 -10
- package/dist/chunk-OCHBZ2X7.js.map +1 -0
- package/dist/{chunk-NBYNFI5T.js → chunk-PMSGE233.js} +5 -3
- package/dist/chunk-PMSGE233.js.map +1 -0
- package/dist/{chunk-GQN2QIR2.js → chunk-WAA247AB.js} +178 -87
- package/dist/chunk-WAA247AB.js.map +1 -0
- package/dist/index.d.ts +7 -34
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/{queue-surface-implementation-3UW3MIIR.js → queue-surface-implementation-UQ535OKQ.js} +44 -12
- package/dist/queue-surface-implementation-UQ535OKQ.js.map +1 -0
- package/dist/{session-LsampPc6.d.ts → session-C3tIXLim.d.ts} +1 -1
- package/dist/{session-ui-BPOV-tuY.d.ts → session-ui-B_7cfN5u.d.ts} +81 -5
- package/dist/session-ui.d.ts +2 -2
- package/dist/session-ui.js +4 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/{use-turn-queue-aDnQvzVz.d.ts → use-turn-queue-Drm_ypue.d.ts} +7 -3
- package/package.json +2 -2
- package/src/components/message-timeline.tsx +10 -0
- package/src/components/queue-surface-implementation.tsx +79 -11
- package/src/components/queue-surface.tsx +5 -5
- package/src/hooks/use-turn-queue.ts +8 -0
- package/src/index.ts +7 -0
- package/src/session-ui.ts +9 -0
- package/src/timeline/index.ts +11 -2
- package/src/timeline/parsers.ts +3 -31
- package/src/timeline/projection.ts +53 -9
- package/src/timeline/turn-summary.tsx +243 -56
- package/src/timeline/types.ts +1 -1
- package/dist/chunk-GQN2QIR2.js.map +0 -1
- package/dist/chunk-NBYNFI5T.js.map +0 -1
- package/dist/chunk-OZDLELJQ.js.map +0 -1
- 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
|
|
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
|
-
() =>
|
|
91
|
-
|
|
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 (
|
|
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 &&
|
|
256
|
+
aria-description={!open && totalCount > 0 ? collapsedPreview.summary : undefined}
|
|
246
257
|
aria-expanded={open}
|
|
247
|
-
aria-label={`${count} queued prompt${count === 1 ? "" : "s"}${
|
|
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 &&
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
35
|
+
<Suspense fallback={<QueueSurfaceFallback />}>
|
|
36
36
|
<LazyQueueSurface {...props} />
|
|
37
37
|
</Suspense>
|
|
38
38
|
);
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function QueueSurfaceFallback(
|
|
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
|
|
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";
|
package/src/timeline/index.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
package/src/timeline/parsers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
-
|
|
292
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
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:
|
|
1205
|
-
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
|
|
1292
|
+
return stringValue(text);
|
|
1249
1293
|
})
|
|
1250
1294
|
.join("");
|
|
1251
1295
|
}
|