@opengeni/react 7.1.0 → 7.1.2
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/dist/artifacts-document.js +2 -2
- package/dist/artifacts-presentation.js +2 -2
- package/dist/artifacts-spreadsheet.js +2 -2
- package/dist/artifacts.js +4 -4
- package/dist/{browser-viewer-MN7MOYQT.js → browser-viewer-TZ6T76E3.js} +2 -2
- package/dist/{chunk-KCGXPP5V.js → chunk-6P7T4RPE.js} +7 -3
- package/dist/chunk-6P7T4RPE.js.map +1 -0
- package/dist/{chunk-SHRCATV6.js → chunk-7J4DUR3E.js} +12 -5
- package/dist/chunk-7J4DUR3E.js.map +1 -0
- package/dist/{chunk-UQS7BGNQ.js → chunk-CP6IFD2Z.js} +79 -5
- package/dist/chunk-CP6IFD2Z.js.map +1 -0
- package/dist/{chunk-VR4Z73EU.js → chunk-G2SCXNXW.js} +9 -2
- package/dist/chunk-G2SCXNXW.js.map +1 -0
- package/dist/{chunk-5UONOQ3Y.js → chunk-K3MQZO7R.js} +150 -67
- package/dist/chunk-K3MQZO7R.js.map +1 -0
- package/dist/{chunk-OWHPKAOV.js → chunk-L6MYP3QU.js} +4 -2
- package/dist/{chunk-OWHPKAOV.js.map → chunk-L6MYP3QU.js.map} +1 -1
- package/dist/{chunk-ZC2DK5SC.js → chunk-VHEY4CB3.js} +84 -5
- package/dist/chunk-VHEY4CB3.js.map +1 -0
- package/dist/{chunk-XMXRI5JK.js → chunk-ZZ4744OZ.js} +128 -71
- package/dist/chunk-ZZ4744OZ.js.map +1 -0
- package/dist/components/artifacts/artifact-surface.d.ts +3 -1
- package/dist/components/artifacts/document-editor.d.ts +4 -2
- package/dist/components/artifacts/editable-document.d.ts +1 -1
- package/dist/components/artifacts/editable-presentation.d.ts +1 -1
- package/dist/components/artifacts/editable-spreadsheet.d.ts +2 -1
- package/dist/components/artifacts/presentation-editor.d.ts +4 -2
- package/dist/components/browser-viewer.d.ts +4 -1
- package/dist/components/markdown.d.ts +2 -0
- package/dist/components/session-capability-frame.d.ts +5 -1
- package/dist/connect.js +2 -2
- package/dist/hooks/use-browser-session.d.ts +3 -0
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +1 -1
- package/dist/session-ui.js +2 -2
- package/package.json +2 -2
- package/src/components/artifacts/artifact-surface.tsx +39 -19
- package/src/components/artifacts/document-editor.tsx +77 -9
- package/src/components/artifacts/editable-document.tsx +3 -0
- package/src/components/artifacts/editable-presentation.tsx +3 -0
- package/src/components/artifacts/editable-spreadsheet.tsx +3 -0
- package/src/components/artifacts/presentation-editor.tsx +6 -0
- package/src/components/browser-viewer.tsx +10 -0
- package/src/components/markdown.tsx +9 -2
- package/src/components/sandbox-workspace.tsx +1 -0
- package/src/components/session-capability-frame.tsx +11 -3
- package/src/components/session-mcp-capability-card.tsx +174 -70
- package/src/hooks/use-browser-session.ts +107 -3
- package/src/timeline/parsers.ts +2 -2
- package/src/timeline/tool-renderers.tsx +105 -16
- package/src/timeline/turn-summary.tsx +10 -2
- package/styles/compiled.css +6 -0
- package/styles/session-capability.css +2 -0
- package/dist/chunk-5UONOQ3Y.js.map +0 -1
- package/dist/chunk-KCGXPP5V.js.map +0 -1
- package/dist/chunk-SHRCATV6.js.map +0 -1
- package/dist/chunk-UQS7BGNQ.js.map +0 -1
- package/dist/chunk-VR4Z73EU.js.map +0 -1
- package/dist/chunk-XMXRI5JK.js.map +0 -1
- package/dist/chunk-ZC2DK5SC.js.map +0 -1
- /package/dist/{browser-viewer-MN7MOYQT.js.map → browser-viewer-TZ6T76E3.js.map} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
authorizeConnectAttempt,
|
|
3
|
+
ConnectPopupClosedError,
|
|
3
4
|
ConnectController,
|
|
4
5
|
createBrowserConnectNavigation,
|
|
5
6
|
reserveBrowserConnectNavigation,
|
|
@@ -77,25 +78,32 @@ function ScopedCard({
|
|
|
77
78
|
const [complete, setComplete] = useState(false);
|
|
78
79
|
const [connected, setConnected] = useState(false);
|
|
79
80
|
const [waiting, setWaiting] = useState(false);
|
|
81
|
+
const [reconciling, setReconciling] = useState(false);
|
|
80
82
|
const [busy, setBusy] = useState(false);
|
|
81
83
|
const [error, setError] = useState<string | null>(null);
|
|
84
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
85
|
+
const [retryFresh, setRetryFresh] = useState(false);
|
|
82
86
|
const [ownership, setOwnership] = useState<ConnectOwnership>("workspace");
|
|
83
87
|
const lifetime = useRef<AbortController | null>(null);
|
|
84
|
-
const
|
|
88
|
+
const authorization = useRef<AbortController | null>(null);
|
|
89
|
+
const operation = useRef<AbortController | null>(null);
|
|
90
|
+
const reservedPopup = useRef<(() => void) | null>(null);
|
|
85
91
|
const startKey = useRef(crypto.randomUUID());
|
|
86
92
|
const advanceKey = useRef(crypto.randomUUID());
|
|
93
|
+
const startInput = useRef<string | null>(null);
|
|
87
94
|
const storageKey = sessionId
|
|
88
95
|
? `opengeni:session-connect:${workspaceId}:${sessionId}:${capabilityId}`
|
|
89
96
|
: `opengeni:workspace-connect:${workspaceId}:${capabilityId}`;
|
|
90
97
|
const current = () => !!lifetime.current && !lifetime.current.signal.aborted;
|
|
91
98
|
|
|
92
|
-
async function load() {
|
|
99
|
+
async function load(active: () => boolean = current) {
|
|
93
100
|
const invocation = lifetime.current;
|
|
94
101
|
const [catalog, session] = await Promise.all([
|
|
95
102
|
client.listCapabilities(workspaceId),
|
|
96
103
|
sessionId ? client.getSession(workspaceId, sessionId) : Promise.resolve(null),
|
|
97
104
|
]);
|
|
98
|
-
if (!invocation || invocation.signal.aborted || lifetime.current !== invocation
|
|
105
|
+
if (!invocation || invocation.signal.aborted || lifetime.current !== invocation || !active())
|
|
106
|
+
return null;
|
|
99
107
|
const resolved = catalog.items.find((entry) => entry.id === capabilityId);
|
|
100
108
|
if (!resolved || resolved.kind !== "mcp" || resolved.authKind !== "oauth2")
|
|
101
109
|
throw new Error(
|
|
@@ -112,11 +120,11 @@ function ScopedCard({
|
|
|
112
120
|
resolved.connectionRef.subjectScope === "subject"
|
|
113
121
|
? await client.listOwnConnectionAccounts(workspaceId)
|
|
114
122
|
: await client.listConnections(workspaceId);
|
|
115
|
-
if (invocation.signal.aborted || lifetime.current !== invocation) return null;
|
|
123
|
+
if (invocation.signal.aborted || lifetime.current !== invocation || !active()) return null;
|
|
116
124
|
const matches = matchingActiveMcpConnections(resolved, connections);
|
|
117
125
|
accountReady = matches.length === 1;
|
|
118
126
|
}
|
|
119
|
-
if (invocation.signal.aborted || lifetime.current !== invocation) return null;
|
|
127
|
+
if (invocation.signal.aborted || lifetime.current !== invocation || !active()) return null;
|
|
120
128
|
const selected =
|
|
121
129
|
session?.toolPolicy.mode === "workspace_default"
|
|
122
130
|
? session.effectiveToolPolicy?.selectedIds
|
|
@@ -185,23 +193,32 @@ function ScopedCard({
|
|
|
185
193
|
};
|
|
186
194
|
}, [client, item?.logoAssetPath]);
|
|
187
195
|
|
|
188
|
-
async function run(action: () => Promise<void>) {
|
|
196
|
+
async function run(action: (active: () => boolean) => Promise<void>) {
|
|
189
197
|
if (operation.current || !current()) return;
|
|
190
|
-
|
|
198
|
+
const pending = new AbortController();
|
|
199
|
+
operation.current = pending;
|
|
200
|
+
const active = () => current() && operation.current === pending && !pending.signal.aborted;
|
|
191
201
|
setBusy(true);
|
|
192
202
|
setError(null);
|
|
203
|
+
setNotice(null);
|
|
204
|
+
setRetryFresh(false);
|
|
193
205
|
try {
|
|
194
|
-
await action();
|
|
206
|
+
await action(active);
|
|
195
207
|
} catch (failure) {
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
if (active()) {
|
|
209
|
+
if (failure instanceof ConnectPopupClosedError) setNotice(failure.message);
|
|
210
|
+
else
|
|
211
|
+
setError(
|
|
212
|
+
failure instanceof Error
|
|
213
|
+
? failure.message
|
|
214
|
+
: "Connection setup could not finish. Try again.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
202
217
|
} finally {
|
|
203
|
-
operation.current
|
|
204
|
-
|
|
218
|
+
if (operation.current === pending) {
|
|
219
|
+
operation.current = null;
|
|
220
|
+
if (current()) setBusy(false);
|
|
221
|
+
}
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
|
|
@@ -215,15 +232,15 @@ function ScopedCard({
|
|
|
215
232
|
}
|
|
216
233
|
}
|
|
217
234
|
|
|
218
|
-
async function reconcile(attempt: ConnectAttempt) {
|
|
219
|
-
if (!
|
|
235
|
+
async function reconcile(attempt: ConnectAttempt, active: () => boolean = current) {
|
|
236
|
+
if (!active()) return;
|
|
220
237
|
if (attempt.state !== "complete" || !attempt.account || !attempt.credentialsCommitted) return;
|
|
221
|
-
const resolved = await load();
|
|
222
|
-
if (!resolved || !
|
|
238
|
+
const resolved = await load(active);
|
|
239
|
+
if (!resolved || !active()) return;
|
|
223
240
|
const connection = (await client.listConnections(workspaceId)).find(
|
|
224
241
|
(entry) => entry.id === attempt.account!.id,
|
|
225
242
|
);
|
|
226
|
-
if (!
|
|
243
|
+
if (!active()) return;
|
|
227
244
|
if (
|
|
228
245
|
!connection ||
|
|
229
246
|
connection.status !== "active" ||
|
|
@@ -255,11 +272,12 @@ function ScopedCard({
|
|
|
255
272
|
connectionId: connection.id,
|
|
256
273
|
},
|
|
257
274
|
});
|
|
258
|
-
if (!
|
|
259
|
-
const enabled = await load();
|
|
260
|
-
if (enabled &&
|
|
261
|
-
await finishConnection(enabled);
|
|
275
|
+
if (!active()) return;
|
|
276
|
+
const enabled = await load(active);
|
|
277
|
+
if (enabled && active()) {
|
|
278
|
+
await finishConnection(enabled, active);
|
|
262
279
|
}
|
|
280
|
+
if (!active()) return;
|
|
263
281
|
try {
|
|
264
282
|
sessionStorage.removeItem(storageKey);
|
|
265
283
|
} catch {
|
|
@@ -269,14 +287,14 @@ function ScopedCard({
|
|
|
269
287
|
|
|
270
288
|
async function open() {
|
|
271
289
|
setExpanded(true);
|
|
272
|
-
await run(async () => {
|
|
273
|
-
await load();
|
|
274
|
-
if (!
|
|
275
|
-
await recoverSavedAttempt();
|
|
290
|
+
await run(async (active) => {
|
|
291
|
+
await load(active);
|
|
292
|
+
if (!active()) return;
|
|
293
|
+
await recoverSavedAttempt(active);
|
|
276
294
|
});
|
|
277
295
|
}
|
|
278
296
|
|
|
279
|
-
async function recoverSavedAttempt() {
|
|
297
|
+
async function recoverSavedAttempt(active: () => boolean = current) {
|
|
280
298
|
let saved: string | null = null;
|
|
281
299
|
try {
|
|
282
300
|
saved = sessionStorage.getItem(storageKey);
|
|
@@ -285,20 +303,31 @@ function ScopedCard({
|
|
|
285
303
|
}
|
|
286
304
|
if (saved) {
|
|
287
305
|
const attempt = await controller.recover(saved);
|
|
288
|
-
|
|
306
|
+
if (!active()) return;
|
|
307
|
+
if (["failed", "expired", "cancelled"].includes(attempt.state)) setRetryFresh(true);
|
|
308
|
+
if (attempt.state === "complete") setReconciling(true);
|
|
309
|
+
try {
|
|
310
|
+
await reconcile(attempt, active);
|
|
311
|
+
} finally {
|
|
312
|
+
if (active()) setReconciling(false);
|
|
313
|
+
}
|
|
289
314
|
}
|
|
290
315
|
}
|
|
291
316
|
|
|
292
|
-
async function begin() {
|
|
293
|
-
await run(async () => {
|
|
294
|
-
if (
|
|
317
|
+
async function begin(fresh = false) {
|
|
318
|
+
await run(async (active) => {
|
|
319
|
+
if (
|
|
320
|
+
fresh ||
|
|
321
|
+
(view.attempt && ["failed", "expired", "cancelled"].includes(view.attempt.state))
|
|
322
|
+
) {
|
|
295
323
|
startKey.current = crypto.randomUUID();
|
|
296
324
|
advanceKey.current = crypto.randomUUID();
|
|
297
325
|
}
|
|
298
326
|
const reserved = reserveBrowserConnectNavigation(window);
|
|
327
|
+
reservedPopup.current = reserved.close;
|
|
299
328
|
try {
|
|
300
|
-
const resolved = await load();
|
|
301
|
-
if (!resolved || !
|
|
329
|
+
const resolved = await load(active);
|
|
330
|
+
if (!resolved || !active()) return;
|
|
302
331
|
if (
|
|
303
332
|
resolved.connectionRef &&
|
|
304
333
|
ownership !==
|
|
@@ -322,13 +351,22 @@ function ScopedCard({
|
|
|
322
351
|
entry.kind === "oauth2" &&
|
|
323
352
|
entry.metadata.mcpUrl === mcpUrl,
|
|
324
353
|
);
|
|
325
|
-
if (!
|
|
354
|
+
if (!active()) return;
|
|
326
355
|
if (connections.length > 1)
|
|
327
356
|
throw new Error(
|
|
328
357
|
"More than one personal account matches. Choose the account in connection settings before reconnecting.",
|
|
329
358
|
);
|
|
330
359
|
reconnectAccountId = connections[0]?.id;
|
|
331
360
|
}
|
|
361
|
+
// A closed request can still have reached the server. Keep its key for
|
|
362
|
+
// an exact replay, but never reuse it when ownership or another input
|
|
363
|
+
// changes while the user reopens setup.
|
|
364
|
+
const input = JSON.stringify({ ownership, returnUrl, reconnectAccountId, mcpUrl });
|
|
365
|
+
if (startInput.current !== null && startInput.current !== input) {
|
|
366
|
+
startKey.current = crypto.randomUUID();
|
|
367
|
+
advanceKey.current = crypto.randomUUID();
|
|
368
|
+
}
|
|
369
|
+
startInput.current = input;
|
|
332
370
|
let attempt = await controller.begin({
|
|
333
371
|
providerId: "mcp-oauth",
|
|
334
372
|
ownership,
|
|
@@ -336,49 +374,79 @@ function ScopedCard({
|
|
|
336
374
|
idempotencyKey: startKey.current,
|
|
337
375
|
...(reconnectAccountId ? { reconnectAccountId } : {}),
|
|
338
376
|
});
|
|
339
|
-
if (!
|
|
377
|
+
if (!active()) return;
|
|
340
378
|
remember(attempt.id);
|
|
341
379
|
if (attempt.state === "credential_input")
|
|
342
380
|
attempt = await controller.advance(
|
|
343
381
|
{ type: "credentials", values: { mcpUrl } },
|
|
344
382
|
advanceKey.current,
|
|
345
383
|
);
|
|
384
|
+
if (!active()) return;
|
|
346
385
|
if (attempt.nextAction.type === "authorize")
|
|
347
|
-
await performAuthorization(attempt, reserved.navigation);
|
|
348
|
-
else await reconcile(attempt);
|
|
386
|
+
await performAuthorization(attempt, reserved.navigation, active);
|
|
387
|
+
else await reconcile(attempt, active);
|
|
349
388
|
} finally {
|
|
389
|
+
if (reservedPopup.current === reserved.close) reservedPopup.current = null;
|
|
350
390
|
reserved.close();
|
|
351
391
|
}
|
|
352
392
|
});
|
|
353
393
|
}
|
|
354
394
|
|
|
355
|
-
async function performAuthorization(
|
|
395
|
+
async function performAuthorization(
|
|
396
|
+
attempt: ConnectAttempt,
|
|
397
|
+
navigation: ConnectNavigation,
|
|
398
|
+
active: () => boolean,
|
|
399
|
+
) {
|
|
400
|
+
if (!active()) return;
|
|
401
|
+
const pending = new AbortController();
|
|
402
|
+
authorization.current = pending;
|
|
403
|
+
const abortOnUnmount = () => pending.abort(lifetime.current?.signal.reason);
|
|
404
|
+
lifetime.current!.signal.addEventListener("abort", abortOnUnmount, { once: true });
|
|
356
405
|
setWaiting(true);
|
|
357
406
|
try {
|
|
358
407
|
const result = await authorizeConnectAttempt(controller.transport, attempt, navigation, {
|
|
359
408
|
mode: "popup",
|
|
360
|
-
signal:
|
|
409
|
+
signal: pending.signal,
|
|
361
410
|
});
|
|
362
|
-
if (!
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
411
|
+
if (!active() || !result) return;
|
|
412
|
+
setWaiting(false);
|
|
413
|
+
setReconciling(true);
|
|
414
|
+
await reconcile(result, active);
|
|
415
|
+
if (!active()) return;
|
|
416
|
+
if (result.state === "cancelled") {
|
|
417
|
+
setNotice("Sign-in was cancelled. You can try connecting again.");
|
|
418
|
+
setRetryFresh(true);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (result.state !== "complete") {
|
|
422
|
+
if (["failed", "expired"].includes(result.state)) setRetryFresh(true);
|
|
366
423
|
throw new Error("Sign-in did not finish. You can try connecting again.");
|
|
424
|
+
}
|
|
367
425
|
} finally {
|
|
368
|
-
|
|
426
|
+
lifetime.current?.signal.removeEventListener("abort", abortOnUnmount);
|
|
427
|
+
if (authorization.current === pending) authorization.current = null;
|
|
428
|
+
if (active()) {
|
|
429
|
+
setReconciling(false);
|
|
430
|
+
setWaiting(false);
|
|
431
|
+
}
|
|
369
432
|
}
|
|
370
433
|
}
|
|
371
434
|
|
|
372
435
|
function authorize(attempt: ConnectAttempt) {
|
|
373
436
|
// run invokes action synchronously, retaining the browser click gesture.
|
|
374
|
-
return run(() =>
|
|
437
|
+
return run((active) =>
|
|
438
|
+
performAuthorization(attempt, createBrowserConnectNavigation(window), active),
|
|
439
|
+
);
|
|
375
440
|
}
|
|
376
441
|
|
|
377
|
-
async function finishConnection(
|
|
442
|
+
async function finishConnection(
|
|
443
|
+
capability: CapabilityCatalogItem,
|
|
444
|
+
active: () => boolean = current,
|
|
445
|
+
) {
|
|
378
446
|
if (!sessionId) {
|
|
379
447
|
// Connection management does not select session tools.
|
|
380
448
|
await onConfigured?.();
|
|
381
|
-
if (!
|
|
449
|
+
if (!active()) return;
|
|
382
450
|
setComplete(true);
|
|
383
451
|
setExpanded(false);
|
|
384
452
|
onClose?.();
|
|
@@ -388,31 +456,47 @@ function ScopedCard({
|
|
|
388
456
|
const selected = (await client.listConnections(workspaceId)).find(
|
|
389
457
|
(entry) => entry.id === capability.connectionRef?.connectionId,
|
|
390
458
|
);
|
|
391
|
-
if (!
|
|
459
|
+
if (!active()) return;
|
|
392
460
|
if (!selected || selected.status !== "active")
|
|
393
461
|
throw new Error("This account needs reconnection before it can be used here.");
|
|
394
462
|
}
|
|
395
463
|
if (capability.connectionRef?.subjectScope === "subject") {
|
|
396
464
|
const accounts = await client.listOwnConnectionAccounts(workspaceId);
|
|
397
|
-
if (!
|
|
465
|
+
if (!active()) return;
|
|
398
466
|
if (matchingActiveMcpConnections(capability, accounts).length === 0)
|
|
399
467
|
throw new Error("Your account needs reconnection before it can be used here.");
|
|
400
468
|
}
|
|
401
|
-
if (!
|
|
402
|
-
await attachSessionCapability(client, workspaceId, sessionId, capability,
|
|
403
|
-
if (!
|
|
469
|
+
if (!active()) return;
|
|
470
|
+
await attachSessionCapability(client, workspaceId, sessionId, capability, active);
|
|
471
|
+
if (!active()) return;
|
|
404
472
|
await onConfigured?.();
|
|
405
|
-
if (!
|
|
473
|
+
if (!active()) return;
|
|
406
474
|
setComplete(true);
|
|
407
475
|
setExpanded(false);
|
|
408
476
|
}
|
|
409
477
|
|
|
410
478
|
async function useHere() {
|
|
411
|
-
await run(async () => {
|
|
412
|
-
if (item) await finishConnection(item);
|
|
479
|
+
await run(async (active) => {
|
|
480
|
+
if (item) await finishConnection(item, active);
|
|
413
481
|
});
|
|
414
482
|
}
|
|
415
483
|
|
|
484
|
+
function close() {
|
|
485
|
+
const pending = operation.current;
|
|
486
|
+
if (pending) {
|
|
487
|
+
operation.current = null;
|
|
488
|
+
pending.abort(new ConnectPopupClosedError());
|
|
489
|
+
authorization.current?.abort(new ConnectPopupClosedError());
|
|
490
|
+
authorization.current = null;
|
|
491
|
+
reservedPopup.current?.();
|
|
492
|
+
setBusy(false);
|
|
493
|
+
setWaiting(false);
|
|
494
|
+
setReconciling(false);
|
|
495
|
+
}
|
|
496
|
+
setExpanded(false);
|
|
497
|
+
onClose?.();
|
|
498
|
+
}
|
|
499
|
+
|
|
416
500
|
return (
|
|
417
501
|
<SessionCapabilityFrame
|
|
418
502
|
name={item?.name ?? name}
|
|
@@ -436,15 +520,16 @@ function ScopedCard({
|
|
|
436
520
|
: "Review access before signing in. You'll return here after authorization."
|
|
437
521
|
}
|
|
438
522
|
onOpen={() => void open()}
|
|
439
|
-
onClose={
|
|
440
|
-
setExpanded(false);
|
|
441
|
-
onClose?.();
|
|
442
|
-
}}
|
|
443
|
-
busy={busy || view.busy}
|
|
523
|
+
onClose={close}
|
|
444
524
|
dialogOnly={dialogOnly}
|
|
445
525
|
>
|
|
446
526
|
<div className="og-session-capability-setup">
|
|
447
527
|
{error ? <p role="alert">{error}</p> : null}
|
|
528
|
+
{notice ? (
|
|
529
|
+
<p role="status" className="og-session-capability-notice">
|
|
530
|
+
{notice}
|
|
531
|
+
</p>
|
|
532
|
+
) : null}
|
|
448
533
|
{!item ? (
|
|
449
534
|
<>
|
|
450
535
|
<p role="status">
|
|
@@ -458,11 +543,24 @@ function ScopedCard({
|
|
|
458
543
|
<>
|
|
459
544
|
<p>{item.description || rationale}</p>
|
|
460
545
|
{busy ? (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
546
|
+
<>
|
|
547
|
+
<p role="status" className="og-session-capability-progress">
|
|
548
|
+
{waiting
|
|
549
|
+
? `Finish signing in with ${item.name} in the opened window. This will close automatically when you’re connected.`
|
|
550
|
+
: reconciling
|
|
551
|
+
? "Finishing your connection…"
|
|
552
|
+
: "Preparing your connection…"}
|
|
553
|
+
</p>
|
|
554
|
+
{waiting ? (
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
className="og-session-capability-stop"
|
|
558
|
+
onClick={() => authorization.current?.abort(new ConnectPopupClosedError())}
|
|
559
|
+
>
|
|
560
|
+
Stop waiting
|
|
561
|
+
</button>
|
|
562
|
+
) : null}
|
|
563
|
+
</>
|
|
466
564
|
) : !connected ? (
|
|
467
565
|
<>
|
|
468
566
|
{!item.connectionRef ? (
|
|
@@ -498,11 +596,17 @@ function ScopedCard({
|
|
|
498
596
|
<button
|
|
499
597
|
className="og-session-capability-primary"
|
|
500
598
|
onClick={() => {
|
|
501
|
-
if (
|
|
599
|
+
if (notice || retryFresh) void begin(true);
|
|
600
|
+
else if (
|
|
601
|
+
view.attempt?.nextAction.type === "authorize" &&
|
|
602
|
+
view.attempt.ownership === ownership
|
|
603
|
+
)
|
|
604
|
+
void authorize(view.attempt);
|
|
502
605
|
else void begin();
|
|
503
606
|
}}
|
|
504
607
|
>
|
|
505
|
-
{
|
|
608
|
+
{retryFresh ||
|
|
609
|
+
((error || notice) && view.attempt?.nextAction.type === "authorize")
|
|
506
610
|
? "Try signing in again"
|
|
507
611
|
: `Continue to ${item.name}`}
|
|
508
612
|
</button>
|
|
@@ -23,6 +23,9 @@ export type UseBrowserSessionOptions = EmbeddedBrowserInteractionClientOverride
|
|
|
23
23
|
browserSessionId: string | null;
|
|
24
24
|
enabled?: boolean | undefined;
|
|
25
25
|
pollIntervalMs?: number | undefined;
|
|
26
|
+
/** Keep session/tab inventory current without repeatedly fetching the full
|
|
27
|
+
* accessibility tree while a live frame is available or the viewer is hidden. */
|
|
28
|
+
semanticObservationEnabled?: boolean | undefined;
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
export type UseBrowserSessionResult = {
|
|
@@ -58,6 +61,13 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
58
61
|
const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
|
|
59
62
|
const browserSessionId = options.browserSessionId;
|
|
60
63
|
const enabled = (options.enabled ?? true) && browserSessionId !== null;
|
|
64
|
+
const semanticObservationEnabled = options.semanticObservationEnabled ?? true;
|
|
65
|
+
const semanticObservationEnabledRef = useRef(semanticObservationEnabled);
|
|
66
|
+
semanticObservationEnabledRef.current = semanticObservationEnabled;
|
|
67
|
+
const previousSemanticObservationRef = useRef({
|
|
68
|
+
browserSessionId,
|
|
69
|
+
enabled: semanticObservationEnabled,
|
|
70
|
+
});
|
|
61
71
|
const pageLive = usePageLiveActivity();
|
|
62
72
|
const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2_000);
|
|
63
73
|
const [state, setState] = useState<{
|
|
@@ -75,10 +85,16 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
75
85
|
const refreshBlocked = isNonRetryableInteractionError(visible.error);
|
|
76
86
|
const selectedTargetIdRef = useRef<string | null>(visible.selectedTargetId);
|
|
77
87
|
selectedTargetIdRef.current = visible.selectedTargetId;
|
|
88
|
+
const selectedTargetRef = useRef<BrowserTarget | null>(
|
|
89
|
+
visible.targets.find((target) => target.id === visible.selectedTargetId) ?? null,
|
|
90
|
+
);
|
|
91
|
+
selectedTargetRef.current =
|
|
92
|
+
visible.targets.find((target) => target.id === visible.selectedTargetId) ?? null;
|
|
78
93
|
const observationRef = useRef<{
|
|
79
94
|
browserSessionId: string | null;
|
|
80
95
|
observation: BrowserObservation | null;
|
|
81
96
|
}>({ browserSessionId, observation: visible.observation });
|
|
97
|
+
const lastObservationAtRef = useRef(0);
|
|
82
98
|
if (observationRef.current.browserSessionId !== browserSessionId) {
|
|
83
99
|
observationRef.current = {
|
|
84
100
|
browserSessionId,
|
|
@@ -124,7 +140,15 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
124
140
|
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
125
141
|
const selected = chooseTarget(targetResponse.targets, selectedTargetIdRef.current);
|
|
126
142
|
selectedTargetIdRef.current = selected?.id ?? null;
|
|
127
|
-
|
|
143
|
+
const previousObservation =
|
|
144
|
+
observationRef.current.browserSessionId === browserSessionId
|
|
145
|
+
? observationRef.current.observation
|
|
146
|
+
: null;
|
|
147
|
+
const retainedObservation =
|
|
148
|
+
selected && previousObservation && sameObservationTarget(previousObservation, selected)
|
|
149
|
+
? previousObservation
|
|
150
|
+
: null;
|
|
151
|
+
observationRef.current = { browserSessionId, observation: retainedObservation };
|
|
128
152
|
setState((current) =>
|
|
129
153
|
current.browserSessionId === browserSessionId
|
|
130
154
|
? {
|
|
@@ -132,13 +156,13 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
132
156
|
session,
|
|
133
157
|
targets: targetResponse.targets,
|
|
134
158
|
selectedTargetId: selected?.id ?? null,
|
|
135
|
-
observation:
|
|
159
|
+
observation: retainedObservation,
|
|
136
160
|
loading: false,
|
|
137
161
|
error: null,
|
|
138
162
|
}
|
|
139
163
|
: current,
|
|
140
164
|
);
|
|
141
|
-
if (!selected) return;
|
|
165
|
+
if (!selected || !semanticObservationEnabledRef.current) return;
|
|
142
166
|
try {
|
|
143
167
|
const observation = await client.observeBrowserTarget(
|
|
144
168
|
workspaceId,
|
|
@@ -148,6 +172,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
148
172
|
);
|
|
149
173
|
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
150
174
|
observationRef.current = { browserSessionId, observation };
|
|
175
|
+
lastObservationAtRef.current = Date.now();
|
|
151
176
|
setState((current) =>
|
|
152
177
|
current.browserSessionId === browserSessionId &&
|
|
153
178
|
current.selectedTargetId === observation.target.id
|
|
@@ -180,6 +205,37 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
180
205
|
}
|
|
181
206
|
}, [browserSessionId, client, enabled, workspaceId]);
|
|
182
207
|
|
|
208
|
+
const refreshSemantic = useCallback(async (): Promise<void> => {
|
|
209
|
+
if (!enabled || !browserSessionId) return;
|
|
210
|
+
const target = selectedTargetRef.current;
|
|
211
|
+
if (!target) return;
|
|
212
|
+
const id = requestRef.current.id + 1;
|
|
213
|
+
requestRef.current.controller?.abort();
|
|
214
|
+
const controller = new AbortController();
|
|
215
|
+
requestRef.current = { id, controller };
|
|
216
|
+
try {
|
|
217
|
+
const observation = await client.observeBrowserTarget(
|
|
218
|
+
workspaceId,
|
|
219
|
+
browserSessionId,
|
|
220
|
+
target.id,
|
|
221
|
+
{ signal: controller.signal },
|
|
222
|
+
);
|
|
223
|
+
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
224
|
+
if (!sameObservationTarget(observation, selectedTargetRef.current)) return;
|
|
225
|
+
observationRef.current = { browserSessionId, observation };
|
|
226
|
+
lastObservationAtRef.current = Date.now();
|
|
227
|
+
setState((current) =>
|
|
228
|
+
current.browserSessionId === browserSessionId && current.selectedTargetId === target.id
|
|
229
|
+
? { ...current, observation }
|
|
230
|
+
: current,
|
|
231
|
+
);
|
|
232
|
+
} catch {
|
|
233
|
+
// A failed semantic fallback cannot disable an otherwise live browser.
|
|
234
|
+
} finally {
|
|
235
|
+
if (requestRef.current.id === id) requestRef.current = { id, controller: null };
|
|
236
|
+
}
|
|
237
|
+
}, [browserSessionId, client, enabled, workspaceId]);
|
|
238
|
+
|
|
183
239
|
useEffect(() => {
|
|
184
240
|
mountedRef.current = true;
|
|
185
241
|
if (!enabled) {
|
|
@@ -199,6 +255,37 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
199
255
|
};
|
|
200
256
|
}, [browserSessionId, enabled, invalidateRefresh, refresh]);
|
|
201
257
|
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
const previous = previousSemanticObservationRef.current;
|
|
260
|
+
previousSemanticObservationRef.current = {
|
|
261
|
+
browserSessionId,
|
|
262
|
+
enabled: semanticObservationEnabled,
|
|
263
|
+
};
|
|
264
|
+
if (
|
|
265
|
+
enabled &&
|
|
266
|
+
semanticObservationEnabled &&
|
|
267
|
+
previous.browserSessionId === browserSessionId &&
|
|
268
|
+
!previous.enabled
|
|
269
|
+
) {
|
|
270
|
+
// The media stream disappeared or the user returned to the Browser tab.
|
|
271
|
+
// A just-returned action/tab observation is already a fresh fallback.
|
|
272
|
+
// Otherwise drop the old tree/fence until a new read settles.
|
|
273
|
+
const currentObservation = observationRef.current.observation;
|
|
274
|
+
if (
|
|
275
|
+
currentObservation &&
|
|
276
|
+
sameObservationTarget(currentObservation, selectedTargetRef.current) &&
|
|
277
|
+
Date.now() - lastObservationAtRef.current < pollIntervalMs
|
|
278
|
+
) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
observationRef.current = { browserSessionId, observation: null };
|
|
282
|
+
setState((current) =>
|
|
283
|
+
current.browserSessionId === browserSessionId ? { ...current, observation: null } : current,
|
|
284
|
+
);
|
|
285
|
+
void refreshSemantic();
|
|
286
|
+
}
|
|
287
|
+
}, [browserSessionId, enabled, pollIntervalMs, refreshSemantic, semanticObservationEnabled]);
|
|
288
|
+
|
|
202
289
|
useEffect(() => {
|
|
203
290
|
if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
|
|
204
291
|
const timer = setInterval(() => {
|
|
@@ -301,6 +388,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
301
388
|
}
|
|
302
389
|
selectedTargetIdRef.current = target.id;
|
|
303
390
|
observationRef.current = { browserSessionId, observation };
|
|
391
|
+
lastObservationAtRef.current = Date.now();
|
|
304
392
|
setState((current) =>
|
|
305
393
|
current.browserSessionId === browserSessionId
|
|
306
394
|
? {
|
|
@@ -332,6 +420,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
332
420
|
const target = observation.target;
|
|
333
421
|
selectedTargetIdRef.current = target.id;
|
|
334
422
|
observationRef.current = { browserSessionId, observation };
|
|
423
|
+
lastObservationAtRef.current = Date.now();
|
|
335
424
|
setState((current) =>
|
|
336
425
|
current.browserSessionId === browserSessionId
|
|
337
426
|
? {
|
|
@@ -365,6 +454,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
365
454
|
: null;
|
|
366
455
|
selectedTargetIdRef.current = selected?.id ?? null;
|
|
367
456
|
observationRef.current = { browserSessionId, observation };
|
|
457
|
+
if (observation) lastObservationAtRef.current = Date.now();
|
|
368
458
|
setState((current) =>
|
|
369
459
|
current.browserSessionId === browserSessionId
|
|
370
460
|
? {
|
|
@@ -424,6 +514,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
424
514
|
browserSessionId,
|
|
425
515
|
observation: receipt.observation,
|
|
426
516
|
};
|
|
517
|
+
lastObservationAtRef.current = Date.now();
|
|
427
518
|
setState((current) =>
|
|
428
519
|
current.browserSessionId === browserSessionId
|
|
429
520
|
? {
|
|
@@ -540,6 +631,19 @@ function replaceTarget(targets: readonly BrowserTarget[], target: BrowserTarget)
|
|
|
540
631
|
return next.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
541
632
|
}
|
|
542
633
|
|
|
634
|
+
function sameObservationTarget(
|
|
635
|
+
observation: BrowserObservation,
|
|
636
|
+
target: BrowserTarget | null,
|
|
637
|
+
): boolean {
|
|
638
|
+
return (
|
|
639
|
+
target !== null &&
|
|
640
|
+
observation.target.id === target.id &&
|
|
641
|
+
observation.target.controllerGeneration === target.controllerGeneration &&
|
|
642
|
+
observation.target.targetGeneration === target.targetGeneration &&
|
|
643
|
+
observation.target.documentGeneration === target.documentGeneration
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
543
647
|
function isMissingBrowserTarget(error: unknown): boolean {
|
|
544
648
|
return error instanceof OpenGeniApiError && error.code === "target_not_found";
|
|
545
649
|
}
|
package/src/timeline/parsers.ts
CHANGED
|
@@ -566,8 +566,8 @@ export function retainedScreenshotMetadata(out: unknown): RetainedArtifactMetada
|
|
|
566
566
|
if (!value.available) {
|
|
567
567
|
return typeof value.reason === "string" ? (value as unknown as RetainedArtifactMetadata) : null;
|
|
568
568
|
}
|
|
569
|
-
return value.kind === "computer_screenshot" &&
|
|
570
|
-
|
|
569
|
+
return (value.kind === "computer_screenshot" || value.kind === "browser_screenshot") &&
|
|
570
|
+
["image/png", "image/jpeg", "image/webp"].includes(String(value.contentType)) &&
|
|
571
571
|
typeof value.originalBytes === "number" &&
|
|
572
572
|
typeof value.sha256 === "string" &&
|
|
573
573
|
value.dimensions !== null &&
|