@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.
- package/README.md +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
- package/dist/chunk-LS4POUTY.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
- package/dist/chunk-RSXJN73N.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
- package/dist/chunk-SE4W4WQ3.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +50 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +103 -59
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +215 -18
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +529 -25
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +77 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /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(
|
|
94
|
-
|
|
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>(
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
221
|
+
selectComputerSession({ sessionId: preferred.id, pinned: false });
|
|
163
222
|
}
|
|
164
223
|
}
|
|
165
|
-
}, [
|
|
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:
|
|
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
|
|
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 (
|
|
343
|
+
if (createInFlightRef.current) return;
|
|
344
|
+
createInFlightRef.current = true;
|
|
345
|
+
setCreateError(null);
|
|
267
346
|
setCreating(true);
|
|
268
|
-
void createSession({ sessionId, name: "
|
|
347
|
+
void createSession({ sessionId, name: "Desktop" })
|
|
269
348
|
.then((response) => {
|
|
270
|
-
|
|
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
|
-
.
|
|
273
|
-
|
|
274
|
-
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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">
|
|
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
|
-
|
|
532
|
+
{error?.message ?? "Preparing this agent’s desktop. It will appear here when ready."}
|
|
346
533
|
</p>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
564
|
+
selectComputerSession({ sessionId: computerSessionId, pinned: true })
|
|
379
565
|
}
|
|
380
566
|
onFollow={() => {
|
|
381
|
-
if (relevant[0])
|
|
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
|
|
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
|
|
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={
|
|
449
|
-
|
|
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, "
|
|
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
|
-
|
|
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">
|
|
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.
|
|
493
|
-
? "
|
|
494
|
-
:
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|
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">
|
|
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
|
-
|
|
565
|
-
<
|
|
566
|
-
|
|
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
|
|
790
|
+
aria-label="Refresh desktops"
|
|
578
791
|
>
|
|
579
792
|
<RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
|
|
580
793
|
</button>
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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="
|
|
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
|
-
{
|
|
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
|
-
{
|
|
739
|
-
"
|
|
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("
|
|
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 ?? "
|
|
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="
|
|
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("
|
|
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
|
-
? "
|
|
1232
|
-
:
|
|
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 ?? "
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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 "
|
|
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 "
|
|
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
|
|
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
|
|
1862
|
+
return "Opening desktop…";
|
|
1610
1863
|
case "suspending":
|
|
1611
|
-
return "Pausing
|
|
1864
|
+
return "Pausing desktop…";
|
|
1612
1865
|
case "suspended":
|
|
1613
|
-
return "
|
|
1866
|
+
return "Desktop paused";
|
|
1614
1867
|
case "restoring":
|
|
1615
|
-
return "Restoring
|
|
1868
|
+
return "Restoring desktop…";
|
|
1616
1869
|
case "ending":
|
|
1617
|
-
return "Closing
|
|
1870
|
+
return "Closing desktop…";
|
|
1618
1871
|
default:
|
|
1619
|
-
return "Connecting to
|
|
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
|
|
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
|
|
1887
|
+
return "Waiting for desktop…";
|
|
1635
1888
|
}
|
|
1636
1889
|
}
|