@opengeni/react 0.6.3 → 0.9.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/README.md +17 -6
- package/dist/index.d.ts +25 -8
- package/dist/index.js +544 -218
- package/dist/index.js.map +1 -1
- package/dist/{machines-Bv9tG7qZ.d.ts → machines-BeZ3bD3t.d.ts} +1 -1
- package/dist/machines.d.ts +1 -1
- package/package.json +2 -2
- package/src/client.ts +1 -0
- package/src/components/message-timeline.tsx +144 -17
- package/src/hooks/use-session-capabilities.ts +3 -2
- package/src/hooks/use-session-events.ts +294 -8
- package/src/timeline/activity-rail.tsx +4 -1
- package/src/timeline/entrance.tsx +30 -0
- package/src/timeline/projection.ts +18 -3
- package/src/timeline/turn-summary.tsx +3 -1
|
@@ -7,7 +7,7 @@ import { ReactNode } from 'react';
|
|
|
7
7
|
* pass the real SDK client, a proxy-backed client that routes through their
|
|
8
8
|
* own API, or a scripted client in tests/demos.
|
|
9
9
|
*/
|
|
10
|
-
type SessionClientLike = Pick<OpenGeniClient, "getClientConfig" | "getSession" | "updateSession" | "listSessions" | "sendMessage" | "steerMessage" | "interrupt" | "sendApprovalDecision" | "streamEvents" | "listTurns" | "updateQueuedTurn" | "reorderQueuedTurns" | "deleteQueuedTurn" | "getGoal" | "updateGoal" | "clearSessionContext" | "compactSessionContext" | "listScheduledTasks" | "uploadFile" | "getFile" | "createFileDownloadUrl" | "listEnvironments" | "createEnvironment" | "updateEnvironment" | "deleteEnvironment" | "setEnvironmentVariable" | "deleteEnvironmentVariable" | "listPacks" | "registerPack" | "enablePack" | "deletePack" | "listWorkspaces" | "createWorkspace" | "updateWorkspace" | "getBillingUsage" | "getClientConfig" | "getStreamCapabilities" | "acknowledgeStream" | "attachViewer" | "heartbeatViewer" | "detachViewer" | "fsList" | "fsRead" | "fsWrite" | "fsDelete" | "fsMove" | "fsMkdir" | "gitStatus" | "gitDiff" | "terminalExec" | "terminalPtyOpen" | "terminalPtyWrite" | "terminalPtyResize" | "terminalPtyClose">;
|
|
10
|
+
type SessionClientLike = Pick<OpenGeniClient, "getClientConfig" | "getSession" | "updateSession" | "listSessions" | "sendMessage" | "steerMessage" | "interrupt" | "sendApprovalDecision" | "listEvents" | "streamEvents" | "listTurns" | "updateQueuedTurn" | "reorderQueuedTurns" | "deleteQueuedTurn" | "getGoal" | "updateGoal" | "clearSessionContext" | "compactSessionContext" | "listScheduledTasks" | "uploadFile" | "getFile" | "createFileDownloadUrl" | "listEnvironments" | "createEnvironment" | "updateEnvironment" | "deleteEnvironment" | "setEnvironmentVariable" | "deleteEnvironmentVariable" | "listPacks" | "registerPack" | "enablePack" | "deletePack" | "listWorkspaces" | "createWorkspace" | "updateWorkspace" | "getBillingUsage" | "getClientConfig" | "getStreamCapabilities" | "acknowledgeStream" | "attachViewer" | "heartbeatViewer" | "detachViewer" | "fsList" | "fsRead" | "fsWrite" | "fsDelete" | "fsMove" | "fsMkdir" | "gitStatus" | "gitDiff" | "terminalExec" | "terminalPtyOpen" | "terminalPtyWrite" | "terminalPtyResize" | "terminalPtyClose">;
|
|
11
11
|
|
|
12
12
|
type OpenGeniContextValue = {
|
|
13
13
|
client: SessionClientLike;
|
package/dist/machines.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { a as CONNECTION_STATUS_META, b as ConnectionDot, c as ConnectionDotProps, d as ConnectionStatus, e as ConnectionStatusMeta, f as ConnectionStatusPill, g as ConnectionStatusPillProps, D as DeviceFlowPhase, E as EnrollmentConsent, h as EnrollmentConsentMachine, i as EnrollmentConsentPhase, j as EnrollmentConsentProps, k as EnrollmentDeviceFlow, l as EnrollmentDeviceFlowProps, M as MACHINE_STATE_BADGE_META, m as MachineCard, n as MachineCardProps, o as MachineDockBar, p as MachineDockBarProps, q as MachineMetrics, r as MachineMetricsProps, s as MachineStateBadgeMeta, t as MachineStatusPill, u as MachineStatusPillProps, v as MachinesClientLike, w as MachinesDashboard, x as MachinesDashboardProps, A as SharedMachineDisclosure, B as SharedMachineDisclosureProps, U as UseMachinesOptions, F as UseMachinesResult, G as connectionStatusForState, H as useMachines } from './machines-
|
|
1
|
+
export { a as CONNECTION_STATUS_META, b as ConnectionDot, c as ConnectionDotProps, d as ConnectionStatus, e as ConnectionStatusMeta, f as ConnectionStatusPill, g as ConnectionStatusPillProps, D as DeviceFlowPhase, E as EnrollmentConsent, h as EnrollmentConsentMachine, i as EnrollmentConsentPhase, j as EnrollmentConsentProps, k as EnrollmentDeviceFlow, l as EnrollmentDeviceFlowProps, M as MACHINE_STATE_BADGE_META, m as MachineCard, n as MachineCardProps, o as MachineDockBar, p as MachineDockBarProps, q as MachineMetrics, r as MachineMetricsProps, s as MachineStateBadgeMeta, t as MachineStatusPill, u as MachineStatusPillProps, v as MachinesClientLike, w as MachinesDashboard, x as MachinesDashboardProps, A as SharedMachineDisclosure, B as SharedMachineDisclosureProps, U as UseMachinesOptions, F as UseMachinesResult, G as connectionStatusForState, H as useMachines } from './machines-BeZ3bD3t.js';
|
|
2
2
|
export { MachineKind, MachineMetricsSeriesResponse, MachineState, MachineView, MachinesResponse, MetricSample } from '@opengeni/sdk';
|
|
3
3
|
import 'react/jsx-runtime';
|
|
4
4
|
import 'react';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@bufbuild/protobuf": "^2.2.0",
|
|
48
|
-
"@opengeni/sdk": "^0.
|
|
48
|
+
"@opengeni/sdk": "^0.9.0",
|
|
49
49
|
"clsx": "^2.1.1",
|
|
50
50
|
"lucide-react": "^1.8.0",
|
|
51
51
|
"motion": "^12.0.0",
|
package/src/client.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "lucide-react";
|
|
12
12
|
import type { ComponentType } from "react";
|
|
13
13
|
import { AnimatePresence, motion } from "motion/react";
|
|
14
|
-
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
14
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
15
15
|
import { cn } from "../lib/cn";
|
|
16
16
|
import { formatRelativeTime, truncate } from "../lib/format";
|
|
17
17
|
import { Markdown } from "./markdown";
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
TurnSummary,
|
|
33
33
|
} from "../timeline";
|
|
34
34
|
import { SESSION_STATUS_META, StatusDot } from "./session-status";
|
|
35
|
+
import { EntranceAnimationProvider, useEntranceAnimation } from "../timeline/entrance";
|
|
35
36
|
|
|
36
37
|
export type MessageTimelineProps = {
|
|
37
38
|
/** Raw session events (projected internally) … */
|
|
@@ -52,6 +53,12 @@ export type MessageTimelineProps = {
|
|
|
52
53
|
toolRegistry?: ToolRegistry | undefined;
|
|
53
54
|
/** Follow new events when pinned to the bottom. Defaults to true. */
|
|
54
55
|
autoFollow?: boolean | undefined;
|
|
56
|
+
/** Older durable history exists above the current window (see useSessionEvents). */
|
|
57
|
+
hasOlder?: boolean | undefined;
|
|
58
|
+
/** An older window is being fetched; shows the quiet top shimmer. */
|
|
59
|
+
loadingOlder?: boolean | undefined;
|
|
60
|
+
/** Called when the reader nears the top and older history should backfill. */
|
|
61
|
+
onLoadOlder?: (() => void) | undefined;
|
|
55
62
|
emptyState?: ReactNode | undefined;
|
|
56
63
|
className?: string | undefined;
|
|
57
64
|
};
|
|
@@ -70,14 +77,40 @@ export function MessageTimeline({
|
|
|
70
77
|
onOpenSession,
|
|
71
78
|
toolRegistry = defaultToolRegistry,
|
|
72
79
|
autoFollow = true,
|
|
80
|
+
hasOlder = false,
|
|
81
|
+
loadingOlder = false,
|
|
82
|
+
onLoadOlder,
|
|
73
83
|
emptyState,
|
|
74
84
|
className,
|
|
75
85
|
}: MessageTimelineProps) {
|
|
76
86
|
const resolvedItems = useMemo(() => items ?? buildTimeline(events ?? []), [items, events]);
|
|
77
87
|
const groups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
|
|
88
|
+
const firstGroupKey = groups[0] ? timelineGroupKey(groups[0]) : null;
|
|
78
89
|
|
|
79
90
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
91
|
+
const topSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
92
|
+
const previousBulkFirstKeyRef = useRef<string | null | undefined>(undefined);
|
|
80
93
|
const [pinned, setPinned] = useState(true);
|
|
94
|
+
const [bulkActive, setBulkActive] = useState(true);
|
|
95
|
+
// Content stays invisible until its first bottom-anchored frame, so a flash
|
|
96
|
+
// of the window's TOP while a large timeline lays out across commits is
|
|
97
|
+
// structurally impossible — the reader only ever sees it already at the
|
|
98
|
+
// bottom. An empty timeline reveals immediately (there is nothing to anchor).
|
|
99
|
+
const [revealed, setRevealed] = useState(false);
|
|
100
|
+
// Our own scrollTop assignments echo back as scroll events; those must never
|
|
101
|
+
// UNPIN the reader (they are not reader intent). Marked around every
|
|
102
|
+
// programmatic assignment and consumed by onScroll. When an assignment is a
|
|
103
|
+
// NO-OP (already at the target) no scroll event will fire, so the mark must
|
|
104
|
+
// self-clear — a stale mark would eat the reader's next real scroll-up.
|
|
105
|
+
const programmaticScrollRef = useRef(false);
|
|
106
|
+
const assignScrollTop = useCallback((node: HTMLElement, value: number) => {
|
|
107
|
+
const previous = node.scrollTop;
|
|
108
|
+
programmaticScrollRef.current = true;
|
|
109
|
+
node.scrollTop = value;
|
|
110
|
+
if (node.scrollTop === previous) {
|
|
111
|
+
programmaticScrollRef.current = false;
|
|
112
|
+
}
|
|
113
|
+
}, []);
|
|
81
114
|
// Mirror `pinned` into a ref so the ResizeObserver callback (a stable closure)
|
|
82
115
|
// always reads the live value without re-subscribing on every scroll.
|
|
83
116
|
const pinnedRef = useRef(true);
|
|
@@ -89,9 +122,15 @@ export function MessageTimeline({
|
|
|
89
122
|
const lastItem = resolvedItems[resolvedItems.length - 1];
|
|
90
123
|
const streaming = lastItem !== undefined && (lastItem.kind === "agent-message" || lastItem.kind === "reasoning") && lastItem.streaming;
|
|
91
124
|
const working = status === "running" && !streaming;
|
|
125
|
+
// Bulk paints (the initial tail window, a prepended older window — detected
|
|
126
|
+
// by the first group key changing) must not run per-row entrance animations.
|
|
127
|
+
const firstKeyChangedForBulk = previousBulkFirstKeyRef.current !== undefined && previousBulkFirstKeyRef.current !== firstGroupKey;
|
|
128
|
+
const bulkRender = groups.length > 0 && (bulkActive || firstKeyChangedForBulk);
|
|
92
129
|
|
|
93
130
|
// Snapshot the topmost visible element and where it sits in the viewport, so a
|
|
94
|
-
// later reflow can restore it to the same spot.
|
|
131
|
+
// later reflow can restore it to the same spot. Transient chrome (the backfill
|
|
132
|
+
// sentinel and shimmer) is skipped — anchoring to a row that unmounts when the
|
|
133
|
+
// older window lands would drop the correction mid-prepend.
|
|
95
134
|
const captureAnchor = useCallback(() => {
|
|
96
135
|
const node = scrollRef.current;
|
|
97
136
|
const inner = node?.firstElementChild;
|
|
@@ -101,6 +140,9 @@ export function MessageTimeline({
|
|
|
101
140
|
}
|
|
102
141
|
const containerTop = node.getBoundingClientRect().top;
|
|
103
142
|
for (const child of Array.from(inner.children)) {
|
|
143
|
+
if (child instanceof HTMLElement && child.dataset.ogTimelineChrome !== undefined) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
104
146
|
const rect = child.getBoundingClientRect();
|
|
105
147
|
if (rect.bottom > containerTop + 1) {
|
|
106
148
|
anchorRef.current = { el: child, top: rect.top - containerTop };
|
|
@@ -111,12 +153,56 @@ export function MessageTimeline({
|
|
|
111
153
|
}, []);
|
|
112
154
|
|
|
113
155
|
// Follow the stream while pinned to the bottom; never fight the reader.
|
|
114
|
-
|
|
156
|
+
// A LAYOUT effect so the very first paint of a freshly loaded session is
|
|
157
|
+
// already anchored at the bottom — no visible traversal down the history.
|
|
158
|
+
useLayoutEffect(() => {
|
|
115
159
|
const node = scrollRef.current;
|
|
116
160
|
if (node && autoFollow && pinned) {
|
|
117
|
-
node
|
|
161
|
+
assignScrollTop(node, node.scrollHeight);
|
|
162
|
+
}
|
|
163
|
+
if (!revealed && groups.length > 0) {
|
|
164
|
+
setRevealed(true);
|
|
165
|
+
}
|
|
166
|
+
}, [resolvedItems, working, autoFollow, pinned, revealed, groups.length, assignScrollTop]);
|
|
167
|
+
|
|
168
|
+
// A cleared timeline (stream identity change) re-arms the reveal so the next
|
|
169
|
+
// session also first paints at its bottom.
|
|
170
|
+
useLayoutEffect(() => {
|
|
171
|
+
if (groups.length === 0 && revealed) {
|
|
172
|
+
setRevealed(false);
|
|
118
173
|
}
|
|
119
|
-
}, [
|
|
174
|
+
}, [groups.length, revealed]);
|
|
175
|
+
|
|
176
|
+
// Clear the bulk-paint marker a frame after it renders, so rows appended
|
|
177
|
+
// live (streams, new turns) animate exactly as before.
|
|
178
|
+
useLayoutEffect(() => {
|
|
179
|
+
previousBulkFirstKeyRef.current = firstGroupKey;
|
|
180
|
+
if (!bulkRender) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
setBulkActive(true);
|
|
184
|
+
const frame = requestFrame(() => setBulkActive(false));
|
|
185
|
+
return () => cancelFrame(frame);
|
|
186
|
+
}, [bulkRender, firstGroupKey]);
|
|
187
|
+
|
|
188
|
+
// Prefetch older history well before the reader reaches the top: the
|
|
189
|
+
// sentinel sits above the first group and trips 1600px early, so backfill
|
|
190
|
+
// is usually rendered (and anchored by the ResizeObserver below) before the
|
|
191
|
+
// top of the window ever becomes visible.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const root = scrollRef.current;
|
|
194
|
+
const target = topSentinelRef.current;
|
|
195
|
+
if (!root || !target || !hasOlder || loadingOlder || !onLoadOlder || typeof IntersectionObserver === "undefined") {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const observer = new IntersectionObserver((entries) => {
|
|
199
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
200
|
+
onLoadOlder();
|
|
201
|
+
}
|
|
202
|
+
}, { root, rootMargin: "1600px 0px 0px 0px" });
|
|
203
|
+
observer.observe(target);
|
|
204
|
+
return () => observer.disconnect();
|
|
205
|
+
}, [hasOlder, loadingOlder, onLoadOlder, firstGroupKey]);
|
|
120
206
|
|
|
121
207
|
// Scroll anchoring: when the content reflows (a fold expands/collapses, a
|
|
122
208
|
// stream appends), keep following the bottom if pinned; otherwise pin the
|
|
@@ -136,7 +222,7 @@ export function MessageTimeline({
|
|
|
136
222
|
return;
|
|
137
223
|
}
|
|
138
224
|
if (autoFollow && pinnedRef.current) {
|
|
139
|
-
current
|
|
225
|
+
assignScrollTop(current, current.scrollHeight);
|
|
140
226
|
} else {
|
|
141
227
|
const anchor = anchorRef.current;
|
|
142
228
|
if (anchor && anchor.el.isConnected) {
|
|
@@ -144,7 +230,7 @@ export function MessageTimeline({
|
|
|
144
230
|
const now = anchor.el.getBoundingClientRect().top - containerTop;
|
|
145
231
|
const diff = now - anchor.top;
|
|
146
232
|
if (diff !== 0) {
|
|
147
|
-
current.scrollTop
|
|
233
|
+
assignScrollTop(current, current.scrollTop + diff);
|
|
148
234
|
}
|
|
149
235
|
}
|
|
150
236
|
}
|
|
@@ -152,27 +238,47 @@ export function MessageTimeline({
|
|
|
152
238
|
});
|
|
153
239
|
observer.observe(inner);
|
|
154
240
|
return () => observer.disconnect();
|
|
155
|
-
}, [autoFollow, captureAnchor]);
|
|
241
|
+
}, [autoFollow, captureAnchor, assignScrollTop]);
|
|
156
242
|
|
|
157
243
|
const onScroll = () => {
|
|
158
244
|
const node = scrollRef.current;
|
|
159
245
|
if (!node) {
|
|
160
246
|
return;
|
|
161
247
|
}
|
|
248
|
+
const programmatic = programmaticScrollRef.current;
|
|
249
|
+
programmaticScrollRef.current = false;
|
|
162
250
|
const nextPinned = node.scrollHeight - node.scrollTop - node.clientHeight < 48;
|
|
163
|
-
|
|
164
|
-
|
|
251
|
+
// Echoes of our own assignments may PIN but never UNPIN — only the reader
|
|
252
|
+
// scrolling away releases the bottom-follow.
|
|
253
|
+
if (nextPinned || !programmatic) {
|
|
254
|
+
pinnedRef.current = nextPinned;
|
|
255
|
+
setPinned(nextPinned);
|
|
256
|
+
}
|
|
165
257
|
captureAnchor();
|
|
166
258
|
};
|
|
167
259
|
|
|
168
260
|
return (
|
|
169
261
|
<LightboxProvider>
|
|
262
|
+
<EntranceAnimationProvider value={!bulkRender}>
|
|
170
263
|
<div className={cn("og-root relative flex min-h-0 flex-col", className)}>
|
|
171
|
-
|
|
264
|
+
{/* overflow-anchor off: the browser's native scroll anchoring would fight
|
|
265
|
+
the ResizeObserver corrections above — one authority only. */}
|
|
266
|
+
<div
|
|
267
|
+
ref={scrollRef}
|
|
268
|
+
onScroll={onScroll}
|
|
269
|
+
style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
|
|
270
|
+
className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 [overflow-anchor:none] sm:px-6"
|
|
271
|
+
>
|
|
172
272
|
<div className="mx-auto flex w-full max-w-3xl flex-col gap-5">
|
|
173
273
|
{groups.length === 0 && !working
|
|
174
274
|
? (emptyState ?? <p className="py-10 text-center text-sm text-og-fg-subtle">No activity yet.</p>)
|
|
175
275
|
: null}
|
|
276
|
+
{hasOlder ? <div ref={topSentinelRef} data-og-top-sentinel="" data-og-timeline-chrome="" aria-hidden="true" className="h-px w-full shrink-0" /> : null}
|
|
277
|
+
{loadingOlder ? (
|
|
278
|
+
<div data-og-timeline-chrome="" className="flex items-center gap-2 text-sm">
|
|
279
|
+
<span className="og-shimmer-text font-medium">Loading earlier activity…</span>
|
|
280
|
+
</div>
|
|
281
|
+
) : null}
|
|
176
282
|
{groups.map((group) => (
|
|
177
283
|
<TimelineGroupView
|
|
178
284
|
key={timelineGroupKey(group)}
|
|
@@ -183,7 +289,7 @@ export function MessageTimeline({
|
|
|
183
289
|
/>
|
|
184
290
|
))}
|
|
185
291
|
{working ? (
|
|
186
|
-
<div className="
|
|
292
|
+
<div className="flex items-center gap-2 text-sm">
|
|
187
293
|
<span className="og-shimmer-text font-medium">Working…</span>
|
|
188
294
|
</div>
|
|
189
295
|
) : null}
|
|
@@ -218,10 +324,26 @@ export function MessageTimeline({
|
|
|
218
324
|
) : null}
|
|
219
325
|
</AnimatePresence>
|
|
220
326
|
</div>
|
|
327
|
+
</EntranceAnimationProvider>
|
|
221
328
|
</LightboxProvider>
|
|
222
329
|
);
|
|
223
330
|
}
|
|
224
331
|
|
|
332
|
+
function requestFrame(callback: FrameRequestCallback): number {
|
|
333
|
+
if (typeof requestAnimationFrame === "function") {
|
|
334
|
+
return requestAnimationFrame(callback);
|
|
335
|
+
}
|
|
336
|
+
return window.setTimeout(() => callback(performance.now()), 16);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function cancelFrame(id: number): void {
|
|
340
|
+
if (typeof cancelAnimationFrame === "function") {
|
|
341
|
+
cancelAnimationFrame(id);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
window.clearTimeout(id);
|
|
345
|
+
}
|
|
346
|
+
|
|
225
347
|
function TimelineGroupView({
|
|
226
348
|
group,
|
|
227
349
|
renderMessageText,
|
|
@@ -353,8 +475,9 @@ function UserMessageRow({
|
|
|
353
475
|
item: UserMessageItem;
|
|
354
476
|
renderMessageText?: ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
355
477
|
}) {
|
|
478
|
+
const enter = useEntranceAnimation();
|
|
356
479
|
return (
|
|
357
|
-
<div className="animate-og-enter flex justify-end">
|
|
480
|
+
<div className={cn(enter && "animate-og-enter", "flex justify-end")}>
|
|
358
481
|
<div className="flex max-w-[85%] min-w-0 flex-col items-end gap-1">
|
|
359
482
|
{item.pending ? <span className="px-1 text-og-xs text-og-fg-subtle">queued</span> : null}
|
|
360
483
|
<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">
|
|
@@ -372,11 +495,12 @@ function AgentMessageRow({
|
|
|
372
495
|
item: AgentMessageItem;
|
|
373
496
|
renderMessageText?: ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
|
|
374
497
|
}) {
|
|
498
|
+
const enter = useEntranceAnimation();
|
|
375
499
|
const caret = item.streaming ? (
|
|
376
500
|
<span className="ml-0.5 inline-block h-[1.1em] w-[2px] translate-y-[3px] animate-og-blink rounded-full bg-og-accent" aria-hidden />
|
|
377
501
|
) : null;
|
|
378
502
|
return (
|
|
379
|
-
<div className="animate-og-enter min-w-0 text-og-md leading-7 text-og-fg">
|
|
503
|
+
<div className={cn(enter && "animate-og-enter", "min-w-0 text-og-md leading-7 text-og-fg")}>
|
|
380
504
|
{renderMessageText ? (
|
|
381
505
|
<>
|
|
382
506
|
{renderMessageText(item.text, item)}
|
|
@@ -396,9 +520,10 @@ function AgentMessageRow({
|
|
|
396
520
|
}
|
|
397
521
|
|
|
398
522
|
function SessionStatusRow({ item }: { item: { status: SessionStatus; occurredAt: string } }) {
|
|
523
|
+
const enter = useEntranceAnimation();
|
|
399
524
|
const meta = SESSION_STATUS_META[item.status];
|
|
400
525
|
return (
|
|
401
|
-
<div className="animate-og-enter flex items-center gap-3 text-og-xs text-og-fg-subtle" role="status">
|
|
526
|
+
<div className={cn(enter && "animate-og-enter", "flex items-center gap-3 text-og-xs text-og-fg-subtle")} role="status">
|
|
402
527
|
<span className="h-px flex-1 bg-og-border" />
|
|
403
528
|
<span className="inline-flex items-center gap-1.5">
|
|
404
529
|
<StatusDot status={item.status} className="size-1" />
|
|
@@ -445,9 +570,10 @@ const GOAL_META: Record<GoalItem["action"], GoalMeta> = {
|
|
|
445
570
|
* palette stays restrained — see that table for the per-action rationale.
|
|
446
571
|
*/
|
|
447
572
|
function GoalRow({ item }: { item: GoalItem }) {
|
|
573
|
+
const enter = useEntranceAnimation();
|
|
448
574
|
const { label, pill, icon: Icon } = GOAL_META[item.action];
|
|
449
575
|
return (
|
|
450
|
-
<div className="animate-og-enter flex justify-center">
|
|
576
|
+
<div className={cn(enter && "animate-og-enter", "flex justify-center")}>
|
|
451
577
|
<span className={cn("inline-flex max-w-full items-center gap-1.5 rounded-full border px-3 py-1 text-og-sm", pill)}>
|
|
452
578
|
<Icon className="size-3.5 shrink-0" />
|
|
453
579
|
<span className="truncate">
|
|
@@ -460,6 +586,7 @@ function GoalRow({ item }: { item: GoalItem }) {
|
|
|
460
586
|
}
|
|
461
587
|
|
|
462
588
|
function NoticeRow({ item }: { item: NoticeItem }) {
|
|
589
|
+
const enter = useEntranceAnimation();
|
|
463
590
|
const tone =
|
|
464
591
|
item.tone === "failed"
|
|
465
592
|
? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
|
|
@@ -467,7 +594,7 @@ function NoticeRow({ item }: { item: NoticeItem }) {
|
|
|
467
594
|
? "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting"
|
|
468
595
|
: "border-og-border bg-og-surface-1 text-og-fg-muted";
|
|
469
596
|
return (
|
|
470
|
-
<div className={cn("animate-og-enter flex items-start gap-2.5 rounded-og-md border px-3.5 py-2.5 text-sm", tone)} role="status">
|
|
597
|
+
<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">
|
|
471
598
|
<TriangleAlertIcon className={cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60")} />
|
|
472
599
|
<span className="min-w-0 whitespace-pre-wrap break-words">{item.text}</span>
|
|
473
600
|
</div>
|
|
@@ -52,8 +52,9 @@ export type UseSessionCapabilitiesOptions = ClientOverride & {
|
|
|
52
52
|
warmingPollMs?: number | undefined;
|
|
53
53
|
/**
|
|
54
54
|
* Give up waiting for `warm` after this long while polling (ms) and surface a
|
|
55
|
-
* stalled error with a manual `renegotiate`. Default 30000
|
|
56
|
-
*
|
|
55
|
+
* stalled error with a manual `renegotiate`. Default 30000. This is a UI
|
|
56
|
+
* patience deadline, independent from the worker's sandbox warming timeout.
|
|
57
|
+
* 0 disables the deadline.
|
|
57
58
|
*/
|
|
58
59
|
warmingDeadlineMs?: number | undefined;
|
|
59
60
|
};
|