electrobun 1.18.1 → 1.18.4-beta.5

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/README.md CHANGED
@@ -63,9 +63,11 @@ Don't miss our:
63
63
  - [GOG Achievements GUI](https://github.com/timendum/gog-achievements-gui) - desktop app for managing GOG achievements
64
64
  - [groov](https://github.com/laurenzcodes/groov) - desktop audio deck monitor
65
65
  - [Guerilla Glass](https://github.com/okikeSolutions/guerillaglass) - open-source cross-platform creator studio for fast Record -> Edit -> Deliver workflows
66
+ - [Invoke](https://getinvoke.com) - macOS UI automation & shortcut platform
66
67
  - [Marginalia](https://github.com/lars-hoeijmans/Marginalia) - a simple note taking app
67
68
  - [MarkBun](https://github.com/xiaochong/markbun) - fast, beautiful, Typora-like markdown desktop editor
68
69
  - [md-browse](https://github.com/needle-tools/md-browse) - a markdown-first browser that converts web pages to clean markdown
70
+ - [moop](https://github.com/zrubinrattet/moop/) - desktop app for batch image optimization for the web
69
71
  - [Patchline](https://github.com/adwaithks/Patchline) - lightweight desktop Git client for reading patches and line diffs, then staging and committing changes
70
72
  - [peekachu](https://github.com/needle-tools/peekachu) - password manager for AIs; store secrets in your OS keychain and scrub output so AI assistants never see actual values
71
73
  - [PiBun](https://github.com/khairold/pibun) - desktop GUI for the Pi coding agent with chat, terminal, git integration, and plugin system
@@ -7,6 +7,8 @@ interface ElectrobunEncryptResult {
7
7
  }
8
8
 
9
9
  interface ElectrobunBridge {
10
+ receiveMessageFromHost: (msg: unknown) => void;
11
+ receiveInternalMessageFromHost: (msg: unknown) => void;
10
12
  receiveMessageFromBun: (msg: unknown) => void;
11
13
  receiveInternalMessageFromBun: (msg: unknown) => void;
12
14
  }
@@ -20,10 +22,13 @@ declare global {
20
22
  __electrobunWebviewId: number;
21
23
  __electrobunWindowId: number;
22
24
  __electrobunRpcSocketPort: number;
25
+ __electrobunHostSocketPort?: number;
23
26
  __electrobun?: ElectrobunBridge;
27
+ __electrobunPendingHostMessages?: unknown[];
24
28
  __electrobun_encrypt: (msg: string) => Promise<ElectrobunEncryptResult>;
25
29
  __electrobun_decrypt: (encryptedData: string, iv: string, tag: string) => Promise<string>;
26
30
  __electrobunInternalBridge?: MessageHandler;
31
+ __electrobunHostBridge?: MessageHandler;
27
32
  __electrobunBunBridge?: MessageHandler;
28
33
  }
29
34
  }
@@ -15,13 +15,23 @@ import { type WgpuTagElement, type WgpuEventTypes } from "./wgputag";
15
15
  import "./global.d.ts";
16
16
 
17
17
  const WEBVIEW_ID = window.__electrobunWebviewId;
18
- const RPC_SOCKET_PORT = window.__electrobunRpcSocketPort;
18
+ const HOST_SOCKET_PORT =
19
+ window.__electrobunHostSocketPort ?? window.__electrobunRpcSocketPort;
19
20
 
20
21
  class Electroview<T extends RPCWithTransport> {
21
- bunSocket?: WebSocket;
22
+ hostSocket?: WebSocket;
23
+ hostSocketCanSend = false;
22
24
  // user's custom rpc browser <-> bun
23
25
  rpc?: T;
24
26
  rpcHandler?: (msg: unknown) => void;
27
+ carrots = {
28
+ invoke: <R = unknown>(
29
+ carrotId: string,
30
+ method: string,
31
+ params?: unknown,
32
+ options?: { windowId?: string },
33
+ ) => this.invokeCarrot<R>(carrotId, method, params, options),
34
+ };
25
35
 
26
36
  constructor(config: { rpc: T }) {
27
37
  this.rpc = config.rpc;
@@ -29,34 +39,42 @@ class Electroview<T extends RPCWithTransport> {
29
39
  }
30
40
 
31
41
  init() {
32
- this.initSocketToBun();
42
+ this.initSocketToHost();
33
43
 
34
- // Set up handler for user RPC messages from bun
35
- // Note: receiveInternalMessageFromBun is set up by the preload script
36
- window.__electrobun!.receiveMessageFromBun =
37
- this.receiveMessageFromBun.bind(this);
44
+ // Set up handler for user RPC messages from the host runtime.
45
+ const hostMessageHandler = this.receiveMessageFromHost.bind(this);
46
+ window.__electrobun!.receiveMessageFromHost = hostMessageHandler;
47
+ window.__electrobun!.receiveMessageFromBun = hostMessageHandler;
38
48
 
39
49
  if (this.rpc) {
40
50
  this.rpc.setTransport(this.createTransport());
41
51
  }
52
+
53
+ const pendingMessages = window.__electrobunPendingHostMessages;
54
+ if (pendingMessages?.length) {
55
+ window.__electrobunPendingHostMessages = [];
56
+ for (const message of pendingMessages) {
57
+ hostMessageHandler(message);
58
+ }
59
+ }
42
60
  }
43
61
 
44
- initSocketToBun() {
62
+ initSocketToHost() {
45
63
  // Skip native socket when running in a remote browser (no port/webview ID)
46
- if (!RPC_SOCKET_PORT || !WEBVIEW_ID) {
64
+ if (!HOST_SOCKET_PORT || !WEBVIEW_ID) {
47
65
  return;
48
66
  }
49
67
 
50
- // Note: Using ws:// for localhost is intentional - all RPC messages are
68
+ // Note: Using ws:// for loopback is intentional - all RPC messages are
51
69
  // encrypted with per-webview AES-GCM keys, making TLS redundant
52
70
  const socket = new WebSocket(
53
- `ws://localhost:${RPC_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
71
+ `ws://127.0.0.1:${HOST_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
54
72
  );
55
73
 
56
- this.bunSocket = socket;
74
+ this.hostSocket = socket;
57
75
 
58
76
  socket.addEventListener("open", () => {
59
- // this.bunSocket?.send("Hello from webview " + WEBVIEW_ID);
77
+ this.hostSocketCanSend = true;
60
78
  });
61
79
 
62
80
  socket.addEventListener("message", async (event) => {
@@ -71,6 +89,7 @@ class Electroview<T extends RPCWithTransport> {
71
89
  encryptedPacket.tag,
72
90
  );
73
91
 
92
+ this.hostSocketCanSend = true;
74
93
  this.rpcHandler?.(JSON.parse(decrypted));
75
94
  } catch (err) {
76
95
  console.error("Error parsing bun message:", err);
@@ -83,10 +102,12 @@ class Electroview<T extends RPCWithTransport> {
83
102
  });
84
103
 
85
104
  socket.addEventListener("error", (event) => {
105
+ this.hostSocketCanSend = false;
86
106
  console.error("Socket error:", event);
87
107
  });
88
108
 
89
109
  socket.addEventListener("close", (_event) => {
110
+ this.hostSocketCanSend = false;
90
111
  // console.log("Socket closed:", event);
91
112
  });
92
113
  }
@@ -97,9 +118,9 @@ class Electroview<T extends RPCWithTransport> {
97
118
  send(message: unknown) {
98
119
  try {
99
120
  const messageString = JSON.stringify(message);
100
- that.bunBridge(messageString);
121
+ that.sendMessageToHost(messageString);
101
122
  } catch (error) {
102
- console.error("bun: failed to serialize message to webview", error);
123
+ console.error("host: failed to serialize message to webview", error);
103
124
  }
104
125
  },
105
126
  registerHandler(handler: (msg: unknown) => void) {
@@ -108,8 +129,11 @@ class Electroview<T extends RPCWithTransport> {
108
129
  };
109
130
  }
110
131
 
111
- async bunBridge(msg: string) {
112
- if (this.bunSocket?.readyState === WebSocket.OPEN) {
132
+ async sendMessageToHost(msg: string) {
133
+ if (
134
+ this.hostSocketCanSend &&
135
+ this.hostSocket?.readyState === WebSocket.OPEN
136
+ ) {
113
137
  try {
114
138
  const { encryptedData, iv, tag } =
115
139
  await window.__electrobun_encrypt(msg);
@@ -120,24 +144,42 @@ class Electroview<T extends RPCWithTransport> {
120
144
  tag: tag,
121
145
  };
122
146
  const encryptedPacketString = JSON.stringify(encryptedPacket);
123
- this.bunSocket.send(encryptedPacketString);
147
+ this.hostSocket.send(encryptedPacketString);
124
148
  return;
125
149
  } catch (error) {
126
- console.error("Error sending message to bun via socket:", error);
150
+ console.error("Error sending message to host via socket:", error);
127
151
  }
128
152
  }
129
153
 
130
154
  // if socket's are unavailable, fallback to postMessage
131
- window.__electrobunBunBridge?.postMessage(msg);
155
+ window.__electrobunHostBridge?.postMessage(msg);
132
156
  }
133
157
 
134
- receiveMessageFromBun(msg: unknown) {
135
- // NOTE: in the webview messages are passed by executing ElectrobunView.receiveMessageFromBun(object)
158
+ receiveMessageFromHost(msg: unknown) {
159
+ // NOTE: in the webview messages are passed by executing window.__electrobun.receiveMessageFromHost(object)
136
160
  // so they're already parsed into an object here
137
161
  if (this.rpcHandler) {
138
162
  this.rpcHandler(msg);
139
163
  }
140
164
  }
165
+
166
+ async invokeCarrot<R = unknown>(
167
+ carrotId: string,
168
+ method: string,
169
+ params?: unknown,
170
+ options?: { windowId?: string },
171
+ ): Promise<R> {
172
+ const requestProxy = (this.rpc as any)?.request;
173
+ if (!requestProxy || typeof requestProxy.invokeCarrot !== "function") {
174
+ throw new Error("Renderer carrot invocation is not available in this Electrobun host.");
175
+ }
176
+ return requestProxy.invokeCarrot({
177
+ carrotId,
178
+ method,
179
+ params,
180
+ windowId: options?.windowId,
181
+ }) as Promise<R>;
182
+ }
141
183
  static defineRPC<Schema extends ElectrobunRPCSchema>(
142
184
  config: ElectrobunRPCConfig<Schema, "webview">,
143
185
  ) {
@@ -13,6 +13,28 @@ type BunBuildOptions = Omit<
13
13
  "entrypoints" | "outdir" | "target"
14
14
  >;
15
15
 
16
+ type CarrotFileActivatorConfig = {
17
+ baseName?: string;
18
+ nodeType?: "file" | "dir" | "any";
19
+ slate: {
20
+ type: string;
21
+ name?: string;
22
+ icon?: string;
23
+ config?: Record<string, unknown>;
24
+ };
25
+ };
26
+
27
+ type CarrotContributionsConfig = {
28
+ fileActivators?: CarrotFileActivatorConfig[];
29
+ };
30
+
31
+ type CarrotUIDefinition = {
32
+ name?: string;
33
+ entrypoint?: string;
34
+ path?: string;
35
+ [key: string]: unknown;
36
+ };
37
+
16
38
  export interface ElectrobunConfig {
17
39
  /**
18
40
  * Application metadata configuration
@@ -112,6 +134,14 @@ export interface ElectrobunConfig {
112
134
  * Build configuration options
113
135
  */
114
136
  build?: {
137
+ /**
138
+ * Main process implementation to build and package.
139
+ * - "bun": bundle and run the Bun main process entrypoint
140
+ * - "zig": compile and run the Zig main process entrypoint
141
+ * @default "bun"
142
+ */
143
+ mainProcess?: "bun" | "zig";
144
+
115
145
  /**
116
146
  * Bun process build configuration.
117
147
  * Accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
@@ -125,6 +155,18 @@ export interface ElectrobunConfig {
125
155
  entrypoint?: string;
126
156
  } & BunBuildOptions;
127
157
 
158
+ /**
159
+ * Zig main process build configuration.
160
+ * Used when `build.mainProcess` is set to `"zig"`.
161
+ */
162
+ zig?: {
163
+ /**
164
+ * Entry point for the main Zig process
165
+ * @default "src/zig/main.zig"
166
+ */
167
+ entrypoint?: string;
168
+ };
169
+
128
170
  /**
129
171
  * Browser view build configurations.
130
172
  * Each view accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
@@ -244,7 +286,9 @@ export interface ElectrobunConfig {
244
286
  carrotOnly?: boolean;
245
287
  permissions?: Record<string, unknown>;
246
288
  dependencies?: Record<string, string>;
247
- remoteUIs?: Record<string, { entrypoint: string; [key: string]: unknown }>;
289
+ remoteUIs?: Record<string, CarrotUIDefinition>;
290
+ slateUIs?: Record<string, CarrotUIDefinition>;
291
+ contributions?: CarrotContributionsConfig;
248
292
  };
249
293
 
250
294
  /**
@@ -1,5 +1,4 @@
1
- import { native, toCString, ffi } from "../proc/native";
2
- import * as fs from "fs";
1
+ import { ffi } from "../proc/native";
3
2
  import electrobunEventEmitter from "../events/eventEmitter";
4
3
  import {
5
4
  type ElectrobunRPCSchema,
@@ -7,10 +6,8 @@ import {
7
6
  type RPCWithTransport,
8
7
  defineElectrobunRPC,
9
8
  } from "../../shared/rpc.js";
10
- import { Updater } from "./Updater";
11
9
  import { BuildConfig } from "./BuildConfig";
12
10
  import {
13
- rpcPort,
14
11
  sendMessageToWebviewViaSocket,
15
12
  removeSocketForWebview,
16
13
  } from "./Socket";
@@ -20,7 +17,6 @@ import { type Pointer } from "bun:ffi";
20
17
  const BrowserViewMap: {
21
18
  [id: number]: BrowserView<any>;
22
19
  } = {};
23
- let nextWebviewId = 1;
24
20
 
25
21
  export type BrowserViewOptions<T = undefined> = {
26
22
  url: string | null;
@@ -38,7 +34,6 @@ export type BrowserViewOptions<T = undefined> = {
38
34
  rpc: T;
39
35
  hostWebviewId: number;
40
36
  autoResize: boolean;
41
-
42
37
  windowId: number;
43
38
  navigationRules: string | null;
44
39
  // Sandbox mode: when true, disables RPC and only allows event emission
@@ -52,8 +47,7 @@ export type BrowserViewOptions<T = undefined> = {
52
47
  // renderer:
53
48
  };
54
49
 
55
- const hash = await Updater.localInfo.hash();
56
- const buildConfig = await BuildConfig.get();
50
+ const buildConfig = BuildConfig.getSync();
57
51
 
58
52
  const defaultOptions: Partial<BrowserViewOptions> = {
59
53
  url: null,
@@ -68,13 +62,8 @@ const defaultOptions: Partial<BrowserViewOptions> = {
68
62
  height: 600,
69
63
  },
70
64
  };
71
- // Note: we use the build's hash to separate from different apps and different builds
72
- // but we also want a randomId to separate different instances of the same app
73
- const randomId = Math.random().toString(36).substring(7);
74
-
75
65
  export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
76
- id: number = nextWebviewId++;
77
- ptr: Pointer | null = null;
66
+ id = 0;
78
67
  hostWebviewId?: number;
79
68
  windowId!: number;
80
69
  renderer!: "cef" | "native";
@@ -95,9 +84,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
95
84
  width: 800,
96
85
  height: 600,
97
86
  };
98
- pipePrefix!: string;
99
- inStream!: fs.WriteStream;
100
- outStream!: ReadableStream<Uint8Array>;
101
87
  secretKey!: Uint8Array;
102
88
  rpc?: T;
103
89
  rpcHandler?: (msg: unknown) => void;
@@ -108,6 +94,13 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
108
94
  startPassthrough: boolean = false;
109
95
  isRemoved: boolean = false;
110
96
 
97
+ get ptr(): Pointer | null {
98
+ if (this.isRemoved) {
99
+ return null;
100
+ }
101
+ return ffi.request.getWebviewPointer({ id: this.id }) as Pointer | null;
102
+ }
103
+
111
104
  constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
112
105
  // const rpc = options.rpc;
113
106
 
@@ -124,9 +117,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
124
117
  this.rpc = options.rpc;
125
118
  this.secretKey = new Uint8Array(randomBytes(32));
126
119
  this.partition = options.partition || null;
127
- // todo (yoav): since collisions can crash the app add a function that checks if the
128
- // file exists first
129
- this.pipePrefix = `/private/tmp/electrobun_ipc_pipe_${hash}_${randomId}_${this.id}`;
130
120
  this.hostWebviewId = options.hostWebviewId;
131
121
  this.windowId = options.windowId ?? 0;
132
122
  this.autoResize = options.autoResize === false ? false : true;
@@ -136,8 +126,8 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
136
126
  this.startTransparent = options.startTransparent ?? false;
137
127
  this.startPassthrough = options.startPassthrough ?? false;
138
128
 
129
+ this.id = this.init() as number;
139
130
  BrowserViewMap[this.id] = this;
140
- this.ptr = this.init() as Pointer;
141
131
 
142
132
  // If HTML content was provided, load it after webview creation.
143
133
  if (this.html) {
@@ -148,21 +138,17 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
148
138
  }
149
139
 
150
140
  init() {
151
- this.createStreams();
141
+ this.initializeRpcTransport();
152
142
 
153
143
  return ffi.request.createWebview({
154
- id: this.id,
155
144
  windowId: this.windowId,
145
+ hostWebviewId: this.hostWebviewId ?? null,
156
146
  renderer: this.renderer,
157
- rpcPort: rpcPort,
158
147
  // todo: consider sending secretKey as base64
159
148
  secretKey: this.secretKey.toString(),
160
- hostWebviewId: this.hostWebviewId || null,
161
- pipePrefix: this.pipePrefix,
162
149
  partition: this.partition,
163
150
  // Only pass URL if no HTML content is provided to avoid conflicts
164
151
  url: this.html ? null : this.url,
165
- html: this.html,
166
152
  preload: this.preload,
167
153
  viewsRoot: this.viewsRoot,
168
154
  frame: {
@@ -180,7 +166,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
180
166
  });
181
167
  }
182
168
 
183
- createStreams() {
169
+ initializeRpcTransport() {
184
170
  if (!this.rpc) {
185
171
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
172
  this.rpc = BrowserView.defineRPC({
@@ -191,23 +177,23 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
191
177
  this.rpc!.setTransport(this.createTransport());
192
178
  }
193
179
 
194
- sendMessageToWebviewViaExecute(jsonMessage: unknown) {
180
+ sendHostMessageToWebviewViaExecute(jsonMessage: unknown) {
195
181
  const stringifiedMessage =
196
182
  typeof jsonMessage === "string"
197
183
  ? jsonMessage
198
184
  : JSON.stringify(jsonMessage);
199
185
  // todo (yoav): make this a shared const with the browser api
200
- const wrappedMessage = `window.__electrobun.receiveMessageFromBun(${stringifiedMessage})`;
186
+ const wrappedMessage = `window.__electrobun.receiveMessageFromHost(${stringifiedMessage})`;
201
187
  this.executeJavascript(wrappedMessage);
202
188
  }
203
189
 
204
- sendInternalMessageViaExecute(jsonMessage: unknown) {
190
+ sendInternalHostMessageViaExecute(jsonMessage: unknown) {
205
191
  const stringifiedMessage =
206
192
  typeof jsonMessage === "string"
207
193
  ? jsonMessage
208
194
  : JSON.stringify(jsonMessage);
209
195
  // todo (yoav): make this a shared const with the browser api
210
- const wrappedMessage = `window.__electrobun.receiveInternalMessageFromBun(${stringifiedMessage})`;
196
+ const wrappedMessage = `window.__electrobun.receiveInternalMessageFromHost(${stringifiedMessage})`;
211
197
  this.executeJavascript(wrappedMessage);
212
198
  }
213
199
 
@@ -224,7 +210,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
224
210
 
225
211
  loadURL(url: string) {
226
212
  this.url = url;
227
- native!.symbols.loadURLInWebView(this.ptr, toCString(this.url));
213
+ ffi.request.loadURLInWebView({ id: this.id, url: this.url });
228
214
  }
229
215
 
230
216
  loadHTML(html: string) {
@@ -232,18 +218,18 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
232
218
 
233
219
  if (this.renderer === "cef") {
234
220
  // For CEF, store HTML content in native map and use scheme handler
235
- native!.symbols.setWebviewHTMLContent(this.id, toCString(html));
221
+ ffi.request.setWebviewHTMLContent({ id: this.id, html });
236
222
  this.loadURL("views://internal/index.html");
237
223
  } else {
238
224
  // For WKWebView, load HTML content directly
239
- native!.symbols.loadHTMLInWebView(this.ptr, toCString(html));
225
+ ffi.request.loadHTMLInWebView({ id: this.id, html });
240
226
  }
241
227
  }
242
228
 
243
229
  setNavigationRules(rules: string[]) {
244
230
  this.navigationRules = JSON.stringify(rules);
245
231
  const rulesJson = JSON.stringify(rules);
246
- native!.symbols.setWebviewNavigationRules(this.ptr, toCString(rulesJson));
232
+ ffi.request.setWebviewNavigationRules({ id: this.id, rulesJson });
247
233
  }
248
234
 
249
235
  findInPage(
@@ -252,28 +238,28 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
252
238
  ) {
253
239
  const forward = options?.forward ?? true;
254
240
  const matchCase = options?.matchCase ?? false;
255
- native!.symbols.webviewFindInPage(
256
- this.ptr,
257
- toCString(searchText),
241
+ ffi.request.webviewFindInPage({
242
+ id: this.id,
243
+ searchText,
258
244
  forward,
259
245
  matchCase,
260
- );
246
+ });
261
247
  }
262
248
 
263
249
  stopFindInPage() {
264
- native!.symbols.webviewStopFind(this.ptr);
250
+ ffi.request.webviewStopFind({ id: this.id });
265
251
  }
266
252
 
267
253
  openDevTools() {
268
- native!.symbols.webviewOpenDevTools(this.ptr);
254
+ ffi.request.webviewOpenDevTools({ id: this.id });
269
255
  }
270
256
 
271
257
  closeDevTools() {
272
- native!.symbols.webviewCloseDevTools(this.ptr);
258
+ ffi.request.webviewCloseDevTools({ id: this.id });
273
259
  }
274
260
 
275
261
  toggleDevTools() {
276
- native!.symbols.webviewToggleDevTools(this.ptr);
262
+ ffi.request.webviewToggleDevTools({ id: this.id });
277
263
  }
278
264
 
279
265
  /**
@@ -281,7 +267,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
281
267
  * @param zoomLevel - The zoom level (1.0 = 100%, 1.5 = 150%, etc.)
282
268
  */
283
269
  setPageZoom(zoomLevel: number) {
284
- native!.symbols.webviewSetPageZoom(this.ptr, zoomLevel);
270
+ ffi.request.webviewSetPageZoom({ id: this.id, zoomLevel });
285
271
  }
286
272
 
287
273
  /**
@@ -289,7 +275,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
289
275
  * @returns The current zoom level (1.0 = 100%)
290
276
  */
291
277
  getPageZoom(): number {
292
- return native!.symbols.webviewGetPageZoom(this.ptr) as number;
278
+ return ffi.request.webviewGetPageZoom({ id: this.id }) as number;
293
279
  }
294
280
 
295
281
  // todo (yoav): move this to a class that also has off, append, prepend, etc.
@@ -326,9 +312,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
326
312
  if (!sentOverSocket) {
327
313
  try {
328
314
  const messageString = JSON.stringify(message);
329
- that.sendMessageToWebviewViaExecute(messageString);
315
+ that.sendHostMessageToWebviewViaExecute(messageString);
330
316
  } catch (error) {
331
- console.error("bun: failed to serialize message to webview", error);
317
+ console.error("host: failed to serialize message to webview", error);
332
318
  }
333
319
  }
334
320
  },
@@ -342,10 +328,9 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
342
328
  };
343
329
 
344
330
  remove() {
345
- if (!this.ptr || this.isRemoved) {
331
+ if (this.isRemoved) {
346
332
  return;
347
333
  }
348
- const ptr = this.ptr;
349
334
  this.isRemoved = true;
350
335
  // Drop JS-side references first so late callbacks cannot target a stale view.
351
336
  delete BrowserViewMap[this.id];
@@ -356,16 +341,72 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
356
341
  unregisterHandler() {},
357
342
  });
358
343
  this.rpcHandler = undefined;
359
-
360
- this.rpcHandler = undefined;
361
- this.ptr = null;
362
- native!.symbols.webviewRemove(ptr);
344
+ try {
345
+ ffi.request.webviewRemove({ id: this.id });
346
+ } catch (error) {
347
+ console.error(`Error removing webview ${this.id}:`, error);
348
+ }
363
349
  }
364
350
 
365
351
  static getById(id: number) {
366
352
  return BrowserViewMap[id];
367
353
  }
368
354
 
355
+ // Core can create webviews before Bun has constructed a JS wrapper for them.
356
+ // Use this in native/runtime paths that need to ensure a wrapper exists.
357
+ static ensureWrapped<T extends RPCWithTransport = RPCWithTransport>(
358
+ id: number,
359
+ options: Partial<BrowserViewOptions<T>> = {},
360
+ ) {
361
+ return (
362
+ (BrowserViewMap[id] as BrowserView<T> | undefined) ??
363
+ BrowserView.adoptExisting(id, options)
364
+ );
365
+ }
366
+
367
+ static adoptExisting<T extends RPCWithTransport = RPCWithTransport>(
368
+ id: number,
369
+ options: Partial<BrowserViewOptions<T>> = {},
370
+ ) {
371
+ const existing = BrowserViewMap[id] as BrowserView<T> | undefined;
372
+ if (existing) {
373
+ return existing;
374
+ }
375
+
376
+ const ptr = ffi.request.getWebviewPointer({ id }) as Pointer | null;
377
+ if (!ptr) {
378
+ return undefined;
379
+ }
380
+
381
+ const view = Object.create(BrowserView.prototype) as BrowserView<T>;
382
+ view.id = id;
383
+ view.hostWebviewId = options.hostWebviewId;
384
+ view.windowId = options.windowId ?? 0;
385
+ view.renderer = options.renderer ?? defaultOptions.renderer ?? "native";
386
+ view.url = options.url ?? defaultOptions.url ?? null;
387
+ view.html = options.html ?? defaultOptions.html ?? null;
388
+ view.preload = options.preload ?? defaultOptions.preload ?? null;
389
+ view.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? null;
390
+ view.partition = options.partition ?? null;
391
+ view.frame = {
392
+ x: options.frame?.x ?? defaultOptions.frame!.x,
393
+ y: options.frame?.y ?? defaultOptions.frame!.y,
394
+ width: options.frame?.width ?? defaultOptions.frame!.width,
395
+ height: options.frame?.height ?? defaultOptions.frame!.height,
396
+ };
397
+ view.secretKey = new Uint8Array(0);
398
+ view.rpc = options.rpc;
399
+ view.rpcHandler = undefined;
400
+ view.autoResize = options.autoResize === false ? false : true;
401
+ view.navigationRules = options.navigationRules ?? null;
402
+ view.sandbox = options.sandbox ?? false;
403
+ view.startTransparent = options.startTransparent ?? false;
404
+ view.startPassthrough = options.startPassthrough ?? false;
405
+ view.isRemoved = false;
406
+ BrowserViewMap[id] = view as BrowserView<any>;
407
+ return view;
408
+ }
409
+
369
410
  static getAll() {
370
411
  return Object.values(BrowserViewMap);
371
412
  }