bunite-core 0.12.1 → 0.16.0

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 (36) hide show
  1. package/package.json +4 -4
  2. package/src/host/core/App.ts +19 -2
  3. package/src/host/core/BrowserView.ts +515 -38
  4. package/src/host/core/SurfaceBrowserIPC.ts +53 -3
  5. package/src/host/core/SurfaceManager.ts +603 -30
  6. package/src/host/core/SurfaceRegistry.ts +9 -1
  7. package/src/host/core/inputDispatch.ts +147 -0
  8. package/src/host/events/webviewEvents.ts +25 -1
  9. package/src/host/log.ts +6 -1
  10. package/src/host/native.ts +263 -1
  11. package/src/host/preloadBundle.ts +7 -2
  12. package/src/native/linux/bunite_linux_ffi.cpp +427 -6
  13. package/src/native/linux/bunite_linux_internal.h +18 -0
  14. package/src/native/linux/bunite_linux_runtime.cpp +6 -1
  15. package/src/native/linux/bunite_linux_utils.cpp +2 -2
  16. package/src/native/linux/bunite_linux_view.cpp +296 -5
  17. package/src/native/mac/bunite_mac_ffi.mm +630 -8
  18. package/src/native/mac/bunite_mac_internal.h +19 -0
  19. package/src/native/mac/bunite_mac_utils.mm +2 -2
  20. package/src/native/mac/bunite_mac_view.mm +371 -9
  21. package/src/native/shared/ffi_exports.h +200 -2
  22. package/src/native/win/native_host_cef.cpp +186 -11
  23. package/src/native/win/native_host_ffi.cpp +1194 -1
  24. package/src/native/win/native_host_internal.h +35 -0
  25. package/src/native/win/native_host_utils.cpp +2 -1
  26. package/src/native/win/process_helper_win.cpp +54 -27
  27. package/src/native/win-webview2/bunite_webview2_ffi.cpp +1023 -12
  28. package/src/native/win-webview2/webview2_internal.h +25 -0
  29. package/src/native/win-webview2/webview2_runtime.cpp +403 -34
  30. package/src/native/win-webview2/webview2_utils.cpp +30 -12
  31. package/src/preload/runtime.built.js +1 -1
  32. package/src/preload/runtime.ts +97 -0
  33. package/src/rpc/framework.ts +340 -8
  34. package/src/rpc/index.ts +32 -0
  35. package/src/webview/native.ts +253 -51
  36. package/src/webview/polyfill.ts +283 -22
@@ -21,6 +21,13 @@ export function trackSurface(surfaceId: number, record: SurfaceRecord) {
21
21
  ids.add(surfaceId);
22
22
  }
23
23
 
24
+ type DisposeHook = (surfaceId: number) => void;
25
+ const disposeHooks: DisposeHook[] = [];
26
+
27
+ export function onSurfaceDispose(cb: DisposeHook) {
28
+ disposeHooks.push(cb);
29
+ }
30
+
24
31
  export function untrackSurface(surfaceId: number) {
25
32
  const record = surfaces.get(surfaceId);
26
33
  if (!record) return;
@@ -30,6 +37,7 @@ export function untrackSurface(surfaceId: number) {
30
37
  ids.delete(surfaceId);
31
38
  if (ids.size === 0) hostSurfaceIds.delete(record.hostViewId);
32
39
  }
40
+ for (const cb of disposeHooks) cb(surfaceId);
33
41
  }
34
42
 
35
43
  export function getOwnedSurface(surfaceId: number, ctx: { viewId: number }): SurfaceRecord | null {
@@ -54,7 +62,7 @@ export function removeSurfacesForHostView(hostViewId: number) {
54
62
  for (const surfaceId of Array.from(ids)) {
55
63
  const record = surfaces.get(surfaceId);
56
64
  if (!record) continue;
57
- untrackSurface(surfaceId);
65
+ untrackSurface(surfaceId); // fires disposeHooks
58
66
  record.view.remove();
59
67
  }
60
68
  }
@@ -0,0 +1,147 @@
1
+ // Input dispatch helpers — modifier encoding + DOM `key` → Win32 VK + macOS
2
+ // Quartz key code + DOM `code` + char. Backends translate the FFI bitmask
3
+ // (Alt=1, Ctrl=2, Meta=4, Shift=8) to native form. Stage B keymap covers
4
+ // ASCII + the named keys Playwright-style automation relies on.
5
+
6
+ import type { Modifier } from "../../rpc/framework";
7
+
8
+ export function encodeModifiers(mods: Modifier[] | undefined): number {
9
+ if (!mods) return 0;
10
+ let bits = 0;
11
+ for (const m of mods) {
12
+ if (m === "alt") bits |= 1;
13
+ else if (m === "ctrl") bits |= 2;
14
+ else if (m === "meta") bits |= 4;
15
+ else if (m === "shift") bits |= 8;
16
+ }
17
+ return bits;
18
+ }
19
+
20
+ export interface ResolvedKey {
21
+ windowsVkCode: number;
22
+ macKeyCode: number;
23
+ /** DOM `KeyboardEvent.key` — pass-through; native dispatchers forward to engines. */
24
+ key: string;
25
+ /** DOM `KeyboardEvent.code` — derived from US keyboard mapping. */
26
+ code: string;
27
+ /** Text payload for the CHAR / insertText event; empty = skip char. */
28
+ character: string;
29
+ /** Win scancode 0xE0 prefix: nav cluster (Arrow/Insert/Delete/Home/End/
30
+ * PageUp/PageDown/Meta/ContextMenu) AND Numpad-Enter. Distinct from
31
+ * `location` — most extended keys are NOT numpad (location 0). */
32
+ extended: boolean;
33
+ /** DOM `KeyboardEvent.location`: 0=standard, 1=left mod, 2=right mod,
34
+ * 3=numpad. WV2 CDP uses this; CEF derives from scancode 0xE0 prefix. */
35
+ location: 0 | 1 | 2 | 3;
36
+ }
37
+
38
+ /** Maps a DOM `KeyboardEvent.key` value to backend-neutral identifiers. */
39
+ export function resolveKey(domKey: string): ResolvedKey {
40
+ if (domKey.length === 0) {
41
+ return { windowsVkCode: 0, macKeyCode: 0, key: "", code: "", character: "", extended: false, location: 0 };
42
+ }
43
+
44
+ // Named key (Enter, Tab, ArrowLeft …).
45
+ const named = NAMED_KEYS[domKey];
46
+ if (named) {
47
+ return {
48
+ windowsVkCode: named.win,
49
+ macKeyCode: named.mac,
50
+ key: domKey,
51
+ code: named.code,
52
+ // Space/Tab/Enter generate text in CDP automatically; we pass an explicit
53
+ // character so DOM `keypress` fires consistently across engines.
54
+ character: named.character ?? "",
55
+ extended: named.ext === true,
56
+ location: named.loc ?? 0,
57
+ };
58
+ }
59
+
60
+ // Single Unicode codepoint — letter / digit / printable / extended.
61
+ if ([...domKey].length === 1) {
62
+ const cp = domKey.codePointAt(0)!;
63
+ // ASCII A-Z / a-z → matching Win VK + mac keyCode + DOM code "KeyX".
64
+ if ((cp >= 0x41 && cp <= 0x5A) || (cp >= 0x61 && cp <= 0x7A)) {
65
+ const upper = cp & ~0x20; // strip lowercase bit
66
+ return {
67
+ windowsVkCode: upper,
68
+ macKeyCode: MAC_KEY_LETTER[upper - 0x41],
69
+ key: domKey,
70
+ code: `Key${String.fromCharCode(upper)}`,
71
+ character: domKey,
72
+ extended: false,
73
+ location: 0,
74
+ };
75
+ }
76
+ // ASCII 0-9.
77
+ if (cp >= 0x30 && cp <= 0x39) {
78
+ return {
79
+ windowsVkCode: cp,
80
+ macKeyCode: MAC_KEY_DIGIT[cp - 0x30],
81
+ key: domKey,
82
+ code: `Digit${domKey}`,
83
+ character: domKey,
84
+ extended: false,
85
+ location: 0,
86
+ };
87
+ }
88
+ // Other printable codepoint — char event only, no virtual key.
89
+ return { windowsVkCode: 0, macKeyCode: 0, key: domKey, code: "", character: domKey, extended: false, location: 0 };
90
+ }
91
+
92
+ // Multi-codepoint string we don't recognise as a named key — pass-through.
93
+ return { windowsVkCode: 0, macKeyCode: 0, key: domKey, code: "", character: "", extended: false, location: 0 };
94
+ }
95
+
96
+ // Win32 VK_* + Quartz Event Services kVK_* + DOM code + literal character +
97
+ // LPARAM extended-key flag. `ext: true` for nav-cluster keys (separate from
98
+ // numpad equivalents) and Numpad-Enter — Chromium derives `KeyboardEvent.code`
99
+ // from LPARAM scancode + extended bit. Sources:
100
+ // learn.microsoft.com/windows/win32/inputdev/virtual-key-codes
101
+ // chromium/ui/events/keycodes/dom/keycode_converter_data.inc
102
+ type NamedKey = { win: number; mac: number; code: string; character?: string; ext?: true; loc?: 1 | 2 | 3 };
103
+ const NAMED_KEYS: Record<string, NamedKey> = {
104
+ Backspace: { win: 0x08, mac: 0x33, code: "Backspace" },
105
+ Tab: { win: 0x09, mac: 0x30, code: "Tab", character: "\t" },
106
+ Enter: { win: 0x0D, mac: 0x24, code: "Enter", character: "\r" },
107
+ NumpadEnter: { win: 0x0D, mac: 0x4C, code: "NumpadEnter", character: "\r", ext: true, loc: 3 },
108
+ Escape: { win: 0x1B, mac: 0x35, code: "Escape" },
109
+ " ": { win: 0x20, mac: 0x31, code: "Space", character: " " },
110
+ Space: { win: 0x20, mac: 0x31, code: "Space", character: " " },
111
+ PageUp: { win: 0x21, mac: 0x74, code: "PageUp", ext: true },
112
+ PageDown: { win: 0x22, mac: 0x79, code: "PageDown", ext: true },
113
+ End: { win: 0x23, mac: 0x77, code: "End", ext: true },
114
+ Home: { win: 0x24, mac: 0x73, code: "Home", ext: true },
115
+ ArrowLeft: { win: 0x25, mac: 0x7B, code: "ArrowLeft", ext: true },
116
+ ArrowUp: { win: 0x26, mac: 0x7E, code: "ArrowUp", ext: true },
117
+ ArrowRight: { win: 0x27, mac: 0x7C, code: "ArrowRight", ext: true },
118
+ ArrowDown: { win: 0x28, mac: 0x7D, code: "ArrowDown", ext: true },
119
+ Insert: { win: 0x2D, mac: 0x72, code: "Insert", ext: true },
120
+ Delete: { win: 0x2E, mac: 0x75, code: "Delete", ext: true },
121
+ Meta: { win: 0x5B, mac: 0x37, code: "MetaLeft", ext: true },
122
+ ContextMenu: { win: 0x5D, mac: 0x6E, code: "ContextMenu", ext: true },
123
+ F1: { win: 0x70, mac: 0x7A, code: "F1" },
124
+ F2: { win: 0x71, mac: 0x78, code: "F2" },
125
+ F3: { win: 0x72, mac: 0x63, code: "F3" },
126
+ F4: { win: 0x73, mac: 0x76, code: "F4" },
127
+ F5: { win: 0x74, mac: 0x60, code: "F5" },
128
+ F6: { win: 0x75, mac: 0x61, code: "F6" },
129
+ F7: { win: 0x76, mac: 0x62, code: "F7" },
130
+ F8: { win: 0x77, mac: 0x64, code: "F8" },
131
+ F9: { win: 0x78, mac: 0x65, code: "F9" },
132
+ F10: { win: 0x79, mac: 0x6D, code: "F10" },
133
+ F11: { win: 0x7A, mac: 0x67, code: "F11" },
134
+ F12: { win: 0x7B, mac: 0x6F, code: "F12" },
135
+ };
136
+
137
+ // US keyboard layout — Quartz hardware key code per ASCII letter (A → 0x00, …, Z → 0x06).
138
+ const MAC_KEY_LETTER = [
139
+ // A B C D E F G H I J K L M
140
+ 0x00, 0x0B, 0x08, 0x02, 0x0E, 0x03, 0x05, 0x04, 0x22, 0x26, 0x28, 0x25, 0x2E,
141
+ // N O P Q R S T U V W X Y Z
142
+ 0x2D, 0x1F, 0x23, 0x0C, 0x0F, 0x01, 0x11, 0x20, 0x09, 0x0D, 0x07, 0x10, 0x06,
143
+ ];
144
+ // US keyboard layout — Quartz hardware key code per digit (0 → 0x1D, 1 → 0x12 …).
145
+ const MAC_KEY_DIGIT = [
146
+ 0x1D, 0x12, 0x13, 0x14, 0x15, 0x17, 0x16, 0x1A, 0x1C, 0x19,
147
+ ];
@@ -8,5 +8,29 @@ export default {
8
8
  new BuniteEvent("new-window-open", data),
9
9
  permissionRequested: (data: { requestId: number; kind: number; url?: string }) =>
10
10
  new BuniteEvent("permission-requested", data),
11
- titleChanged: (data: { detail: string }) => new BuniteEvent("title-changed", data)
11
+ titleChanged: (data: { detail: string }) => new BuniteEvent("title-changed", data),
12
+ loadStart: (data: { detail: string }) => new BuniteEvent("load-start", data),
13
+ loadFinish: (data: { detail: string }) => new BuniteEvent("load-finish", data),
14
+ loadFail: (data: { url: string; reason?: string }) => new BuniteEvent("load-fail", data),
15
+ dialog: (data: { requestId: number; kind: "alert" | "confirm" | "prompt" | "beforeunload"; message: string; defaultPrompt?: string }) =>
16
+ new BuniteEvent("dialog", data),
17
+ consoleMessage: (data: { level: "log" | "warn" | "error" | "info" | "debug"; args: string[]; ts: number }) =>
18
+ new BuniteEvent("console-message", data),
19
+ downloadEvent: (data: {
20
+ kind: "started" | "progress" | "completed" | "failed" | "blocked";
21
+ id: string;
22
+ url?: string;
23
+ suggestedFilename?: string;
24
+ mimeType?: string;
25
+ sizeBytes?: number;
26
+ receivedBytes?: number;
27
+ totalBytes?: number;
28
+ localPath?: string;
29
+ reason?: string;
30
+ }) => new BuniteEvent("download-event", data),
31
+ popupRequested: (data: {
32
+ newSurfaceId: number;
33
+ url: string;
34
+ disposition: "tab" | "window" | "popup";
35
+ }) => new BuniteEvent("popup-requested", data),
12
36
  };
package/src/host/log.ts CHANGED
@@ -8,7 +8,12 @@ const levels: Record<LogLevel, number> = {
8
8
  silent: 4
9
9
  };
10
10
 
11
- let currentLevel: LogLevel = "warn";
11
+ function initialLevel(): LogLevel {
12
+ const v = (typeof process !== "undefined" ? process.env?.BUNITE_LOG_LEVEL : undefined);
13
+ return v === "debug" || v === "info" || v === "warn" || v === "error" || v === "silent" ? v : "warn";
14
+ }
15
+
16
+ let currentLevel: LogLevel = initialLevel();
12
17
 
13
18
  function shouldLog(level: LogLevel): boolean {
14
19
  return levels[level] >= levels[currentLevel];
@@ -102,6 +102,51 @@ type NativeSymbols = {
102
102
  bunite_view_reload: (viewId: number) => void;
103
103
  bunite_view_execute_javascript: (viewId: number, script: CStringPointer) => void;
104
104
  bunite_view_evaluate: (viewId: number, requestId: number, script: CStringPointer) => void;
105
+ bunite_view_click: (
106
+ viewId: number, x: number, y: number,
107
+ button: number, clickCount: number, modifiers: number
108
+ ) => void;
109
+ bunite_view_type: (viewId: number, text: CStringPointer) => void;
110
+ bunite_view_press: (
111
+ viewId: number, windowsVkCode: number, macKeyCode: number,
112
+ key: CStringPointer, code: CStringPointer, character: CStringPointer,
113
+ modifiers: number, action: number, extended: boolean, location: number
114
+ ) => void;
115
+ bunite_view_scroll: (
116
+ viewId: number, dx: number, dy: number,
117
+ x: number, y: number, modifiers: number
118
+ ) => void;
119
+ bunite_view_mouse: (
120
+ viewId: number, action: number, x: number, y: number,
121
+ button: number, modifiers: number
122
+ ) => void;
123
+ bunite_view_respond_dialog: (
124
+ viewId: number, requestId: number, accept: boolean, text: CStringPointer
125
+ ) => void;
126
+ bunite_view_screenshot: (
127
+ viewId: number, requestId: number, format: CStringPointer, quality: number
128
+ ) => void;
129
+ bunite_view_accessibility_snapshot: (
130
+ viewId: number, requestId: number, interestingOnly: number
131
+ ) => void;
132
+ bunite_view_list_frames: (viewId: number, requestId: number) => void;
133
+ bunite_view_evaluate_in_frame: (
134
+ viewId: number, requestId: number, script: CStringPointer, frameId: CStringPointer
135
+ ) => void;
136
+ bunite_view_resolve_and_click: (
137
+ viewId: number, requestId: number,
138
+ selector: CStringPointer, frameId: CStringPointer,
139
+ button: number, clickCount: number, modifiers: number
140
+ ) => void;
141
+ bunite_view_set_download_policy: (
142
+ viewId: number, policy: number, downloadDir: CStringPointer
143
+ ) => void;
144
+ bunite_view_popup_accept: (
145
+ newViewId: number, hostWindowId: number,
146
+ x: number, y: number, w: number, h: number,
147
+ ) => void;
148
+ bunite_view_popup_dismiss: (newViewId: number) => void;
149
+ bunite_view_capabilities: (viewId: number) => number;
105
150
  bunite_view_load_url: (viewId: number, url: CStringPointer) => void;
106
151
  bunite_view_load_html: (viewId: number, html: CStringPointer) => void;
107
152
  bunite_view_remove: (viewId: number) => void;
@@ -289,6 +334,66 @@ const nativeSymbolDefinitions = {
289
334
  args: [FFIType.u32, FFIType.u32, FFIType.cstring],
290
335
  returns: FFIType.void
291
336
  },
337
+ bunite_view_click: {
338
+ args: [FFIType.u32, FFIType.f64, FFIType.f64, FFIType.i32, FFIType.i32, FFIType.u32],
339
+ returns: FFIType.void
340
+ },
341
+ bunite_view_type: {
342
+ args: [FFIType.u32, FFIType.cstring],
343
+ returns: FFIType.void
344
+ },
345
+ bunite_view_press: {
346
+ args: [FFIType.u32, FFIType.i32, FFIType.i32, FFIType.cstring, FFIType.cstring, FFIType.cstring, FFIType.u32, FFIType.i32, FFIType.bool, FFIType.i32],
347
+ returns: FFIType.void
348
+ },
349
+ bunite_view_scroll: {
350
+ args: [FFIType.u32, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.u32],
351
+ returns: FFIType.void
352
+ },
353
+ bunite_view_mouse: {
354
+ args: [FFIType.u32, FFIType.i32, FFIType.f64, FFIType.f64, FFIType.i32, FFIType.u32],
355
+ returns: FFIType.void
356
+ },
357
+ bunite_view_respond_dialog: {
358
+ args: [FFIType.u32, FFIType.u32, FFIType.bool, FFIType.cstring],
359
+ returns: FFIType.void
360
+ },
361
+ bunite_view_screenshot: {
362
+ args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.i32],
363
+ returns: FFIType.void
364
+ },
365
+ bunite_view_accessibility_snapshot: {
366
+ args: [FFIType.u32, FFIType.u32, FFIType.i32],
367
+ returns: FFIType.void
368
+ },
369
+ bunite_view_list_frames: {
370
+ args: [FFIType.u32, FFIType.u32],
371
+ returns: FFIType.void
372
+ },
373
+ bunite_view_evaluate_in_frame: {
374
+ args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.cstring],
375
+ returns: FFIType.void
376
+ },
377
+ bunite_view_resolve_and_click: {
378
+ args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.cstring, FFIType.i32, FFIType.i32, FFIType.u32],
379
+ returns: FFIType.void
380
+ },
381
+ bunite_view_set_download_policy: {
382
+ args: [FFIType.u32, FFIType.i32, FFIType.cstring],
383
+ returns: FFIType.void
384
+ },
385
+ bunite_view_popup_accept: {
386
+ args: [FFIType.u32, FFIType.u32, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64],
387
+ returns: FFIType.void
388
+ },
389
+ bunite_view_popup_dismiss: {
390
+ args: [FFIType.u32],
391
+ returns: FFIType.void
392
+ },
393
+ bunite_view_capabilities: {
394
+ args: [FFIType.u32],
395
+ returns: FFIType.u32
396
+ },
292
397
  bunite_view_load_url: {
293
398
  args: [FFIType.u32, FFIType.cstring],
294
399
  returns: FFIType.void
@@ -350,6 +455,75 @@ export function setEvaluateResultHandler(handler: (viewId: number, result: Nativ
350
455
  evaluateResultHandler = handler;
351
456
  }
352
457
 
458
+ export type NativeScreenshotResult = {
459
+ requestId: number;
460
+ ok: boolean;
461
+ format?: "png" | "jpeg";
462
+ mime?: string;
463
+ dataBase64?: string;
464
+ code?: string;
465
+ message?: string;
466
+ };
467
+ let screenshotResultHandler: ((viewId: number, result: NativeScreenshotResult) => void) | null = null;
468
+ export function setScreenshotResultHandler(handler: (viewId: number, result: NativeScreenshotResult) => void) {
469
+ screenshotResultHandler = handler;
470
+ }
471
+
472
+ export type NativeAccessibilityResult = {
473
+ requestId: number;
474
+ ok: boolean;
475
+ tree?: unknown; // CDP Accessibility.AXNode tree as JSON value
476
+ code?: string;
477
+ message?: string;
478
+ };
479
+ let accessibilityResultHandler: ((viewId: number, result: NativeAccessibilityResult) => void) | null = null;
480
+ export function setAccessibilityResultHandler(handler: (viewId: number, result: NativeAccessibilityResult) => void) {
481
+ accessibilityResultHandler = handler;
482
+ }
483
+
484
+ export type NativeListFramesResult = {
485
+ requestId: number;
486
+ ok: boolean;
487
+ /** Raw CDP `Page.getFrameTree` result (`{frameTree: {frame, childFrames}}`). TS flattens. */
488
+ raw?: unknown;
489
+ code?: string;
490
+ message?: string;
491
+ };
492
+ let listFramesResultHandler: ((viewId: number, result: NativeListFramesResult) => void) | null = null;
493
+ export function setListFramesResultHandler(handler: (viewId: number, result: NativeListFramesResult) => void) {
494
+ listFramesResultHandler = handler;
495
+ }
496
+
497
+ export type NativeResolveAndClickResult = {
498
+ requestId: number;
499
+ ok: boolean;
500
+ rect?: { x: number; y: number; width: number; height: number };
501
+ isTrustedEvent?: boolean;
502
+ code?: string;
503
+ message?: string;
504
+ };
505
+ let resolveAndClickResultHandler: ((viewId: number, result: NativeResolveAndClickResult) => void) | null = null;
506
+ export function setResolveAndClickResultHandler(handler: (viewId: number, result: NativeResolveAndClickResult) => void) {
507
+ resolveAndClickResultHandler = handler;
508
+ }
509
+
510
+ export type NativeDownloadEvent = {
511
+ kind: "started" | "progress" | "completed" | "failed" | "blocked";
512
+ id: string;
513
+ url?: string;
514
+ suggestedFilename?: string;
515
+ mimeType?: string;
516
+ sizeBytes?: number;
517
+ receivedBytes?: number;
518
+ totalBytes?: number;
519
+ localPath?: string;
520
+ reason?: string;
521
+ };
522
+ let downloadEventHandler: ((viewId: number, event: NativeDownloadEvent) => void) | null = null;
523
+ export function setDownloadEventHandler(handler: (viewId: number, event: NativeDownloadEvent) => void) {
524
+ downloadEventHandler = handler;
525
+ }
526
+
353
527
  // Per-view deferred resolvers for "view-ready" (OnAfterCreated).
354
528
  const viewReadyResolvers = new Map<number, () => void>();
355
529
 
@@ -499,6 +673,47 @@ function registerNativeCallbacks(library: LoadedNativeLibrary) {
499
673
  evaluateResultHandler?.(viewId, parsed);
500
674
  break;
501
675
  }
676
+ case "screenshot-result": {
677
+ const parsed = maybeParsePayload(payload) as NativeScreenshotResult;
678
+ screenshotResultHandler?.(viewId, parsed);
679
+ break;
680
+ }
681
+ case "accessibility-result": {
682
+ const parsed = maybeParsePayload(payload) as NativeAccessibilityResult;
683
+ accessibilityResultHandler?.(viewId, parsed);
684
+ break;
685
+ }
686
+ case "list-frames-result": {
687
+ const parsed = maybeParsePayload(payload) as NativeListFramesResult;
688
+ listFramesResultHandler?.(viewId, parsed);
689
+ break;
690
+ }
691
+ case "resolve-and-click-result": {
692
+ const parsed = maybeParsePayload(payload) as NativeResolveAndClickResult;
693
+ resolveAndClickResultHandler?.(viewId, parsed);
694
+ break;
695
+ }
696
+ case "download-event": {
697
+ const parsed = maybeParsePayload(payload) as NativeDownloadEvent;
698
+ downloadEventHandler?.(viewId, parsed);
699
+ buniteEventEmitter.emitEvent(
700
+ buniteEventEmitter.events.webview.downloadEvent(parsed),
701
+ viewId
702
+ );
703
+ break;
704
+ }
705
+ case "popup-requested": {
706
+ const parsed = maybeParsePayload(payload) as {
707
+ newSurfaceId: number;
708
+ url: string;
709
+ disposition: "tab" | "window" | "popup";
710
+ };
711
+ buniteEventEmitter.emitEvent(
712
+ buniteEventEmitter.events.webview.popupRequested(parsed),
713
+ viewId
714
+ );
715
+ break;
716
+ }
502
717
  case "title-changed": {
503
718
  const parsed = maybeParsePayload(payload) as { title: string };
504
719
  buniteEventEmitter.emitEvent(
@@ -507,6 +722,53 @@ function registerNativeCallbacks(library: LoadedNativeLibrary) {
507
722
  );
508
723
  break;
509
724
  }
725
+ case "load-start":
726
+ buniteEventEmitter.emitEvent(
727
+ buniteEventEmitter.events.webview.loadStart({ detail: payload }),
728
+ viewId
729
+ );
730
+ break;
731
+ case "load-finish":
732
+ buniteEventEmitter.emitEvent(
733
+ buniteEventEmitter.events.webview.loadFinish({ detail: payload }),
734
+ viewId
735
+ );
736
+ break;
737
+ case "load-fail": {
738
+ const parsed = maybeParsePayload(payload) as { url?: string; reason?: string };
739
+ buniteEventEmitter.emitEvent(
740
+ buniteEventEmitter.events.webview.loadFail({
741
+ url: parsed.url ?? "", reason: parsed.reason,
742
+ }),
743
+ viewId
744
+ );
745
+ break;
746
+ }
747
+ case "dialog": {
748
+ const parsed = maybeParsePayload(payload) as {
749
+ requestId: number;
750
+ kind: "alert" | "confirm" | "prompt" | "beforeunload";
751
+ message: string;
752
+ defaultPrompt?: string;
753
+ };
754
+ buniteEventEmitter.emitEvent(
755
+ buniteEventEmitter.events.webview.dialog(parsed),
756
+ viewId
757
+ );
758
+ break;
759
+ }
760
+ case "console-message": {
761
+ const parsed = maybeParsePayload(payload) as {
762
+ level: "log" | "warn" | "error" | "info" | "debug";
763
+ args: string[];
764
+ ts: number;
765
+ };
766
+ buniteEventEmitter.emitEvent(
767
+ buniteEventEmitter.events.webview.consoleMessage(parsed),
768
+ viewId
769
+ );
770
+ break;
771
+ }
510
772
  }
511
773
  },
512
774
  {
@@ -656,7 +918,7 @@ export async function initNativeRuntime(
656
918
  throw new Error(`bunite: failed to load native library at ${artifacts.nativeLibPath}.`);
657
919
  }
658
920
 
659
- const EXPECTED_ABI = 5;
921
+ const EXPECTED_ABI = 11;
660
922
  const nativeAbi = nativeLibrary.symbols.bunite_abi_version();
661
923
  if (nativeAbi !== EXPECTED_ABI) {
662
924
  throw new Error(
@@ -70,11 +70,16 @@ export function buildViewPreloadScript(options: {
70
70
  }) {
71
71
  const secretKeyBase64 = Buffer.from(options.secretKey).toString("base64");
72
72
 
73
- // Per-view config — these globals are consumed by the pre-built runtime
73
+ // Per-view config — these globals are consumed by the pre-built runtime.
74
74
  const config = `var __buniteWebviewId=${options.webviewId},__buniteRpcSocketPort=${options.rpcSocketPort},__buniteSecretKeyBase64=${JSON.stringify(secretKeyBase64)};`;
75
75
 
76
76
  const runtime = getPreloadRuntime();
77
77
  const customPreload = readCustomPreload(options.preload, options.appresRoot).trim();
78
78
 
79
- return [config, runtime, customPreload].filter(Boolean).join("\n");
79
+ // `;\n` between segments guards against ASI / token-boundary issues.
80
+ const inner = [config, runtime, customPreload].filter(Boolean).join(";\n");
81
+ // Catch top-level errors so a broken segment doesn't abort the surrounding
82
+ // native IIFE wrapper. Stashes the error on globalThis for programmatic
83
+ // inspection in addition to console.error.
84
+ return `try{${inner}\n}catch(e){try{globalThis.__bunitePreloadError=e}catch(_){}try{console.error("[bunite preload] failed:",e&&e.stack||e)}catch(_){}}`;
80
85
  }