@opengeni/react 0.9.0 → 0.11.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
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": {
@@ -41,11 +41,12 @@
41
41
  "typecheck": "tsc --noEmit",
42
42
  "build": "tsup",
43
43
  "demo": "vite dev demo --port 3100",
44
- "demo:build": "vite build demo"
44
+ "demo:build": "vite build demo",
45
+ "prepublishOnly": "bash ../../scripts/prepublish-guard"
45
46
  },
46
47
  "dependencies": {
47
48
  "@bufbuild/protobuf": "^2.2.0",
48
- "@opengeni/sdk": "^0.9.0",
49
+ "@opengeni/sdk": "^0.11.0",
49
50
  "clsx": "^2.1.1",
50
51
  "lucide-react": "^1.8.0",
51
52
  "motion": "^12.0.0",
@@ -108,33 +109,5 @@
108
109
  "@xterm/xterm": {
109
110
  "optional": true
110
111
  }
111
- },
112
- "devDependencies": {
113
- "@codemirror/lang-css": "^6.3.1",
114
- "@opengeni/agent-proto": "workspace:*",
115
- "@codemirror/lang-html": "^6.4.11",
116
- "@codemirror/lang-javascript": "^6.2.5",
117
- "@codemirror/lang-json": "^6.0.2",
118
- "@codemirror/lang-markdown": "^6.5.0",
119
- "@codemirror/lang-python": "^6.2.1",
120
- "@fontsource-variable/inter": "^5.2.8",
121
- "@fontsource-variable/jetbrains-mono": "^5.2.8",
122
- "@happy-dom/global-registrator": "^20.10.2",
123
- "@novnc/novnc": "^1.7.0",
124
- "@pierre/diffs": "^1.2.11",
125
- "@tailwindcss/vite": "^4.2.4",
126
- "@uiw/react-codemirror": "^4.25.10",
127
- "@types/react": "^19.2.14",
128
- "@types/react-dom": "^19.2.3",
129
- "@vitejs/plugin-react": "^6.0.1",
130
- "@xterm/addon-fit": "^0.11.0",
131
- "@xterm/addon-web-links": "^0.12.0",
132
- "@xterm/xterm": "^6.0.0",
133
- "react": "^19.2.5",
134
- "react-dom": "^19.2.5",
135
- "tailwindcss": "^4.2.4",
136
- "tsup": "^8.5.0",
137
- "typescript": "^6.0.3",
138
- "vite": "^8.0.9"
139
112
  }
140
113
  }
@@ -279,13 +279,14 @@ export function MessageTimeline({
279
279
  <span className="og-shimmer-text font-medium">Loading earlier activity…</span>
280
280
  </div>
281
281
  ) : null}
282
- {groups.map((group) => (
282
+ {groups.map((group, index) => (
283
283
  <TimelineGroupView
284
284
  key={timelineGroupKey(group)}
285
285
  group={group}
286
286
  renderMessageText={renderMessageText}
287
287
  onOpenSession={onOpenSession}
288
288
  toolRegistry={toolRegistry}
289
+ foldLiveCluster={isAgentProgress(groups[index + 1])}
289
290
  />
290
291
  ))}
291
292
  {working ? (
@@ -350,11 +351,16 @@ function TimelineGroupView({
350
351
  onOpenSession,
351
352
  toolRegistry,
352
353
  insideTurn = false,
354
+ foldLiveCluster = false,
353
355
  }: {
354
356
  group: TimelineGroup;
355
357
  renderMessageText?: ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
356
358
  onOpenSession?: ((sessionId: string) => void) | undefined;
357
359
  toolRegistry: ToolRegistry;
360
+ /** A completed cluster of a still-RUNNING turn (not the live tail) folds
361
+ behind a neutral chip — the one place activity without an outcome still
362
+ folds, bounding the DOM of days-long autonomous turns. */
363
+ foldLiveCluster?: boolean;
358
364
  /** Rendering inside an expanded turn group: the outer chip already owns the
359
365
  failure surface, so nested chips stay tinted but quiet (no repeated
360
366
  failure text, no auto-open) — one loud error, N calm sub-expands. */
@@ -362,7 +368,7 @@ function TimelineGroupView({
362
368
  }) {
363
369
  switch (group.kind) {
364
370
  case "activity":
365
- return group.outcome ? (
371
+ return group.outcome || (foldLiveCluster && clusterIsSettled(group)) ? (
366
372
  <TurnSummary
367
373
  items={group.items}
368
374
  outcome={group.outcome}
@@ -417,6 +423,30 @@ function timelineGroupKey(group: TimelineGroup): string {
417
423
  }
418
424
  }
419
425
 
426
+ /** The agent has moved PAST a cluster only when what follows is more agent
427
+ progress — new activity, a settled turn, or narration. A waiting notice
428
+ (approval pause), a pending queued message, a goal pill, or nothing at all
429
+ do NOT advance the story, and folding on them would hide exactly the work
430
+ the reader needs in view. */
431
+ function isAgentProgress(next: TimelineGroup | undefined): boolean {
432
+ if (!next) {
433
+ return false;
434
+ }
435
+ return next.kind === "activity" || next.kind === "turn" || (next.kind === "item" && next.item.kind === "agent-message");
436
+ }
437
+
438
+ /** No item still running or streaming — the only state safe to fold live.
439
+ Position alone is a broken proxy: a pending queued message (or any trailing
440
+ item) can sit after the ACTIVE cluster, which must never fold mid-work. */
441
+ function clusterIsSettled(group: Extract<TimelineGroup, { kind: "activity" }>): boolean {
442
+ return group.items.every((item) => {
443
+ if (item.kind === "reasoning") {
444
+ return !item.streaming;
445
+ }
446
+ return item.status !== "running";
447
+ });
448
+ }
449
+
420
450
  function flattenActivityItems(groups: TimelineGroup[]): ActivityItem[] {
421
451
  const items: ActivityItem[] = [];
422
452
  for (const group of groups) {
@@ -596,7 +626,17 @@ function NoticeRow({ item }: { item: NoticeItem }) {
596
626
  return (
597
627
  <div className={cn(enter && "animate-og-enter", "flex items-start gap-2.5 rounded-og-md border px-3.5 py-2.5 text-sm", tone)} role="status">
598
628
  <TriangleAlertIcon className={cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60")} />
599
- <span className="min-w-0 whitespace-pre-wrap break-words">{item.text}</span>
629
+ <span className="min-w-0 flex-1 whitespace-pre-wrap break-words">{item.text}</span>
630
+ {item.action ? (
631
+ <a
632
+ className="shrink-0 rounded-og-sm border border-current/25 px-2 py-1 text-xs font-medium hover:bg-current/10"
633
+ href={item.action.url}
634
+ rel="noreferrer"
635
+ target="_blank"
636
+ >
637
+ {item.action.label}
638
+ </a>
639
+ ) : null}
600
640
  </div>
601
641
  );
602
642
  }
@@ -262,7 +262,10 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
262
262
  }
263
263
 
264
264
  case "sandbox.command.output.delta": {
265
- const text = typeof payload.text === "string" ? payload.text : typeof payload.output === "string" ? payload.output : "";
265
+ // `chunk` is the canonical wire field; text/output are legacy shapes.
266
+ const text = typeof payload.chunk === "string"
267
+ ? payload.chunk
268
+ : typeof payload.text === "string" ? payload.text : typeof payload.output === "string" ? payload.output : "";
266
269
  if (!text) {
267
270
  break;
268
271
  }
@@ -310,6 +313,20 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
310
313
  break;
311
314
  }
312
315
 
316
+ case "tool.auth_needed": {
317
+ closeStreamingTail();
318
+ const authorizationUrl = typeof payload.authorizationUrl === "string" ? payload.authorizationUrl : null;
319
+ items.push({
320
+ kind: "notice",
321
+ id: event.id,
322
+ tone: "waiting",
323
+ text: authNeededNoticeText(payload),
324
+ ...(authorizationUrl ? { action: { label: "Connect", url: authorizationUrl } } : {}),
325
+ occurredAt: event.occurredAt,
326
+ });
327
+ break;
328
+ }
329
+
313
330
  case "turn.completed": {
314
331
  finalizeOpen(turnId);
315
332
  items.push(turnEndItem(event, "complete", null));
@@ -812,6 +829,22 @@ function goalText(payload: Record<string, unknown>): string | null {
812
829
  return null;
813
830
  }
814
831
 
832
+ function authNeededNoticeText(payload: Record<string, unknown>): string {
833
+ const provider =
834
+ typeof payload.providerDomain === "string" && payload.providerDomain.trim().length > 0 ? payload.providerDomain.trim() : "This service";
835
+ const scopes = Array.isArray(payload.scopes)
836
+ ? payload.scopes.filter((scope): scope is string => typeof scope === "string" && scope.trim().length > 0)
837
+ : [];
838
+
839
+ if (payload.reason === "insufficient_scope") {
840
+ return scopes.length > 0 ? `${provider} needs additional access (${scopes.join(", ")}).` : `${provider} needs additional access.`;
841
+ }
842
+ if (payload.reason === "expired" || payload.reason === "refresh_failed") {
843
+ return `${provider} needs to be reconnected.`;
844
+ }
845
+ return `${provider} needs a connection.`;
846
+ }
847
+
815
848
  function reasoningText(payload: unknown): string {
816
849
  const record = asRecord(payload);
817
850
  if (typeof record.text === "string") {
@@ -24,7 +24,12 @@ export type { TurnOutcome } from "./types";
24
24
  export type TurnSummaryProps = {
25
25
  /** The activity items in the turn (used only to compute the facet counts). */
26
26
  items: ActivityItem[];
27
- outcome: TurnOutcome;
27
+ /**
28
+ * The settled verdict — or absent for a completed CLUSTER of a still-running
29
+ * turn, which folds neutrally: no verdict glyph (the turn has none yet), a
30
+ * quiet pulse dot in its place so alignment and the running feel both hold.
31
+ */
32
+ outcome?: TurnOutcome | undefined;
28
33
  /** A short failure reason shown inline on a failed chip (never hidden). */
29
34
  failureText?: string | undefined;
30
35
  /** Elapsed turn duration; shown as a trailing facet when at least 1s. */
@@ -78,8 +83,10 @@ export function TurnSummary({ items, outcome, failureText, durationMs, defaultOp
78
83
  <TriangleAlertIcon className="size-3" />
79
84
  ) : outcome === "cancelled" ? (
80
85
  <CircleSlashIcon className="size-3" />
81
- ) : (
86
+ ) : outcome === "complete" ? (
82
87
  <CheckIcon className="size-3.5" />
88
+ ) : (
89
+ <span className="size-1.5 animate-og-pulse rounded-full bg-og-fg-subtle" />
83
90
  )}
84
91
  </span>
85
92
  <span className="min-w-0 flex-1 truncate text-og-fg-muted">
@@ -116,6 +116,7 @@ export type NoticeItem = {
116
116
  id: string;
117
117
  tone: "waiting" | "cancelled" | "failed";
118
118
  text: string;
119
+ action?: { label: string; url: string };
119
120
  occurredAt: string;
120
121
  };
121
122