@omercnet/paseo-shared-browser 1.2.2 → 2.0.0-next.249.1

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 (173) hide show
  1. package/OVERVIEW.md +2 -2
  2. package/README.md +316 -27
  3. package/REMOTE_BROWSER_RESEARCH.md +395 -0
  4. package/STREAMING_DESIGN.md +196 -0
  5. package/client/browser-auto-capture-density.test.ts +102 -0
  6. package/client/browser-auto-capture-density.ts +30 -0
  7. package/client/browser-canvas-input.test.ts +198 -0
  8. package/client/browser-canvas-input.ts +202 -0
  9. package/client/browser-canvas-keyboard-web.test.ts +113 -0
  10. package/client/browser-canvas-keyboard-web.ts +230 -0
  11. package/client/browser-canvas-keyboard.test.ts +123 -0
  12. package/client/browser-canvas-keyboard.ts +126 -0
  13. package/client/browser-canvas-layout.test.ts +109 -0
  14. package/client/browser-canvas-layout.ts +51 -0
  15. package/client/browser-canvas-viewport-web.test.ts +77 -0
  16. package/client/browser-canvas-viewport-web.ts +63 -0
  17. package/client/browser-canvas-viewport.tsx +152 -0
  18. package/client/browser-capture-cadence.test.ts +9 -0
  19. package/client/browser-capture-cadence.ts +6 -0
  20. package/client/browser-capture-density-controls.tsx +75 -0
  21. package/client/browser-chrome.tsx +234 -0
  22. package/client/browser-client-presence.ts +184 -0
  23. package/client/browser-compose-controls.test.tsx +275 -0
  24. package/client/browser-compose-controls.tsx +174 -0
  25. package/client/browser-control-button.tsx +109 -0
  26. package/client/browser-control-dialog.tsx +71 -0
  27. package/client/browser-control-request.test.ts +27 -0
  28. package/client/browser-control-request.ts +57 -0
  29. package/client/browser-control-tooltip-web.ts +10 -0
  30. package/client/browser-dialog-body.tsx +22 -0
  31. package/client/browser-emulation-mode.test.ts +75 -0
  32. package/client/browser-emulation-mode.ts +98 -0
  33. package/client/browser-fill-viewport.test.ts +45 -0
  34. package/client/browser-fill-viewport.ts +28 -0
  35. package/client/browser-frame-buffer.test.ts +157 -0
  36. package/client/browser-frame-buffer.ts +148 -0
  37. package/client/browser-frame-image.tsx +62 -0
  38. package/client/browser-image-load.test.ts +23 -0
  39. package/client/browser-image-load.ts +26 -0
  40. package/client/browser-input-queue.test.ts +1254 -0
  41. package/client/browser-input-queue.ts +519 -0
  42. package/client/browser-menu-placement.test.ts +90 -0
  43. package/client/browser-menu-placement.ts +78 -0
  44. package/client/browser-panel-styles.ts +384 -0
  45. package/client/browser-quality-controls.test.ts +26 -0
  46. package/client/browser-quality-controls.tsx +139 -0
  47. package/client/browser-resolution-picker.tsx +189 -0
  48. package/client/browser-tabs.tsx +117 -0
  49. package/client/browser-toolbar-menu-web.test.ts +185 -0
  50. package/client/browser-toolbar-menu-web.ts +159 -0
  51. package/client/browser-toolbar-menu.tsx +255 -0
  52. package/client/browser-toolbar-submenu.tsx +93 -0
  53. package/client/browser-video-bytes.test.ts +44 -0
  54. package/client/browser-video-currency.ts +45 -0
  55. package/client/browser-video-decoder.test.ts +473 -0
  56. package/client/browser-video-decoder.ts +311 -0
  57. package/client/browser-video-surface.tsx +21 -0
  58. package/client/browser-video-visibility.test.ts +45 -0
  59. package/client/browser-viewer-recovery.test.ts +111 -0
  60. package/client/browser-viewer-recovery.ts +78 -0
  61. package/client/browser.tsx +1650 -1579
  62. package/client/favorite-star.tsx +23 -0
  63. package/client/frame-lifecycle.ts +62 -59
  64. package/client/sidebar.tsx +1 -2
  65. package/client/use-browser-auto-capture-density.test.ts +104 -0
  66. package/client/use-browser-auto-capture-density.ts +43 -0
  67. package/client/use-browser-canvas-input.ts +375 -0
  68. package/client/use-browser-capture-density.ts +87 -0
  69. package/client/use-browser-emulation-mode.ts +56 -0
  70. package/client/use-browser-fill-viewport.test.ts +108 -0
  71. package/client/use-browser-fill-viewport.ts +62 -0
  72. package/client/use-browser-frame-buffer.ts +66 -0
  73. package/client/use-browser-image-capture.ts +139 -0
  74. package/client/use-browser-scoped-mutation.test.ts +118 -0
  75. package/client/use-browser-scoped-mutation.ts +59 -0
  76. package/client/use-browser-video.test.ts +1060 -0
  77. package/client/use-browser-video.ts +481 -0
  78. package/client/use-browser-viewer-recovery.ts +26 -0
  79. package/client/use-resolution-favorites.ts +90 -0
  80. package/client/web.test.ts +125 -0
  81. package/client/web.ts +405 -0
  82. package/index.client.tsx +0 -10
  83. package/index.server.ts +32 -4
  84. package/npm-shrinkwrap.json +9 -9
  85. package/package.json +10 -5
  86. package/paseo-plugin.json +1 -1
  87. package/scripts/prepare-runtime.mjs +1 -1
  88. package/server/agent-browser-runtime.ts +1601 -314
  89. package/server/browser-cursor.test.ts +64 -0
  90. package/server/browser-cursor.ts +69 -0
  91. package/server/browser-frame-context.ts +22 -0
  92. package/server/browser-frame-epoch.test.ts +95 -0
  93. package/server/browser-frame-rejection.ts +17 -0
  94. package/server/browser-gesture.ts +120 -0
  95. package/server/browser-operation-diagnostics.test.ts +79 -0
  96. package/server/browser-operation-diagnostics.ts +103 -0
  97. package/server/browser-policy.ts +1316 -151
  98. package/server/browser-video-input-authority.test.ts +211 -0
  99. package/server/browser-video-policy.test.ts +564 -0
  100. package/server/browser-video-socket.test.ts +368 -0
  101. package/server/browser.ts +151 -16
  102. package/server/capture-transport-policy.test.ts +105 -0
  103. package/server/capture-transport-policy.ts +154 -0
  104. package/server/capture-transport-runtime.test.ts +213 -0
  105. package/server/daemon-launch-chain.test.ts +70 -0
  106. package/server/daemon-shutdown.test.ts +289 -0
  107. package/server/discrete-input-freshness.test.ts +364 -0
  108. package/server/discrete-input-native-lifetime.test.ts +150 -0
  109. package/server/display-loss-recovery.test.ts +294 -0
  110. package/server/emulation-restoration.test.ts +262 -0
  111. package/server/input-cleanup.ts +23 -0
  112. package/server/input-generation.test.ts +31 -0
  113. package/server/input-generation.ts +36 -0
  114. package/server/jpeg-capture-demand.test.ts +76 -0
  115. package/server/jpeg-capture-demand.ts +98 -0
  116. package/server/jpeg-frame.ts +29 -0
  117. package/server/keyboard-input.test.ts +61 -0
  118. package/server/keyboard-input.ts +104 -0
  119. package/server/live-input-policy.test.ts +958 -0
  120. package/server/live-input-runtime.test.ts +732 -0
  121. package/server/live-input-supervisor.test.ts +147 -0
  122. package/server/mcp-entry.ts +44 -2
  123. package/server/native-video-backpressure.test.ts +228 -0
  124. package/server/native-video-buffer.test.ts +141 -0
  125. package/server/native-video-buffer.ts +114 -0
  126. package/server/native-video-capture.test.ts +599 -0
  127. package/server/native-video-capture.ts +497 -0
  128. package/server/native-video-extension-source.test.ts +201 -0
  129. package/server/native-video-extension-source.ts +364 -0
  130. package/server/native-video-extension.test.ts +41 -0
  131. package/server/native-video-extension.ts +83 -0
  132. package/server/native-video-packet.ts +9 -0
  133. package/server/native-video-settings.test.ts +245 -0
  134. package/server/navigation-budget.ts +8 -0
  135. package/server/navigation-commit.ts +125 -0
  136. package/server/navigation-policy.test.ts +153 -0
  137. package/server/process-identity.ts +58 -0
  138. package/server/reattach-held-input.test.ts +143 -0
  139. package/server/runtime-frame-reuse.test.ts +295 -0
  140. package/server/runtime-jpeg-demand.test.ts +200 -0
  141. package/server/runtime-jpeg-shutdown.test.ts +210 -0
  142. package/server/runtime-jpeg-source-order.test.ts +201 -0
  143. package/server/runtime-launch-environment.test.ts +111 -0
  144. package/server/runtime-navigation.test.ts +327 -0
  145. package/server/runtime-owner-display.test.ts +152 -0
  146. package/server/runtime-owner.ts +340 -31
  147. package/server/runtime-page-state.test.ts +249 -0
  148. package/server/runtime-protocol.ts +32 -2
  149. package/server/runtime-target-retention.test.ts +181 -0
  150. package/server/runtime-video-opt-in.test.ts +82 -0
  151. package/server/runtime-video.test.ts +444 -0
  152. package/server/screencast-source-time.ts +46 -0
  153. package/server/stale-wheel-admission.test.ts +136 -0
  154. package/server/supervisor-client.ts +28 -4
  155. package/server/supervisor-heartbeat.test.ts +80 -0
  156. package/server/supervisor.ts +553 -55
  157. package/server/tab-close-confirmation.test.ts +97 -0
  158. package/server/video-source-recovery.test.ts +20 -0
  159. package/server/video-source-recovery.ts +64 -0
  160. package/server/viewer-capture-lifetime.ts +51 -0
  161. package/server/virtual-display.test.ts +184 -0
  162. package/server/virtual-display.ts +236 -0
  163. package/shared/browser-display-preferences.ts +46 -0
  164. package/shared/browser-video.ts +83 -0
  165. package/shared/browser.ts +286 -65
  166. package/shared/capture-density.ts +16 -0
  167. package/shared/capture-settings.ts +17 -0
  168. package/shared/device-presets.ts +90 -0
  169. package/shared/resolution-menu.test.ts +120 -0
  170. package/shared/resolution-menu.ts +72 -0
  171. package/shared/video-settings.test.ts +38 -0
  172. package/shared/video-settings.ts +34 -0
  173. package/shared/viewport-limits.ts +5 -0
@@ -1,26 +1,105 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { AgentBrowserRuntime, type BrowserViewport } from "./agent-browser-runtime";
4
+ import { browserGestureKeySchema, MAX_BROWSER_TABS } from "../shared/browser";
5
+ import { videoBitrateSchema, videoFrameRateSchema } from "../shared/video-settings";
6
+ import {
7
+ AgentBrowserRuntime,
8
+ type AgentBrowserRuntimeOptions,
9
+ type BrowserViewport,
10
+ type RuntimeTarget,
11
+ } from "./agent-browser-runtime";
12
+ import { CdpUnknownOutcomeError } from "./cdp";
5
13
  import { resolveBrowserRuntimeRoot } from "./runtime-path";
6
14
  import type { JsonValue } from "./runtime-protocol";
7
15
  import type { RuntimeOwner } from "./supervisor";
16
+ import {
17
+ createPrivateVirtualDisplay,
18
+ PrivateDisplayError,
19
+ type PrivateVirtualDisplay,
20
+ } from "./virtual-display";
8
21
 
9
22
  const DEFAULT_BROWSER_URL = "https://example.com/";
10
23
 
11
24
  interface OwnedRuntime {
12
25
  runtimeId: string;
13
26
  runtime: AgentBrowserRuntime;
27
+ rootTargetId: string;
28
+ runtimeOptions: AgentBrowserRuntimeOptions;
29
+ tabs: Map<string, Promise<AgentBrowserRuntime>>;
30
+ tabOrder: string[];
31
+ display: PrivateVirtualDisplay | null;
32
+ /** Bounded, secret-free reason an opted-in private display is not in use. */
33
+ displayNotice: string | null;
14
34
  }
15
35
  interface RuntimeOwnerOptions {
16
36
  initialUrl?: string;
17
37
  headed?: boolean;
38
+ /**
39
+ * Trusted opt-in for a private Linux Xvfb display. When omitted, each runtime creation reads
40
+ * PASEO_SHARED_BROWSER_XVFB from this process's environment and enables it only for "1".
41
+ * Explicit false overrides the environment.
42
+ */
43
+ virtualDisplay?: boolean;
44
+ /** Encoded video is opt-in; defaults to PASEO_SHARED_BROWSER_VIDEO=1 in the daemon environment. */
45
+ nativeVideo?: boolean;
18
46
  }
19
47
 
20
48
  function paseoHome(): string {
21
49
  return process.env.PASEO_HOME ?? join(homedir(), ".paseo");
22
50
  }
23
51
 
52
+ /** Retain omission for legacy/native held-key inference; reject malformed explicit masks. */
53
+ function optionalMouseModifiers(data: Record<string, JsonValue>): [] | [number] {
54
+ return data.modifiers === undefined
55
+ ? []
56
+ : [browserGestureKeySchema.shape.modifiers.parse(data.modifiers)];
57
+ }
58
+
59
+ /** Retain known tab positions across unordered CDP snapshots; append newly discovered pages. */
60
+ async function orderedTargets(owned: OwnedRuntime): Promise<RuntimeTarget[]> {
61
+ const targets = await owned.runtime.targets();
62
+ const byId = new Map(targets.map((target) => [target.targetId, target]));
63
+ const surviving = owned.tabOrder.filter((targetId) => byId.has(targetId));
64
+ const known = new Set(surviving);
65
+ for (const target of targets) {
66
+ if (!known.has(target.targetId)) {
67
+ surviving.push(target.targetId);
68
+ known.add(target.targetId);
69
+ }
70
+ }
71
+ owned.tabOrder = surviving;
72
+ return surviving.map((targetId) => byId.get(targetId)!);
73
+ }
74
+
75
+ /** Give each page its own CDP attachment and media state within one Chromium profile. */
76
+ async function tabRuntime(owned: OwnedRuntime, targetId: string): Promise<AgentBrowserRuntime> {
77
+ const existing = owned.tabs.get(targetId);
78
+ if (existing) return existing;
79
+
80
+ const attaching = (async () => {
81
+ const targets = await owned.runtime.targets();
82
+ if (!targets.some((target) => target.targetId === targetId)) {
83
+ throw new Error("Browser tab is no longer available");
84
+ }
85
+ const runtime = new AgentBrowserRuntime(owned.runtimeOptions);
86
+ try {
87
+ await runtime.attachTarget(targetId);
88
+ return runtime;
89
+ } catch (error) {
90
+ await runtime.shutdown().catch(() => undefined);
91
+ throw error;
92
+ }
93
+ })();
94
+ owned.tabs.set(targetId, attaching);
95
+ try {
96
+ return await attaching;
97
+ } catch (error) {
98
+ if (owned.tabs.get(targetId) === attaching) owned.tabs.delete(targetId);
99
+ throw error;
100
+ }
101
+ }
102
+
24
103
  export async function createRuntimeOwner(
25
104
  options: RuntimeOwnerOptions = {},
26
105
  ): Promise<RuntimeOwner<OwnedRuntime>> {
@@ -48,97 +127,327 @@ export async function createRuntimeOwner(
48
127
  return {
49
128
  async create(workspaceId) {
50
129
  const hash = createHash("sha256").update(workspaceId).digest("hex");
51
- const runtime = new AgentBrowserRuntime({
52
- binaryPath,
53
- executablePath,
54
- profilePath: join(root, "profiles", hash),
55
- ipcDirectory,
56
- session: `ws-${hash.slice(0, 16)}`,
57
- initialUrl: options.initialUrl ?? DEFAULT_BROWSER_URL,
58
- headed: options.headed ?? false,
59
- });
60
- await runtime.launch();
61
- return { runtimeId: randomUUID(), runtime };
130
+ // Explicit headed mode uses the caller's real display. An opted-in hidden Linux mode
131
+ // gets a private virtual mouse so native pointer media survive emulation.
132
+ let display: PrivateVirtualDisplay | null = null;
133
+ let displayNotice: string | null = null;
134
+ const wantsDisplay = options.virtualDisplay ?? process.env.PASEO_SHARED_BROWSER_XVFB === "1";
135
+ if (!options.headed && wantsDisplay) {
136
+ try {
137
+ display = await createPrivateVirtualDisplay();
138
+ if (!display) displayNotice = "Private display unavailable: Xvfb was not found.";
139
+ } catch (error) {
140
+ // The helper has finished cleanup and no Chromium was constructed yet.
141
+ displayNotice = `Private display unavailable: ${
142
+ error instanceof PrivateDisplayError ? error.message : "unexpected startup failure"
143
+ }.`;
144
+ }
145
+ if (displayNotice) {
146
+ // The detached supervisor discards stdio; the notice also reaches the viewer state.
147
+ console.warn(`[shared-browser] ${displayNotice} Using headless mode.`);
148
+ }
149
+ }
150
+ let runtime: AgentBrowserRuntime | null = null;
151
+ try {
152
+ const runtimeOptions: AgentBrowserRuntimeOptions = {
153
+ binaryPath,
154
+ executablePath,
155
+ profilePath: join(root, "profiles", hash),
156
+ ipcDirectory,
157
+ session: `ws-${hash.slice(0, 16)}`,
158
+ initialUrl: options.initialUrl ?? DEFAULT_BROWSER_URL,
159
+ headed: display ? true : (options.headed ?? false),
160
+ nativeVideo: options.nativeVideo ?? process.env.PASEO_SHARED_BROWSER_VIDEO === "1",
161
+ ...(display ? { launchEnvironment: display.launchEnvironment } : {}),
162
+ };
163
+ runtime = new AgentBrowserRuntime(runtimeOptions);
164
+ await runtime.launch();
165
+ display?.assertAvailable();
166
+ const identity = await runtime.identity();
167
+ return {
168
+ runtimeId: randomUUID(),
169
+ runtime,
170
+ rootTargetId: identity.targetId,
171
+ runtimeOptions,
172
+ tabs: new Map(),
173
+ tabOrder: [identity.targetId],
174
+ display,
175
+ displayNotice,
176
+ };
177
+ } catch (error) {
178
+ let cleanupFailure: unknown = null;
179
+ try {
180
+ await runtime?.shutdown();
181
+ } catch (failure) {
182
+ cleanupFailure = failure;
183
+ } finally {
184
+ await display?.stop();
185
+ }
186
+ // An unconfirmed cleanup must not hide why creation failed.
187
+ if (cleanupFailure)
188
+ throw new AggregateError(
189
+ [error, cleanupFailure],
190
+ `Runtime creation failed (${error instanceof Error ? error.message : String(error)}) and its cleanup was not confirmed`,
191
+ );
192
+ throw error;
193
+ }
62
194
  },
63
195
  async request(owned, operation, input) {
196
+ owned.display?.assertAvailable();
64
197
  const data =
65
198
  input && typeof input === "object" && !Array.isArray(input)
66
199
  ? (input as Record<string, JsonValue>)
67
200
  : {};
201
+ if (operation === "tabs.list") {
202
+ return (await orderedTargets(owned)).map(({ targetId, title, url }) => ({
203
+ targetId,
204
+ title,
205
+ url,
206
+ }));
207
+ }
208
+ if (operation === "tabs.create") {
209
+ if ((await orderedTargets(owned)).length >= MAX_BROWSER_TABS)
210
+ throw new Error("Browser tab limit reached");
211
+ const targetId = await owned.runtime.createTarget();
212
+ owned.tabOrder.push(targetId);
213
+ return { targetId };
214
+ }
215
+ if (operation === "tabs.close") {
216
+ const targetId = String(data.targetId);
217
+ const targets = await orderedTargets(owned);
218
+ if (!targets.some((target) => target.targetId === targetId)) {
219
+ throw new Error("Browser tab is no longer available");
220
+ }
221
+ if (targets.length <= 1) {
222
+ throw new Error("The last browser tab cannot be closed");
223
+ }
224
+ const controller = owned.tabs.get(targetId);
225
+ if (controller) await (await controller).shutdown();
226
+ if (owned.rootTargetId === targetId) {
227
+ const successor = targets.find((target) => target.targetId !== targetId)!;
228
+ await owned.runtime.selectTarget(successor.targetId, false);
229
+ owned.rootTargetId = successor.targetId;
230
+ }
231
+ await owned.runtime.closeTarget(targetId);
232
+ // Target.closeTarget can acknowledge before Chromium drops the page from
233
+ // its target list. Success requires observing its absence; the close is
234
+ // never replayed, and exhaustion is reported rather than assumed.
235
+ let gone = false;
236
+ for (let attempt = 0; attempt < 20 && !gone; attempt++) {
237
+ gone = !(await owned.runtime.targets()).some((target) => target.targetId === targetId);
238
+ if (gone) break;
239
+ const { promise, resolve } = Promise.withResolvers<void>();
240
+ setTimeout(resolve, 50);
241
+ await promise;
242
+ }
243
+ owned.tabs.delete(targetId);
244
+ if (!gone) throw new CdpUnknownOutcomeError("Browser tab close could not be confirmed");
245
+ owned.tabOrder = owned.tabOrder.filter((id) => id !== targetId);
246
+ return null;
247
+ }
248
+ const targetId = typeof data.targetId === "string" ? data.targetId : null;
249
+ // Keep the launch page on its original CDP session. A second session on
250
+ // the same target can reset emulation and interrupt its video source.
251
+ const runtime =
252
+ targetId && (targetId !== owned.rootTargetId || owned.tabs.has(targetId))
253
+ ? await tabRuntime(owned, targetId)
254
+ : owned.runtime;
255
+ const gestureId = typeof data.gestureId === "string" ? data.gestureId : undefined;
68
256
  switch (operation) {
69
- case "identity":
70
- return owned.runtime.identity() as unknown as JsonValue;
257
+ case "identity": {
258
+ const identity = await runtime.identity();
259
+ return {
260
+ ...identity,
261
+ ...(owned.displayNotice ? { displayNotice: owned.displayNotice } : {}),
262
+ } as unknown as JsonValue;
263
+ }
71
264
  case "state":
72
- return owned.runtime.state() as unknown as JsonValue;
265
+ if (data.timeoutMs === undefined) return runtime.state() as unknown as JsonValue;
266
+ if (typeof data.timeoutMs !== "number")
267
+ throw new Error("Invalid browser metadata timeout");
268
+ return runtime.state({ timeoutMs: data.timeoutMs }) as unknown as JsonValue;
73
269
  case "navigate":
74
- await owned.runtime.navigate(String(data.url));
270
+ await runtime.navigate(String(data.url));
75
271
  return null;
76
272
  case "back":
77
- await owned.runtime.back();
273
+ await runtime.back();
78
274
  return null;
79
275
  case "forward":
80
- await owned.runtime.forward();
276
+ await runtime.forward();
81
277
  return null;
82
278
  case "reload":
83
- await owned.runtime.reload();
279
+ await runtime.reload();
84
280
  return null;
85
281
  case "emulate":
86
- await owned.runtime.emulate(data as unknown as BrowserViewport);
282
+ await runtime.emulate(data as unknown as BrowserViewport);
283
+ return null;
284
+ case "capture.density":
285
+ await runtime.setCaptureDensity(Number(data.density), Number(data.deviceScaleFactor));
87
286
  return null;
88
287
  case "screencast.start":
89
- await owned.runtime.startScreencast(Number(data.quality));
288
+ await runtime.startScreencast(Number(data.quality));
90
289
  return null;
91
290
  case "screencast.stop":
92
- await owned.runtime.stopScreencast();
291
+ await runtime.stopScreencast();
292
+ return null;
293
+ case "video.read":
294
+ return (await runtime.readVideo({
295
+ quality: data.quality === "low" || data.quality === "medium" ? data.quality : "high",
296
+ streamId: typeof data.streamId === "string" ? data.streamId : null,
297
+ requestKeyFrame: data.requestKeyFrame === true,
298
+ ...(data.bitrate === undefined
299
+ ? {}
300
+ : { bitrate: videoBitrateSchema.parse(data.bitrate) }),
301
+ ...(data.fps === undefined ? {} : { fps: videoFrameRateSchema.parse(data.fps) }),
302
+ afterSequence: Number(data.afterSequence ?? 0),
303
+ waitMs: Number(data.waitMs ?? 250),
304
+ })) as unknown as JsonValue;
305
+ case "video.stop":
306
+ await runtime.stopVideo();
307
+ return null;
308
+ case "video.invalidate":
309
+ runtime.invalidateQueuedVideoFrames();
93
310
  return null;
94
311
  case "frame":
95
- return (await owned.runtime.frame(
312
+ return (await runtime.frame(
96
313
  Number(data.maxBytes),
97
314
  Number(data.quality),
98
315
  Number(data.waitMs),
99
316
  )) as unknown as JsonValue;
100
317
  case "mouse.move":
101
- await owned.runtime.mouseMove(Number(data.x), Number(data.y));
318
+ await runtime.mouseMove(
319
+ Number(data.x),
320
+ Number(data.y),
321
+ gestureId,
322
+ ...optionalMouseModifiers(data),
323
+ );
102
324
  return null;
103
325
  case "mouse.down":
104
- await owned.runtime.mouseDown(
326
+ await runtime.mouseDown(
105
327
  Number(data.x),
106
328
  Number(data.y),
107
329
  String(data.button) as "left" | "middle" | "right",
108
330
  Number(data.clickCount),
331
+ gestureId,
332
+ ...optionalMouseModifiers(data),
109
333
  );
110
334
  return null;
111
335
  case "mouse.up":
112
- await owned.runtime.mouseUp(
336
+ await runtime.mouseUp(
113
337
  Number(data.x),
114
338
  Number(data.y),
115
339
  String(data.button) as "left" | "middle" | "right",
116
340
  Number(data.clickCount),
341
+ gestureId,
342
+ ...optionalMouseModifiers(data),
117
343
  );
118
344
  return null;
119
345
  case "mouse.wheel":
120
- await owned.runtime.wheel(
346
+ await runtime.wheel(
121
347
  Number(data.x),
122
348
  Number(data.y),
123
349
  Number(data.deltaX),
124
350
  Number(data.deltaY),
351
+ gestureId,
352
+ ...optionalMouseModifiers(data),
125
353
  );
126
354
  return null;
355
+ case "mouse.leave":
356
+ if (!gestureId) throw new Error("Gesture identity is required");
357
+ await runtime.mouseLeave(gestureId);
358
+ return null;
359
+ case "input.begin":
360
+ if (!gestureId) throw new Error("Gesture identity is required");
361
+ if (typeof data.expectedInputGeneration !== "string") {
362
+ throw new Error("Expected native input generation is required");
363
+ }
364
+ await runtime.beginLiveInput(gestureId, data.expectedInputGeneration);
365
+ return null;
366
+ case "input.check":
367
+ if (!gestureId) throw new Error("Gesture identity is required");
368
+ await runtime.assertLiveInput(gestureId);
369
+ return null;
370
+ case "input.end":
371
+ if (!gestureId) throw new Error("Gesture identity is required");
372
+ await runtime.endLiveInput(gestureId);
373
+ return null;
374
+ case "cursor":
375
+ if (!gestureId) throw new Error("Gesture identity is required");
376
+ return runtime.cursorAt(Number(data.x), Number(data.y), gestureId);
377
+ case "touch": {
378
+ if (!gestureId || !Array.isArray(data.points) || data.points.length > 5)
379
+ throw new Error("Invalid touch input");
380
+ const points = data.points.map((value) => {
381
+ if (!value || typeof value !== "object" || Array.isArray(value))
382
+ throw new Error("Invalid touch point");
383
+ const id = value.id;
384
+ if (
385
+ typeof id !== "number" ||
386
+ typeof value.x !== "number" ||
387
+ typeof value.y !== "number"
388
+ )
389
+ throw new Error("Invalid touch point numbers");
390
+ if (!Number.isSafeInteger(id) || id < 0 || id > 2_147_483_647)
391
+ throw new Error("Invalid touch identifier");
392
+ return { x: Number(value.x), y: Number(value.y), id };
393
+ });
394
+ if (new Set(points.map((point) => point.id)).size !== points.length)
395
+ throw new Error("Duplicate touch identifiers");
396
+ const type = data.type;
397
+ if (type !== "start" && type !== "move" && type !== "end" && type !== "cancel")
398
+ throw new Error("Invalid touch phase");
399
+ if ((type === "end" || type === "cancel") !== (points.length === 0))
400
+ throw new Error("Invalid touch phase points");
401
+ const nativeType =
402
+ type === "start"
403
+ ? "touchStart"
404
+ : type === "move"
405
+ ? "touchMove"
406
+ : type === "end"
407
+ ? "touchEnd"
408
+ : "touchCancel";
409
+ await runtime.touch(nativeType, points, gestureId);
410
+ return null;
411
+ }
412
+ case "input.key":
413
+ if (!gestureId) throw new Error("Gesture identity is required");
414
+ await runtime.dispatchKey(browserGestureKeySchema.parse(data.event), gestureId);
415
+ return null;
416
+ case "input.text":
417
+ if (
418
+ !gestureId ||
419
+ typeof data.text !== "string" ||
420
+ data.text.length < 1 ||
421
+ data.text.length > 16_000
422
+ ) {
423
+ throw new Error("Invalid committed text input");
424
+ }
425
+ await runtime.insertText(data.text, gestureId);
426
+ return null;
127
427
  case "text.insert":
128
- await owned.runtime.insertText(String(data.text));
428
+ await runtime.insertText(String(data.text), gestureId);
129
429
  return null;
130
430
  case "key.down":
131
- await owned.runtime.keyDown(String(data.key));
431
+ await runtime.keyDown(String(data.key), String(data.key), gestureId);
132
432
  return null;
133
433
  case "key.up":
134
- await owned.runtime.keyUp(String(data.key));
434
+ await runtime.keyUp(String(data.key), String(data.key), gestureId);
135
435
  return null;
136
436
  default:
137
437
  throw new Error(`Unknown browser runtime operation: ${operation}`);
138
438
  }
139
439
  },
140
440
  async stop(owned) {
141
- await owned.runtime.shutdown();
441
+ try {
442
+ await Promise.allSettled(
443
+ [...owned.tabs.values()].map(async (controller) =>
444
+ (await controller).shutdown().catch(() => undefined),
445
+ ),
446
+ );
447
+ await owned.runtime.shutdown();
448
+ } finally {
449
+ await owned.display?.stop();
450
+ }
142
451
  },
143
452
  };
144
453
  }