diff --git a/VERSION b/VERSION
index a758e3a..9ebecc7 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.0.57
+0.0.58
diff --git a/src/agent_mcp.c b/src/agent_mcp.c
index d49a15d..0b3db52 100644
--- a/src/agent_mcp.c
+++ b/src/agent_mcp.c
@@ -230,8 +230,8 @@ const mcp_tool_def_t tool_defs[] = {
"{\"type\":\"object\",\"properties\":{\"ref\":{\"type\":\"string\"},\"selector\":{\"type\":\"string\"}}}"},
{"click_at",
- "Click at explicit viewport coordinates (x, y) via GDK event synthesis. Bypasses selector resolution — useful when the target point is known from a screenshot or get_box result. Triggers real WebKit hit-testing and full event propagation.",
- "{\"type\":\"object\",\"properties\":{\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"}},\"required\":[\"x\",\"y\"]}"},
+ "Click at explicit viewport coordinates (x, y) via GDK event synthesis. Bypasses selector resolution — useful when the target point is known from a screenshot or get_box result. Triggers real WebKit hit-testing and full event propagation. Optional 'button': 1=left (default), 2=middle, 3=right (opens the native context menu).",
+ "{\"type\":\"object\",\"properties\":{\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"button\":{\"type\":\"integer\",\"enum\":[1,2,3],\"default\":1}},\"required\":[\"x\",\"y\"]}"},
{"fill",
"Clear an input and fill it with a value.",
diff --git a/src/agent_tools.c b/src/agent_tools.c
index 74cd1e3..fc5ba7a 100644
--- a/src/agent_tools.c
+++ b/src/agent_tools.c
@@ -130,9 +130,12 @@ static WebKitWebView *get_active_webview(void) {
*
* Coordinates from getBoundingClientRect() are relative to the viewport
* and match the webview's GdkWindow coordinate space when the webview
- * fills its parent window. Returns TRUE on success. */
-static gboolean synthesize_click_at(WebKitWebView *wv, double x, double y) {
+ * fills its parent window. `button` is 1 (left), 2 (middle), or 3 (right).
+ * Returns TRUE on success. */
+static gboolean synthesize_click_at(WebKitWebView *wv, double x, double y,
+ int button) {
if (wv == NULL) return FALSE;
+ if (button < 1) button = 1;
GtkWidget *widget = GTK_WIDGET(wv);
GdkWindow *window = gtk_widget_get_window(widget);
@@ -150,7 +153,7 @@ static gboolean synthesize_click_at(WebKitWebView *wv, double x, double y) {
press->button.y = y;
press->button.axes = NULL;
press->button.state = 0; /* no modifiers */
- press->button.button = 1; /* left button */
+ press->button.button = button;
press->button.device = gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_window_get_display(window)));
gdk_event_put(press);
gdk_event_free(press);
@@ -170,7 +173,7 @@ static gboolean synthesize_click_at(WebKitWebView *wv, double x, double y) {
release->button.y = y;
release->button.axes = NULL;
release->button.state = 0;
- release->button.button = 1;
+ release->button.button = button;
release->button.device = gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_window_get_display(window)));
gdk_event_put(release);
gdk_event_free(release);
@@ -1166,7 +1169,7 @@ static cJSON *tool_click(cJSON *params) {
double cy = jy->valuedouble + jh->valuedouble / 2.0;
cJSON_Delete(box);
g_free(sel);
- if (synthesize_click_at(wv, cx, cy)) {
+ if (synthesize_click_at(wv, cx, cy, 1)) {
return make_success(NULL);
}
/* Fall through to JS fallback if GDK synthesis failed. */
@@ -1210,7 +1213,17 @@ static cJSON *tool_click(cJSON *params) {
/* click_at — Click at explicit viewport coordinates via GDK event
* synthesis. Useful when the agent already knows the target point
* (e.g. from a screenshot or get_box result) and wants to bypass
- * selector resolution entirely. */
+ * selector resolution entirely. Optional 'button': 1=left (default),
+ * 2=middle, 3=right (triggers the native context menu). */
+static int click_button_from_params(cJSON *params) {
+ cJSON *jb = cJSON_GetObjectItem(params, "button");
+ if (jb && cJSON_IsNumber(jb)) {
+ int b = jb->valueint;
+ if (b >= 1 && b <= 3) return b;
+ }
+ return 1;
+}
+
static cJSON *tool_click_at(cJSON *params) {
cJSON *jx = cJSON_GetObjectItem(params, "x");
cJSON *jy = cJSON_GetObjectItem(params, "y");
@@ -1223,8 +1236,9 @@ static cJSON *tool_click_at(cJSON *params) {
double x = jx->valuedouble;
double y = jy->valuedouble;
+ int button = click_button_from_params(params);
- if (!synthesize_click_at(wv, x, y)) {
+ if (!synthesize_click_at(wv, x, y, button)) {
return make_error("CLICK_FAILED", "Failed to synthesize GDK click event");
}
return make_success(NULL);
@@ -4581,7 +4595,7 @@ cJSON *agent_tools_dispatch(cJSON *request, SoupWebsocketConnection *conn) {
double cy = jy->valuedouble + jh->valuedouble / 2.0;
cJSON_Delete(box);
g_free(box_result);
- if (synthesize_click_at(wv, cx, cy)) {
+ if (synthesize_click_at(wv, cx, cy, 1)) {
g_free(sel);
/* Send a success response directly through the
* WebSocket connection, matching the async path. */
@@ -4619,7 +4633,9 @@ cJSON *agent_tools_dispatch(cJSON *request, SoupWebsocketConnection *conn) {
/* click_at — coordinate-based click via GDK event synthesis (sync).
* Takes explicit x/y viewport coordinates and dispatches a real
- * GDK button press/release. Bypasses selector resolution entirely. */
+ * GDK button press/release. Optional 'button': 1=left (default),
+ * 2=middle, 3=right (triggers the native context menu). Bypasses
+ * selector resolution entirely. */
if (conn != NULL && strcmp(tool_name, "click_at") == 0) {
cJSON *jx = cJSON_GetObjectItem(params, "x");
cJSON *jy = cJSON_GetObjectItem(params, "y");
@@ -4630,7 +4646,8 @@ cJSON *agent_tools_dispatch(cJSON *request, SoupWebsocketConnection *conn) {
if (wv == NULL) return make_error("NO_TAB", "No active tab");
double x = jx->valuedouble;
double y = jy->valuedouble;
- if (!synthesize_click_at(wv, x, y)) {
+ int button = click_button_from_params(params);
+ if (!synthesize_click_at(wv, x, y, button)) {
return make_error("CLICK_FAILED", "Failed to synthesize GDK click event");
}
/* Send success response directly through the WebSocket. */
diff --git a/src/bookmarks.c b/src/bookmarks.c
index 46b9bfc..1ff37de 100644
--- a/src/bookmarks.c
+++ b/src/bookmarks.c
@@ -8,12 +8,13 @@
* - d tag = HMAC-SHA256(hmac_key, path) (deterministic, opaque, 64 hex)
* - content = NIP-44 encrypted JSON {"path": ..., "bookmarks": [...]}
* - hmac_key = HMAC-SHA256(privkey, BOOKMARKS_HMAC_KEY_LABEL)
- *
* On startup, the relay fetch retrieves kind 30003 events, which are
* decrypted and loaded into an in-memory trie. Legacy events with plaintext
- * d tags (from the previous flat-directory implementation) are detected,
- * migrated to the new HMAC-d format, and their old events are marked for
- * kind 5 deletion.
+ * d tags (from the previous flat-directory implementation) are IGNORED on
+ * load — they are neither migrated nor re-published. This project is
+ * pre-public and the only user has already re-published every folder with
+ * HMAC d tags, so legacy events are dead data. Skipping them avoids the
+ * per-boot migration/publish storm that earlier versions triggered.
*/
#include "bookmarks.h"
@@ -541,6 +542,8 @@ int bookmarks_init(nostr_signer_t *signer, const char *pubkey_hex) {
if (events != NULL) {
int n = cJSON_GetArraySize(events);
g_print("[bookmarks] Loading %d cached folder event(s)\n", n);
+ int legacy_skipped = 0;
+ int loaded = 0;
cJSON *event;
cJSON_ArrayForEach(event, events) {
/* Get the d tag. */
@@ -560,6 +563,25 @@ int bookmarks_init(nostr_signer_t *signer, const char *pubkey_hex) {
}
}
+ /* Ignore legacy (non-HMAC) bookmark events entirely.
+ * Legacy events used the plaintext folder path as the d tag;
+ * the current format uses an opaque HMAC-SHA256 d tag (see
+ * path_to_d_tag). This project is pre-public and the only
+ * user has already re-published every folder with HMAC d
+ * tags, so legacy events are dead data. Skipping them here
+ * (a) avoids the per-boot migration/publish storm that
+ * re-encrypted and re-published every folder on every launch,
+ * and (b) keeps the local SQLite cache from re-triggering
+ * migration via the duplicate legacy rows that earlier
+ * migrations left behind (publish_deletion_for_d stored a
+ * kind 5 tombstone but never deleted the old kind 30003
+ * row). Legacy events are simply not loaded; they are not
+ * deleted from the db or from relays here. */
+ if (!is_hmac_d_tag(d_value)) {
+ legacy_skipped++;
+ continue;
+ }
+
/* Decrypt content. */
cJSON *content = cJSON_GetObjectItemCaseSensitive(event, "content");
if (!cJSON_IsString(content) || content->valuestring[0] == '\0')
@@ -568,22 +590,14 @@ int bookmarks_init(nostr_signer_t *signer, const char *pubkey_hex) {
char *plaintext = decrypt_content(content->valuestring);
if (plaintext == NULL) continue;
- /* Determine the path. */
+ /* New format: path is inside the encrypted content. */
char *path_from_content = NULL;
bookmark_node_t scratch = {0};
scratch.name = g_strdup("");
scratch.path = g_strdup("");
node_load_from_content(&scratch, plaintext, &path_from_content);
- const char *path = NULL;
- if (is_hmac_d_tag(d_value)) {
- /* New format: path is inside the encrypted content. */
- path = path_from_content;
- } else {
- /* Legacy: d tag is the plaintext path. */
- path = d_value;
- }
-
+ const char *path = path_from_content;
if (path == NULL || path[0] == '\0') path = "General";
bookmark_node_t *leaf = node_ensure_path(&g_root, path, NULL);
@@ -595,21 +609,7 @@ int bookmarks_init(nostr_signer_t *signer, const char *pubkey_hex) {
scratch.bookmarks = NULL;
scratch.bookmark_count = 0;
}
-
- /* Migration: re-publish with the current single-step HMAC d
- * tag and delete the old event if the d tag doesn't match.
- * This covers both legacy plaintext d tags AND old two-step
- * HMAC d tags (from before the single-step switch). */
- if (g_have_signer && path != NULL && path[0] != '\0') {
- char *new_d = path_to_d_tag(path);
- if (new_d != NULL && strcmp(new_d, d_value) != 0) {
- g_print("[bookmarks] Migrating folder '%s' to current HMAC d tag\n",
- path);
- if (leaf) publish_node(leaf);
- publish_deletion_for_d(d_value);
- }
- g_free(new_d);
- }
+ loaded++;
g_free(path_from_content);
/* node_free frees scratch.name, scratch.path, and any
@@ -618,6 +618,11 @@ int bookmarks_init(nostr_signer_t *signer, const char *pubkey_hex) {
free(plaintext);
}
cJSON_Delete(events);
+ if (legacy_skipped > 0) {
+ g_print("[bookmarks] Skipped %d legacy (non-HMAC) folder event(s)\n",
+ legacy_skipped);
+ }
+ g_print("[bookmarks] Loaded %d HMAC folder event(s)\n", loaded);
}
}
@@ -1055,6 +1060,16 @@ int bookmarks_store_and_load_event(const void *event_cjson) {
}
}
+ /* Ignore legacy (non-HMAC) bookmark events. See the matching comment in
+ * bookmarks_init() for the full rationale. The event has already been
+ * stored in SQLite by the caller (relay_fetch); we simply do not load
+ * it into the in-memory tree and do not migrate/re-publish it. This
+ * breaks the per-boot migration storm that was triggered every time the
+ * relay fetch re-served a legacy event. */
+ if (!is_hmac_d_tag(d_value)) {
+ return 0;
+ }
+
cJSON *content = cJSON_GetObjectItemCaseSensitive(event, "content");
if (!cJSON_IsString(content) || content->valuestring[0] == '\0')
return 0;
@@ -1068,7 +1083,7 @@ int bookmarks_store_and_load_event(const void *event_cjson) {
scratch.path = g_strdup("");
node_load_from_content(&scratch, plaintext, &path_from_content);
- const char *path = is_hmac_d_tag(d_value) ? path_from_content : d_value;
+ const char *path = path_from_content;
if (path == NULL || path[0] == '\0') path = "General";
bookmark_node_t *leaf = node_ensure_path(&g_root, path, NULL);
@@ -1080,18 +1095,6 @@ int bookmarks_store_and_load_event(const void *event_cjson) {
scratch.bookmark_count = 0;
}
- /* Migration: re-publish with the current single-step HMAC d tag and
- * delete the old event if the d tag doesn't match. Covers both legacy
- * plaintext d tags and old two-step HMAC d tags. */
- if (g_have_signer && path != NULL && path[0] != '\0') {
- char *new_d = path_to_d_tag(path);
- if (new_d != NULL && strcmp(new_d, d_value) != 0) {
- if (leaf) publish_node(leaf);
- publish_deletion_for_d(d_value);
- }
- g_free(new_d);
- }
-
g_free(path_from_content);
/* node_free frees scratch.name, scratch.path, and any remaining
* bookmarks (already moved to leaf, so NULL). */
diff --git a/src/settings.c b/src/settings.c
index 7468266..86b13c0 100644
--- a/src/settings.c
+++ b/src/settings.c
@@ -129,6 +129,11 @@ static void settings_set_defaults(browser_settings_t *s) {
s->offline_site_max_pages = SETTINGS_OFFLINE_SITE_MAX_PAGES_DEFAULT;
s->offline_site_request_delay_ms = SETTINGS_OFFLINE_SITE_REQUEST_DELAY_MS_DEFAULT;
+
+ /* Perf probe is OFF by default — its instrumentation overhead
+ * (patched addEventListener/setTimeout/setInterval/XHR + a permanent
+ * rAF loop) breaks scrolling on listener-heavy pages. */
+ s->perf_probe_enabled = FALSE;
}
/* ── Parsing helpers ──────────────────────────────────────────────── */
@@ -331,6 +336,9 @@ void settings_load_user(void) {
if (g_settings.offline_site_request_delay_ms < 0) g_settings.offline_site_request_delay_ms = 0;
}
+ val = db_kv_get("perf_probe.enabled");
+ if (val) g_settings.perf_probe_enabled = parse_bool(val, g_settings.perf_probe_enabled);
+
/* Agent LLM provider settings — these are the "resolved" values
* (active provider's base_url + api_key + selected model). They are
* populated from db_kv here for local persistence, and overwritten
@@ -502,6 +510,8 @@ void settings_save_user(void) {
snprintf(buf, sizeof(buf), "%d", g_settings.offline_site_request_delay_ms);
db_kv_set("offline_site.request_delay_ms", buf);
+ db_kv_set("perf_probe.enabled", g_settings.perf_probe_enabled ? "true" : "false");
+
/* Agent LLM provider settings — resolved values (active provider). */
db_kv_set("agent.llm_base_url", g_settings.agent_llm_base_url);
db_kv_set("agent.llm_api_key", g_settings.agent_llm_api_key);
diff --git a/src/settings.h b/src/settings.h
index 44053a9..cd2ad05 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -127,6 +127,17 @@ typedef struct {
/* Offline website downloader (right-click tab → Download Website). */
int offline_site_max_pages; /* safety cap on pages crawled */
int offline_site_request_delay_ms; /* politeness delay between requests */
+
+ /* Per-tab performance probe (sovereign://processes). The probe patches
+ * addEventListener / setTimeout / setInterval / XHR and runs a permanent
+ * requestAnimationFrame loop on every page, which adds main-thread
+ * overhead. On listener-heavy pages (e.g. NIP-17 DM clients that
+ * re-register many listeners per re-render) this overhead is enough to
+ * starve the main thread and break scrolling — the page's own
+ * scroll-to-bottom rAF wins the race and yanks the view to the bottom.
+ * Default OFF so normal browsing is not penalized; enable explicitly when
+ * you want the processes/perf view. */
+ gboolean perf_probe_enabled;
} browser_settings_t;
/*
diff --git a/src/site_downloader.c b/src/site_downloader.c
index 45f16e2..f5dae91 100644
--- a/src/site_downloader.c
+++ b/src/site_downloader.c
@@ -220,7 +220,12 @@ static const char *DL_RENDER_JS =
" });"
" el.setAttribute('srcset', out.join(', '));"
" });"
- " /* Handle inline style url() and CSS. */"
+ " /* Build the offline HTML from the rendered DOM. We remove all"
+ " * . */
+ for (const char *q = p; *q; q++) {
+ if (g_ascii_strncasecmp(q, "", 9) == 0) {
+ end = q;
+ break;
+ }
+ }
+ if (end) {
+ p = end + 9; /* skip */
+ continue;
+ } else {
+ /* Unterminated script — skip to end. */
+ break;
+ }
+ }
+ g_string_append_c(stripped, *p);
+ p++;
+ }
+ write_text_to_file(path, stripped->str,
+ stripped->len);
+ g_string_free(stripped, TRUE);
+ }
g_free(rendered_html);
}
if (rendered_links) {
diff --git a/src/tab_manager.c b/src/tab_manager.c
index 0aebb83..de20b48 100644
--- a/src/tab_manager.c
+++ b/src/tab_manager.c
@@ -137,9 +137,20 @@ static GtkWidget *g_target_notebook = NULL;
* When non-NULL, tab_create() creates the webview with
* webkit_web_view_new_with_related_view() so it shares the parent's
* WebProcess and window features (avoids the WindowFeatures assertion
- * crash). Used by tab_manager_new_window(). */
+ * crash). Used by tab_manager_new_window() and on_create_webview(). */
static WebKitWebView *g_target_related_view = NULL;
+/* When TRUE, tab_create() skips the explicit webkit_web_view_load_uri()
+ * call. This is only needed in the "create" signal path (on_create_webview),
+ * where WebKit already has a pending NavigationAction with WindowFeatures
+ * that it will apply to the returned webview itself — an explicit load
+ * here would race with WebKit's own navigation setup and trigger the
+ * std::optional assertion crash.
+ *
+ * tab_manager_new_window() does NOT set this: it creates a fresh webview
+ * with no pending WebKit navigation, so it MUST load the URL explicitly. */
+static gboolean g_target_skip_load = FALSE;
+
/* Dynamic array of tab_info_t pointers, indexed by notebook page number. */
static tab_info_t **g_tabs = NULL;
static int g_tab_count = 0;
@@ -1150,9 +1161,17 @@ static GtkWidget *on_create_webview(WebKitWebView *webview,
if (uri && uri[0]) {
/* Set the related view so tab_create() uses
* webkit_web_view_new_with_related_view() — shares the parent's
- * WebProcess and avoids the WindowFeatures assertion crash. */
+ * WebProcess and avoids the WindowFeatures assertion crash. Also
+ * set g_target_skip_load: WebKit already has a pending
+ * NavigationAction with WindowFeatures that it will apply to the
+ * returned webview itself, so tab_create() must NOT call
+ * webkit_web_view_load_uri() (that would race with WebKit's own
+ * navigation setup and trigger the std::optional
+ * assertion crash). */
g_target_related_view = webview;
+ g_target_skip_load = TRUE;
int idx = tab_manager_new_tab(uri);
+ g_target_skip_load = FALSE;
g_target_related_view = NULL;
if (idx >= 0) {
@@ -1308,8 +1327,14 @@ static GtkWidget *tab_manager_new_window(const char *url,
}
/* Inject the per-tab performance probe (sovereign://processes).
- * Skips internal sovereign:// pages at runtime via the preamble. */
- perf_probe_setup(tab->webview, index);
+ * Skips internal sovereign:// pages at runtime via the preamble.
+ * Gated on the perf_probe_enabled setting: the probe patches
+ * addEventListener/setTimeout/setInterval/XHR and runs a permanent
+ * rAF loop, which adds enough main-thread overhead to break
+ * scrolling on listener-heavy pages. Default OFF. */
+ if (settings_get()->perf_probe_enabled) {
+ perf_probe_setup(tab->webview, index);
+ }
const browser_settings_t *s = settings_get();
int page_num = gtk_notebook_append_page(GTK_NOTEBOOK(notebook),
@@ -1759,6 +1784,14 @@ static void on_title_changed(WebKitWebView *webview,
/* ── Web view context menu (right-click) ──────────────────────────── */
+/* Context passed to on_context_open_in_new_window(): the link URL and the
+ * webview that was right-clicked (used as the related view so the new
+ * window shares the parent's WebProcess). */
+struct new_window_ctx {
+ char *url;
+ WebKitWebView *related_view;
+};
+
/* Callback for "Open Link in New Tab" / "Open Page in New Tab". */
static void on_context_open_in_new_tab(GSimpleAction *action,
GVariant *parameter,
@@ -1772,6 +1805,47 @@ static void on_context_open_in_new_tab(GSimpleAction *action,
g_free(url);
}
+/* Idle callback that actually creates the new window. We defer the
+ * tab_manager_new_window() call out of the GAction "activate" handler
+ * because that runs inside the GtkMenu's signal emission / GSource
+ * teardown. Creating a new top-level window (which calls
+ * gtk_widget_show_all + gtk_window_present and thus processes pending
+ * events) from within that handler caused GLib-CRITICAL
+ * g_source_ref/g_source_unref assertion failures as the menu's own
+ * GSources were being released concurrently. Running on the next idle
+ * tick lets the menu fully close first. */
+static gboolean on_idle_open_in_new_window(gpointer user_data) {
+ struct new_window_ctx *ctx = (struct new_window_ctx *)user_data;
+ if (ctx && ctx->url && ctx->url[0]) {
+ tab_manager_new_window(ctx->url, ctx->related_view);
+ }
+ if (ctx) {
+ g_free(ctx->url);
+ g_slice_free(struct new_window_ctx, ctx);
+ }
+ return G_SOURCE_REMOVE; /* run once */
+}
+
+/* Callback for "Open Link in New Window". Opens the link URL in a brand
+ * new top-level GtkWindow (via tab_manager_new_window) instead of a new
+ * tab. The related view (the webview that was right-clicked) is passed so
+ * the new window shares the parent's WebProcess. The actual window
+ * creation is deferred to an idle handler (see on_idle_open_in_new_window)
+ * to avoid GLib-CRITICAL source-refcount warnings from creating the
+ * window inside the menu's activate callback. */
+static void on_context_open_in_new_window(GSimpleAction *action,
+ GVariant *parameter,
+ gpointer user_data) {
+ (void)action;
+ (void)parameter;
+ /* Steal the ctx and defer to idle — do NOT free it here; the idle
+ * callback owns and frees it. */
+ struct new_window_ctx *ctx = (struct new_window_ctx *)user_data;
+ if (ctx) {
+ g_idle_add(on_idle_open_in_new_window, ctx);
+ }
+}
+
/* Context menu signal handler for the webview. Appends "Open in New Tab"
* to the default WebKit context menu — either "Open Link in New Tab" if
* right-clicking a link, or "Open Page in New Tab" if right-clicking the
@@ -1821,21 +1895,63 @@ static gboolean on_webview_context_menu(WebKitWebView *webview,
NULL);
/* For links: WebKit's default menu starts with "Open Link" (pos 0)
- * then "Open Link in New Window" (pos 1). Insert "Open Link in New
- * Tab" at position 1 — between "Open Link" and "Open Link in New
- * Window" — so the order is:
+ * then "Open Link in New Window" (pos 1). We replace that default
+ * "Open Link in New Window" item with our own that actually opens a
+ * real top-level GtkWindow (WebKit's default fires the "create"
+ * signal, which our on_create_webview handler routes to a new *tab*,
+ * so without this replacement "Open in New Window" would wrongly open
+ * a tab). We then insert "Open Link in New Tab" at position 1, so the
+ * final order is:
* Open Link
* Open Link in New Tab
- * Open Link in New Window
+ * Open Link in New Window (ours — real new window)
*
- * For page background: insert at position 0 (top). */
- int insert_pos = is_link ? 1 : 0;
+ * For page background: insert the "Open Page in New Tab" item at
+ * position 0 (top). */
+ int insert_pos;
+ if (is_link) {
+ /* Find and remove WebKit's stock "Open Link in New Window" item
+ * so we can replace it with our own below. */
+ guint n = webkit_context_menu_get_n_items(context_menu);
+ for (guint i = 0; i < n; i++) {
+ WebKitContextMenuItem *it =
+ webkit_context_menu_get_item_at_position(context_menu, i);
+ if (it == NULL) continue;
+ if (webkit_context_menu_item_get_stock_action(it) ==
+ WEBKIT_CONTEXT_MENU_ACTION_OPEN_LINK_IN_NEW_WINDOW) {
+ webkit_context_menu_remove(context_menu, it);
+ break;
+ }
+ }
+ insert_pos = 1;
+ } else {
+ insert_pos = 0;
+ }
webkit_context_menu_insert(context_menu, item, insert_pos);
/* The action is owned by the menu item now. g_object_unref is safe
* because the WebKitContextMenuItem holds its own ref. */
g_object_unref(action);
+ /* For links: also add our own "Open Link in New Window" item that
+ * opens a real top-level GtkWindow. Insert it right after the
+ * "Open Link in New Tab" item we just added (position 2). */
+ if (is_link) {
+ GSimpleAction *win_action =
+ g_simple_action_new("open-in-new-window", NULL);
+ struct new_window_ctx *ctx = g_slice_new(struct new_window_ctx);
+ ctx->url = g_strdup(url);
+ ctx->related_view = webview; /* share WebProcess with parent */
+ g_signal_connect(win_action, "activate",
+ G_CALLBACK(on_context_open_in_new_window), ctx);
+
+ WebKitContextMenuItem *win_item =
+ webkit_context_menu_item_new_from_gaction(
+ G_ACTION(win_action), "Open Link in New Window", NULL);
+ webkit_context_menu_insert(context_menu, win_item, 2);
+ g_object_unref(win_action);
+ }
+
return FALSE; /* Let WebKit show the menu. */
}
@@ -3029,7 +3145,7 @@ static tab_info_t *tab_create(const char *url) {
* _M_get() const: Assertion 'this->_M_is_engaged()' failed.
* So we skip the explicit load and let WebKit drive the navigation. The
* tab's URL bar / title were already initialized from default_url above. */
- if (g_target_related_view == NULL) {
+ if (!g_target_skip_load) {
webkit_web_view_load_uri(tab->webview, default_url);
}
g_free(default_url);
@@ -3582,8 +3698,11 @@ int tab_manager_new_tab(const char *url) {
}
/* Inject the per-tab performance probe (sovereign://processes).
- * Skips internal sovereign:// pages at runtime via the preamble. */
- perf_probe_setup(tab->webview, index);
+ * Skips internal sovereign:// pages at runtime via the preamble.
+ * Gated on the perf_probe_enabled setting (see comment above). */
+ if (settings_get()->perf_probe_enabled) {
+ perf_probe_setup(tab->webview, index);
+ }
/* Add to the effective notebook (active window's notebook, or the
* main notebook as fallback). This makes Ctrl+T / "New Tab" open in
@@ -3610,8 +3729,23 @@ int tab_manager_new_tab(const char *url) {
gtk_widget_grab_focus(tab->url_entry);
gtk_editable_select_region(GTK_EDITABLE(tab->url_entry), 0, -1);
- g_print("[tabs] Created tab %d, total: %d\n", page_num, g_tab_count);
- return page_num;
+ g_print("[tabs] Created tab %d (page %d), total: %d\n",
+ index, page_num, g_tab_count);
+
+ /* Return the g_tabs array index (NOT the notebook page_num).
+ *
+ * The notebook page number only matches the g_tabs index for the
+ * main window; auxiliary windows have their own page numbering, and
+ * even in the main window the two can diverge after tabs are closed
+ * and reopened. The only caller that uses the return value
+ * (on_create_webview, for the WebKit "create" signal) passes it to
+ * tab_manager_get(), which expects a g_tabs index. Returning
+ * page_num there caused tab_manager_get() to resolve the WRONG tab
+ * — an already-existing webview with no pending WindowFeatures —
+ * which WebKit then tried to apply WindowFeatures to, triggering:
+ * std::optional::_M_get(): Assertion
+ * 'this->_M_is_engaged()' failed. */
+ return index;
}
void tab_manager_close_tab(int index) {
diff --git a/src/version.h b/src/version.h
index 69a3041..cac6546 100644
--- a/src/version.h
+++ b/src/version.h
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
-#define SB_VERSION "v0.0.57"
+#define SB_VERSION "v0.0.58"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
-#define SB_VERSION_PATCH 57
+#define SB_VERSION_PATCH 58
#endif /* SOVEREIGN_BROWSER_VERSION_H */