@livx.cc/appwrap 0.55.0 → 0.56.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 +2 -3
- package/runtime/app/shell/capabilities.manifest.ts +3 -3
- package/runtime/tests/bridge-response-delivery.test.ts +10 -1
- package/runtime/tests/env-scheme.test.ts +6 -0
- package/scripts/stage-assets.mjs +1 -1
- package/src/cli.ts +40 -570
- package/src/config.ts +1 -1
- package/src/handlers.ts +1 -1
- package/src/packs.ts +1 -1
- package/runtime-desktop/autotest/index.html +0 -77
- package/runtime-desktop/bridge-shim/build.sh +0 -9
- package/runtime-desktop/bridge-shim/content-entry.ts +0 -87
- package/runtime-desktop/bridge-shim/shim.ts +0 -132
- package/runtime-desktop/chrome/appwrap-browser-chrome.html +0 -185
- package/runtime-desktop/crates/webview-control/Cargo.lock +0 -330
- package/runtime-desktop/crates/webview-control/Cargo.toml +0 -28
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +0 -362
- package/runtime-desktop/crates/webview-control/src/embedded.rs +0 -138
- package/runtime-desktop/crates/webview-control/src/lib.rs +0 -100
- package/runtime-desktop/crates/webview-control/src/popup.rs +0 -522
- package/runtime-desktop/crates/webview-control/src/profiles.rs +0 -57
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +0 -558
- package/runtime-desktop/src-tauri/Cargo.lock +0 -5474
- package/runtime-desktop/src-tauri/Cargo.toml +0 -45
- package/runtime-desktop/src-tauri/build.rs +0 -3
- package/runtime-desktop/src-tauri/capabilities/default.json +0 -6
- package/runtime-desktop/src-tauri/icons/icon.png +0 -0
- package/runtime-desktop/src-tauri/shell_config.json +0 -10
- package/runtime-desktop/src-tauri/src/biometrics_mac.rs +0 -138
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +0 -424
- package/runtime-desktop/src-tauri/src/browser_tab.rs +0 -804
- package/runtime-desktop/src-tauri/src/main.rs +0 -1228
- package/runtime-desktop/src-tauri/src/notifications_mac.rs +0 -202
- package/runtime-desktop/src-tauri/src/oauth_mac.rs +0 -139
- package/runtime-desktop/src-tauri/src/plugin_host.rs +0 -522
- package/runtime-desktop/src-tauri/src/popup_mac.rs +0 -5
- package/runtime-desktop/src-tauri/src/push_mac.rs +0 -193
- package/runtime-desktop/src-tauri/src/server.rs +0 -156
- package/runtime-desktop/src-tauri/src/sidecar.rs +0 -398
- package/runtime-desktop/src-tauri/tauri.conf.json +0 -14
- package/src/desktop.ts +0 -300
- package/src/plugin/host.ts +0 -242
|
@@ -1,804 +0,0 @@
|
|
|
1
|
-
// browser_tab — Tauri commands for an EMBEDDED, tear-off/dock-able browser tab (project "browser-tab"
|
|
2
|
-
// Phase 3). Thin front-end-facing wrapper over `webview_control::embedded::Controller`: the same live
|
|
3
|
-
// WKWebView (page + WKWebsiteDataStore session + bridgeShim) is mounted `ChildOf` a host pane NSView
|
|
4
|
-
// and torn off / docked with `set_mount`.
|
|
5
|
-
//
|
|
6
|
-
// R2 (agent-drive, DRY): the tab is created in the crate-level registry keyed by `id` (its label), so
|
|
7
|
-
// the control-channel adapter (`bridge_mac.rs`) — and thus the mcp-desktop-browser MCP — can drive
|
|
8
|
-
// this SAME embedded tab (navigate/eval/screenshot/close) by that id, with NO new TS<->Rust binding.
|
|
9
|
-
//
|
|
10
|
-
// Landmine (a): the pane NSView* is fetched + the webview mounted on the MAIN THREAD, AFTER the pane
|
|
11
|
-
// container has non-zero bounds (we create the container at the caller-supplied paneRect), else blank.
|
|
12
|
-
#[cfg(target_os = "macos")]
|
|
13
|
-
mod imp {
|
|
14
|
-
use std::collections::HashMap;
|
|
15
|
-
use std::ffi::c_void;
|
|
16
|
-
use std::path::PathBuf;
|
|
17
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
18
|
-
use std::sync::mpsc::channel;
|
|
19
|
-
use std::sync::{Mutex, OnceLock};
|
|
20
|
-
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
21
|
-
|
|
22
|
-
use objc2::rc::Retained;
|
|
23
|
-
use objc2::{MainThreadMarker, MainThreadOnly};
|
|
24
|
-
use objc2_app_kit::{NSAutoresizingMaskOptions, NSView, NSWindow, NSWindowStyleMask};
|
|
25
|
-
use objc2_foundation::{NSPoint, NSRect, NSSize};
|
|
26
|
-
use objc2_web_kit::WKWebView;
|
|
27
|
-
use serde::Deserialize;
|
|
28
|
-
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
|
|
29
|
-
|
|
30
|
-
use webview_control::embedded::Controller;
|
|
31
|
-
use webview_control::{CreateSpec, Mount, ViewHandle};
|
|
32
|
-
|
|
33
|
-
/// Fixed height (points) of the sub-window chrome strip (‹ › ⟳ + address bar + profile▾). The
|
|
34
|
-
/// controlled WKWebView pane is pinned BELOW this within the sub-window's contentView.
|
|
35
|
-
pub const CHROME_H: f64 = 44.0;
|
|
36
|
-
|
|
37
|
-
/// Autoresizing mask keeping the controlled-webview container pinned below the chrome bar while it
|
|
38
|
-
/// tracks window resize: both margins fixed (44pt top gap, 0 bottom), width+height flexible.
|
|
39
|
-
fn container_mask() -> NSAutoresizingMaskOptions {
|
|
40
|
-
NSAutoresizingMaskOptions(
|
|
41
|
-
NSAutoresizingMaskOptions::ViewWidthSizable.0 | NSAutoresizingMaskOptions::ViewHeightSizable.0,
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// Percent-encode a string for safe use as a URL hash-fragment param value (RFC 3986 unreserved
|
|
46
|
-
/// stay literal; everything else → %XX). URLSearchParams on the chrome side decodes it back.
|
|
47
|
-
fn hash_encode(s: &str) -> String {
|
|
48
|
-
let mut out = String::new();
|
|
49
|
-
for b in s.bytes() {
|
|
50
|
-
match b {
|
|
51
|
-
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
|
|
52
|
-
_ => out.push_str(&format!("%{b:02X}")),
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
out
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/// A unique sub-window id / bridge tab label.
|
|
59
|
-
fn new_subwin_id() -> String {
|
|
60
|
-
let ms = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0);
|
|
61
|
-
format!("subwin-{ms}")
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/// Web-DOM pane rect the front-end reserves (top-left origin, CSS px == points here since the
|
|
65
|
-
/// shell runs at the window's backing scale). We flip Y to AppKit's bottom-left origin at mount.
|
|
66
|
-
#[derive(Deserialize, Clone, Copy)]
|
|
67
|
-
pub struct PaneRect {
|
|
68
|
-
pub x: f64,
|
|
69
|
-
pub y: f64,
|
|
70
|
-
pub width: f64,
|
|
71
|
-
pub height: f64,
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// id -> host pane container NSView* (as usize). The container is retained by being a subview of
|
|
75
|
-
// the window contentView; we hold only its pointer to reposition / reparent / remove it. Access
|
|
76
|
-
// is main-thread only (all commands marshal via run_on_main_thread), the Mutex just satisfies the
|
|
77
|
-
// `static` Send/Sync bound for the raw pointer.
|
|
78
|
-
fn containers() -> &'static Mutex<HashMap<String, usize>> {
|
|
79
|
-
static CONTAINERS: OnceLock<Mutex<HashMap<String, usize>>> = OnceLock::new();
|
|
80
|
-
CONTAINERS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---- Session persistence: restore open browser sub-windows across app restarts ----
|
|
84
|
-
//
|
|
85
|
-
// We mirror the live set of open sub-windows (profile + last-navigated url, keyed by view id) to a
|
|
86
|
-
// JSON file. On the next launch `restore()` re-opens them. A user-closed window is dropped from the
|
|
87
|
-
// snapshot immediately; on app QUIT the snapshot is left intact (guarded by `APP_QUITTING`), so
|
|
88
|
-
// whatever was open comes back. Restore-fidelity is "last explicit navigation" (back/fwd history
|
|
89
|
-
// is not persisted) — deliberately lean.
|
|
90
|
-
|
|
91
|
-
/// Set on app exit so the per-window teardown (which fires on BOTH user-close and quit) leaves the
|
|
92
|
-
/// snapshot intact on quit but prunes it on a genuine user-close.
|
|
93
|
-
pub static APP_QUITTING: AtomicBool = AtomicBool::new(false);
|
|
94
|
-
|
|
95
|
-
fn sessions() -> &'static Mutex<HashMap<String, (String, String)>> {
|
|
96
|
-
static S: OnceLock<Mutex<HashMap<String, (String, String)>>> = OnceLock::new();
|
|
97
|
-
S.get_or_init(|| Mutex::new(HashMap::new()))
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
fn state_path() -> PathBuf {
|
|
101
|
-
PathBuf::from(std::env::var("HOME").unwrap_or_default())
|
|
102
|
-
.join("Library/Application Support/appwrap-desktop/browser-subwindows.json")
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/// Write the current live session set (profile,url pairs) to disk. Best-effort (logs on failure).
|
|
106
|
-
fn persist() {
|
|
107
|
-
let list: Vec<(String, String)> = sessions().lock().unwrap().values().cloned().collect();
|
|
108
|
-
let path = state_path();
|
|
109
|
-
if let Some(dir) = path.parent() {
|
|
110
|
-
let _ = std::fs::create_dir_all(dir);
|
|
111
|
-
}
|
|
112
|
-
if let Err(e) = std::fs::write(&path, serde_json::to_string_pretty(&list).unwrap_or_default()) {
|
|
113
|
-
eprintln!("[browser] persist sub-window sessions failed: {e}");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
fn record_open(id: &str, profile: &str, url: &str) {
|
|
118
|
-
sessions().lock().unwrap().insert(id.to_string(), (profile.to_string(), url.to_string()));
|
|
119
|
-
persist();
|
|
120
|
-
}
|
|
121
|
-
fn record_url(id: &str, url: &str) {
|
|
122
|
-
if let Some(e) = sessions().lock().unwrap().get_mut(id) {
|
|
123
|
-
e.1 = url.to_string();
|
|
124
|
-
} else {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
persist();
|
|
128
|
-
}
|
|
129
|
-
fn record_profile(id: &str, profile: &str) {
|
|
130
|
-
if let Some(e) = sessions().lock().unwrap().get_mut(id) {
|
|
131
|
-
e.0 = profile.to_string();
|
|
132
|
-
e.1 = "about:blank".to_string();
|
|
133
|
-
} else {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
persist();
|
|
137
|
-
}
|
|
138
|
-
/// Drop a session on genuine user-close; on app quit keep it so `restore()` re-opens it next launch.
|
|
139
|
-
fn record_close(id: &str) {
|
|
140
|
-
if APP_QUITTING.load(Ordering::SeqCst) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
sessions().lock().unwrap().remove(id);
|
|
144
|
-
persist();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
unsafe fn appkit_rect(content: &NSView, r: PaneRect) -> NSRect {
|
|
148
|
-
let ch = content.bounds().size.height;
|
|
149
|
-
NSRect::new(
|
|
150
|
-
NSPoint::new(r.x, ch - r.y - r.height),
|
|
151
|
-
NSSize::new(r.width, r.height),
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/// Map a DOM rect `r` (SHELL WEBVIEW viewport coords, top-left origin) to an AppKit frame in the
|
|
156
|
-
/// container's superview (contentView) coordinate space. Two corrections vs a naive `content.bounds`
|
|
157
|
-
/// flip: (1) `fe_in_content` — the shell webview's own rect in contentView coords (handles the
|
|
158
|
-
/// webview being offset/nested); (2) `sa` — the webview's SAFE-AREA insets. On a full-size-content
|
|
159
|
-
/// macOS window WKWebView insets its web VISUAL VIEWPORT under the titlebar (e.g. innerHeight 897 in
|
|
160
|
-
/// a 929pt webview), so DOM y=0 sits `sa.top` px BELOW the webview's frame top. Without adding
|
|
161
|
-
/// `sa.top`/`sa.left` the pane rides exactly that inset too high, covering the FE chrome. `cv_h` is
|
|
162
|
-
/// the contentView height (for the single bottom-left-origin flip). Callers pass
|
|
163
|
-
/// `wk.convertRect_toView(wk.bounds(), Some(content))` and `wk.safeAreaInsets()`.
|
|
164
|
-
fn appkit_rect_in(cv_h: f64, fe_in_content: NSRect, sa_top: f64, sa_left: f64, r: PaneRect) -> NSRect {
|
|
165
|
-
// Shell webview's frame top, measured from the contentView TOP (top-left origin space).
|
|
166
|
-
let fe_top_from_cv_top = cv_h - (fe_in_content.origin.y + fe_in_content.size.height);
|
|
167
|
-
// Pane top-left from contentView top: webview offset + safe-area inset + DOM offset.
|
|
168
|
-
let x = fe_in_content.origin.x + sa_left + r.x;
|
|
169
|
-
let top_from_cv_top = fe_top_from_cv_top + sa_top + r.y;
|
|
170
|
-
// Single flip to AppKit bottom-left origin.
|
|
171
|
-
NSRect::new(
|
|
172
|
-
NSPoint::new(x, cv_h - top_from_cv_top - r.height),
|
|
173
|
-
NSSize::new(r.width, r.height),
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/// The main window's contentView (parent of the shell WKWebView). Main-thread only.
|
|
178
|
-
unsafe fn content_view(win: &NSWindow) -> Retained<NSView> {
|
|
179
|
-
win.contentView().expect("main window has a contentView")
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
pub fn create(
|
|
183
|
-
app: tauri::AppHandle,
|
|
184
|
-
id: String,
|
|
185
|
-
url: String,
|
|
186
|
-
profile_id: String,
|
|
187
|
-
rect: PaneRect,
|
|
188
|
-
) -> Result<(), String> {
|
|
189
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
190
|
-
// with_webview runs its closure on the AppKit main thread. Build the pane container at
|
|
191
|
-
// non-zero bounds FIRST (landmine a), then mount the webview ChildOf it — all on main.
|
|
192
|
-
let (tx, rx) = std::sync::mpsc::channel();
|
|
193
|
-
win.with_webview(move |w| {
|
|
194
|
-
let res = (|| -> Result<(), String> {
|
|
195
|
-
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
196
|
-
unsafe {
|
|
197
|
-
// Idempotent: a fast unmount→remount (flexlayout re-render / StrictMode) can call
|
|
198
|
-
// create for an id whose webview+container still exist. Tear the old one down first
|
|
199
|
-
// so we never STACK duplicate WKWebViews for one tab (each is heavy — the prime
|
|
200
|
-
// suspect for the runaway memory growth when panes churn).
|
|
201
|
-
Controller::new().close(mtm, &id);
|
|
202
|
-
if let Some(ptr) = containers().lock().unwrap().remove(&id) {
|
|
203
|
-
(*(ptr as *mut NSView)).removeFromSuperview();
|
|
204
|
-
}
|
|
205
|
-
let wk = &*(w.inner() as *mut WKWebView);
|
|
206
|
-
let ns_win = wk.window().ok_or("shell webview has no window")?;
|
|
207
|
-
let content = content_view(&ns_win);
|
|
208
|
-
let fe_in_content = wk.convertRect_toView(wk.bounds(), Some(&content));
|
|
209
|
-
let sa = wk.safeAreaInsets();
|
|
210
|
-
let cb = content.bounds().size;
|
|
211
|
-
let frame = appkit_rect_in(cb.height, fe_in_content, sa.top, sa.left, rect);
|
|
212
|
-
let container = NSView::initWithFrame(NSView::alloc(mtm), frame);
|
|
213
|
-
content.addSubview(&container);
|
|
214
|
-
let cptr = Retained::as_ptr(&container) as *mut c_void;
|
|
215
|
-
let handle = ViewHandle::from_ptr(cptr);
|
|
216
|
-
// R2 (agent-drive, DRY): wire the SAME bridge init-JS + live self-healing page sink
|
|
217
|
-
// the sub-window path uses, so an embedded tab registers as a bod-browser tab
|
|
218
|
-
// addressable by `id` over the control channel — thus drivable by mcp-desktop-browser.
|
|
219
|
-
// Inert when the bridge feature is off (empty init-JS, None-safe sink); doesn't
|
|
220
|
-
// affect mount/rect (init_js/sink only touch page registration, not layout).
|
|
221
|
-
let (init_js, sink) = bridge_wiring(&id);
|
|
222
|
-
let spec = CreateSpec {
|
|
223
|
-
label: id.clone(),
|
|
224
|
-
url: url.clone(),
|
|
225
|
-
profile_id: profile_id.clone(),
|
|
226
|
-
init_js,
|
|
227
|
-
mount: Mount::ChildOf(handle),
|
|
228
|
-
};
|
|
229
|
-
Controller::new().create_webview(mtm, spec, sink)?;
|
|
230
|
-
// contentView holds a strong ref via addSubview; store only the pointer.
|
|
231
|
-
containers().lock().unwrap().insert(id.clone(), cptr as usize);
|
|
232
|
-
}
|
|
233
|
-
Ok(())
|
|
234
|
-
})();
|
|
235
|
-
let _ = tx.send(res);
|
|
236
|
-
})
|
|
237
|
-
.map_err(|e| e.to_string())?;
|
|
238
|
-
rx.recv().map_err(|e| e.to_string())?
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/// Reposition an embedded tab's host pane container to `rect` (front-end DOM coords). The webview
|
|
242
|
-
/// is a fill-masked subview of the container, so moving/resizing the container carries the live
|
|
243
|
-
/// webview with it — no webview re-create, session intact. Recomputes `appkit_rect` (Y-flip vs the
|
|
244
|
-
/// current content-view height) EXACTLY as `create()` does, so a layout move/resize/scroll on the
|
|
245
|
-
/// FE side keeps the native pane glued to the reserved DOM rect. Runs on the main thread.
|
|
246
|
-
pub fn set_rect(app: tauri::AppHandle, id: String, rect: PaneRect) -> Result<(), String> {
|
|
247
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
248
|
-
let (tx, rx) = std::sync::mpsc::channel();
|
|
249
|
-
win.with_webview(move |w| {
|
|
250
|
-
let res = (|| -> Result<(), String> {
|
|
251
|
-
let _mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
252
|
-
unsafe {
|
|
253
|
-
let wk = &*(w.inner() as *mut WKWebView);
|
|
254
|
-
let ns_win = wk.window().ok_or("shell webview has no window")?;
|
|
255
|
-
let content = content_view(&ns_win);
|
|
256
|
-
let fe_in_content = wk.convertRect_toView(wk.bounds(), Some(&content));
|
|
257
|
-
let sa = wk.safeAreaInsets();
|
|
258
|
-
let cb = content.bounds().size;
|
|
259
|
-
let frame = appkit_rect_in(cb.height, fe_in_content, sa.top, sa.left, rect);
|
|
260
|
-
let ptr = *containers().lock().unwrap().get(&id).ok_or("no pane for id")?;
|
|
261
|
-
let container = &*(ptr as *mut NSView);
|
|
262
|
-
container.setFrame(frame);
|
|
263
|
-
}
|
|
264
|
-
Ok(())
|
|
265
|
-
})();
|
|
266
|
-
let _ = tx.send(res);
|
|
267
|
-
})
|
|
268
|
-
.map_err(|e| e.to_string())?;
|
|
269
|
-
rx.recv().map_err(|e| e.to_string())?
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
pub fn set_mount(app: tauri::AppHandle, id: String, mode: String) -> Result<(), String> {
|
|
273
|
-
let (tx, rx) = std::sync::mpsc::channel();
|
|
274
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
275
|
-
win.run_on_main_thread(move || {
|
|
276
|
-
let res = (|| -> Result<(), String> {
|
|
277
|
-
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
278
|
-
let mount = match mode.as_str() {
|
|
279
|
-
"toplevel" => Mount::TopLevel,
|
|
280
|
-
"child" => {
|
|
281
|
-
let ptr = *containers().lock().unwrap().get(&id).ok_or("no pane for id")?;
|
|
282
|
-
unsafe { Mount::ChildOf(ViewHandle::from_ptr(ptr as *mut c_void)) }
|
|
283
|
-
}
|
|
284
|
-
other => return Err(format!("bad mount mode: {other}")),
|
|
285
|
-
};
|
|
286
|
-
if Controller::new().set_mount(mtm, &id, mount) {
|
|
287
|
-
Ok(())
|
|
288
|
-
} else {
|
|
289
|
-
Err(format!("no webview {id}"))
|
|
290
|
-
}
|
|
291
|
-
})();
|
|
292
|
-
let _ = tx.send(res);
|
|
293
|
-
})
|
|
294
|
-
.map_err(|e| e.to_string())?;
|
|
295
|
-
rx.recv().map_err(|e| e.to_string())?
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
pub fn navigate(app: tauri::AppHandle, id: String, url: String) -> Result<(), String> {
|
|
299
|
-
let (tx, rx) = std::sync::mpsc::channel();
|
|
300
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
301
|
-
let (id_c, url_c) = (id.clone(), url.clone());
|
|
302
|
-
win.run_on_main_thread(move || {
|
|
303
|
-
let res = (|| -> Result<(), String> {
|
|
304
|
-
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
305
|
-
Controller::new().navigate(mtm, &id, &url)
|
|
306
|
-
})();
|
|
307
|
-
let _ = tx.send(res);
|
|
308
|
-
})
|
|
309
|
-
.map_err(|e| e.to_string())?;
|
|
310
|
-
rx.recv().map_err(|e| e.to_string())??;
|
|
311
|
-
// Persist the last-navigated url for sub-window session restore (no-op for embedded tabs).
|
|
312
|
-
record_url(&id_c, &url_c);
|
|
313
|
-
Ok(())
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
pub fn close(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
317
|
-
let (tx, rx) = std::sync::mpsc::channel();
|
|
318
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
319
|
-
win.run_on_main_thread(move || {
|
|
320
|
-
let res = (|| -> Result<(), String> {
|
|
321
|
-
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
322
|
-
Controller::new().close(mtm, &id);
|
|
323
|
-
if let Some(ptr) = containers().lock().unwrap().remove(&id) {
|
|
324
|
-
unsafe {
|
|
325
|
-
let container = &*(ptr as *mut NSView);
|
|
326
|
-
container.removeFromSuperview();
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
Ok(())
|
|
330
|
-
})();
|
|
331
|
-
let _ = tx.send(res);
|
|
332
|
-
})
|
|
333
|
-
.map_err(|e| e.to_string())?;
|
|
334
|
-
rx.recv().map_err(|e| e.to_string())?
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/// Tear down EVERY embedded pane (main-window children). Called on ⌘R app reload: `reload()`
|
|
338
|
-
/// reloads the FE but never runs React's unmount cleanup, so `close` is never called and the panes
|
|
339
|
-
/// orphan — leaked WKWebViews + visual mixing during the reload gap. The reloaded FE re-creates its
|
|
340
|
-
/// tabs by the SAME id (`create` is idempotent), so this is a safe pre-reload wipe. Sub-window panes
|
|
341
|
-
/// (`*-view`) live in their own OS windows, untouched by a main-window reload, so they're skipped.
|
|
342
|
-
/// Main-thread only: the ⌘R menu handler already runs on main, so we run inline with its
|
|
343
|
-
/// `MainThreadMarker` — NOT via run_on_main_thread+recv, which would deadlock main against itself.
|
|
344
|
-
pub fn close_all_embedded(mtm: MainThreadMarker) {
|
|
345
|
-
let ids: Vec<String> = containers()
|
|
346
|
-
.lock()
|
|
347
|
-
.unwrap()
|
|
348
|
-
.keys()
|
|
349
|
-
.filter(|id| !id.ends_with("-view"))
|
|
350
|
-
.cloned()
|
|
351
|
-
.collect();
|
|
352
|
-
let c = Controller::new();
|
|
353
|
-
for id in &ids {
|
|
354
|
-
c.close(mtm, id);
|
|
355
|
-
if let Some(ptr) = containers().lock().unwrap().remove(id) {
|
|
356
|
-
unsafe { (*(ptr as *mut NSView)).removeFromSuperview() };
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ---- Sub-window: a standalone Tauri chrome window + a controlled WKWebView pane below the bar ----
|
|
362
|
-
|
|
363
|
-
/// Bridge init-JS + page sink for a sub-window webview, so it registers as a bod-browser tab when
|
|
364
|
-
/// the shim is running, and is inert-but-functional otherwise (empty init, no-op sink).
|
|
365
|
-
fn bridge_wiring(id: &str) -> (String, webview_control::MessageSink) {
|
|
366
|
-
let init_js = crate::bridge_mac::bridge_init_js(id).unwrap_or_default();
|
|
367
|
-
// Live self-healing sink: delivers to whichever shim is connected at send-time (None-safe),
|
|
368
|
-
// so a sub-window created while the shim is down becomes agent-exposed on a later connect.
|
|
369
|
-
let sink = crate::bridge_mac::bridge_sink();
|
|
370
|
-
(init_js, sink)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/// Mount a controlled WKWebView `ChildOf` a pane pinned below the chrome bar in `win`'s content
|
|
374
|
-
/// view, wiring the bridge for agent-exposure. Runs on the main thread (via `with_webview`).
|
|
375
|
-
fn mount_pane(win: &tauri::WebviewWindow, id: &str, profile_id: &str, url: &str) -> Result<(), String> {
|
|
376
|
-
let (tx, rx) = channel();
|
|
377
|
-
let (id, profile_id, url) = (id.to_string(), profile_id.to_string(), url.to_string());
|
|
378
|
-
win.with_webview(move |w| {
|
|
379
|
-
let res = (|| -> Result<(), String> {
|
|
380
|
-
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
381
|
-
unsafe {
|
|
382
|
-
let wk = &*(w.inner() as *mut WKWebView);
|
|
383
|
-
let ns_win = wk.window().ok_or("chrome webview has no window")?;
|
|
384
|
-
// wry gives the sub-window a FULL-SIZE content view: contentView spans the whole
|
|
385
|
-
// frame and the traffic-light titlebar overlays its top, hiding all but a sliver of
|
|
386
|
-
// the HTML chrome bar. Drop the full-size-content style so contentView sits BELOW
|
|
387
|
-
// the titlebar — the bar + pane then lay out in the visible region with no offset.
|
|
388
|
-
ns_win.setStyleMask(NSWindowStyleMask(
|
|
389
|
-
ns_win.styleMask().0 & !NSWindowStyleMask::FullSizeContentView.0,
|
|
390
|
-
));
|
|
391
|
-
ns_win.setTitlebarAppearsTransparent(false);
|
|
392
|
-
let content = content_view(&ns_win);
|
|
393
|
-
let b = content.bounds().size;
|
|
394
|
-
let rect = PaneRect { x: 0.0, y: CHROME_H, width: b.width, height: (b.height - CHROME_H).max(0.0) };
|
|
395
|
-
let frame = appkit_rect(&content, rect);
|
|
396
|
-
let container = NSView::initWithFrame(NSView::alloc(mtm), frame);
|
|
397
|
-
container.setAutoresizingMask(container_mask());
|
|
398
|
-
content.addSubview(&container);
|
|
399
|
-
let cptr = Retained::as_ptr(&container) as *mut c_void;
|
|
400
|
-
let handle = ViewHandle::from_ptr(cptr);
|
|
401
|
-
let (init_js, sink) = bridge_wiring(&id);
|
|
402
|
-
let spec = CreateSpec {
|
|
403
|
-
label: id.clone(),
|
|
404
|
-
url: url.clone(),
|
|
405
|
-
profile_id: profile_id.clone(),
|
|
406
|
-
init_js,
|
|
407
|
-
mount: Mount::ChildOf(handle),
|
|
408
|
-
};
|
|
409
|
-
Controller::new().create_webview(mtm, spec, sink)?;
|
|
410
|
-
containers().lock().unwrap().insert(id.clone(), cptr as usize);
|
|
411
|
-
}
|
|
412
|
-
Ok(())
|
|
413
|
-
})();
|
|
414
|
-
let _ = tx.send(res);
|
|
415
|
-
})
|
|
416
|
-
.map_err(|e| e.to_string())?;
|
|
417
|
-
rx.recv().map_err(|e| e.to_string())?
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/// Clean up a sub-window's controlled webview + container. Main-thread only (called from the
|
|
421
|
-
/// window's Destroyed event, which fires on the main thread).
|
|
422
|
-
fn teardown(id: &str, mtm: MainThreadMarker) {
|
|
423
|
-
Controller::new().close(mtm, id);
|
|
424
|
-
if let Some(ptr) = containers().lock().unwrap().remove(id) {
|
|
425
|
-
unsafe { (*(ptr as *mut NSView)).removeFromSuperview() };
|
|
426
|
-
}
|
|
427
|
-
// Prune from the restore snapshot on a genuine user-close; kept intact on app quit.
|
|
428
|
-
record_close(id);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/// Open a sub-window: build the Tauri chrome window (loads the bundled chrome HTML asset, `?id=`
|
|
432
|
-
/// stamped so it binds to this webview), then mount the controlled webview below the chrome bar.
|
|
433
|
-
pub fn open(app: tauri::AppHandle, profile_id: String) -> Result<String, String> {
|
|
434
|
-
// Two DISTINCT labels: the Tauri chrome window (`win_id`) and the controlled WKWebView it
|
|
435
|
-
// hosts (`view_id = <win_id>-view`). Sharing one label double-listed it in `listWindows`
|
|
436
|
-
// (once from `app.webview_windows()`, once from the crate registry). The chrome HTML, its
|
|
437
|
-
// invokes, the container/registry key, the bridge `__BRIDGE_LABEL`, and what agents drive all
|
|
438
|
-
// key on `view_id`; only the OS window keeps `win_id`.
|
|
439
|
-
let win_id = new_subwin_id();
|
|
440
|
-
let view_id = format!("{win_id}-view");
|
|
441
|
-
// Pass the view id via a hash FRAGMENT, not a query — Tauri's `WebviewUrl::App` treats the
|
|
442
|
-
// string as a path and the `?…` breaks asset resolution (looks up a literal `…html?id=…` file
|
|
443
|
-
// → 404 → blank chrome). Fragments are never used for lookup and survive in `location.hash`.
|
|
444
|
-
// Also pass the profile so the chrome dropdown reflects the ACTUAL mounted profile (a restored
|
|
445
|
-
// window may be on a non-default profile). Percent-encode it — profile names are user-entered.
|
|
446
|
-
let chrome_url = format!(
|
|
447
|
-
"appwrap-browser-chrome.html#id={view_id}&profile={}",
|
|
448
|
-
hash_encode(&profile_id)
|
|
449
|
-
);
|
|
450
|
-
// Open at the main window's size (logical points) so the sub-window matches the app it spawned
|
|
451
|
-
// from, falling back to a sane default if the main window or its metrics are unavailable.
|
|
452
|
-
let (w, h) = app
|
|
453
|
-
.get_webview_window("main")
|
|
454
|
-
.and_then(|m| {
|
|
455
|
-
let sf = m.scale_factor().ok()?;
|
|
456
|
-
let sz = m.inner_size().ok()?;
|
|
457
|
-
Some((sz.width as f64 / sf, sz.height as f64 / sf))
|
|
458
|
-
})
|
|
459
|
-
.unwrap_or((1100.0, 800.0));
|
|
460
|
-
let win = WebviewWindowBuilder::new(&app, &win_id, WebviewUrl::App(chrome_url.into()))
|
|
461
|
-
.title("Browser")
|
|
462
|
-
.inner_size(w, h)
|
|
463
|
-
.build()
|
|
464
|
-
.map_err(|e| e.to_string())?;
|
|
465
|
-
// Tear the controlled webview down when the sub-window closes, so it doesn't linger as a
|
|
466
|
-
// registry zombie / stray bod-browser tab.
|
|
467
|
-
let idc = view_id.clone();
|
|
468
|
-
win.on_window_event(move |ev| {
|
|
469
|
-
if matches!(ev, tauri::WindowEvent::Destroyed) {
|
|
470
|
-
if let Some(mtm) = MainThreadMarker::new() {
|
|
471
|
-
teardown(&idc, mtm);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
mount_pane(&win, &view_id, &profile_id, "about:blank")?;
|
|
476
|
-
record_open(&view_id, &profile_id, "about:blank");
|
|
477
|
-
Ok(view_id)
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/// Re-open the sub-windows that were open at last quit (profile + last-navigated url). Best-effort:
|
|
481
|
-
/// a failed re-open is logged and skipped. Called once on app startup.
|
|
482
|
-
pub fn restore(app: tauri::AppHandle) {
|
|
483
|
-
let saved: Vec<(String, String)> = std::fs::read_to_string(state_path())
|
|
484
|
-
.ok()
|
|
485
|
-
.and_then(|s| serde_json::from_str(&s).ok())
|
|
486
|
-
.unwrap_or_default();
|
|
487
|
-
for (profile, url) in saved {
|
|
488
|
-
match open(app.clone(), profile) {
|
|
489
|
-
Ok(id) => {
|
|
490
|
-
if !url.is_empty() && url != "about:blank" {
|
|
491
|
-
if let Err(e) = navigate(app.clone(), id, url) {
|
|
492
|
-
eprintln!("[browser] restore navigate failed: {e}");
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
Err(e) => eprintln!("[browser] restore open failed: {e}"),
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/// Re-fire the `register` beacon on every open sub-window controlled webview. Called when the
|
|
502
|
-
/// shim (re)connects: their live sink can now deliver, but an already-loaded document won't
|
|
503
|
-
/// re-run its init-JS `register()`, so nudge each via the bridgeShim handler to re-post it.
|
|
504
|
-
/// Idempotent + inert when the bridge feature is off (no bridgeShim handler → the try/catch eats
|
|
505
|
-
/// it) and when there are no sub-windows.
|
|
506
|
-
pub fn reregister_all(app: &tauri::AppHandle) {
|
|
507
|
-
let ids: Vec<String> = containers().lock().unwrap().keys().cloned().collect();
|
|
508
|
-
if ids.is_empty() {
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
let _ = app.run_on_main_thread(move || {
|
|
512
|
-
if let Some(mtm) = MainThreadMarker::new() {
|
|
513
|
-
let c = Controller::new();
|
|
514
|
-
for id in &ids {
|
|
515
|
-
let js = "try{if(window.__BRIDGE_LABEL){webkit.messageHandlers.bridgeShim.postMessage(JSON.stringify({label:window.__BRIDGE_LABEL,type:'register',url:location.href,title:document.title}))}}catch(e){}".to_string();
|
|
516
|
-
let _ = c.inject_script(mtm, id, &js);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/// Swap the sub-window's controlled webview to another profile: profile is immutable per-webview,
|
|
523
|
-
/// so close the current one (keeps the pane container) and recreate `ChildOf` the SAME container
|
|
524
|
-
/// with the new profile. The Tauri chrome window is untouched.
|
|
525
|
-
pub fn switch_profile(app: tauri::AppHandle, id: String, profile_id: String) -> Result<(), String> {
|
|
526
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
527
|
-
let (id_c, profile_c) = (id.clone(), profile_id.clone());
|
|
528
|
-
let (tx, rx) = channel();
|
|
529
|
-
win.run_on_main_thread(move || {
|
|
530
|
-
let res = (|| -> Result<(), String> {
|
|
531
|
-
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
532
|
-
let ptr = *containers().lock().unwrap().get(&id).ok_or("no pane for id")?;
|
|
533
|
-
Controller::new().close(mtm, &id);
|
|
534
|
-
unsafe {
|
|
535
|
-
let handle = ViewHandle::from_ptr(ptr as *mut c_void);
|
|
536
|
-
let (init_js, sink) = bridge_wiring(&id);
|
|
537
|
-
let spec = CreateSpec {
|
|
538
|
-
label: id.clone(),
|
|
539
|
-
url: "about:blank".to_string(),
|
|
540
|
-
profile_id: profile_id.clone(),
|
|
541
|
-
init_js,
|
|
542
|
-
mount: Mount::ChildOf(handle),
|
|
543
|
-
};
|
|
544
|
-
Controller::new().create_webview(mtm, spec, sink)?;
|
|
545
|
-
}
|
|
546
|
-
Ok(())
|
|
547
|
-
})();
|
|
548
|
-
let _ = tx.send(res);
|
|
549
|
-
})
|
|
550
|
-
.map_err(|e| e.to_string())?;
|
|
551
|
-
rx.recv().map_err(|e| e.to_string())??;
|
|
552
|
-
// Persist the new profile (resets the restored url to about:blank) for session restore.
|
|
553
|
-
record_profile(&id_c, &profile_c);
|
|
554
|
-
Ok(())
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/// Route a nav op (`back`/`forward`/`reload`) to the controlled webview `id` on the main thread.
|
|
558
|
-
pub fn nav_op(app: tauri::AppHandle, id: String, op: &'static str) -> Result<(), String> {
|
|
559
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
560
|
-
let (tx, rx) = channel();
|
|
561
|
-
win.run_on_main_thread(move || {
|
|
562
|
-
let res = (|| -> Result<(), String> {
|
|
563
|
-
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
564
|
-
let c = Controller::new();
|
|
565
|
-
match op {
|
|
566
|
-
"back" => c.go_back(mtm, &id),
|
|
567
|
-
"forward" => c.go_forward(mtm, &id),
|
|
568
|
-
"reload" => c.reload(mtm, &id),
|
|
569
|
-
other => Err(format!("bad nav op: {other}")),
|
|
570
|
-
}
|
|
571
|
-
})();
|
|
572
|
-
let _ = tx.send(res);
|
|
573
|
-
})
|
|
574
|
-
.map_err(|e| e.to_string())?;
|
|
575
|
-
rx.recv().map_err(|e| e.to_string())?
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/// Read the controlled webview `id`'s current `{href,title}` as a JSON string (for the address bar).
|
|
579
|
-
pub fn current_url(app: tauri::AppHandle, id: String) -> Result<String, String> {
|
|
580
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
581
|
-
let (tx, rx) = channel();
|
|
582
|
-
win.run_on_main_thread(move || {
|
|
583
|
-
let mtm = match MainThreadMarker::new() {
|
|
584
|
-
Some(m) => m,
|
|
585
|
-
None => { let _ = tx.send(Err("not main thread".to_string())); return; }
|
|
586
|
-
};
|
|
587
|
-
Controller::new().eval_async(
|
|
588
|
-
mtm,
|
|
589
|
-
&id,
|
|
590
|
-
"return JSON.stringify({href:location.href,title:document.title});".to_string(),
|
|
591
|
-
move |r| { let _ = tx.send(r); },
|
|
592
|
-
);
|
|
593
|
-
})
|
|
594
|
-
.map_err(|e| e.to_string())?;
|
|
595
|
-
// Short timeout: this is a 1s address-bar poll; if the webview is mid-teardown the eval never
|
|
596
|
-
// resolves, so cap the wait low rather than blocking on a dying window.
|
|
597
|
-
rx.recv_timeout(Duration::from_millis(1500)).map_err(|e| e.to_string())?
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/// CSP-immune eval of `js` (a function body that `return`s a value) into the controlled webview
|
|
601
|
-
/// `id`. Returns the raw JS return value as a string ("null" for non-string/undefined returns —
|
|
602
|
-
/// callers that need structured data should `return JSON.stringify(...)`). Powers the agent's
|
|
603
|
-
/// read-page / click / type / scroll (all injected JS) over the SAME embedded WKWebView.
|
|
604
|
-
pub fn eval(app: tauri::AppHandle, id: String, js: String) -> Result<String, String> {
|
|
605
|
-
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
606
|
-
let (tx, rx) = channel();
|
|
607
|
-
win.run_on_main_thread(move || {
|
|
608
|
-
let mtm = match MainThreadMarker::new() {
|
|
609
|
-
Some(m) => m,
|
|
610
|
-
None => { let _ = tx.send(Err("not main thread".to_string())); return; }
|
|
611
|
-
};
|
|
612
|
-
Controller::new().eval_async(mtm, &id, js, move |r| { let _ = tx.send(r); });
|
|
613
|
-
})
|
|
614
|
-
.map_err(|e| e.to_string())?;
|
|
615
|
-
// Longer than the address-bar poll: page reads/clicks can await layout/network.
|
|
616
|
-
rx.recv_timeout(Duration::from_secs(10)).map_err(|e| e.to_string())?
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
#[cfg(target_os = "macos")]
|
|
621
|
-
#[tauri::command]
|
|
622
|
-
pub async fn browser_tab_create(
|
|
623
|
-
app: tauri::AppHandle,
|
|
624
|
-
id: String,
|
|
625
|
-
url: String,
|
|
626
|
-
profile_id: String,
|
|
627
|
-
rect: imp::PaneRect,
|
|
628
|
-
) -> Result<String, String> {
|
|
629
|
-
imp::create(app, id.clone(), url, profile_id, rect).map(|_| id)
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
#[cfg(target_os = "macos")]
|
|
633
|
-
#[tauri::command]
|
|
634
|
-
pub async fn browser_tab_set_rect(app: tauri::AppHandle, id: String, rect: imp::PaneRect) -> Result<(), String> {
|
|
635
|
-
imp::set_rect(app, id, rect)
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
#[cfg(target_os = "macos")]
|
|
639
|
-
#[tauri::command]
|
|
640
|
-
pub async fn browser_tab_set_mount(app: tauri::AppHandle, id: String, mode: String) -> Result<(), String> {
|
|
641
|
-
imp::set_mount(app, id, mode)
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
#[cfg(target_os = "macos")]
|
|
645
|
-
#[tauri::command]
|
|
646
|
-
pub async fn browser_tab_navigate(app: tauri::AppHandle, id: String, url: String) -> Result<(), String> {
|
|
647
|
-
imp::navigate(app, id, url)
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
#[cfg(target_os = "macos")]
|
|
651
|
-
#[tauri::command]
|
|
652
|
-
pub async fn browser_tab_close(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
653
|
-
imp::close(app, id)
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
#[cfg(target_os = "macos")]
|
|
657
|
-
#[tauri::command]
|
|
658
|
-
pub async fn browser_subwindow_open(app: tauri::AppHandle, profile_id: Option<String>) -> Result<String, String> {
|
|
659
|
-
imp::open(app, profile_id.unwrap_or_else(|| "default".to_string()))
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
#[cfg(target_os = "macos")]
|
|
663
|
-
#[tauri::command]
|
|
664
|
-
pub async fn browser_subwindow_switch_profile(app: tauri::AppHandle, id: String, profile_id: String) -> Result<(), String> {
|
|
665
|
-
imp::switch_profile(app, id, profile_id)
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
#[cfg(target_os = "macos")]
|
|
669
|
-
#[tauri::command]
|
|
670
|
-
pub async fn browser_tab_back(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
671
|
-
imp::nav_op(app, id, "back")
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
#[cfg(target_os = "macos")]
|
|
675
|
-
#[tauri::command]
|
|
676
|
-
pub async fn browser_tab_forward(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
677
|
-
imp::nav_op(app, id, "forward")
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
#[cfg(target_os = "macos")]
|
|
681
|
-
#[tauri::command]
|
|
682
|
-
pub async fn browser_tab_reload(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
683
|
-
imp::nav_op(app, id, "reload")
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
#[cfg(target_os = "macos")]
|
|
687
|
-
#[tauri::command]
|
|
688
|
-
pub async fn browser_tab_current_url(app: tauri::AppHandle, id: String) -> Result<String, String> {
|
|
689
|
-
imp::current_url(app, id)
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
#[cfg(target_os = "macos")]
|
|
693
|
-
#[tauri::command]
|
|
694
|
-
pub async fn browser_list_profiles() -> Result<Vec<String>, String> {
|
|
695
|
-
Ok(webview_control::list_profiles())
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/// Nudge every open browser sub-window to re-post its `register` beacon (called by the bridge control
|
|
699
|
-
/// channel when a shim (re)connects, so pre-existing sub-windows self-heal into agent-exposure).
|
|
700
|
-
#[cfg(target_os = "macos")]
|
|
701
|
-
pub fn reregister_subwindows(app: &tauri::AppHandle) {
|
|
702
|
-
imp::reregister_all(app);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// ---- Bridge entry points (served-PWA path via `browser.window.*`, distinct from the Tauri commands
|
|
706
|
-
// which only reach app://-served content). Sync, callable from the appwrap bridge worker thread. ----
|
|
707
|
-
|
|
708
|
-
/// Open an app-owned browser sub-window; returns its view id. Used by the `browser.window.open` bridge
|
|
709
|
-
/// method so a server-loaded PWA (no injected `__TAURI__`) can spawn one.
|
|
710
|
-
#[cfg(target_os = "macos")]
|
|
711
|
-
pub fn open_subwindow(app: tauri::AppHandle, profile_id: String) -> Result<String, String> {
|
|
712
|
-
imp::open(app, profile_id)
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/// Switch a sub-window to another profile (creating+persisting the profile on first use).
|
|
716
|
-
#[cfg(target_os = "macos")]
|
|
717
|
-
pub fn switch_subwindow_profile(app: tauri::AppHandle, id: String, profile_id: String) -> Result<(), String> {
|
|
718
|
-
imp::switch_profile(app, id, profile_id)
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// ---- Bridge entry points for EMBEDDED tabs (served-PWA path via `browser.tab.*`). Sync, callable
|
|
722
|
-
// from the appwrap bridge worker thread; each marshals to the main thread itself. `rect` arrives as
|
|
723
|
-
// JSON from the wire (kept as `Value` here so the `PaneRect` shape stays private to `imp`). ----
|
|
724
|
-
|
|
725
|
-
/// Create an embedded browser tab `ChildOf` a front-end-reserved pane; returns its id.
|
|
726
|
-
#[cfg(target_os = "macos")]
|
|
727
|
-
pub fn tab_create(
|
|
728
|
-
app: tauri::AppHandle,
|
|
729
|
-
id: String,
|
|
730
|
-
url: String,
|
|
731
|
-
profile_id: String,
|
|
732
|
-
rect: serde_json::Value,
|
|
733
|
-
) -> Result<String, String> {
|
|
734
|
-
let rect: imp::PaneRect = serde_json::from_value(rect).map_err(|e| format!("bad rect: {e}"))?;
|
|
735
|
-
imp::create(app, id.clone(), url, profile_id, rect).map(|_| id)
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/// Reposition an embedded tab's pane container to a new front-end DOM rect.
|
|
739
|
-
#[cfg(target_os = "macos")]
|
|
740
|
-
pub fn tab_set_rect(app: tauri::AppHandle, id: String, rect: serde_json::Value) -> Result<(), String> {
|
|
741
|
-
let rect: imp::PaneRect = serde_json::from_value(rect).map_err(|e| format!("bad rect: {e}"))?;
|
|
742
|
-
imp::set_rect(app, id, rect)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/// Navigate an embedded tab to `url`.
|
|
746
|
-
#[cfg(target_os = "macos")]
|
|
747
|
-
pub fn tab_navigate(app: tauri::AppHandle, id: String, url: String) -> Result<(), String> {
|
|
748
|
-
imp::navigate(app, id, url)
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/// Tear-off (`toplevel`) / dock (`child`) an embedded tab — same live webview, session intact.
|
|
752
|
-
#[cfg(target_os = "macos")]
|
|
753
|
-
pub fn tab_set_mount(app: tauri::AppHandle, id: String, mode: String) -> Result<(), String> {
|
|
754
|
-
imp::set_mount(app, id, mode)
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/// Close + drop an embedded tab and remove its pane container.
|
|
758
|
-
#[cfg(target_os = "macos")]
|
|
759
|
-
pub fn tab_close(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
760
|
-
imp::close(app, id)
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
/// Tear down every embedded browser pane before a ⌘R app reload. Main-thread only — the ⌘R menu
|
|
764
|
-
/// handler runs on main, so we grab the `MainThreadMarker` inline (no marshalling → no deadlock).
|
|
765
|
-
/// No-op off the main thread. See `imp::close_all_embedded`.
|
|
766
|
-
#[cfg(target_os = "macos")]
|
|
767
|
-
pub fn close_all_embedded() {
|
|
768
|
-
if let Some(mtm) = objc2::MainThreadMarker::new() {
|
|
769
|
-
imp::close_all_embedded(mtm);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/// Route a history/reload op (`back`/`forward`/`reload`) to an embedded tab's controlled webview.
|
|
774
|
-
#[cfg(target_os = "macos")]
|
|
775
|
-
pub fn tab_nav(app: tauri::AppHandle, id: String, op: &'static str) -> Result<(), String> {
|
|
776
|
-
imp::nav_op(app, id, op)
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/// Read an embedded tab's current `{href,title}` (JSON string) — for polling the address bar so it
|
|
780
|
-
/// tracks in-page navigation (link clicks / redirects), not just user-typed URLs.
|
|
781
|
-
#[cfg(target_os = "macos")]
|
|
782
|
-
pub fn tab_current_url(app: tauri::AppHandle, id: String) -> Result<String, String> {
|
|
783
|
-
imp::current_url(app, id)
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
/// Eval `js` (a function body) into an embedded tab's controlled webview; returns the raw string
|
|
787
|
-
/// result. Backs the `browser.tab.eval` bridge method so the side-panel agent can read page content
|
|
788
|
-
/// (innerText) and drive clicks/typing/scroll over the SAME embedded WKWebView.
|
|
789
|
-
#[cfg(target_os = "macos")]
|
|
790
|
-
pub fn tab_eval(app: tauri::AppHandle, id: String, js: String) -> Result<String, String> {
|
|
791
|
-
imp::eval(app, id, js)
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
/// Re-open the sub-windows that were open at last quit. Called once on app startup.
|
|
795
|
-
#[cfg(target_os = "macos")]
|
|
796
|
-
pub fn restore_subwindows(app: tauri::AppHandle) {
|
|
797
|
-
imp::restore(app);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/// Mark the app as quitting so per-window teardown keeps (not prunes) the restore snapshot.
|
|
801
|
-
#[cfg(target_os = "macos")]
|
|
802
|
-
pub fn mark_quitting() {
|
|
803
|
-
imp::APP_QUITTING.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
804
|
-
}
|