@opengeni/react 0.6.0 → 0.6.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/dist/index.d.ts +2 -0
- package/dist/index.js +116 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/message-timeline.tsx +5 -2
- package/src/timeline/projection.ts +142 -1
- package/src/timeline/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -287,8 +287,11 @@ function UserMessageRow({
|
|
|
287
287
|
}) {
|
|
288
288
|
return (
|
|
289
289
|
<div className="animate-og-enter flex justify-end">
|
|
290
|
-
<div className="max-w-[85%] min-w-0
|
|
291
|
-
{
|
|
290
|
+
<div className="flex max-w-[85%] min-w-0 flex-col items-end gap-1">
|
|
291
|
+
{item.pending ? <span className="px-1 text-og-xs text-og-fg-subtle">queued</span> : null}
|
|
292
|
+
<div className="w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg">
|
|
293
|
+
{renderMessageText ? renderMessageText(item.text, item) : <Markdown>{item.text}</Markdown>}
|
|
294
|
+
</div>
|
|
292
295
|
</div>
|
|
293
296
|
</div>
|
|
294
297
|
);
|
|
@@ -34,7 +34,8 @@ const WORKER_INTERRUPT_TOOL = "session_interrupt";
|
|
|
34
34
|
|
|
35
35
|
export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
36
36
|
const items: TimelineItem[] = [];
|
|
37
|
-
const
|
|
37
|
+
const prescan = prescanTurnAnchors(events);
|
|
38
|
+
const ordered = orderTimelineEvents(events, prescan);
|
|
38
39
|
|
|
39
40
|
const last = (): TimelineItem | undefined => items[items.length - 1];
|
|
40
41
|
|
|
@@ -76,6 +77,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
76
77
|
kind: "user-message",
|
|
77
78
|
id: event.id,
|
|
78
79
|
text: typeof payload.text === "string" ? payload.text : "",
|
|
80
|
+
...(event.pendingUserMessage ? { pending: true } : {}),
|
|
79
81
|
resources: resourceRefs(payload.resources),
|
|
80
82
|
tools: toolRefs(payload.tools),
|
|
81
83
|
occurredAt: event.occurredAt,
|
|
@@ -280,6 +282,14 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
280
282
|
if (!isSessionStatus(status)) {
|
|
281
283
|
break;
|
|
282
284
|
}
|
|
285
|
+
// Only attention-worthy statuses earn a timeline divider. queued /
|
|
286
|
+
// running / idle are machinery telemetry: the header pill carries the
|
|
287
|
+
// live status, the shimmer says "running", and the turn chip's duration
|
|
288
|
+
// facet says how long — a stale "idle · 27s" row is pure noise,
|
|
289
|
+
// especially in historical traces.
|
|
290
|
+
if (!ATTENTION_STATUSES.has(status)) {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
283
293
|
const previous = [...items].reverse().find((item): item is SessionStatusItem => item.kind === "session-status");
|
|
284
294
|
if (previous?.status === status) {
|
|
285
295
|
break;
|
|
@@ -324,6 +334,12 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
|
|
|
324
334
|
}
|
|
325
335
|
|
|
326
336
|
case "turn.cancelled": {
|
|
337
|
+
// A retraction of a never-started queued turn is not a turn ending —
|
|
338
|
+
// the message was withdrawn before any work happened; show nothing.
|
|
339
|
+
// A null turnId proves nothing, so it keeps the legacy finalize path.
|
|
340
|
+
if (turnId && !prescan.startedTurnIds.has(turnId)) {
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
327
343
|
const hadActivity = hasTurnActivity(items, turnId);
|
|
328
344
|
finalizeOpen(turnId, "cancelled");
|
|
329
345
|
items.push(turnEndItem(event, "cancelled", null));
|
|
@@ -425,6 +441,128 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
|
|
|
425
441
|
|
|
426
442
|
/* --- helpers ---------------------------------------------------------------- */
|
|
427
443
|
|
|
444
|
+
type TimelineEvent = SessionEvent & { pendingUserMessage?: true };
|
|
445
|
+
|
|
446
|
+
type TurnAnchorPrescan = {
|
|
447
|
+
queuedTurnByTrigger: Map<string, string>;
|
|
448
|
+
startSeqByTrigger: Map<string, number>;
|
|
449
|
+
cancelledBeforeStartTriggers: Set<string>;
|
|
450
|
+
startedTurnIds: Set<string>;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
function prescanTurnAnchors(events: SessionEvent[]): TurnAnchorPrescan {
|
|
454
|
+
const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
|
|
455
|
+
const queuedTurnByTrigger = new Map<string, string>();
|
|
456
|
+
const startSeqByTrigger = new Map<string, number>();
|
|
457
|
+
const cancelledTurnIds = new Set<string>();
|
|
458
|
+
const fallbackSeqByTurn = new Map<string, number>();
|
|
459
|
+
const startedTurnIds = new Set<string>();
|
|
460
|
+
|
|
461
|
+
for (const event of ordered) {
|
|
462
|
+
const payload = asRecord(event.payload);
|
|
463
|
+
const turnId = event.turnId ?? null;
|
|
464
|
+
if (event.type === "turn.queued") {
|
|
465
|
+
const triggerEventId = typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
|
|
466
|
+
const queuedTurnId = typeof payload.turnId === "string" ? payload.turnId : turnId;
|
|
467
|
+
if (triggerEventId && queuedTurnId) {
|
|
468
|
+
queuedTurnByTrigger.set(triggerEventId, queuedTurnId);
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (event.type === "turn.started") {
|
|
473
|
+
const triggerEventId = typeof payload.triggerEventId === "string" ? payload.triggerEventId : null;
|
|
474
|
+
if (triggerEventId) {
|
|
475
|
+
startSeqByTrigger.set(triggerEventId, event.sequence);
|
|
476
|
+
}
|
|
477
|
+
if (turnId) {
|
|
478
|
+
startedTurnIds.add(turnId);
|
|
479
|
+
}
|
|
480
|
+
} else if (event.type === "turn.cancelled" && turnId) {
|
|
481
|
+
cancelledTurnIds.add(turnId);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (turnId && event.type !== "turn.queued" && event.type !== "turn.cancelled") {
|
|
485
|
+
const previous = fallbackSeqByTurn.get(turnId);
|
|
486
|
+
if (previous === undefined || event.sequence < previous) {
|
|
487
|
+
fallbackSeqByTurn.set(turnId, event.sequence);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (turnId && isAgentActivityEvent(event.type)) {
|
|
491
|
+
startedTurnIds.add(turnId);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
|
|
496
|
+
if (!startSeqByTrigger.has(triggerEventId)) {
|
|
497
|
+
const fallbackSeq = fallbackSeqByTurn.get(turnId);
|
|
498
|
+
if (fallbackSeq !== undefined) {
|
|
499
|
+
startSeqByTrigger.set(triggerEventId, fallbackSeq);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const cancelledBeforeStartTriggers = new Set<string>();
|
|
505
|
+
for (const [triggerEventId, turnId] of queuedTurnByTrigger) {
|
|
506
|
+
if (cancelledTurnIds.has(turnId) && !startSeqByTrigger.has(triggerEventId) && !fallbackSeqByTurn.has(turnId)) {
|
|
507
|
+
cancelledBeforeStartTriggers.add(triggerEventId);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return { queuedTurnByTrigger, startSeqByTrigger, cancelledBeforeStartTriggers, startedTurnIds };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function orderTimelineEvents(events: SessionEvent[], prescan: TurnAnchorPrescan): TimelineEvent[] {
|
|
515
|
+
const ordered = [...events].sort((a, b) => a.sequence - b.sequence);
|
|
516
|
+
const insertions = new Map<number, TimelineEvent[]>();
|
|
517
|
+
const pending: TimelineEvent[] = [];
|
|
518
|
+
|
|
519
|
+
for (const event of ordered) {
|
|
520
|
+
if (event.type !== "user.message") {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const queuedTurnId = prescan.queuedTurnByTrigger.get(event.id);
|
|
524
|
+
if (!queuedTurnId) {
|
|
525
|
+
pushInsertion(insertions, event.sequence, event);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (prescan.cancelledBeforeStartTriggers.has(event.id)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const startSeq = prescan.startSeqByTrigger.get(event.id);
|
|
532
|
+
if (startSeq !== undefined) {
|
|
533
|
+
pushInsertion(insertions, startSeq, event);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
pending.push({ ...event, pendingUserMessage: true });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const projected: TimelineEvent[] = [];
|
|
540
|
+
for (const event of ordered) {
|
|
541
|
+
const before = insertions.get(event.sequence);
|
|
542
|
+
if (before) {
|
|
543
|
+
projected.push(...before);
|
|
544
|
+
}
|
|
545
|
+
if (event.type !== "user.message") {
|
|
546
|
+
projected.push(event);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
projected.push(...pending);
|
|
550
|
+
return projected;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function pushInsertion(insertions: Map<number, TimelineEvent[]>, sequence: number, event: SessionEvent): void {
|
|
554
|
+
const bucket = insertions.get(sequence);
|
|
555
|
+
if (bucket) {
|
|
556
|
+
bucket.push(event);
|
|
557
|
+
} else {
|
|
558
|
+
insertions.set(sequence, [event]);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function isAgentActivityEvent(type: string): boolean {
|
|
563
|
+
return type.startsWith("agent.") || type.startsWith("sandbox.");
|
|
564
|
+
}
|
|
565
|
+
|
|
428
566
|
function turnEndItem(
|
|
429
567
|
event: SessionEvent,
|
|
430
568
|
outcome: TurnEndItem["outcome"],
|
|
@@ -577,6 +715,9 @@ function asRecord(value: unknown): Record<string, unknown> {
|
|
|
577
715
|
|
|
578
716
|
const SESSION_STATUSES: readonly SessionStatus[] = ["queued", "running", "idle", "requires_action", "failed", "cancelled"];
|
|
579
717
|
|
|
718
|
+
/** Statuses that demand the reader's attention and so earn a timeline divider. */
|
|
719
|
+
const ATTENTION_STATUSES: ReadonlySet<SessionStatus> = new Set(["requires_action", "failed", "cancelled"]);
|
|
720
|
+
|
|
580
721
|
/** Keep only entries that match the wire shapes; user payloads are untyped. */
|
|
581
722
|
function resourceRefs(value: unknown): import("@opengeni/sdk").ResourceRef[] {
|
|
582
723
|
if (!Array.isArray(value)) {
|
package/src/timeline/types.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type UserMessageItem = {
|
|
|
13
13
|
kind: "user-message";
|
|
14
14
|
id: string;
|
|
15
15
|
text: string;
|
|
16
|
+
/** Queued for a future turn whose start has not appeared in the event log. */
|
|
17
|
+
pending?: boolean;
|
|
16
18
|
/** Resources attached to this message (file uploads, repositories). */
|
|
17
19
|
resources: ResourceRef[];
|
|
18
20
|
/** Tools requested for the turn this message starts. */
|