@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.
- package/README.md +36 -11
- 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-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
- package/dist/chunk-5RZN3DD5.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-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-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
- package/dist/chunk-UOLJWT5Y.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-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
- package/dist/chunk-XSA2QAKP.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 +10 -3
- package/dist/realtime.js +138 -29
- 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 +48 -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 +91 -55
- 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 +162 -17
- 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 +200 -37
- 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 +476 -23
- 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 +75 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AttachedBrowserBridge,
|
|
2
3
|
AttachedBrowserDevice,
|
|
3
4
|
BrowserAction,
|
|
4
5
|
BrowserActionReceipt,
|
|
@@ -15,7 +16,9 @@ import type {
|
|
|
15
16
|
InteractionPlacement,
|
|
16
17
|
InteractionIntervention,
|
|
17
18
|
InteractionSemanticNode,
|
|
19
|
+
SiteAuthConnection,
|
|
18
20
|
} from "@opengeni/sdk/interaction";
|
|
21
|
+
import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
|
|
19
22
|
import {
|
|
20
23
|
BugIcon,
|
|
21
24
|
ArchiveIcon,
|
|
@@ -61,9 +64,11 @@ import { useBrowserDownloads } from "../hooks/use-browser-downloads";
|
|
|
61
64
|
import { useBrowserSession } from "../hooks/use-browser-session";
|
|
62
65
|
import { useBrowserSessions } from "../hooks/use-browser-sessions";
|
|
63
66
|
import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
|
|
67
|
+
import { useSiteAuthConnections } from "../hooks/use-site-auth-connections";
|
|
64
68
|
import { cn } from "../lib/cn";
|
|
65
69
|
import { copyTextToClipboard } from "../lib/clipboard";
|
|
66
70
|
import { formatBytes } from "../lib/format";
|
|
71
|
+
import { isSourcePlacementChangedError } from "../lib/interaction-errors";
|
|
67
72
|
import type { EmbeddedBrowserInteractionClientOverride } from "../session-context";
|
|
68
73
|
import { browserKey, HUMAN_BROWSER_HOME_URL, normalizeBrowserAddress } from "./browser-input";
|
|
69
74
|
import { InteractionInterventionBanner } from "./interaction-intervention-banner";
|
|
@@ -93,12 +98,18 @@ export type BrowserViewerProps = EmbeddedBrowserInteractionClientOverride & {
|
|
|
93
98
|
| undefined;
|
|
94
99
|
/** Navigate to the exact linked ComputerSession; never a lookalike desktop. */
|
|
95
100
|
onOpenComputer?: ((computerSessionId: string) => void) | undefined;
|
|
101
|
+
/** Restore and report the BrowserSession selected in this task's dock. */
|
|
102
|
+
initialBrowserSessionId?: string | null | undefined;
|
|
103
|
+
onBrowserSessionIdChange?: ((browserSessionId: string | null) => void) | undefined;
|
|
104
|
+
/** Host-owned install/setup surface for attaching an existing Chrome profile.
|
|
105
|
+
* The machine bridge remains discoverable even when this URL is omitted. */
|
|
106
|
+
browserExtensionSetupUrl?: string | undefined;
|
|
96
107
|
};
|
|
97
108
|
|
|
98
109
|
type BrowserSelection = { sessionId: string; pinned: boolean } | null;
|
|
99
110
|
type BrowserLaunchChoice =
|
|
100
111
|
| { kind: "clean" }
|
|
101
|
-
| { kind: "profile"; identityId: string }
|
|
112
|
+
| { kind: "profile"; identityId: string; baseRevisionId?: string }
|
|
102
113
|
| { kind: "attached"; device: AttachedBrowserDevice };
|
|
103
114
|
type PointerStart = {
|
|
104
115
|
x: number;
|
|
@@ -134,26 +145,72 @@ export function BrowserViewer({
|
|
|
134
145
|
renderEmpty,
|
|
135
146
|
createLinkedComputer,
|
|
136
147
|
onOpenComputer,
|
|
148
|
+
initialBrowserSessionId,
|
|
149
|
+
onBrowserSessionIdChange,
|
|
150
|
+
browserExtensionSetupUrl,
|
|
137
151
|
...override
|
|
138
152
|
}: BrowserViewerProps) {
|
|
139
153
|
const registry = useBrowserSessions({ ...override, sessionId, enabled });
|
|
140
154
|
const attached = useAttachedBrowsers({ ...override, enabled });
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
[profiles.identities],
|
|
155
|
+
const onlineAttachedBridges = useMemo(
|
|
156
|
+
() => attached.bridges.filter((bridge) => bridge.state === "online"),
|
|
157
|
+
[attached.bridges],
|
|
145
158
|
);
|
|
159
|
+
const profiles = useBrowserIdentities({ ...override, enabled, includeArchived: true });
|
|
160
|
+
const siteAuth = useSiteAuthConnections({
|
|
161
|
+
...override,
|
|
162
|
+
enabled: enabled && profiles.identities.length > 0,
|
|
163
|
+
});
|
|
146
164
|
const createRegistryBrowser = registry.create;
|
|
165
|
+
const refreshRegistry = registry.refresh;
|
|
147
166
|
const loadProfileRevisions = profiles.revisions;
|
|
148
167
|
const liveSessions = useMemo(
|
|
149
168
|
() => registry.sessions.filter((session) => isLiveBrowser(session)),
|
|
150
169
|
[registry.sessions],
|
|
151
170
|
);
|
|
171
|
+
const attachedGenerationLoss = useMemo(
|
|
172
|
+
() => attachedChromeGenerationLoss(registry.sessions),
|
|
173
|
+
[registry.sessions],
|
|
174
|
+
);
|
|
175
|
+
const endedGenerationLossRef = useRef(new Set<string>());
|
|
176
|
+
const endStaleBrowser = registry.end;
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!enabled) return;
|
|
179
|
+
for (const session of registry.sessions) {
|
|
180
|
+
if (
|
|
181
|
+
session.lifecycle !== "lost" ||
|
|
182
|
+
session.failureCode !== "controller_transition_expired" ||
|
|
183
|
+
session.placement.kind !== "attached_device"
|
|
184
|
+
) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (endedGenerationLossRef.current.has(session.id)) continue;
|
|
188
|
+
endedGenerationLossRef.current.add(session.id);
|
|
189
|
+
void endStaleBrowser(session.id).catch(() => {
|
|
190
|
+
endedGenerationLossRef.current.delete(session.id);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}, [enabled, endStaleBrowser, registry.sessions]);
|
|
194
|
+
const replacementChromeDevice = useMemo(() => {
|
|
195
|
+
if (!attachedGenerationLoss) return null;
|
|
196
|
+
return (
|
|
197
|
+
attached.devices.find((candidate) => candidate.id === attachedGenerationLoss.deviceId) ?? null
|
|
198
|
+
);
|
|
199
|
+
}, [attached.devices, attachedGenerationLoss]);
|
|
152
200
|
const relevant = useMemo(
|
|
153
201
|
() => registry.relevantSessions.filter((session) => isLiveBrowser(session)),
|
|
154
202
|
[registry.relevantSessions],
|
|
155
203
|
);
|
|
156
|
-
const [selection, setSelection] = useState<BrowserSelection>(
|
|
204
|
+
const [selection, setSelection] = useState<BrowserSelection>(() =>
|
|
205
|
+
initialBrowserSessionId ? { sessionId: initialBrowserSessionId, pinned: true } : null,
|
|
206
|
+
);
|
|
207
|
+
const selectBrowserSession = useCallback(
|
|
208
|
+
(next: BrowserSelection) => {
|
|
209
|
+
setSelection(next);
|
|
210
|
+
onBrowserSessionIdChange?.(next?.sessionId ?? null);
|
|
211
|
+
},
|
|
212
|
+
[onBrowserSessionIdChange],
|
|
213
|
+
);
|
|
157
214
|
const interventions = useInteractionInterventions({
|
|
158
215
|
...override,
|
|
159
216
|
enabled,
|
|
@@ -188,11 +245,14 @@ export function BrowserViewer({
|
|
|
188
245
|
useEffect(() => {
|
|
189
246
|
if (previousSessionIdRef.current !== sessionId) {
|
|
190
247
|
previousSessionIdRef.current = sessionId;
|
|
191
|
-
setSelection(
|
|
248
|
+
setSelection(
|
|
249
|
+
initialBrowserSessionId ? { sessionId: initialBrowserSessionId, pinned: true } : null,
|
|
250
|
+
);
|
|
192
251
|
}
|
|
193
|
-
}, [sessionId]);
|
|
252
|
+
}, [initialBrowserSessionId, sessionId]);
|
|
194
253
|
|
|
195
254
|
useEffect(() => {
|
|
255
|
+
if (!enabled || registry.loading) return;
|
|
196
256
|
const selectedStillLive = liveSessions.some((session) => session.id === selection?.sessionId);
|
|
197
257
|
// A manually selected workspace browser stays selected, but an unrelated
|
|
198
258
|
// browser must never become the implicit browser for this agent. That made
|
|
@@ -200,15 +260,15 @@ export function BrowserViewer({
|
|
|
200
260
|
if (selection?.pinned && selectedStillLive) return;
|
|
201
261
|
const preferred = relevant[0] ?? null;
|
|
202
262
|
if (!preferred) {
|
|
203
|
-
if (selection)
|
|
263
|
+
if (selection) selectBrowserSession(null);
|
|
204
264
|
return;
|
|
205
265
|
}
|
|
206
266
|
if (!selectedStillLive || !selection?.pinned) {
|
|
207
267
|
if (selection?.sessionId !== preferred.id || selection.pinned) {
|
|
208
|
-
|
|
268
|
+
selectBrowserSession({ sessionId: preferred.id, pinned: false });
|
|
209
269
|
}
|
|
210
270
|
}
|
|
211
|
-
}, [liveSessions, relevant, selection]);
|
|
271
|
+
}, [enabled, liveSessions, registry.loading, relevant, selectBrowserSession, selection]);
|
|
212
272
|
|
|
213
273
|
const selectedRegistrySession = useMemo(
|
|
214
274
|
() => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
|
|
@@ -289,6 +349,10 @@ export function BrowserViewer({
|
|
|
289
349
|
browserSessionId: selection?.sessionId ?? null,
|
|
290
350
|
enabled: enabled && selection !== null && controllerReady,
|
|
291
351
|
});
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
if (!isSourcePlacementChangedError(browser.error, "browser_session")) return;
|
|
354
|
+
void refreshRegistry();
|
|
355
|
+
}, [browser.error, refreshRegistry]);
|
|
292
356
|
const downloads = useBrowserDownloads({
|
|
293
357
|
...override,
|
|
294
358
|
browserSessionId: selection?.sessionId ?? null,
|
|
@@ -363,6 +427,15 @@ export function BrowserViewer({
|
|
|
363
427
|
) ?? null,
|
|
364
428
|
[browser.session?.identityId, profiles.identities, selectedRegistrySession?.identityId],
|
|
365
429
|
);
|
|
430
|
+
const selectedProfileAuth = useMemo(
|
|
431
|
+
() =>
|
|
432
|
+
selectedProfile
|
|
433
|
+
? siteAuth.connections.filter(
|
|
434
|
+
(connection) => connection.preferredIdentityId === selectedProfile.id,
|
|
435
|
+
)
|
|
436
|
+
: [],
|
|
437
|
+
[selectedProfile, siteAuth.connections],
|
|
438
|
+
);
|
|
366
439
|
|
|
367
440
|
useEffect(() => {
|
|
368
441
|
diagnosticRequestRef.current += 1;
|
|
@@ -469,8 +542,15 @@ export function BrowserViewer({
|
|
|
469
542
|
if (creating) return;
|
|
470
543
|
const identity =
|
|
471
544
|
choice.kind === "profile"
|
|
472
|
-
?
|
|
545
|
+
? profiles.identities.find((candidate) => candidate.id === choice.identityId)
|
|
473
546
|
: null;
|
|
547
|
+
if (choice.kind === "profile" && (!identity || identity.status !== "active")) {
|
|
548
|
+
onNotify?.({
|
|
549
|
+
kind: "error",
|
|
550
|
+
message: "This browser profile is no longer available. Refresh and choose another.",
|
|
551
|
+
});
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
474
554
|
const device = choice.kind === "attached" ? choice.device : null;
|
|
475
555
|
const browserName =
|
|
476
556
|
device?.profileLabel ?? device?.name ?? (identity ? `${identity.name} browser` : "Browser");
|
|
@@ -481,7 +561,7 @@ export function BrowserViewer({
|
|
|
481
561
|
if (createLinkedComputer) {
|
|
482
562
|
try {
|
|
483
563
|
linkedComputer = await createLinkedComputer(
|
|
484
|
-
`${browserName}
|
|
564
|
+
`${browserName} desktop`,
|
|
485
565
|
device ? { kind: "attached_device", deviceId: device.id } : undefined,
|
|
486
566
|
);
|
|
487
567
|
} catch {
|
|
@@ -514,13 +594,20 @@ export function BrowserViewer({
|
|
|
514
594
|
placement: linkedComputer.placement,
|
|
515
595
|
}
|
|
516
596
|
: {}),
|
|
517
|
-
...(identity
|
|
597
|
+
...(identity
|
|
598
|
+
? {
|
|
599
|
+
identityId: identity.id,
|
|
600
|
+
...(choice.kind === "profile" && choice.baseRevisionId
|
|
601
|
+
? { baseRevisionId: choice.baseRevisionId }
|
|
602
|
+
: {}),
|
|
603
|
+
}
|
|
604
|
+
: {}),
|
|
518
605
|
});
|
|
519
|
-
|
|
606
|
+
selectBrowserSession({ sessionId: response.session.id, pinned: true });
|
|
520
607
|
if (computerViewUnavailable) {
|
|
521
608
|
onNotify?.({
|
|
522
609
|
kind: "info",
|
|
523
|
-
message: "Browser opened.
|
|
610
|
+
message: "Browser opened. Desktop view is unavailable on this placement.",
|
|
524
611
|
});
|
|
525
612
|
}
|
|
526
613
|
})()
|
|
@@ -533,7 +620,8 @@ export function BrowserViewer({
|
|
|
533
620
|
creating,
|
|
534
621
|
notifyError,
|
|
535
622
|
onNotify,
|
|
536
|
-
|
|
623
|
+
selectBrowserSession,
|
|
624
|
+
profiles.identities,
|
|
537
625
|
sessionId,
|
|
538
626
|
],
|
|
539
627
|
);
|
|
@@ -656,17 +744,25 @@ export function BrowserViewer({
|
|
|
656
744
|
<span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
|
|
657
745
|
<Globe2Icon className="size-4.5" />
|
|
658
746
|
</span>
|
|
659
|
-
<p className="mt-3 text-og-menu font-medium text-og-fg">
|
|
747
|
+
<p className="mt-3 text-og-menu font-medium text-og-fg">
|
|
748
|
+
{attachedGenerationLoss
|
|
749
|
+
? "Chrome reconnected—open a fresh browser/desktop."
|
|
750
|
+
: "No browser open"}
|
|
751
|
+
</p>
|
|
660
752
|
<p className="mt-1 text-og-control leading-5 text-og-muted">
|
|
661
|
-
|
|
753
|
+
{attachedGenerationLoss
|
|
754
|
+
? "This Chrome profile is live again, but the previous browser cannot move to the new connection. Use Browser → New browser → Connected Chrome."
|
|
755
|
+
: "A browser appears here when this agent—or another agent in the workspace—opens one."}
|
|
662
756
|
</p>
|
|
663
757
|
<BrowserLaunchMenu
|
|
758
|
+
attachedBridges={onlineAttachedBridges}
|
|
664
759
|
attachedDevices={attached.devices}
|
|
665
760
|
identities={profiles.identities}
|
|
666
761
|
creating={creating}
|
|
667
762
|
mutatingProfile={savingProfile}
|
|
668
763
|
onCreate={createBrowser}
|
|
669
764
|
onRestore={(identity) => updateProfile(identity, { status: "active" })}
|
|
765
|
+
setupUrl={browserExtensionSetupUrl}
|
|
670
766
|
prominent
|
|
671
767
|
/>
|
|
672
768
|
{registry.error ? (
|
|
@@ -686,23 +782,31 @@ export function BrowserViewer({
|
|
|
686
782
|
relevantSessionIds={new Set(relevant.map((session) => session.id))}
|
|
687
783
|
selectedSessionId={selection?.sessionId ?? null}
|
|
688
784
|
identities={profiles.identities}
|
|
785
|
+
attachedBridges={onlineAttachedBridges}
|
|
689
786
|
attachedDevices={attached.devices}
|
|
690
787
|
selectedProfile={selectedProfile}
|
|
691
788
|
baseRevisionOrdinal={baseRevisionOrdinal}
|
|
692
789
|
profileHistory={profileHistory}
|
|
790
|
+
authConnections={selectedProfileAuth}
|
|
693
791
|
creating={creating}
|
|
694
792
|
savingProfile={savingProfile}
|
|
695
793
|
refreshing={registry.refreshing || attached.refreshing}
|
|
696
794
|
interventionCounts={interventionCounts}
|
|
697
|
-
onSelect={(browserSessionId) =>
|
|
795
|
+
onSelect={(browserSessionId) =>
|
|
796
|
+
selectBrowserSession({ sessionId: browserSessionId, pinned: true })
|
|
797
|
+
}
|
|
698
798
|
onFollow={() => {
|
|
699
799
|
const preferred = relevant[0];
|
|
700
|
-
if (preferred)
|
|
800
|
+
if (preferred) selectBrowserSession({ sessionId: preferred.id, pinned: false });
|
|
701
801
|
}}
|
|
702
802
|
onCreate={createBrowser}
|
|
703
803
|
onSaveProfile={saveProfileVersion}
|
|
704
804
|
onUpdateProfile={updateProfile}
|
|
805
|
+
onOpenProfileVersion={(identityId, baseRevisionId) =>
|
|
806
|
+
createBrowser({ kind: "profile", identityId, baseRevisionId })
|
|
807
|
+
}
|
|
705
808
|
onRefresh={() => void Promise.all([registry.refresh(), attached.refresh()])}
|
|
809
|
+
setupUrl={browserExtensionSetupUrl}
|
|
706
810
|
/>
|
|
707
811
|
<InteractionInterventionBanner
|
|
708
812
|
interventions={selectedInterventions}
|
|
@@ -787,7 +891,26 @@ export function BrowserViewer({
|
|
|
787
891
|
return receipt;
|
|
788
892
|
}}
|
|
789
893
|
onReadClipboard={browser.readClipboard}
|
|
790
|
-
onReconnect={
|
|
894
|
+
onReconnect={
|
|
895
|
+
attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error)
|
|
896
|
+
? () => {
|
|
897
|
+
if (replacementChromeDevice) {
|
|
898
|
+
createBrowser({ kind: "attached", device: replacementChromeDevice });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
: frames.reconnect
|
|
902
|
+
}
|
|
903
|
+
reconnectLabel={
|
|
904
|
+
(attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error)) &&
|
|
905
|
+
replacementChromeDevice
|
|
906
|
+
? "Open a fresh Connected Chrome"
|
|
907
|
+
: undefined
|
|
908
|
+
}
|
|
909
|
+
reconnectMessage={
|
|
910
|
+
attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error)
|
|
911
|
+
? "Chrome reconnected—open a fresh browser/desktop."
|
|
912
|
+
: undefined
|
|
913
|
+
}
|
|
791
914
|
onError={(cause) => notifyError(cause, "Browser input failed.")}
|
|
792
915
|
/>
|
|
793
916
|
<BrowserStatusBar
|
|
@@ -884,11 +1007,13 @@ function BrowserToolbar(props: {
|
|
|
884
1007
|
sessions: BrowserSession[];
|
|
885
1008
|
relevantSessionIds: Set<string>;
|
|
886
1009
|
selectedSessionId: string | null;
|
|
1010
|
+
attachedBridges: AttachedBrowserBridge[];
|
|
887
1011
|
attachedDevices: AttachedBrowserDevice[];
|
|
888
1012
|
identities: BrowserIdentity[];
|
|
889
1013
|
selectedProfile: BrowserIdentity | null;
|
|
890
1014
|
baseRevisionOrdinal: number | null;
|
|
891
1015
|
profileHistory: { identityId: string; loading: boolean; revisions: BrowserRevision[] } | null;
|
|
1016
|
+
authConnections: SiteAuthConnection[];
|
|
892
1017
|
creating: boolean;
|
|
893
1018
|
savingProfile: boolean;
|
|
894
1019
|
refreshing: boolean;
|
|
@@ -901,7 +1026,9 @@ function BrowserToolbar(props: {
|
|
|
901
1026
|
identity: BrowserIdentity,
|
|
902
1027
|
update: { name?: string; status?: "active" | "archived"; defaultRevisionId?: string },
|
|
903
1028
|
) => Promise<boolean>;
|
|
1029
|
+
onOpenProfileVersion: (identityId: string, baseRevisionId: string) => void;
|
|
904
1030
|
onRefresh: () => void;
|
|
1031
|
+
setupUrl?: string | undefined;
|
|
905
1032
|
}) {
|
|
906
1033
|
const detailsRef = useRef<HTMLDetailsElement | null>(null);
|
|
907
1034
|
const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
|
|
@@ -957,9 +1084,11 @@ function BrowserToolbar(props: {
|
|
|
957
1084
|
identity={props.selectedProfile}
|
|
958
1085
|
baseRevisionOrdinal={props.baseRevisionOrdinal}
|
|
959
1086
|
history={props.profileHistory}
|
|
1087
|
+
authConnections={props.authConnections}
|
|
960
1088
|
saving={props.savingProfile}
|
|
961
1089
|
onSave={props.onSaveProfile}
|
|
962
1090
|
onUpdate={props.onUpdateProfile}
|
|
1091
|
+
onOpenVersion={props.onOpenProfileVersion}
|
|
963
1092
|
/>
|
|
964
1093
|
<button
|
|
965
1094
|
type="button"
|
|
@@ -970,12 +1099,14 @@ function BrowserToolbar(props: {
|
|
|
970
1099
|
<RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
|
|
971
1100
|
</button>
|
|
972
1101
|
<BrowserLaunchMenu
|
|
1102
|
+
attachedBridges={props.attachedBridges}
|
|
973
1103
|
attachedDevices={props.attachedDevices}
|
|
974
1104
|
identities={props.identities}
|
|
975
1105
|
creating={props.creating}
|
|
976
1106
|
mutatingProfile={props.savingProfile}
|
|
977
1107
|
onCreate={props.onCreate}
|
|
978
1108
|
onRestore={(identity) => props.onUpdateProfile(identity, { status: "active" })}
|
|
1109
|
+
setupUrl={props.setupUrl}
|
|
979
1110
|
/>
|
|
980
1111
|
</div>
|
|
981
1112
|
);
|
|
@@ -1065,12 +1196,14 @@ function BrowserSessionGroup(props: {
|
|
|
1065
1196
|
}
|
|
1066
1197
|
|
|
1067
1198
|
function BrowserLaunchMenu(props: {
|
|
1199
|
+
attachedBridges: AttachedBrowserBridge[];
|
|
1068
1200
|
attachedDevices: AttachedBrowserDevice[];
|
|
1069
1201
|
identities: BrowserIdentity[];
|
|
1070
1202
|
creating: boolean;
|
|
1071
1203
|
mutatingProfile: boolean;
|
|
1072
1204
|
onCreate: (choice?: BrowserLaunchChoice) => void;
|
|
1073
1205
|
onRestore: (identity: BrowserIdentity) => Promise<boolean>;
|
|
1206
|
+
setupUrl?: string | undefined;
|
|
1074
1207
|
prominent?: boolean;
|
|
1075
1208
|
}) {
|
|
1076
1209
|
const detailsRef = useRef<HTMLDetailsElement | null>(null);
|
|
@@ -1113,7 +1246,7 @@ function BrowserLaunchMenu(props: {
|
|
|
1113
1246
|
props.prominent ? "left-1/2 top-10 -translate-x-1/2" : "right-0 top-8",
|
|
1114
1247
|
)}
|
|
1115
1248
|
>
|
|
1116
|
-
{props.attachedDevices.length > 0 ? (
|
|
1249
|
+
{props.attachedDevices.length > 0 || props.attachedBridges.length > 0 ? (
|
|
1117
1250
|
<div className="pb-1">
|
|
1118
1251
|
<p className="px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
|
|
1119
1252
|
Connected Chrome
|
|
@@ -1138,12 +1271,48 @@ function BrowserLaunchMenu(props: {
|
|
|
1138
1271
|
<span className="size-1.5 shrink-0 rounded-full bg-og-status-running" />
|
|
1139
1272
|
</button>
|
|
1140
1273
|
))}
|
|
1274
|
+
{props.attachedBridges.length > 0 ? (
|
|
1275
|
+
props.setupUrl ? (
|
|
1276
|
+
<a
|
|
1277
|
+
href={props.setupUrl}
|
|
1278
|
+
target="_blank"
|
|
1279
|
+
rel="noreferrer"
|
|
1280
|
+
onClick={() => detailsRef.current?.removeAttribute("open")}
|
|
1281
|
+
className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
|
|
1282
|
+
>
|
|
1283
|
+
<PlusIcon className="size-3.5 shrink-0 text-og-muted" />
|
|
1284
|
+
<span className="min-w-0 flex-1">
|
|
1285
|
+
<span className="block truncate text-og-control text-og-fg">
|
|
1286
|
+
{props.attachedDevices.length > 0
|
|
1287
|
+
? "Connect another Chrome profile"
|
|
1288
|
+
: "Connect this Chrome profile"}
|
|
1289
|
+
</span>
|
|
1290
|
+
<span className="block truncate text-og-xs text-og-subtle">
|
|
1291
|
+
Machine connected · Chrome extension missing
|
|
1292
|
+
</span>
|
|
1293
|
+
</span>
|
|
1294
|
+
</a>
|
|
1295
|
+
) : (
|
|
1296
|
+
<div className="flex w-full items-center gap-2 px-2 py-2 text-left">
|
|
1297
|
+
<MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
|
|
1298
|
+
<span className="min-w-0 flex-1">
|
|
1299
|
+
<span className="block truncate text-og-control text-og-fg">
|
|
1300
|
+
Chrome connection ready
|
|
1301
|
+
</span>
|
|
1302
|
+
<span className="block text-og-xs leading-4 text-og-subtle">
|
|
1303
|
+
Install the OpenGeni Browser extension in the profile you want to use.
|
|
1304
|
+
</span>
|
|
1305
|
+
</span>
|
|
1306
|
+
</div>
|
|
1307
|
+
)
|
|
1308
|
+
) : null}
|
|
1141
1309
|
</div>
|
|
1142
1310
|
) : null}
|
|
1143
1311
|
<p
|
|
1144
1312
|
className={cn(
|
|
1145
1313
|
"px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle",
|
|
1146
|
-
props.attachedDevices.length > 0
|
|
1314
|
+
(props.attachedDevices.length > 0 || props.attachedBridges.length > 0) &&
|
|
1315
|
+
"border-t border-og-border",
|
|
1147
1316
|
)}
|
|
1148
1317
|
>
|
|
1149
1318
|
Open isolated browser
|
|
@@ -1213,12 +1382,14 @@ function BrowserProfileMenu(props: {
|
|
|
1213
1382
|
identity: BrowserIdentity | null;
|
|
1214
1383
|
baseRevisionOrdinal: number | null;
|
|
1215
1384
|
history: { identityId: string; loading: boolean; revisions: BrowserRevision[] } | null;
|
|
1385
|
+
authConnections: SiteAuthConnection[];
|
|
1216
1386
|
saving: boolean;
|
|
1217
1387
|
onSave: (newProfileName?: string) => Promise<boolean>;
|
|
1218
1388
|
onUpdate: (
|
|
1219
1389
|
identity: BrowserIdentity,
|
|
1220
1390
|
update: { name?: string; status?: "active" | "archived"; defaultRevisionId?: string },
|
|
1221
1391
|
) => Promise<boolean>;
|
|
1392
|
+
onOpenVersion: (identityId: string, baseRevisionId: string) => void;
|
|
1222
1393
|
}) {
|
|
1223
1394
|
const detailsRef = useRef<HTMLDetailsElement | null>(null);
|
|
1224
1395
|
const nameInputId = useId();
|
|
@@ -1237,6 +1408,10 @@ function BrowserProfileMenu(props: {
|
|
|
1237
1408
|
detailsRef.current?.removeAttribute("open");
|
|
1238
1409
|
}
|
|
1239
1410
|
};
|
|
1411
|
+
const openVersion = (identityId: string, baseRevisionId: string) => {
|
|
1412
|
+
detailsRef.current?.removeAttribute("open");
|
|
1413
|
+
props.onOpenVersion(identityId, baseRevisionId);
|
|
1414
|
+
};
|
|
1240
1415
|
const versionLabel = attached
|
|
1241
1416
|
? "live"
|
|
1242
1417
|
: props.identity
|
|
@@ -1299,37 +1474,57 @@ function BrowserProfileMenu(props: {
|
|
|
1299
1474
|
{revisions.map((revision) => {
|
|
1300
1475
|
const isDefault = revision.id === props.identity?.defaultRevisionId;
|
|
1301
1476
|
const isCurrent = revision.ordinal === props.baseRevisionOrdinal;
|
|
1477
|
+
const materialization = revisionMaterializationSummary(revision);
|
|
1302
1478
|
return (
|
|
1303
|
-
<
|
|
1479
|
+
<div
|
|
1304
1480
|
key={revision.id}
|
|
1305
|
-
|
|
1306
|
-
disabled={isDefault || props.saving || props.identity?.status === "archived"}
|
|
1307
|
-
onClick={() =>
|
|
1308
|
-
void props.onUpdate(props.identity!, { defaultRevisionId: revision.id })
|
|
1309
|
-
}
|
|
1310
|
-
className="flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2 disabled:cursor-default disabled:opacity-70"
|
|
1481
|
+
className="flex w-full items-center gap-1 rounded-og-sm transition hover:bg-og-surface-2"
|
|
1311
1482
|
>
|
|
1312
|
-
<
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
? "
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1483
|
+
<button
|
|
1484
|
+
type="button"
|
|
1485
|
+
disabled={props.saving || props.identity?.status === "archived"}
|
|
1486
|
+
aria-label={`Open ${props.identity!.name} version ${revision.ordinal}`}
|
|
1487
|
+
onClick={() => openVersion(props.identity!.id, revision.id)}
|
|
1488
|
+
className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left disabled:opacity-50"
|
|
1489
|
+
>
|
|
1490
|
+
<span className="grid size-4 place-items-center text-og-muted">
|
|
1491
|
+
{isCurrent ? <CheckIcon className="size-3.5 text-og-accent" /> : null}
|
|
1492
|
+
</span>
|
|
1493
|
+
<span className="min-w-0 flex-1">
|
|
1494
|
+
<span className="block text-og-control text-og-fg">
|
|
1495
|
+
Version {revision.ordinal}
|
|
1496
|
+
</span>
|
|
1497
|
+
<span className="block truncate text-[10px] text-og-subtle">
|
|
1498
|
+
{materialization.label}
|
|
1499
|
+
</span>
|
|
1500
|
+
</span>
|
|
1501
|
+
{isCurrent ? (
|
|
1502
|
+
<span className="text-[10px] text-og-subtle">Current</span>
|
|
1503
|
+
) : null}
|
|
1504
|
+
</button>
|
|
1505
|
+
<button
|
|
1506
|
+
type="button"
|
|
1507
|
+
disabled={isDefault || props.saving || props.identity?.status === "archived"}
|
|
1508
|
+
aria-label={
|
|
1509
|
+
isDefault
|
|
1510
|
+
? `${props.identity!.name} version ${revision.ordinal} is the default`
|
|
1511
|
+
: `Use ${props.identity!.name} version ${revision.ordinal} by default`
|
|
1512
|
+
}
|
|
1513
|
+
title={isDefault ? "Default version" : "Use by default"}
|
|
1514
|
+
onClick={() =>
|
|
1515
|
+
void props.onUpdate(props.identity!, { defaultRevisionId: revision.id })
|
|
1516
|
+
}
|
|
1517
|
+
className="mr-1 shrink-0 rounded-og-sm px-2 py-1 text-[10px] text-og-subtle transition hover:bg-og-surface-3 hover:text-og-fg disabled:cursor-default disabled:opacity-70"
|
|
1518
|
+
>
|
|
1519
|
+
{isDefault ? "Default" : "Make default"}
|
|
1520
|
+
</button>
|
|
1521
|
+
</div>
|
|
1328
1522
|
);
|
|
1329
1523
|
})}
|
|
1330
1524
|
</div>
|
|
1331
1525
|
<p className="mt-1 px-2 text-[10px] leading-4 text-og-subtle">
|
|
1332
|
-
Changing the default affects only browsers opened later.
|
|
1526
|
+
Changing the default affects only browsers opened later. Saved browser data can be
|
|
1527
|
+
copied; a website may still expire or re-verify its own session.
|
|
1333
1528
|
</p>
|
|
1334
1529
|
</div>
|
|
1335
1530
|
) : props.identity &&
|
|
@@ -1339,6 +1534,34 @@ function BrowserProfileMenu(props: {
|
|
|
1339
1534
|
<LoaderCircleIcon className="size-3 animate-spin" /> Loading saved versions…
|
|
1340
1535
|
</p>
|
|
1341
1536
|
) : null}
|
|
1537
|
+
{props.identity && props.authConnections.length > 0 ? (
|
|
1538
|
+
<div className="mt-3 border-t border-og-border pt-2">
|
|
1539
|
+
<p className="px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
|
|
1540
|
+
Login health
|
|
1541
|
+
</p>
|
|
1542
|
+
<div className="mt-1 space-y-1">
|
|
1543
|
+
{props.authConnections.map((connection) => {
|
|
1544
|
+
const health = siteAuthHealth(connection);
|
|
1545
|
+
return (
|
|
1546
|
+
<div
|
|
1547
|
+
key={connection.id}
|
|
1548
|
+
className="flex items-start gap-2 rounded-og-sm px-2 py-1.5"
|
|
1549
|
+
>
|
|
1550
|
+
<span className={cn("mt-1 size-1.5 shrink-0 rounded-full", health.dotClass)} />
|
|
1551
|
+
<span className="min-w-0 flex-1">
|
|
1552
|
+
<span className="block truncate text-og-control text-og-fg">
|
|
1553
|
+
{connection.name}
|
|
1554
|
+
</span>
|
|
1555
|
+
<span className="block truncate text-[10px] text-og-subtle">
|
|
1556
|
+
{connection.accountLabel} · {health.label}
|
|
1557
|
+
</span>
|
|
1558
|
+
</span>
|
|
1559
|
+
</div>
|
|
1560
|
+
);
|
|
1561
|
+
})}
|
|
1562
|
+
</div>
|
|
1563
|
+
</div>
|
|
1564
|
+
) : null}
|
|
1342
1565
|
{canSave ? (
|
|
1343
1566
|
props.identity ? (
|
|
1344
1567
|
<button
|
|
@@ -1427,6 +1650,45 @@ function BrowserProfileMenu(props: {
|
|
|
1427
1650
|
);
|
|
1428
1651
|
}
|
|
1429
1652
|
|
|
1653
|
+
function revisionMaterializationSummary(revision: BrowserRevision): { label: string } {
|
|
1654
|
+
const materializations = revision.components.map((component) => component.materialization);
|
|
1655
|
+
const placementBound = materializations.find(
|
|
1656
|
+
(materialization) => materialization.portability === "placement_bound",
|
|
1657
|
+
);
|
|
1658
|
+
if (placementBound) {
|
|
1659
|
+
return {
|
|
1660
|
+
label: placementBound.reason
|
|
1661
|
+
? `This machine only · ${placementBound.reason}`
|
|
1662
|
+
: "This machine only",
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
const providerBound = materializations.find(
|
|
1666
|
+
(materialization) => materialization.portability === "provider_bound",
|
|
1667
|
+
);
|
|
1668
|
+
if (providerBound) {
|
|
1669
|
+
return {
|
|
1670
|
+
label: providerBound.reason ? `Provider-bound · ${providerBound.reason}` : "Provider-bound",
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
return { label: "Portable browser data" };
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function siteAuthHealth(connection: SiteAuthConnection): {
|
|
1677
|
+
label: string;
|
|
1678
|
+
dotClass: string;
|
|
1679
|
+
} {
|
|
1680
|
+
switch (connection.verificationState) {
|
|
1681
|
+
case "verified":
|
|
1682
|
+
return { label: "Verified", dotClass: "bg-og-status-running" };
|
|
1683
|
+
case "needs_repair":
|
|
1684
|
+
return { label: "Sign-in needs attention", dotClass: "bg-og-status-waiting" };
|
|
1685
|
+
case "failed":
|
|
1686
|
+
return { label: "Verification failed", dotClass: "bg-og-status-error" };
|
|
1687
|
+
case "unknown":
|
|
1688
|
+
return { label: "Not checked yet", dotClass: "bg-og-muted" };
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1430
1692
|
function MenuButton(props: { children: ReactNode; onClick: () => void; disabled?: boolean }) {
|
|
1431
1693
|
return (
|
|
1432
1694
|
<button
|
|
@@ -1567,6 +1829,8 @@ function BrowserViewport(props: {
|
|
|
1567
1829
|
onAction: (action: BrowserAction, frame: BrowserFrame | null) => Promise<BrowserActionReceipt>;
|
|
1568
1830
|
onReadClipboard: () => Promise<{ text: string }>;
|
|
1569
1831
|
onReconnect: () => void;
|
|
1832
|
+
reconnectLabel?: string | undefined;
|
|
1833
|
+
reconnectMessage?: string | undefined;
|
|
1570
1834
|
onError: (cause: unknown) => void;
|
|
1571
1835
|
}) {
|
|
1572
1836
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
@@ -1596,6 +1860,7 @@ function BrowserViewport(props: {
|
|
|
1596
1860
|
const readClipboardRef = useRef(props.onReadClipboard);
|
|
1597
1861
|
const errorRef = useRef(props.onError);
|
|
1598
1862
|
const actionTailRef = useRef<Promise<void>>(Promise.resolve());
|
|
1863
|
+
const actionQueueEpochRef = useRef(0);
|
|
1599
1864
|
const queuedFrameRef = useRef<BrowserFrame | null>(null);
|
|
1600
1865
|
const decodingFrameRef = useRef(false);
|
|
1601
1866
|
const mountedRef = useRef(true);
|
|
@@ -1677,13 +1942,19 @@ function BrowserViewport(props: {
|
|
|
1677
1942
|
frame: BrowserFrame | null,
|
|
1678
1943
|
after?: (receipt: BrowserActionReceipt) => Promise<void>,
|
|
1679
1944
|
) => {
|
|
1945
|
+
const epoch = actionQueueEpochRef.current;
|
|
1680
1946
|
actionTailRef.current = actionTailRef.current
|
|
1681
1947
|
.catch(() => undefined)
|
|
1682
1948
|
.then(async () => {
|
|
1949
|
+
if (epoch !== actionQueueEpochRef.current) return;
|
|
1683
1950
|
const receipt = await actionRef.current(action, frame);
|
|
1684
1951
|
await after?.(receipt);
|
|
1685
1952
|
})
|
|
1686
|
-
.catch((cause) =>
|
|
1953
|
+
.catch((cause) => {
|
|
1954
|
+
// Do not replay or continue input collected behind a failed request.
|
|
1955
|
+
actionQueueEpochRef.current += 1;
|
|
1956
|
+
errorRef.current(cause);
|
|
1957
|
+
});
|
|
1687
1958
|
},
|
|
1688
1959
|
[],
|
|
1689
1960
|
);
|
|
@@ -1930,6 +2201,8 @@ function BrowserViewport(props: {
|
|
|
1930
2201
|
error={props.connectionError}
|
|
1931
2202
|
onAction={(action) => enqueue(action, null)}
|
|
1932
2203
|
onReconnect={props.onReconnect}
|
|
2204
|
+
reconnectLabel={props.reconnectLabel}
|
|
2205
|
+
reconnectMessage={props.reconnectMessage}
|
|
1933
2206
|
/>
|
|
1934
2207
|
) : null}
|
|
1935
2208
|
{props.mutating ? (
|
|
@@ -1948,7 +2221,12 @@ function SemanticBrowserFallback(props: {
|
|
|
1948
2221
|
error: Error | null;
|
|
1949
2222
|
onAction: (action: BrowserAction) => void;
|
|
1950
2223
|
onReconnect: () => void;
|
|
2224
|
+
reconnectLabel?: string | undefined;
|
|
2225
|
+
reconnectMessage?: string | undefined;
|
|
1951
2226
|
}) {
|
|
2227
|
+
const controlFailure = interactionControlFailureFromError(props.error);
|
|
2228
|
+
const generationLoss =
|
|
2229
|
+
Boolean(props.reconnectMessage) || isAttachedChromeGenerationLossError(props.error);
|
|
1952
2230
|
const nodes = semanticNodes(
|
|
1953
2231
|
props.observation?.semantic?.kind === "snapshot" ? props.observation.semantic.roots : [],
|
|
1954
2232
|
);
|
|
@@ -1965,13 +2243,20 @@ function SemanticBrowserFallback(props: {
|
|
|
1965
2243
|
<LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
|
|
1966
2244
|
)}
|
|
1967
2245
|
<p className="text-og-menu font-medium text-og-fg">
|
|
1968
|
-
{
|
|
1969
|
-
? "
|
|
1970
|
-
: props.
|
|
1971
|
-
?
|
|
1972
|
-
:
|
|
2246
|
+
{generationLoss
|
|
2247
|
+
? "Chrome reconnected—open a fresh browser/desktop."
|
|
2248
|
+
: props.error
|
|
2249
|
+
? "Live view disconnected"
|
|
2250
|
+
: props.supportsLiveFrames
|
|
2251
|
+
? browserConnectionLabel(props.connectionState)
|
|
2252
|
+
: "Semantic browser"}
|
|
1973
2253
|
</p>
|
|
1974
2254
|
</div>
|
|
2255
|
+
{props.error || props.reconnectMessage ? (
|
|
2256
|
+
<p className="mt-2 text-og-control leading-5 text-og-muted">
|
|
2257
|
+
{props.reconnectMessage ?? controlFailure?.message ?? props.error?.message}
|
|
2258
|
+
</p>
|
|
2259
|
+
) : null}
|
|
1975
2260
|
{interactive.length > 0 ? (
|
|
1976
2261
|
<div className="mt-3 border-t border-og-border pt-3">
|
|
1977
2262
|
<p className="mb-2 text-og-xs text-og-subtle">
|
|
@@ -1998,13 +2283,14 @@ function SemanticBrowserFallback(props: {
|
|
|
1998
2283
|
</div>
|
|
1999
2284
|
</div>
|
|
2000
2285
|
) : null}
|
|
2001
|
-
{props.error && props.supportsLiveFrames ? (
|
|
2286
|
+
{props.error && props.supportsLiveFrames && (!generationLoss || props.reconnectLabel) ? (
|
|
2002
2287
|
<button
|
|
2003
2288
|
type="button"
|
|
2004
2289
|
onClick={props.onReconnect}
|
|
2005
2290
|
className="mt-3 text-og-control font-medium text-og-accent hover:underline"
|
|
2006
2291
|
>
|
|
2007
|
-
|
|
2292
|
+
{props.reconnectLabel ??
|
|
2293
|
+
(controlFailure?.retryable === false ? "Try again" : "Reconnect")}
|
|
2008
2294
|
</button>
|
|
2009
2295
|
) : null}
|
|
2010
2296
|
</div>
|
|
@@ -2052,10 +2338,10 @@ function BrowserStatusBar(props: {
|
|
|
2052
2338
|
type="button"
|
|
2053
2339
|
onClick={props.onOpenComputer}
|
|
2054
2340
|
className="flex items-center gap-1 rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg"
|
|
2055
|
-
aria-label="Open this browser window in
|
|
2341
|
+
aria-label="Open this browser window in Desktop"
|
|
2056
2342
|
>
|
|
2057
2343
|
<MonitorIcon className="size-3" />
|
|
2058
|
-
|
|
2344
|
+
Desktop
|
|
2059
2345
|
</button>
|
|
2060
2346
|
) : null}
|
|
2061
2347
|
<button
|
|
@@ -2638,6 +2924,23 @@ function isLiveBrowser(session: BrowserSession): boolean {
|
|
|
2638
2924
|
return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
|
|
2639
2925
|
}
|
|
2640
2926
|
|
|
2927
|
+
function attachedChromeGenerationLoss(
|
|
2928
|
+
sessions: readonly BrowserSession[],
|
|
2929
|
+
): { deviceId: string } | null {
|
|
2930
|
+
const lost = sessions.find(
|
|
2931
|
+
(session) =>
|
|
2932
|
+
session.lifecycle === "lost" &&
|
|
2933
|
+
session.failureCode === "controller_transition_expired" &&
|
|
2934
|
+
session.placement.kind === "attached_device",
|
|
2935
|
+
);
|
|
2936
|
+
return lost?.placement.kind === "attached_device" ? { deviceId: lost.placement.deviceId } : null;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
function isAttachedChromeGenerationLossError(error: Error | null): boolean {
|
|
2940
|
+
if (!error) return false;
|
|
2941
|
+
return /placement instance changed|controller authority changed/i.test(error.message);
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2641
2944
|
function countInterventions(
|
|
2642
2945
|
interventions: readonly InteractionIntervention[],
|
|
2643
2946
|
): Map<string, number> {
|