Commit Graph
9542 Commits
Author SHA1 Message Date
Karl Seguin 67927b9bde Add PDF support
adds --dump screen.pdf and CDP's Page.printToPDF (along with IO.read and
IO.read). Builds ontop of https://github.com/lightpanda-io/browser/pull/3231

This generates a text-based PDF, not just our PNG placed into a PDF. The biggest
change to the existing (isolated) code was adding href information so that links
are actually clickable.

Like 3231 the hard parts are all Claude but isolated. The one place it diverged
from the plan was NOT using the subsetter rust crate for trimming the size of
the embedded font. It felt that the dependencies were too much (e.g. a second
version of skrifa, ...). One subsetter was removed, the pdf rendering, while
still a blackbox, was moved from Rust to Zig (pdf.zig), which is kind of nice.
2026-08-28 12:53:36 +08:00
Muki Kiboigo bc199bdbc8 assert blob response type is octet-stream 2026-08-27 19:11:55 -07:00
Muki Kiboigo dcbf1e0a8d add application/octet-stream to Mime 2026-08-27 19:11:43 -07:00
Karl Seguin fa1b9ab03d Merge pull request #3316 from staylor/fix/mutation-observer-runaway
stability: disconnect runaway mutation observers
2026-08-28 10:06:45 +08:00
Karl Seguin 0f91a0f643 Simplify mutation limit tracking
hit_limit_this_session is more explicit as a local.
2026-08-28 09:44:30 +08:00
Karl Seguin 3ee9bb0ab0 Merge pull request #3315 from staylor/fix/css-property-value-release
css: reuse storage for updated property values
2026-08-28 09:04:44 +08:00
Karl Seguin b584b5f376 Merge pull request #3283 from staylor/fix/immediate-meta-refresh-navigation
Handle immediate meta refresh navigation
2026-08-28 07:44:42 +08:00
Karl Seguin 2670af68ad Merge pull request #3308 from lightpanda-io/screenshot-improvements
tweak: improve screenshot code
2026-08-28 07:44:25 +08:00
Adrià Arrufat 97f91b3134 Merge pull request #3301 from lightpanda-io/screenshot-tool
tools: add a screenshot tool
2026-08-28 07:35:25 +08:00
Karl Seguin 72d668f973 Merge pull request #3319 from lightpanda-io/no-intfromfloat
zig: replace the deprecated @intFromFloat with @trunc
2026-08-28 07:11:25 +08:00
Karl Seguin 4add53a924 zig fmt 2026-08-28 07:05:03 +08:00
Karl Seguin ddcac4ee60 Merge pull request #3305 from lightpanda-io/disable-iframes-and-workers
breaking: disable worker and iframe loading by default
2026-08-28 07:04:06 +08:00
Karl Seguin b943870f2c Merge pull request #3306 from lightpanda-io/transfer-submit-double-free
http: fix possible double-free on submit error
2026-08-28 07:03:32 +08:00
Karl Seguin 2840e5c1d8 Merge pull request #3299 from lightpanda-io/search-params-url-sync
webapi: searchParams keeps a linked URL in sync
2026-08-28 07:03:04 +08:00
Karl Seguin aea04ce85c Merge pull request #3298 from lightpanda-io/wpt-text-editing-events
webapi: edit key support in input/textarea
2026-08-28 07:02:48 +08:00
Karl Seguin 9abfd8eca0 Merge pull request #3297 from lightpanda-io/fix-with-base-attribute
fix: --with-base now uses proper attribute
2026-08-28 07:02:30 +08:00
Karl Seguin 1ec1fb4ac4 log on ignored meta refresh 2026-08-28 07:01:35 +08:00
Adrià Arrufat da8aa307a6 zig: replace the deprecated @intFromFloat with @trunc
Zig 0.16 forwards the result type through the unary float builtins, so
@trunc produces the integer directly and @intFromFloat is deprecated as
redundant with it. Every site was a truncating cast, or already held an
integral value, so the behaviour is unchanged.
2026-08-27 22:39:30 +02:00
Scott Taylor 105ba20b05 stability: disconnect runaway mutation observers
Track delivery sessions that repeatedly hit the recursion cap without draining. Disconnect the frame's observers after 100 such sessions so timer-rearmed mutation cascades cannot consume memory indefinitely.

Assisted-By: devx/1a7af220-8162-4dc8-b16c-088baab97324
2026-08-27 16:11:50 -04:00
Scott Taylor 526f4a4718 css: reuse storage for updated property values
Allocate mutable CSS property values from the factory slab instead of the frame-lifetime arena. Replacing or removing a property now returns its owned value storage for reuse without attempting to free interned strings.

Assisted-By: devx/1a7af220-8162-4dc8-b16c-088baab97324
2026-08-27 16:11:50 -04:00
Adrià Arrufat ce6f87f3c2 Merge pull request #3309 from lightpanda-io/mcp-http-single-buffer
mcp: release a large HTTP response buffer after the reply
2026-08-27 21:45:21 +02:00
Adrià Arrufat e6165d23bb mcp: release a large HTTP response buffer after the reply
The transport already drops its staging buffer past 256 KB, but the
connection buffer that receives the copy lives for the whole keep-alive
connection and was only ever cleared, so one screenshot pinned its
capacity per open connection. Apply the same threshold there.

Claude-Session: https://claude.ai/code/session_01S9t1TX3vTKunaXdnatbcjB
2026-08-27 21:28:32 +02:00
Adrià Arrufat d10029fb19 Merge branch 'main' into screenshot-tool 2026-08-27 21:13:36 +02:00
Adrià Arrufat f5e89abd03 Merge branch 'screenshot-tool' of https://github.com/lightpanda-io/browser into screenshot-tool 2026-08-27 21:11:03 +02:00
Adrià Arrufat a1c4a67c08 Merge pull request #3307 from lightpanda-io/agent-terminal-tests
agent: run the Terminal tests
2026-08-27 16:59:26 +02:00
Adrià Arrufat b8e619b8b3 Merge pull request #3313 from lightpanda-io/agent-tool-session
agent: fold the browser quartet onto lp.ToolSession
2026-08-27 16:58:02 +02:00
Adrià Arrufat 51dc7872b1 tools: report a fixed strip's own height for a saved screenshot
The renderer returns the content height; a viewport shot of a short
page said 1920x184 for a 1920x1080 file.
2026-08-27 16:49:18 +02:00
Adrià Arrufat 7318fbfcec agent: use zenai's hasImage
zenai exports it now; drop the local copy and re-pin.
2026-08-27 16:49:18 +02:00
Adrià Arrufat 051a70cfdd screenshot: fit() owns the inline sizing; prune() owns image expiry
The renderer knows that layout reflows to the width and that height 0
means the whole content, so the bounding rule moves next to measure()
and only measures when the strip isn't already fixed: the default
viewport shot no longer pays a second layout pass. The limits are
constants spliced into the tool description, not a caller-tunable that
nothing tuned. expireImages runs inside prune so there is one end-of-
turn hook, and re-homes a stripped result instead of casting away const.
2026-08-27 16:49:18 +02:00
Adrià Arrufat d34d3a0f28 agent: bound what a screenshot costs the model
An inline screenshot is re-sent on every request for as long as it sits
in history, and a full-page render could reach 1920x16384. Inline images
are now rendered at most 1280 wide and 4096 tall (measured after the
reflow, so short pages aren't padded), files keep full size; the
conversation keeps only the newest two images, older tool results keep
their text with a note; the MCP transport releases its buffer after a
large response.
2026-08-27 16:49:18 +02:00
Adrià Arrufat cadf8f9f64 agent: let the model see a REPL /screenshot; fold the tool-result adapters
The slash path's result has two consumers: the terminal, which can't
show an image, and the conversation, which can. Opt in when a model is
attached and forward the image through the same adapter the model-driven
path uses, and stop mapping a failed adapter to a text-only success.
MCP's ImageContent and CallToolResult take their payload type like
TextContent does; resolveScope reuses resolveTarget; needsLocator folds
into replayRequires.
2026-08-27 16:49:18 +02:00
Adrià Arrufat 30669acb0e agent: hand the model inline screenshots
zenai tool results now carry image parts (lightpanda-io/zenai#12, #13),
so the model-driven tool path opts into inline images: a screenshot
without `path` reaches the model as text plus the PNG, on every backend.
The slash-command path still needs `path`, its result goes to the
terminal.
2026-08-27 16:49:18 +02:00
Adrià Arrufat 4a516027d3 tools: declare image support at the call site, not in each consumer
Callers pass CallOpts.inline_image; execScreenshot rejects a path-less
call before navigating or rendering, which removes the per-consumer
guards and covers the model-driven tool path that had none. MCP image
content is a protocol type, the screenshot recording rule joins the
recorder's replayRequires predicate, and the viewport-to-Opts mapping,
node-scope ladder and save-path helpers are shared instead of copied.
2026-08-27 16:49:18 +02:00
Adrià Arrufat 9239e1eadd tools: add a screenshot tool
The PNG renderer was reachable from CDP and fetch --dump png only. The
tool renders the page or one node; with `path` it writes the file and
returns its location (agent, PandaScript, MCP), without it MCP returns
the image inline as base64 content. ToolResult carries the prepared
image so the transport streams it; the agent and script runtime reject
the inline form since their tool results are text. An inline screenshot
is not recorded, as it has no replayable form.
2026-08-27 16:49:18 +02:00
Adrià Arrufat 8e122fcb1e Merge pull request #3302 from lightpanda-io/readme-agent-providers
readme: list all agent providers and gateway examples
2026-08-27 16:48:10 +02:00
Adrià Arrufat 0865f4e337 agent: fold the browser quartet onto lp.ToolSession
Agent held the same Browser/Session/Notification/registry fields and
teardown order as mcp/Server.zig, plus its own "enableConsoleCapture
after every newSession" for /reset. ToolSession.restartSession() owns
that now; init is written in terms of it.

Claude-Session: https://claude.ai/code/session_01M6WGk8wZSE28efFQkYT9SK
2026-08-27 16:40:53 +02:00
Adrià Arrufat dadbae121c ToolSession: test the shared-thread contract
Two sessions on one thread, each entered only around its own use, with
JS state proving the isolates are separate and a balanced park/enter/
deinit teardown. Pins what the C API relies on without Server in the loop.

Claude-Session: https://claude.ai/code/session_01M6WGk8wZSE28efFQkYT9SK
2026-08-27 16:35:11 +02:00
Adrià Arrufat d8893f95d7 mcp: bracket every isolate use, drop park_isolates
stdio kept the isolate permanently entered while HTTP parked it between
requests, so a handler touching page JS without enterIsolate passed the
stdio-mode tests and only failed under --port. Bracket unconditionally:
stdio is the one-isolate case of the same discipline.

The flag survives only as `multi_session`, which is what it always gated:
the session tools need a transport that routes by id.

Claude-Session: https://claude.ai/code/session_01M6WGk8wZSE28efFQkYT9SK
2026-08-27 16:30:49 +02:00
Adrià Arrufat 071eca1f3f mcp: fold Server.Session onto lp.ToolSession
Extract the Browser+Session+Notification+node Registry quartet, with its
ordering-sensitive init/teardown and isolate enter/exit, into
src/ToolSession.zig. mcp/Server.zig's sessions map now holds
*lp.ToolSession directly; the session id lives only as the map key.

Claude-Session: https://claude.ai/code/session_01M6WGk8wZSE28efFQkYT9SK
2026-08-27 16:27:27 +02:00
Adrià Arrufat 9a3585079d cdp: the label index is required, not optional
A fresh index costs the same single walk as the scan it replaced, so
the null path bought nothing and left two lookups to maintain.
2026-08-27 16:10:22 +02:00
Adrià Arrufat 6bff402a24 interactive: take roles and names from AXNode
findElement and interactiveElements had their own role table and name
algorithm, which knew nothing of <label for>, wrapping labels or
aria-labelledby, so a control the tree showed as "Email address" was
unfindable by that name. Both now come from AXNode, with the element's
text as the name of last resort for elements that have no role (a div
interactive only by its listener or tabindex). getName takes an
optional label index so a walk resolves <label for> with one document
scan; the semantic tree walk uses it too.
2026-08-27 16:01:12 +02:00
Adrià Arrufat a8340a10b7 tools: report a fixed strip's own height for a saved screenshot
The renderer returns the content height; a viewport shot of a short
page said 1920x184 for a 1920x1080 file.
2026-08-27 15:08:24 +02:00
Adrià Arrufat e70b316aea agent: use zenai's hasImage
zenai exports it now; drop the local copy and re-pin.
2026-08-27 15:03:19 +02:00
Adrià Arrufat 45744407af screenshot: fit() owns the inline sizing; prune() owns image expiry
The renderer knows that layout reflows to the width and that height 0
means the whole content, so the bounding rule moves next to measure()
and only measures when the strip isn't already fixed: the default
viewport shot no longer pays a second layout pass. The limits are
constants spliced into the tool description, not a caller-tunable that
nothing tuned. expireImages runs inside prune so there is one end-of-
turn hook, and re-homes a stripped result instead of casting away const.
2026-08-27 15:03:19 +02:00
Adrià Arrufat c693979468 agent: bound what a screenshot costs the model
An inline screenshot is re-sent on every request for as long as it sits
in history, and a full-page render could reach 1920x16384. Inline images
are now rendered at most 1280 wide and 4096 tall (measured after the
reflow, so short pages aren't padded), files keep full size; the
conversation keeps only the newest two images, older tool results keep
their text with a note; the MCP transport releases its buffer after a
large response.
2026-08-27 15:03:19 +02:00
Adrià Arrufat cff32f6c04 agent: let the model see a REPL /screenshot; fold the tool-result adapters
The slash path's result has two consumers: the terminal, which can't
show an image, and the conversation, which can. Opt in when a model is
attached and forward the image through the same adapter the model-driven
path uses, and stop mapping a failed adapter to a text-only success.
MCP's ImageContent and CallToolResult take their payload type like
TextContent does; resolveScope reuses resolveTarget; needsLocator folds
into replayRequires.
2026-08-27 15:03:19 +02:00
Adrià Arrufat e6203ff1fe agent: hand the model inline screenshots
zenai tool results now carry image parts (lightpanda-io/zenai#12, #13),
so the model-driven tool path opts into inline images: a screenshot
without `path` reaches the model as text plus the PNG, on every backend.
The slash-command path still needs `path`, its result goes to the
terminal.
2026-08-27 15:03:19 +02:00
Adrià Arrufat 193f15558c tools: declare image support at the call site, not in each consumer
Callers pass CallOpts.inline_image; execScreenshot rejects a path-less
call before navigating or rendering, which removes the per-consumer
guards and covers the model-driven tool path that had none. MCP image
content is a protocol type, the screenshot recording rule joins the
recorder's replayRequires predicate, and the viewport-to-Opts mapping,
node-scope ladder and save-path helpers are shared instead of copied.
2026-08-27 15:03:19 +02:00
Adrià Arrufat 56c6a0a7ab tools: add a screenshot tool
The PNG renderer was reachable from CDP and fetch --dump png only. The
tool renders the page or one node; with `path` it writes the file and
returns its location (agent, PandaScript, MCP), without it MCP returns
the image inline as base64 content. ToolResult carries the prepared
image so the transport streams it; the agent and script runtime reject
the inline form since their tool results are text. An inline screenshot
is not recorded, as it has no replayable form.
2026-08-27 15:03:19 +02:00
Karl Seguin a0e21b9aeb tweak: improve screenshot code
Remove singleton and give each Browser a screenshot.Renderer.

Also, add tests to make sure the Rust and Zig structures/constants match (this
has proven useful with zig-v8-fork).
2026-08-27 19:22:04 +08:00