@opengeni/react 7.1.2 → 7.2.0-canary.36239117573001

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 (34) hide show
  1. package/dist/{browser-viewer-TZ6T76E3.js → browser-viewer-GEYFZE2W.js} +2 -2
  2. package/dist/{chunk-CP6IFD2Z.js → chunk-6IUUN2AQ.js} +124 -58
  3. package/dist/chunk-6IUUN2AQ.js.map +1 -0
  4. package/dist/{chunk-VHEY4CB3.js → chunk-RSFCYKA7.js} +5 -3
  5. package/dist/{chunk-VHEY4CB3.js.map → chunk-RSFCYKA7.js.map} +1 -1
  6. package/dist/{chunk-VI7HELBI.js → chunk-SJ5HE4ZA.js} +21 -2
  7. package/dist/chunk-SJ5HE4ZA.js.map +1 -0
  8. package/dist/{chunk-4ACTWOOW.js → chunk-VDSZ3AVO.js} +41 -18
  9. package/dist/chunk-VDSZ3AVO.js.map +1 -0
  10. package/dist/{chunk-2O2HXRGP.js → chunk-Y6DMQTLA.js} +130 -58
  11. package/dist/chunk-Y6DMQTLA.js.map +1 -0
  12. package/dist/composer.js +1 -1
  13. package/dist/{computer-viewer-FX4WOZMX.js → computer-viewer-BNHZ4PYO.js} +2 -2
  14. package/dist/index.js +5 -5
  15. package/dist/interaction.js +2 -2
  16. package/dist/session-ui.js +3 -3
  17. package/dist/session.js +1 -1
  18. package/dist/timeline/types.d.ts +2 -0
  19. package/package.json +5 -5
  20. package/src/components/browser-viewer.tsx +146 -57
  21. package/src/components/composer-transcription-control.tsx +4 -2
  22. package/src/components/composer.tsx +45 -20
  23. package/src/components/computer-viewer.tsx +161 -57
  24. package/src/hooks/use-browser-frame-stream.ts +3 -1
  25. package/src/hooks/use-voice-input.ts +12 -1
  26. package/src/timeline/compaction-copy.ts +2 -0
  27. package/src/timeline/projection.ts +31 -5
  28. package/src/timeline/types.ts +2 -0
  29. package/dist/chunk-2O2HXRGP.js.map +0 -1
  30. package/dist/chunk-4ACTWOOW.js.map +0 -1
  31. package/dist/chunk-CP6IFD2Z.js.map +0 -1
  32. package/dist/chunk-VI7HELBI.js.map +0 -1
  33. /package/dist/{browser-viewer-TZ6T76E3.js.map → browser-viewer-GEYFZE2W.js.map} +0 -0
  34. /package/dist/{computer-viewer-FX4WOZMX.js.map → computer-viewer-BNHZ4PYO.js.map} +0 -0
@@ -32,6 +32,7 @@ import {
32
32
  type WheelEvent,
33
33
  useCallback,
34
34
  useEffect,
35
+ useLayoutEffect,
35
36
  useMemo,
36
37
  useRef,
37
38
  useState,
@@ -282,7 +283,8 @@ export function ComputerViewer({
282
283
  const displayedFrame =
283
284
  frames.frame &&
284
285
  frames.frame.computerSessionId === selection?.sessionId &&
285
- frames.frame.targetId === computer.selectedTarget?.id
286
+ frames.frame.targetId === computer.selectedTarget?.id &&
287
+ frames.frame.targetGeneration === computer.selectedTarget?.targetGeneration
286
288
  ? frames.frame
287
289
  : null;
288
290
  const rfbStream =
@@ -434,11 +436,15 @@ export function ComputerViewer({
434
436
 
435
437
  const perform = useCallback(
436
438
  async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
439
+ let receipt;
437
440
  if (action.type === "pointer") {
438
441
  if (!frame) throw new Error("Desktop view is not ready for pointer input.");
439
- await actFromFrame(action, frame);
442
+ receipt = await actFromFrame(action, frame);
440
443
  } else {
441
- await act(action);
444
+ receipt = await act(action);
445
+ }
446
+ if (receipt.state !== "completed") {
447
+ throw new Error(receipt.error?.message ?? "Desktop input did not complete.");
442
448
  }
443
449
  },
444
450
  [act, actFromFrame],
@@ -627,6 +633,12 @@ export function ComputerViewer({
627
633
  </div>
628
634
  ) : (
629
635
  <ComputerViewport
636
+ key={JSON.stringify([
637
+ selection?.sessionId,
638
+ computer.selectedTarget?.id,
639
+ computer.selectedTarget?.targetGeneration,
640
+ frames.attachment?.controllerGeneration,
641
+ ])}
630
642
  frame={machineLocked ? null : displayedFrame}
631
643
  observation={computer.observation}
632
644
  target={computer.selectedTarget}
@@ -1030,7 +1042,11 @@ function ComputerViewport(props: {
1030
1042
  const readClipboardRef = useRef(props.onReadClipboard);
1031
1043
  const errorRef = useRef(props.onError);
1032
1044
  const actionTailRef = useRef<Promise<void>>(Promise.resolve());
1045
+ const actionQueueEpochRef = useRef(0);
1033
1046
  const queuedFrameRef = useRef<ComputerFrame | null>(null);
1047
+ const currentFrameRef = useRef<ComputerFrame | null>(props.frame);
1048
+ const paintedFrameRef = useRef<ComputerFrame | null>(null);
1049
+ const [paintedFrame, setPaintedFrame] = useState<ComputerFrame | null>(null);
1034
1050
  const decodingFrameRef = useRef(false);
1035
1051
  const mountedRef = useRef(true);
1036
1052
  actionRef.current = props.onAction;
@@ -1042,6 +1058,18 @@ function ComputerViewport(props: {
1042
1058
  !props.machineLocked &&
1043
1059
  (!props.backgroundActions || props.target?.kind === "screen" || props.target?.focused === true);
1044
1060
 
1061
+ const clearBufferedInput = useCallback(() => {
1062
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1063
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
1064
+ if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
1065
+ clickTimerRef.current = null;
1066
+ wheelRef.current = null;
1067
+ pendingTextRef.current = null;
1068
+ pointerStartRef.current = null;
1069
+ lastClickRef.current = null;
1070
+ if (inputRef.current) inputRef.current.value = "";
1071
+ }, []);
1072
+
1045
1073
  const paintQueuedFrames = useCallback(() => {
1046
1074
  if (decodingFrameRef.current) return;
1047
1075
  decodingFrameRef.current = true;
@@ -1058,8 +1086,15 @@ function ComputerViewport(props: {
1058
1086
  const bitmap = await createImageBitmap(blob);
1059
1087
  try {
1060
1088
  const canvas = canvasRef.current;
1061
- if (mountedRef.current && canvas) {
1089
+ if (
1090
+ mountedRef.current &&
1091
+ canvas &&
1092
+ currentFrameRef.current &&
1093
+ sameComputerTarget(frame, currentFrameRef.current)
1094
+ ) {
1062
1095
  paintCanvas(canvas, bitmap, frame.width, frame.height);
1096
+ paintedFrameRef.current = frame;
1097
+ setPaintedFrame(frame);
1063
1098
  }
1064
1099
  } finally {
1065
1100
  bitmap.close();
@@ -1069,11 +1104,23 @@ function ComputerViewport(props: {
1069
1104
  objectUrl = URL.createObjectURL(blob);
1070
1105
  const image = await loadImage(objectUrl);
1071
1106
  const canvas = canvasRef.current;
1072
- if (mountedRef.current && canvas) {
1107
+ if (
1108
+ mountedRef.current &&
1109
+ canvas &&
1110
+ currentFrameRef.current &&
1111
+ sameComputerTarget(frame, currentFrameRef.current)
1112
+ ) {
1073
1113
  paintCanvas(canvas, image, frame.width, frame.height);
1114
+ paintedFrameRef.current = frame;
1115
+ setPaintedFrame(frame);
1074
1116
  }
1075
1117
  } catch (cause) {
1076
- if (mountedRef.current) errorRef.current(cause);
1118
+ if (
1119
+ mountedRef.current &&
1120
+ currentFrameRef.current &&
1121
+ sameComputerTarget(frame, currentFrameRef.current)
1122
+ )
1123
+ errorRef.current(cause);
1077
1124
  } finally {
1078
1125
  if (objectUrl) URL.revokeObjectURL(objectUrl);
1079
1126
  }
@@ -1085,40 +1132,64 @@ function ComputerViewport(props: {
1085
1132
  })();
1086
1133
  }, []);
1087
1134
 
1088
- useEffect(() => {
1135
+ useLayoutEffect(() => {
1089
1136
  mountedRef.current = true;
1090
1137
  return () => {
1091
1138
  mountedRef.current = false;
1139
+ actionQueueEpochRef.current += 1;
1092
1140
  queuedFrameRef.current = null;
1141
+ clearBufferedInput();
1093
1142
  };
1094
- }, []);
1143
+ }, [clearBufferedInput]);
1095
1144
 
1096
- useEffect(() => {
1097
- if (!props.frame) return;
1145
+ useLayoutEffect(() => {
1146
+ if (currentFrameRef.current && !props.frame) {
1147
+ actionQueueEpochRef.current += 1;
1148
+ clearBufferedInput();
1149
+ }
1150
+ currentFrameRef.current = props.frame;
1151
+ if (!props.frame) {
1152
+ queuedFrameRef.current = null;
1153
+ paintedFrameRef.current = null;
1154
+ setPaintedFrame(null);
1155
+ return;
1156
+ }
1098
1157
  queuedFrameRef.current = props.frame;
1099
1158
  paintQueuedFrames();
1100
- }, [paintQueuedFrames, props.frame]);
1159
+ }, [clearBufferedInput, paintQueuedFrames, props.frame]);
1101
1160
 
1102
- useEffect(
1103
- () => () => {
1104
- if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1105
- if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
1106
- if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
1107
- },
1108
- [],
1109
- );
1161
+ useLayoutEffect(() => {
1162
+ if (!rawInputEnabled) {
1163
+ actionQueueEpochRef.current += 1;
1164
+ clearBufferedInput();
1165
+ }
1166
+ }, [clearBufferedInput, rawInputEnabled]);
1110
1167
 
1111
1168
  const enqueue = useCallback(
1112
- (action: ComputerAction, frame: ComputerFrame | null, after?: () => Promise<void>) => {
1169
+ (
1170
+ action: ComputerAction,
1171
+ frame: ComputerFrame | null,
1172
+ after?: (isCurrent: () => boolean) => Promise<void>,
1173
+ ) => {
1174
+ const epoch = actionQueueEpochRef.current;
1175
+ const dispatch = actionRef.current;
1176
+ const isCurrent = () => mountedRef.current && epoch === actionQueueEpochRef.current;
1113
1177
  actionTailRef.current = actionTailRef.current
1114
1178
  .catch(() => undefined)
1115
1179
  .then(async () => {
1116
- await actionRef.current(action, frame);
1117
- await after?.();
1180
+ if (!isCurrent()) return;
1181
+ await dispatch(action, frame);
1182
+ if (!isCurrent()) return;
1183
+ await after?.(isCurrent);
1118
1184
  })
1119
- .catch((cause) => errorRef.current(cause));
1185
+ .catch((cause) => {
1186
+ if (!isCurrent()) return;
1187
+ actionQueueEpochRef.current += 1;
1188
+ clearBufferedInput();
1189
+ errorRef.current(cause);
1190
+ });
1120
1191
  },
1121
- [],
1192
+ [clearBufferedInput],
1122
1193
  );
1123
1194
 
1124
1195
  const point = useCallback(
@@ -1153,14 +1224,37 @@ function ComputerViewport(props: {
1153
1224
  );
1154
1225
  }, [enqueue]);
1155
1226
 
1227
+ const flushPendingWheel = useCallback(() => {
1228
+ const batch = wheelRef.current;
1229
+ if (!batch) return;
1230
+ if (batch.timer) clearTimeout(batch.timer);
1231
+ wheelRef.current = null;
1232
+ enqueue(
1233
+ {
1234
+ type: "pointer",
1235
+ frameId: batch.frame.frameId,
1236
+ action: "scroll",
1237
+ x: batch.x,
1238
+ y: batch.y,
1239
+ deltaX: batch.deltaX,
1240
+ deltaY: batch.deltaY,
1241
+ },
1242
+ batch.frame,
1243
+ );
1244
+ }, [enqueue]);
1245
+
1156
1246
  const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
1157
- if (!props.frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
1247
+ const frame = paintedFrameRef.current;
1248
+ if (!frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
1249
+ // Preserve the keyboard sink's focus through the canvas pointer default action.
1250
+ event.preventDefault();
1251
+ flushPendingWheel();
1158
1252
  flushPendingText();
1159
1253
  pointerStartRef.current = {
1160
1254
  x: event.clientX,
1161
1255
  y: event.clientY,
1162
1256
  pointerId: event.pointerId,
1163
- frame: props.frame,
1257
+ frame,
1164
1258
  };
1165
1259
  event.currentTarget.setPointerCapture?.(event.pointerId);
1166
1260
  inputRef.current?.focus({ preventScroll: true });
@@ -1233,8 +1327,9 @@ function ComputerViewport(props: {
1233
1327
  const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
1234
1328
  event.preventDefault();
1235
1329
  if (!rawInputEnabled) return;
1236
- const frame = props.frame;
1330
+ const frame = paintedFrameRef.current;
1237
1331
  if (!frame) return;
1332
+ flushPendingWheel();
1238
1333
  flushPendingText();
1239
1334
  flushPendingClick();
1240
1335
  const at = point(frame, event.clientX, event.clientY);
@@ -1255,44 +1350,35 @@ function ComputerViewport(props: {
1255
1350
 
1256
1351
  const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
1257
1352
  if (!rawInputEnabled) return;
1258
- const frame = props.frame;
1353
+ const frame = paintedFrameRef.current;
1259
1354
  if (!frame) return;
1260
1355
  const at = point(frame, event.clientX, event.clientY);
1261
1356
  if (!at) return;
1262
1357
  flushPendingText();
1263
1358
  flushPendingClick();
1264
1359
  event.preventDefault();
1265
- const pending = wheelRef.current;
1266
- if (pending?.timer) clearTimeout(pending.timer);
1360
+ let pending = wheelRef.current;
1361
+ if (
1362
+ pending &&
1363
+ (!sameComputerTarget(pending.frame, frame) ||
1364
+ Math.hypot(pending.x - at.x, pending.y - at.y) > 6)
1365
+ ) {
1366
+ flushPendingWheel();
1367
+ pending = null;
1368
+ }
1267
1369
  wheelRef.current = {
1268
1370
  x: at.x,
1269
1371
  y: at.y,
1270
- deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
1271
- deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
1372
+ deltaX: (pending?.deltaX ?? 0) + event.deltaX,
1373
+ deltaY: (pending?.deltaY ?? 0) + event.deltaY,
1272
1374
  frame,
1273
- timer: setTimeout(() => {
1274
- const batch = wheelRef.current;
1275
- wheelRef.current = null;
1276
- if (batch) {
1277
- enqueue(
1278
- {
1279
- type: "pointer",
1280
- frameId: batch.frame.frameId,
1281
- action: "scroll",
1282
- x: batch.x,
1283
- y: batch.y,
1284
- deltaX: batch.deltaX,
1285
- deltaY: batch.deltaY,
1286
- },
1287
- batch.frame,
1288
- );
1289
- }
1290
- }, 45),
1375
+ timer: pending?.timer ?? setTimeout(flushPendingWheel, 45),
1291
1376
  };
1292
1377
  };
1293
1378
 
1294
1379
  const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
1295
1380
  if (!rawInputEnabled) return;
1381
+ flushPendingWheel();
1296
1382
  const command = event.metaKey || event.ctrlKey;
1297
1383
  if (
1298
1384
  props.clipboardEnabled &&
@@ -1314,10 +1400,13 @@ function ComputerViewport(props: {
1314
1400
  const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1315
1401
  if (!props.clipboardEnabled || !rawInputEnabled) return;
1316
1402
  event.preventDefault();
1403
+ flushPendingWheel();
1317
1404
  flushPendingText();
1318
1405
  flushPendingClick();
1319
- enqueue({ type: "clipboard", operation: "copy" }, null, async () => {
1320
- const clipboard = await readClipboardRef.current();
1406
+ const readClipboard = readClipboardRef.current;
1407
+ enqueue({ type: "clipboard", operation: "copy" }, null, async (isCurrent) => {
1408
+ const clipboard = await readClipboard();
1409
+ if (!isCurrent()) return;
1321
1410
  if (!clipboard.text) return;
1322
1411
  if (!(await copyTextToClipboard(clipboard.text))) {
1323
1412
  throw new Error("Desktop text could not be copied to the local clipboard");
@@ -1328,17 +1417,23 @@ function ComputerViewport(props: {
1328
1417
  const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1329
1418
  if (!props.clipboardEnabled || !rawInputEnabled) return;
1330
1419
  event.preventDefault();
1420
+ flushPendingWheel();
1331
1421
  flushPendingText();
1332
1422
  flushPendingClick();
1333
1423
  const text = event.clipboardData.getData("text/plain");
1334
- enqueue({ type: "clipboard", operation: "write", text }, null, async () => {
1335
- assertExactComputerClipboard(await readClipboardRef.current(), text);
1336
- await actionRef.current({ type: "clipboard", operation: "paste" }, null);
1424
+ const readClipboard = readClipboardRef.current;
1425
+ const dispatch = actionRef.current;
1426
+ enqueue({ type: "clipboard", operation: "write", text }, null, async (isCurrent) => {
1427
+ const clipboard = await readClipboard();
1428
+ if (!isCurrent()) return;
1429
+ assertExactComputerClipboard(clipboard, text);
1430
+ await dispatch({ type: "clipboard", operation: "paste" }, null);
1337
1431
  });
1338
1432
  };
1339
1433
 
1340
1434
  const input = (value: string) => {
1341
1435
  if (!value || !rawInputEnabled) return;
1436
+ flushPendingWheel();
1342
1437
  flushPendingClick();
1343
1438
  const pending = pendingTextRef.current;
1344
1439
  if (pending) {
@@ -1354,7 +1449,12 @@ function ComputerViewport(props: {
1354
1449
  if (inputRef.current) inputRef.current.value = "";
1355
1450
  };
1356
1451
 
1357
- const showCanvas = props.frame !== null && !props.machineLocked && !streamFailed;
1452
+ const showCanvas =
1453
+ props.frame !== null &&
1454
+ paintedFrame !== null &&
1455
+ sameComputerTarget(props.frame, paintedFrame) &&
1456
+ !props.machineLocked &&
1457
+ !streamFailed;
1358
1458
  return (
1359
1459
  <div className="relative min-h-0 flex-1 overflow-hidden bg-black">
1360
1460
  <canvas
@@ -1772,9 +1872,13 @@ export function computerKey(
1772
1872
  }
1773
1873
 
1774
1874
  function sameFrameFence(left: ComputerFrame, right: ComputerFrame): boolean {
1875
+ return left.frameId === right.frameId && sameComputerTarget(left, right);
1876
+ }
1877
+
1878
+ function sameComputerTarget(left: ComputerFrame, right: ComputerFrame): boolean {
1775
1879
  return (
1776
- left.frameId === right.frameId &&
1777
1880
  left.computerSessionId === right.computerSessionId &&
1881
+ left.controllerGeneration === right.controllerGeneration &&
1778
1882
  left.targetId === right.targetId &&
1779
1883
  left.targetGeneration === right.targetGeneration
1780
1884
  );
@@ -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",
@@ -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":
@@ -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)
@@ -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