Commit Graph

8347 Commits

Author SHA1 Message Date
Adrià Arrufat
a5e7ec16be agent: unify interactive prompts and tool execution 2026-05-09 20:40:38 +02:00
Adrià Arrufat
1de98efbc4 agent: simplify model string ownership 2026-05-09 20:27:16 +02:00
Adrià Arrufat
5284abc3e7 agent: consolidate listModels logic into Agent.zig 2026-05-09 20:18:27 +02:00
Adrià Arrufat
938795ec8d agent: add --pick-model for interactive selection 2026-05-09 20:11:52 +02:00
Adrià Arrufat
16b83f5093 agent: add provider auto-detection and --no-llm flag 2026-05-09 19:52:42 +02:00
Adrià Arrufat
357033eb0c agent: add --list-models flag 2026-05-09 19:16:48 +02:00
Adrià Arrufat
5f2a46b311 browser: use explicit switch cases for tool actions 2026-05-09 18:27:51 +02:00
Adrià Arrufat
2b4e69b517 refactor: improve recorder and spinner state management
Moves recording state into Recorder and uses a tagged union for Spinner
state. Introduces TurnInput to simplify Agent turn processing.
2026-05-09 18:05:52 +02:00
Adrià Arrufat
7726ad4623 agent: avoid JSON round-trips in tool calls
Use `std.json.Value` directly for tool arguments instead of
stringifying and reparsing. This optimizes the hot replay path
in the agent and MCP server.
2026-05-09 17:53:41 +02:00
Adrià Arrufat
5b5e42cbaa refactor: optimize string building and unify text extraction 2026-05-09 17:47:28 +02:00
Navid EMAD
d7e283fed9 Don't propagate http_client.request errors from Fetch.init
When http_client.request fails synchronously (e.g. RobotsLayer returning
RobotsBlocked because robots.txt is already cached), Client.request
invokes our error_callback before returning the error. httpErrorCallback
rejects the promise and releases response._arena. Letting the error
propagate from Fetch.init also fires the `errdefer response.deinit`,
double-freeing the arena and corrupting the arena pool — eventually
surfacing as a malloc abort during teardown.

Fixes #2403.
2026-05-09 14:57:08 +02:00
Scott Taylor
b272b0e33c Add --disable-subframes CLI flag
Complementary to LP.setSubframeLoading (preceding commit): exposes
the same iframe-skip behavior as a CLI option that applies to all
sessions in the process. Useful for:

  * the 'fetch' subcommand (no CDP driver to call LP.setSubframeLoading)
  * 'serve' deployments where the operator wants iframes off by
    default for every connecting client (the LP method can still
    re-enable per-session if needed)
  * Playwright's chromium.connectOverCDP, which can't reliably issue
    custom CDP methods on Lightpanda today: BrowserContext.newCDPSession
    and Browser.newBrowserCDPSession both attach a new CRSession that
    collides with the STARTUP-session reuse from #2399, triggering a
    Playwright internal assertion. With --disable-subframes set on the
    server, Playwright doesn't need to issue any custom CDP \u2014 every
    session inherits subframes-off and the executionContextId churn
    from #2400 never trips.

Verified:

  serve --disable-subframes + plain puppeteer-core goto
    [ok] goto status=200 elapsed=6354ms frameAttached=0

  fetch --disable-subframes --dump html https://www.allbirds.com/...
    exit=0
    html bytes: 1021562
    title: <title>Allbirds Wool Runners, Men's | ...</title>
    iframe count in dumped html: 2  (still in DOM, just not loaded)

521/521 unit tests pass.
2026-05-08 17:12:54 -04:00
Scott Taylor
62d8bf7b0a Add LP.setSubframeLoading CDP method to skip iframe loading
Adds a Lightpanda-specific CDP method that lets drivers opt out of
subframe processing entirely:

  await client.send('LP.setSubframeLoading', { enabled: false });

When disabled, the HTML parser silently bypasses every <iframe> it
encounters: no child Frame is created, no document fetch is issued,
and no Page.frameAttached / Page.frameNavigated /
Runtime.executionContextCreated events are emitted. The driver only
sees the main frame's lifecycle.

Motivation: pages that load large numbers of analytics / pixel
iframes (Shopify storefronts, ad-heavy news sites) trigger #2400
\u2014 each subframe navigation re-registers the main frame's V8 context
under the child's frameId and invalidates the executionContextId the
driver had pinned for the main frame. Subsequent Runtime.evaluate
fails with 'Cannot find context with specified id' (Playwright
surfaces this as 'Execution context was destroyed', Puppeteer hangs
in IsolatedWorld.evaluate waiting for a 'context' event). The proper
fix is per-frame V8 inspector context groups (or per-frame
IsolatedWorld), discussed in #2400; this method gives drivers a
clean opt-in workaround in the meantime.

Mechanism: new bool field Session.subframe_loading_enabled (default
true). Frame.iframeAddedCallback short-circuits when false, marking
the iframe as _executed so the parser doesn't re-deliver it.

Verified against the puppeteer-core repro on
https://www.allbirds.com/products/mens-wool-runners (which
instantiates ~11 web-pixel iframes during initial render):

  baseline (subframe loading ON):
    page.title()  works (lucky timing) but server segfaults on
                  disconnect from the worker re-entrancy bug; iframes
                  do load and trigger the executionContextId churn

  with LP.setSubframeLoading(false):
    [opt-out] LP.setSubframeLoading reply: {}
    [ok] goto status=200 elapsed=6166ms
    [stats] frame_attached_events_seen=0
    [ok] page.title() = "Allbirds Wool Runners, Men's | ..."
    [ok] evaluate(1+1) = 2
    [ok] evaluate(document.title) = "Allbirds Wool Runners, Men's | ..."
    [ok] body.innerHTML.length = 923161

521/521 unit tests still pass.
2026-05-08 17:05:43 -04:00
Navid EMAD
1bc0ca6b8f add AGENTS.md and CLAUDE.md
Cross-vendor agent doc with the project-specific bits not derivable
from README or code: test filter env vars, the leak-detection test
invariant, the exact zig fmt CI command, and a pointer to mirror
neighboring file conventions for @import alias case and struct-init
inference. CLAUDE.md is a one-line @AGENTS.md import shim for Claude
Code, which doesn't read AGENTS.md natively.
2026-05-08 09:50:09 +02:00
Navid EMAD
d8b9391e33 xpath: drop internal references from comments
Strip mentions of the private gem and its internal paths from xpath
module docstrings, the conformance test header, and the dom dispatch
heuristic. Comments now describe behavior directly without pointing at
sources public readers can't access.
2026-05-08 08:58:07 +02:00
Navid EMAD
0b0a34c4a2 cdp: match closed set of axis names in isXPathQuery
The previous `::` heuristic accepted any identifier-like character before
`::`, which misrouted CSS pseudo-elements (`a::before`, `div::after`) to
the XPath evaluator. Walk back the run of [a-zA-Z-] characters and look
the candidate up in a StaticStringMap of the 13 XPath 1.0 named axes,
so only real axis names match.
2026-05-08 08:44:32 +02:00
Karl Seguin
9830da04d8 Naming convention fixes
Disable xpath_perf benchmark from test run as its quite verbose.
2026-05-08 08:44:31 +02:00
Navid EMAD
ce722c1f6e xpath: extend fast path to non-positional descendant queries
Generalizes 8733e33b's //tag[@id='x'] shape: tryFusedDescendantFastPath
handles any //tag[safe] or .//tag[safe] where the predicates are
non-positional boolean/node-set checks. Walks the search root's
descendants once in document order, applies node test + predicates
inline, no per-step materialization, no dedup.

5-9x on //div, //*, //*[@class='x'], //div[contains(...)]; ~25x on
(//div)[1] and count(//div) where the inner path is the shape.

Safety gate rejects predicates that could produce a number at the
top level (number, neg, arithmetic binop, numeric-returning fn-call)
and any predicate containing position()/last() anywhere. Conservative:
a nested sub-path's local positional predicate is rejected even though
it's scoped to its own axis.
2026-05-08 08:44:31 +02:00
Navid EMAD
c4c700f7ab xpath: id-lookup fast path + perf benchmark
evalPath recognizes //tag[@id='x'] and .//tag[@id='x'] (plus the
//*[@id='x'] wildcard) and serves them via frame.getElementByIdFromNode.
~100-150x speedup on ID lookups (3231us -> 22.6us for //*[@id='target']
in the new benchmark). Falls through to general path on any deviation
(extra step, extra predicate, non-eq, non-literal RHS).

Inherits the same duplicate-ID compromise selector/List.zig ships for
querySelector(All): the id-map stores only the first element per ID in
document order. Capybara/Selenium hot paths assume unique IDs.

tests/xpath/xpath_perf.html is the 13-query micro-benchmark used to
collect the numbers; batched console.warn output survives test runner
interleaving.
2026-05-08 08:44:31 +02:00
Navid EMAD
379664044e xpath: apply review correctness feedback
- Document.evaluate / XPathEvaluator.evaluate / XPathExpression.evaluate:
  result_type / requested_type now optional u16 defaulting to ANY_TYPE
  (matches WHATWG: `optional unsigned short type = 0`). context_node
  stays nullable with a fallback to the document — preserves the
  polyfill's behavior asserted by the `default_context` fixture
- ast.zig NodeTest: clarify that namespaced names (`prefix:*`,
  `prefix:local`) are stored verbatim and fall through to a literal
  match against the node name — consistent with the `namespace::` axis
  stub (decision #3). Adds a TODO for if the polyfill ever drops the
  stub
- Parser: cap recursive descent at depth 64 with new
  error.MaxDepthExceeded; depth tracked across parseExpr (parens,
  predicates, function args) and parseUnaryExpr (chained `-`). Two
  regression tests cover deep parenthesization and deep unary minus
2026-05-08 08:44:31 +02:00
Navid EMAD
94bcee6322 xpath: apply review style/convention feedback
- Rename Result.zig / Ast.zig / Functions.zig to snake_case (no
  top-level fields per Zig style guide)
- Restructure imports across xpath module: lib (std/lp) → relative
  (further → nearer) → aliases
- Move `frame` to last parameter on Evaluator.evaluate, searchAll,
  Functions.call, idFn (matches js bridge convention); call sites
  updated in webapi/XPath{Result,Expression}.zig and cdp/domains/dom.zig
- Local-pos style in XPathResult.iterateNext
2026-05-08 08:44:31 +02:00
Navid EMAD
e7c3e77c41 xpath: match CDATASection in text() node test
Per XPath 1.0 §5.7, the data model has no CDATASection node — CDATA
content is part of the text node value. The text() node test was only
matching DOM nodeType 3 (Text), silently excluding CDATA sections
(nodeType 4) parsed via DOMParser/XMLDocument and inline foreign
content like SVG with embedded scripts.
2026-05-08 08:44:31 +02:00
Navid EMAD
a4abbb6d13 xpath: cache attribute axis nodes via frame lookup
The attribute axis was calling Entry.toAttribute on every visit,
materializing fresh *Attribute structs (plus duped name/value strings)
into page-lifetime storage. Repeated XPath queries — the Capybara/
Selenium polling pattern this PR targets — accumulated unbounded
copies for the same DOM entries. Route through frame._attribute_lookup
so each Entry resolves to a single cached *Attribute, matching
List.getAttribute and NamedNodeMap.getAtIndex.
2026-05-08 08:44:30 +02:00
Navid EMAD
33714a4dfd cdp: tighten isXPathQuery '::' heuristic
A bare indexOf("::") matched CSS pseudo-elements (a::before) and
attribute values containing '::' ([data-x="x::y"]), misrouting them
to the XPath evaluator. Require an axis-name shape ([a-zA-Z-])
immediately before '::' so only real axis specifiers like
descendant::p are dispatched to XPath.
2026-05-08 08:44:30 +02:00
Navid EMAD
0fcd47e1e1 xpath: dupe expression into arena before parsing
The Parser borrows string slices from its input for AST literals,
names, and var refs. Without duping, the AST holds slices into the JS
call_arena, which is reset when the top-level call returns — every
subsequent evaluate() of a cached XPathExpression would dereference
freed memory.
2026-05-08 08:44:30 +02:00
Navid EMAD
290fc7a9df xpath: implement XPath 1.0 evaluator
Ports the capybara-lightpanda XPath 1.0 polyfill into Lightpanda.
Exposes the WHATWG Document.evaluate / XPathResult / XPathEvaluator
/ XPathExpression surface and routes CDP DOM.performSearch XPath
queries through the new evaluator. The capybara-lightpanda gem can
drop its ~700-line JS polyfill in the next release.

New module src/browser/xpath/ (Tokenizer, Parser, Ast, Evaluator,
Functions, Result). New webapi types XPathResult,
XPathExpression, XPathEvaluator. Coverage and stubs match the
polyfill 1:1 — see capybara-lightpanda/XPATH_COMPLIANCE.md for
the full spec.

Tests: 91-case conformance + result-API + evaluator-API + CDP
fixtures, plus the engine's Zig unit suite (601/601 pass).
2026-05-08 08:44:30 +02:00
Adrià Arrufat
372c0a12a2 agent: simplify logic and cleanup comments 2026-05-08 08:23:19 +02:00
Adrià Arrufat
8e1bf69b57 Merge branch 'main' into agent 2026-05-08 08:08:39 +02:00
Karl Seguin
c633617544 Add timeslice to scheduler
Give scheduler a 500ms timeslice to run per queue (high/low priority).

A site can load hundreds of timeouts to all execute at the same time. These
can be relatively expensive (e.g. lots of calls directly or indirectly to
getBoundingClientRect). As-is, the scheduler drains its queue to completion and
other timeouts, like --wait-ms can't do what they're meant to do. By adding
timeslice, we prevent many tasks all scheduled for the same time to go
unchecked.

I was initially planning on putting this higher in runMacrotasks, but that could
lead to starvation, i.e. if the first context used up all the time. Having it
per context is more fair, at the cost of running 500ms * context. But, (a) the
number of context we allow is fixed and (b) the reality is that most sites have
few contexts and normally only the first one is doing anything interesting.
2026-05-08 10:34:47 +08:00
Karl Seguin
6e9156a86f Merge pull request #2389 from lightpanda-io/interception-layer-on-serve
InterceptionLayer only on `.serve` mode
2026-05-08 06:51:03 +08:00
Karl Seguin
97f95a992e Merge pull request #2388 from lightpanda-io/cache-clear
Add Clear to Cache and FsCache
2026-05-08 06:50:35 +08:00
Karl Seguin
ffee9e67ce Merge pull request #2377 from lightpanda-io/page_dom_version
Track DOM version on the page
2026-05-08 06:45:16 +08:00
Karl Seguin
b8674cd252 Merge pull request #2379 from lightpanda-io/setter_and_static_arity
Give setters an arity of 1
2026-05-08 06:45:00 +08:00
Adrià Arrufat
77fc818976 refactor: optimize tool calls and improve script healing 2026-05-07 20:30:49 +02:00
Adrià Arrufat
622d408d03 mcp: add verification to script_step 2026-05-07 20:18:37 +02:00
Adrià Arrufat
c6ccd83ac4 mcp: add pandascript recording and self-healing tools
Adds tools to record sessions and heal scripts over MCP. Refactors
shared logic to `script.zig` and adds a TTY spinner for the agent.
2026-05-07 20:11:40 +02:00
Muki Kiboigo
66293ebc99 only enable InterceptionLayer on .serve mode 2026-05-07 09:03:40 -07:00
Muki Kiboigo
14e1f1bcf6 add clear fn to Cache and FsCache 2026-05-07 09:00:33 -07:00
Pierre Tachoire
1131cb09ff Merge pull request #2387 from lightpanda-io/zig-fmt-ci
Fix zig fmt step in CI
2026-05-07 17:49:15 +02:00
Adrià Arrufat
7bf69a9a34 agent: remove integrated mcp server
Removes the `--mcp` flag and the internal `task` tool from the agent.
Users should use `lightpanda mcp` for external agent integrations.
2026-05-07 17:12:46 +02:00
Muki Kiboigo
e5e5f78928 fix formatting on EventTarget.zig 2026-05-07 08:10:43 -07:00
Muki Kiboigo
f54ee13b32 fix zig fmt step in CI 2026-05-07 08:09:33 -07:00
Karl Seguin
fbb8126cc4 Merge pull request #2378 from lightpanda-io/blank_navigation
Protect assertion when reload from about:blank
2026-05-07 23:00:33 +08:00
Karl Seguin
823a7c480d Merge pull request #2380 from lightpanda-io/illegal_instructor_capture_name
On Illegal Constructor, try to capture name (for logs)
2026-05-07 23:00:13 +08:00
Adrià Arrufat
16e61e342e terminal: simplify spinner access and organize ansi codes 2026-05-07 16:38:11 +02:00
Karl Seguin
307e016aa5 optimize elementFromPoint
This adds 3 optimizations to elementFromPoint (which we've seen some sites can
call frequently and, on those sites, it dominates benchmarks).

1. Enable the visibility cache. Might seen like pure overhead, since every node
   is visited once. But a node's visibility check includes its ancestors.

2. Instead of using getBoundingClientRectForVisible, use getElementDimensions.
   getBoundingClientRect has no context, so it has to calculate the element's
   x and y by walking [part of] the document. But in elementFromPoint we're
   already walking the DOM in order, so we have all the context we need for x
   and y.

3. Exit early. Once we're past the target y, no element can match.
2026-05-07 22:37:28 +08:00
Adrià Arrufat
855ab768fa terminal: show tool execution trace in spinner mode 2026-05-07 16:28:16 +02:00
Karl Seguin
d4a210c5f1 Merge pull request #2385 from lightpanda-io/avoid_script_error_double_free
On error, don't free headers
2026-05-07 22:11:58 +08:00
Adrià Arrufat
a254e4041a agent: refine verbosity levels and tool output UI 2026-05-07 16:11:28 +02:00
Adrià Arrufat
54d873e007 agent: add proactive login and silence tool errors 2026-05-07 15:51:18 +02:00