bunite-core 0.14.0 → 0.17.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 +4 -4
- package/src/host/core/App.ts +6 -3
- package/src/host/core/BrowserView.ts +345 -24
- package/src/host/core/BrowserWindow.ts +52 -6
- package/src/host/core/SurfaceBrowserIPC.ts +10 -1
- package/src/host/core/SurfaceManager.ts +357 -16
- package/src/host/core/windowCap.ts +69 -0
- package/src/host/events/webviewEvents.ts +18 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +145 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +225 -1
- package/src/native/linux/bunite_linux_internal.h +12 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_view.cpp +211 -5
- package/src/native/mac/bunite_mac_ffi.mm +293 -4
- package/src/native/mac/bunite_mac_internal.h +13 -0
- package/src/native/mac/bunite_mac_view.mm +227 -7
- package/src/native/shared/ffi_exports.h +97 -30
- package/src/native/win/native_host_cef.cpp +107 -13
- package/src/native/win/native_host_ffi.cpp +831 -2
- package/src/native/win/native_host_internal.h +22 -0
- package/src/native/win/native_host_runtime.cpp +34 -0
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +827 -5
- package/src/native/win-webview2/webview2_internal.h +19 -0
- package/src/native/win-webview2/webview2_runtime.cpp +383 -31
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +39 -0
- package/src/rpc/framework.ts +194 -12
- package/src/rpc/index.ts +12 -0
- package/src/rpc/peer.ts +1 -1
- package/src/webview/native.ts +142 -32
- package/src/webview/polyfill.ts +91 -14
package/src/rpc/framework.ts
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { call, defineCap, stream, cap } from "./schema";
|
|
2
2
|
import type { CapDef } from "./schema";
|
|
3
3
|
|
|
4
|
+
/** Window state for custom-titlebar rendering (max/restore glyph + focus ring). */
|
|
5
|
+
export interface WindowState {
|
|
6
|
+
maximized: boolean;
|
|
7
|
+
minimized: boolean;
|
|
8
|
+
focused: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export const BrowserWindowCap = defineCap("bunite.BrowserWindow", {
|
|
5
12
|
focus: call<void, void>(),
|
|
6
|
-
close: call<void, void>(),
|
|
13
|
+
close: call<void, void>(), // vetoable — routes through close-requested
|
|
7
14
|
setBounds: call<{ x: number; y: number; w: number; h: number }, void>(),
|
|
8
15
|
setTitle: call<{ title: string }, void>(),
|
|
9
16
|
id: call<void, number>({ idempotent: true }),
|
|
10
17
|
label: call<void, string>({ idempotent: true }),
|
|
18
|
+
minimize: call<void, void>(),
|
|
19
|
+
unminimize: call<void, void>(),
|
|
20
|
+
maximize: call<void, void>(),
|
|
21
|
+
unmaximize: call<void, void>(),
|
|
22
|
+
toggleMaximize: call<void, void>(),
|
|
23
|
+
getState: call<void, WindowState>({ idempotent: true }),
|
|
24
|
+
stateWatch: stream<void, WindowState>(), // emits current state on subscribe, then on change
|
|
25
|
+
// Start an OS window move (Tauri startDragging equiv) — call from a custom
|
|
26
|
+
// titlebar mousedown. Preload auto-calls this for `app-region: drag`; manual
|
|
27
|
+
// callers use it for custom hit-testing. Start-only; native follows the cursor.
|
|
28
|
+
beginMoveDrag: call<void, void>(),
|
|
11
29
|
});
|
|
12
30
|
|
|
13
31
|
export const WindowCap = defineCap("bunite.Window", {
|
|
14
32
|
create: call<WindowCreateOpts, typeof BrowserWindowCap>({ returns: cap(BrowserWindowCap) }),
|
|
15
33
|
list: call<void, typeof BrowserWindowCap>({ returns: cap.array(BrowserWindowCap), idempotent: true }),
|
|
34
|
+
current: call<void, typeof BrowserWindowCap>({ returns: cap(BrowserWindowCap), idempotent: true }),
|
|
16
35
|
focus: call<{ id?: number; label?: string }, void>(),
|
|
17
36
|
close: call<{ id?: number; label?: string }, void>(),
|
|
18
37
|
});
|
|
@@ -82,9 +101,9 @@ export const PageReportingCap = defineCap("bunite.PageReporting", {
|
|
|
82
101
|
|
|
83
102
|
export type SurfaceMask = { x: number; y: number; w: number; h: number };
|
|
84
103
|
|
|
85
|
-
/** Automation feature flags reported per surface.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
104
|
+
/** Automation feature flags reported per surface. Two categories: method
|
|
105
|
+
* gate (false → must not call) and property advertise (false → method runs,
|
|
106
|
+
* property degrades; per-field JSDoc below). See `.agents/architecture.md`. */
|
|
88
107
|
export interface SurfaceCapabilities {
|
|
89
108
|
evaluate: boolean;
|
|
90
109
|
crossOriginEval: boolean;
|
|
@@ -110,19 +129,142 @@ export interface SurfaceCapabilities {
|
|
|
110
129
|
screenshot: boolean;
|
|
111
130
|
/** Present only when `screenshot` is true. */
|
|
112
131
|
formats?: ("png" | "jpeg")[];
|
|
132
|
+
accessibilitySnapshot: boolean;
|
|
133
|
+
getBoundingRect: boolean;
|
|
134
|
+
/** `listFrames` works + `evaluate({frameId})` reaches the target frame's
|
|
135
|
+
* isolated world. Frame-targeted input dispatch is not yet implemented. */
|
|
136
|
+
frames: boolean;
|
|
137
|
+
/** Backend can intercept downloads and emit lifecycle events. When `false`,
|
|
138
|
+
* every download attempt emits `{kind: "blocked", reason: "not_supported"}`. */
|
|
139
|
+
downloads: boolean;
|
|
140
|
+
/** Backend can intercept popups (window.open / target=_blank / cmd-click) and
|
|
141
|
+
* mint a new surface with the opener relationship preserved. Host adopts via
|
|
142
|
+
* `acceptPopup({newSurfaceId, hostViewId, bounds})`. */
|
|
143
|
+
popups: boolean;
|
|
144
|
+
/** Atomic `resolveAndClick(selector)`. Click trust is per-call (see `ResolveAndClickResult`). */
|
|
145
|
+
resolveAndClick: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface AxNode {
|
|
149
|
+
nodeId: string;
|
|
150
|
+
role: string;
|
|
151
|
+
name: string;
|
|
152
|
+
value?: string;
|
|
153
|
+
description?: string;
|
|
154
|
+
level?: number;
|
|
155
|
+
checked?: boolean | "mixed";
|
|
156
|
+
pressed?: boolean | "mixed";
|
|
157
|
+
expanded?: boolean;
|
|
158
|
+
disabled?: boolean;
|
|
159
|
+
focused?: boolean;
|
|
160
|
+
invalid?: boolean;
|
|
161
|
+
required?: boolean;
|
|
162
|
+
selected?: boolean;
|
|
163
|
+
children?: AxNode[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface AccessibilitySnapshotArgs {
|
|
167
|
+
surfaceId: number;
|
|
168
|
+
/** Drop nodes with `ignored:true` (and reparent their children). Default true. */
|
|
169
|
+
interestingOnly?: boolean;
|
|
170
|
+
}
|
|
171
|
+
export type AccessibilitySnapshotResult =
|
|
172
|
+
| { ok: true; tree: AxNode }
|
|
173
|
+
| { ok: false; code: "not_supported" | "runtime_error" | "timeout"; message: string };
|
|
174
|
+
|
|
175
|
+
export interface BoundingRectArgs {
|
|
176
|
+
surfaceId: number;
|
|
177
|
+
selector: string;
|
|
178
|
+
/** When set, `rect` is FRAME-LOCAL (the iframe's own viewport). Use frame-local
|
|
179
|
+
* coords for further `evaluate({frameId})` ops; do not pass to `click({x,y})`
|
|
180
|
+
* which expects page-viewport coords. */
|
|
181
|
+
frameId?: string;
|
|
182
|
+
}
|
|
183
|
+
export type BoundingRectResult =
|
|
184
|
+
/** `visible` = rect has size AND intersects the frame's viewport. opacity /
|
|
185
|
+
* visibility:hidden / occlusion are NOT checked — agent must `evaluate` for those. */
|
|
186
|
+
| { ok: true; rect: { x: number; y: number; width: number; height: number }; visible: boolean }
|
|
187
|
+
| { ok: false; code: "not_found" | "runtime_error" | "cross_origin" | "not_supported"; message: string };
|
|
188
|
+
|
|
189
|
+
export interface Frame {
|
|
190
|
+
frameId: string;
|
|
191
|
+
parentFrameId: string | null;
|
|
192
|
+
origin: string;
|
|
193
|
+
url: string;
|
|
194
|
+
name?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type ListFramesResult =
|
|
198
|
+
| { ok: true; frames: Frame[] }
|
|
199
|
+
| { ok: false; code: "not_supported" | "runtime_error"; message: string };
|
|
200
|
+
|
|
201
|
+
export type DownloadPolicy = "auto" | "ask" | "block";
|
|
202
|
+
|
|
203
|
+
export type DownloadEvent =
|
|
204
|
+
| { kind: "started"; id: string; url: string; suggestedFilename: string; mimeType?: string; sizeBytes?: number }
|
|
205
|
+
| { kind: "progress"; id: string; receivedBytes: number; totalBytes?: number }
|
|
206
|
+
| { kind: "completed"; id: string; localPath: string }
|
|
207
|
+
| { kind: "failed"; id: string; reason: string }
|
|
208
|
+
| { kind: "blocked"; id: string; url: string; reason: "host-policy" | "backend-block" | "mime-blocked" | "not_supported" | "ask-not-implemented" };
|
|
209
|
+
|
|
210
|
+
export type WaitForDownloadResult =
|
|
211
|
+
| { ok: true; id: string; suggestedFilename: string; url: string; mimeType?: string; sizeBytes?: number; localPath: string }
|
|
212
|
+
| { ok: false; code: "timeout" | "blocked" | "failed" | "not_supported"; message: string };
|
|
213
|
+
|
|
214
|
+
export interface SetDownloadPolicyArgs {
|
|
215
|
+
surfaceId: number;
|
|
216
|
+
policy: DownloadPolicy;
|
|
217
|
+
/** Absolute path. When omitted, backend keeps current dir (or temp). */
|
|
218
|
+
downloadDir?: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface AcceptPopupArgs {
|
|
222
|
+
newSurfaceId: number;
|
|
223
|
+
/** The host BrowserView that will own the new surface. */
|
|
224
|
+
hostViewId: number;
|
|
225
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
226
|
+
}
|
|
227
|
+
export type AcceptPopupResult =
|
|
228
|
+
| { ok: true }
|
|
229
|
+
| { ok: false; code: "not_found" | "host_view_invalid"; message: string };
|
|
230
|
+
|
|
231
|
+
export interface ExtendPopupTimeoutArgs {
|
|
232
|
+
newSurfaceId: number;
|
|
233
|
+
/** Reset semantic: new deadline = now + gracePeriodMs. Not cumulative.
|
|
234
|
+
* Capped at 60s since the popup arm was emitted. */
|
|
235
|
+
gracePeriodMs: number;
|
|
113
236
|
}
|
|
237
|
+
export type ExtendPopupTimeoutResult =
|
|
238
|
+
| { ok: true; deadlineMs: number } // epoch ms of the new deadline
|
|
239
|
+
| { ok: false; code: "not_found" | "already_adopted" | "already_dismissed" | "cap_exceeded"; message: string };
|
|
114
240
|
|
|
115
|
-
/** Surface lifecycle event arm
|
|
116
|
-
|
|
117
|
-
* `load-start` → `navigate` → `load-finish` (order varies per backend hook
|
|
118
|
-
* sequence and isn't a strict invariant). A navigation produces `load-finish`
|
|
119
|
-
* OR `load-fail`, never both. */
|
|
120
|
-
export type SurfaceEvent =
|
|
241
|
+
/** Surface lifecycle event arm before the surface pipeline stamps `epoch`. */
|
|
242
|
+
export type SurfaceEventBase =
|
|
121
243
|
| { type: "navigate"; url: string }
|
|
122
244
|
| { type: "load-start"; url: string }
|
|
123
245
|
| { type: "load-finish"; url: string }
|
|
124
246
|
| { type: "load-fail"; url: string; reason?: string }
|
|
125
|
-
| { type: "title-change"; title: string }
|
|
247
|
+
| { type: "title-change"; title: string }
|
|
248
|
+
| {
|
|
249
|
+
type: "popup";
|
|
250
|
+
url: string;
|
|
251
|
+
disposition: "tab" | "window" | "popup";
|
|
252
|
+
openerSurfaceId: number;
|
|
253
|
+
/** Native surface ID minted before the arm is emitted. Host must call
|
|
254
|
+
* `acceptPopup` (within `popupAdoptionTimeoutMs`, default 5s) to attach,
|
|
255
|
+
* or `dismissPopup` to close. Auto-dismiss fires on timeout. */
|
|
256
|
+
newSurfaceId: number;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** Wire form of `SurfaceEventBase`. `epoch` bumps on every `navigate` (incl. SPA
|
|
260
|
+
* `pushState`); other arms carry the current epoch. See `docs/browser-automation.md`. */
|
|
261
|
+
export type SurfaceEvent = SurfaceEventBase & { epoch: number };
|
|
262
|
+
|
|
263
|
+
export interface NavigationState {
|
|
264
|
+
lastLoadEpoch: number;
|
|
265
|
+
isLoading: boolean;
|
|
266
|
+
currentUrl: string;
|
|
267
|
+
}
|
|
126
268
|
|
|
127
269
|
export type EvaluateResult =
|
|
128
270
|
| { ok: true; value: unknown }
|
|
@@ -206,6 +348,7 @@ export interface WaitForSelectorArgs {
|
|
|
206
348
|
selector: string;
|
|
207
349
|
/** Default 5000ms. Polled at 50ms intervals via `evaluate`. */
|
|
208
350
|
timeoutMs?: number;
|
|
351
|
+
frameId?: string;
|
|
209
352
|
}
|
|
210
353
|
export interface WaitForFunctionArgs {
|
|
211
354
|
surfaceId: number;
|
|
@@ -215,6 +358,7 @@ export interface WaitForFunctionArgs {
|
|
|
215
358
|
timeoutMs?: number;
|
|
216
359
|
/** Default 50ms. Increase for heavy expressions to reduce IPC load. */
|
|
217
360
|
pollIntervalMs?: number;
|
|
361
|
+
frameId?: string;
|
|
218
362
|
}
|
|
219
363
|
export type WaitResult =
|
|
220
364
|
| { ok: true }
|
|
@@ -248,6 +392,22 @@ export type ScreenshotResult =
|
|
|
248
392
|
| { ok: true; data: Uint8Array; mime: string; format: "png" | "jpeg" }
|
|
249
393
|
| { ok: false; code: "not_supported" | "runtime_error" | "timeout" | "black_frame"; message: string };
|
|
250
394
|
|
|
395
|
+
export interface ResolveAndClickArgs {
|
|
396
|
+
surfaceId: number;
|
|
397
|
+
selector: string;
|
|
398
|
+
/** Same-origin iframe (CEF/WV2). OOPIF → `cross_origin`. mac/linux → `not_supported`. */
|
|
399
|
+
frameId?: string;
|
|
400
|
+
button?: "left" | "middle" | "right";
|
|
401
|
+
clickCount?: number;
|
|
402
|
+
modifiers?: Modifier[];
|
|
403
|
+
}
|
|
404
|
+
/** rect is viewport-normalized. `isTrustedEvent` is empirical per backend —
|
|
405
|
+
* CEF/WV2 CDP `Input.dispatchMouseEvent` produces trusted events; mac NSEvent
|
|
406
|
+
* direct dispatch is also trusted. All shipped backends report `true`. */
|
|
407
|
+
export type ResolveAndClickResult =
|
|
408
|
+
| { ok: true; rect: { x: number; y: number; width: number; height: number }; isTrustedEvent: boolean }
|
|
409
|
+
| { ok: false; code: "not_found" | "not_visible" | "runtime_error" | "cross_origin" | "not_supported"; message: string };
|
|
410
|
+
|
|
251
411
|
export const SurfaceCap = defineCap("bunite.Surface", {
|
|
252
412
|
init: call<{
|
|
253
413
|
src: string;
|
|
@@ -266,7 +426,7 @@ export const SurfaceCap = defineCap("bunite.Surface", {
|
|
|
266
426
|
navigate: call<{ surfaceId: number; url: string }, void>(),
|
|
267
427
|
goBack: call<{ surfaceId: number }, void>(),
|
|
268
428
|
reload: call<{ surfaceId: number }, void>(),
|
|
269
|
-
evaluate: call<{ surfaceId: number; script: string }, EvaluateResult>(),
|
|
429
|
+
evaluate: call<{ surfaceId: number; script: string; frameId?: string }, EvaluateResult>(),
|
|
270
430
|
capabilities: call<{ surfaceId: number }, SurfaceCapabilities>(),
|
|
271
431
|
click: call<ClickArgs, void>(),
|
|
272
432
|
type: call<TypeArgs, void>(),
|
|
@@ -282,8 +442,29 @@ export const SurfaceCap = defineCap("bunite.Surface", {
|
|
|
282
442
|
surfaceEvents: stream<{ surfaceId: number }, SurfaceEvent>(),
|
|
283
443
|
dialogs: stream<{ surfaceId: number }, DialogEvent>(),
|
|
284
444
|
consoleEvents: stream<{ surfaceId: number }, ConsoleEntry>(),
|
|
445
|
+
getNavigationState: call<{ surfaceId: number }, NavigationState>({ idempotent: true }),
|
|
446
|
+
accessibilitySnapshot: call<AccessibilitySnapshotArgs, AccessibilitySnapshotResult>(),
|
|
447
|
+
getBoundingRect: call<BoundingRectArgs, BoundingRectResult>({ idempotent: true }),
|
|
448
|
+
listFrames: call<{ surfaceId: number }, ListFramesResult>({ idempotent: true }),
|
|
449
|
+
downloadEvents: stream<{ surfaceId: number }, DownloadEvent>(),
|
|
450
|
+
waitForDownload: call<{ surfaceId: number; timeoutMs?: number }, WaitForDownloadResult>(),
|
|
451
|
+
setDownloadPolicy: call<SetDownloadPolicyArgs, void>(),
|
|
452
|
+
acceptPopup: call<AcceptPopupArgs, AcceptPopupResult>(),
|
|
453
|
+
dismissPopup: call<{ newSurfaceId: number }, void>(),
|
|
454
|
+
extendPopupTimeout: call<ExtendPopupTimeoutArgs, ExtendPopupTimeoutResult>(),
|
|
455
|
+
resolveAndClick: call<ResolveAndClickArgs, ResolveAndClickResult>(),
|
|
285
456
|
});
|
|
286
457
|
|
|
458
|
+
/** Process-lifetime cumulative counts of popup lifecycle resolutions. Useful
|
|
459
|
+
* for tuning the default popup adoption timeout. */
|
|
460
|
+
export interface PopupMetrics {
|
|
461
|
+
armed: number;
|
|
462
|
+
adopted: number;
|
|
463
|
+
dismissed: number;
|
|
464
|
+
timeoutFired: number;
|
|
465
|
+
extended: number;
|
|
466
|
+
}
|
|
467
|
+
|
|
287
468
|
export const RuntimeCap = defineCap("bunite.Runtime", {
|
|
288
469
|
window: call<void, typeof WindowCap>({ returns: cap(WindowCap), idempotent: true }),
|
|
289
470
|
dialogs: call<void, typeof DialogsCap>({ returns: cap(DialogsCap), idempotent: true }),
|
|
@@ -295,6 +476,7 @@ export const RuntimeCap = defineCap("bunite.Runtime", {
|
|
|
295
476
|
themeWatch: stream<void, "light" | "dark">(),
|
|
296
477
|
surface: call<void, typeof SurfaceCap>({ returns: cap(SurfaceCap), idempotent: true }),
|
|
297
478
|
reporting: call<void, typeof PageReportingCap>({ returns: cap(PageReportingCap), idempotent: true }),
|
|
479
|
+
popupMetrics: call<void, PopupMetrics>({ idempotent: true }),
|
|
298
480
|
});
|
|
299
481
|
|
|
300
482
|
export const FRAMEWORK_TYPE_IDS = {
|
package/src/rpc/index.ts
CHANGED
|
@@ -107,11 +107,17 @@ export {
|
|
|
107
107
|
|
|
108
108
|
export type {
|
|
109
109
|
WindowCreateOpts,
|
|
110
|
+
WindowState,
|
|
110
111
|
DialogOpenFileOpts,
|
|
111
112
|
DialogSaveFileOpts,
|
|
112
113
|
DialogMessageOpts,
|
|
113
114
|
SurfaceCapabilities,
|
|
114
115
|
SurfaceEvent,
|
|
116
|
+
SurfaceEventBase,
|
|
117
|
+
NavigationState,
|
|
118
|
+
DownloadEvent,
|
|
119
|
+
DownloadPolicy,
|
|
120
|
+
WaitForDownloadResult,
|
|
115
121
|
SurfaceMask,
|
|
116
122
|
Modifier,
|
|
117
123
|
ClickArgs,
|
|
@@ -130,6 +136,12 @@ export type {
|
|
|
130
136
|
ScreenshotArgs,
|
|
131
137
|
ScreenshotResult,
|
|
132
138
|
EvaluateResult,
|
|
139
|
+
AcceptPopupArgs,
|
|
140
|
+
AcceptPopupResult,
|
|
141
|
+
ExtendPopupTimeoutArgs,
|
|
142
|
+
ExtendPopupTimeoutResult,
|
|
143
|
+
ResolveAndClickArgs,
|
|
144
|
+
ResolveAndClickResult,
|
|
133
145
|
} from "./framework";
|
|
134
146
|
|
|
135
147
|
export type {
|
package/src/rpc/peer.ts
CHANGED
|
@@ -56,7 +56,7 @@ export const FIRST_USER_TYPE_ID = 128;
|
|
|
56
56
|
|
|
57
57
|
export const MAX_CAPS_PER_CONNECTION = 1024;
|
|
58
58
|
export const MAX_IN_FLIGHT_CALLS_PER_CONNECTION = 1024;
|
|
59
|
-
/** Client-side LRU cap for revoked cap-ids — prevents unbounded growth on long-lived connections with frequent plugin churn
|
|
59
|
+
/** Client-side LRU cap for revoked cap-ids — prevents unbounded growth on long-lived connections with frequent plugin churn. */
|
|
60
60
|
const REVOKED_CACHE_SIZE = MAX_CAPS_PER_CONNECTION * 4;
|
|
61
61
|
|
|
62
62
|
const DEFAULT_DEADLINE_GRACE_MS = 500;
|
package/src/webview/native.ts
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
import type { ClientOf } from "../rpc/index";
|
|
4
4
|
import type {
|
|
5
5
|
SurfaceCap, EvaluateResult, SurfaceCapabilities, ScreenshotResult,
|
|
6
|
-
SurfaceEvent, ConsoleEntry, WaitResult,
|
|
6
|
+
SurfaceEvent, ConsoleEntry, WaitResult, NavigationState,
|
|
7
|
+
AccessibilitySnapshotResult, BoundingRectResult, ListFramesResult,
|
|
8
|
+
DownloadEvent, DownloadPolicy, WaitForDownloadResult,
|
|
9
|
+
ResolveAndClickResult,
|
|
7
10
|
} from "../rpc/framework";
|
|
8
11
|
|
|
12
|
+
declare const __buniteWebviewId: number;
|
|
13
|
+
|
|
9
14
|
declare const host: {
|
|
10
15
|
runtime(): Promise<ClientOf<typeof import("../rpc/framework").RuntimeCap>>;
|
|
11
16
|
};
|
|
@@ -138,17 +143,24 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
138
143
|
this._unsubNavigate = () => ctrl.abort();
|
|
139
144
|
void (async () => {
|
|
140
145
|
try {
|
|
141
|
-
// Wait until *this* connection's init resolves, then capture surfaceId
|
|
142
|
-
// atomically before subscribing. Re-entrant connects abort prior loops
|
|
143
|
-
// via `ctrl`, so a stale resolve from a previous cycle can't leak in.
|
|
144
146
|
const s = await getSurfaceCap();
|
|
145
147
|
if (ctrl.signal.aborted) return;
|
|
146
148
|
await this._waitForSurfaceId(ctrl.signal);
|
|
147
149
|
const sid = this._surfaceId;
|
|
148
150
|
if (ctrl.signal.aborted || sid == null) return;
|
|
149
|
-
const
|
|
150
|
-
this._activeStreams.push(
|
|
151
|
-
|
|
151
|
+
const surfStream = s.surfaceEvents({ surfaceId: sid });
|
|
152
|
+
this._activeStreams.push(surfStream as { cancel?: () => void });
|
|
153
|
+
const dlStream = s.downloadEvents({ surfaceId: sid });
|
|
154
|
+
this._activeStreams.push(dlStream as { cancel?: () => void });
|
|
155
|
+
(async () => {
|
|
156
|
+
try {
|
|
157
|
+
for await (const event of dlStream) {
|
|
158
|
+
if (ctrl.signal.aborted) break;
|
|
159
|
+
this.dispatchEvent(new CustomEvent<DownloadEvent>("download-event", { detail: event }));
|
|
160
|
+
}
|
|
161
|
+
} catch { /* stream torn down */ }
|
|
162
|
+
})();
|
|
163
|
+
for await (const event of surfStream) {
|
|
152
164
|
if (ctrl.signal.aborted) break;
|
|
153
165
|
this.dispatchEvent(new CustomEvent<SurfaceEvent>("surface-event", { detail: event }));
|
|
154
166
|
}
|
|
@@ -264,10 +276,10 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
264
276
|
this.setAttribute("src", url);
|
|
265
277
|
}
|
|
266
278
|
|
|
267
|
-
async evaluate(script: string): Promise<EvaluateResult> {
|
|
279
|
+
async evaluate(script: string, opts?: { frameId?: string }): Promise<EvaluateResult> {
|
|
268
280
|
const sid = this._surfaceId;
|
|
269
281
|
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
270
|
-
return callSurfaceTyped((s) => s.evaluate({ surfaceId: sid, script }));
|
|
282
|
+
return callSurfaceTyped((s) => s.evaluate({ surfaceId: sid, script, frameId: opts?.frameId }));
|
|
271
283
|
}
|
|
272
284
|
|
|
273
285
|
async capabilities(): Promise<SurfaceCapabilities> {
|
|
@@ -277,7 +289,8 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
277
289
|
evaluate: false, crossOriginEval: false, surfaceEvents: false,
|
|
278
290
|
nativeInputTrusted: false, click: false, type: false, press: false,
|
|
279
291
|
scroll: false, mouse: false, dialogs: false, console: false,
|
|
280
|
-
screenshot: false,
|
|
292
|
+
screenshot: false, accessibilitySnapshot: false, getBoundingRect: false,
|
|
293
|
+
frames: false, downloads: false, popups: false, resolveAndClick: false,
|
|
281
294
|
};
|
|
282
295
|
}
|
|
283
296
|
return callSurfaceTyped((s) => s.capabilities({ surfaceId: sid }));
|
|
@@ -361,6 +374,64 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
361
374
|
return (await callSurfaceTyped((s) => s.getConsoleBuffer({ surfaceId: sid, clear: opts?.clear }))) ?? [];
|
|
362
375
|
}
|
|
363
376
|
|
|
377
|
+
async getNavigationState(): Promise<NavigationState> {
|
|
378
|
+
const sid = this._surfaceId;
|
|
379
|
+
if (sid == null) return { lastLoadEpoch: 0, isLoading: false, currentUrl: "" };
|
|
380
|
+
return callSurfaceTyped((s) => s.getNavigationState({ surfaceId: sid }));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async accessibilitySnapshot(opts?: { interestingOnly?: boolean }): Promise<AccessibilitySnapshotResult> {
|
|
384
|
+
const sid = this._surfaceId;
|
|
385
|
+
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
386
|
+
return callSurfaceTyped((s) => s.accessibilitySnapshot({ surfaceId: sid, interestingOnly: opts?.interestingOnly }));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async getBoundingRect(selector: string, opts?: { frameId?: string }): Promise<BoundingRectResult> {
|
|
390
|
+
const sid = this._surfaceId;
|
|
391
|
+
if (sid == null) return { ok: false, code: "runtime_error", message: "surface not ready" };
|
|
392
|
+
return callSurfaceTyped((s) => s.getBoundingRect({ surfaceId: sid, selector, frameId: opts?.frameId }));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async listFrames(): Promise<ListFramesResult> {
|
|
396
|
+
const sid = this._surfaceId;
|
|
397
|
+
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
398
|
+
return callSurfaceTyped((s) => s.listFrames({ surfaceId: sid }));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async resolveAndClick(
|
|
402
|
+
selector: string,
|
|
403
|
+
opts?: {
|
|
404
|
+
frameId?: string;
|
|
405
|
+
button?: "left" | "middle" | "right";
|
|
406
|
+
clickCount?: number;
|
|
407
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">;
|
|
408
|
+
}
|
|
409
|
+
): Promise<ResolveAndClickResult> {
|
|
410
|
+
const sid = this._surfaceId;
|
|
411
|
+
if (sid == null) return { ok: false, code: "runtime_error", message: "surface not ready" };
|
|
412
|
+
return callSurfaceTyped((s) => s.resolveAndClick({ surfaceId: sid, selector, ...opts }));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async setDownloadPolicy(policy: DownloadPolicy, downloadDir?: string): Promise<void> {
|
|
416
|
+
const sid = this._surfaceId;
|
|
417
|
+
if (sid == null) return;
|
|
418
|
+
await callSurface((s) => s.setDownloadPolicy({ surfaceId: sid, policy, downloadDir }));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async waitForDownload(opts?: { timeoutMs?: number }): Promise<WaitForDownloadResult> {
|
|
422
|
+
const sid = this._surfaceId;
|
|
423
|
+
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
424
|
+
return callSurfaceTyped((s) => s.waitForDownload({ surfaceId: sid, timeoutMs: opts?.timeoutMs }));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async dismissPopup(newSurfaceId: number): Promise<void> {
|
|
428
|
+
await callSurface((s) => s.dismissPopup({ newSurfaceId }));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async extendAdoptionTimeout(newSurfaceId: number, gracePeriodMs: number) {
|
|
432
|
+
return callSurfaceTyped((s) => s.extendPopupTimeout({ newSurfaceId, gracePeriodMs }));
|
|
433
|
+
}
|
|
434
|
+
|
|
364
435
|
async screenshot(args?: { format?: "png" | "jpeg"; quality?: number }): Promise<ScreenshotResult> {
|
|
365
436
|
const sid = this._surfaceId;
|
|
366
437
|
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
@@ -374,11 +445,71 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
374
445
|
void callSurface((s) => s.setHidden({ surfaceId: sid, hidden }));
|
|
375
446
|
}
|
|
376
447
|
|
|
448
|
+
private _setupSyncCtrl() {
|
|
449
|
+
this._syncCtrl = new OverlaySyncController(this, (rect) => {
|
|
450
|
+
const sid = this._surfaceId;
|
|
451
|
+
if (sid == null) return;
|
|
452
|
+
const isZero = rect.width === 0 && rect.height === 0;
|
|
453
|
+
if (isZero) {
|
|
454
|
+
if (!this._syncHidden) {
|
|
455
|
+
this._syncHidden = true;
|
|
456
|
+
this._applySurfaceHidden();
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (this._syncHidden) {
|
|
461
|
+
this._syncHidden = false;
|
|
462
|
+
this._applySurfaceHidden();
|
|
463
|
+
}
|
|
464
|
+
void callSurface((s) => s.resize({
|
|
465
|
+
surfaceId: sid, x: rect.x, y: rect.y, w: rect.width, h: rect.height,
|
|
466
|
+
}));
|
|
467
|
+
});
|
|
468
|
+
this._syncCtrl.start();
|
|
469
|
+
}
|
|
470
|
+
|
|
377
471
|
private initSurface() {
|
|
378
472
|
if (this._surfaceId != null || this._initPromise != null) return;
|
|
379
473
|
|
|
380
474
|
const dpr = window.devicePixelRatio || 1;
|
|
381
475
|
const r = this.getBoundingClientRect();
|
|
476
|
+
const adoptAttr = this.getAttribute("adopt-popup-id");
|
|
477
|
+
const adoptId = adoptAttr ? Number(adoptAttr) : NaN;
|
|
478
|
+
if (Number.isFinite(adoptId)) {
|
|
479
|
+
// Popup adoption — bind a backend-minted surface (received via the parent
|
|
480
|
+
// page's `surface-event` `popup` arm) to this element.
|
|
481
|
+
const initPromise = getSurfaceCap().then(async (s) => {
|
|
482
|
+
const res = await s.acceptPopup({
|
|
483
|
+
newSurfaceId: adoptId,
|
|
484
|
+
hostViewId: __buniteWebviewId,
|
|
485
|
+
bounds: {
|
|
486
|
+
x: Math.round(r.x * dpr),
|
|
487
|
+
y: Math.round(r.y * dpr),
|
|
488
|
+
width: Math.round(r.width * dpr),
|
|
489
|
+
height: Math.round(r.height * dpr),
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
if (!res.ok) throw new Error(`acceptPopup failed: ${res.code}: ${res.message}`);
|
|
493
|
+
return { surfaceId: adoptId };
|
|
494
|
+
}) as Promise<SurfaceInitResponse>;
|
|
495
|
+
this._initPromise = initPromise;
|
|
496
|
+
initPromise.then((response) => {
|
|
497
|
+
if (this._initPromise !== initPromise) return;
|
|
498
|
+
if (this._aborted) {
|
|
499
|
+
void callSurface((s) => s.remove({ surfaceId: response.surfaceId }));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this._surfaceId = response.surfaceId;
|
|
503
|
+
this._setupSyncCtrl();
|
|
504
|
+
}).catch((err) => {
|
|
505
|
+
if ((globalThis as { __BUNITE_DEBUG__?: boolean }).__BUNITE_DEBUG__) {
|
|
506
|
+
console.warn("[bunite] adopt-popup-id init failed", err);
|
|
507
|
+
}
|
|
508
|
+
this._initPromise = null;
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
382
513
|
const src = this._pendingSrc || this.getAttribute("src") || "";
|
|
383
514
|
this._pendingSrc = null;
|
|
384
515
|
|
|
@@ -415,28 +546,7 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
415
546
|
}
|
|
416
547
|
}
|
|
417
548
|
|
|
418
|
-
this.
|
|
419
|
-
const sid = this._surfaceId;
|
|
420
|
-
if (sid == null) return;
|
|
421
|
-
|
|
422
|
-
const isZero = rect.width === 0 && rect.height === 0;
|
|
423
|
-
if (isZero) {
|
|
424
|
-
if (!this._syncHidden) {
|
|
425
|
-
this._syncHidden = true;
|
|
426
|
-
this._applySurfaceHidden();
|
|
427
|
-
}
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
if (this._syncHidden) {
|
|
431
|
-
this._syncHidden = false;
|
|
432
|
-
this._applySurfaceHidden();
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
void callSurface((s) => s.resize({
|
|
436
|
-
surfaceId: sid, x: rect.x, y: rect.y, w: rect.width, h: rect.height,
|
|
437
|
-
}));
|
|
438
|
-
});
|
|
439
|
-
this._syncCtrl.start();
|
|
549
|
+
this._setupSyncCtrl();
|
|
440
550
|
})
|
|
441
551
|
.catch(() => {})
|
|
442
552
|
.finally(() => {
|