@opengeni/react 1.0.1 → 2.3.0-canary.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.
Files changed (174) hide show
  1. package/README.md +33 -10
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/chunk-5YK2ZDIG.js +116 -0
  8. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  9. package/dist/chunk-H5YGYNXS.js +329 -0
  10. package/dist/chunk-H5YGYNXS.js.map +1 -0
  11. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  12. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  13. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  14. package/dist/chunk-JOAVA2TW.js.map +1 -0
  15. package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
  16. package/dist/chunk-LS4POUTY.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
  24. package/dist/chunk-RSXJN73N.js.map +1 -0
  25. package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
  26. package/dist/chunk-SE4W4WQ3.js.map +1 -0
  27. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  28. package/dist/chunk-W6NFJXSA.js.map +1 -0
  29. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  30. package/dist/chunk-X4A66EWZ.js.map +1 -0
  31. package/dist/chunk-X4JAFD5R.js +3907 -0
  32. package/dist/chunk-X4JAFD5R.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +6 -3
  83. package/dist/realtime.js +36 -14
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +50 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +103 -59
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +215 -18
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +53 -17
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +529 -25
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +77 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
@@ -211,10 +211,10 @@ export function useComputerFrameStream(
211
211
  const frame = await decodeComputerFrameMessage(bytes);
212
212
  if (disposed || source !== socket) continue;
213
213
  if (frame.computerSessionId !== computerSessionId || frame.targetId !== targetId) {
214
- throw new Error("computer frame belongs to another resource");
214
+ throw new Error("desktop frame belongs to another resource");
215
215
  }
216
216
  if (!controllerGeneration || frame.controllerGeneration !== controllerGeneration) {
217
- throw new Error("computer frame belongs to a stale controller");
217
+ throw new Error("desktop frame belongs to a stale controller");
218
218
  }
219
219
  const key = `${computerSessionId}:${targetId}:${frame.controllerGeneration}:${frame.targetGeneration}`;
220
220
  if (latestRef.current.key === key && frame.sequence <= latestRef.current.sequence)
@@ -246,7 +246,7 @@ export function useComputerFrameStream(
246
246
  if (disposed || source !== socket) return;
247
247
  let frameBytes = bytes;
248
248
  if (activeStream?.kind === "relay") {
249
- if (bytes.length < 1) throw new Error("computer relay returned an empty message");
249
+ if (bytes.length < 1) throw new Error("desktop relay returned an empty message");
250
250
  const tag = bytes[0];
251
251
  const body = bytes.subarray(1);
252
252
  if (tag === RELAY_TAG_OPEN_ACK) {
@@ -255,7 +255,7 @@ export function useComputerFrameStream(
255
255
  activeAttachment = null;
256
256
  activeStream = null;
257
257
  attachmentExpiresAt = 0;
258
- throw new Error(ack.error?.message ?? "computer stream was rejected by the relay");
258
+ throw new Error(ack.error?.message ?? "desktop stream was rejected by the relay");
259
259
  }
260
260
  relayAccepted = true;
261
261
  return;
@@ -269,7 +269,7 @@ export function useComputerFrameStream(
269
269
  attachmentExpiresAt = 0;
270
270
  lastRelaySequence = null;
271
271
  relayAccepted = false;
272
- fail(new Error("Computer frame source ended."), false);
272
+ fail(new Error("Desktop frame source ended."), false);
273
273
  return;
274
274
  }
275
275
  if (tag !== RELAY_TAG_FRAME || !relayAccepted) return;
@@ -290,7 +290,7 @@ export function useComputerFrameStream(
290
290
 
291
291
  const onError = (source: ComputerFrameWebSocket) => {
292
292
  if (source !== socket) return;
293
- fail(new Error("Computer view lost connection."));
293
+ fail(new Error("Desktop view lost connection."));
294
294
  };
295
295
 
296
296
  const onClose = (source: ComputerFrameWebSocket) => {
@@ -363,7 +363,7 @@ export function useComputerFrameStream(
363
363
  attachment.computerSessionId !== computerSessionId ||
364
364
  attachment.targetId !== targetId
365
365
  ) {
366
- throw new Error("computer attachment does not match the requested resource");
366
+ throw new Error("desktop attachment does not match the requested resource");
367
367
  }
368
368
  controllerGeneration = attachment.controllerGeneration;
369
369
  activeAttachment = attachment;
@@ -397,7 +397,7 @@ export function useComputerFrameStream(
397
397
  connectSocket();
398
398
  } catch (cause) {
399
399
  if (attachmentAbort.signal.aborted || disposed) return;
400
- fail(cause);
400
+ fail(cause, !isPlacementGenerationLossError(cause));
401
401
  }
402
402
  };
403
403
 
@@ -430,5 +430,10 @@ async function messageBytes(value: unknown): Promise<Uint8Array> {
430
430
  if (typeof Blob !== "undefined" && value instanceof Blob) {
431
431
  return new Uint8Array(await value.arrayBuffer());
432
432
  }
433
- throw new Error("computer frame stream returned a non-binary message");
433
+ throw new Error("desktop frame stream returned a non-binary message");
434
+ }
435
+
436
+ function isPlacementGenerationLossError(cause: unknown): boolean {
437
+ const message = cause instanceof Error ? cause.message : String(cause);
438
+ return /placement instance changed|controller authority changed/i.test(message);
434
439
  }
@@ -8,6 +8,7 @@ import type {
8
8
  ComputerTarget,
9
9
  } from "@opengeni/sdk/interaction";
10
10
  import { useCallback, useEffect, useRef, useState } from "react";
11
+ import { isNonRetryableInteractionError } from "../lib/interaction-errors";
11
12
  import {
12
13
  type EmbeddedComputerInteractionClientOverride,
13
14
  useEmbeddedComputerInteraction,
@@ -55,6 +56,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
55
56
  );
56
57
  const visible =
57
58
  state.computerSessionId === computerSessionId ? state : emptyState(computerSessionId, enabled);
59
+ const refreshBlocked = isNonRetryableInteractionError(visible.error);
58
60
  const selectedTargetIdRef = useRef<string | null>(visible.selectedTargetId);
59
61
  selectedTargetIdRef.current = visible.selectedTargetId;
60
62
  const targetsRef = useRef<{
@@ -173,15 +175,15 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
173
175
  }, [computerSessionId, enabled, invalidateRefresh, refresh]);
174
176
 
175
177
  useEffect(() => {
176
- if (!enabled || !pageLive || visible.mutating) return;
178
+ if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
177
179
  const timer = setInterval(() => {
178
180
  if (!requestRef.current.controller) void refresh();
179
181
  }, pollIntervalMs);
180
182
  return () => clearInterval(timer);
181
- }, [enabled, pageLive, pollIntervalMs, refresh, visible.mutating]);
183
+ }, [enabled, pageLive, pollIntervalMs, refresh, refreshBlocked, visible.mutating]);
182
184
 
183
185
  useEffect(() => {
184
- if (!enabled || !computerSessionId || !pageLive) return;
186
+ if (!enabled || !computerSessionId || !pageLive || refreshBlocked) return;
185
187
  let disposed = false;
186
188
  const timer = setInterval(() => {
187
189
  void client.heartbeatComputerSession(workspaceId, computerSessionId).catch(() => {
@@ -192,7 +194,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
192
194
  disposed = true;
193
195
  clearInterval(timer);
194
196
  };
195
- }, [client, computerSessionId, enabled, pageLive, refresh, workspaceId]);
197
+ }, [client, computerSessionId, enabled, pageLive, refresh, refreshBlocked, workspaceId]);
196
198
 
197
199
  const runMutation = useCallback(
198
200
  async <T>(scopeComputerSessionId: string, operation: () => Promise<T>): Promise<T> => {
@@ -234,9 +236,9 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
234
236
 
235
237
  const selectTarget = useCallback(
236
238
  async (targetId: string): Promise<ComputerTarget> => {
237
- if (!computerSessionId) throw new Error("No ComputerSession is selected.");
239
+ if (!computerSessionId) throw new Error("No desktop is selected.");
238
240
  const target = targetsRef.current.targets.find((candidate) => candidate.id === targetId);
239
- if (!target) throw new Error("The selected computer target is no longer available.");
241
+ if (!target) throw new Error("The selected desktop target is no longer available.");
240
242
  invalidateRefresh();
241
243
  selectedTargetIdRef.current = target.id;
242
244
  observationRef.current = { computerSessionId, observation: null };
@@ -284,9 +286,9 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
284
286
  operationId: string,
285
287
  frame: ComputerFrame | null,
286
288
  ): Promise<ComputerActionReceipt> => {
287
- if (!computerSessionId) throw new Error("No ComputerSession is selected.");
289
+ if (!computerSessionId) throw new Error("No desktop is selected.");
288
290
  if (frame && frame.computerSessionId !== computerSessionId) {
289
- throw new Error("The displayed computer frame belongs to another ComputerSession.");
291
+ throw new Error("The displayed desktop frame belongs to another desktop session.");
290
292
  }
291
293
  const currentObservation =
292
294
  observationRef.current.computerSessionId === computerSessionId
@@ -302,18 +304,18 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
302
304
  (currentObservation?.target.id === frame.targetId ? currentObservation.target : null))
303
305
  : null;
304
306
  const target = frameTarget ?? focusTarget ?? currentObservation?.target ?? null;
305
- if (!target) throw new Error("The computer target is not ready for input.");
307
+ if (!target) throw new Error("The desktop target is not ready for input.");
306
308
  if (frame && frame.targetId !== selectedTargetIdRef.current) {
307
- throw new Error("The displayed computer frame is no longer selected.");
309
+ throw new Error("The displayed desktop frame is no longer selected.");
308
310
  }
309
311
  if (action.type === "pointer") {
310
312
  const expectedFrameId = frame?.frameId ?? currentObservation?.frameId ?? null;
311
313
  if (!expectedFrameId || action.frameId !== expectedFrameId) {
312
- throw new Error("Pointer input must reference the exact displayed computer frame.");
314
+ throw new Error("Pointer input must reference the exact displayed desktop frame.");
313
315
  }
314
316
  }
315
317
  if (action.type === "semantic" && !currentObservation) {
316
- throw new Error("The computer accessibility tree is not ready for input.");
318
+ throw new Error("The desktop accessibility tree is not ready for input.");
317
319
  }
318
320
 
319
321
  return await runMutation(computerSessionId, async () => {
@@ -367,7 +369,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
367
369
  );
368
370
 
369
371
  const readClipboard = useCallback(async (): Promise<ComputerClipboard> => {
370
- if (!computerSessionId) throw new Error("No ComputerSession is selected.");
372
+ if (!computerSessionId) throw new Error("No desktop is selected.");
371
373
  return await client.readComputerClipboard(workspaceId, computerSessionId);
372
374
  }, [client, computerSessionId, workspaceId]);
373
375
 
@@ -432,9 +434,9 @@ function chooseTarget(
432
434
  return (
433
435
  targets.find((target) => target.focused && target.kind === "window") ??
434
436
  targets.find((target) => target.kind === "window") ??
437
+ targets.find((target) => target.kind === "screen") ??
435
438
  targets.find((target) => target.focused && target.kind === "app") ??
436
439
  targets.find((target) => target.kind === "app") ??
437
- targets.find((target) => target.kind === "screen") ??
438
440
  targets[0] ??
439
441
  null
440
442
  );
@@ -74,6 +74,7 @@ export function useComputerSessions(
74
74
  requestRef.current = { id, controller };
75
75
  setState((current) => ({
76
76
  ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
77
+ active: true,
77
78
  loading: foreground,
78
79
  refreshing: !foreground,
79
80
  error: foreground || current.workspaceId !== workspaceId ? null : current.error,
@@ -85,6 +86,7 @@ export function useComputerSessions(
85
86
  if (!mountedRef.current || requestRef.current.id !== id) return;
86
87
  setState({
87
88
  workspaceId,
89
+ active: true,
88
90
  revision: response.revision,
89
91
  sessions: sortComputerSessions(response.sessions),
90
92
  loading: false,
@@ -187,7 +189,10 @@ export function useComputerSessions(
187
189
  revision: visible.revision,
188
190
  sessions: visible.sessions,
189
191
  relevantSessions,
190
- loading: visible.loading,
192
+ // A disabled registry is deliberately empty. When its surface becomes
193
+ // visible again, expose that transition as loading in the same render so
194
+ // consumers cannot mistake the stale empty snapshot for a completed list.
195
+ loading: enabled ? !visible.active || visible.loading : false,
191
196
  refreshing: visible.refreshing,
192
197
  error: visible.error,
193
198
  refresh,
@@ -198,6 +203,7 @@ export function useComputerSessions(
198
203
 
199
204
  type ComputerRegistryState = {
200
205
  workspaceId: string;
206
+ active: boolean;
201
207
  revision: number;
202
208
  sessions: ComputerSession[];
203
209
  loading: boolean;
@@ -208,6 +214,7 @@ type ComputerRegistryState = {
208
214
  function emptyState(workspaceId: string, loading: boolean): ComputerRegistryState {
209
215
  return {
210
216
  workspaceId,
217
+ active: loading,
211
218
  revision: 0,
212
219
  sessions: [],
213
220
  loading,
@@ -33,7 +33,7 @@ function policyFromTurn(turn: SessionTurn | null | undefined): LastStartedTurnPo
33
33
  return {
34
34
  model: turn.model,
35
35
  reasoningEffort: turn.reasoningEffort,
36
- latencyMode: turn.latencyMode ?? "standard",
36
+ latencyMode: turn.latencyMode,
37
37
  turnId: turn.id,
38
38
  };
39
39
  }
@@ -4,13 +4,13 @@ import type { SessionCapabilitiesState } from "./use-session-capabilities";
4
4
  import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
5
5
  import { connectionStatusForState } from "../types/machines";
6
6
 
7
- /** The one truthful machine indicator. Honest staleness beats
8
- * fake liveness: a cold/asleep box reads "offline — as of <time>", never "live". */
7
+ /** The one truthful machine indicator. Managed cold compute is sleeping; a
8
+ * Connected Machine that is genuinely unreachable remains offline. */
9
9
  export type MachineChipState = "live" | "waking" | "offline";
10
10
 
11
11
  export type MachineChip = {
12
12
  state: MachineChipState;
13
- /** A ready-to-render label ("Live" / "Waking…" / "Offline as of 3m ago"). */
13
+ /** A ready-to-render label ("Live" / "Waking…" / "Sleeping · saved 3m ago"). */
14
14
  label: string;
15
15
  /** The capture time backing an offline/stale label (ISO), or null. M4 may
16
16
  * reformat this; `label` already embeds a relative form. */
@@ -64,7 +64,8 @@ export function formatAsOf(capturedAt: string, now: number): string {
64
64
  export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
65
65
  const now = input.now ?? Date.now();
66
66
  const asOf = input.capturedAt ?? null;
67
- const offlineLabel = asOf ? `Offline as of ${formatAsOf(asOf, now)}` : "Offline";
67
+ const managedRestingLabel = asOf ? `Sleeping · saved ${formatAsOf(asOf, now)}` : "Sleeping";
68
+ const offlineLabel = "Offline";
68
69
 
69
70
  // A connected machine has no Modal group lease, so capability negotiation
70
71
  // legitimately reports `cold` until a stream holder is minted. Its fleet
@@ -86,6 +87,16 @@ export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
86
87
  const selfhostedOffline =
87
88
  input.activeIsSelfhosted === true && input.activeMachineState === "offline";
88
89
 
90
+ // Capability negotiation has definitively failed. A retained warm intent
91
+ // must not turn that error into the optimistic Waking state.
92
+ if (input.capabilitiesState === "error") {
93
+ return {
94
+ state: "offline",
95
+ label: input.activeIsSelfhosted === true ? offlineLabel : managedRestingLabel,
96
+ asOf,
97
+ };
98
+ }
99
+
89
100
  // 2. Actively coming up: negotiation in flight, an edit/terminal is warming, or
90
101
  // the active machine is (re)connecting. Never when self-hosted is hard-offline.
91
102
  const machineWarming =
@@ -97,8 +108,14 @@ export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
97
108
  return { state: "waking", label: "Waking…", asOf };
98
109
  }
99
110
 
100
- // 3. Cold / asleep / offline / error honest stale label.
101
- return { state: "offline", label: offlineLabel, asOf };
111
+ // 3. Managed cold compute is sleeping with saved-workspace freshness. A
112
+ // Connected Machine is only described as Offline when its reachability is
113
+ // genuinely unavailable; capture time is not a heartbeat.
114
+ return {
115
+ state: "offline",
116
+ label: input.activeIsSelfhosted === true ? offlineLabel : managedRestingLabel,
117
+ asOf,
118
+ };
102
119
  }
103
120
 
104
121
  export type UseMachineChipOptions = DeriveMachineChipInput;
@@ -2,12 +2,34 @@ import { useCallback, useRef, useState } from "react";
2
2
  import type {
3
3
  RemoveEnrollmentRequest,
4
4
  RemoveEnrollmentResponse,
5
+ MachineOperationPolicy,
5
6
  SwapActiveSandboxResponse,
7
+ UpdateMachineOperationPolicyRequest,
8
+ UpdateMachineAgentResponse,
6
9
  } from "@opengeni/sdk";
7
10
  import { useOpenGeni, type ClientOverride } from "../provider";
8
- import { useMutationRunner, usePolledValue } from "./internal";
11
+ import { useMutationRunner } from "./internal";
12
+ import { EMPTY_MACHINES, useSharedMachinesList } from "./shared-machines-list";
9
13
  import type { MachinesResponse, MachineView, MetricSample } from "../types/machines";
10
14
 
15
+ /** Dashboard list: cheap heartbeat read, keep the pill reasonably fresh. */
16
+ export const MACHINES_DASHBOARD_POLL_MS = 10_000;
17
+ /** Session surfaces share one poll for the same workspace+session. */
18
+ export const MACHINES_SESSION_POLL_MS = 15_000;
19
+ /** Composer polls after the first load shows a machine, or to retry a real error. */
20
+ export const MACHINES_COMPOSER_POLL_MS = 30_000;
21
+
22
+ const machinesClientIds = new WeakMap<object, number>();
23
+ let nextMachinesClientId = 1;
24
+
25
+ function machinesClientId(client: object): number {
26
+ const existing = machinesClientIds.get(client);
27
+ if (existing !== undefined) return existing;
28
+ const id = nextMachinesClientId++;
29
+ machinesClientIds.set(client, id);
30
+ return id;
31
+ }
32
+
11
33
  /**
12
34
  * The slice of the SDK client the Machines surface needs. The method NAMES +
13
35
  * SIGNATURES match M10's `OpenGeniClient` (`listMachines`, `machineMetricsSeries`)
@@ -42,6 +64,17 @@ export type MachinesClientLike = {
42
64
  enrollmentId: string,
43
65
  request?: RemoveEnrollmentRequest,
44
66
  ) => Promise<RemoveEnrollmentResponse>;
67
+ /** POST .../machines/:enrollmentId/update — signed, generation-fenced update. */
68
+ updateMachineAgent?: (
69
+ workspaceId: string,
70
+ enrollmentId: string,
71
+ ) => Promise<UpdateMachineAgentResponse>;
72
+ /** PATCH .../machines/:enrollmentId/operation-policy — revision-fenced policy. */
73
+ updateMachineOperationPolicy?: (
74
+ workspaceId: string,
75
+ enrollmentId: string,
76
+ request: UpdateMachineOperationPolicyRequest,
77
+ ) => Promise<MachineOperationPolicy>;
45
78
  /**
46
79
  * POST .../sessions/:sessionId/active-sandbox — swap the session's active
47
80
  * sandbox to a machine. The default swap path; the real SDK client provides it.
@@ -87,6 +120,17 @@ export type UseMachinesResult = {
87
120
  ) => Promise<RemoveEnrollmentResponse | null>;
88
121
  canRemove: boolean;
89
122
  removingEnrollmentId: string | null;
123
+ /** Start/retry the signed self-update for one Connected Machine. */
124
+ updateAgent: (enrollmentId: string) => Promise<UpdateMachineAgentResponse | null>;
125
+ canUpdateAgent: boolean;
126
+ updatingEnrollmentId: string | null;
127
+ /** Update the optional per-enrollment command memory policy. */
128
+ updateOperationPolicy: (
129
+ enrollmentId: string,
130
+ request: UpdateMachineOperationPolicyRequest,
131
+ ) => Promise<MachineOperationPolicy | null>;
132
+ canUpdateOperationPolicy: boolean;
133
+ updatingOperationPolicyEnrollmentId: string | null;
90
134
  /** Whether the host wired an attach/swap path (drives the card affordance). */
91
135
  canAttach: boolean;
92
136
  /** Fetch a downsampled metric series for one enrolled machine. */
@@ -101,8 +145,6 @@ export type UseMachinesResult = {
101
145
  clearMutationError: () => void;
102
146
  };
103
147
 
104
- const EMPTY: MachinesResponse = { activeSandboxId: null, activeEpoch: 0, machines: [] };
105
-
106
148
  function swapFailureMessage(result: SwapActiveSandboxResponse): string {
107
149
  if (result.reason?.trim()) return result.reason;
108
150
  switch (result.code) {
@@ -134,7 +176,7 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
134
176
  const machinesClient = (options.machinesClient ??
135
177
  (client as unknown as MachinesClientLike)) satisfies MachinesClientLike;
136
178
  const sessionId = options.sessionId;
137
- const identityKey = `${workspaceId}\u0000${sessionId ?? ""}`;
179
+ const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${machinesClientId(machinesClient)}`;
138
180
  const identityRef = useRef(identityKey);
139
181
  identityRef.current = identityKey;
140
182
 
@@ -153,7 +195,7 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
153
195
  loading,
154
196
  error,
155
197
  refresh,
156
- } = usePolledValue(load, {
198
+ } = useSharedMachinesList(identityKey, load, {
157
199
  pollIntervalMs: options.pollIntervalMs,
158
200
  enabled: options.enabled,
159
201
  });
@@ -170,8 +212,20 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
170
212
  }>(() => ({ identity: identityKey, enrollmentId: null }));
171
213
  const removingEnrollmentId =
172
214
  removeState.identity === identityKey ? removeState.enrollmentId : null;
215
+ const [updateState, setUpdateState] = useState<{
216
+ identity: string;
217
+ enrollmentId: string | null;
218
+ }>(() => ({ identity: identityKey, enrollmentId: null }));
219
+ const updatingEnrollmentId =
220
+ updateState.identity === identityKey ? updateState.enrollmentId : null;
221
+ const [operationPolicyState, setOperationPolicyState] = useState<{
222
+ identity: string;
223
+ enrollmentId: string | null;
224
+ }>(() => ({ identity: identityKey, enrollmentId: null }));
225
+ const updatingOperationPolicyEnrollmentId =
226
+ operationPolicyState.identity === identityKey ? operationPolicyState.enrollmentId : null;
173
227
 
174
- const data = loadedData ?? EMPTY;
228
+ const data = loadedData ?? EMPTY_MACHINES;
175
229
  // The swap is session-scoped: a host adapter (`attachMachine`) wins; otherwise
176
230
  // the default `swapActiveSandbox` path is wired whenever a sessionId is in
177
231
  // scope. Either way attach needs a sessionId to point at.
@@ -180,6 +234,9 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
180
234
  (typeof machinesClient.attachMachine === "function" ||
181
235
  typeof machinesClient.swapActiveSandbox === "function");
182
236
  const canRemove = typeof machinesClient.removeEnrollment === "function";
237
+ const canUpdateAgent = typeof machinesClient.updateMachineAgent === "function";
238
+ const canUpdateOperationPolicy =
239
+ typeof machinesClient.updateMachineOperationPolicy === "function";
183
240
 
184
241
  const attach = useCallback(
185
242
  async (sandboxId: string): Promise<boolean> => {
@@ -246,6 +303,41 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
246
303
  [machinesClient, workspaceId, identityKey, run, refresh],
247
304
  );
248
305
 
306
+ const updateAgent = useCallback(
307
+ async (enrollmentId: string): Promise<UpdateMachineAgentResponse | null> => {
308
+ if (!machinesClient.updateMachineAgent) return null;
309
+ const ownedIdentity = identityKey;
310
+ setUpdateState({ identity: ownedIdentity, enrollmentId });
311
+ const result = await run(() => machinesClient.updateMachineAgent!(workspaceId, enrollmentId));
312
+ if (identityRef.current === ownedIdentity) {
313
+ setUpdateState({ identity: ownedIdentity, enrollmentId: null });
314
+ await refresh();
315
+ }
316
+ return result;
317
+ },
318
+ [machinesClient, workspaceId, identityKey, run, refresh],
319
+ );
320
+
321
+ const updateOperationPolicy = useCallback(
322
+ async (
323
+ enrollmentId: string,
324
+ request: UpdateMachineOperationPolicyRequest,
325
+ ): Promise<MachineOperationPolicy | null> => {
326
+ if (!machinesClient.updateMachineOperationPolicy) return null;
327
+ const ownedIdentity = identityKey;
328
+ setOperationPolicyState({ identity: ownedIdentity, enrollmentId });
329
+ const result = await run(() =>
330
+ machinesClient.updateMachineOperationPolicy!(workspaceId, enrollmentId, request),
331
+ );
332
+ if (identityRef.current === ownedIdentity) {
333
+ setOperationPolicyState({ identity: ownedIdentity, enrollmentId: null });
334
+ if (result) await refresh();
335
+ }
336
+ return result;
337
+ },
338
+ [machinesClient, workspaceId, identityKey, run, refresh],
339
+ );
340
+
249
341
  return {
250
342
  machines: data.machines,
251
343
  activeSandboxId: data.activeSandboxId,
@@ -261,6 +353,12 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
261
353
  remove,
262
354
  canRemove,
263
355
  removingEnrollmentId,
356
+ updateAgent,
357
+ canUpdateAgent,
358
+ updatingEnrollmentId,
359
+ updateOperationPolicy,
360
+ canUpdateOperationPolicy,
361
+ updatingOperationPolicyEnrollmentId,
264
362
  mutationError,
265
363
  clearMutationError,
266
364
  };