@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.
- package/dist/{browser-viewer-TZ6T76E3.js → browser-viewer-GEYFZE2W.js} +2 -2
- package/dist/{chunk-CP6IFD2Z.js → chunk-6IUUN2AQ.js} +124 -58
- package/dist/chunk-6IUUN2AQ.js.map +1 -0
- package/dist/{chunk-VHEY4CB3.js → chunk-RSFCYKA7.js} +5 -3
- package/dist/{chunk-VHEY4CB3.js.map → chunk-RSFCYKA7.js.map} +1 -1
- package/dist/{chunk-VI7HELBI.js → chunk-SJ5HE4ZA.js} +21 -2
- package/dist/chunk-SJ5HE4ZA.js.map +1 -0
- package/dist/{chunk-4ACTWOOW.js → chunk-VDSZ3AVO.js} +41 -18
- package/dist/chunk-VDSZ3AVO.js.map +1 -0
- package/dist/{chunk-2O2HXRGP.js → chunk-Y6DMQTLA.js} +130 -58
- package/dist/chunk-Y6DMQTLA.js.map +1 -0
- package/dist/composer.js +1 -1
- package/dist/{computer-viewer-FX4WOZMX.js → computer-viewer-BNHZ4PYO.js} +2 -2
- package/dist/index.js +5 -5
- package/dist/interaction.js +2 -2
- package/dist/session-ui.js +3 -3
- package/dist/session.js +1 -1
- package/dist/timeline/types.d.ts +2 -0
- package/package.json +5 -5
- package/src/components/browser-viewer.tsx +146 -57
- package/src/components/composer-transcription-control.tsx +4 -2
- package/src/components/composer.tsx +45 -20
- package/src/components/computer-viewer.tsx +161 -57
- package/src/hooks/use-browser-frame-stream.ts +3 -1
- package/src/hooks/use-voice-input.ts +12 -1
- package/src/timeline/compaction-copy.ts +2 -0
- package/src/timeline/projection.ts +31 -5
- package/src/timeline/types.ts +2 -0
- package/dist/chunk-2O2HXRGP.js.map +0 -1
- package/dist/chunk-4ACTWOOW.js.map +0 -1
- package/dist/chunk-CP6IFD2Z.js.map +0 -1
- package/dist/chunk-VI7HELBI.js.map +0 -1
- /package/dist/{browser-viewer-TZ6T76E3.js.map → browser-viewer-GEYFZE2W.js.map} +0 -0
- /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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1097
|
-
if (!props.frame)
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
1117
|
-
await
|
|
1180
|
+
if (!isCurrent()) return;
|
|
1181
|
+
await dispatch(action, frame);
|
|
1182
|
+
if (!isCurrent()) return;
|
|
1183
|
+
await after?.(isCurrent);
|
|
1118
1184
|
})
|
|
1119
|
-
.catch((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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1266
|
-
if (
|
|
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
|
|
1271
|
-
deltaY: (pending
|
|
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
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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)
|
package/src/timeline/types.ts
CHANGED
|
@@ -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
|
|