v0.0.14 - Fix tab URL tracking: use main-frame URI instead of decide-policy, sync URL bar on navigation

This commit is contained in:
Laan Tungir
2026-10-01 15:37:25 -04:00
parent b4f519d8ed
commit 24325815da
7 changed files with 137 additions and 28 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ members = [
[package]
name = "sovereign_browser"
version = "0.0.13"
version = "0.0.14"
edition = "2021"
license = "MIT"
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
+1 -1
View File
@@ -1 +1 @@
0.0.13
0.0.14
+5 -1
View File
@@ -111,8 +111,12 @@ fn handle_sovereign_scheme(request: &URISchemeRequest) {
}
// ── Processes REST API ─────────────────────────────────────────
// Pass `path` (with its query string): probe-report reads
// tab_index/body and tab_probe/tab_action read index/action from it.
// Passing `route` dropped every parameter, so no probe report was ever
// recorded and the Tabs view stayed blank.
if route.starts_with("processes/") {
handle_processes_api(request, &route[10..]);
handle_processes_api(request, &path[10..]);
return;
}
+39 -3
View File
@@ -229,15 +229,19 @@ fn add_scrollbar_visibility_fix(manager: &UserContentManager) {
manager.add_style_sheet(&sheet);
}
/// Perf probe preamble — skips sovereign:// and about: pages, sets tab index.
/// Perf probe preamble — skips sovereign:// pages, sets tab index.
/// The tab index is baked in at injection time as a literal.
///
/// Only `sovereign:` is skipped. The previous check also skipped `about:`,
/// but at document-start the location of a freshly created top-level
/// document can still be `about:blank`, which silently disabled the probe
/// for every real page.
fn perf_probe_preamble(tab_index: i32) -> String {
format!(
r#"(function(){{
'use strict';
try {{
var u = location.href || '';
if (u.indexOf('sovereign://') === 0 || u.indexOf('about:') === 0) {{
if (location.protocol === 'sovereign:') {{
window.__sbPerfProbeSkip = true;
}} else {{
window.__sbPerfProbeSkip = false;
@@ -250,6 +254,28 @@ fn perf_probe_preamble(tab_index: i32) -> String {
)
}
/// Isolated-world relay for perf probe reports. The probe (main world)
/// dispatches a `__sb_probe_report` DOM event carrying the JSON payload as a
/// string; this relay performs the synchronous XHR to
/// `sovereign://processes/probe-report`. Doing the XHR from an isolated world
/// exempts it from the page's Content-Security-Policy (connect-src), which
/// otherwise blocks the request on many sites.
const PERF_PROBE_RELAY_JS: &str = r#"
(function() {
'use strict';
document.addEventListener('__sb_probe_report', function(ev) {
try {
var d = JSON.parse(ev.detail);
var url = 'sovereign://processes/probe-report?tab_index=' + d.tab_index +
'&body=' + encodeURIComponent(ev.detail);
var x = new XMLHttpRequest();
x.open('GET', url, false);
x.send();
} catch (e) { /* probe must never break the page */ }
});
})();
"#;
/// The perf-probe.js script (embedded from www/js/perf-probe.js).
const PERF_PROBE_JS: &str = include_str!("../www/js/perf-probe.js");
@@ -274,6 +300,16 @@ pub fn nostr_inject_create_content_manager_with_probe(tab_index: i32) -> UserCon
// Top frame only — mirrors the C version (perf_probe.c:67). Injecting
// into all frames would run a separate probe instance (with its own
// rAF loop and 1s sync XHR) in every iframe.
let probe_relay = UserScript::for_world(
PERF_PROBE_RELAY_JS,
UserContentInjectedFrames::TopFrame,
UserScriptInjectionTime::Start,
BRIDGE_WORLD,
&[],
&[],
);
manager.add_script(&probe_relay);
let preamble = UserScript::new(
&perf_probe_preamble(tab_index),
UserContentInjectedFrames::TopFrame,
+45 -7
View File
@@ -841,11 +841,13 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
return true;
}
// Update tab URL in state for non-rewritten URLs.
let mut state = G_TAB_STATE.lock().unwrap();
if let Some(t) = state.tabs.iter_mut().find(|t| t.id == tab_id_clone2) {
t.url = uri_str;
}
// NOTE: the tab's URL in state is intentionally NOT
// updated here. decide-policy also fires for
// sub-frame/iframe navigations (e.g. a PayPal
// button) and for not-yet-committed requests, which
// would clobber the real page URL. The main-frame
// URI is tracked via `notify::uri` below.
let _ = tab_id_clone2;
}
}
}
@@ -853,6 +855,31 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
true
});
// Track the main-frame URI: keep the tab state and the URL entry in
// sync with what the webview is actually showing (covers link clicks,
// redirects, history navigation, and Recents/Bookmarks menu loads).
let tab_id_uri = tab_id;
let url_entry_uri = url_entry.clone();
webview.connect_uri_notify(move |wv| {
let Some(uri) = wv.uri() else { return };
let uri_str = uri.to_string();
if uri_str.is_empty() {
return;
}
{
let mut state = G_TAB_STATE.lock().unwrap();
if let Some(t) = state.tabs.iter_mut().find(|t| t.id == tab_id_uri) {
t.url = uri_str.clone();
}
}
if uri_str.starts_with("about:blank") {
url_entry_uri.set_text("");
} else if url_entry_uri.text().as_str() != uri_str {
url_entry_uri.set_text(&uri_str);
url_entry_uri.set_position(-1);
}
});
// Webview context menu — add "Open Link in New Tab" when right-clicking
// a link, and "Open Page in New Tab" when right-clicking the page
// background. Mirrors the C version (tab_manager.c:1912).
@@ -1669,8 +1696,13 @@ pub fn tab_manager_reload_tab(tab_id: i32) {
/// Duplicate a tab by ID (opens the same URL in a new tab in the same window).
pub fn tab_manager_duplicate(tab_id: i32) {
let state = G_TAB_STATE.lock().unwrap();
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
let state_url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
drop(state);
// Prefer the webview's live main-frame URI over the stored one.
let url = tab_webview(tab_id)
.and_then(|wv| wv.uri().map(|u| u.to_string()))
.filter(|u| !u.is_empty())
.or(state_url);
// Duplicate into the tab's own notebook (its window).
let nb = match tab_notebook(tab_id) {
Some(n) => n,
@@ -1692,7 +1724,13 @@ pub fn tab_manager_open_in_new_window(tab_id: i32) {
// Capture the URL and the tab's own notebook before moving.
let (url, nb) = {
let state = G_TAB_STATE.lock().unwrap();
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
let state_url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
drop(state);
// Prefer the webview's live main-frame URI over the stored one.
let url = tab_webview(tab_id)
.and_then(|wv| wv.uri().map(|u| u.to_string()))
.filter(|u| !u.is_empty())
.or(state_url);
(url, tab_notebook(tab_id))
};
let nb = match nb {
+2 -2
View File
@@ -1,7 +1,7 @@
//! Version information for sovereign_browser
/// The current version of sovereign_browser (with leading 'v').
pub const VERSION: &str = "v0.0.13";
pub const VERSION: &str = "v0.0.14";
/// Major version number.
pub const VERSION_MAJOR: u32 = 0;
@@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0;
pub const VERSION_MINOR: u32 = 0;
/// Patch version number.
pub const VERSION_PATCH: u32 = 13;
pub const VERSION_PATCH: u32 = 14;
+44 -13
View File
@@ -125,6 +125,8 @@
try {
var resObs = new PerformanceObserver(function (list) {
list.getEntries().forEach(function (e) {
// Ignore the probe's own reports and other browser-internal calls.
if (e.name.indexOf('sovereign://') === 0) return;
netRequestsLastSec.push({ name: e.name, type: e.initiatorType });
var key = e.name.split('?')[0];
netEndpoints[key] = (netEndpoints[key] || 0) + 1;
@@ -135,13 +137,42 @@
}
// ── FPS via requestAnimationFrame ───────────────────────────────
// The rAF loop is the probe's only continuous main-thread work. It is
// paused whenever the document is hidden (background tab / minimised
// window) so a backgrounded tab can never keep the compositor and font
// machinery busy. It is also guarded so callbacks can't stack.
var rafFrames = 0;
var fps = 0;
function rafLoop(ts) {
var rafScheduled = false;
var rafActive = false;
function rafLoop() {
rafScheduled = false;
if (!rafActive) return;
rafFrames++;
scheduleRaf();
}
function scheduleRaf() {
if (rafScheduled || !rafActive) return;
rafScheduled = true;
requestAnimationFrame(rafLoop);
}
requestAnimationFrame(rafLoop);
function setRafActive(on) {
if (on === rafActive) return;
rafActive = on;
if (on) {
scheduleRaf();
} else {
rafFrames = 0;
fps = 0;
}
}
rafActive = !document.hidden;
if (rafActive) scheduleRaf();
var onVisibility = function () { setRafActive(!document.hidden); };
onVisibility.__sbInternal = true; // don't count our own listener
document.addEventListener('visibilitychange', onVisibility);
// ── Timer tracking (patched setTimeout/setInterval) ─────────────
var activeTimers = {}; // id -> {code, interval_ms, count}
@@ -344,21 +375,21 @@
netEndpoints = {};
timerTop = {};
// Send. Use sync XHR to sovereign:// (same convention as the
// window.nostr shim — WebKit's custom scheme handler doesn't
// reliably support fetch() or async XHR for sovereign:// URLs).
// Send. The actual sync XHR to sovereign:// runs in an isolated
// script world (see PERF_PROBE_RELAY_JS in nostr_inject.rs) so that
// pages with a strict CSP can't block it. We hand the payload over as
// a JSON string in a DOM CustomEvent (objects don't cross worlds).
try {
var body = encodeURIComponent(JSON.stringify(payload));
var url = 'sovereign://processes/probe-report?tab_index=' + TAB_INDEX +
'&body=' + body;
var x = new XMLHttpRequest();
x.open('GET', url, false); // synchronous
x.send();
document.dispatchEvent(new CustomEvent('__sb_probe_report', {
detail: JSON.stringify(payload)
}));
} catch (e) { /* swallow — probe must never break the page */ }
}
// FPS counter: count frames in each 1s window.
setInterval(function () {
// FPS counter: count frames in each 1s window. Use the original
// setInterval so the probe doesn't count itself as an active timer.
origSetInterval.call(window, function () {
if (!rafActive) return;
fps = rafFrames;
rafFrames = 0;
}, 1000);