@opengeni/react 0.6.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "0.6.1",
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 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">
291
- {renderMessageText ? renderMessageText(item.text, item) : <Markdown>{item.text}</Markdown>}
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 ordered = [...events].sort((a, b) => a.sequence - b.sequence);
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,
@@ -332,6 +334,12 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
332
334
  }
333
335
 
334
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
+ }
335
343
  const hadActivity = hasTurnActivity(items, turnId);
336
344
  finalizeOpen(turnId, "cancelled");
337
345
  items.push(turnEndItem(event, "cancelled", null));
@@ -433,6 +441,128 @@ export function groupTimeline(items: TimelineItem[]): TimelineGroup[] {
433
441
 
434
442
  /* --- helpers ---------------------------------------------------------------- */
435
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
+
436
566
  function turnEndItem(
437
567
  event: SessionEvent,
438
568
  outcome: TurnEndItem["outcome"],
@@ -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. */