@opengeni/react 0.9.1 → 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/LICENSE +190 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +57 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -31
- package/src/components/message-timeline.tsx +43 -3
- package/src/timeline/projection.ts +34 -1
- package/src/timeline/turn-summary.tsx +9 -2
- package/src/timeline/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "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.
|
|
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
|
-
|
|
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
|
-
|
|
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">
|