Skip to content

Wire browser cookies, storage, tabs, scripts, styles, highlight#27

Draft
mvbmir wants to merge 4 commits into
mvbmir/wire-browser-dispatcherfrom
mvbmir/wire-browser-dispatcher-extra
Draft

Wire browser cookies, storage, tabs, scripts, styles, highlight#27
mvbmir wants to merge 4 commits into
mvbmir/wire-browser-dispatcherfrom
mvbmir/wire-browser-dispatcher-extra

Conversation

@mvbmir
Copy link
Copy Markdown
Owner

@mvbmir mvbmir commented Apr 17, 2026

Stacked on #25 (Wire pane/browser control dispatcher). Adds tiers 3, 4, and evaluate_javascript infra on top of the snapshot/action/console baseline.

T3: cookies, storage, state (login persistence)

Persistent cookie store enabled by default. `NetworkSession::cookie_manager` now writes to `$XDG_DATA_HOME/limux/cookies.sqlite`, so logins survive limux relaunches without any explicit save/load — all cookies including HttpOnly persist transparently.

New methods:

  • `browser.cookies.get` / `browser.cookies.clear` — script-visible cookies only (HttpOnly excluded by design, sqlite handles those)
  • `browser.storage.local.{get, set, clear}`
  • `browser.storage.session.{get, set, clear}`
  • `browser.state.save` — bundle visible cookies + both storages as JSON for export
  • `browser.state.load` — apply a saved bundle

T4: tabs, scripts, styles, highlight

Tab management (fills the gap between `browser.open_split` which creates sibling-pane surfaces and the tab-strip inside each pane):

  • `browser.tab.list` — list tabs in a pane
  • `browser.tab.new` — smart default: stacks into an existing browser pane if one exists, falls back to any non-caller pane, splits caller's pane if only one pane exists. Caller's terminal stays visible
  • `browser.tab.switch` — activate a tab by surface id
  • `browser.tab.close` — close a tab by surface id

Persistent injection via webkit's `UserContentManager`:

  • `browser.addscript` — one-shot JS (alias for browser.eval without the JSON wrapping contract)
  • `browser.addinitscript` — persistent JS, re-injected on every top-frame navigation
  • `browser.addstyle` — persistent CSS

Debug helper:

  • `browser.highlight` — 3px magenta outline pulse, configurable duration

T5: async-eval infra

`evaluate_javascript` doesn't await Promise returns and surfaces "Unsupported result type" for async IIFEs. Switched to `call_async_javascript_function` so webkit awaits the script before returning the jsc::Value. Trailing semicolons stripped before wrapping as `return (...);` to avoid spurious SyntaxErrors.

This is infra only — it unblocks future high-level primitives (`navigate_and_wait`, `click_and_await_nav`, `fill_and_submit`). Those flows can't complete inside a single evaluate roundtrip because `location.href` and `el.click()` both destroy the async JS context mid-await, so they'll arrive as Rust-side orchestrators using webkit's `load-changed` signal.

Test plan

  • `browser.cookies.get` lists cookies after login flow
  • `~/.local/share/limux/cookies.sqlite` exists and grows after login
  • Logout + relaunch + reopen → still logged in
  • `browser.storage.local.set/get/clear` round-trip
  • `browser.state.save` dumps full bundle; `browser.state.load` applies it
  • `browser.tab.new` stacks in existing browser pane (doesn't clobber caller terminal)
  • `browser.tab.switch` activates a surface by id
  • `browser.tab.close` removes a tab by id
  • `browser.addstyle` applies CSS that persists across navigation
  • `browser.addinitscript` injects JS that runs on every fresh page load
  • `browser.highlight` visibly pulses an element outline
  • `browser.snapshot --since ` returns `{unchanged: true}` when DOM didn't change

mvbmir added 3 commits April 18, 2026 02:10
Adds tier 3 of the browser dispatcher: login persistence via a
persistent cookie store plus programmatic session export/import.

Persistent cookies:
  The NetworkSession's CookieManager is now backed by a sqlite file at
  $XDG_DATA_HOME/limux/cookies.sqlite. All cookies (including HttpOnly)
  are persisted automatically, so logins survive limux relaunches
  without any explicit save/load — open a site, log in, relaunch,
  you're still logged in.

New methods wired via build_browser_script:

  browser.cookies.get         — list same-origin script-visible cookies
  browser.cookies.clear       — expire by name or all

  browser.storage.local.get   — fetch one key or all items
  browser.storage.local.set
  browser.storage.local.clear
  browser.storage.session.{get, set, clear}

  browser.state.save          — bundle visible cookies + localStorage +
                                sessionStorage as JSON for export
  browser.state.load          — apply a bundle back to the current page

HttpOnly cookies are intentionally invisible to document.cookie (that's
the point of the flag), so cookies.get and state.save only surface
script-visible cookies. This is fine for the intended use case: the
sqlite store handles the authentication tokens transparently, while
save/load covers programmatic session setup for testing scenarios.
Adds tier 4 of the browser dispatcher: tab management, persistent
script/style injection, and visual highlight.

Tab management:

  browser.tab.list       — list every surface in a pane
  browser.tab.new        — open a browser tab inside an existing pane
  browser.tab.switch     — activate a specific surface by id
  browser.tab.close      — close a specific surface by id

Where browser.open_split creates the browser in a sibling pane (or
splits to make one), browser.tab.new stays in the caller's pane —
useful for stacking multiple browser tabs in one workspace slot.

Injection:

  browser.addscript      — run arbitrary JS in the current page (alias
                           for browser.eval without the JSON-reply
                           wrapping contract)
  browser.addinitscript  — install a UserScript that webkit re-injects
                           on every top-frame navigation; use for
                           ref taggers, console shims, auth bootstraps
                           that must survive SPA route changes
  browser.addstyle       — install a UserStyleSheet that persists
                           across navigations

Both injection variants use webkit6::UserContentManager with
InjectedFrames::TopFrame so iframes stay untouched (consistent with
the built-in limux init script's scope).

Visual:

  browser.highlight      — pulse a 3px magenta outline on the target
                           element for N ms; used for debugging ref
                           resolution ("is @e7 really the button I
                           think it is?")

Iframe scope methods (browser.frame.main / browser.frame.select) are
deferred — they require per-surface scope tracking that isn't needed
by any current workflow.
evaluate_javascript can't await Promise return values — it returns
"Unsupported result type" for any async IIFE. Switching to
call_async_javascript_function makes webkit auto-await the script
before returning the jsc::Value, so async helpers are now viable.

Trailing semicolons on expression-style scripts are stripped before
wrapping, otherwise `(() => ...)();` would become `return (...;);` and
SyntaxError at the JS side.

Result conversion falls back to to_json when to_str yields an empty
string or "[object Promise]", so non-string return values surface
through the bridge cleanly.

No user-facing methods change in this commit. This is infra for the
upcoming navigate_and_wait / click_and_await_nav / fill_and_submit
primitives, which will arrive once they're reimplemented as Rust-side
orchestrators (location.href and el.click() both destroy the async
JS context mid-await, so those flows have to coordinate via the
load-changed signal rather than a single evaluate roundtrip).
@mvbmir mvbmir force-pushed the mvbmir/wire-browser-dispatcher-extra branch from a544268 to 399dd40 Compare April 17, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant