@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
@@ -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
- observationRef.current = { browserSessionId, observation: null };
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: null,
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
  }
@@ -163,6 +163,7 @@ export function useVoiceInput({
163
163
  const automaticRetryAttemptRef = useRef(0);
164
164
  const ownerHeartbeatTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
165
165
  const controllerRef = useRef<AbortController | null>(null);
166
+ const disabledRef = useRef(disabled);
166
167
  const valueRef = useRef(value);
167
168
  const storeRef = useRef<VoiceRecordingStore | null>(null);
168
169
  const storePromiseRef = useRef<Promise<VoiceRecordingStore> | null>(null);
@@ -176,6 +177,7 @@ export function useVoiceInput({
176
177
  const statusRef = useRef(status);
177
178
  valueRef.current = value;
178
179
  statusRef.current = status;
180
+ disabledRef.current = disabled;
179
181
  workspaceIdRef.current = workspaceId;
180
182
 
181
183
  const ensureOwnerId = useCallback(async (): Promise<string> => {
@@ -468,6 +470,14 @@ export function useVoiceInput({
468
470
  return;
469
471
  }
470
472
 
473
+ // Session creation can disable the composer while a recording is
474
+ // transcribing. Keep the durable transcript for explicit insertion
475
+ // later; never append text after Send captured its visible snapshot.
476
+ if (disabledRef.current) {
477
+ setStatus("transcript-ready");
478
+ return;
479
+ }
480
+
471
481
  const next = appendFinalTranscript(valueRef.current, response.text);
472
482
  if (next !== valueRef.current) {
473
483
  valueRef.current = next;
@@ -944,6 +954,7 @@ export function useVoiceInput({
944
954
  const insertSavedTranscript = useCallback(async (): Promise<void> => {
945
955
  const manifest = manifestRef.current;
946
956
  if (
957
+ disabledRef.current ||
947
958
  !manifest ||
948
959
  manifest.workspaceId !== workspaceId ||
949
960
  manifest.finalizationState !== "transcript-ready" ||
@@ -955,7 +966,7 @@ export function useVoiceInput({
955
966
  controllerRef.current?.abort();
956
967
  controllerRef.current = null;
957
968
  const store = await ensureStore();
958
- if (generation !== generationRef.current) return;
969
+ if (generation !== generationRef.current || disabledRef.current) return;
959
970
  const next = appendFinalTranscript(valueRef.current, manifest.transcriptText);
960
971
  if (next !== valueRef.current) {
961
972
  valueRef.current = next;
@@ -15,6 +15,8 @@ export function compactionSkipSubtitle(
15
15
  return "No active history to compact";
16
16
  case "replacement_not_smaller":
17
17
  return "Checkpoint would not reduce memory size";
18
+ case "replacement_exceeds_model_budget":
19
+ return "Checkpoint exceeds this model's context budget. Chat history is unchanged.";
18
20
  case "replacement_unchanged":
19
21
  return "Checkpoint made no progress";
20
22
  case "summarization_failed":
@@ -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
- value.contentType === "image/png" &&
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 &&
@@ -1016,6 +1016,7 @@ export function buildTimeline(
1016
1016
  case "tool.auth_needed":
1017
1017
  case "credential.auth_needed": {
1018
1018
  const capability = capabilityAuthorizationRequest(payload.capability);
1019
+ const setupRequest = customMcpSetupRequest(payload.setupRequest);
1019
1020
  if (
1020
1021
  event.type === "tool.auth_needed" &&
1021
1022
  !stringValue(payload.toolName) &&
@@ -1045,11 +1046,12 @@ export function buildTimeline(
1045
1046
  payload.connectionSubjectScope === "workspace"
1046
1047
  ? payload.connectionSubjectScope
1047
1048
  : null,
1048
- source: capability
1049
- ? "capability"
1050
- : event.type === "tool.auth_needed"
1051
- ? "tool"
1052
- : "credential",
1049
+ source:
1050
+ capability || setupRequest
1051
+ ? "capability"
1052
+ : event.type === "tool.auth_needed"
1053
+ ? "tool"
1054
+ : "credential",
1053
1055
  providerDomain: stringValue(payload.providerDomain),
1054
1056
  connectionId: typeof payload.connectionId === "string" ? payload.connectionId : null,
1055
1057
  authoritySource: payload.authoritySource === "host" ? "host" : null,
@@ -1064,6 +1066,7 @@ export function buildTimeline(
1064
1066
  authorizationUrl:
1065
1067
  typeof payload.authorizationUrl === "string" ? payload.authorizationUrl : null,
1066
1068
  capability,
1069
+ setupRequest,
1067
1070
  occurredAt: event.occurredAt,
1068
1071
  });
1069
1072
  break;
@@ -2623,6 +2626,29 @@ function capabilityAuthorizationRequest(
2623
2626
  };
2624
2627
  }
2625
2628
 
2629
+ function customMcpSetupRequest(value: unknown): NonNullable<AuthNeededItem["setupRequest"]> | null {
2630
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2631
+ const input = value as Record<string, unknown>;
2632
+ if (
2633
+ input.kind !== "mcp" ||
2634
+ typeof input.name !== "string" ||
2635
+ typeof input.rationale !== "string" ||
2636
+ typeof input.endpointUrl !== "string"
2637
+ )
2638
+ return null;
2639
+ try {
2640
+ if (new URL(input.endpointUrl).protocol !== "https:") return null;
2641
+ } catch {
2642
+ return null;
2643
+ }
2644
+ return {
2645
+ kind: "mcp",
2646
+ name: input.name,
2647
+ endpointUrl: input.endpointUrl,
2648
+ rationale: input.rationale,
2649
+ };
2650
+ }
2651
+
2626
2652
  function stringList(value: unknown): string[] {
2627
2653
  return Array.isArray(value)
2628
2654
  ? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
@@ -772,21 +772,73 @@ function ComputerCallRenderer({ item, loadRetainedScreenshot }: ToolRendererProp
772
772
  );
773
773
  }
774
774
 
775
- function RetainedSessionImageDisclosure({
776
- artifact,
777
- load,
778
- title,
779
- caption,
780
- noun,
781
- icon,
782
- lightboxLabel,
783
- batched,
784
- failed,
785
- cancelled,
786
- filename,
787
- defaultOpen,
788
- children,
789
- }: {
775
+ /** Browser tool images share authenticated screenshot loading, with their own media kind. */
776
+ function BrowserScreenshotRenderer(props: ToolRendererProps) {
777
+ const { item, loadRetainedScreenshot } = props;
778
+ const leaf = mcpToolLeaf(item.name);
779
+ const title =
780
+ leaf === "browser_observe"
781
+ ? "Observed browser"
782
+ : leaf === "browser_act"
783
+ ? "Used browser"
784
+ : "Browser screenshot";
785
+ const retained = retainedScreenshotMetadata(item.output);
786
+ const isFailed = item.status === "failed";
787
+ const isCancelled = item.status === "cancelled";
788
+
789
+ if (item.status === "running" && leaf === "browser_screenshot") {
790
+ return (
791
+ <ActivityDisclosure
792
+ icon={<CameraIcon className={ICON_SIZE} />}
793
+ iconTone="running"
794
+ title={title}
795
+ running
796
+ media={<MediaSkeleton />}
797
+ >
798
+ <BodyNote>capturing frame…</BodyNote>
799
+ </ActivityDisclosure>
800
+ );
801
+ }
802
+
803
+ if (!retained) return <GenericRenderer {...props} />;
804
+ if (!retained.available) {
805
+ const state =
806
+ retained.reason === "expired" || retained.reason === "deleted"
807
+ ? retained.reason
808
+ : "unavailable";
809
+ return (
810
+ <ActivityDisclosure
811
+ icon={<CameraOffIcon className={ICON_SIZE} />}
812
+ iconTone={isFailed ? "failed" : "muted"}
813
+ title={`${title} · ${state}`}
814
+ failed={isFailed}
815
+ cancelled={isCancelled}
816
+ preview={`screenshot ${state}`}
817
+ media={<MediaEmpty />}
818
+ >
819
+ <BodyNote tone={isFailed ? "error" : undefined}>
820
+ Screenshot {state}: {retained.reason.replaceAll("_", " ")}.
821
+ </BodyNote>
822
+ </ActivityDisclosure>
823
+ );
824
+ }
825
+ return (
826
+ <RetainedSessionImageDisclosure
827
+ artifact={retained}
828
+ load={loadRetainedScreenshot}
829
+ title={title}
830
+ caption={title}
831
+ noun="screenshot"
832
+ icon={<CameraIcon className={ICON_SIZE} />}
833
+ lightboxLabel="Browser screenshot"
834
+ batched={null}
835
+ failed={isFailed}
836
+ cancelled={isCancelled}
837
+ />
838
+ );
839
+ }
840
+
841
+ type RetainedSessionImageDisclosureProps = {
790
842
  artifact: RetainedArtifactReference;
791
843
  load: ToolRendererProps["loadRetainedArtifact"];
792
844
  title: string;
@@ -800,7 +852,41 @@ function RetainedSessionImageDisclosure({
800
852
  filename?: string;
801
853
  defaultOpen?: boolean;
802
854
  children?: ReactNode;
803
- }) {
855
+ };
856
+
857
+ function RetainedSessionImageDisclosure(props: RetainedSessionImageDisclosureProps) {
858
+ const compact = useContext(CompactActivityContext);
859
+ // The rolling progress label has no screenshot loader by design. Do not run
860
+ // the full image hook there: it would report the missing loader as an error.
861
+ if (compact) {
862
+ return (
863
+ <ActivityDisclosure
864
+ icon={props.icon}
865
+ title={props.title}
866
+ compactPreview={null}
867
+ failed={props.failed}
868
+ cancelled={props.cancelled}
869
+ />
870
+ );
871
+ }
872
+ return <LoadedRetainedSessionImageDisclosure {...props} />;
873
+ }
874
+
875
+ function LoadedRetainedSessionImageDisclosure({
876
+ artifact,
877
+ load,
878
+ title,
879
+ caption,
880
+ noun,
881
+ icon,
882
+ lightboxLabel,
883
+ batched,
884
+ failed,
885
+ cancelled,
886
+ filename,
887
+ defaultOpen,
888
+ children,
889
+ }: RetainedSessionImageDisclosureProps) {
804
890
  const state = useRetainedImageObjectUrl(artifact, load);
805
891
  const downloadFilename = filename ?? retainedImageFilename(artifact);
806
892
 
@@ -2401,6 +2487,9 @@ const BASE_ENTRIES: ToolRegistryEntry[] = [
2401
2487
  { match: "name", name: "apply_patch_call", render: ApplyPatchRenderer },
2402
2488
  { match: "name", name: "apply_patch", render: ApplyPatchRenderer },
2403
2489
  { match: "name", name: "computer_call", render: ComputerCallRenderer },
2490
+ { match: "name", name: "browser_screenshot", render: BrowserScreenshotRenderer },
2491
+ { match: "name", name: "browser_observe", render: BrowserScreenshotRenderer },
2492
+ { match: "name", name: "browser_act", render: BrowserScreenshotRenderer },
2404
2493
  // Function-mode computer tools (codex / chat-wire transports).
2405
2494
  { match: "name", name: "computer_screenshot", render: ComputerCallRenderer },
2406
2495
  { match: "name", name: "computer_click", render: ComputerCallRenderer },
@@ -21,9 +21,11 @@ import {
21
21
  applyPatchOpsFromToolItem,
22
22
  isApplyPatch,
23
23
  mediaPreviewFact,
24
+ retainedScreenshotMetadata,
24
25
  screenshotDataUrl,
25
26
  } from "./parsers";
26
27
  import { rawTypeOf } from "./registry";
28
+ import { mcpToolLeaf } from "./tool-display-name";
27
29
  import type { ActivityItem, ToolCallItem, TurnOutcome } from "./types";
28
30
  export type { TurnOutcome } from "./types";
29
31
 
@@ -626,11 +628,17 @@ const BUILT_IN_TURN_SUMMARY_FACETS: readonly TurnSummaryFacet[] = Object.freeze(
626
628
  summarize: ({ toolCalls }) => {
627
629
  let screenshots = 0;
628
630
  for (const item of toolCalls) {
631
+ const leaf = mcpToolLeaf(item.name);
629
632
  if (
630
633
  (rawTypeOf(item) === "computer_call" ||
631
634
  item.name === "computer_call" ||
632
- item.name === "computer_screenshot") &&
633
- (screenshotDataUrl(item.output) !== null || mediaPreviewFact(item.output) !== null)
635
+ item.name === "computer_screenshot" ||
636
+ leaf === "browser_screenshot" ||
637
+ leaf === "browser_observe" ||
638
+ leaf === "browser_act") &&
639
+ (retainedScreenshotMetadata(item.output) !== null ||
640
+ screenshotDataUrl(item.output) !== null ||
641
+ mediaPreviewFact(item.output) !== null)
634
642
  ) {
635
643
  screenshots += 1;
636
644
  }
@@ -475,6 +475,8 @@ export type AuthNeededItem = {
475
475
  authorizationUrl: string | null;
476
476
  /** Agent-selected catalog recommendation. The host still owns authorization. */
477
477
  capability?: NonNullable<ToolAuthNeededPayload["capability"]> | null | undefined;
478
+ /** Human-reviewed custom integration proposal. Never treated as catalog authority. */
479
+ setupRequest?: ToolAuthNeededPayload["setupRequest"] | null | undefined;
478
480
  occurredAt: string;
479
481
  };
480
482
 
@@ -809,6 +809,9 @@
809
809
  :where(.og-root).og-session-capability-setup [role=alert],:where(.og-root) .og-session-capability-setup [role=alert] {
810
810
  color: var(--og-color-danger,#b42318);
811
811
  }
812
+ :where(.og-root).og-session-capability-setup .og-session-capability-notice,:where(.og-root) .og-session-capability-setup .og-session-capability-notice {
813
+ color: var(--og-color-fg-muted);
814
+ }
812
815
  :where(.og-root).og-session-capability-shell[data-state="complete"],:where(.og-root) .og-session-capability-shell[data-state="complete"] {
813
816
  border-color: var(--og-color-status-success,#16804a);
814
817
  @supports (color: color-mix(in lab, red, red)) {
@@ -857,6 +860,9 @@
857
860
  color: var(--og-color-fg-muted);
858
861
  padding-block: 8px;
859
862
  }
863
+ :where(.og-root).og-session-capability-setup .og-session-capability-stop,:where(.og-root) .og-session-capability-setup .og-session-capability-stop {
864
+ justify-self: end;
865
+ }
860
866
  @media (pointer:coarse) {
861
867
  :where(.og-root).og-session-capability-actions button,:where(.og-root) .og-session-capability-actions button,:where(.og-root).og-session-capability-setup button,:where(.og-root) .og-session-capability-setup button {
862
868
  min-height: 44px;
@@ -28,6 +28,7 @@
28
28
  .og-session-capability-setup button { font:inherit; padding:9px 12px; border-radius:7px; border:1px solid var(--og-color-border); background:var(--og-color-surface-2); color:inherit; cursor:pointer; }
29
29
  .og-session-capability-setup button:disabled { opacity:.55; cursor:not-allowed; }
30
30
  .og-session-capability-setup [role=alert] { color:var(--og-color-danger,#b42318); }
31
+ .og-session-capability-setup .og-session-capability-notice { color:var(--og-color-fg-muted); }
31
32
  .og-session-capability-shell[data-state="complete"] { border-color:color-mix(in srgb,var(--og-color-status-success,#16804a) 35%,var(--og-color-border)); }
32
33
  .og-session-capability-shell[data-state="complete"] .og-session-capability-header > svg,
33
34
  .og-session-capability-shell[data-state="complete"] .og-session-capability-copy { color:var(--og-color-status-success,#16804a); }
@@ -39,4 +40,5 @@
39
40
  .og-session-capability-setup .og-session-capability-primary { justify-self:end; min-height:40px; padding:10px 16px; border:0; background:var(--og-color-accent-deep,#006cdb); color:var(--og-color-accent-fg,#fff); font-weight:500; }
40
41
  .og-session-capability-setup .og-session-capability-primary:hover:not(:disabled) { filter:brightness(.94); }
41
42
  .og-session-capability-progress { color:var(--og-color-fg-muted); padding-block:8px; }
43
+ .og-session-capability-setup .og-session-capability-stop { justify-self:end; }
42
44
  @media (pointer:coarse) { .og-session-capability-actions button,.og-session-capability-setup button { min-height:44px; } }