bunite-core 0.14.0 → 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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bunite-core",
3
3
  "description": "Uniting UI and Bun",
4
- "version": "0.14.0",
4
+ "version": "0.16.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "setup:cef": "bun ../tools/bunite-dev/scripts/setup-cef.ts",
@@ -24,8 +24,8 @@
24
24
  "msgpackr": "^1.11.9"
25
25
  },
26
26
  "optionalDependencies": {
27
- "bunite-native-win-x64": "0.0.12",
28
- "bunite-native-mac-arm64": "0.0.3",
29
- "bunite-native-linux-x64": "0.0.3"
27
+ "bunite-native-win-x64": "0.0.14",
28
+ "bunite-native-mac-arm64": "0.0.4",
29
+ "bunite-native-linux-x64": "0.0.4"
30
30
  }
31
31
  }
@@ -15,7 +15,7 @@ import {
15
15
  } from "../native";
16
16
  import { ensureRpcServer } from "./Socket";
17
17
  import { BrowserWindow } from "./BrowserWindow";
18
- import { createSurfaceCapImpl } from "./SurfaceManager";
18
+ import { createSurfaceCapImpl, getPopupMetricsSnapshot } from "./SurfaceManager";
19
19
  import "./SurfaceBrowserIPC";
20
20
  import { log, logLevelToInt } from "../log";
21
21
  import { RuntimeCap, SurfaceCap, PageReportingCap, IpcError, type ImplOf } from "../../rpc/index";
@@ -205,6 +205,7 @@ export class AppRuntime {
205
205
  });
206
206
  },
207
207
  }),
208
+ popupMetrics: () => getPopupMetricsSnapshot(),
208
209
  } satisfies ImplOf<typeof RuntimeCap>;
209
210
  return impl;
210
211
  }
@@ -8,13 +8,20 @@ import {
8
8
  type Connection,
9
9
  type BytesPipe,
10
10
  } from "../../rpc/index";
11
- import type { EvaluateResult, SurfaceCapabilities, ScreenshotResult, Modifier } from "../../rpc/framework";
11
+ import type {
12
+ EvaluateResult, SurfaceCapabilities, ScreenshotResult, Modifier,
13
+ AccessibilitySnapshotResult, AxNode, ListFramesResult,
14
+ ResolveAndClickArgs, ResolveAndClickResult,
15
+ } from "../../rpc/framework";
12
16
  import { encodeModifiers, resolveKey } from "./inputDispatch";
13
17
  import { createEncryptedPipe } from "../encryptedPipe";
14
18
  import {
15
19
  ensureNativeRuntime, getNativeLibrary, toCString, waitForViewReady, cancelWaitForViewReady,
16
20
  setEvaluateResultHandler, type NativeEvaluateResult,
17
21
  setScreenshotResultHandler, type NativeScreenshotResult,
22
+ setAccessibilityResultHandler, type NativeAccessibilityResult,
23
+ setListFramesResultHandler, type NativeListFramesResult,
24
+ setResolveAndClickResultHandler, type NativeResolveAndClickResult,
18
25
  } from "../native";
19
26
  import { attachBrowserViewRegistry, getRpcPort } from "./Socket";
20
27
  import { getAppRuntimeOrThrow } from "./App";
@@ -69,6 +76,175 @@ function rejectScreenshotsForView(viewId: number) {
69
76
  }
70
77
  }
71
78
 
79
+ type AxPending = { viewId: number; resolve: (result: AccessibilitySnapshotResult) => void; interestingOnly: boolean };
80
+ let nextAxRequestId = 1;
81
+ const axResolvers = new Map<number, AxPending>();
82
+
83
+ function registerAxRequest(viewId: number, resolve: (result: AccessibilitySnapshotResult) => void, interestingOnly: boolean): number {
84
+ const id = nextAxRequestId++;
85
+ axResolvers.set(id, { viewId, resolve, interestingOnly });
86
+ return id;
87
+ }
88
+
89
+ function rejectAxForView(viewId: number) {
90
+ for (const [reqId, entry] of axResolvers) {
91
+ if (entry.viewId === viewId) {
92
+ axResolvers.delete(reqId);
93
+ entry.resolve({ ok: false, code: "not_supported", message: "view destroyed" });
94
+ }
95
+ }
96
+ }
97
+
98
+ // CDP `Accessibility.getFullAXTree` returns `{nodes: [flat]}` with `childIds`
99
+ // references — build a nested tree from the first node. When `interestingOnly`
100
+ // is true, ignored nodes are dropped and their children reparent up.
101
+ function convertAxTree(cdpResult: { nodes?: any[] } | undefined, interestingOnly: boolean): AxNode {
102
+ const flat = cdpResult?.nodes ?? [];
103
+ if (flat.length === 0) return { nodeId: "", role: "", name: "" };
104
+ const byId = new Map<string, any>();
105
+ for (const n of flat) if (n?.nodeId != null) byId.set(String(n.nodeId), n);
106
+ // Root = node without a parentId (rather than relying on flat[0] ordering).
107
+ const root = flat.find((n) => n?.parentId == null) ?? flat[0];
108
+ // Walk childIds skipping ignored nodes (when filtering) and produce a flat list
109
+ // of "interesting" descendants for the given node.
110
+ const seen = new Set<string>();
111
+ const interestingDescendants = (cdpNode: any): any[] => {
112
+ const out: any[] = [];
113
+ if (!Array.isArray(cdpNode?.childIds)) return out;
114
+ for (const cid of cdpNode.childIds) {
115
+ const child = byId.get(String(cid));
116
+ if (!child) continue;
117
+ if (interestingOnly && child.ignored === true) {
118
+ out.push(...interestingDescendants(child));
119
+ } else {
120
+ out.push(child);
121
+ }
122
+ }
123
+ return out;
124
+ };
125
+ const build = (n: any): AxNode => {
126
+ const id = String(n?.nodeId ?? "");
127
+ if (id && seen.has(id)) return { nodeId: id, role: "", name: "" }; // cycle guard
128
+ if (id) seen.add(id);
129
+ const props = new Map<string, unknown>();
130
+ if (Array.isArray(n?.properties)) {
131
+ for (const p of n.properties) {
132
+ if (p?.name && p.value && "value" in p.value) props.set(p.name, p.value.value);
133
+ }
134
+ }
135
+ const out: AxNode = {
136
+ nodeId: id,
137
+ role: String(n?.role?.value ?? ""),
138
+ name: String(n?.name?.value ?? ""),
139
+ };
140
+ if (n?.value?.value !== undefined) out.value = String(n.value.value);
141
+ if (n?.description?.value !== undefined) out.description = String(n.description.value);
142
+ const level = props.get("level"); if (typeof level === "number") out.level = level;
143
+ const checked = props.get("checked"); if (checked === true || checked === false || checked === "mixed") out.checked = checked;
144
+ const pressed = props.get("pressed"); if (pressed === true || pressed === false || pressed === "mixed") out.pressed = pressed;
145
+ const expanded = props.get("expanded"); if (typeof expanded === "boolean") out.expanded = expanded;
146
+ const disabled = props.get("disabled"); if (typeof disabled === "boolean") out.disabled = disabled;
147
+ const focused = props.get("focused"); if (typeof focused === "boolean") out.focused = focused;
148
+ const invalid = props.get("invalid"); if (typeof invalid === "boolean") out.invalid = invalid;
149
+ const required = props.get("required"); if (typeof required === "boolean") out.required = required;
150
+ const selected = props.get("selected"); if (typeof selected === "boolean") out.selected = selected;
151
+ const kids = interestingDescendants(n).map(build);
152
+ if (kids.length > 0) out.children = kids;
153
+ return out;
154
+ };
155
+ return build(root);
156
+ }
157
+
158
+ type FramesPending = { viewId: number; resolve: (result: ListFramesResult) => void };
159
+ let nextFramesRequestId = 1;
160
+ const framesResolvers = new Map<number, FramesPending>();
161
+ function registerFramesRequest(viewId: number, resolve: (result: ListFramesResult) => void): number {
162
+ const id = nextFramesRequestId++;
163
+ framesResolvers.set(id, { viewId, resolve });
164
+ return id;
165
+ }
166
+ function rejectFramesForView(viewId: number) {
167
+ for (const [reqId, entry] of framesResolvers) {
168
+ if (entry.viewId === viewId) {
169
+ framesResolvers.delete(reqId);
170
+ entry.resolve({ ok: false, code: "runtime_error", message: "view destroyed" });
171
+ }
172
+ }
173
+ }
174
+
175
+ type ResolveAndClickPending = { viewId: number; resolve: (result: ResolveAndClickResult) => void };
176
+ let nextResolveAndClickRequestId = 1;
177
+ const resolveAndClickResolvers = new Map<number, ResolveAndClickPending>();
178
+ function registerResolveAndClickRequest(viewId: number, resolve: (result: ResolveAndClickResult) => void): number {
179
+ const id = nextResolveAndClickRequestId++;
180
+ resolveAndClickResolvers.set(id, { viewId, resolve });
181
+ return id;
182
+ }
183
+ function rejectResolveAndClickForView(viewId: number) {
184
+ for (const [reqId, entry] of resolveAndClickResolvers) {
185
+ if (entry.viewId === viewId) {
186
+ resolveAndClickResolvers.delete(reqId);
187
+ entry.resolve({ ok: false, code: "runtime_error", message: "view destroyed" });
188
+ }
189
+ }
190
+ }
191
+
192
+ function flattenFrameTree(raw: any): { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string }[] {
193
+ const out: { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string }[] = [];
194
+ const walk = (node: any, parent: string | null) => {
195
+ const f = node?.frame;
196
+ if (!f) return;
197
+ const entry: { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string } = {
198
+ frameId: String(f.id ?? ""),
199
+ parentFrameId: parent,
200
+ origin: String(f.securityOrigin ?? ""),
201
+ url: String(f.url ?? ""),
202
+ };
203
+ if (typeof f.name === "string" && f.name.length > 0) entry.name = f.name;
204
+ out.push(entry);
205
+ if (Array.isArray(node.childFrames)) for (const c of node.childFrames) walk(c, entry.frameId);
206
+ };
207
+ const root = raw?.frameTree;
208
+ if (root) walk(root, null);
209
+ return out;
210
+ }
211
+
212
+ setListFramesResultHandler((viewId, raw: NativeListFramesResult) => {
213
+ const entry = framesResolvers.get(raw.requestId);
214
+ if (!entry || entry.viewId !== viewId) return;
215
+ framesResolvers.delete(raw.requestId);
216
+ if (raw.ok && raw.raw) {
217
+ try {
218
+ const frames = flattenFrameTree(raw.raw);
219
+ entry.resolve({ ok: true, frames });
220
+ } catch (e) {
221
+ entry.resolve({ ok: false, code: "runtime_error", message: `frame tree flatten failed: ${(e as Error).message}` });
222
+ }
223
+ } else {
224
+ entry.resolve({
225
+ ok: false,
226
+ code: (raw.code as "not_supported" | "runtime_error") ?? "runtime_error",
227
+ message: raw.message ?? "list frames failed",
228
+ });
229
+ }
230
+ });
231
+
232
+ setAccessibilityResultHandler((viewId, raw: NativeAccessibilityResult) => {
233
+ const entry = axResolvers.get(raw.requestId);
234
+ if (!entry || entry.viewId !== viewId) return;
235
+ axResolvers.delete(raw.requestId);
236
+ if (raw.ok && raw.tree) {
237
+ try { entry.resolve({ ok: true, tree: convertAxTree(raw.tree as any, entry.interestingOnly) }); }
238
+ catch (e) { entry.resolve({ ok: false, code: "runtime_error", message: `ax tree convert failed: ${(e as Error).message}` }); }
239
+ } else {
240
+ entry.resolve({
241
+ ok: false,
242
+ code: (raw.code as "not_supported" | "runtime_error" | "timeout") ?? "runtime_error",
243
+ message: raw.message ?? "accessibility snapshot failed",
244
+ });
245
+ }
246
+ });
247
+
72
248
  function decodeBase64(b64: string): Uint8Array {
73
249
  const bin = atob(b64);
74
250
  const out = new Uint8Array(bin.length);
@@ -96,6 +272,19 @@ setScreenshotResultHandler((viewId, raw: NativeScreenshotResult) => {
96
272
  }
97
273
  });
98
274
 
275
+ setResolveAndClickResultHandler((viewId, raw: NativeResolveAndClickResult) => {
276
+ const entry = resolveAndClickResolvers.get(raw.requestId);
277
+ if (!entry || entry.viewId !== viewId) return;
278
+ resolveAndClickResolvers.delete(raw.requestId);
279
+ if (raw.ok && raw.rect) {
280
+ entry.resolve({ ok: true, rect: raw.rect, isTrustedEvent: !!raw.isTrustedEvent });
281
+ } else {
282
+ type FailCode = "not_found" | "not_visible" | "runtime_error" | "cross_origin" | "not_supported";
283
+ const code = (raw.code as FailCode | undefined) ?? "runtime_error";
284
+ entry.resolve({ ok: false, code, message: raw.message ?? "resolveAndClick failed" });
285
+ }
286
+ });
287
+
99
288
  setEvaluateResultHandler((viewId, raw: NativeEvaluateResult) => {
100
289
  const entry = evaluateResolvers.get(raw.requestId);
101
290
  if (!entry) return;
@@ -131,6 +320,12 @@ const CAP_FORMAT_JPEG = 1 << 10;
131
320
  const CAP_MOUSE = 1 << 11;
132
321
  const CAP_DIALOGS = 1 << 12;
133
322
  const CAP_CONSOLE = 1 << 13;
323
+ const CAP_AX = 1 << 15;
324
+ const CAP_BOUNDING_RECT = 1 << 16;
325
+ const CAP_FRAMES = 1 << 17;
326
+ const CAP_DOWNLOADS = 1 << 18;
327
+ const CAP_POPUPS = 1 << 19;
328
+ const CAP_RESOLVE_AND_CLICK = 1 << 20;
134
329
 
135
330
  function decodeCapabilityBits(bits: number): SurfaceCapabilities {
136
331
  const formats: ("png" | "jpeg")[] = [];
@@ -149,6 +344,12 @@ function decodeCapabilityBits(bits: number): SurfaceCapabilities {
149
344
  dialogs: !!(bits & CAP_DIALOGS),
150
345
  console: !!(bits & CAP_CONSOLE),
151
346
  screenshot: !!(bits & CAP_SCREENSHOT),
347
+ accessibilitySnapshot: !!(bits & CAP_AX),
348
+ getBoundingRect: !!(bits & CAP_BOUNDING_RECT),
349
+ frames: !!(bits & CAP_FRAMES),
350
+ downloads: !!(bits & CAP_DOWNLOADS),
351
+ popups: !!(bits & CAP_POPUPS),
352
+ resolveAndClick: !!(bits & CAP_RESOLVE_AND_CLICK),
152
353
  ...(formats.length > 0 ? { formats } : {}),
153
354
  };
154
355
  }
@@ -208,9 +409,36 @@ export class BrowserView {
208
409
  sandbox: boolean;
209
410
  secretKey: Uint8Array;
210
411
 
211
- constructor(options: Partial<BrowserViewOptions>) {
412
+ /** Wrap a pre-existing native view (popup mint). Skips `bunite_view_create`;
413
+ * the new view is then attached to the host window via `bunite_view_popup_accept`. */
414
+ static adopt(args: {
415
+ nativeViewId: number;
416
+ hostWindowId: number;
417
+ bounds: { x: number; y: number; width: number; height: number };
418
+ appresRoot: string | null;
419
+ }): BrowserView {
420
+ return new BrowserView({
421
+ adoptNativeViewId: args.nativeViewId,
422
+ windowId: args.hostWindowId,
423
+ frame: args.bounds,
424
+ appresRoot: args.appresRoot,
425
+ autoResize: false,
426
+ } as Partial<BrowserViewOptions> & { adoptNativeViewId: number });
427
+ }
428
+
429
+ static dismissPopupById(newSurfaceId: number) {
430
+ getNativeLibrary()?.symbols.bunite_view_popup_dismiss(newSurfaceId);
431
+ }
432
+
433
+ constructor(options: Partial<BrowserViewOptions> & { adoptNativeViewId?: number }) {
212
434
  ensureNativeRuntime();
213
435
 
436
+ const adopting = options.adoptNativeViewId != null;
437
+ if (adopting) {
438
+ this.id = options.adoptNativeViewId!;
439
+ // Adopted IDs live in the upper u32 half (popup namespace). Keep TS's
440
+ // sequential allocator untouched so normal creates stay below 0x80000000.
441
+ }
214
442
  this.windowId = options.windowId ?? defaultOptions.windowId;
215
443
  this.url = options.url ?? defaultOptions.url;
216
444
  this.html = options.html ?? defaultOptions.html;
@@ -242,23 +470,33 @@ export class BrowserView {
242
470
 
243
471
  BrowserViewMap[this.id] = this;
244
472
  this._readyPromise = waitForViewReady(this.id);
245
- this.nativeAttached =
246
- getNativeLibrary()?.symbols.bunite_view_create(
247
- this.id,
248
- this.windowId,
249
- toCString(this.url ?? ""),
250
- toCString(this.html ?? ""),
251
- toCString(preloadScript),
252
- toCString(this.appresRoot ?? ""),
253
- toCString(this.navigationRules ? JSON.stringify(this.navigationRules) : ""),
254
- this.frame.x,
255
- this.frame.y,
256
- this.frame.width,
257
- this.frame.height,
258
- this.autoResize,
259
- this.sandbox,
260
- toCString(this.preloadOrigins ? JSON.stringify(this.preloadOrigins) : "")
261
- ) ?? false;
473
+ if (adopting) {
474
+ // Native popup mint already created the view; bind to host window + bounds.
475
+ const lib = getNativeLibrary();
476
+ lib?.symbols.bunite_view_popup_accept(
477
+ this.id, this.windowId,
478
+ this.frame.x, this.frame.y, this.frame.width, this.frame.height,
479
+ );
480
+ this.nativeAttached = true;
481
+ } else {
482
+ this.nativeAttached =
483
+ getNativeLibrary()?.symbols.bunite_view_create(
484
+ this.id,
485
+ this.windowId,
486
+ toCString(this.url ?? ""),
487
+ toCString(this.html ?? ""),
488
+ toCString(preloadScript),
489
+ toCString(this.appresRoot ?? ""),
490
+ toCString(this.navigationRules ? JSON.stringify(this.navigationRules) : ""),
491
+ this.frame.x,
492
+ this.frame.y,
493
+ this.frame.width,
494
+ this.frame.height,
495
+ this.autoResize,
496
+ this.sandbox,
497
+ toCString(this.preloadOrigins ? JSON.stringify(this.preloadOrigins) : "")
498
+ ) ?? false;
499
+ }
262
500
 
263
501
  if (this.nativeAttached) {
264
502
  this.on("did-navigate", (event: any) => {
@@ -342,13 +580,74 @@ export class BrowserView {
342
580
  }
343
581
  }
344
582
 
345
- evaluate(script: string): Promise<EvaluateResult> {
583
+ evaluate(script: string, frameId?: string): Promise<EvaluateResult> {
346
584
  if (!this.nativeAttached) {
347
585
  return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
348
586
  }
349
587
  return new Promise<EvaluateResult>((resolve) => {
350
- const requestId = registerEvaluateRequest(this.id, resolve);
351
- getNativeLibrary()?.symbols.bunite_view_evaluate(this.id, requestId, toCString(script));
588
+ let timer: ReturnType<typeof setTimeout> | null = null;
589
+ const wrappedResolve = (r: EvaluateResult) => { if (timer) clearTimeout(timer); resolve(r); };
590
+ const requestId = registerEvaluateRequest(this.id, wrappedResolve);
591
+ // 30s timeout — two-hop CDP path (createIsolatedWorld → Runtime.evaluate)
592
+ // can silently hang if the frame is destroyed mid-flight.
593
+ timer = setTimeout(() => {
594
+ if (evaluateResolvers.delete(requestId)) {
595
+ resolve({ ok: false, code: "timeout", message: "evaluate timed out after 30s" });
596
+ }
597
+ }, 30_000);
598
+ const lib = getNativeLibrary();
599
+ if (frameId) {
600
+ lib?.symbols.bunite_view_evaluate_in_frame(this.id, requestId, toCString(script), toCString(frameId));
601
+ } else {
602
+ lib?.symbols.bunite_view_evaluate(this.id, requestId, toCString(script));
603
+ }
604
+ });
605
+ }
606
+
607
+ setDownloadPolicy(policy: "auto" | "ask" | "block", downloadDir?: string) {
608
+ if (!this.nativeAttached) return;
609
+ const policyCode = policy === "auto" ? 0 : policy === "ask" ? 1 : 2;
610
+ getNativeLibrary()?.symbols.bunite_view_set_download_policy(
611
+ this.id, policyCode, toCString(downloadDir ?? "")
612
+ );
613
+ }
614
+
615
+ listFrames(): Promise<ListFramesResult> {
616
+ if (!this.nativeAttached) {
617
+ return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
618
+ }
619
+ return new Promise<ListFramesResult>((resolve) => {
620
+ const requestId = registerFramesRequest(this.id, resolve);
621
+ const timer = setTimeout(() => {
622
+ if (framesResolvers.delete(requestId)) {
623
+ resolve({ ok: false, code: "runtime_error", message: "list frames timed out after 10s" });
624
+ }
625
+ }, 10_000);
626
+ const wrappedResolve = (r: ListFramesResult) => { clearTimeout(timer); resolve(r); };
627
+ framesResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
628
+ getNativeLibrary()?.symbols.bunite_view_list_frames(this.id, requestId);
629
+ });
630
+ }
631
+
632
+ resolveAndClick(args: ResolveAndClickArgs): Promise<ResolveAndClickResult> {
633
+ if (!this.nativeAttached) {
634
+ return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
635
+ }
636
+ return new Promise<ResolveAndClickResult>((resolve) => {
637
+ const requestId = registerResolveAndClickRequest(this.id, resolve);
638
+ const timer = setTimeout(() => {
639
+ if (resolveAndClickResolvers.delete(requestId)) {
640
+ resolve({ ok: false, code: "runtime_error", message: "resolveAndClick timed out after 5s" });
641
+ }
642
+ }, 5_000);
643
+ const wrappedResolve = (r: ResolveAndClickResult) => { clearTimeout(timer); resolve(r); };
644
+ resolveAndClickResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
645
+ const button = args.button === "right" ? 2 : args.button === "middle" ? 1 : 0;
646
+ getNativeLibrary()?.symbols.bunite_view_resolve_and_click(
647
+ this.id, requestId,
648
+ toCString(args.selector), toCString(args.frameId ?? ""),
649
+ button, args.clickCount ?? 1, encodeModifiers(args.modifiers),
650
+ );
352
651
  });
353
652
  }
354
653
 
@@ -440,6 +739,24 @@ export class BrowserView {
440
739
  });
441
740
  }
442
741
 
742
+ accessibilitySnapshot(interestingOnly: boolean): Promise<AccessibilitySnapshotResult> {
743
+ if (!this.nativeAttached) {
744
+ return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
745
+ }
746
+ return new Promise<AccessibilitySnapshotResult>((resolve) => {
747
+ const requestId = registerAxRequest(this.id, resolve, interestingOnly);
748
+ const timer = setTimeout(() => {
749
+ if (axResolvers.delete(requestId)) {
750
+ resolve({ ok: false, code: "timeout", message: "accessibility snapshot timed out after 30s" });
751
+ }
752
+ }, 30_000);
753
+ const wrappedResolve = (r: AccessibilitySnapshotResult) => { clearTimeout(timer); resolve(r); };
754
+ axResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve, interestingOnly });
755
+ // Native flag is currently unused (filter is TS-side); kept for ABI shape stability.
756
+ getNativeLibrary()?.symbols.bunite_view_accessibility_snapshot(this.id, requestId, interestingOnly ? 1 : 0);
757
+ });
758
+ }
759
+
443
760
  goBack() {
444
761
  if (this.nativeAttached) {
445
762
  getNativeLibrary()?.symbols.bunite_view_go_back(this.id);
@@ -546,9 +863,13 @@ export class BrowserView {
546
863
  cancelWaitForViewReady(this.id);
547
864
  rejectEvaluatesForView(this.id);
548
865
  rejectScreenshotsForView(this.id);
866
+ rejectAxForView(this.id);
867
+ rejectFramesForView(this.id);
868
+ rejectResolveAndClickForView(this.id);
549
869
  this.nativeAttached = false;
550
870
  for (const eventName of [
551
- "will-navigate", "did-navigate", "dom-ready", "new-window-open", "permission-requested", "title-changed"
871
+ "will-navigate", "did-navigate", "dom-ready", "new-window-open", "permission-requested", "title-changed",
872
+ "load-start", "load-finish", "load-fail", "dialog", "console-message", "download-event", "popup-requested",
552
873
  ]) {
553
874
  buniteEventEmitter.removeAllListeners(`${eventName}-${this.id}`);
554
875
  }
@@ -556,7 +877,7 @@ export class BrowserView {
556
877
  }
557
878
 
558
879
  on(
559
- name: "will-navigate" | "did-navigate" | "dom-ready" | "new-window-open" | "permission-requested" | "title-changed" | "load-start" | "load-finish" | "load-fail" | "dialog" | "console-message",
880
+ name: "will-navigate" | "did-navigate" | "dom-ready" | "new-window-open" | "permission-requested" | "title-changed" | "load-start" | "load-finish" | "load-fail" | "dialog" | "console-message" | "download-event" | "popup-requested",
560
881
  handler: (event: unknown) => void
561
882
  ) {
562
883
  const specificName = `${name}-${this.id}`;
@@ -1,9 +1,11 @@
1
1
  import {
2
- onSurfaceInit, emitSurfaceEvent, emitConsole,
2
+ onSurfaceInit, emitSurfaceEvent, emitConsole, emitDownload, emitPopupRequested,
3
3
  registerDialogRequest, disposeSurfaceState, clearConsoleBuffer,
4
4
  } from "./SurfaceManager";
5
+ import { log } from "../log";
5
6
 
6
7
  onSurfaceInit((surfaceId, hostViewId, view) => {
8
+ log.debug("surface/init surfaceId=" + surfaceId + " hostViewId=" + hostViewId);
7
9
  view.on("did-navigate", (event: any) => {
8
10
  emitSurfaceEvent(hostViewId, surfaceId, { type: "navigate", url: event.data.detail });
9
11
  });
@@ -32,6 +34,7 @@ onSurfaceInit((surfaceId, hostViewId, view) => {
32
34
  message: string;
33
35
  defaultPrompt?: string;
34
36
  };
37
+ log.debug("surface/dialog-ipc surfaceId=" + surfaceId + " hostViewId=" + hostViewId + " kind=" + d.kind);
35
38
  registerDialogRequest(hostViewId, surfaceId, d);
36
39
  });
37
40
  view.on("console-message", (event: any) => {
@@ -39,6 +42,12 @@ onSurfaceInit((surfaceId, hostViewId, view) => {
39
42
  // microtask — no extra deferral needed at the listener level.
40
43
  emitConsole(hostViewId, surfaceId, event.data);
41
44
  });
45
+ view.on("download-event", (event: any) => {
46
+ emitDownload(hostViewId, surfaceId, event.data);
47
+ });
48
+ view.on("popup-requested", (event: any) => {
49
+ emitPopupRequested(hostViewId, surfaceId, event.data);
50
+ });
42
51
  });
43
52
 
44
53
  // Surface registry's untrackSurface doesn't fire a teardown event — wire it