bunite-core 0.12.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +4 -4
  2. package/src/host/core/App.ts +19 -2
  3. package/src/host/core/BrowserView.ts +515 -38
  4. package/src/host/core/SurfaceBrowserIPC.ts +53 -3
  5. package/src/host/core/SurfaceManager.ts +603 -30
  6. package/src/host/core/SurfaceRegistry.ts +9 -1
  7. package/src/host/core/inputDispatch.ts +147 -0
  8. package/src/host/events/webviewEvents.ts +25 -1
  9. package/src/host/log.ts +6 -1
  10. package/src/host/native.ts +263 -1
  11. package/src/host/preloadBundle.ts +7 -2
  12. package/src/native/linux/bunite_linux_ffi.cpp +427 -6
  13. package/src/native/linux/bunite_linux_internal.h +18 -0
  14. package/src/native/linux/bunite_linux_runtime.cpp +6 -1
  15. package/src/native/linux/bunite_linux_utils.cpp +2 -2
  16. package/src/native/linux/bunite_linux_view.cpp +296 -5
  17. package/src/native/mac/bunite_mac_ffi.mm +630 -8
  18. package/src/native/mac/bunite_mac_internal.h +19 -0
  19. package/src/native/mac/bunite_mac_utils.mm +2 -2
  20. package/src/native/mac/bunite_mac_view.mm +371 -9
  21. package/src/native/shared/ffi_exports.h +200 -2
  22. package/src/native/win/native_host_cef.cpp +186 -11
  23. package/src/native/win/native_host_ffi.cpp +1194 -1
  24. package/src/native/win/native_host_internal.h +35 -0
  25. package/src/native/win/native_host_utils.cpp +2 -1
  26. package/src/native/win/process_helper_win.cpp +54 -27
  27. package/src/native/win-webview2/bunite_webview2_ffi.cpp +1023 -12
  28. package/src/native/win-webview2/webview2_internal.h +25 -0
  29. package/src/native/win-webview2/webview2_runtime.cpp +403 -34
  30. package/src/native/win-webview2/webview2_utils.cpp +30 -12
  31. package/src/preload/runtime.built.js +1 -1
  32. package/src/preload/runtime.ts +97 -0
  33. package/src/rpc/framework.ts +340 -8
  34. package/src/rpc/index.ts +32 -0
  35. package/src/webview/native.ts +253 -51
  36. package/src/webview/polyfill.ts +283 -22
@@ -61,6 +61,16 @@ struct ViewState {
61
61
  // HTML stashed by load_html; appres handler serves at internal/index.html.
62
62
  std::string stored_html;
63
63
  std::vector<std::string> navigation_rules;
64
+
65
+ // Page-initiated dialogs awaiting host response (alert/confirm/prompt).
66
+ // WKUIDelegate completion handlers are held in `__strong` blocks until
67
+ // respondToDialog invokes them; the page execution is paused meanwhile.
68
+ std::unordered_map<uint32_t, void(^)(bool /*accept*/, const std::string& /*text*/)> pending_dialogs;
69
+ uint32_t next_dialog_request_id = 1;
70
+
71
+ // Download policy: 0=auto, 1=ask (treated as block), 2=block (default).
72
+ std::atomic<int32_t> download_policy{2};
73
+ std::string download_dir;
64
74
  };
65
75
 
66
76
  // ---------------------------------------------------------------------------
@@ -86,6 +96,11 @@ struct RuntimeState {
86
96
 
87
97
  BuniteWebviewEventHandler webview_event_handler = nullptr;
88
98
  BuniteWindowEventHandler window_event_handler = nullptr;
99
+
100
+ // Hidden NSWindow — popup-minted WKWebViews park here until adoption.
101
+ __strong NSWindow* popup_parent = nil;
102
+ // Parked popup webviews awaiting acceptPopup/dismissPopup. Key = popup view_id (>= 0x80000000).
103
+ __strong NSMutableDictionary<NSNumber*, WKWebView*>* parked_popups = nil;
89
104
  };
90
105
 
91
106
  extern RuntimeState g_runtime;
@@ -133,6 +148,10 @@ void destroyWindow(uint32_t window_id);
133
148
  // Defined in bunite_mac_view.mm.
134
149
  ViewState* findView(uint32_t view_id);
135
150
  uint32_t viewIdForWebView(WKWebView* wv); // returns 0 if not tracked
151
+ void registerWebViewId(WKWebView* wv, uint32_t view_id);
152
+ void unregisterWebViewId(WKWebView* wv);
153
+ bool acceptParkedPopup(uint32_t new_view_id, uint32_t host_window_id, double x, double y, double w, double h);
154
+ void dismissParkedPopup(uint32_t new_view_id);
136
155
  bool createView(uint32_t view_id, uint32_t window_id,
137
156
  NSString* url, NSString* html, NSString* preload, NSString* appres_root,
138
157
  NSString* navigation_rules_json, NSString* preload_origins_json,
@@ -100,8 +100,8 @@ std::vector<std::string> parseNavigationRulesJson(NSString* json) {
100
100
  }
101
101
 
102
102
  bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
103
- return url == "about:blank" ||
104
- url.rfind("appres://app.internal/internal/", 0) == 0;
103
+ // Exact-match prefix would let `../../evil` style paths bypass scrutiny.
104
+ return url == "about:blank" || url == "appres://app.internal/internal/index.html";
105
105
  }
106
106
 
107
107
  bool shouldAllowNavigation(const ViewState* view, const std::string& url) {
@@ -3,6 +3,7 @@
3
3
  #import "bunite_mac_internal.h"
4
4
 
5
5
  #include "webview_storage.h"
6
+ #include <objc/runtime.h>
6
7
 
7
8
  @implementation BunitePendingPermission
8
9
  @end
@@ -27,6 +28,125 @@
27
28
  using bunite_mac::g_runtime;
28
29
  using bunite_mac::utf8ToNSString;
29
30
 
31
+ // WKDownload delegate — decideDestination gates host policy + emits
32
+ // started/blocked, KVO on download.progress emits progress, didFinish/didFail
33
+ // emit terminal events.
34
+ @interface BuniteDownloadDelegate : NSObject <WKDownloadDelegate>
35
+ @property (nonatomic) uint32_t viewId;
36
+ @property (nonatomic) uint64_t downloadId;
37
+ @property (nonatomic, strong) NSString* destinationPath;
38
+ @property (nonatomic, weak) NSProgress* observedProgress;
39
+ @property (nonatomic) int64_t lastReportedBytes;
40
+ @property (atomic) BOOL terminalReached;
41
+ @end
42
+
43
+ static char kBuniteProgressCtx;
44
+
45
+ @implementation BuniteDownloadDelegate
46
+
47
+ - (void)download:(WKDownload*)download
48
+ decideDestinationUsingResponse:(NSURLResponse*)response
49
+ suggestedFilename:(NSString*)suggested
50
+ completionHandler:(void (^)(NSURL*))completionHandler
51
+ {
52
+ auto* st = bunite_mac::findView(self.viewId);
53
+ if (!st) { completionHandler(nil); return; }
54
+ const int32_t policy = st->download_policy.load();
55
+ NSString* url = download.originalRequest.URL.absoluteString ?: @"";
56
+ std::string url_s = url.UTF8String ?: "";
57
+ if (policy != 0) {
58
+ const char* reason = (policy == 1) ? "ask-not-implemented" : "host-policy";
59
+ std::string payload = "{\"kind\":\"blocked\",\"id\":\"mac-" + std::to_string(self.downloadId) +
60
+ "\",\"url\":\"" + bunite_mac::escapeJsonString(url_s) +
61
+ "\",\"reason\":\"" + reason + "\"}";
62
+ bunite_mac::emitWebviewEvent(self.viewId, "download-event", payload);
63
+ completionHandler(nil);
64
+ return;
65
+ }
66
+ NSString* dirStr = st->download_dir.empty() ? nil : [NSString stringWithUTF8String:st->download_dir.c_str()];
67
+ if (!dirStr || dirStr.length == 0) {
68
+ NSArray<NSURL*>* paths = [NSFileManager.defaultManager URLsForDirectory:NSDownloadsDirectory inDomains:NSUserDomainMask];
69
+ dirStr = paths.firstObject.path ?: NSTemporaryDirectory();
70
+ }
71
+ NSString* fname = suggested.length > 0 ? suggested : @"download";
72
+ NSString* path = [dirStr stringByAppendingPathComponent:fname];
73
+ self.destinationPath = path;
74
+ std::string mime = (response.MIMEType ?: @"").UTF8String ?: "";
75
+ long long total = response.expectedContentLength > 0 ? (long long)response.expectedContentLength : 0;
76
+ std::string started = "{\"kind\":\"started\",\"id\":\"mac-" + std::to_string(self.downloadId) +
77
+ "\",\"url\":\"" + bunite_mac::escapeJsonString(url_s) +
78
+ "\",\"suggestedFilename\":\"" + bunite_mac::escapeJsonString(fname.UTF8String ?: "") +
79
+ "\",\"mimeType\":\"" + bunite_mac::escapeJsonString(mime) + "\"";
80
+ if (total > 0) started += ",\"sizeBytes\":" + std::to_string(total);
81
+ started += "}";
82
+ bunite_mac::emitWebviewEvent(self.viewId, "download-event", started);
83
+ NSProgress* prog = download.progress;
84
+ if (prog) {
85
+ self.observedProgress = prog;
86
+ self.lastReportedBytes = 0;
87
+ [prog addObserver:self
88
+ forKeyPath:@"completedUnitCount"
89
+ options:NSKeyValueObservingOptionNew
90
+ context:&kBuniteProgressCtx];
91
+ }
92
+ completionHandler([NSURL fileURLWithPath:path]);
93
+ }
94
+
95
+ - (void)observeValueForKeyPath:(NSString*)keyPath
96
+ ofObject:(id)object
97
+ change:(NSDictionary*)change
98
+ context:(void*)context
99
+ {
100
+ if (context != &kBuniteProgressCtx) {
101
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
102
+ return;
103
+ }
104
+ if (self.terminalReached) return;
105
+ NSProgress* prog = (NSProgress*)object;
106
+ int64_t received = prog.completedUnitCount;
107
+ int64_t total = prog.totalUnitCount;
108
+ if (received <= self.lastReportedBytes) return;
109
+ self.lastReportedBytes = received;
110
+ std::string payload = "{\"kind\":\"progress\",\"id\":\"mac-" + std::to_string(self.downloadId) +
111
+ "\",\"receivedBytes\":" + std::to_string(received);
112
+ if (total > 0) payload += ",\"totalBytes\":" + std::to_string(total);
113
+ payload += "}";
114
+ bunite_mac::emitWebviewEvent(self.viewId, "download-event", payload);
115
+ }
116
+
117
+ - (void)_detachProgressObserver {
118
+ NSProgress* prog = self.observedProgress;
119
+ if (!prog) return;
120
+ @try {
121
+ [prog removeObserver:self forKeyPath:@"completedUnitCount" context:&kBuniteProgressCtx];
122
+ } @catch (NSException* ex) {} // tolerate over-remove on edge lifecycles
123
+ self.observedProgress = nil;
124
+ }
125
+
126
+ - (void)downloadDidFinish:(WKDownload*)download {
127
+ self.terminalReached = YES;
128
+ [self _detachProgressObserver];
129
+ std::string dest = self.destinationPath ? std::string(self.destinationPath.UTF8String ?: "") : "";
130
+ std::string payload = "{\"kind\":\"completed\",\"id\":\"mac-" + std::to_string(self.downloadId) +
131
+ "\",\"localPath\":\"" + bunite_mac::escapeJsonString(dest) + "\"}";
132
+ bunite_mac::emitWebviewEvent(self.viewId, "download-event", payload);
133
+ }
134
+
135
+ - (void)download:(WKDownload*)download didFailWithError:(NSError*)error resumeData:(NSData*)resumeData {
136
+ self.terminalReached = YES;
137
+ [self _detachProgressObserver];
138
+ std::string reason = error ? std::string(error.localizedDescription.UTF8String ?: "unknown") : "unknown";
139
+ std::string payload = "{\"kind\":\"failed\",\"id\":\"mac-" + std::to_string(self.downloadId) +
140
+ "\",\"reason\":\"" + bunite_mac::escapeJsonString(reason) + "\"}";
141
+ bunite_mac::emitWebviewEvent(self.viewId, "download-event", payload);
142
+ }
143
+
144
+ - (void)dealloc {
145
+ [self _detachProgressObserver];
146
+ }
147
+
148
+ @end
149
+
30
150
  @interface BuniteNavigationDelegate : NSObject <WKNavigationDelegate>
31
151
  @end
32
152
 
@@ -49,14 +169,95 @@ decidePolicyForNavigationAction:(WKNavigationAction*)action
49
169
  decisionHandler(allow ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel);
50
170
  }
51
171
 
52
- - (void)webView:(WKWebView*)wv didFinishNavigation:(WKNavigation*)nav {
172
+ - (void)webView:(WKWebView*)wv didStartProvisionalNavigation:(WKNavigation*)nav {
53
173
  (void)nav;
54
174
  uint32_t view_id = bunite_mac::viewIdForWebView(wv);
55
175
  NSString* url = wv.URL.absoluteString ?: @"";
176
+ bunite_mac::emitWebviewEvent(view_id, "load-start", url.UTF8String);
177
+ }
178
+
179
+ - (void)webView:(WKWebView*)wv didCommitNavigation:(WKNavigation*)nav {
180
+ (void)nav;
181
+ uint32_t view_id = bunite_mac::viewIdForWebView(wv);
182
+ NSString* url = wv.URL.absoluteString ?: @"";
183
+ // URL commit point — surfaceEvents `navigate` arm. WKWebView fires this
184
+ // when the document begins loading after server response (post-redirect).
56
185
  bunite_mac::emitWebviewEvent(view_id, "did-navigate", url.UTF8String);
186
+ }
187
+
188
+ - (void)webView:(WKWebView*)wv didFinishNavigation:(WKNavigation*)nav {
189
+ (void)nav;
190
+ uint32_t view_id = bunite_mac::viewIdForWebView(wv);
191
+ NSString* url = wv.URL.absoluteString ?: @"";
192
+ bunite_mac::emitWebviewEvent(view_id, "load-finish", url.UTF8String);
57
193
  bunite_mac::emitWebviewEvent(view_id, "dom-ready", url.UTF8String);
58
194
  }
59
195
 
196
+ - (void)webView:(WKWebView*)wv didFailNavigation:(WKNavigation*)nav withError:(NSError*)error {
197
+ (void)nav;
198
+ uint32_t view_id = bunite_mac::viewIdForWebView(wv);
199
+ NSString* url = wv.URL.absoluteString ?: @"";
200
+ std::string payload = "{\"url\":\"" + bunite_mac::escapeJsonString(url.UTF8String ?: "") +
201
+ "\",\"reason\":\"" + bunite_mac::escapeJsonString(error.localizedDescription.UTF8String ?: "") + "\"}";
202
+ bunite_mac::emitWebviewEvent(view_id, "load-fail", payload);
203
+ }
204
+
205
+ - (void)webView:(WKWebView*)wv didFailProvisionalNavigation:(WKNavigation*)nav withError:(NSError*)error {
206
+ (void)nav;
207
+ uint32_t view_id = bunite_mac::viewIdForWebView(wv);
208
+ NSString* failingUrl = ((NSURL*)error.userInfo[NSURLErrorFailingURLErrorKey]).absoluteString
209
+ ?: (wv.URL.absoluteString ?: @"");
210
+ std::string payload = "{\"url\":\"" + bunite_mac::escapeJsonString(failingUrl.UTF8String ?: "") +
211
+ "\",\"reason\":\"" + bunite_mac::escapeJsonString(error.localizedDescription.UTF8String ?: "") + "\"}";
212
+ bunite_mac::emitWebviewEvent(view_id, "load-fail", payload);
213
+ }
214
+
215
+ - (void)webView:(WKWebView*)wv navigationAction:(WKNavigationAction*)action didBecomeDownload:(WKDownload*)download {
216
+ static std::atomic<uint64_t> g_seq{1};
217
+ BuniteDownloadDelegate* d = [BuniteDownloadDelegate new];
218
+ d.viewId = bunite_mac::viewIdForWebView(wv);
219
+ d.downloadId = g_seq.fetch_add(1);
220
+ download.delegate = d;
221
+ objc_setAssociatedObject(download, "bunite.dl.delegate", d, OBJC_ASSOCIATION_RETAIN);
222
+ }
223
+
224
+ - (void)webView:(WKWebView*)wv navigationResponse:(WKNavigationResponse*)response didBecomeDownload:(WKDownload*)download {
225
+ static std::atomic<uint64_t> g_seq{0x80000000ULL};
226
+ BuniteDownloadDelegate* d = [BuniteDownloadDelegate new];
227
+ d.viewId = bunite_mac::viewIdForWebView(wv);
228
+ d.downloadId = g_seq.fetch_add(1);
229
+ download.delegate = d;
230
+ objc_setAssociatedObject(download, "bunite.dl.delegate", d, OBJC_ASSOCIATION_RETAIN);
231
+ }
232
+
233
+ @end
234
+
235
+ @interface BuniteTitleObserver : NSObject
236
+ + (instancetype)shared;
237
+ @end
238
+
239
+ @implementation BuniteTitleObserver
240
+ + (instancetype)shared {
241
+ static BuniteTitleObserver* o = nil;
242
+ static dispatch_once_t once;
243
+ dispatch_once(&once, ^{ o = [[BuniteTitleObserver alloc] init]; });
244
+ return o;
245
+ }
246
+ - (void)observeValueForKeyPath:(NSString*)keyPath
247
+ ofObject:(id)object
248
+ change:(NSDictionary<NSKeyValueChangeKey, id>*)change
249
+ context:(void*)context
250
+ {
251
+ (void)change; (void)context;
252
+ if (![keyPath isEqualToString:@"title"]) return;
253
+ WKWebView* wv = (WKWebView*)object;
254
+ uint32_t view_id = bunite_mac::viewIdForWebView(wv);
255
+ if (!view_id) return;
256
+ NSString* title = wv.title ?: @"";
257
+ std::string payload = "{\"title\":\"" +
258
+ bunite_mac::escapeJsonString(title.UTF8String ?: "") + "\"}";
259
+ bunite_mac::emitWebviewEvent(view_id, "title-changed", payload);
260
+ }
60
261
  @end
61
262
 
62
263
  @interface BuniteUIDelegate : NSObject <WKUIDelegate>
@@ -69,14 +270,118 @@ createWebViewWithConfiguration:(WKWebViewConfiguration*)config
69
270
  forNavigationAction:(WKNavigationAction*)action
70
271
  windowFeatures:(WKWindowFeatures*)features
71
272
  {
72
- (void)config; (void)features;
73
- uint32_t view_id = bunite_mac::viewIdForWebView(wv);
273
+ (void)features;
274
+ uint32_t opener_view_id = bunite_mac::viewIdForWebView(wv);
74
275
  NSString* url = action.request.URL.absoluteString ?: @"";
75
- // Match win OnBeforePopup/OnOpenURLFromTab: emit, cancel; JS decides via load_url.
76
- std::string payload = "{\"url\":\"" +
77
- bunite_mac::escapeJsonString(url.UTF8String ?: "") + "\"}";
78
- bunite_mac::emitWebviewEvent(view_id, "new-window-open", payload);
79
- return nil;
276
+ if (bunite_mac::g_runtime.popup_blocking) {
277
+ std::string payload = "{\"url\":\"" + bunite_mac::escapeJsonString(url.UTF8String ?: "") + "\"}";
278
+ bunite_mac::emitWebviewEvent(opener_view_id, "new-window-open", payload);
279
+ return nil;
280
+ }
281
+ static std::atomic<uint32_t> g_popup_seq{0x80000000u};
282
+ const uint32_t new_view_id = g_popup_seq.fetch_add(1);
283
+ WKWebView* popup = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 1, 1) configuration:config];
284
+ popup.navigationDelegate = wv.navigationDelegate;
285
+ popup.UIDelegate = wv.UIDelegate;
286
+ if (!bunite_mac::g_runtime.parked_popups) {
287
+ bunite_mac::g_runtime.parked_popups = [NSMutableDictionary dictionary];
288
+ }
289
+ if (!bunite_mac::g_runtime.popup_parent) {
290
+ bunite_mac::g_runtime.popup_parent =
291
+ [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 1, 1)
292
+ styleMask:NSWindowStyleMaskBorderless
293
+ backing:NSBackingStoreBuffered
294
+ defer:YES];
295
+ }
296
+ bunite_mac::g_runtime.parked_popups[@(new_view_id)] = popup;
297
+ [bunite_mac::g_runtime.popup_parent.contentView addSubview:popup];
298
+ bunite_mac::registerWebViewId(popup, new_view_id);
299
+ {
300
+ std::lock_guard<std::mutex> lock(bunite_mac::g_runtime.object_mutex);
301
+ auto& st = bunite_mac::g_runtime.views[new_view_id];
302
+ st.webview = popup;
303
+ st.container = nil; // bound on adoption
304
+ st.window_id = 0;
305
+ }
306
+ std::string payload = "{\"newSurfaceId\":" + std::to_string(new_view_id) +
307
+ ",\"url\":\"" + bunite_mac::escapeJsonString(url.UTF8String ?: "") +
308
+ "\",\"disposition\":\"popup\"}";
309
+ bunite_mac::emitWebviewEvent(opener_view_id, "popup-requested", payload);
310
+ return popup;
311
+ }
312
+
313
+ // --- Dialog handlers (alert / confirm / prompt). beforeunload is not
314
+ // surfaced by WKUIDelegate on macOS — the cancel/proceed decision is made
315
+ // through the navigation-policy delegate's `decidePolicyForNavigationAction:`
316
+ // which we already drive via the `will-navigate` allow-list. Hence no
317
+ // beforeunload arm on this backend.
318
+
319
+ - (void)webView:(WKWebView*)wv
320
+ runJavaScriptAlertPanelWithMessage:(NSString*)message
321
+ initiatedByFrame:(WKFrameInfo*)frame
322
+ completionHandler:(void (^)(void))completionHandler
323
+ {
324
+ (void)frame;
325
+ uint32_t view_id = bunite_mac::viewIdForWebView(wv);
326
+ auto* v = bunite_mac::findView(view_id);
327
+ if (!v) { completionHandler(); return; }
328
+ uint32_t rid = v->next_dialog_request_id++;
329
+ v->pending_dialogs[rid] = ^(bool /*accept*/, const std::string& /*text*/) {
330
+ completionHandler();
331
+ };
332
+ std::string payload = "{\"requestId\":" + std::to_string(rid) +
333
+ ",\"kind\":\"alert\",\"message\":\"" +
334
+ bunite_mac::escapeJsonString(message.UTF8String ?: "") + "\"}";
335
+ // Post emit to next runloop turn so a heavy host-side listener cannot stall
336
+ // the cooperative pump while the page is awaiting the completion handler.
337
+ dispatch_async(dispatch_get_main_queue(), ^{
338
+ bunite_mac::emitWebviewEvent(view_id, "dialog", payload);
339
+ });
340
+ }
341
+
342
+ - (void)webView:(WKWebView*)wv
343
+ runJavaScriptConfirmPanelWithMessage:(NSString*)message
344
+ initiatedByFrame:(WKFrameInfo*)frame
345
+ completionHandler:(void (^)(BOOL))completionHandler
346
+ {
347
+ (void)frame;
348
+ uint32_t view_id = bunite_mac::viewIdForWebView(wv);
349
+ auto* v = bunite_mac::findView(view_id);
350
+ if (!v) { completionHandler(NO); return; }
351
+ uint32_t rid = v->next_dialog_request_id++;
352
+ v->pending_dialogs[rid] = ^(bool accept, const std::string& /*text*/) {
353
+ completionHandler(accept ? YES : NO);
354
+ };
355
+ std::string payload = "{\"requestId\":" + std::to_string(rid) +
356
+ ",\"kind\":\"confirm\",\"message\":\"" +
357
+ bunite_mac::escapeJsonString(message.UTF8String ?: "") + "\"}";
358
+ dispatch_async(dispatch_get_main_queue(), ^{
359
+ bunite_mac::emitWebviewEvent(view_id, "dialog", payload);
360
+ });
361
+ }
362
+
363
+ - (void)webView:(WKWebView*)wv
364
+ runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
365
+ defaultText:(NSString*)defaultText
366
+ initiatedByFrame:(WKFrameInfo*)frame
367
+ completionHandler:(void (^)(NSString*))completionHandler
368
+ {
369
+ (void)frame;
370
+ uint32_t view_id = bunite_mac::viewIdForWebView(wv);
371
+ auto* v = bunite_mac::findView(view_id);
372
+ if (!v) { completionHandler(nil); return; }
373
+ uint32_t rid = v->next_dialog_request_id++;
374
+ v->pending_dialogs[rid] = ^(bool accept, const std::string& text) {
375
+ completionHandler(accept ? [NSString stringWithUTF8String:text.c_str()] : nil);
376
+ };
377
+ std::string payload = "{\"requestId\":" + std::to_string(rid) +
378
+ ",\"kind\":\"prompt\",\"message\":\"" +
379
+ bunite_mac::escapeJsonString(prompt.UTF8String ?: "") +
380
+ "\",\"defaultPrompt\":\"" +
381
+ bunite_mac::escapeJsonString(defaultText.UTF8String ?: "") + "\"}";
382
+ dispatch_async(dispatch_get_main_queue(), ^{
383
+ bunite_mac::emitWebviewEvent(view_id, "dialog", payload);
384
+ });
80
385
  }
81
386
 
82
387
  - (void)webView:(WKWebView*)wv
@@ -142,6 +447,14 @@ uint32_t viewIdForWebView(WKWebView* wv) {
142
447
  return id ? id.unsignedIntValue : 0;
143
448
  }
144
449
 
450
+ void registerWebViewId(WKWebView* wv, uint32_t view_id) {
451
+ [webviewIdTable() setObject:@(view_id) forKey:wv];
452
+ }
453
+
454
+ void unregisterWebViewId(WKWebView* wv) {
455
+ [webviewIdTable() removeObjectForKey:wv];
456
+ }
457
+
145
458
  bool createView(uint32_t view_id, uint32_t window_id,
146
459
  NSString* url, NSString* html, NSString* preload, NSString* appres_root,
147
460
  NSString* navigation_rules_json, NSString* preload_origins_json,
@@ -196,6 +509,7 @@ bool createView(uint32_t view_id, uint32_t window_id,
196
509
  static __strong BuniteUIDelegate* uiDelegate = [[BuniteUIDelegate alloc] init];
197
510
  wv.navigationDelegate = navDelegate;
198
511
  wv.UIDelegate = uiDelegate;
512
+ [wv addObserver:[BuniteTitleObserver shared] forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
199
513
 
200
514
  [container addSubview:wv];
201
515
  [window_state->window.contentView addSubview:container];
@@ -226,6 +540,50 @@ bool createView(uint32_t view_id, uint32_t window_id,
226
540
  return true;
227
541
  }
228
542
 
543
+ bool acceptParkedPopup(uint32_t new_view_id, uint32_t host_window_id, double x, double y, double w, double h) {
544
+ WKWebView* popup = nil;
545
+ if (g_runtime.parked_popups) {
546
+ popup = g_runtime.parked_popups[@(new_view_id)];
547
+ if (popup) [g_runtime.parked_popups removeObjectForKey:@(new_view_id)];
548
+ }
549
+ if (!popup) return false;
550
+ WindowState* host = findWindow(host_window_id);
551
+ if (!host || !host->window) return false;
552
+ [popup removeFromSuperview];
553
+ BunitePassthroughContainer* container = [[BunitePassthroughContainer alloc] init];
554
+ container.frame = NSMakeRect(x, y, w, h);
555
+ popup.frame = NSMakeRect(0, 0, w, h);
556
+ popup.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
557
+ [container addSubview:popup];
558
+ [host->window.contentView addSubview:container];
559
+ {
560
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
561
+ auto& st = g_runtime.views[new_view_id];
562
+ st.container = container;
563
+ st.webview = popup;
564
+ st.window_id = host_window_id;
565
+ }
566
+ // Re-emit view-ready so TS BrowserView.adopt resolves — the initial
567
+ // did-navigate fired before the adopter registered.
568
+ emitWebviewEvent(new_view_id, "view-ready", "");
569
+ return true;
570
+ }
571
+
572
+ void dismissParkedPopup(uint32_t new_view_id) {
573
+ WKWebView* popup = nil;
574
+ if (g_runtime.parked_popups) {
575
+ popup = g_runtime.parked_popups[@(new_view_id)];
576
+ if (popup) [g_runtime.parked_popups removeObjectForKey:@(new_view_id)];
577
+ }
578
+ if (!popup) return;
579
+ [popup removeFromSuperview];
580
+ unregisterWebViewId(popup);
581
+ {
582
+ std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
583
+ g_runtime.views.erase(new_view_id);
584
+ }
585
+ }
586
+
229
587
  void removeView(uint32_t view_id) {
230
588
  __strong WKWebView* wv = nil;
231
589
  __strong NSView* container = nil;
@@ -272,7 +630,11 @@ void removeView(uint32_t view_id) {
272
630
  for (BunitePendingPermission* p in to_deny) p.handler(WKPermissionDecisionDeny);
273
631
  }
274
632
 
275
- if (wv) [webviewIdTable() removeObjectForKey:wv];
633
+ if (wv) {
634
+ @try { [wv removeObserver:[BuniteTitleObserver shared] forKeyPath:@"title"]; }
635
+ @catch (NSException*) { /* never registered (race) */ }
636
+ [webviewIdTable() removeObjectForKey:wv];
637
+ }
276
638
  if (container) [container removeFromSuperview];
277
639
  }
278
640