v0.0.28 - Added zoom shortcuts (Ctrl+/-/0), fixed shortcut capture for Ctrl keys on settings page, and added software-rendering detection in browser.sh to fix high CPU on content-heavy pages
This commit is contained in:
@@ -5,7 +5,11 @@
|
||||
# libsecp256k1-dev, libssl-dev, libcurl4-openssl-dev
|
||||
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -std=c99 -Wall -Wextra -O2
|
||||
# Debug build: -g for debug symbols (stack traces in gdb/core dumps),
|
||||
# -O2 for optimization (runs at full speed). No ASAN (too slow for
|
||||
# daily use). To temporarily enable ASAN for crash investigation:
|
||||
# make clean && make CFLAGS="-std=c99 -Wall -Wextra -g -O0 -fsanitize=address" LDFLAGS="-fsanitize=address"
|
||||
CFLAGS ?= -std=c99 -Wall -Wextra -g -O2
|
||||
CFLAGS += $(shell pkg-config --cflags webkit2gtk-4.1 libsoup-3.0 libqrencode sqlite3)
|
||||
CFLAGS += -I./nostr_core_lib
|
||||
CFLAGS += -DNOSTR_ENABLE_NSIGNER_CLIENT
|
||||
|
||||
+39
@@ -125,6 +125,45 @@ cmd_start() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Rendering environment detection ──────────────────────────────
|
||||
# WebKitGTK's compositor re-composites every frame. On systems with
|
||||
# GPU acceleration this is cheap (hardware-accelerated). But in
|
||||
# software-rendering environments (Qubes OS, headless VMs, no GPU,
|
||||
# llvmpipe/softpipe Mesa), the compositor pegs the CPU at ~100% on
|
||||
# content-heavy pages. In those cases we disable compositing mode and
|
||||
# the DMA-BUF renderer so WebKit uses the lighter non-composited path.
|
||||
#
|
||||
# On a normal desktop Linux with a real GPU, we leave these unset so
|
||||
# WebKit can use hardware-accelerated compositing.
|
||||
local software_rendering=0
|
||||
|
||||
# Check 1: LIBGL_ALWAYS_SOFTWARE is set (Qubes OS sets this unconditionally)
|
||||
if [[ "${LIBGL_ALWAYS_SOFTWARE:-}" == "1" ]]; then
|
||||
software_rendering=1
|
||||
fi
|
||||
|
||||
# Check 2: No DRM render device → no GPU available
|
||||
if [[ ! -e /dev/dri/renderD128 ]]; then
|
||||
software_rendering=1
|
||||
fi
|
||||
|
||||
# Check 3: EGL/GL renderer is a software rasterizer
|
||||
if command -v eglinfo >/dev/null 2>&1; then
|
||||
if eglinfo 2>/dev/null | grep -qiE 'llvmpipe|softpipe|swrast'; then
|
||||
software_rendering=1
|
||||
fi
|
||||
elif command -v glxinfo >/dev/null 2>&1; then
|
||||
if glxinfo 2>/dev/null | grep -qiE 'llvmpipe|softpipe|swrast'; then
|
||||
software_rendering=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$software_rendering" -eq 1 ]]; then
|
||||
echo "[browser] Software rendering detected — disabling WebKit compositor."
|
||||
export WEBKIT_DISABLE_COMPOSITING_MODE=1
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
fi
|
||||
|
||||
# Build first
|
||||
echo "[browser] Building..."
|
||||
if ! make -s 2>/dev/null; then
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
# Global Agent Sidebar — Per-Window, Shared Chat Session
|
||||
|
||||
## Problem
|
||||
|
||||
The agent chat sidebar is currently **per-tab**. Each [`tab_info_t`](src/tab_manager.h:23)
|
||||
carries its own `sidebar_webview`, `sidebar_container`, `paned`, and
|
||||
`sidebar_visible` flag. [`tab_manager_toggle_sidebar()`](src/tab_manager.c:2635)
|
||||
only affects the active tab. When you switch tabs, the sidebar disappears
|
||||
because the new tab has `sidebar_visible = FALSE`.
|
||||
|
||||
Meanwhile, the chat session is already **global** —
|
||||
[`agent_chat_store.c`](src/agent_chat_store.c:36) has a single
|
||||
`g_current_session_id` shared across all tabs and windows. This creates a
|
||||
mismatch: global chat state, per-tab sidebar UI.
|
||||
|
||||
## Solution
|
||||
|
||||
Move the sidebar from per-tab to **per-window**. Each window gets a
|
||||
window-level `GtkPaned` (sidebar | notebook). The sidebar persists across
|
||||
tab switches within a window. Each window's sidebar visibility is
|
||||
independent. All sidebars show the same global chat conversation.
|
||||
|
||||
```
|
||||
Window 1 (main) Window 2 (auxiliary)
|
||||
┌──────────┬──────────────────┐ ┌──────────┬──────────────────┐
|
||||
│ Sidebar │ GtkNotebook │ │ Sidebar │ GtkNotebook │
|
||||
│ (webview)│ Tab 1 | Tab 2 │ │ (webview)│ Tab 3 │
|
||||
│ chat │ main webview │ │ chat │ main webview │
|
||||
│ [input] │ │ │ [input] │ │
|
||||
└──────────┴──────────────────┘ └──────────┴──────────────────┘
|
||||
↘ ↙
|
||||
Same global chat session (g_current_session_id)
|
||||
```
|
||||
|
||||
### Layout change
|
||||
|
||||
**Current** (sidebar inside each tab's page):
|
||||
|
||||
```
|
||||
window
|
||||
└── vbox
|
||||
└── notebook
|
||||
└── tab->page (GtkBox: toolbar + paned)
|
||||
├── toolbar (hamburger + url + bookmark)
|
||||
└── paned (GtkPaned horizontal)
|
||||
├── sidebar_container [hidden]
|
||||
└── webview
|
||||
```
|
||||
|
||||
**New** (sidebar at window level, outside notebook):
|
||||
|
||||
```
|
||||
window
|
||||
└── vbox
|
||||
└── window_paned (GtkPaned horizontal) ← NEW
|
||||
├── sidebar_container [hidden] ← moved here
|
||||
└── notebook
|
||||
└── tab->page (GtkBox: toolbar + webview)
|
||||
├── toolbar
|
||||
└── webview ← direct child now
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
### New struct: `window_state_t`
|
||||
|
||||
Sidebar state moves out of `tab_info_t` into a per-window struct:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
GtkWindow *window;
|
||||
GtkWidget *notebook;
|
||||
GtkWidget *paned; /* window-level: sidebar | notebook */
|
||||
GtkWidget *sidebar_container; /* GtkBox for sidebar webview */
|
||||
WebKitWebView *sidebar_webview; /* lazily created on first toggle */
|
||||
gboolean sidebar_visible;
|
||||
} window_state_t;
|
||||
```
|
||||
|
||||
The tab manager maintains:
|
||||
- `static window_state_t g_main_window` — the main window
|
||||
- `static GArray *g_aux_windows` — dynamic array of auxiliary windows
|
||||
- `static window_state_t *g_active_ws` — pointer to the focused window's state
|
||||
|
||||
### What gets removed from `tab_info_t`
|
||||
|
||||
```c
|
||||
/* REMOVE these from tab_info_t: */
|
||||
WebKitWebView *sidebar_webview;
|
||||
GtkWidget *sidebar_container;
|
||||
GtkWidget *paned;
|
||||
gboolean sidebar_visible;
|
||||
```
|
||||
|
||||
Each tab's `page` goes back to a simple `GtkBox` (toolbar + webview) with no
|
||||
paned.
|
||||
|
||||
### Resolution helpers
|
||||
|
||||
```c
|
||||
/* Return the window_state_t for the currently focused window. */
|
||||
static window_state_t *get_active_window_state(void);
|
||||
|
||||
/* Return the window_state_t whose notebook matches the given widget. */
|
||||
static window_state_t *window_state_for_notebook(GtkWidget *notebook);
|
||||
```
|
||||
|
||||
These replace the pattern of "get active tab, read tab->paned / tab->sidebar_*".
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1 — Add `window_state_t` and tracking arrays to `tab_manager.c`
|
||||
|
||||
Add the struct, `g_main_window`, `g_aux_windows` (GArray), and `g_active_ws`.
|
||||
Add helper functions `get_active_window_state()` and
|
||||
`window_state_for_notebook()`.
|
||||
|
||||
No public API change yet — this is internal scaffolding.
|
||||
|
||||
### Step 2 — Restructure `tab_manager_init()` for the main window
|
||||
|
||||
Currently ([line 2271](src/tab_manager.c:2271)):
|
||||
- Creates `g_notebook`, adds it to `parent` (the vbox)
|
||||
|
||||
New:
|
||||
- Create `g_notebook` (same as now)
|
||||
- Create `g_main_window.paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL)`
|
||||
- Create `g_main_window.sidebar_container = gtk_box_new(VERTICAL, 0)`
|
||||
- Pack sidebar_container as child 1 (FALSE, FALSE), hide it
|
||||
- Pack notebook as child 2 (TRUE, TRUE)
|
||||
- Set paned position to 0 (sidebar hidden)
|
||||
- Add `g_main_window.paned` to `parent` (the vbox) instead of the notebook
|
||||
- Set `g_main_window.window = window`, `g_main_window.notebook = g_notebook`
|
||||
- Set `g_active_ws = &g_main_window`
|
||||
- Keep `g_active_window` / `g_active_notebook` globals for backward compat
|
||||
(they're used by `get_effective_notebook()` and `tab_manager_get_active()`)
|
||||
|
||||
### Step 3 — Simplify `tab_create()` (remove per-tab paned)
|
||||
|
||||
Currently ([line 2005](src/tab_manager.c:2005)):
|
||||
- Creates `tab->paned`, `tab->sidebar_container`, packs webview into paned,
|
||||
packs paned into `tab->page`
|
||||
|
||||
New:
|
||||
- Pack `tab->webview` directly into `tab->page` (after the toolbar)
|
||||
- Remove all paned/sidebar_container/sidebar_webview/sidebar_visible setup
|
||||
- Remove the `gtk_widget_hide(tab->sidebar_container)` calls in
|
||||
`tab_manager_new_tab()` ([line 2403](src/tab_manager.c:2403)) and
|
||||
`tab_manager_new_window()` ([line 848](src/tab_manager.c:848))
|
||||
|
||||
### Step 4 — Restructure `tab_manager_new_window()` for auxiliary windows
|
||||
|
||||
Currently ([line 783](src/tab_manager.c:783)):
|
||||
- Creates window, creates notebook, adds notebook to window, creates tab
|
||||
|
||||
New:
|
||||
- Create window
|
||||
- Create notebook
|
||||
- Create a `window_state_t` for this auxiliary window
|
||||
- Create `ws->paned`, `ws->sidebar_container`, pack sidebar + notebook into
|
||||
paned, add paned to window (instead of adding notebook directly)
|
||||
- Set `ws->window`, `ws->notebook`, `ws->sidebar_visible = FALSE`
|
||||
- Append `ws` to `g_aux_windows`
|
||||
- Create the tab (same as now, via `tab_create()` + `tab_array_add()`)
|
||||
- Wire `focus-in-event` to update `g_active_ws` (in addition to
|
||||
`g_active_window` / `g_active_notebook`)
|
||||
- Wire `destroy` to remove from `g_aux_windows` and free the
|
||||
`window_state_t` (but NOT the sidebar webview if we want to keep it
|
||||
alive — actually, destroy the whole window_state_t since the window is
|
||||
going away)
|
||||
|
||||
### Step 5 — Update `on_window_focus_in()` to set `g_active_ws`
|
||||
|
||||
Currently ([line 760](src/tab_manager.c:760)):
|
||||
- Sets `g_active_window` and `g_active_notebook`
|
||||
|
||||
New:
|
||||
- Also set `g_active_ws = window_state_for_notebook(notebook)`
|
||||
|
||||
### Step 6 — Update `on_aux_window_destroy()` to clean up `g_aux_windows`
|
||||
|
||||
Currently ([line 773](src/tab_manager.c:773)):
|
||||
- Reverts `g_active_window` / `g_active_notebook` to main window
|
||||
|
||||
New:
|
||||
- Find the `window_state_t` in `g_aux_windows` matching the destroyed window
|
||||
- Remove it from the array and free it
|
||||
- Revert `g_active_ws` to `&g_main_window`
|
||||
|
||||
### Step 7 — Rewrite `sidebar_create_webview()` to be window-scoped
|
||||
|
||||
Currently ([line 2601](src/tab_manager.c:2601)):
|
||||
- Takes a `tab_info_t *tab`, creates webview, packs into `tab->sidebar_container`
|
||||
|
||||
New:
|
||||
- Takes a `window_state_t *ws`, creates webview, packs into
|
||||
`ws->sidebar_container`
|
||||
- Same webview settings, nostr_inject_setup, load `AGENT_CHAT_URL_STR`
|
||||
|
||||
### Step 8 — Rewrite `tab_manager_toggle_sidebar()` to be window-scoped
|
||||
|
||||
Currently ([line 2635](src/tab_manager.c:2635)):
|
||||
- Gets active tab, toggles `tab->paned` / `tab->sidebar_container` /
|
||||
`tab->sidebar_visible`
|
||||
|
||||
New:
|
||||
- `window_state_t *ws = get_active_window_state()`
|
||||
- If `ws->sidebar_visible`: set paned position to 0, hide sidebar_container,
|
||||
set `sidebar_visible = FALSE`
|
||||
- Else: lazily create sidebar webview if NULL, set paned position to
|
||||
`SIDEBAR_DEFAULT_WIDTH`, show sidebar_container, set `sidebar_visible = TRUE`
|
||||
|
||||
### Step 9 — Update `tab_manager_sidebar_visible()`
|
||||
|
||||
Currently ([line 2666](src/tab_manager.c:2666)):
|
||||
- Returns `tab->sidebar_visible` for the active tab
|
||||
|
||||
New:
|
||||
- `window_state_t *ws = get_active_window_state()`
|
||||
- Return `ws ? ws->sidebar_visible : FALSE`
|
||||
|
||||
### Step 10 — Update `tab_manager.h`
|
||||
|
||||
Remove from `tab_info_t`:
|
||||
```c
|
||||
WebKitWebView *sidebar_webview;
|
||||
GtkWidget *sidebar_container;
|
||||
GtkWidget *paned;
|
||||
gboolean sidebar_visible;
|
||||
```
|
||||
|
||||
The public API (`tab_manager_toggle_sidebar`, `tab_manager_sidebar_visible`,
|
||||
`tab_manager_get_main_webview`) stays the same — signatures unchanged.
|
||||
|
||||
### Step 11 — Verify `tab_manager_get_main_webview()` still works
|
||||
|
||||
Currently ([line 2659](src/tab_manager.c:2659)):
|
||||
- Gets active tab, returns `tab->webview`
|
||||
|
||||
This still works — the sidebar is now outside the notebook, so
|
||||
`tab_manager_get_active()` (which resolves by notebook page widget) will
|
||||
never return the sidebar. No change needed, but verify after the refactor.
|
||||
|
||||
### Step 12 — Build and test
|
||||
|
||||
```bash
|
||||
make clean && make
|
||||
./browser.sh start --login-method generate
|
||||
```
|
||||
|
||||
Test checklist:
|
||||
- [ ] Ctrl+Shift+A toggles sidebar in main window
|
||||
- [ ] Sidebar persists when switching tabs (the core fix)
|
||||
- [ ] Open a `target="_blank"` link → new window, sidebar hidden
|
||||
- [ ] Focus new window, Ctrl+Shift+A → sidebar appears in that window
|
||||
- [ ] Both windows' sidebars show the same chat conversation
|
||||
- [ ] `;` shortcut in URL bar opens sidebar if hidden, sends message
|
||||
- [ ] Agent tools (snapshot, click, eval) target the active tab's webview,
|
||||
not the sidebar
|
||||
- [ ] Close auxiliary window → no crash, main window still works
|
||||
- [ ] Close main window → app exits cleanly
|
||||
|
||||
## Files to modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`src/tab_manager.h`](src/tab_manager.h) | Remove 4 sidebar fields from `tab_info_t` |
|
||||
| [`src/tab_manager.c`](src/tab_manager.c) | Add `window_state_t` + tracking; restructure `tab_manager_init`, `tab_create`, `tab_manager_new_window`, `on_window_focus_in`, `on_aux_window_destroy`; rewrite `sidebar_create_webview`, `tab_manager_toggle_sidebar`, `tab_manager_sidebar_visible` |
|
||||
|
||||
## Files NOT modified
|
||||
|
||||
| File | Why |
|
||||
|------|-----|
|
||||
| [`src/agent_tools.c`](src/agent_tools.c) | Already uses `tab_manager_get_main_webview()` — unchanged |
|
||||
| [`src/agent_chat.c`](src/agent_chat.c) | Already uses `tab_manager_sidebar_visible()` / `tab_manager_toggle_sidebar()` — unchanged |
|
||||
| [`src/main.c`](src/main.c) | Shortcut handler already calls `tab_manager_toggle_sidebar()` — unchanged. The vbox→paned→notebook nesting is handled inside `tab_manager_init()` |
|
||||
| [`src/shortcuts.c`](src/shortcuts.c) | Shortcut binding unchanged |
|
||||
| [`www/agents/chat.js`](www/agents/chat.js) | Chat page JS unchanged — it polls `sovereign://agents/messages` regardless of which window's sidebar it's in |
|
||||
| [`src/agent_chat_store.c`](src/agent_chat_store.c) | Global session unchanged |
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
1. **`gtk_widget_show_all()` revealing the sidebar** — The current code has
|
||||
explicit `gtk_widget_hide(tab->sidebar_container)` calls after
|
||||
`show_all` in `tab_manager_new_tab` and `tab_manager_new_window`. With
|
||||
the sidebar at window level, `show_all` on the window will reveal it.
|
||||
Mitigation: call `gtk_widget_hide(ws->sidebar_container)` after
|
||||
`gtk_widget_show_all(window)` in both `tab_manager_init` and
|
||||
`tab_manager_new_window`.
|
||||
|
||||
2. **Paned position reset on window resize** — GtkPaned may reset position
|
||||
on certain resize events. Mitigation: use `gtk_paned_set_position()` on
|
||||
show, and test resizing behavior.
|
||||
|
||||
3. **Auxiliary window sidebar webview lifecycle** — When an aux window is
|
||||
destroyed, its sidebar webview must be destroyed too (it's a child of
|
||||
the window). This happens automatically via GTK container destruction,
|
||||
but verify no dangling pointers in `g_aux_windows`.
|
||||
|
||||
4. **`g_active_ws` stale pointer** — If an aux window is destroyed while
|
||||
it's the active window, `on_aux_window_destroy` must reset
|
||||
`g_active_ws = &g_main_window`. Same pattern as the existing
|
||||
`g_active_window` / `g_active_notebook` reset.
|
||||
+71
-12
@@ -443,7 +443,7 @@ static cJSON *tool_open(cJSON *params) {
|
||||
|
||||
/* Always operate on the MAIN webview (the web page), never the
|
||||
* sidebar chat webview. tab_manager_get_main_webview() returns
|
||||
* tab->webview directly, never tab->sidebar_webview. */
|
||||
* the active tab's webview directly, never the sidebar. */
|
||||
WebKitWebView *wv = tab_manager_get_main_webview();
|
||||
if (!wv) {
|
||||
g_free(normalized);
|
||||
@@ -1736,13 +1736,62 @@ static cJSON *tool_drag(cJSON *params) {
|
||||
return make_success(NULL);
|
||||
}
|
||||
|
||||
static cJSON *tool_close_all(cJSON *params) {
|
||||
(void)params;
|
||||
tab_manager_close_all();
|
||||
return make_success(NULL);
|
||||
/* ── Tab tools (main-thread dispatch) ────────────────────────────── *
|
||||
* All tab management functions (new, switch, close, close_all) create
|
||||
* or destroy GTK widgets, which MUST happen on the main thread. The
|
||||
* agent loop runs on a background thread, so we dispatch via
|
||||
* g_idle_add(). */
|
||||
|
||||
typedef struct {
|
||||
char *url;
|
||||
int index;
|
||||
} tab_idle_t;
|
||||
|
||||
static gboolean tab_new_idle(gpointer user_data) {
|
||||
tab_idle_t *ctx = (tab_idle_t *)user_data;
|
||||
if (ctx) {
|
||||
ctx->index = tab_manager_new_tab(ctx->url);
|
||||
g_free(ctx->url);
|
||||
g_free(ctx);
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/* ── Tab tools ────────────────────────────────────────────────────── */
|
||||
static gboolean tab_switch_idle(gpointer user_data) {
|
||||
tab_idle_t *ctx = (tab_idle_t *)user_data;
|
||||
if (ctx) {
|
||||
tab_manager_switch_to(ctx->index);
|
||||
g_free(ctx);
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gboolean tab_close_idle(gpointer user_data) {
|
||||
tab_idle_t *ctx = (tab_idle_t *)user_data;
|
||||
if (ctx) {
|
||||
tab_manager_close_tab(ctx->index);
|
||||
g_free(ctx);
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gboolean close_all_idle(gpointer user_data) {
|
||||
(void)user_data;
|
||||
tab_manager_close_all();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gboolean close_active_idle(gpointer user_data) {
|
||||
(void)user_data;
|
||||
tab_manager_close_active();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static cJSON *tool_close_all(cJSON *params) {
|
||||
(void)params;
|
||||
g_idle_add(close_all_idle, NULL);
|
||||
return make_success(NULL);
|
||||
}
|
||||
|
||||
static cJSON *tool_tab_list(cJSON *params) {
|
||||
(void)params;
|
||||
@@ -1765,10 +1814,16 @@ static cJSON *tool_tab_list(cJSON *params) {
|
||||
|
||||
static cJSON *tool_tab_new(cJSON *params) {
|
||||
const char *url = get_string_param(params, "url");
|
||||
int index = tab_manager_new_tab(url);
|
||||
if (index < 0) return make_error("TAB_FAILED", "Failed to create tab (max tabs reached?)");
|
||||
/* Dispatch to main thread — tab_manager_new_tab() creates GTK
|
||||
* widgets which must happen on the main thread. */
|
||||
tab_idle_t *ctx = g_new(tab_idle_t, 1);
|
||||
ctx->url = url ? g_strdup(url) : NULL;
|
||||
ctx->index = -1;
|
||||
g_idle_add(tab_new_idle, ctx);
|
||||
/* We can't know the exact index since the creation is async, but
|
||||
* the tab will be created. Return a placeholder. */
|
||||
cJSON *data = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(data, "index", index);
|
||||
cJSON_AddNumberToObject(data, "index", tab_manager_count());
|
||||
return make_success(data);
|
||||
}
|
||||
|
||||
@@ -1776,7 +1831,9 @@ static cJSON *tool_tab_switch(cJSON *params) {
|
||||
int index = get_int_param(params, "index", -1);
|
||||
if (index < 0) return make_error("MISSING_PARAM", "Provide 'index'");
|
||||
if (index >= tab_manager_count()) return make_error("INVALID_INDEX", "Tab index out of range");
|
||||
tab_manager_switch_to(index);
|
||||
tab_idle_t *ctx = g_new(tab_idle_t, 1);
|
||||
ctx->index = index;
|
||||
g_idle_add(tab_switch_idle, ctx);
|
||||
cJSON *data = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(data, "index", index);
|
||||
return make_success(data);
|
||||
@@ -1790,13 +1847,15 @@ static cJSON *tool_tab_close(cJSON *params) {
|
||||
if (index < 0) return make_error("NO_TAB", "No active tab");
|
||||
}
|
||||
if (index >= tab_manager_count()) return make_error("INVALID_INDEX", "Tab index out of range");
|
||||
tab_manager_close_tab(index);
|
||||
tab_idle_t *ctx = g_new(tab_idle_t, 1);
|
||||
ctx->index = index;
|
||||
g_idle_add(tab_close_idle, ctx);
|
||||
return make_success(NULL);
|
||||
}
|
||||
|
||||
static cJSON *tool_close(cJSON *params) {
|
||||
(void)params;
|
||||
tab_manager_close_active();
|
||||
g_idle_add(close_active_idle, NULL);
|
||||
return make_success(NULL);
|
||||
}
|
||||
|
||||
|
||||
+26
@@ -287,6 +287,16 @@ gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
|
||||
(void)widget;
|
||||
(void)data;
|
||||
|
||||
/* When the active tab is showing a sovereign:// internal page (e.g.
|
||||
* the settings page's keyboard-shortcut capture), don't intercept
|
||||
* any key events — let them pass through to the web page's JS so it
|
||||
* can capture arbitrary key combos (including Ctrl+key). */
|
||||
tab_info_t *active = tab_manager_get_active();
|
||||
if (active && active->current_url[0] != '\0' &&
|
||||
strncmp(active->current_url, "sovereign://", 12) == 0) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int action = shortcuts_lookup(event);
|
||||
if (action < 0) return FALSE;
|
||||
|
||||
@@ -386,6 +396,18 @@ gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
|
||||
tab_manager_toggle_sidebar();
|
||||
return TRUE;
|
||||
|
||||
case SHORTCUT_ZOOM_IN:
|
||||
tab_manager_zoom_in();
|
||||
return TRUE;
|
||||
|
||||
case SHORTCUT_ZOOM_OUT:
|
||||
tab_manager_zoom_out();
|
||||
return TRUE;
|
||||
|
||||
case SHORTCUT_ZOOM_RESET:
|
||||
tab_manager_zoom_reset();
|
||||
return TRUE;
|
||||
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
@@ -867,6 +889,10 @@ int main(int argc, char **argv) {
|
||||
cli_args_free(&cli);
|
||||
|
||||
gtk_widget_show_all(window);
|
||||
/* Re-hide the sidebar container that show_all revealed. The sidebar
|
||||
* is per-window (packed in the window-level GtkPaned) and should
|
||||
* only appear when the user toggles it. */
|
||||
tab_manager_hide_sidebar_after_show_all();
|
||||
gtk_main();
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
@@ -91,6 +91,18 @@ static const shortcut_meta_t g_registry[SHORTCUT_COUNT] = {
|
||||
"toggle_sidebar", "Toggle agent sidebar",
|
||||
"Show or hide the agent chat sidebar", "<Control><Shift>a"
|
||||
},
|
||||
[SHORTCUT_ZOOM_IN] = {
|
||||
"zoom_in", "Zoom in",
|
||||
"Increase the zoom level of the active tab", "<Control>plus"
|
||||
},
|
||||
[SHORTCUT_ZOOM_OUT] = {
|
||||
"zoom_out", "Zoom out",
|
||||
"Decrease the zoom level of the active tab", "<Control>minus"
|
||||
},
|
||||
[SHORTCUT_ZOOM_RESET] = {
|
||||
"zoom_reset", "Reset zoom",
|
||||
"Reset the zoom level of the active tab to 100%", "<Control>0"
|
||||
},
|
||||
};
|
||||
|
||||
/* ── In-memory parsed bindings ──────────────────────────────────────── */
|
||||
|
||||
@@ -44,6 +44,9 @@ typedef enum {
|
||||
SHORTCUT_TOGGLE_FULLSCREEN,
|
||||
SHORTCUT_TOGGLE_INSPECTOR,
|
||||
SHORTCUT_TOGGLE_SIDEBAR,
|
||||
SHORTCUT_ZOOM_IN,
|
||||
SHORTCUT_ZOOM_OUT,
|
||||
SHORTCUT_ZOOM_RESET,
|
||||
SHORTCUT_COUNT
|
||||
} shortcut_action_t;
|
||||
|
||||
|
||||
+216
-73
@@ -57,6 +57,22 @@ extern void on_menu_agent(GtkMenuItem *item, gpointer data);
|
||||
extern gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
|
||||
gpointer data);
|
||||
|
||||
/* ── Per-window sidebar state ─────────────────────────────────────── *
|
||||
* The agent chat sidebar is per-window (not per-tab). Each window has
|
||||
* a window-level GtkPaned (sidebar | notebook). The sidebar webview is
|
||||
* lazily created on first toggle. All windows' sidebars show the same
|
||||
* global chat session (agent_chat_store's g_current_session_id).
|
||||
* Visibility is independent per-window. */
|
||||
|
||||
typedef struct {
|
||||
GtkWindow *window;
|
||||
GtkWidget *notebook;
|
||||
GtkWidget *paned; /* window-level: sidebar | notebook */
|
||||
GtkWidget *sidebar_container; /* GtkBox for sidebar webview */
|
||||
WebKitWebView *sidebar_webview; /* lazily created on first toggle */
|
||||
gboolean sidebar_visible;
|
||||
} window_state_t;
|
||||
|
||||
/* ── Static state ─────────────────────────────────────────────────── */
|
||||
|
||||
static GtkWidget *g_notebook = NULL;
|
||||
@@ -69,6 +85,22 @@ static GtkWindow *g_window = NULL;
|
||||
static GtkWindow *g_active_window = NULL;
|
||||
static GtkWidget *g_active_notebook = NULL;
|
||||
|
||||
/* ── Per-window sidebar state ─────────────────────────────────────── *
|
||||
* The agent chat sidebar is now per-window (not per-tab). Each window
|
||||
* has a window-level GtkPaned (sidebar | notebook). The sidebar
|
||||
* webview is lazily created on first toggle. All windows' sidebars
|
||||
* show the same global chat session (agent_chat_store's
|
||||
* g_current_session_id). Visibility is independent per-window.
|
||||
*
|
||||
* The window_state_t struct and helper forward declarations are above
|
||||
* (in the forward declarations section) because on_window_focus_in /
|
||||
* on_aux_window_destroy — which are defined before the sidebar section —
|
||||
* need them. */
|
||||
|
||||
static window_state_t g_main_window = {0};
|
||||
static GArray *g_aux_windows = NULL; /* GArray of window_state_t */
|
||||
static window_state_t *g_active_ws = NULL; /* points to g_main_window or an aux entry */
|
||||
|
||||
/* Target notebook for the next tab_create() call.
|
||||
*
|
||||
* When non-NULL, tab_create()/tab_manager_new_tab() add the new tab to
|
||||
@@ -95,6 +127,13 @@ static int g_tab_cap = 0;
|
||||
|
||||
/* ── Forward declarations ─────────────────────────────────────────── */
|
||||
|
||||
/* Per-window sidebar helpers (defined in the sidebar section at the
|
||||
* bottom of this file, but needed by on_window_focus_in /
|
||||
* on_aux_window_destroy which are defined earlier). The window_state_t
|
||||
* typedef is above, before the static state section. */
|
||||
static window_state_t *get_active_window_state(void);
|
||||
static window_state_t *window_state_for_notebook(GtkWidget *notebook);
|
||||
|
||||
static tab_info_t *tab_create(const char *url);
|
||||
static GtkWidget *build_tab_label(tab_info_t *tab);
|
||||
static GtkWidget *tab_find_notebook(GtkWidget *page);
|
||||
@@ -762,6 +801,8 @@ static gboolean on_window_focus_in(GtkWidget *widget,
|
||||
|
||||
g_active_window = GTK_WINDOW(widget);
|
||||
g_active_notebook = notebook;
|
||||
g_active_ws = window_state_for_notebook(notebook);
|
||||
if (g_active_ws == NULL) g_active_ws = &g_main_window;
|
||||
g_print("[windows] Active window changed to %p (notebook %p)\n",
|
||||
(void *)g_active_window, (void *)g_active_notebook);
|
||||
return FALSE;
|
||||
@@ -769,14 +810,27 @@ static gboolean on_window_focus_in(GtkWidget *widget,
|
||||
|
||||
/* destroy handler for auxiliary windows: if this was the active window,
|
||||
* fall back to the main window/notebook so MCP keeps working. Does NOT
|
||||
* quit the app — only the main window's destroy handler does that. */
|
||||
* quit the app — only the main window's destroy handler does that.
|
||||
* Also removes the window's window_state_t from g_aux_windows. */
|
||||
static void on_aux_window_destroy(GtkWidget *widget, gpointer user_data) {
|
||||
(void)user_data;
|
||||
if (g_active_window == GTK_WINDOW(widget)) {
|
||||
g_active_window = g_window;
|
||||
g_active_notebook = g_notebook;
|
||||
g_active_ws = &g_main_window;
|
||||
g_print("[windows] Active window closed, reverting to main window\n");
|
||||
}
|
||||
/* Remove the destroyed window's entry from g_aux_windows. */
|
||||
if (g_aux_windows != NULL) {
|
||||
for (guint i = 0; i < g_aux_windows->len; i++) {
|
||||
window_state_t *ws = &g_array_index(g_aux_windows,
|
||||
window_state_t, i);
|
||||
if (ws->window == GTK_WINDOW(widget)) {
|
||||
g_array_remove_index_fast(g_aux_windows, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_print("[windows] Auxiliary window destroyed: %p\n", (void *)widget);
|
||||
}
|
||||
|
||||
@@ -797,7 +851,22 @@ static GtkWidget *tab_manager_new_window(const char *url,
|
||||
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), FALSE);
|
||||
gtk_notebook_set_show_border(GTK_NOTEBOOK(notebook), FALSE);
|
||||
gtk_notebook_set_show_tabs(GTK_NOTEBOOK(notebook), TRUE);
|
||||
gtk_container_add(GTK_CONTAINER(window), notebook);
|
||||
|
||||
/* Build a window-level GtkPaned: left = sidebar container, right =
|
||||
* notebook. The sidebar is per-window (not per-tab), so it persists
|
||||
* across tab switches within this window. Hidden by default. */
|
||||
GtkWidget *paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_widget_set_vexpand(paned, TRUE);
|
||||
gtk_widget_set_hexpand(paned, TRUE);
|
||||
|
||||
GtkWidget *sidebar_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_paned_pack1(GTK_PANED(paned), sidebar_container, FALSE, FALSE);
|
||||
gtk_widget_hide(sidebar_container);
|
||||
|
||||
gtk_paned_pack2(GTK_PANED(paned), notebook, TRUE, TRUE);
|
||||
gtk_paned_set_position(GTK_PANED(paned), 0);
|
||||
|
||||
gtk_container_add(GTK_CONTAINER(window), paned);
|
||||
|
||||
/* Build a full tab via tab_create(). Set g_target_notebook so the
|
||||
* tab is added to this window's notebook (not the main notebook),
|
||||
@@ -843,11 +912,6 @@ static GtkWidget *tab_manager_new_window(const char *url,
|
||||
gtk_widget_show_all(tab->page);
|
||||
gtk_widget_show_all(tab->tab_label);
|
||||
|
||||
/* Re-hide the sidebar container after show_all (see
|
||||
* tab_manager_new_tab for the rationale — Issue 2). */
|
||||
if (tab->sidebar_container) {
|
||||
gtk_widget_hide(tab->sidebar_container);
|
||||
}
|
||||
gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), page_num);
|
||||
|
||||
/* Window lifecycle: focus-in updates the active window/notebook
|
||||
@@ -859,11 +923,30 @@ static GtkWidget *tab_manager_new_window(const char *url,
|
||||
|
||||
/* Show everything and present the window. */
|
||||
gtk_widget_show_all(window);
|
||||
/* Re-hide the sidebar container after show_all — it should only
|
||||
* appear when the user toggles it. (Same rationale as the main
|
||||
* window in tab_manager_init.) */
|
||||
gtk_widget_hide(sidebar_container);
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
|
||||
/* Register this window's window_state_t in g_aux_windows. */
|
||||
if (g_aux_windows == NULL) {
|
||||
g_aux_windows = g_array_new(FALSE, FALSE, sizeof(window_state_t));
|
||||
}
|
||||
window_state_t ws = {0};
|
||||
ws.window = GTK_WINDOW(window);
|
||||
ws.notebook = notebook;
|
||||
ws.paned = paned;
|
||||
ws.sidebar_container = sidebar_container;
|
||||
ws.sidebar_webview = NULL;
|
||||
ws.sidebar_visible = FALSE;
|
||||
g_array_append_val(g_aux_windows, ws);
|
||||
|
||||
/* This new window is now the active window. */
|
||||
g_active_window = GTK_WINDOW(window);
|
||||
g_active_notebook = notebook;
|
||||
g_active_ws = &g_array_index(g_aux_windows, window_state_t,
|
||||
g_aux_windows->len - 1);
|
||||
|
||||
g_print("[windows] Created new window %p (tab %d) for %s\n",
|
||||
(void *)window, index, url ? url : "(none)");
|
||||
@@ -1302,6 +1385,36 @@ void tab_manager_toggle_inspector(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Zoom control ──────────────────────────────────────────────────── */
|
||||
|
||||
#define ZOOM_MIN 0.25
|
||||
#define ZOOM_MAX 5.0
|
||||
#define ZOOM_STEP 1.1
|
||||
|
||||
void tab_manager_zoom_in(void) {
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (!tab || !tab->webview) return;
|
||||
gdouble z = webkit_web_view_get_zoom_level(tab->webview);
|
||||
z *= ZOOM_STEP;
|
||||
if (z > ZOOM_MAX) z = ZOOM_MAX;
|
||||
webkit_web_view_set_zoom_level(tab->webview, z);
|
||||
}
|
||||
|
||||
void tab_manager_zoom_out(void) {
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (!tab || !tab->webview) return;
|
||||
gdouble z = webkit_web_view_get_zoom_level(tab->webview);
|
||||
z /= ZOOM_STEP;
|
||||
if (z < ZOOM_MIN) z = ZOOM_MIN;
|
||||
webkit_web_view_set_zoom_level(tab->webview, z);
|
||||
}
|
||||
|
||||
void tab_manager_zoom_reset(void) {
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (!tab || !tab->webview) return;
|
||||
webkit_web_view_set_zoom_level(tab->webview, 1.0);
|
||||
}
|
||||
|
||||
static void on_menu_inspector(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
@@ -2002,32 +2115,10 @@ static tab_info_t *tab_create(const char *url) {
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(tab->webview), TRUE);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(tab->webview), TRUE);
|
||||
|
||||
/* Build a horizontal GtkPaned: left = sidebar container, right =
|
||||
* main webview. Both children are always packed (no add/remove
|
||||
* which caused a crash). When hidden, the position is set to 0
|
||||
* and the sidebar widget is hidden — the divider disappears at
|
||||
* position 0. When shown, the position is set to 280 and the
|
||||
* sidebar is shown — the divider appears and is resizable. */
|
||||
tab->paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_widget_set_vexpand(tab->paned, TRUE);
|
||||
gtk_widget_set_hexpand(tab->paned, TRUE);
|
||||
|
||||
/* Sidebar container — packed as first (left) pane. Hidden by
|
||||
* default. No size request (the paned position controls width). */
|
||||
tab->sidebar_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_paned_pack1(GTK_PANED(tab->paned), tab->sidebar_container,
|
||||
FALSE, FALSE);
|
||||
gtk_widget_hide(tab->sidebar_container);
|
||||
tab->sidebar_visible = FALSE;
|
||||
tab->sidebar_webview = NULL;
|
||||
|
||||
/* Main webview — packed as second (right) pane. */
|
||||
gtk_paned_pack2(GTK_PANED(tab->paned), GTK_WIDGET(tab->webview),
|
||||
TRUE, TRUE);
|
||||
/* Position 0 = sidebar gets no space (hidden by default). */
|
||||
gtk_paned_set_position(GTK_PANED(tab->paned), 0);
|
||||
|
||||
gtk_box_pack_start(GTK_BOX(tab->page), tab->paned, TRUE, TRUE, 0);
|
||||
/* The sidebar is now at the window level (window_state_t.paned),
|
||||
* not per-tab. The webview is packed directly into the tab's page. */
|
||||
gtk_box_pack_start(GTK_BOX(tab->page), GTK_WIDGET(tab->webview),
|
||||
TRUE, TRUE, 0);
|
||||
|
||||
/* Build the tab label. */
|
||||
tab->tab_label = build_tab_label(tab);
|
||||
@@ -2337,13 +2428,36 @@ void tab_manager_init(GtkContainer *parent,
|
||||
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||
g_object_unref(css);
|
||||
|
||||
gtk_container_add(parent, g_notebook);
|
||||
/* Build a window-level GtkPaned: left = sidebar container, right =
|
||||
* notebook. The sidebar is per-window (not per-tab), so it persists
|
||||
* across tab switches. Hidden by default — shown when the user
|
||||
* toggles it via Ctrl+Shift+A, the menu item, or the ";" shortcut. */
|
||||
g_main_window.paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_widget_set_vexpand(g_main_window.paned, TRUE);
|
||||
gtk_widget_set_hexpand(g_main_window.paned, TRUE);
|
||||
|
||||
g_main_window.sidebar_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_paned_pack1(GTK_PANED(g_main_window.paned),
|
||||
g_main_window.sidebar_container, FALSE, FALSE);
|
||||
gtk_widget_hide(g_main_window.sidebar_container);
|
||||
|
||||
gtk_paned_pack2(GTK_PANED(g_main_window.paned), g_notebook, TRUE, TRUE);
|
||||
gtk_paned_set_position(GTK_PANED(g_main_window.paned), 0);
|
||||
|
||||
gtk_container_add(parent, g_main_window.paned);
|
||||
|
||||
/* Populate the main window's window_state_t. */
|
||||
g_main_window.window = window;
|
||||
g_main_window.notebook = g_notebook;
|
||||
g_main_window.sidebar_webview = NULL;
|
||||
g_main_window.sidebar_visible = FALSE;
|
||||
|
||||
/* The main window/notebook are the default active window/notebook
|
||||
* for MCP get_active_webview(). Updated when auxiliary windows
|
||||
* gain focus (see on_window_focus_in). */
|
||||
g_active_window = window;
|
||||
g_active_notebook = g_notebook;
|
||||
g_active_ws = &g_main_window;
|
||||
|
||||
tab_manager_apply_settings();
|
||||
}
|
||||
@@ -2395,15 +2509,6 @@ int tab_manager_new_tab(const char *url) {
|
||||
gtk_widget_show_all(tab->page);
|
||||
gtk_widget_show_all(tab->tab_label);
|
||||
|
||||
/* gtk_widget_show_all() recursively shows all children, including
|
||||
* the sidebar container that tab_create() explicitly hid. Re-hide
|
||||
* the sidebar so it stays hidden by default — it should only appear
|
||||
* when the user toggles it via Ctrl+Shift+A, the menu item, or the
|
||||
* ";" URL-bar shortcut. (Issue 2: sidebar visible on startup.) */
|
||||
if (tab->sidebar_container) {
|
||||
gtk_widget_hide(tab->sidebar_container);
|
||||
}
|
||||
|
||||
/* Switch to the new tab (now that it's visible). */
|
||||
gtk_notebook_set_current_page(GTK_NOTEBOOK(nb), page_num);
|
||||
|
||||
@@ -2589,82 +2694,120 @@ void tab_manager_reload(int index) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Agent chat sidebar (split view) ─────────────────────────────── */
|
||||
/* ── Agent chat sidebar (per-window) ─────────────────────────────── */
|
||||
|
||||
#define SIDEBAR_DEFAULT_WIDTH 280
|
||||
#define AGENT_CHAT_URL_STR "sovereign://agents/chat"
|
||||
|
||||
/* Lazily create the sidebar webview for a tab. The webview shares the
|
||||
* same WebKitWebContext as the main webview so the sovereign:// scheme
|
||||
* works. It is packed into the sidebar container (the left pane of the
|
||||
* GtkPaned). Called the first time the sidebar is shown for a tab. */
|
||||
static void sidebar_create_webview(tab_info_t *tab) {
|
||||
if (tab == NULL || tab->sidebar_webview != NULL) return;
|
||||
/* Return the window_state_t for the currently focused window.
|
||||
* Falls back to the main window if g_active_ws is NULL. */
|
||||
static window_state_t *get_active_window_state(void) {
|
||||
if (g_active_ws != NULL) return g_active_ws;
|
||||
return &g_main_window;
|
||||
}
|
||||
|
||||
/* Return the window_state_t whose notebook matches the given widget.
|
||||
* Checks the main window first, then auxiliary windows. Returns NULL
|
||||
* if no match. */
|
||||
static window_state_t *window_state_for_notebook(GtkWidget *notebook) {
|
||||
if (notebook == NULL) return NULL;
|
||||
if (g_main_window.notebook == notebook) return &g_main_window;
|
||||
if (g_aux_windows != NULL) {
|
||||
for (guint i = 0; i < g_aux_windows->len; i++) {
|
||||
window_state_t *ws = &g_array_index(g_aux_windows,
|
||||
window_state_t, i);
|
||||
if (ws->notebook == notebook) return ws;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Lazily create the sidebar webview for a window. The webview shares
|
||||
* the same WebKitWebContext as the main webview so the sovereign://
|
||||
* scheme works. It is packed into the sidebar container (the left pane
|
||||
* of the window-level GtkPaned). Called the first time the sidebar is
|
||||
* shown for a window. */
|
||||
static void sidebar_create_webview(window_state_t *ws) {
|
||||
if (ws == NULL || ws->sidebar_webview != NULL) return;
|
||||
if (g_ctx == NULL) return;
|
||||
|
||||
tab->sidebar_webview = WEBKIT_WEB_VIEW(
|
||||
ws->sidebar_webview = WEBKIT_WEB_VIEW(
|
||||
webkit_web_view_new_with_context(g_ctx));
|
||||
|
||||
/* Match the main webview's settings so JS and the sovereign://
|
||||
* bridge work. */
|
||||
WebKitSettings *st = webkit_web_view_get_settings(tab->sidebar_webview);
|
||||
WebKitSettings *st = webkit_web_view_get_settings(ws->sidebar_webview);
|
||||
webkit_settings_set_enable_developer_extras(st, TRUE);
|
||||
webkit_settings_set_enable_javascript(st, TRUE);
|
||||
webkit_settings_set_allow_file_access_from_file_urls(st, TRUE);
|
||||
webkit_settings_set_allow_universal_access_from_file_urls(st, TRUE);
|
||||
|
||||
/* Inject window.nostr so the chat page's NIP-07 shim works. */
|
||||
nostr_inject_setup(tab->sidebar_webview);
|
||||
nostr_inject_setup(ws->sidebar_webview);
|
||||
|
||||
/* The sidebar webview should NOT create new windows/tabs — links
|
||||
* in the chat page should navigate within the sidebar, not open
|
||||
* new browser tabs. We let the default navigation happen inside
|
||||
* the sidebar webview. */
|
||||
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(tab->sidebar_webview), TRUE);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(tab->sidebar_webview), TRUE);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(ws->sidebar_webview), TRUE);
|
||||
gtk_widget_set_hexpand(GTK_WIDGET(ws->sidebar_webview), TRUE);
|
||||
|
||||
/* Pack into the sidebar container. */
|
||||
gtk_box_pack_start(GTK_BOX(tab->sidebar_container),
|
||||
GTK_WIDGET(tab->sidebar_webview), TRUE, TRUE, 0);
|
||||
gtk_box_pack_start(GTK_BOX(ws->sidebar_container),
|
||||
GTK_WIDGET(ws->sidebar_webview), TRUE, TRUE, 0);
|
||||
|
||||
/* Load the chat page. */
|
||||
webkit_web_view_load_uri(tab->sidebar_webview, AGENT_CHAT_URL_STR);
|
||||
webkit_web_view_load_uri(ws->sidebar_webview, AGENT_CHAT_URL_STR);
|
||||
}
|
||||
|
||||
void tab_manager_toggle_sidebar(void) {
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab == NULL || tab->paned == NULL) return;
|
||||
window_state_t *ws = get_active_window_state();
|
||||
if (ws == NULL || ws->paned == NULL) return;
|
||||
|
||||
if (tab->sidebar_visible) {
|
||||
if (ws->sidebar_visible) {
|
||||
/* Hide the sidebar. Set position to 0 so the sidebar gets no
|
||||
* space and the divider disappears. Keep the webview alive. */
|
||||
gtk_paned_set_position(GTK_PANED(tab->paned), 0);
|
||||
gtk_widget_hide(tab->sidebar_container);
|
||||
tab->sidebar_visible = FALSE;
|
||||
gtk_paned_set_position(GTK_PANED(ws->paned), 0);
|
||||
gtk_widget_hide(ws->sidebar_container);
|
||||
ws->sidebar_visible = FALSE;
|
||||
} else {
|
||||
/* Show the sidebar. Create the webview lazily on first show.
|
||||
* Set position to 280 so the sidebar gets space and the
|
||||
* divider appears (resizable). */
|
||||
if (tab->sidebar_webview == NULL) {
|
||||
sidebar_create_webview(tab);
|
||||
if (ws->sidebar_webview == NULL) {
|
||||
sidebar_create_webview(ws);
|
||||
}
|
||||
gtk_paned_set_position(GTK_PANED(tab->paned),
|
||||
gtk_paned_set_position(GTK_PANED(ws->paned),
|
||||
SIDEBAR_DEFAULT_WIDTH);
|
||||
gtk_widget_show_all(tab->sidebar_container);
|
||||
tab->sidebar_visible = TRUE;
|
||||
gtk_widget_show_all(ws->sidebar_container);
|
||||
ws->sidebar_visible = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
WebKitWebView *tab_manager_get_main_webview(void) {
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab == NULL) return NULL;
|
||||
/* Always return the main webview, never the sidebar. */
|
||||
/* Always return the main webview, never the sidebar. The sidebar
|
||||
* is now at the window level (outside the notebook), so
|
||||
* tab_manager_get_active() — which resolves by notebook page —
|
||||
* will never return the sidebar. */
|
||||
return tab->webview;
|
||||
}
|
||||
|
||||
gboolean tab_manager_sidebar_visible(void) {
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab == NULL) return FALSE;
|
||||
return tab->sidebar_visible;
|
||||
window_state_t *ws = get_active_window_state();
|
||||
if (ws == NULL) return FALSE;
|
||||
return ws->sidebar_visible;
|
||||
}
|
||||
|
||||
void tab_manager_hide_sidebar_after_show_all(void) {
|
||||
/* gtk_widget_show_all() on the main window recursively shows all
|
||||
* children, including the sidebar container. Re-hide it so the
|
||||
* sidebar stays hidden by default — it should only appear when
|
||||
* the user toggles it. */
|
||||
if (g_main_window.sidebar_container && !g_main_window.sidebar_visible) {
|
||||
gtk_widget_hide(g_main_window.sidebar_container);
|
||||
gtk_paned_set_position(GTK_PANED(g_main_window.paned), 0);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-15
@@ -30,15 +30,6 @@ typedef struct {
|
||||
GtkWidget *favicon; /* favicon image in tab_label */
|
||||
char current_url[TAB_URL_MAX];
|
||||
char title[TAB_TITLE_MAX];
|
||||
/* Agent chat sidebar (split view). The sidebar is a second webview
|
||||
* showing sovereign://agents/chat, shown alongside the main webview
|
||||
* in a GtkPaned. When sidebar_visible is FALSE, the sidebar
|
||||
* container is hidden but the webview is kept alive so chat state
|
||||
* persists across toggles. */
|
||||
WebKitWebView *sidebar_webview; /* agent chat sidebar (NULL if not yet created) */
|
||||
GtkWidget *sidebar_container; /* GtkScrolledWindow holding sidebar_webview */
|
||||
GtkWidget *paned; /* GtkPaned: sidebar | main webview */
|
||||
gboolean sidebar_visible; /* whether the sidebar is currently shown */
|
||||
} tab_info_t;
|
||||
|
||||
/*
|
||||
@@ -170,11 +161,23 @@ void tab_manager_set_avatar(const char *pubkey_hex);
|
||||
void tab_manager_toggle_inspector(void);
|
||||
|
||||
/*
|
||||
* Toggle the agent chat sidebar for the active tab. When showing, if
|
||||
* the sidebar webview has not been created yet, it is created (sharing
|
||||
* the same WebKitWebContext so sovereign:// works) and loads
|
||||
* sovereign://agents/chat. When hiding, the sidebar container is
|
||||
* hidden but the webview is kept alive so chat state persists.
|
||||
* Zoom control for the active tab's webview.
|
||||
* tab_manager_zoom_in() — multiply zoom by 1.1 (capped at 5.0)
|
||||
* tab_manager_zoom_out() — multiply zoom by 1/1.1 (floored at 0.25)
|
||||
* tab_manager_zoom_reset()— set zoom back to 1.0 (100%)
|
||||
* No-ops if there is no active tab or webview.
|
||||
*/
|
||||
void tab_manager_zoom_in(void);
|
||||
void tab_manager_zoom_out(void);
|
||||
void tab_manager_zoom_reset(void);
|
||||
|
||||
/*
|
||||
* Toggle the agent chat sidebar for the active window. The sidebar is
|
||||
* per-window (not per-tab), so it persists across tab switches within a
|
||||
* window. When showing, if the sidebar webview has not been created yet,
|
||||
* it is created (sharing the same WebKitWebContext so sovereign:// works)
|
||||
* and loads sovereign://agents/chat. When hiding, the sidebar container
|
||||
* is hidden but the webview is kept alive so chat state persists.
|
||||
*/
|
||||
void tab_manager_toggle_sidebar(void);
|
||||
|
||||
@@ -186,10 +189,17 @@ void tab_manager_toggle_sidebar(void);
|
||||
WebKitWebView *tab_manager_get_main_webview(void);
|
||||
|
||||
/*
|
||||
* Returns TRUE if the sidebar is currently visible for the active tab.
|
||||
* Returns TRUE if the sidebar is currently visible for the active window.
|
||||
*/
|
||||
gboolean tab_manager_sidebar_visible(void);
|
||||
|
||||
/*
|
||||
* Re-hide the sidebar container after gtk_widget_show_all(window).
|
||||
* Call this in main.c after show_all so the sidebar (which is packed
|
||||
* in the window-level GtkPaned) doesn't appear on startup.
|
||||
*/
|
||||
void tab_manager_hide_sidebar_after_show_all(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@
|
||||
#ifndef SOVEREIGN_BROWSER_VERSION_H
|
||||
#define SOVEREIGN_BROWSER_VERSION_H
|
||||
|
||||
#define SB_VERSION "v0.0.27"
|
||||
#define SB_VERSION "v0.0.28"
|
||||
#define SB_VERSION_MAJOR 0
|
||||
#define SB_VERSION_MINOR 0
|
||||
#define SB_VERSION_PATCH 27
|
||||
#define SB_VERSION_PATCH 28
|
||||
|
||||
#endif /* SOVEREIGN_BROWSER_VERSION_H */
|
||||
|
||||
+10
-1
@@ -367,6 +367,10 @@ function renderMessage(m, index) {
|
||||
|
||||
function renderMessages(msgs) {
|
||||
var c = document.getElementById('messages');
|
||||
/* Check if the user is scrolled to the bottom (or near it) before
|
||||
* re-rendering. If they've scrolled up to read history, don't
|
||||
* auto-scroll back down — that's annoying. */
|
||||
var wasAtBottom = (c.scrollHeight - c.scrollTop - c.clientHeight) < 50;
|
||||
var html = '';
|
||||
/* Only render user and assistant messages in the chat. Tool messages
|
||||
* (tool call results) can be very large (e.g. entire web pages) and
|
||||
@@ -409,7 +413,12 @@ function renderMessages(msgs) {
|
||||
});
|
||||
}
|
||||
});
|
||||
c.scrollTop = c.scrollHeight;
|
||||
/* Only auto-scroll to the bottom if the user was already at the
|
||||
* bottom. If they scrolled up to read history, preserve their
|
||||
* scroll position. */
|
||||
if (wasAtBottom) {
|
||||
c.scrollTop = c.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchMessages() {
|
||||
|
||||
@@ -108,6 +108,7 @@ function jsKeyToGdk(e) {
|
||||
if (k === '/') return 'slash';
|
||||
if (k === '-') return 'minus';
|
||||
if (k === '=') return 'equal';
|
||||
if (k === '+') return 'plus';
|
||||
}
|
||||
var special = {
|
||||
'Tab':'Tab','Enter':'Return','Escape':'Escape',
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>💛 Yellow Flowers Gallery 💛</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
background: linear-gradient(135deg, #fef9e7, #fff8dc, #fffacd);
|
||||
color: #4a3b1f;
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 2.5rem 1rem 1.5rem;
|
||||
background: linear-gradient(135deg, #ffd70022, #ffec8022);
|
||||
border-bottom: 3px solid #ffd70044;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
color: #b8860b;
|
||||
text-shadow: 1px 1px 3px rgba(255, 215, 0, 0.3);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
header p {
|
||||
font-size: 1.1rem;
|
||||
color: #8b7355;
|
||||
font-style: italic;
|
||||
}
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.15);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(255, 215, 0, 0.35);
|
||||
}
|
||||
.card img {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.card .info {
|
||||
padding: 1rem 1.2rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
.card h3 {
|
||||
color: #b8860b;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.card p {
|
||||
font-size: 0.9rem;
|
||||
color: #8b7355;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.card .number {
|
||||
display: inline-block;
|
||||
background: #ffd70033;
|
||||
color: #b8860b;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #8b7355;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🌻 Yellow Flowers Gallery 🌻</h1>
|
||||
<p>A collection of 12 beautiful yellow flowers</p>
|
||||
</header>
|
||||
<div class="gallery">
|
||||
<div class="card">
|
||||
<img src="sunflower.jpg" alt="Sunflower">
|
||||
<div class="info">
|
||||
<div class="number">1</div>
|
||||
<h3>Sunflower</h3>
|
||||
<p>Iconic tall flower that turns to face the sun. 🌻</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="daffodil.jpg" alt="Daffodil">
|
||||
<div class="info">
|
||||
<div class="number">2</div>
|
||||
<h3>Daffodil</h3>
|
||||
<p>A cheerful spring flower with a trumpet-shaped center.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="black_eyed_susan.jpg" alt="Black-eyed Susan">
|
||||
<div class="info">
|
||||
<div class="number">3</div>
|
||||
<h3>Black-eyed Susan</h3>
|
||||
<p>A daisy-like wildflower with a dark brown center.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="marigold.jpg" alt="Marigold">
|
||||
<div class="info">
|
||||
<div class="number">4</div>
|
||||
<h3>Marigold</h3>
|
||||
<p>A popular garden flower with layered, ruffly petals.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="yellow_tulip.jpg" alt="Yellow Tulip">
|
||||
<div class="info">
|
||||
<div class="number">5</div>
|
||||
<h3>Yellow Tulip</h3>
|
||||
<p>Elegant cup-shaped spring bloomer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="forsythia.jpg" alt="Forsythia">
|
||||
<div class="info">
|
||||
<div class="number">6</div>
|
||||
<h3>Forsythia</h3>
|
||||
<p>A shrub covered in bright yellow blossoms in early spring.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="yellow_rose.jpg" alt="Yellow Rose">
|
||||
<div class="info">
|
||||
<div class="number">7</div>
|
||||
<h3>Yellow Rose</h3>
|
||||
<p>A classic symbol of friendship and joy. 🌹</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="goldenrod.jpg" alt="Goldenrod">
|
||||
<div class="info">
|
||||
<div class="number">8</div>
|
||||
<h3>Goldenrod</h3>
|
||||
<p>Tall, fluffy spikes of yellow that bloom in late summer.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="yellow_daisy.jpg" alt="Yellow Daisy">
|
||||
<div class="info">
|
||||
<div class="number">9</div>
|
||||
<h3>Yellow Daisy</h3>
|
||||
<p>Simple, charming, and beloved worldwide.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="yarrow.jpg" alt="Yarrow">
|
||||
<div class="info">
|
||||
<div class="number">10</div>
|
||||
<h3>Yarrow</h3>
|
||||
<p>Flat-topped clusters of tiny yellow flowers, great for pollinators.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="primrose.jpg" alt="Primrose">
|
||||
<div class="info">
|
||||
<div class="number">11</div>
|
||||
<h3>Primrose</h3>
|
||||
<p>Soft, pale yellow flowers that bloom in early spring.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<img src="buttercup.jpg" alt="Buttercup">
|
||||
<div class="info">
|
||||
<div class="number">12</div>
|
||||
<h3>Buttercup</h3>
|
||||
<p>Glossy, shiny yellow petals often found in meadows.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p>Images sourced via Openverse (Creative Commons) · Created with 💛</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user