bunite-core 0.12.0 → 0.14.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 +17 -1
- package/src/host/core/BrowserView.ts +197 -28
- package/src/host/core/SurfaceBrowserIPC.ts +44 -3
- package/src/host/core/SurfaceManager.ts +260 -28
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +8 -1
- package/src/host/native.ts +124 -1
- package/src/native/linux/bunite_linux_ffi.cpp +223 -6
- package/src/native/linux/bunite_linux_internal.h +6 -0
- package/src/native/linux/bunite_linux_runtime.cpp +1 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +85 -0
- package/src/native/mac/bunite_mac_ffi.mm +356 -8
- package/src/native/mac/bunite_mac_internal.h +6 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +144 -2
- package/src/native/shared/ffi_exports.h +135 -0
- package/src/native/win/native_host_cef.cpp +86 -3
- package/src/native/win/native_host_ffi.cpp +378 -1
- package/src/native/win/native_host_internal.h +13 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +303 -9
- package/src/native/win-webview2/webview2_internal.h +11 -0
- package/src/native/win-webview2/webview2_runtime.cpp +128 -12
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +173 -4
- package/src/rpc/index.ts +21 -0
- package/src/webview/native.ts +126 -25
- package/src/webview/polyfill.ts +196 -12
package/src/rpc/framework.ts
CHANGED
|
@@ -70,6 +70,16 @@ export const ShellCap = defineCap("bunite.Shell", {
|
|
|
70
70
|
showItemInFolder: call<{ path: string }, void>(),
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
/** page → host event sink. Distinct from RuntimeCap (page → host *request* API)
|
|
74
|
+
* so future arms like `reportError` / `reportPerformance` don't pollute the
|
|
75
|
+
* runtime cap. Mounted as a sub-cap via `RuntimeCap.reporting()`. */
|
|
76
|
+
export const PageReportingCap = defineCap("bunite.PageReporting", {
|
|
77
|
+
/** preload coalesces console calls in a 16ms window and pushes the batch.
|
|
78
|
+
* Fire-and-forget — preload `.catch`es resource_exhausted to avoid throwing
|
|
79
|
+
* inside `console.log` (which would corrupt page semantics). */
|
|
80
|
+
reportConsoleBatch: call<{ entries: ConsoleEntry[] }, void>(),
|
|
81
|
+
});
|
|
82
|
+
|
|
73
83
|
export type SurfaceMask = { x: number; y: number; w: number; h: number };
|
|
74
84
|
|
|
75
85
|
/** Automation feature flags reported per surface. Append-only — consumers
|
|
@@ -78,22 +88,166 @@ export type SurfaceMask = { x: number; y: number; w: number; h: number };
|
|
|
78
88
|
export interface SurfaceCapabilities {
|
|
79
89
|
evaluate: boolean;
|
|
80
90
|
crossOriginEval: boolean;
|
|
81
|
-
|
|
82
|
-
/**
|
|
91
|
+
surfaceEvents: boolean;
|
|
92
|
+
/** click/type/press/mouse produce DOM events with `isTrusted=true` on the page.
|
|
93
|
+
* Does NOT cover scroll/screenshot (those may use CDP path with isTrusted=false). */
|
|
83
94
|
nativeInputTrusted: boolean;
|
|
84
95
|
click: boolean;
|
|
85
96
|
type: boolean;
|
|
86
97
|
press: boolean;
|
|
87
98
|
scroll: boolean;
|
|
99
|
+
/** Raw mouse primitives (move/down/up) — required for drag & hover. */
|
|
100
|
+
mouse: boolean;
|
|
101
|
+
/** Page-initiated dialogs (alert/confirm/prompt/beforeunload) routed through
|
|
102
|
+
* `dialogs` stream + `respondToDialog`. `beforeunload` is Win-only (WebKit
|
|
103
|
+
* handles it through navigation-policy delegate, not script-dialog). */
|
|
104
|
+
dialogs: boolean;
|
|
105
|
+
/** Page `console.{log,warn,error,info,debug}` captured via preload proxy.
|
|
106
|
+
* Only effective for surfaces whose origin is in the preload allowlist
|
|
107
|
+
* (default `appres://app.internal/*`); cross-origin pages don't get preload
|
|
108
|
+
* injection, so `consoleEvents` stays empty even though the flag is true. */
|
|
109
|
+
console: boolean;
|
|
88
110
|
screenshot: boolean;
|
|
89
111
|
/** Present only when `screenshot` is true. */
|
|
90
112
|
formats?: ("png" | "jpeg")[];
|
|
91
113
|
}
|
|
92
114
|
|
|
115
|
+
/** Surface lifecycle event arm. Backends emit a subset honestly — SPA
|
|
116
|
+
* `history.pushState` fires only `navigate`; classic navigation fires
|
|
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 =
|
|
121
|
+
| { type: "navigate"; url: string }
|
|
122
|
+
| { type: "load-start"; url: string }
|
|
123
|
+
| { type: "load-finish"; url: string }
|
|
124
|
+
| { type: "load-fail"; url: string; reason?: string }
|
|
125
|
+
| { type: "title-change"; title: string };
|
|
126
|
+
|
|
93
127
|
export type EvaluateResult =
|
|
94
128
|
| { ok: true; value: unknown }
|
|
95
129
|
| { ok: false; code: "cross_origin" | "runtime_error" | "not_supported" | "timeout"; message: string };
|
|
96
130
|
|
|
131
|
+
/** Modifier bitmask for input dispatch. Backends translate to native form. */
|
|
132
|
+
export type Modifier = "alt" | "ctrl" | "meta" | "shift";
|
|
133
|
+
|
|
134
|
+
export interface ClickArgs {
|
|
135
|
+
surfaceId: number;
|
|
136
|
+
x: number;
|
|
137
|
+
y: number;
|
|
138
|
+
button?: "left" | "middle" | "right";
|
|
139
|
+
clickCount?: number;
|
|
140
|
+
modifiers?: Modifier[];
|
|
141
|
+
}
|
|
142
|
+
export interface TypeArgs { surfaceId: number; text: string }
|
|
143
|
+
export interface PressArgs {
|
|
144
|
+
surfaceId: number;
|
|
145
|
+
key: string;
|
|
146
|
+
modifiers?: Modifier[];
|
|
147
|
+
/** "both" (default) emits down→char→up; "down" / "up" emit only that half.
|
|
148
|
+
* For Playwright-style modifier-held wrap: keydown → click → keyup. */
|
|
149
|
+
action?: "down" | "up" | "both";
|
|
150
|
+
}
|
|
151
|
+
export interface MouseArgs {
|
|
152
|
+
surfaceId: number;
|
|
153
|
+
/** "move" produces mouseMove only; "down"/"up" produces a single half of
|
|
154
|
+
* a mouse-button event. Compose drag = down → move(s) → up. modifiers are
|
|
155
|
+
* per-call atomic (no sticky state across calls). */
|
|
156
|
+
action: "move" | "down" | "up";
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
/** Required for "down"/"up"; ignored for "move". */
|
|
160
|
+
button?: "left" | "middle" | "right";
|
|
161
|
+
modifiers?: Modifier[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Page-initiated modal dialog. Backend holds page execution until consumer
|
|
165
|
+
* calls `respondToDialog` with the matching `requestId`. If no response
|
|
166
|
+
* arrives within `setDialogTimeout` (default 5000ms; null = wait forever),
|
|
167
|
+
* host auto-dismisses and emits `{kind: "auto-dismissed", originalKind, message}`
|
|
168
|
+
* so the consumer learns the page proceeded without explicit answer.
|
|
169
|
+
*
|
|
170
|
+
* `beforeunload` arm support per backend:
|
|
171
|
+
* - CEF (Win) : ✔ — `OnBeforeUnloadDialog`
|
|
172
|
+
* - WV2 (Win) : ✔ — `ScriptDialogOpening` with kind `BEFOREUNLOAD`
|
|
173
|
+
* - WKWebView (mac) : ✘ — handled by WKNavigationDelegate, surfaced as `will-navigate`
|
|
174
|
+
* - WebKitGTK (linux) : ✔ — `script-dialog` signal with `BEFORE_UNLOAD_CONFIRM` */
|
|
175
|
+
export type DialogEvent =
|
|
176
|
+
| {
|
|
177
|
+
kind: "alert" | "confirm" | "prompt" | "beforeunload";
|
|
178
|
+
requestId: number;
|
|
179
|
+
message: string;
|
|
180
|
+
/** Initial text for `prompt` only. */
|
|
181
|
+
defaultPrompt?: string;
|
|
182
|
+
}
|
|
183
|
+
| {
|
|
184
|
+
kind: "auto-dismissed";
|
|
185
|
+
originalKind: "alert" | "confirm" | "prompt" | "beforeunload";
|
|
186
|
+
message: string;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export interface RespondToDialogArgs {
|
|
190
|
+
surfaceId: number;
|
|
191
|
+
requestId: number;
|
|
192
|
+
accept: boolean;
|
|
193
|
+
/** For `prompt` dialogs — the text the page receives. Ignored otherwise. */
|
|
194
|
+
text?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface SetDialogTimeoutArgs {
|
|
198
|
+
surfaceId: number;
|
|
199
|
+
/** Milliseconds before unanswered dialog is auto-dismissed. `null` disables
|
|
200
|
+
* the safety net — page hangs until consumer responds. Default 5000. */
|
|
201
|
+
ms: number | null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface WaitForSelectorArgs {
|
|
205
|
+
surfaceId: number;
|
|
206
|
+
selector: string;
|
|
207
|
+
/** Default 5000ms. Polled at 50ms intervals via `evaluate`. */
|
|
208
|
+
timeoutMs?: number;
|
|
209
|
+
}
|
|
210
|
+
export interface WaitForFunctionArgs {
|
|
211
|
+
surfaceId: number;
|
|
212
|
+
/** JS expression returning truthy when satisfied. */
|
|
213
|
+
expression: string;
|
|
214
|
+
/** Default 5000ms. */
|
|
215
|
+
timeoutMs?: number;
|
|
216
|
+
/** Default 50ms. Increase for heavy expressions to reduce IPC load. */
|
|
217
|
+
pollIntervalMs?: number;
|
|
218
|
+
}
|
|
219
|
+
export type WaitResult =
|
|
220
|
+
| { ok: true }
|
|
221
|
+
| { ok: false; code: "timeout" | "runtime_error" | "cross_origin"; message: string };
|
|
222
|
+
|
|
223
|
+
export type ConsoleLevel = "log" | "warn" | "error" | "info" | "debug";
|
|
224
|
+
|
|
225
|
+
export interface ConsoleEntry {
|
|
226
|
+
level: ConsoleLevel;
|
|
227
|
+
/** `.toString()` / JSON.stringify-ed arguments — preload doesn't structured-clone. */
|
|
228
|
+
args: string[];
|
|
229
|
+
/** Page-side `Date.now()` at capture time. */
|
|
230
|
+
ts: number;
|
|
231
|
+
}
|
|
232
|
+
export interface ScrollArgs {
|
|
233
|
+
surfaceId: number;
|
|
234
|
+
dx: number;
|
|
235
|
+
dy: number;
|
|
236
|
+
x?: number;
|
|
237
|
+
y?: number;
|
|
238
|
+
modifiers?: Modifier[];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface ScreenshotArgs {
|
|
242
|
+
surfaceId: number;
|
|
243
|
+
format?: "png" | "jpeg";
|
|
244
|
+
/** JPEG only; 0–100. Ignored for PNG. */
|
|
245
|
+
quality?: number;
|
|
246
|
+
}
|
|
247
|
+
export type ScreenshotResult =
|
|
248
|
+
| { ok: true; data: Uint8Array; mime: string; format: "png" | "jpeg" }
|
|
249
|
+
| { ok: false; code: "not_supported" | "runtime_error" | "timeout" | "black_frame"; message: string };
|
|
250
|
+
|
|
97
251
|
export const SurfaceCap = defineCap("bunite.Surface", {
|
|
98
252
|
init: call<{
|
|
99
253
|
src: string;
|
|
@@ -114,8 +268,20 @@ export const SurfaceCap = defineCap("bunite.Surface", {
|
|
|
114
268
|
reload: call<{ surfaceId: number }, void>(),
|
|
115
269
|
evaluate: call<{ surfaceId: number; script: string }, EvaluateResult>(),
|
|
116
270
|
capabilities: call<{ surfaceId: number }, SurfaceCapabilities>(),
|
|
117
|
-
|
|
118
|
-
|
|
271
|
+
click: call<ClickArgs, void>(),
|
|
272
|
+
type: call<TypeArgs, void>(),
|
|
273
|
+
press: call<PressArgs, void>(),
|
|
274
|
+
scroll: call<ScrollArgs, void>(),
|
|
275
|
+
mouse: call<MouseArgs, void>(),
|
|
276
|
+
screenshot: call<ScreenshotArgs, ScreenshotResult>(),
|
|
277
|
+
waitForSelector: call<WaitForSelectorArgs, WaitResult>(),
|
|
278
|
+
waitForFunction: call<WaitForFunctionArgs, WaitResult>(),
|
|
279
|
+
respondToDialog: call<RespondToDialogArgs, void>(),
|
|
280
|
+
setDialogTimeout: call<SetDialogTimeoutArgs, void>(),
|
|
281
|
+
getConsoleBuffer: call<{ surfaceId: number; clear?: boolean }, ConsoleEntry[]>({ idempotent: true }),
|
|
282
|
+
surfaceEvents: stream<{ surfaceId: number }, SurfaceEvent>(),
|
|
283
|
+
dialogs: stream<{ surfaceId: number }, DialogEvent>(),
|
|
284
|
+
consoleEvents: stream<{ surfaceId: number }, ConsoleEntry>(),
|
|
119
285
|
});
|
|
120
286
|
|
|
121
287
|
export const RuntimeCap = defineCap("bunite.Runtime", {
|
|
@@ -128,6 +294,7 @@ export const RuntimeCap = defineCap("bunite.Runtime", {
|
|
|
128
294
|
theme: call<void, "light" | "dark">({ idempotent: true }),
|
|
129
295
|
themeWatch: stream<void, "light" | "dark">(),
|
|
130
296
|
surface: call<void, typeof SurfaceCap>({ returns: cap(SurfaceCap), idempotent: true }),
|
|
297
|
+
reporting: call<void, typeof PageReportingCap>({ returns: cap(PageReportingCap), idempotent: true }),
|
|
131
298
|
});
|
|
132
299
|
|
|
133
300
|
export const FRAMEWORK_TYPE_IDS = {
|
|
@@ -139,6 +306,7 @@ export const FRAMEWORK_TYPE_IDS = {
|
|
|
139
306
|
Shell: 6,
|
|
140
307
|
BrowserWindow: 7,
|
|
141
308
|
Surface: 8,
|
|
309
|
+
PageReporting: 9,
|
|
142
310
|
} as const;
|
|
143
311
|
|
|
144
312
|
const FRAMEWORK_CAP_TYPE_IDS = new Map<CapDef<any, any>, number>([
|
|
@@ -150,6 +318,7 @@ const FRAMEWORK_CAP_TYPE_IDS = new Map<CapDef<any, any>, number>([
|
|
|
150
318
|
[ShellCap, FRAMEWORK_TYPE_IDS.Shell],
|
|
151
319
|
[BrowserWindowCap, FRAMEWORK_TYPE_IDS.BrowserWindow],
|
|
152
320
|
[SurfaceCap, FRAMEWORK_TYPE_IDS.Surface],
|
|
321
|
+
[PageReportingCap, FRAMEWORK_TYPE_IDS.PageReporting],
|
|
153
322
|
]);
|
|
154
323
|
|
|
155
324
|
export function frameworkTypeIdOf(cap: CapDef<any, any>): number | undefined {
|
package/src/rpc/index.ts
CHANGED
|
@@ -101,6 +101,7 @@ export {
|
|
|
101
101
|
ClipboardCap,
|
|
102
102
|
ShellCap,
|
|
103
103
|
SurfaceCap,
|
|
104
|
+
PageReportingCap,
|
|
104
105
|
FRAMEWORK_TYPE_IDS,
|
|
105
106
|
} from "./framework";
|
|
106
107
|
|
|
@@ -109,6 +110,26 @@ export type {
|
|
|
109
110
|
DialogOpenFileOpts,
|
|
110
111
|
DialogSaveFileOpts,
|
|
111
112
|
DialogMessageOpts,
|
|
113
|
+
SurfaceCapabilities,
|
|
114
|
+
SurfaceEvent,
|
|
115
|
+
SurfaceMask,
|
|
116
|
+
Modifier,
|
|
117
|
+
ClickArgs,
|
|
118
|
+
TypeArgs,
|
|
119
|
+
PressArgs,
|
|
120
|
+
ScrollArgs,
|
|
121
|
+
MouseArgs,
|
|
122
|
+
DialogEvent,
|
|
123
|
+
RespondToDialogArgs,
|
|
124
|
+
SetDialogTimeoutArgs,
|
|
125
|
+
WaitForSelectorArgs,
|
|
126
|
+
WaitForFunctionArgs,
|
|
127
|
+
WaitResult,
|
|
128
|
+
ConsoleLevel,
|
|
129
|
+
ConsoleEntry,
|
|
130
|
+
ScreenshotArgs,
|
|
131
|
+
ScreenshotResult,
|
|
132
|
+
EvaluateResult,
|
|
112
133
|
} from "./framework";
|
|
113
134
|
|
|
114
135
|
export type {
|
package/src/webview/native.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// <bunite-webview> custom element — registered in every appres:// page via preload.
|
|
2
2
|
|
|
3
3
|
import type { ClientOf } from "../rpc/index";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
SurfaceCap, EvaluateResult, SurfaceCapabilities, ScreenshotResult,
|
|
6
|
+
SurfaceEvent, ConsoleEntry, WaitResult,
|
|
7
|
+
} from "../rpc/framework";
|
|
5
8
|
|
|
6
9
|
declare const host: {
|
|
7
10
|
runtime(): Promise<ClientOf<typeof import("../rpc/framework").RuntimeCap>>;
|
|
@@ -120,6 +123,7 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
120
123
|
private _userHidden = false;
|
|
121
124
|
private _layoutObserver: ResizeObserver | null = null;
|
|
122
125
|
private _unsubNavigate: (() => void) | null = null;
|
|
126
|
+
private _activeStreams: Array<{ cancel?: () => void }> = [];
|
|
123
127
|
|
|
124
128
|
constructor() {
|
|
125
129
|
super();
|
|
@@ -134,39 +138,45 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
134
138
|
this._unsubNavigate = () => ctrl.abort();
|
|
135
139
|
void (async () => {
|
|
136
140
|
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.
|
|
137
144
|
const s = await getSurfaceCap();
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
if (ctrl.signal.aborted) return;
|
|
146
|
+
await this._waitForSurfaceId(ctrl.signal);
|
|
147
|
+
const sid = this._surfaceId;
|
|
148
|
+
if (ctrl.signal.aborted || sid == null) return;
|
|
149
|
+
const stream = s.surfaceEvents({ surfaceId: sid });
|
|
150
|
+
this._activeStreams.push(stream as { cancel?: () => void });
|
|
151
|
+
for await (const event of stream) {
|
|
140
152
|
if (ctrl.signal.aborted) break;
|
|
141
|
-
|
|
142
|
-
this.dispatchEvent(new CustomEvent("did-navigate", { detail: { url: ev.url } }));
|
|
143
|
-
}
|
|
153
|
+
this.dispatchEvent(new CustomEvent<SurfaceEvent>("surface-event", { detail: event }));
|
|
144
154
|
}
|
|
145
155
|
} catch (err) {
|
|
146
156
|
if ((globalThis as { __BUNITE_DEBUG__?: boolean }).__BUNITE_DEBUG__) {
|
|
147
|
-
console.warn("[bunite]
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
})();
|
|
151
|
-
void (async () => {
|
|
152
|
-
try {
|
|
153
|
-
const s = await getSurfaceCap();
|
|
154
|
-
const stream = s.titleChanged();
|
|
155
|
-
for await (const ev of stream) {
|
|
156
|
-
if (ctrl.signal.aborted) break;
|
|
157
|
-
if (ev.surfaceId === this._surfaceId) {
|
|
158
|
-
this.dispatchEvent(new CustomEvent("title-changed", { detail: { title: ev.title } }));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} catch (err) {
|
|
162
|
-
if ((globalThis as { __BUNITE_DEBUG__?: boolean }).__BUNITE_DEBUG__) {
|
|
163
|
-
console.warn("[bunite] titleChanged stream failed", err);
|
|
157
|
+
console.warn("[bunite] surfaceEvents stream failed", err);
|
|
164
158
|
}
|
|
165
159
|
}
|
|
166
160
|
})();
|
|
167
161
|
this._waitForLayout();
|
|
168
162
|
}
|
|
169
163
|
|
|
164
|
+
private async _waitForSurfaceId(signal: AbortSignal): Promise<void> {
|
|
165
|
+
while (this._surfaceId == null && !signal.aborted) {
|
|
166
|
+
const pending = this._initPromise;
|
|
167
|
+
if (pending) {
|
|
168
|
+
// Await this exact init attempt; if it rejects, bail (init failed —
|
|
169
|
+
// we'd otherwise spin forever waiting for a surfaceId that never lands).
|
|
170
|
+
try { await pending; } catch { return; }
|
|
171
|
+
// After resolve, _surfaceId may still be null if disconnect raced —
|
|
172
|
+
// the next loop iteration checks signal.aborted to exit cleanly.
|
|
173
|
+
} else {
|
|
174
|
+
// No init in flight yet (waiting for layout). Yield then re-check.
|
|
175
|
+
await new Promise((r) => setTimeout(r, 16));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
170
180
|
private _waitForLayout() {
|
|
171
181
|
if (this._layoutObserver) return; // already waiting
|
|
172
182
|
|
|
@@ -198,6 +208,12 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
198
208
|
this._aborted = true;
|
|
199
209
|
this._unsubNavigate?.();
|
|
200
210
|
this._unsubNavigate = null;
|
|
211
|
+
// Cancel pending stream iterators so the `for await` actually unblocks —
|
|
212
|
+
// AbortController alone only takes effect at the next received chunk.
|
|
213
|
+
for (const stream of this._activeStreams) {
|
|
214
|
+
try { stream.cancel?.(); } catch {}
|
|
215
|
+
}
|
|
216
|
+
this._activeStreams = [];
|
|
201
217
|
this._layoutObserver?.disconnect();
|
|
202
218
|
this._layoutObserver = null;
|
|
203
219
|
this._syncCtrl?.stop();
|
|
@@ -258,14 +274,99 @@ class BuniteWebviewElement extends HTMLElement {
|
|
|
258
274
|
const sid = this._surfaceId;
|
|
259
275
|
if (sid == null) {
|
|
260
276
|
return {
|
|
261
|
-
evaluate: false, crossOriginEval: false,
|
|
277
|
+
evaluate: false, crossOriginEval: false, surfaceEvents: false,
|
|
262
278
|
nativeInputTrusted: false, click: false, type: false, press: false,
|
|
263
|
-
scroll: false,
|
|
279
|
+
scroll: false, mouse: false, dialogs: false, console: false,
|
|
280
|
+
screenshot: false,
|
|
264
281
|
};
|
|
265
282
|
}
|
|
266
283
|
return callSurfaceTyped((s) => s.capabilities({ surfaceId: sid }));
|
|
267
284
|
}
|
|
268
285
|
|
|
286
|
+
// Automation input — `send*` prefix avoids clashing with HTMLElement.click() / .scroll().
|
|
287
|
+
async sendClick(args: {
|
|
288
|
+
x: number; y: number;
|
|
289
|
+
button?: "left" | "middle" | "right";
|
|
290
|
+
clickCount?: number;
|
|
291
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">;
|
|
292
|
+
}): Promise<void> {
|
|
293
|
+
const sid = this._surfaceId;
|
|
294
|
+
if (sid == null) return;
|
|
295
|
+
await callSurfaceTyped((s) => s.click({ surfaceId: sid, ...args }));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async sendType(text: string): Promise<void> {
|
|
299
|
+
const sid = this._surfaceId;
|
|
300
|
+
if (sid == null) return;
|
|
301
|
+
await callSurfaceTyped((s) => s.type({ surfaceId: sid, text }));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async sendPress(
|
|
305
|
+
key: string,
|
|
306
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">,
|
|
307
|
+
action?: "down" | "up" | "both"
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
const sid = this._surfaceId;
|
|
310
|
+
if (sid == null) return;
|
|
311
|
+
await callSurfaceTyped((s) => s.press({ surfaceId: sid, key, modifiers, action }));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async sendScroll(args: {
|
|
315
|
+
dx: number; dy: number; x?: number; y?: number;
|
|
316
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">;
|
|
317
|
+
}): Promise<void> {
|
|
318
|
+
const sid = this._surfaceId;
|
|
319
|
+
if (sid == null) return;
|
|
320
|
+
await callSurfaceTyped((s) => s.scroll({ surfaceId: sid, ...args }));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async sendMouse(args: {
|
|
324
|
+
action: "move" | "down" | "up";
|
|
325
|
+
x: number; y: number;
|
|
326
|
+
button?: "left" | "middle" | "right";
|
|
327
|
+
modifiers?: Array<"alt" | "ctrl" | "meta" | "shift">;
|
|
328
|
+
}): Promise<void> {
|
|
329
|
+
const sid = this._surfaceId;
|
|
330
|
+
if (sid == null) return;
|
|
331
|
+
await callSurfaceTyped((s) => s.mouse({ surfaceId: sid, ...args }));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async respondToDialog(requestId: number, accept: boolean, text?: string): Promise<void> {
|
|
335
|
+
const sid = this._surfaceId;
|
|
336
|
+
if (sid == null) return;
|
|
337
|
+
await callSurfaceTyped((s) => s.respondToDialog({ surfaceId: sid, requestId, accept, text }));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async setDialogTimeout(ms: number | null): Promise<void> {
|
|
341
|
+
const sid = this._surfaceId;
|
|
342
|
+
if (sid == null) return;
|
|
343
|
+
await callSurfaceTyped((s) => s.setDialogTimeout({ surfaceId: sid, ms }));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async waitForSelector(selector: string, timeoutMs?: number): Promise<WaitResult> {
|
|
347
|
+
const sid = this._surfaceId;
|
|
348
|
+
if (sid == null) return { ok: false, code: "runtime_error", message: "surface not ready" };
|
|
349
|
+
return callSurfaceTyped((s) => s.waitForSelector({ surfaceId: sid, selector, timeoutMs }));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async waitForFunction(expression: string, opts?: { timeoutMs?: number; pollIntervalMs?: number }): Promise<WaitResult> {
|
|
353
|
+
const sid = this._surfaceId;
|
|
354
|
+
if (sid == null) return { ok: false, code: "runtime_error", message: "surface not ready" };
|
|
355
|
+
return callSurfaceTyped((s) => s.waitForFunction({ surfaceId: sid, expression, ...opts }));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async getConsoleBuffer(opts?: { clear?: boolean }): Promise<ConsoleEntry[]> {
|
|
359
|
+
const sid = this._surfaceId;
|
|
360
|
+
if (sid == null) return [];
|
|
361
|
+
return (await callSurfaceTyped((s) => s.getConsoleBuffer({ surfaceId: sid, clear: opts?.clear }))) ?? [];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async screenshot(args?: { format?: "png" | "jpeg"; quality?: number }): Promise<ScreenshotResult> {
|
|
365
|
+
const sid = this._surfaceId;
|
|
366
|
+
if (sid == null) return { ok: false, code: "not_supported", message: "surface not ready" };
|
|
367
|
+
return callSurfaceTyped((s) => s.screenshot({ surfaceId: sid, ...args }));
|
|
368
|
+
}
|
|
369
|
+
|
|
269
370
|
private _applySurfaceHidden() {
|
|
270
371
|
const sid = this._surfaceId;
|
|
271
372
|
if (sid == null) return;
|