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 */