@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
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ browserDisplayPreferences,
4
+ browserDisplayPreferencesSchema,
5
+ } from "./browser-display-preferences";
6
+ import { DEVICE_PRESETS } from "./device-presets";
7
+ import {
8
+ groupResolutionPresets,
9
+ normalizeResolutionFavorites,
10
+ orderedResolutionFavorites,
11
+ toggleResolutionFavorite,
12
+ } from "./resolution-menu";
13
+
14
+ describe("resolution choices and host preferences", () => {
15
+ it("groups the whole catalogue by viewport ratio, sorts width and retains mobile ties", () => {
16
+ const groups = groupResolutionPresets();
17
+ expect(groups.map((group) => group.label)).toEqual(["16:9", "16:10", "1:1", "Mobile"]);
18
+ expect(
19
+ groups
20
+ .flatMap((group) => group.presets)
21
+ .map((preset) => preset.id)
22
+ .sort(),
23
+ ).toEqual(DEVICE_PRESETS.map((preset) => preset.id).sort());
24
+ for (const group of groups) {
25
+ expect(group.presets.map((preset) => preset.viewport.width)).toEqual(
26
+ group.presets.map((preset) => preset.viewport.width).sort((a, b) => a - b),
27
+ );
28
+ for (const preset of group.presets) {
29
+ if (group.id === "mobile") expect(preset.isMobile).toBe(true);
30
+ else {
31
+ expect(preset.isMobile).toBe(false);
32
+ const [width, height] = group.id.split(":").map(Number);
33
+ if (width === undefined || height === undefined) throw new Error("Invalid ratio fixture");
34
+ expect(preset.viewport.width * height).toBe(preset.viewport.height * width);
35
+ }
36
+ }
37
+ }
38
+ expect(groups[3]?.presets.map((preset) => preset.id)).toEqual([
39
+ "iphone-15-pro",
40
+ "pixel-7",
41
+ "pixel-7-sharp",
42
+ "ipad-pro-11",
43
+ ]);
44
+ });
45
+
46
+ it("projects scrambled saved favorites in the grouped picker order without rewriting them", () => {
47
+ const saved = Object.freeze([
48
+ "pixel-7-sharp",
49
+ "desktop-1440x1440",
50
+ "desktop-1920x1200",
51
+ "desktop-2560x1440",
52
+ "desktop-1440x900",
53
+ "desktop-chrome",
54
+ "desktop-1440x810",
55
+ "desktop-chrome",
56
+ "retired",
57
+ ]);
58
+ expect(orderedResolutionFavorites(saved)).toEqual([
59
+ "desktop-chrome",
60
+ "desktop-1440x810",
61
+ "desktop-2560x1440",
62
+ "desktop-1440x900",
63
+ "desktop-1920x1200",
64
+ "desktop-1440x1440",
65
+ "pixel-7-sharp",
66
+ ]);
67
+ expect(saved[0]).toBe("pixel-7-sharp");
68
+ expect(saved).toHaveLength(9);
69
+ });
70
+
71
+ it("prunes removed favorites and duplicates without changing stable favorite order", () => {
72
+ expect(
73
+ normalizeResolutionFavorites([
74
+ "retired",
75
+ "pixel-7-sharp",
76
+ "desktop-chrome",
77
+ "pixel-7-sharp",
78
+ "unknown",
79
+ ]),
80
+ ).toEqual(["pixel-7-sharp", "desktop-chrome"]);
81
+ expect(normalizeResolutionFavorites(Array(100).fill("pixel-7"))).toEqual(["pixel-7"]);
82
+ expect(
83
+ normalizeResolutionFavorites(DEVICE_PRESETS.flatMap((preset) => [preset.id, preset.id])),
84
+ ).toHaveLength(DEVICE_PRESETS.length);
85
+ });
86
+
87
+ it("toggles only catalogue entries and does not mutate stored input", () => {
88
+ const input = Object.freeze(["retired", "desktop-chrome", "desktop-chrome"]);
89
+ expect(toggleResolutionFavorite(input, "pixel-7")).toEqual(["desktop-chrome", "pixel-7"]);
90
+ expect(toggleResolutionFavorite(input, "desktop-chrome")).toEqual([]);
91
+ expect(toggleResolutionFavorite(input, "unknown")).toEqual(["desktop-chrome"]);
92
+ expect(input).toEqual(["retired", "desktop-chrome", "desktop-chrome"]);
93
+ });
94
+
95
+ it("defaults host favorites and quality but allows retired IDs to remain readable", () => {
96
+ expect(browserDisplayPreferences.scope).toBe("host");
97
+ expect(browserDisplayPreferencesSchema.parse({})).toEqual({
98
+ favoritePresetIds: [],
99
+ captureQuality: "medium",
100
+ videoBitrate: 12_000_000,
101
+ videoFps: 30,
102
+ });
103
+ expect(
104
+ browserDisplayPreferencesSchema.parse({ favoritePresetIds: ["retired"] }).favoritePresetIds,
105
+ ).toEqual(["retired"]);
106
+ expect(browserDisplayPreferencesSchema.safeParse({ captureQuality: "ultra" }).success).toBe(
107
+ false,
108
+ );
109
+ expect(
110
+ browserDisplayPreferencesSchema.safeParse({
111
+ favoritePresetIds: Array(DEVICE_PRESETS.length + 1).fill("pixel-7"),
112
+ }).success,
113
+ ).toBe(false);
114
+ expect(
115
+ browserDisplayPreferencesSchema.safeParse({
116
+ favoritePresetIds: ["x".repeat(129)],
117
+ }).success,
118
+ ).toBe(false);
119
+ });
120
+ });
@@ -0,0 +1,72 @@
1
+ /** Catalogue grouping and stable-ID favorites for every resolution picker surface. */
2
+ import { DEVICE_PRESET_IDS, DEVICE_PRESETS, type DevicePresetId } from "./device-presets";
3
+
4
+ export type ResolutionPreset = (typeof DEVICE_PRESETS)[number];
5
+ export type ResolutionGroupId = "16:9" | "16:10" | "1:1" | "mobile";
6
+ export interface ResolutionGroup {
7
+ id: ResolutionGroupId;
8
+ label: string;
9
+ presets: readonly ResolutionPreset[];
10
+ }
11
+
12
+ const GROUPS = [
13
+ { id: "16:9", label: "16:9" },
14
+ { id: "16:10", label: "16:10" },
15
+ { id: "1:1", label: "1:1" },
16
+ { id: "mobile", label: "Mobile" },
17
+ ] as const;
18
+ const KNOWN_PRESET_IDS: ReadonlySet<string> = new Set(DEVICE_PRESET_IDS);
19
+
20
+ /** Group by actual viewport facts, keeping equal-width presets in catalogue order. */
21
+ export function groupResolutionPresets(): ResolutionGroup[] {
22
+ const groups: ResolutionGroup[] = GROUPS.map((group) => ({
23
+ ...group,
24
+ presets: [],
25
+ }));
26
+ for (const preset of DEVICE_PRESETS) {
27
+ const { width, height } = preset.viewport;
28
+ let id: ResolutionGroupId;
29
+ if (preset.isMobile) id = "mobile";
30
+ else if (width === height) id = "1:1";
31
+ else if (width * 9 === height * 16) id = "16:9";
32
+ else if (width * 10 === height * 16) id = "16:10";
33
+ else throw new Error(`Resolution preset ${preset.id} requires an aspect-ratio group`);
34
+ const group = groups.find((candidate) => candidate.id === id);
35
+ if (!group) throw new Error(`Resolution group ${id} is missing`);
36
+ group.presets = [...group.presets, preset];
37
+ }
38
+ return groups.map((group) => ({
39
+ ...group,
40
+ presets: [...group.presets].sort((a, b) => a.viewport.width - b.viewport.width),
41
+ }));
42
+ }
43
+
44
+ /** Drop removed/unknown IDs and duplicates, retaining the user's favorite order. */
45
+ export function normalizeResolutionFavorites(ids: readonly string[]): DevicePresetId[] {
46
+ const favorites: DevicePresetId[] = [];
47
+ const seen = new Set<string>();
48
+ for (const id of ids) {
49
+ if (!KNOWN_PRESET_IDS.has(id) || seen.has(id)) continue;
50
+ seen.add(id);
51
+ favorites.push(id as DevicePresetId);
52
+ if (favorites.length === DEVICE_PRESETS.length) break;
53
+ }
54
+ return favorites;
55
+ }
56
+
57
+ /** Project stored favorites in picker order without rewriting the user's saved settings. */
58
+ export function orderedResolutionFavorites(ids: readonly string[]): DevicePresetId[] {
59
+ const favorites = new Set(normalizeResolutionFavorites(ids));
60
+ return groupResolutionPresets()
61
+ .flatMap((group) => group.presets)
62
+ .filter((preset) => favorites.has(preset.id))
63
+ .map((preset) => preset.id);
64
+ }
65
+
66
+ /** Toggle only a current preset. Unknown IDs cannot become persisted shortcuts. */
67
+ export function toggleResolutionFavorite(ids: readonly string[], id: string): DevicePresetId[] {
68
+ const current = normalizeResolutionFavorites(ids);
69
+ if (!KNOWN_PRESET_IDS.has(id)) return current;
70
+ if (current.includes(id as DevicePresetId)) return current.filter((existing) => existing !== id);
71
+ return [...current, id as DevicePresetId];
72
+ }
@@ -0,0 +1,38 @@
1
+ import { expect, it } from "vitest";
2
+ import { captureBrowserRpc } from "./browser";
3
+ import {
4
+ browserDisplayPreferences,
5
+ browserDisplayPreferencesSchema,
6
+ } from "./browser-display-preferences";
7
+ import { readBrowserVideoRpc } from "./browser-video";
8
+ import { JPEG_QUALITY } from "./capture-settings";
9
+ import { resolveVideoEncoderSettings, videoEncoderKey } from "./video-settings";
10
+
11
+ it("retains current defaults and migrates the old coupled quality without losing favorites", () => {
12
+ expect(resolveVideoEncoderSettings({})).toEqual({ bitrate: 12_000_000, fps: 30 });
13
+ const migrated = browserDisplayPreferences.migrate?.(
14
+ { captureQuality: "low", favoritePresetIds: ["pixel-7-sharp"] },
15
+ 1,
16
+ );
17
+ expect(browserDisplayPreferencesSchema.parse(migrated)).toEqual({
18
+ captureQuality: "low",
19
+ favoritePresetIds: ["pixel-7-sharp"],
20
+ videoBitrate: 2_000_000,
21
+ videoFps: 30,
22
+ });
23
+ });
24
+ it("accepts explicit JPEG100 and video24Mbps60fps, and refuses arbitrary settings", () => {
25
+ const viewerToken = "v".repeat(32);
26
+ expect(captureBrowserRpc.input.parse({ viewerToken, quality: "maximum" }).quality).toBe(
27
+ "maximum",
28
+ );
29
+ expect(JPEG_QUALITY.maximum).toBe(100);
30
+ expect(
31
+ readBrowserVideoRpc.input.parse({ viewerToken, bitrate: 24_000_000, fps: 60 }),
32
+ ).toMatchObject({ bitrate: 24_000_000, fps: 60 });
33
+ for (const invalid of [{ bitrate: 25_000_000 }, { fps: 120 }, { fps: NaN }, { bitrate: 0 }])
34
+ expect(readBrowserVideoRpc.input.safeParse({ viewerToken, ...invalid }).success).toBe(false);
35
+ expect(videoEncoderKey(resolveVideoEncoderSettings({ bitrate: 24_000_000, fps: 15 }))).toBe(
36
+ "24000000:15",
37
+ );
38
+ });
@@ -0,0 +1,34 @@
1
+ /** Bounded viewer encoder profiles. Settings affect encoded output only, never
2
+ * page layout, pixel density, control ownership or another viewer's encoder. */
3
+ import { z } from "zod";
4
+ export const VIDEO_BITRATES = [2_000_000, 5_000_000, 12_000_000, 24_000_000] as const;
5
+ export const VIDEO_FRAME_RATES = [15, 30, 60] as const;
6
+ export const DEFAULT_VIDEO_BITRATE = 12_000_000;
7
+ export const DEFAULT_VIDEO_FPS = 30;
8
+ export const VIDEO_ENCODER_LIMIT = 3;
9
+ export const VIDEO_ENCODER_IDLE_MS = 2_000;
10
+ export const videoBitrateSchema = z.union(VIDEO_BITRATES.map((value) => z.literal(value)));
11
+ export const videoFrameRateSchema = z.union(VIDEO_FRAME_RATES.map((value) => z.literal(value)));
12
+ export const videoEncoderSettingsSchema = z.object({
13
+ bitrate: videoBitrateSchema,
14
+ fps: videoFrameRateSchema,
15
+ });
16
+ export type VideoEncoderSettings = z.infer<typeof videoEncoderSettingsSchema>;
17
+ const LEGACY_BITRATES = { low: 2_000_000, medium: 5_000_000, high: 12_000_000 } as const;
18
+ /** Legacy profile callers retain their exact bitrate; new callers supply explicit
19
+ * bounded values. Unknown values fail before allocating a helper or encoder. */
20
+ export function resolveVideoEncoderSettings(input: {
21
+ quality?: "low" | "medium" | "high";
22
+ bitrate?: number;
23
+ fps?: number;
24
+ }): VideoEncoderSettings {
25
+ return videoEncoderSettingsSchema.parse({
26
+ bitrate: input.bitrate ?? LEGACY_BITRATES[input.quality ?? "high"],
27
+ fps: input.fps ?? DEFAULT_VIDEO_FPS,
28
+ });
29
+ }
30
+ /** Canonical key includes both encoder settings, avoiding bitrate/FPS aliasing. */
31
+ export function videoEncoderKey(settings: VideoEncoderSettings): string {
32
+ const value = videoEncoderSettingsSchema.parse(settings);
33
+ return `${value.bitrate}:${value.fps}`;
34
+ }
@@ -0,0 +1,5 @@
1
+ /** Shared bounds for custom viewports, frame contracts, and the agent adapter. */
2
+ export const MIN_VIEWPORT = { width: 320, height: 480 } as const;
3
+ export const MAX_VIEWPORT = { width: 3840, height: 3840 } as const;
4
+ /** Agents keep the smaller original bound; humans may choose any size up to MAX_VIEWPORT. */
5
+ export const MAX_AGENT_VIEWPORT = { width: 1600, height: 1200 } as const;