@opengeni/react 0.57.0 → 2.2.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 +36 -11
  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-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
  8. package/dist/chunk-5RZN3DD5.js.map +1 -0
  9. package/dist/chunk-5YK2ZDIG.js +116 -0
  10. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  11. package/dist/chunk-H5YGYNXS.js +329 -0
  12. package/dist/chunk-H5YGYNXS.js.map +1 -0
  13. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  14. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  15. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  16. package/dist/chunk-JOAVA2TW.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-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
  24. package/dist/chunk-UOLJWT5Y.js.map +1 -0
  25. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  26. package/dist/chunk-W6NFJXSA.js.map +1 -0
  27. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  28. package/dist/chunk-X4A66EWZ.js.map +1 -0
  29. package/dist/chunk-X4JAFD5R.js +3907 -0
  30. package/dist/chunk-X4JAFD5R.js.map +1 -0
  31. package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
  32. package/dist/chunk-XSA2QAKP.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 +10 -3
  83. package/dist/realtime.js +138 -29
  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 +48 -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 +91 -55
  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 +162 -17
  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 +200 -37
  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 +476 -23
  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 +75 -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
@@ -8,6 +8,7 @@ import type {
8
8
  InteractionIntervention,
9
9
  InteractionSemanticNode,
10
10
  } from "@opengeni/sdk/interaction";
11
+ import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
11
12
  import type { DesktopStreamCapability } from "@opengeni/sdk";
12
13
  import {
13
14
  ChevronDownIcon,
@@ -44,6 +45,7 @@ import { useComputerSessions } from "../hooks/use-computer-sessions";
44
45
  import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
45
46
  import { cn } from "../lib/cn";
46
47
  import { copyTextToClipboard } from "../lib/clipboard";
48
+ import { isSourcePlacementChangedError } from "../lib/interaction-errors";
47
49
  import type { EmbeddedComputerInteractionClientOverride } from "../session-context";
48
50
  import { InteractionInterventionBanner } from "./interaction-intervention-banner";
49
51
  import { DesktopViewer } from "./desktop-viewer";
@@ -62,6 +64,9 @@ export type ComputerViewerProps = EmbeddedComputerInteractionClientOverride & {
62
64
  /** Host navigation request for one exact ComputerSession. */
63
65
  requestedComputerSessionId?: string | null | undefined;
64
66
  requestedComputerRequestId?: string | number | null | undefined;
67
+ /** Restore and report the Desktop session selected in this task's dock. */
68
+ initialComputerSessionId?: string | null | undefined;
69
+ onComputerSessionIdChange?: ((computerSessionId: string | null) => void) | undefined;
65
70
  };
66
71
 
67
72
  type ComputerSelection = { sessionId: string; pinned: boolean } | null;
@@ -78,6 +83,8 @@ export function ComputerViewer({
78
83
  renderEmpty,
79
84
  requestedComputerSessionId,
80
85
  requestedComputerRequestId,
86
+ initialComputerSessionId,
87
+ onComputerSessionIdChange,
81
88
  ...override
82
89
  }: ComputerViewerProps) {
83
90
  const registry = useComputerSessions({ ...override, sessionId, enabled });
@@ -87,11 +94,26 @@ export function ComputerViewer({
87
94
  () => registry.relevantSessions.filter((session) => isLiveComputer(session)),
88
95
  [registry.relevantSessions],
89
96
  );
97
+ const connectedPlacementLoss = useMemo(
98
+ () =>
99
+ registry.relevantSessions.find(
100
+ (session) =>
101
+ session.lifecycle === "lost" &&
102
+ session.failureCode === "source_placement_changed" &&
103
+ session.placement.kind === "connected_machine",
104
+ ) ?? null,
105
+ [registry.relevantSessions],
106
+ );
90
107
  const recentRelevantFailure = useMemo(
91
108
  () =>
92
109
  liveRelevant.length === 0
93
- ? (registry.relevantSessions.find((session) =>
94
- ["failed", "lost"].includes(session.lifecycle),
110
+ ? (registry.relevantSessions.find(
111
+ (session) =>
112
+ ["failed", "lost"].includes(session.lifecycle) &&
113
+ !(
114
+ session.failureCode === "source_placement_changed" &&
115
+ session.placement.kind === "connected_machine"
116
+ ),
95
117
  ) ?? null)
96
118
  : null,
97
119
  [liveRelevant.length, registry.relevantSessions],
@@ -105,17 +127,46 @@ export function ComputerViewer({
105
127
  liveRelevant.length > 0 ? liveRelevant : recentRelevantFailure ? [recentRelevantFailure] : [],
106
128
  [liveRelevant, recentRelevantFailure],
107
129
  );
108
- const [selection, setSelection] = useState<ComputerSelection>(null);
130
+ const [selection, setSelection] = useState<ComputerSelection>(() =>
131
+ initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null,
132
+ );
133
+ const selectComputerSession = useCallback(
134
+ (next: ComputerSelection) => {
135
+ setSelection(next);
136
+ onComputerSessionIdChange?.(next?.sessionId ?? null);
137
+ },
138
+ [onComputerSessionIdChange],
139
+ );
109
140
  const interventions = useInteractionInterventions({
110
141
  ...override,
111
142
  enabled,
112
143
  resourceKind: "computer_session",
113
144
  });
114
145
  const [creating, setCreating] = useState(false);
146
+ const [createError, setCreateError] = useState<Error | null>(null);
115
147
  const [showControls, setShowControls] = useState(false);
116
148
  const previousSessionIdRef = useRef(sessionId);
117
149
  const handledRequestRef = useRef<string | null>(null);
118
150
  const seenInterventionIdsRef = useRef(new Set<string>());
151
+ const createInFlightRef = useRef(false);
152
+ const autoCreateSessionRef = useRef<string | null>(null);
153
+ const emptyCatalogConfirmationRef = useRef<string | null>(null);
154
+ const endedGenerationLossRef = useRef(new Set<string>());
155
+ const recreatedPlacementLossRef = useRef(new Set<string>());
156
+ const [suppressGenericCreate, setSuppressGenericCreate] = useState(false);
157
+ const endStaleComputer = registry.end;
158
+ useEffect(() => {
159
+ if (!enabled) return;
160
+ for (const session of registry.sessions) {
161
+ if (!isAttachedChromeGenerationLoss(session)) continue;
162
+ setSuppressGenericCreate(true);
163
+ if (endedGenerationLossRef.current.has(session.id)) continue;
164
+ endedGenerationLossRef.current.add(session.id);
165
+ void endStaleComputer(session.id).catch(() => {
166
+ endedGenerationLossRef.current.delete(session.id);
167
+ });
168
+ }
169
+ }, [enabled, endStaleComputer, registry.sessions]);
119
170
 
120
171
  const notifyError = useCallback(
121
172
  (cause: unknown, fallback: string) => {
@@ -131,10 +182,18 @@ export function ComputerViewer({
131
182
  if (previousSessionIdRef.current === sessionId) return;
132
183
  previousSessionIdRef.current = sessionId;
133
184
  handledRequestRef.current = null;
134
- setSelection(null);
135
- }, [sessionId]);
185
+ autoCreateSessionRef.current = null;
186
+ emptyCatalogConfirmationRef.current = null;
187
+ recreatedPlacementLossRef.current.clear();
188
+ setSuppressGenericCreate(false);
189
+ setCreateError(null);
190
+ setSelection(
191
+ initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null,
192
+ );
193
+ }, [initialComputerSessionId, sessionId]);
136
194
 
137
195
  useEffect(() => {
196
+ if (!enabled || registry.loading) return;
138
197
  const requested = requestedComputerSessionId
139
198
  ? (liveSessions.find((session) => session.id === requestedComputerSessionId) ?? null)
140
199
  : null;
@@ -143,7 +202,7 @@ export function ComputerViewer({
143
202
  : null;
144
203
  if (requested && requestKey && handledRequestRef.current !== requestKey) {
145
204
  handledRequestRef.current = requestKey;
146
- setSelection({ sessionId: requested.id, pinned: true });
205
+ selectComputerSession({ sessionId: requested.id, pinned: true });
147
206
  return;
148
207
  }
149
208
 
@@ -154,15 +213,24 @@ export function ComputerViewer({
154
213
  if (selection?.pinned && selectedStillLive) return;
155
214
  const preferred = relevant[0] ?? null;
156
215
  if (!preferred) {
157
- if (selection) setSelection(null);
216
+ if (selection) selectComputerSession(null);
158
217
  return;
159
218
  }
160
219
  if (!selectedStillLive || !selection?.pinned) {
161
220
  if (selection?.sessionId !== preferred.id || selection.pinned) {
162
- setSelection({ sessionId: preferred.id, pinned: false });
221
+ selectComputerSession({ sessionId: preferred.id, pinned: false });
163
222
  }
164
223
  }
165
- }, [liveSessions, relevant, requestedComputerRequestId, requestedComputerSessionId, selection]);
224
+ }, [
225
+ liveSessions,
226
+ enabled,
227
+ registry.loading,
228
+ relevant,
229
+ requestedComputerRequestId,
230
+ requestedComputerSessionId,
231
+ selectComputerSession,
232
+ selection,
233
+ ]);
166
234
 
167
235
  const selectedRegistrySession = useMemo(
168
236
  () => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
@@ -185,6 +253,10 @@ export function ComputerViewer({
185
253
  computerSessionId: selection?.sessionId ?? null,
186
254
  enabled: enabled && selection !== null && controllerReady,
187
255
  });
256
+ useEffect(() => {
257
+ if (!isSourcePlacementChangedError(computer.error, "computer_session")) return;
258
+ void refreshRegistry();
259
+ }, [computer.error, refreshRegistry]);
188
260
  const act = computer.act;
189
261
  const actFromFrame = computer.actFromFrame;
190
262
  const refreshComputer = computer.refresh;
@@ -192,7 +264,12 @@ export function ComputerViewer({
192
264
  ...override,
193
265
  computerSessionId: selection?.sessionId ?? null,
194
266
  targetId: computer.selectedTarget?.id ?? null,
195
- enabled: enabled && selection !== null && controllerReady && computer.selectedTarget !== null,
267
+ enabled:
268
+ enabled &&
269
+ selection !== null &&
270
+ controllerReady &&
271
+ computer.selectedTarget !== null &&
272
+ isComputerFrameTarget(computer.selectedTarget),
196
273
  stream: {
197
274
  format: "jpeg",
198
275
  quality: 78,
@@ -257,26 +334,108 @@ export function ComputerViewer({
257
334
  outcome === "completed" ? "Agent notified. Continuing work." : "Request cancelled.",
258
335
  });
259
336
  })
260
- .catch((cause) => notifyError(cause, "Could not update the computer request."));
337
+ .catch((cause) => notifyError(cause, "Could not update the desktop request."));
261
338
  },
262
339
  [interventions, notifyError, onNotify],
263
340
  );
264
341
 
265
342
  const createComputer = useCallback(() => {
266
- if (creating) return;
343
+ if (createInFlightRef.current) return;
344
+ createInFlightRef.current = true;
345
+ setCreateError(null);
267
346
  setCreating(true);
268
- void createSession({ sessionId, name: "Computer" })
347
+ void createSession({ sessionId, name: "Desktop" })
269
348
  .then((response) => {
270
- setSelection({ sessionId: response.session.id, pinned: false });
349
+ selectComputerSession({ sessionId: response.session.id, pinned: false });
350
+ })
351
+ .catch((cause) => {
352
+ const error = cause instanceof Error ? cause : new Error("Could not open a desktop.");
353
+ setCreateError(error);
354
+ notifyError(error, "Could not open a desktop.");
271
355
  })
272
- .catch((cause) => notifyError(cause, "Could not open a computer."))
273
- .finally(() => setCreating(false));
274
- }, [createSession, creating, notifyError, sessionId]);
356
+ .finally(() => {
357
+ createInFlightRef.current = false;
358
+ setCreating(false);
359
+ });
360
+ }, [createSession, notifyError, selectComputerSession, sessionId]);
361
+
362
+ useEffect(() => {
363
+ if (
364
+ !enabled ||
365
+ registry.loading ||
366
+ registry.refreshing ||
367
+ liveRelevant.length > 0 ||
368
+ !connectedPlacementLoss ||
369
+ recreatedPlacementLossRef.current.has(connectedPlacementLoss.id)
370
+ ) {
371
+ return;
372
+ }
373
+ recreatedPlacementLossRef.current.add(connectedPlacementLoss.id);
374
+ autoCreateSessionRef.current = sessionId;
375
+ emptyCatalogConfirmationRef.current = null;
376
+ createComputer();
377
+ }, [
378
+ connectedPlacementLoss,
379
+ createComputer,
380
+ enabled,
381
+ liveRelevant.length,
382
+ registry.loading,
383
+ registry.refreshing,
384
+ sessionId,
385
+ ]);
386
+
387
+ useEffect(() => {
388
+ // Once this task's Desktop has been observed, a later transient empty
389
+ // registry snapshot must not create a second one. This can happen while the
390
+ // lazy surface is disabled/re-enabled or while an invalidation refresh
391
+ // briefly races the association projection. The existing Desktop remains
392
+ // the task's authority; explicit New desktop is still available.
393
+ if (relevant.length > 0) {
394
+ autoCreateSessionRef.current = sessionId;
395
+ emptyCatalogConfirmationRef.current = null;
396
+ }
397
+ }, [relevant.length, sessionId]);
398
+
399
+ useEffect(() => {
400
+ if (
401
+ !enabled ||
402
+ registry.loading ||
403
+ registry.refreshing ||
404
+ registry.error ||
405
+ relevant.length > 0 ||
406
+ suppressGenericCreate ||
407
+ autoCreateSessionRef.current === sessionId
408
+ ) {
409
+ return;
410
+ }
411
+ // A lazy surface can momentarily publish an empty settled projection while
412
+ // its initial catalog and a workspace invalidation cross. Confirm the empty
413
+ // result with one authoritative refresh before creating anything. A real
414
+ // empty catalog creates on the following render; an existing association
415
+ // appears and cancels this path.
416
+ if (emptyCatalogConfirmationRef.current !== sessionId) {
417
+ emptyCatalogConfirmationRef.current = sessionId;
418
+ void refreshRegistry();
419
+ return;
420
+ }
421
+ autoCreateSessionRef.current = sessionId;
422
+ createComputer();
423
+ }, [
424
+ createComputer,
425
+ enabled,
426
+ refreshRegistry,
427
+ registry.error,
428
+ registry.loading,
429
+ registry.refreshing,
430
+ relevant.length,
431
+ sessionId,
432
+ suppressGenericCreate,
433
+ ]);
275
434
 
276
435
  const perform = useCallback(
277
436
  async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
278
437
  if (action.type === "pointer") {
279
- if (!frame) throw new Error("Computer view is not ready for pointer input.");
438
+ if (!frame) throw new Error("Desktop view is not ready for pointer input.");
280
439
  await actFromFrame(action, frame);
281
440
  } else {
282
441
  await act(action);
@@ -294,10 +453,10 @@ export function ComputerViewer({
294
453
  .then(() => computer.readClipboard())
295
454
  .then(async (clipboard) => {
296
455
  if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
297
- throw new Error("Computer text could not be copied to the local clipboard");
456
+ throw new Error("Desktop text could not be copied to the local clipboard");
298
457
  }
299
458
  })
300
- .catch((cause) => notifyError(cause, "Could not copy from the computer."));
459
+ .catch((cause) => notifyError(cause, "Could not copy from the desktop."));
301
460
  },
302
461
  [computer, notifyError, perform, rfbStream],
303
462
  );
@@ -316,49 +475,76 @@ export function ComputerViewer({
316
475
  await perform({ type: "clipboard", operation: "write", text }, null);
317
476
  assertExactComputerClipboard(await computer.readClipboard(), text);
318
477
  await perform({ type: "clipboard", operation: "paste" }, null);
319
- })().catch((cause) => notifyError(cause, "Could not paste into the computer."));
478
+ })().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
320
479
  return true;
321
480
  },
322
481
  [computer, notifyError, perform, rfbStream],
323
482
  );
324
483
 
484
+ const hideGenericCreate = suppressGenericCreate && liveRelevant.length === 0;
485
+ const generationLossSelected =
486
+ selectedRegistrySession !== null && isAttachedChromeGenerationLoss(selectedRegistrySession);
487
+ const generationLossFrames = isAttachedChromeGenerationLossError(frames.error);
488
+
325
489
  if (!enabled) return null;
326
490
  if (registry.loading && liveSessions.length === 0) {
327
491
  return (
328
492
  <ComputerNotice
329
493
  icon={<LoaderCircleIcon className="size-4 animate-spin" />}
330
- text="Finding workspace computers…"
494
+ text="Finding workspace desktops…"
331
495
  {...(className ? { className } : {})}
332
496
  />
333
497
  );
334
498
  }
335
499
  if (liveSessions.length === 0) {
500
+ if (hideGenericCreate) {
501
+ return (
502
+ <div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
503
+ <div className="max-w-sm text-center">
504
+ <CircleAlertIcon className="mx-auto size-5 text-og-status-error" />
505
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
506
+ Chrome reconnected—open a fresh browser/desktop.
507
+ </p>
508
+ <p className="mt-1 text-og-control leading-5 text-og-muted">
509
+ This Chrome profile is live again, but the previous desktop cannot move to the new
510
+ connection. Use Browser → New browser → Connected Chrome.
511
+ </p>
512
+ </div>
513
+ </div>
514
+ );
515
+ }
336
516
  if (renderEmpty) return renderEmpty(createComputer, creating);
517
+ const error = createError ?? registry.error;
337
518
  return (
338
519
  <div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
339
520
  <div className="max-w-sm text-center">
340
521
  <span className="mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-muted">
341
- <MonitorIcon className="size-5" />
522
+ {error ? (
523
+ <CircleAlertIcon className="size-5 text-og-status-error" />
524
+ ) : (
525
+ <LoaderCircleIcon className="size-5 animate-spin" />
526
+ )}
342
527
  </span>
343
- <p className="mt-3 text-og-menu font-medium text-og-fg">No computer open</p>
528
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
529
+ {error ? "Desktop didn’t open" : "Opening desktop…"}
530
+ </p>
344
531
  <p className="mt-1 text-og-control leading-5 text-og-muted">
345
- Open this agent&apos;s desktop, or switch to any computer another workspace agent uses.
532
+ {error?.message ?? "Preparing this agents desktop. It will appear here when ready."}
346
533
  </p>
347
- <button
348
- type="button"
349
- onClick={createComputer}
350
- disabled={creating}
351
- className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm bg-og-accent-deep px-3 text-og-control font-medium text-og-accent-fg transition hover:brightness-110 disabled:opacity-50"
352
- >
353
- {creating ? (
354
- <LoaderCircleIcon className="size-3.5 animate-spin" />
355
- ) : (
356
- <PlusIcon className="size-3.5" />
357
- )}
358
- Open computer
359
- </button>
360
- {registry.error ? (
361
- <p className="mt-3 text-og-control text-og-status-error">{registry.error.message}</p>
534
+ {error ? (
535
+ <button
536
+ type="button"
537
+ onClick={createError ? createComputer : () => void refreshRegistry()}
538
+ disabled={creating || registry.refreshing}
539
+ className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm bg-og-accent-deep px-3 text-og-control font-medium text-og-accent-fg transition hover:brightness-110 disabled:opacity-50"
540
+ >
541
+ {creating || registry.refreshing ? (
542
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
543
+ ) : (
544
+ <RotateCcwIcon className="size-3.5" />
545
+ )}
546
+ Try again
547
+ </button>
362
548
  ) : null}
363
549
  </div>
364
550
  </div>
@@ -375,12 +561,12 @@ export function ComputerViewer({
375
561
  refreshing={registry.refreshing}
376
562
  interventionCounts={interventionCounts}
377
563
  onSelect={(computerSessionId) =>
378
- setSelection({ sessionId: computerSessionId, pinned: true })
564
+ selectComputerSession({ sessionId: computerSessionId, pinned: true })
379
565
  }
380
566
  onFollow={() => {
381
- if (relevant[0]) setSelection({ sessionId: relevant[0].id, pinned: false });
567
+ if (relevant[0]) selectComputerSession({ sessionId: relevant[0].id, pinned: false });
382
568
  }}
383
- onCreate={createComputer}
569
+ onCreate={hideGenericCreate ? undefined : createComputer}
384
570
  onRefresh={() => void Promise.all([refreshRegistry(), refreshComputer()])}
385
571
  />
386
572
  <InteractionInterventionBanner
@@ -390,7 +576,7 @@ export function ComputerViewer({
390
576
  onOpen={(intervention) =>
391
577
  void computer
392
578
  .selectTarget(intervention.targetId)
393
- .catch((cause) => notifyError(cause, "Could not open the requested computer view."))
579
+ .catch((cause) => notifyError(cause, "Could not open the requested desktop view."))
394
580
  }
395
581
  onResolve={resolveIntervention}
396
582
  />
@@ -398,6 +584,8 @@ export function ComputerViewer({
398
584
  <ComputerUnselectedPanel
399
585
  peerCount={liveSessions.length}
400
586
  creating={creating}
587
+ error={createError}
588
+ generationLoss={suppressGenericCreate}
401
589
  onCreate={createComputer}
402
590
  />
403
591
  ) : !controllerReady ? (
@@ -416,7 +604,7 @@ export function ComputerViewer({
416
604
  onSelect={(targetId) =>
417
605
  void computer
418
606
  .selectTarget(targetId)
419
- .catch((cause) => notifyError(cause, "Could not switch computer views."))
607
+ .catch((cause) => notifyError(cause, "Could not switch desktop views."))
420
608
  }
421
609
  />
422
610
  <div className="flex min-h-0 flex-1">
@@ -445,8 +633,12 @@ export function ComputerViewer({
445
633
  clipboardEnabled={computer.session?.capabilities?.clipboard === true}
446
634
  onAction={perform}
447
635
  onReadClipboard={computer.readClipboard}
448
- onReconnect={frames.reconnect}
449
- onError={(cause) => notifyError(cause, "Computer input failed.")}
636
+ onReconnect={
637
+ generationLossSelected || generationLossFrames
638
+ ? () => undefined
639
+ : frames.reconnect
640
+ }
641
+ onError={(cause) => notifyError(cause, "Desktop input failed.")}
450
642
  />
451
643
  )}
452
644
  {showControls ? (
@@ -455,7 +647,7 @@ export function ComputerViewer({
455
647
  mutating={computer.mutating}
456
648
  onAction={(action) =>
457
649
  void perform(action, null).catch((cause) =>
458
- notifyError(cause, "Computer action failed."),
650
+ notifyError(cause, "Desktop action failed."),
459
651
  )
460
652
  }
461
653
  />
@@ -479,33 +671,50 @@ export function ComputerViewer({
479
671
  function ComputerUnselectedPanel(props: {
480
672
  peerCount: number;
481
673
  creating: boolean;
674
+ error: Error | null;
675
+ generationLoss: boolean;
482
676
  onCreate: () => void;
483
677
  }) {
484
678
  return (
485
679
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
486
680
  <div className="max-w-sm">
487
681
  <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
488
- <MonitorIcon className="size-4.5" />
682
+ {props.error || props.generationLoss ? (
683
+ <CircleAlertIcon className="size-4.5 text-og-status-error" />
684
+ ) : (
685
+ <LoaderCircleIcon className="size-4.5 animate-spin" />
686
+ )}
489
687
  </span>
490
- <p className="mt-3 text-og-menu font-medium text-og-fg">No computer for this agent</p>
688
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
689
+ {props.generationLoss
690
+ ? "Chrome reconnected—open a fresh browser/desktop."
691
+ : props.error
692
+ ? "Desktop didn’t open"
693
+ : "Opening desktop…"}
694
+ </p>
491
695
  <p className="mt-1 text-og-control leading-5 text-og-muted">
492
- {props.peerCount === 1
493
- ? "One workspace computer is available from the computer menu."
494
- : `${props.peerCount} workspace computers are available from the computer menu.`}
696
+ {props.generationLoss
697
+ ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser → New browser → Connected Chrome."
698
+ : (props.error?.message ??
699
+ (props.peerCount === 1
700
+ ? "Preparing this agent’s desktop. One peer desktop remains available from the menu."
701
+ : `Preparing this agent’s desktop. ${props.peerCount} peer desktops remain available from the menu.`))}
495
702
  </p>
496
- <button
497
- type="button"
498
- disabled={props.creating}
499
- onClick={props.onCreate}
500
- className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:cursor-not-allowed disabled:opacity-50"
501
- >
502
- {props.creating ? (
503
- <LoaderCircleIcon className="size-3.5 animate-spin" />
504
- ) : (
505
- <PlusIcon className="size-3.5" />
506
- )}
507
- {props.creating ? "Opening…" : "New computer"}
508
- </button>
703
+ {props.error && !props.generationLoss ? (
704
+ <button
705
+ type="button"
706
+ disabled={props.creating}
707
+ onClick={props.onCreate}
708
+ className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:cursor-not-allowed disabled:opacity-50"
709
+ >
710
+ {props.creating ? (
711
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
712
+ ) : (
713
+ <RotateCcwIcon className="size-3.5" />
714
+ )}
715
+ Try again
716
+ </button>
717
+ ) : null}
509
718
  </div>
510
719
  </div>
511
720
  );
@@ -520,7 +729,7 @@ function ComputerToolbar(props: {
520
729
  interventionCounts: Map<string, number>;
521
730
  onSelect: (id: string) => void;
522
731
  onFollow: () => void;
523
- onCreate: () => void;
732
+ onCreate?: (() => void) | undefined;
524
733
  onRefresh: () => void;
525
734
  }) {
526
735
  const detailsRef = useRef<HTMLDetailsElement | null>(null);
@@ -536,7 +745,9 @@ function ComputerToolbar(props: {
536
745
  <details ref={detailsRef} className="relative min-w-0">
537
746
  <summary className="flex h-7 max-w-52 cursor-pointer list-none items-center gap-2 rounded-og-sm px-2 text-og-control text-og-fg transition hover:bg-og-surface-2 [&::-webkit-details-marker]:hidden">
538
747
  <MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
539
- <span className="truncate font-medium">{selected?.name ?? "Computer"}</span>
748
+ <span className="truncate font-medium">
749
+ {selected?.name === "Computer" ? "Desktop" : (selected?.name ?? "Desktop")}
750
+ </span>
540
751
  {(props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? (
541
752
  <span className="size-1.5 shrink-0 rounded-full bg-og-status-waiting" />
542
753
  ) : null}
@@ -561,9 +772,11 @@ function ComputerToolbar(props: {
561
772
  {current.length > 0 ? (
562
773
  <MenuButton onClick={props.onFollow}>Follow agent</MenuButton>
563
774
  ) : null}
564
- <MenuButton onClick={props.onCreate} disabled={props.creating}>
565
- <PlusIcon className="size-3.5" /> New computer
566
- </MenuButton>
775
+ {props.onCreate ? (
776
+ <MenuButton onClick={props.onCreate} disabled={props.creating}>
777
+ <PlusIcon className="size-3.5" /> New desktop
778
+ </MenuButton>
779
+ ) : null}
567
780
  </div>
568
781
  </div>
569
782
  </details>
@@ -574,23 +787,25 @@ function ComputerToolbar(props: {
574
787
  type="button"
575
788
  onClick={props.onRefresh}
576
789
  className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
577
- aria-label="Refresh computers"
790
+ aria-label="Refresh desktops"
578
791
  >
579
792
  <RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
580
793
  </button>
581
- <button
582
- type="button"
583
- onClick={props.onCreate}
584
- disabled={props.creating}
585
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40"
586
- aria-label="Open a new computer"
587
- >
588
- {props.creating ? (
589
- <LoaderCircleIcon className="size-3.5 animate-spin" />
590
- ) : (
591
- <PlusIcon className="size-3.5" />
592
- )}
593
- </button>
794
+ {props.onCreate ? (
795
+ <button
796
+ type="button"
797
+ onClick={props.onCreate}
798
+ disabled={props.creating}
799
+ className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40"
800
+ aria-label="Open a new desktop"
801
+ >
802
+ {props.creating ? (
803
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
804
+ ) : (
805
+ <PlusIcon className="size-3.5" />
806
+ )}
807
+ </button>
808
+ ) : null}
594
809
  </div>
595
810
  );
596
811
  }
@@ -667,7 +882,7 @@ function ComputerTargetRail(props: {
667
882
  return (
668
883
  <div
669
884
  className="flex h-10 shrink-0 items-center gap-1 overflow-x-auto border-b border-og-border bg-og-surface-0 px-2"
670
- aria-label="Computer views"
885
+ aria-label="Desktop views"
671
886
  >
672
887
  {visualTargets.map((target) => (
673
888
  <button
@@ -715,12 +930,17 @@ function isRenderableComputerView(target: ComputerTarget): boolean {
715
930
  return target.bounds.width >= 160 && target.bounds.height >= 90;
716
931
  }
717
932
 
933
+ function isComputerFrameTarget(target: ComputerTarget): boolean {
934
+ return target.kind === "window" || target.kind === "screen";
935
+ }
936
+
718
937
  function ComputerLifecyclePanel(props: {
719
938
  session: ComputerSession;
720
939
  creating: boolean;
721
940
  onRefresh: () => Promise<void>;
722
941
  onRetry: () => void;
723
942
  }) {
943
+ const generationLoss = isAttachedChromeGenerationLoss(props.session);
724
944
  const failed = ["failed", "lost", "repair_required"].includes(props.session.lifecycle);
725
945
  const retryCreatesSession = ["failed", "lost"].includes(props.session.lifecycle);
726
946
  return (
@@ -732,13 +952,19 @@ function ComputerLifecyclePanel(props: {
732
952
  <LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-muted" />
733
953
  )}
734
954
  <p className="mt-3 text-og-menu font-medium text-og-fg">
735
- {failed ? "Computer needs attention" : lifecycleLabel(props.session.lifecycle)}
955
+ {generationLoss
956
+ ? "Chrome reconnected—open a fresh browser/desktop."
957
+ : failed
958
+ ? "Desktop needs attention"
959
+ : lifecycleLabel(props.session.lifecycle)}
736
960
  </p>
737
961
  <p className="mt-1 text-og-control leading-5 text-og-muted">
738
- {computerFailureMessage(props.session) ??
739
- "The computer is being prepared on its placement. It will appear here when ready."}
962
+ {generationLoss
963
+ ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser New browser → Connected Chrome."
964
+ : (computerFailureMessage(props.session) ??
965
+ "The desktop is being prepared on its placement. It will appear here when ready.")}
740
966
  </p>
741
- {failed ? (
967
+ {failed && !generationLoss ? (
742
968
  <button
743
969
  type="button"
744
970
  onClick={() => (retryCreatesSession ? props.onRetry() : void props.onRefresh())}
@@ -1086,7 +1312,7 @@ function ComputerViewport(props: {
1086
1312
  const clipboard = await readClipboardRef.current();
1087
1313
  if (!clipboard.text) return;
1088
1314
  if (!(await copyTextToClipboard(clipboard.text))) {
1089
- throw new Error("Computer text could not be copied to the local clipboard");
1315
+ throw new Error("Desktop text could not be copied to the local clipboard");
1090
1316
  }
1091
1317
  });
1092
1318
  };
@@ -1137,7 +1363,7 @@ function ComputerViewport(props: {
1137
1363
  }}
1138
1364
  onContextMenu={contextMenu}
1139
1365
  onWheel={wheel}
1140
- aria-label={`Interactive ${props.target?.kind ?? "computer"} view`}
1366
+ aria-label={`Interactive ${props.target?.kind ?? "desktop"} view`}
1141
1367
  />
1142
1368
  <textarea
1143
1369
  ref={inputRef}
@@ -1147,7 +1373,7 @@ function ComputerViewport(props: {
1147
1373
  onCopy={copy}
1148
1374
  onPaste={paste}
1149
1375
  className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
1150
- aria-label="Computer keyboard input"
1376
+ aria-label="Desktop keyboard input"
1151
1377
  autoCapitalize="off"
1152
1378
  autoCorrect="off"
1153
1379
  spellCheck={false}
@@ -1189,7 +1415,7 @@ function ComputerViewport(props: {
1189
1415
 
1190
1416
  function assertExactComputerClipboard(clipboard: ComputerClipboard, expected: string): void {
1191
1417
  if (clipboard.truncated || clipboard.text !== expected) {
1192
- throw new Error("Computer clipboard did not accept the exact pasted text");
1418
+ throw new Error("Desktop clipboard did not accept the exact pasted text");
1193
1419
  }
1194
1420
  }
1195
1421
 
@@ -1214,6 +1440,7 @@ function ComputerViewportFallback(props: {
1214
1440
  </div>
1215
1441
  );
1216
1442
  }
1443
+ const controlFailure = interactionControlFailureFromError(props.error);
1217
1444
  const interactive = semanticNodes(props.observation)
1218
1445
  .filter((node) => semanticAction(node) !== null)
1219
1446
  .slice(0, 10);
@@ -1227,11 +1454,20 @@ function ComputerViewportFallback(props: {
1227
1454
  <LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
1228
1455
  )}
1229
1456
  <p className="text-og-menu font-medium text-og-fg">
1230
- {props.error
1231
- ? "Live view disconnected"
1232
- : computerConnectionLabel(props.connectionState)}
1457
+ {isAttachedChromeGenerationLossError(props.error)
1458
+ ? "Chrome reconnected—open a fresh browser/desktop."
1459
+ : props.error
1460
+ ? "Live view disconnected"
1461
+ : computerConnectionLabel(props.connectionState)}
1233
1462
  </p>
1234
1463
  </div>
1464
+ {props.error ? (
1465
+ <p className="mt-2 text-og-control leading-5 text-og-muted">
1466
+ {isAttachedChromeGenerationLossError(props.error)
1467
+ ? "Chrome reconnected—open a fresh browser/desktop. Use Browser → New browser → Connected Chrome."
1468
+ : (controlFailure?.message ?? props.error.message)}
1469
+ </p>
1470
+ ) : null}
1235
1471
  {interactive.length > 0 ? (
1236
1472
  <div className="mt-3 border-t border-og-border pt-3">
1237
1473
  <p className="mb-2 text-og-xs text-og-subtle">Native controls remain available</p>
@@ -1252,13 +1488,13 @@ function ComputerViewportFallback(props: {
1252
1488
  </div>
1253
1489
  </div>
1254
1490
  ) : null}
1255
- {props.error ? (
1491
+ {props.error && !isAttachedChromeGenerationLossError(props.error) ? (
1256
1492
  <button
1257
1493
  type="button"
1258
1494
  onClick={props.onReconnect}
1259
1495
  className="mt-3 text-og-control font-medium text-og-accent hover:underline"
1260
1496
  >
1261
- Reconnect
1497
+ {controlFailure?.retryable === false ? "Try again" : "Reconnect"}
1262
1498
  </button>
1263
1499
  ) : null}
1264
1500
  </div>
@@ -1392,7 +1628,7 @@ function ComputerStatusBar(props: {
1392
1628
  ? "Full screen · input may move pointer and focus"
1393
1629
  : props.session?.capabilities?.backgroundActions
1394
1630
  ? "Window · semantic controls stay in the background"
1395
- : (props.target?.kind ?? "Computer")}
1631
+ : (props.target?.kind ?? "Desktop")}
1396
1632
  </span>
1397
1633
  {screen ? <MousePointer2Icon className="size-3" aria-hidden /> : null}
1398
1634
  {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
@@ -1466,7 +1702,7 @@ function paintCanvas(
1466
1702
  canvas.width = width;
1467
1703
  canvas.height = height;
1468
1704
  const context = canvas.getContext("2d", { alpha: false });
1469
- if (!context) throw new Error("Computer canvas is unavailable.");
1705
+ if (!context) throw new Error("Desktop canvas is unavailable.");
1470
1706
  context.drawImage(image, 0, 0, width, height);
1471
1707
  }
1472
1708
 
@@ -1474,7 +1710,7 @@ function loadImage(url: string): Promise<HTMLImageElement> {
1474
1710
  return new Promise((resolve, reject) => {
1475
1711
  const image = new Image();
1476
1712
  image.onload = () => resolve(image);
1477
- image.onerror = () => reject(new Error("Computer frame image could not be decoded."));
1713
+ image.onerror = () => reject(new Error("Desktop frame image could not be decoded."));
1478
1714
  image.src = url;
1479
1715
  });
1480
1716
  }
@@ -1545,14 +1781,31 @@ function computerFailureMessage(session: ComputerSession): string | null {
1545
1781
  case "machine_locked":
1546
1782
  return "Unlock the connected Mac, then try again.";
1547
1783
  case "controller_heartbeat_expired":
1548
- return "The computer connection was lost. Try opening it again.";
1784
+ return "The desktop connection was lost. Try opening it again.";
1785
+ case "controller_transition_expired":
1786
+ return session.placement.kind === "attached_device"
1787
+ ? "Chrome reconnected—open a fresh browser/desktop. Use Browser → New browser → Connected Chrome."
1788
+ : "The desktop connection expired. Open a new desktop.";
1549
1789
  case null:
1550
1790
  return null;
1551
1791
  default:
1552
- return "The computer could not be opened. Try again.";
1792
+ return "The desktop could not be opened. Try again.";
1553
1793
  }
1554
1794
  }
1555
1795
 
1796
+ function isAttachedChromeGenerationLoss(session: ComputerSession): boolean {
1797
+ return (
1798
+ session.lifecycle === "lost" &&
1799
+ session.failureCode === "controller_transition_expired" &&
1800
+ session.placement.kind === "attached_device"
1801
+ );
1802
+ }
1803
+
1804
+ function isAttachedChromeGenerationLossError(error: Error | null): boolean {
1805
+ if (!error) return false;
1806
+ return /placement instance changed|controller authority changed/i.test(error.message);
1807
+ }
1808
+
1556
1809
  function countInterventions(
1557
1810
  interventions: readonly InteractionIntervention[],
1558
1811
  ): Map<string, number> {
@@ -1573,7 +1826,7 @@ function interventionTitle(intervention: InteractionIntervention): string {
1573
1826
  case "confirmation":
1574
1827
  return "Action needed";
1575
1828
  case "other":
1576
- return "Computer needs your help";
1829
+ return "Desktop needs your help";
1577
1830
  }
1578
1831
  }
1579
1832
 
@@ -1586,7 +1839,7 @@ function platformLabel(session: ComputerSession): string {
1586
1839
  case "windows":
1587
1840
  return "Windows";
1588
1841
  default:
1589
- return "Computer";
1842
+ return "Desktop";
1590
1843
  }
1591
1844
  }
1592
1845
 
@@ -1599,31 +1852,31 @@ function placementLabel(session: ComputerSession): string {
1599
1852
  case "attached_device":
1600
1853
  return "Your machine";
1601
1854
  case "external_provider":
1602
- return "Cloud computer";
1855
+ return "Cloud desktop";
1603
1856
  }
1604
1857
  }
1605
1858
 
1606
1859
  function lifecycleLabel(lifecycle: ComputerSession["lifecycle"]): string {
1607
1860
  switch (lifecycle) {
1608
1861
  case "starting":
1609
- return "Opening computer…";
1862
+ return "Opening desktop…";
1610
1863
  case "suspending":
1611
- return "Pausing computer…";
1864
+ return "Pausing desktop…";
1612
1865
  case "suspended":
1613
- return "Computer paused";
1866
+ return "Desktop paused";
1614
1867
  case "restoring":
1615
- return "Restoring computer…";
1868
+ return "Restoring desktop…";
1616
1869
  case "ending":
1617
- return "Closing computer…";
1870
+ return "Closing desktop…";
1618
1871
  default:
1619
- return "Connecting to computer…";
1872
+ return "Connecting to desktop…";
1620
1873
  }
1621
1874
  }
1622
1875
 
1623
1876
  function computerConnectionLabel(state: string): string {
1624
1877
  switch (state) {
1625
1878
  case "attaching":
1626
- return "Opening computer view…";
1879
+ return "Opening desktop view…";
1627
1880
  case "connecting":
1628
1881
  return "Connecting…";
1629
1882
  case "reconnecting":
@@ -1631,6 +1884,6 @@ function computerConnectionLabel(state: string): string {
1631
1884
  case "error":
1632
1885
  return "Disconnected";
1633
1886
  default:
1634
- return "Waiting for computer…";
1887
+ return "Waiting for desktop…";
1635
1888
  }
1636
1889
  }