@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.
@@ -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;
@@ -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-Bv9tG7qZ.js';
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.6.3",
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.6.3",
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
@@ -17,6 +17,7 @@ export type SessionClientLike = Pick<
17
17
  | "steerMessage"
18
18
  | "interrupt"
19
19
  | "sendApprovalDecision"
20
+ | "listEvents"
20
21
  | "streamEvents"
21
22
  // Turn queue
22
23
  | "listTurns"
@@ -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
- useEffect(() => {
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.scrollTop = node.scrollHeight;
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
- }, [resolvedItems, working, autoFollow, pinned]);
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.scrollTop = current.scrollHeight;
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 += diff;
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
- pinnedRef.current = nextPinned;
164
- setPinned(nextPinned);
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
- <div ref={scrollRef} onScroll={onScroll} className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 sm:px-6">
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="animate-og-enter flex items-center gap-2 text-sm">
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 (must agree with
56
- * the lease warming TTL I15). 0 disables the deadline.
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
  };