@opengeni/react 7.1.0 → 7.1.2-canary.36079201993001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/artifacts-document.js +2 -2
  2. package/dist/artifacts-presentation.js +2 -2
  3. package/dist/artifacts-spreadsheet.js +2 -2
  4. package/dist/artifacts.js +4 -4
  5. package/dist/{browser-viewer-MN7MOYQT.js → browser-viewer-K2H3SLIW.js} +2 -2
  6. package/dist/{chunk-4ACTWOOW.js → chunk-6N3AZUOV.js} +31 -14
  7. package/dist/chunk-6N3AZUOV.js.map +1 -0
  8. package/dist/{chunk-KCGXPP5V.js → chunk-6P7T4RPE.js} +7 -3
  9. package/dist/chunk-6P7T4RPE.js.map +1 -0
  10. package/dist/{chunk-SHRCATV6.js → chunk-7J4DUR3E.js} +12 -5
  11. package/dist/chunk-7J4DUR3E.js.map +1 -0
  12. package/dist/{chunk-ZC2DK5SC.js → chunk-AWIX4UAC.js} +88 -7
  13. package/dist/chunk-AWIX4UAC.js.map +1 -0
  14. package/dist/{chunk-UQS7BGNQ.js → chunk-ETA37UZ7.js} +82 -11
  15. package/dist/chunk-ETA37UZ7.js.map +1 -0
  16. package/dist/{chunk-VR4Z73EU.js → chunk-G2SCXNXW.js} +9 -2
  17. package/dist/chunk-G2SCXNXW.js.map +1 -0
  18. package/dist/{chunk-5UONOQ3Y.js → chunk-K3MQZO7R.js} +150 -67
  19. package/dist/chunk-K3MQZO7R.js.map +1 -0
  20. package/dist/{chunk-OWHPKAOV.js → chunk-L6MYP3QU.js} +4 -2
  21. package/dist/{chunk-OWHPKAOV.js.map → chunk-L6MYP3QU.js.map} +1 -1
  22. package/dist/{chunk-VI7HELBI.js → chunk-SJ5HE4ZA.js} +21 -2
  23. package/dist/chunk-SJ5HE4ZA.js.map +1 -0
  24. package/dist/{chunk-XMXRI5JK.js → chunk-ZZ4744OZ.js} +128 -71
  25. package/dist/chunk-ZZ4744OZ.js.map +1 -0
  26. package/dist/components/artifacts/artifact-surface.d.ts +3 -1
  27. package/dist/components/artifacts/document-editor.d.ts +4 -2
  28. package/dist/components/artifacts/editable-document.d.ts +1 -1
  29. package/dist/components/artifacts/editable-presentation.d.ts +1 -1
  30. package/dist/components/artifacts/editable-spreadsheet.d.ts +2 -1
  31. package/dist/components/artifacts/presentation-editor.d.ts +4 -2
  32. package/dist/components/browser-viewer.d.ts +4 -1
  33. package/dist/components/markdown.d.ts +2 -0
  34. package/dist/components/session-capability-frame.d.ts +5 -1
  35. package/dist/composer.js +1 -1
  36. package/dist/connect.js +2 -2
  37. package/dist/hooks/use-browser-session.d.ts +3 -0
  38. package/dist/index.js +7 -6
  39. package/dist/index.js.map +1 -1
  40. package/dist/interaction.js +1 -1
  41. package/dist/session-ui.js +4 -4
  42. package/dist/session.js +1 -1
  43. package/dist/timeline/types.d.ts +2 -0
  44. package/package.json +3 -3
  45. package/src/components/artifacts/artifact-surface.tsx +39 -19
  46. package/src/components/artifacts/document-editor.tsx +77 -9
  47. package/src/components/artifacts/editable-document.tsx +3 -0
  48. package/src/components/artifacts/editable-presentation.tsx +3 -0
  49. package/src/components/artifacts/editable-spreadsheet.tsx +3 -0
  50. package/src/components/artifacts/presentation-editor.tsx +6 -0
  51. package/src/components/browser-viewer.tsx +11 -5
  52. package/src/components/composer-transcription-control.tsx +4 -2
  53. package/src/components/composer.tsx +20 -9
  54. package/src/components/markdown.tsx +9 -2
  55. package/src/components/sandbox-workspace.tsx +1 -0
  56. package/src/components/session-capability-frame.tsx +11 -3
  57. package/src/components/session-mcp-capability-card.tsx +174 -70
  58. package/src/hooks/use-browser-frame-stream.ts +3 -1
  59. package/src/hooks/use-browser-session.ts +107 -3
  60. package/src/hooks/use-voice-input.ts +12 -1
  61. package/src/timeline/compaction-copy.ts +2 -0
  62. package/src/timeline/parsers.ts +2 -2
  63. package/src/timeline/projection.ts +31 -5
  64. package/src/timeline/tool-renderers.tsx +105 -16
  65. package/src/timeline/turn-summary.tsx +10 -2
  66. package/src/timeline/types.ts +2 -0
  67. package/styles/compiled.css +6 -0
  68. package/styles/session-capability.css +2 -0
  69. package/dist/chunk-4ACTWOOW.js.map +0 -1
  70. package/dist/chunk-5UONOQ3Y.js.map +0 -1
  71. package/dist/chunk-KCGXPP5V.js.map +0 -1
  72. package/dist/chunk-SHRCATV6.js.map +0 -1
  73. package/dist/chunk-UQS7BGNQ.js.map +0 -1
  74. package/dist/chunk-VI7HELBI.js.map +0 -1
  75. package/dist/chunk-VR4Z73EU.js.map +0 -1
  76. package/dist/chunk-XMXRI5JK.js.map +0 -1
  77. package/dist/chunk-ZC2DK5SC.js.map +0 -1
  78. /package/dist/{browser-viewer-MN7MOYQT.js.map → browser-viewer-K2H3SLIW.js.map} +0 -0
@@ -53,6 +53,7 @@ const InteractiveContext = createContext<{
53
53
  streaming?: boolean | undefined;
54
54
  render?: (block: MarkdownInteractiveBlock) => ReactNode;
55
55
  renderImage?: (image: { src: string; alt: string }) => ReactNode;
56
+ suppressImages?: boolean;
56
57
  }>({ source: "" });
57
58
 
58
59
  export type MarkdownProps = {
@@ -61,6 +62,8 @@ export type MarkdownProps = {
61
62
  /** Reveals a bounded source excerpt; closing Find retains it until explicit restore. */
62
63
  searchTarget?: TimelineSearchTarget | null | undefined;
63
64
  renderImage?: ((image: { src: string; alt: string }) => ReactNode) | undefined;
65
+ /** Show alt text without loading image URLs (e.g. in search previews). */
66
+ suppressImages?: boolean | undefined;
64
67
  /** Host opt-in for assistant-authored interactive fences. */
65
68
  renderInteractiveBlock?: ((block: MarkdownInteractiveBlock) => ReactNode) | undefined;
66
69
  children: string;
@@ -399,7 +402,9 @@ export function retainedImageId(src: string): string | null {
399
402
  );
400
403
  }
401
404
  function MarkdownImage({ src, alt }: ComponentPropsWithoutRef<"img">) {
402
- const { renderImage } = useContext(InteractiveContext);
405
+ const { renderImage, suppressImages } = useContext(InteractiveContext);
406
+ if (suppressImages)
407
+ return <span data-og-image-placeholder="">{alt || "Image"} (preview unavailable)</span>;
403
408
  if (!src) return <span>{alt ?? "Image unavailable"}</span>;
404
409
  if (retainedImageId(src))
405
410
  return renderImage ? (
@@ -592,6 +597,7 @@ function MarkdownImpl({
592
597
  onSandboxFile,
593
598
  renderInteractiveBlock,
594
599
  renderImage,
600
+ suppressImages,
595
601
  }: MarkdownProps) {
596
602
  // Tip-ink engine for THIS body: created on the first streaming render, kept
597
603
  // through a short linger after the stream ends (so the last age window can
@@ -692,8 +698,9 @@ function MarkdownImpl({
692
698
  streaming,
693
699
  ...(renderInteractiveBlock ? { render: renderInteractiveBlock } : {}),
694
700
  ...(renderImage ? { renderImage } : {}),
701
+ ...(suppressImages ? { suppressImages } : {}),
695
702
  }),
696
- [children, streaming, renderInteractiveBlock, renderImage],
703
+ [children, streaming, renderInteractiveBlock, renderImage, suppressImages],
697
704
  );
698
705
 
699
706
  // `min-w-0` lets the prose shrink inside flex parents (message bubbles) so
@@ -843,6 +843,7 @@ export function useSandboxWorkspaceTabs(
843
843
  // start BrowserViewer—the lazy panel is still unmounted until the
844
844
  // user/agent first visits it.
845
845
  enabled={workspaceVisible}
846
+ active={resolvedActiveTab === WORKBENCH_TAB_BROWSER}
846
847
  onNotify={onNotify}
847
848
  initialBrowserSessionId={initialBrowserSessionId}
848
849
  onBrowserSessionIdChange={onBrowserSessionIdChange}
@@ -12,7 +12,11 @@ export type SessionCapabilityFrameProps = {
12
12
  skill: boolean;
13
13
  expanded: boolean;
14
14
  complete: boolean;
15
+ /** Completion copy must reflect the provider's actual scope of access. */
16
+ completeLabel?: string;
15
17
  actionLabel: string;
18
+ /** Direct navigation actions should not announce a dialog on the card. */
19
+ opensDialog?: boolean;
16
20
  note: string;
17
21
  onOpen(): void;
18
22
  onClose(): void;
@@ -48,7 +52,9 @@ export function SessionCapabilityFrame({
48
52
  skill,
49
53
  expanded,
50
54
  complete,
55
+ completeLabel,
51
56
  actionLabel,
57
+ opensDialog = true,
52
58
  note,
53
59
  onOpen,
54
60
  onClose,
@@ -90,7 +96,8 @@ export function SessionCapabilityFrame({
90
96
  </div>
91
97
  {complete ? (
92
98
  <p role="status" className="og-session-capability-copy">
93
- {skill ? "Installed · Workspace" : "Connected · Available in this conversation"}
99
+ {completeLabel ??
100
+ (skill ? "Installed · Workspace" : "Connected · Available in this conversation")}
94
101
  </p>
95
102
  ) : (
96
103
  <>
@@ -104,8 +111,9 @@ export function SessionCapabilityFrame({
104
111
  ref={buttonRef}
105
112
  type="button"
106
113
  onClick={onOpen}
107
- aria-haspopup="dialog"
108
- aria-expanded={expanded}
114
+ disabled={busy}
115
+ aria-haspopup={opensDialog ? "dialog" : undefined}
116
+ aria-expanded={opensDialog ? expanded : undefined}
109
117
  >
110
118
  {actionLabel}
111
119
  </button>
@@ -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 operation = useRef(false);
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) return null;
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
- operation.current = true;
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 (current())
197
- setError(
198
- failure instanceof Error
199
- ? failure.message
200
- : "Connection setup could not finish. Try again.",
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 = false;
204
- if (current()) setBusy(false);
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 (!current()) return;
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 || !current()) return;
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 (!current()) return;
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 (!current()) return;
259
- const enabled = await load();
260
- if (enabled && current()) {
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 (!current()) return;
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
- await reconcile(attempt);
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 (view.attempt && ["failed", "expired", "cancelled"].includes(view.attempt.state)) {
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 || !current()) return;
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 (!current()) return;
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 (!current()) return;
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(attempt: ConnectAttempt, navigation: ConnectNavigation) {
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: lifetime.current!.signal,
409
+ signal: pending.signal,
361
410
  });
362
- if (!current() || !result) return;
363
- await controller.refresh();
364
- await reconcile(result);
365
- if (result.state !== "complete")
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
- if (current()) setWaiting(false);
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(() => performAuthorization(attempt, createBrowserConnectNavigation(window)));
437
+ return run((active) =>
438
+ performAuthorization(attempt, createBrowserConnectNavigation(window), active),
439
+ );
375
440
  }
376
441
 
377
- async function finishConnection(capability: CapabilityCatalogItem) {
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 (!current()) return;
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 (!current()) return;
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 (!current()) return;
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 (!current()) return;
402
- await attachSessionCapability(client, workspaceId, sessionId, capability, current);
403
- if (!current()) return;
469
+ if (!active()) return;
470
+ await attachSessionCapability(client, workspaceId, sessionId, capability, active);
471
+ if (!active()) return;
404
472
  await onConfigured?.();
405
- if (!current()) return;
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
- <p role="status" className="og-session-capability-progress">
462
- {waiting
463
- ? `Finish signing in with ${item.name} in the opened window. This will close automatically when you’re connected.`
464
- : "Preparing your connection…"}
465
- </p>
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 (view.attempt?.nextAction.type === "authorize") void authorize(view.attempt);
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
- {error && view.attempt?.nextAction.type === "authorize"
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>
@@ -19,6 +19,7 @@ import {
19
19
  STREAM_KIND_BROWSER,
20
20
  STREAM_ROLE_CLIENT,
21
21
  } from "../lib/relay-wire";
22
+ import { usePageLiveActivity } from "./internal";
22
23
 
23
24
  export type BrowserFrameConnectionState =
24
25
  | "idle"
@@ -73,7 +74,8 @@ export function useBrowserFrameStream(
73
74
  options: UseBrowserFrameStreamOptions,
74
75
  ): UseBrowserFrameStreamResult {
75
76
  const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
76
- const enabled = options.enabled ?? true;
77
+ const pageLive = usePageLiveActivity();
78
+ const enabled = (options.enabled ?? true) && pageLive;
77
79
  const [nonce, setNonce] = useState(0);
78
80
  const [result, setResult] = useState<Omit<UseBrowserFrameStreamResult, "reconnect">>({
79
81
  state: "idle",