@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
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
useCallback,
|
|
51
51
|
useEffect,
|
|
52
52
|
useId,
|
|
53
|
+
useLayoutEffect,
|
|
53
54
|
useMemo,
|
|
54
55
|
useRef,
|
|
55
56
|
useState,
|
|
@@ -236,6 +237,7 @@ export function BrowserViewer({
|
|
|
236
237
|
const previousSessionIdRef = useRef(sessionId);
|
|
237
238
|
const seenInterventionIdsRef = useRef(new Set<string>());
|
|
238
239
|
const diagnosticRequestRef = useRef(0);
|
|
240
|
+
const viewportFocusHandoffRef = useRef<string | null>(null);
|
|
239
241
|
const [diagnosticsView, setDiagnosticsView] = useState<BrowserDiagnosticsView | null>(null);
|
|
240
242
|
|
|
241
243
|
const notifyError = useCallback(
|
|
@@ -342,12 +344,8 @@ export function BrowserViewer({
|
|
|
342
344
|
setResumeAttempt((visible) =>
|
|
343
345
|
visible?.sessionId === selectedRegistrySession.id ? null : visible,
|
|
344
346
|
);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (selectedRegistrySession.lifecycle === "suspended") {
|
|
348
|
-
void wakeBrowser(selectedRegistrySession);
|
|
349
347
|
}
|
|
350
|
-
}, [selectedRegistrySession
|
|
348
|
+
}, [selectedRegistrySession]);
|
|
351
349
|
|
|
352
350
|
const browser = useBrowserSession({
|
|
353
351
|
...override,
|
|
@@ -883,6 +881,20 @@ export function BrowserViewer({
|
|
|
883
881
|
}}
|
|
884
882
|
/>
|
|
885
883
|
<BrowserViewport
|
|
884
|
+
// A new control scope must discard every buffered gesture and queued action.
|
|
885
|
+
key={JSON.stringify([
|
|
886
|
+
selection?.sessionId,
|
|
887
|
+
browser.selectedTarget?.id,
|
|
888
|
+
browser.selectedTarget?.targetGeneration,
|
|
889
|
+
browser.selectedTarget?.documentGeneration,
|
|
890
|
+
frames.attachment?.controllerGeneration,
|
|
891
|
+
])}
|
|
892
|
+
focusHandoffRef={viewportFocusHandoffRef}
|
|
893
|
+
focusScope={JSON.stringify([
|
|
894
|
+
selection?.sessionId,
|
|
895
|
+
browser.selectedTarget?.id,
|
|
896
|
+
browser.selectedTarget?.targetGeneration,
|
|
897
|
+
])}
|
|
886
898
|
frame={displayedFrame}
|
|
887
899
|
connectionState={displayConnectionState}
|
|
888
900
|
supportsLiveFrames={supportsLiveFrames}
|
|
@@ -1828,6 +1840,8 @@ function BrowserAddressBar(props: {
|
|
|
1828
1840
|
}
|
|
1829
1841
|
|
|
1830
1842
|
function BrowserViewport(props: {
|
|
1843
|
+
focusHandoffRef: { current: string | null };
|
|
1844
|
+
focusScope: string;
|
|
1831
1845
|
frame: BrowserFrame | null;
|
|
1832
1846
|
connectionState: string;
|
|
1833
1847
|
supportsLiveFrames: boolean;
|
|
@@ -1872,12 +1886,26 @@ function BrowserViewport(props: {
|
|
|
1872
1886
|
const actionTailRef = useRef<Promise<void>>(Promise.resolve());
|
|
1873
1887
|
const actionQueueEpochRef = useRef(0);
|
|
1874
1888
|
const queuedFrameRef = useRef<BrowserFrame | null>(null);
|
|
1889
|
+
const currentFrameRef = useRef<BrowserFrame | null>(props.frame);
|
|
1890
|
+
const paintedFrameRef = useRef<BrowserFrame | null>(null);
|
|
1891
|
+
const [paintedFrame, setPaintedFrame] = useState<BrowserFrame | null>(null);
|
|
1875
1892
|
const decodingFrameRef = useRef(false);
|
|
1876
1893
|
const mountedRef = useRef(true);
|
|
1877
1894
|
actionRef.current = props.onAction;
|
|
1878
1895
|
readClipboardRef.current = props.onReadClipboard;
|
|
1879
1896
|
errorRef.current = props.onError;
|
|
1880
1897
|
|
|
1898
|
+
const clearBufferedInput = useCallback(() => {
|
|
1899
|
+
if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
|
|
1900
|
+
if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
|
|
1901
|
+
wheelRef.current = null;
|
|
1902
|
+
pendingTextRef.current = null;
|
|
1903
|
+
pointerStartRef.current = null;
|
|
1904
|
+
lastClickRef.current = null;
|
|
1905
|
+
composingRef.current = false;
|
|
1906
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
1907
|
+
}, []);
|
|
1908
|
+
|
|
1881
1909
|
const paintQueuedFrames = useCallback(() => {
|
|
1882
1910
|
if (decodingFrameRef.current) return;
|
|
1883
1911
|
decodingFrameRef.current = true;
|
|
@@ -1897,8 +1925,15 @@ function BrowserViewport(props: {
|
|
|
1897
1925
|
const bitmap = await createImageBitmap(blob);
|
|
1898
1926
|
try {
|
|
1899
1927
|
const canvas = canvasRef.current;
|
|
1900
|
-
if (
|
|
1928
|
+
if (
|
|
1929
|
+
mountedRef.current &&
|
|
1930
|
+
canvas &&
|
|
1931
|
+
currentFrameRef.current &&
|
|
1932
|
+
sameBrowserDocument(frame, currentFrameRef.current)
|
|
1933
|
+
) {
|
|
1901
1934
|
paintCanvas(canvas, bitmap, frame.width, frame.height);
|
|
1935
|
+
paintedFrameRef.current = frame;
|
|
1936
|
+
setPaintedFrame(frame);
|
|
1902
1937
|
}
|
|
1903
1938
|
} finally {
|
|
1904
1939
|
bitmap.close();
|
|
@@ -1908,11 +1943,23 @@ function BrowserViewport(props: {
|
|
|
1908
1943
|
objectUrl = URL.createObjectURL(blob);
|
|
1909
1944
|
const image = await loadImage(objectUrl);
|
|
1910
1945
|
const canvas = canvasRef.current;
|
|
1911
|
-
if (
|
|
1946
|
+
if (
|
|
1947
|
+
mountedRef.current &&
|
|
1948
|
+
canvas &&
|
|
1949
|
+
currentFrameRef.current &&
|
|
1950
|
+
sameBrowserDocument(frame, currentFrameRef.current)
|
|
1951
|
+
) {
|
|
1912
1952
|
paintCanvas(canvas, image, frame.width, frame.height);
|
|
1953
|
+
paintedFrameRef.current = frame;
|
|
1954
|
+
setPaintedFrame(frame);
|
|
1913
1955
|
}
|
|
1914
1956
|
} catch (cause) {
|
|
1915
|
-
if (
|
|
1957
|
+
if (
|
|
1958
|
+
mountedRef.current &&
|
|
1959
|
+
currentFrameRef.current &&
|
|
1960
|
+
sameBrowserDocument(frame, currentFrameRef.current)
|
|
1961
|
+
)
|
|
1962
|
+
errorRef.current(cause);
|
|
1916
1963
|
} finally {
|
|
1917
1964
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
1918
1965
|
}
|
|
@@ -1924,27 +1971,41 @@ function BrowserViewport(props: {
|
|
|
1924
1971
|
})();
|
|
1925
1972
|
}, []);
|
|
1926
1973
|
|
|
1927
|
-
|
|
1974
|
+
useLayoutEffect(() => {
|
|
1975
|
+
const keyboardInput = inputRef.current;
|
|
1976
|
+
const focusHandoff = props.focusHandoffRef;
|
|
1928
1977
|
mountedRef.current = true;
|
|
1978
|
+
if (focusHandoff.current === props.focusScope) {
|
|
1979
|
+
keyboardInput?.focus({ preventScroll: true });
|
|
1980
|
+
}
|
|
1981
|
+
focusHandoff.current = null;
|
|
1929
1982
|
return () => {
|
|
1983
|
+
// Only a same-target replacement may inherit focus the keyboard sink owned.
|
|
1984
|
+
if (document.activeElement === keyboardInput) {
|
|
1985
|
+
focusHandoff.current = props.focusScope;
|
|
1986
|
+
}
|
|
1930
1987
|
mountedRef.current = false;
|
|
1988
|
+
actionQueueEpochRef.current += 1;
|
|
1931
1989
|
queuedFrameRef.current = null;
|
|
1990
|
+
clearBufferedInput();
|
|
1932
1991
|
};
|
|
1933
|
-
}, []);
|
|
1992
|
+
}, [clearBufferedInput, props.focusHandoffRef, props.focusScope]);
|
|
1934
1993
|
|
|
1935
|
-
|
|
1936
|
-
if (!props.frame)
|
|
1994
|
+
useLayoutEffect(() => {
|
|
1995
|
+
if (currentFrameRef.current && !props.frame) {
|
|
1996
|
+
actionQueueEpochRef.current += 1;
|
|
1997
|
+
clearBufferedInput();
|
|
1998
|
+
}
|
|
1999
|
+
currentFrameRef.current = props.frame;
|
|
2000
|
+
if (!props.frame) {
|
|
2001
|
+
queuedFrameRef.current = null;
|
|
2002
|
+
paintedFrameRef.current = null;
|
|
2003
|
+
setPaintedFrame(null);
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
1937
2006
|
queuedFrameRef.current = props.frame;
|
|
1938
2007
|
paintQueuedFrames();
|
|
1939
|
-
}, [paintQueuedFrames, props.frame]);
|
|
1940
|
-
|
|
1941
|
-
useEffect(
|
|
1942
|
-
() => () => {
|
|
1943
|
-
if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
|
|
1944
|
-
if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
|
|
1945
|
-
},
|
|
1946
|
-
[],
|
|
1947
|
-
);
|
|
2008
|
+
}, [clearBufferedInput, paintQueuedFrames, props.frame]);
|
|
1948
2009
|
|
|
1949
2010
|
const enqueue = useCallback(
|
|
1950
2011
|
(
|
|
@@ -1953,20 +2014,24 @@ function BrowserViewport(props: {
|
|
|
1953
2014
|
after?: (receipt: BrowserActionReceipt) => Promise<void>,
|
|
1954
2015
|
) => {
|
|
1955
2016
|
const epoch = actionQueueEpochRef.current;
|
|
2017
|
+
const dispatch = actionRef.current;
|
|
1956
2018
|
actionTailRef.current = actionTailRef.current
|
|
1957
2019
|
.catch(() => undefined)
|
|
1958
2020
|
.then(async () => {
|
|
1959
|
-
if (epoch !== actionQueueEpochRef.current) return;
|
|
1960
|
-
const receipt = await
|
|
2021
|
+
if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
|
|
2022
|
+
const receipt = await dispatch(action, frame);
|
|
2023
|
+
if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
|
|
1961
2024
|
await after?.(receipt);
|
|
1962
2025
|
})
|
|
1963
2026
|
.catch((cause) => {
|
|
2027
|
+
if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
|
|
1964
2028
|
// Do not replay or continue input collected behind a failed request.
|
|
1965
2029
|
actionQueueEpochRef.current += 1;
|
|
2030
|
+
clearBufferedInput();
|
|
1966
2031
|
errorRef.current(cause);
|
|
1967
2032
|
});
|
|
1968
2033
|
},
|
|
1969
|
-
[],
|
|
2034
|
+
[clearBufferedInput],
|
|
1970
2035
|
);
|
|
1971
2036
|
|
|
1972
2037
|
const point = useCallback(
|
|
@@ -1987,19 +2052,39 @@ function BrowserViewport(props: {
|
|
|
1987
2052
|
lastClickRef.current = null;
|
|
1988
2053
|
}, []);
|
|
1989
2054
|
|
|
2055
|
+
const flushPendingWheel = useCallback(() => {
|
|
2056
|
+
const batch = wheelRef.current;
|
|
2057
|
+
if (!batch) return;
|
|
2058
|
+
if (batch.timer) clearTimeout(batch.timer);
|
|
2059
|
+
wheelRef.current = null;
|
|
2060
|
+
enqueue(
|
|
2061
|
+
{
|
|
2062
|
+
type: "pointer",
|
|
2063
|
+
action: "scroll",
|
|
2064
|
+
x: batch.x,
|
|
2065
|
+
y: batch.y,
|
|
2066
|
+
deltaX: batch.deltaX,
|
|
2067
|
+
deltaY: batch.deltaY,
|
|
2068
|
+
},
|
|
2069
|
+
batch.frame,
|
|
2070
|
+
);
|
|
2071
|
+
}, [enqueue]);
|
|
2072
|
+
|
|
1990
2073
|
const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
|
|
1991
|
-
|
|
2074
|
+
const frame = paintedFrameRef.current;
|
|
2075
|
+
if (!frame || event.button !== 0) return;
|
|
1992
2076
|
// Keep the hidden keyboard sink focused after the browser performs the
|
|
1993
2077
|
// pointerdown default action for the canvas. Without preventing that
|
|
1994
2078
|
// default, Chrome immediately moves focus back to the document and all
|
|
1995
2079
|
// subsequent typing/paste is silently lost.
|
|
1996
2080
|
event.preventDefault();
|
|
2081
|
+
flushPendingWheel();
|
|
1997
2082
|
flushPendingText();
|
|
1998
2083
|
pointerStartRef.current = {
|
|
1999
2084
|
x: event.clientX,
|
|
2000
2085
|
y: event.clientY,
|
|
2001
2086
|
pointerId: event.pointerId,
|
|
2002
|
-
frame
|
|
2087
|
+
frame,
|
|
2003
2088
|
};
|
|
2004
2089
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
2005
2090
|
inputRef.current?.focus({ preventScroll: true });
|
|
@@ -2048,8 +2133,9 @@ function BrowserViewport(props: {
|
|
|
2048
2133
|
|
|
2049
2134
|
const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
|
|
2050
2135
|
event.preventDefault();
|
|
2051
|
-
const frame =
|
|
2136
|
+
const frame = paintedFrameRef.current;
|
|
2052
2137
|
if (!frame) return;
|
|
2138
|
+
flushPendingWheel();
|
|
2053
2139
|
flushPendingText();
|
|
2054
2140
|
flushPendingClick();
|
|
2055
2141
|
const at = point(frame, event.clientX, event.clientY);
|
|
@@ -2057,41 +2143,35 @@ function BrowserViewport(props: {
|
|
|
2057
2143
|
};
|
|
2058
2144
|
|
|
2059
2145
|
const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
|
|
2060
|
-
const frame =
|
|
2146
|
+
const frame = paintedFrameRef.current;
|
|
2061
2147
|
if (!frame) return;
|
|
2062
2148
|
const at = point(frame, event.clientX, event.clientY);
|
|
2063
2149
|
if (!at) return;
|
|
2064
2150
|
flushPendingText();
|
|
2065
2151
|
flushPendingClick();
|
|
2066
2152
|
event.preventDefault();
|
|
2067
|
-
|
|
2068
|
-
if (
|
|
2153
|
+
let pending = wheelRef.current;
|
|
2154
|
+
if (
|
|
2155
|
+
pending &&
|
|
2156
|
+
(!sameBrowserDocument(pending.frame, frame) ||
|
|
2157
|
+
Math.hypot(pending.x - at.x, pending.y - at.y) > 6)
|
|
2158
|
+
) {
|
|
2159
|
+
flushPendingWheel();
|
|
2160
|
+
pending = null;
|
|
2161
|
+
}
|
|
2069
2162
|
wheelRef.current = {
|
|
2070
2163
|
x: at.x,
|
|
2071
2164
|
y: at.y,
|
|
2072
|
-
deltaX: (pending
|
|
2073
|
-
deltaY: (pending
|
|
2165
|
+
deltaX: (pending?.deltaX ?? 0) + event.deltaX,
|
|
2166
|
+
deltaY: (pending?.deltaY ?? 0) + event.deltaY,
|
|
2074
2167
|
frame,
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
wheelRef.current = null;
|
|
2078
|
-
if (batch)
|
|
2079
|
-
enqueue(
|
|
2080
|
-
{
|
|
2081
|
-
type: "pointer",
|
|
2082
|
-
action: "scroll",
|
|
2083
|
-
x: batch.x,
|
|
2084
|
-
y: batch.y,
|
|
2085
|
-
deltaX: batch.deltaX,
|
|
2086
|
-
deltaY: batch.deltaY,
|
|
2087
|
-
},
|
|
2088
|
-
batch.frame,
|
|
2089
|
-
);
|
|
2090
|
-
}, 45),
|
|
2168
|
+
// Retain the first event's deadline so a continuous gesture keeps moving.
|
|
2169
|
+
timer: pending?.timer ?? setTimeout(flushPendingWheel, 45),
|
|
2091
2170
|
};
|
|
2092
2171
|
};
|
|
2093
2172
|
|
|
2094
2173
|
const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
2174
|
+
flushPendingWheel();
|
|
2095
2175
|
const command = event.metaKey || event.ctrlKey;
|
|
2096
2176
|
if (
|
|
2097
2177
|
props.clipboardEnabled &&
|
|
@@ -2107,16 +2187,19 @@ function BrowserViewport(props: {
|
|
|
2107
2187
|
event.preventDefault();
|
|
2108
2188
|
flushPendingText();
|
|
2109
2189
|
flushPendingClick();
|
|
2110
|
-
enqueue({ type: "press", key },
|
|
2190
|
+
enqueue({ type: "press", key }, paintedFrameRef.current);
|
|
2111
2191
|
};
|
|
2112
2192
|
|
|
2113
2193
|
const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
|
|
2114
2194
|
if (!props.clipboardEnabled) return;
|
|
2115
2195
|
event.preventDefault();
|
|
2196
|
+
flushPendingWheel();
|
|
2116
2197
|
flushPendingText();
|
|
2117
2198
|
flushPendingClick();
|
|
2118
|
-
|
|
2119
|
-
|
|
2199
|
+
const readClipboard = readClipboardRef.current;
|
|
2200
|
+
enqueue({ type: "clipboard", operation: "copy" }, paintedFrameRef.current, async () => {
|
|
2201
|
+
const clipboard = await readClipboard();
|
|
2202
|
+
if (!mountedRef.current) return;
|
|
2120
2203
|
if (clipboard.text.length === 0) return;
|
|
2121
2204
|
if (!(await copyTextToClipboard(clipboard.text))) {
|
|
2122
2205
|
throw new Error("Browser text could not be copied to the local clipboard");
|
|
@@ -2127,6 +2210,7 @@ function BrowserViewport(props: {
|
|
|
2127
2210
|
const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
|
|
2128
2211
|
if (!props.clipboardEnabled) return;
|
|
2129
2212
|
event.preventDefault();
|
|
2213
|
+
flushPendingWheel();
|
|
2130
2214
|
flushPendingText();
|
|
2131
2215
|
flushPendingClick();
|
|
2132
2216
|
enqueue(
|
|
@@ -2135,16 +2219,17 @@ function BrowserViewport(props: {
|
|
|
2135
2219
|
operation: "paste",
|
|
2136
2220
|
text: event.clipboardData.getData("text/plain"),
|
|
2137
2221
|
},
|
|
2138
|
-
|
|
2222
|
+
paintedFrameRef.current,
|
|
2139
2223
|
);
|
|
2140
2224
|
};
|
|
2141
2225
|
|
|
2142
2226
|
const input = (value: string, nativeComposing = false) => {
|
|
2143
2227
|
if (composingRef.current || nativeComposing) return;
|
|
2144
2228
|
if (!value) return;
|
|
2229
|
+
flushPendingWheel();
|
|
2145
2230
|
flushPendingClick();
|
|
2146
2231
|
const pending = pendingTextRef.current;
|
|
2147
|
-
if (pending && sameOptionalBrowserFrame(pending.frame,
|
|
2232
|
+
if (pending && sameOptionalBrowserFrame(pending.frame, paintedFrameRef.current)) {
|
|
2148
2233
|
clearTimeout(pending.timer);
|
|
2149
2234
|
pending.text += value;
|
|
2150
2235
|
pending.timer = setTimeout(flushPendingText, 16);
|
|
@@ -2152,7 +2237,7 @@ function BrowserViewport(props: {
|
|
|
2152
2237
|
flushPendingText();
|
|
2153
2238
|
pendingTextRef.current = {
|
|
2154
2239
|
text: value,
|
|
2155
|
-
frame:
|
|
2240
|
+
frame: paintedFrameRef.current,
|
|
2156
2241
|
timer: setTimeout(flushPendingText, 16),
|
|
2157
2242
|
};
|
|
2158
2243
|
}
|
|
@@ -2168,7 +2253,8 @@ function BrowserViewport(props: {
|
|
|
2168
2253
|
input(event.currentTarget.value || event.data);
|
|
2169
2254
|
};
|
|
2170
2255
|
|
|
2171
|
-
const showCanvas =
|
|
2256
|
+
const showCanvas =
|
|
2257
|
+
props.frame !== null && paintedFrame !== null && sameBrowserDocument(props.frame, paintedFrame);
|
|
2172
2258
|
return (
|
|
2173
2259
|
<div className="relative min-h-0 flex-1 overflow-hidden bg-black">
|
|
2174
2260
|
<canvas
|
|
@@ -2916,13 +3002,16 @@ function frameMatchesSelectedTarget(
|
|
|
2916
3002
|
}
|
|
2917
3003
|
|
|
2918
3004
|
function sameFrameFence(left: BrowserFrame, right: BrowserFrame): boolean {
|
|
3005
|
+
return sameBrowserDocument(left, right) && left.frameId === right.frameId;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
function sameBrowserDocument(left: BrowserFrame, right: BrowserFrame): boolean {
|
|
2919
3009
|
return (
|
|
2920
3010
|
left.browserSessionId === right.browserSessionId &&
|
|
2921
3011
|
left.controllerGeneration === right.controllerGeneration &&
|
|
2922
3012
|
left.targetId === right.targetId &&
|
|
2923
3013
|
left.targetGeneration === right.targetGeneration &&
|
|
2924
|
-
left.documentGeneration === right.documentGeneration
|
|
2925
|
-
left.frameId === right.frameId
|
|
3014
|
+
left.documentGeneration === right.documentGeneration
|
|
2926
3015
|
);
|
|
2927
3016
|
}
|
|
2928
3017
|
|
|
@@ -248,6 +248,7 @@ export function ComposerTranscriptionControl({
|
|
|
248
248
|
>
|
|
249
249
|
<button
|
|
250
250
|
type="button"
|
|
251
|
+
disabled={composer.disabled && !retrying}
|
|
251
252
|
onClick={() =>
|
|
252
253
|
savedTranscript
|
|
253
254
|
? void transcription.insertSavedTranscript()
|
|
@@ -265,7 +266,7 @@ export function ComposerTranscriptionControl({
|
|
|
265
266
|
className={cn(
|
|
266
267
|
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
267
268
|
"bg-og-fg text-og-bg transition-colors duration-150 motion-reduce:transition-none",
|
|
268
|
-
"hover:bg-og-fg-muted pointer-coarse:size-11",
|
|
269
|
+
"hover:bg-og-fg-muted disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-11",
|
|
269
270
|
)}
|
|
270
271
|
>
|
|
271
272
|
{savedTranscript ? (
|
|
@@ -280,12 +281,13 @@ export function ComposerTranscriptionControl({
|
|
|
280
281
|
<Tip tip={messages.discardRecovered}>
|
|
281
282
|
<button
|
|
282
283
|
type="button"
|
|
284
|
+
disabled={composer.disabled}
|
|
283
285
|
onClick={() => void transcription.discard()}
|
|
284
286
|
aria-label={messages.discardRecovered}
|
|
285
287
|
className={cn(
|
|
286
288
|
"inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
|
|
287
289
|
"text-og-fg-muted transition-colors duration-150 motion-reduce:transition-none",
|
|
288
|
-
"hover:bg-og-surface-3 hover:text-og-status-failed pointer-coarse:size-11",
|
|
290
|
+
"hover:bg-og-surface-3 hover:text-og-status-failed disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-11",
|
|
289
291
|
)}
|
|
290
292
|
>
|
|
291
293
|
<Trash2Icon className="size-3.5" />
|
|
@@ -457,9 +457,9 @@ export function useChatComposerController({
|
|
|
457
457
|
(event: DragEvent<HTMLDivElement>) => {
|
|
458
458
|
if (!attachments || !dragCarriesFiles(event)) return;
|
|
459
459
|
event.preventDefault();
|
|
460
|
-
setDragging(true);
|
|
460
|
+
if (!disabled) setDragging(true);
|
|
461
461
|
},
|
|
462
|
-
[attachments],
|
|
462
|
+
[attachments, disabled],
|
|
463
463
|
);
|
|
464
464
|
const handleDragLeave = useCallback(
|
|
465
465
|
(event: DragEvent<HTMLDivElement>) => {
|
|
@@ -474,11 +474,16 @@ export function useChatComposerController({
|
|
|
474
474
|
if (!attachments || !dragCarriesFiles(event)) return;
|
|
475
475
|
event.preventDefault();
|
|
476
476
|
setDragging(false);
|
|
477
|
-
if (event.dataTransfer.files.length > 0)
|
|
477
|
+
if (!disabled && event.dataTransfer.files.length > 0)
|
|
478
|
+
attachments.addFiles(event.dataTransfer.files);
|
|
478
479
|
},
|
|
479
|
-
[attachments],
|
|
480
|
+
[attachments, disabled],
|
|
480
481
|
);
|
|
481
482
|
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
if (disabled) setDragging(false);
|
|
485
|
+
}, [disabled]);
|
|
486
|
+
|
|
482
487
|
const [notice, setNotice] = useState<Notice | null>(null);
|
|
483
488
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
484
489
|
const [confirmState, setConfirmState] = useState<ConfirmState>(null);
|
|
@@ -624,17 +629,18 @@ export function useChatComposerController({
|
|
|
624
629
|
);
|
|
625
630
|
const handlePaste = useCallback(
|
|
626
631
|
(event: ClipboardEvent<HTMLTextAreaElement>) => {
|
|
632
|
+
if (disabled) return;
|
|
627
633
|
onPaste?.(event);
|
|
628
634
|
attachments?.addFromPaste(event);
|
|
629
635
|
},
|
|
630
|
-
[attachments, onPaste],
|
|
636
|
+
[attachments, disabled, onPaste],
|
|
631
637
|
);
|
|
632
638
|
const handleFileChange = useCallback(
|
|
633
639
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
634
|
-
if (event.target.files) attachments?.addFiles(event.target.files);
|
|
640
|
+
if (!disabled && event.target.files) attachments?.addFiles(event.target.files);
|
|
635
641
|
event.target.value = "";
|
|
636
642
|
},
|
|
637
|
-
[attachments],
|
|
643
|
+
[attachments, disabled],
|
|
638
644
|
);
|
|
639
645
|
|
|
640
646
|
const helpCommands = useMemo(
|
|
@@ -975,6 +981,7 @@ export function Attachments() {
|
|
|
975
981
|
<AttachmentChips
|
|
976
982
|
attachments={controller.attachments.attachments}
|
|
977
983
|
messages={controller.messages}
|
|
984
|
+
disabled={controller.disabled}
|
|
978
985
|
onRemove={controller.attachments.remove}
|
|
979
986
|
onRetry={controller.attachments.retry}
|
|
980
987
|
onRetainPreview={controller.attachments.retainPreview}
|
|
@@ -996,6 +1003,26 @@ type OwnedInputProps =
|
|
|
996
1003
|
| "aria-keyshortcuts";
|
|
997
1004
|
export type ComposerInputProps = Omit<ComponentPropsWithoutRef<"textarea">, OwnedInputProps>;
|
|
998
1005
|
|
|
1006
|
+
const EDITING_FOCUS_SELECTOR =
|
|
1007
|
+
'input, textarea, select, [contenteditable="true"], [role="textbox"]';
|
|
1008
|
+
const POPUP_FOCUS_SELECTOR =
|
|
1009
|
+
'[role="menu"], [role="menubar"], [role="listbox"], [role="dialog"], [role="alertdialog"]';
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Initial focus is a convenience, not authority to move focus the person has
|
|
1013
|
+
* placed elsewhere. It yields to a field they started editing and to an open
|
|
1014
|
+
* menu, listbox, or dialog that does not contain the composer: moving focus
|
|
1015
|
+
* behind a non-modal popup dismisses it, and a modal surface owns focus until
|
|
1016
|
+
* it closes.
|
|
1017
|
+
*/
|
|
1018
|
+
function focusBelongsElsewhere(textarea: HTMLTextAreaElement): boolean {
|
|
1019
|
+
const active = textarea.ownerDocument.activeElement;
|
|
1020
|
+
if (!active || active === textarea) return false;
|
|
1021
|
+
if (active.closest(EDITING_FOCUS_SELECTOR)) return true;
|
|
1022
|
+
const popup = active.closest(POPUP_FOCUS_SELECTOR);
|
|
1023
|
+
return popup !== null && !popup.contains(textarea);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
999
1026
|
export const Input = forwardRef<HTMLTextAreaElement, ComposerInputProps>(function ComposerInput(
|
|
1000
1027
|
{ rows = 1, placeholder, className, "aria-label": ariaLabel, autoFocus = false, ...props },
|
|
1001
1028
|
forwardedRef,
|
|
@@ -1005,22 +1032,17 @@ export const Input = forwardRef<HTMLTextAreaElement, ComposerInputProps>(functio
|
|
|
1005
1032
|
const paletteOpen =
|
|
1006
1033
|
controller.paletteEnabled && controller.paletteMounted && controller.palette.open;
|
|
1007
1034
|
|
|
1008
|
-
//
|
|
1009
|
-
// hydrate)
|
|
1035
|
+
// Focus once the controller is interactive. The textarea can mount disabled
|
|
1036
|
+
// (create-session draft hydrate), and the route can finish loading after the
|
|
1037
|
+
// person has already moved on, so this deliberately does not use the native
|
|
1038
|
+
// autoFocus attribute: that would take focus at mount without the guard.
|
|
1010
1039
|
useEffect(() => {
|
|
1011
1040
|
if (!autoFocus || controller.disabled || autoFocusedRef.current) return;
|
|
1012
1041
|
const frame = window.requestAnimationFrame(() => {
|
|
1013
1042
|
const textarea = controller.textareaRef.current;
|
|
1014
1043
|
if (!textarea || textarea.disabled) return;
|
|
1015
1044
|
autoFocusedRef.current = true;
|
|
1016
|
-
|
|
1017
|
-
// field. Initial focus is a convenience, not authority to take their caret.
|
|
1018
|
-
const active = textarea.ownerDocument.activeElement;
|
|
1019
|
-
if (
|
|
1020
|
-
active !== textarea &&
|
|
1021
|
-
active?.closest('input, textarea, select, [contenteditable="true"], [role="textbox"]')
|
|
1022
|
-
)
|
|
1023
|
-
return;
|
|
1045
|
+
if (focusBelongsElsewhere(textarea)) return;
|
|
1024
1046
|
textarea.focus();
|
|
1025
1047
|
});
|
|
1026
1048
|
return () => window.cancelAnimationFrame(frame);
|
|
@@ -1041,7 +1063,6 @@ export const Input = forwardRef<HTMLTextAreaElement, ComposerInputProps>(functio
|
|
|
1041
1063
|
: (placeholder ?? controller.messages.messagePlaceholder)
|
|
1042
1064
|
}
|
|
1043
1065
|
disabled={controller.disabled}
|
|
1044
|
-
autoFocus={autoFocus && !controller.disabled}
|
|
1045
1066
|
aria-label={ariaLabel ?? controller.messages.inputLabel}
|
|
1046
1067
|
aria-keyshortcuts="Enter Meta+Enter Control+Enter Shift+Enter"
|
|
1047
1068
|
aria-autocomplete={
|
|
@@ -1679,6 +1700,7 @@ function ConfirmBar({
|
|
|
1679
1700
|
function AttachmentChips({
|
|
1680
1701
|
attachments,
|
|
1681
1702
|
messages,
|
|
1703
|
+
disabled,
|
|
1682
1704
|
onRemove,
|
|
1683
1705
|
onRetry,
|
|
1684
1706
|
onRetainPreview,
|
|
@@ -1686,6 +1708,7 @@ function AttachmentChips({
|
|
|
1686
1708
|
}: {
|
|
1687
1709
|
attachments: UseFileAttachmentsResult["attachments"];
|
|
1688
1710
|
messages: ChatComposerMessages;
|
|
1711
|
+
disabled: boolean;
|
|
1689
1712
|
onRemove: (id: string) => void;
|
|
1690
1713
|
onRetry?: ((id: string) => void) | undefined;
|
|
1691
1714
|
onRetainPreview: UseFileAttachmentsResult["retainPreview"];
|
|
@@ -1837,7 +1860,8 @@ function AttachmentChips({
|
|
|
1837
1860
|
<button
|
|
1838
1861
|
type="button"
|
|
1839
1862
|
onClick={() => onRetry(attachment.id)}
|
|
1840
|
-
|
|
1863
|
+
disabled={disabled}
|
|
1864
|
+
className="shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-10"
|
|
1841
1865
|
aria-label={messages.retryAttachment(attachment.name)}
|
|
1842
1866
|
>
|
|
1843
1867
|
<RotateCwIcon className="size-3.5" />
|
|
@@ -1847,7 +1871,8 @@ function AttachmentChips({
|
|
|
1847
1871
|
<button
|
|
1848
1872
|
type="button"
|
|
1849
1873
|
onClick={() => onRemove(attachment.id)}
|
|
1850
|
-
|
|
1874
|
+
disabled={disabled}
|
|
1875
|
+
className="shrink-0 rounded-og-xs p-1 text-og-fg-muted hover:bg-og-surface-1 hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:size-10"
|
|
1851
1876
|
aria-label={messages.removeAttachment(attachment.name)}
|
|
1852
1877
|
>
|
|
1853
1878
|
<XIcon className="size-3.5" />
|