This fixes a bug in MCP where interactive elements were not assigned
a backendNodeId, preventing agents from clicking or filling them. Also
extracts link collection to a shared browser module.
Address review feedback from @karlseguin:
1. Use Form.getElements() instead of manual TreeWalker for field
collection. This reuses NodeLive(.form) which handles fields
outside the <form> via the form="id" attribute per spec.
2. Add disabled detection: checks both the element's disabled
attribute and ancestor <fieldset disabled> (with first-legend
exemption per spec). Fields are flagged rather than excluded -
agents need visibility into disabled state.
3. allocator is now the first parameter in collectForms/helpers.
4. handleDetectForms returns InvalidParams on bad input instead
of silently swallowing parse errors.
5. Added tests for disabled fields, disabled fieldsets, and
external form fields via form="id".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This is done for a couple reasons. The first is just to have things a little
more self-contained for eventually supporting more advanced "wait" logic, e.g.
waiting for a selector.
The other is to provide callers with more fine-grained controlled. Specifically
the ability to manually "tick", so that they can [presumably] do something
after every tick. This is needed by the test runner to support more advanced
cases (cases that need to test beyond 'load') and it also improves (and fixes
potential use-after-free, the lp.waitForSelector)
Add a detectForms MCP tool and lp.detectForms CDP command that return
structured form metadata from the current page. Each form includes its
action URL, HTTP method, and fields with names, types, required status,
values, select options, and backendNodeIds for use with the fill tool.
This lets AI agents discover and fill forms in a single step instead of
calling interactiveElements, filtering for form fields, and guessing
which fields belong to which form.
New files:
- src/browser/forms.zig: FormInfo/FormField structs, collectForms()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After click, fill, and scroll actions, return the current page URL
and title instead of static success messages. This gives AI agents
immediate feedback about the page state after an action, matching
the pattern already used by waitForSelector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add three test cases covering:
- Immediate match on an already-present element
- Polling match on an element added after a 200ms setTimeout delay
- Timeout error on a non-existent element with a short timeout
Add mcp_wait_for_selector.html test fixture that injects a #delayed
element after 200ms via setTimeout for the polling test.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a waitForSelector tool to the MCP server that polls for a CSS
selector match with a configurable timeout (default 5000ms). Returns the
backendNodeId of the matched element for use with click/fill tools.
The tool runs the session event loop between selector checks, so
dynamically-created elements are found as they appear from JS execution
or network responses.
Small tweaks to https://github.com/lightpanda-io/browser/pull/1896
Improve the wait ergonomics with an Option with default parameter. Revert
page pointer logic to original (don't think that change was necessary).
Add `--wait_until` and `--wait_ms` CLI arguments to configure session wait behavior. Updates `Session.wait` to evaluate specific page load states (`load`, `domcontentloaded`, `networkidle`, `fixed`) before completing the wait loop.
The introduction of frames means that data is no longer tied to a specific Page
or Context. 255b9a91cc introduced Origins for
v8 values shared across frames of the same origin. The commit highlighted the
lifetime mismatched that we now have with data that can outlive 1 frame. A
specific issue with that commit was the finalizers were still Context-owned.
But like any other piece of data, that isn't right; aside from modules, nothing
should be context-owned.
This commit continues where the last left off and moves finalizers from Context
to Origin. This is done in a separate commit because it introduces significant
changes. Currently, finalizers take a *Page, but that's no longer correct. A
value created in one Page, can outlive the Page. We need another container. I
original thought to use Origin, but that isn't know to CDP/MCP. Instead, I
decide to enhance the Session.
Session is now the owner of the page.arena, the page.factory and the
page.arena_pool. Finalizers are given a *Session which they can use to release
their arena.
It isn't safe/correct to call `navigate` on the same page multiple times. A page
is meant to have 1 navigate call. The caller should either remove the page
and create a new one, or call Session.replacePage.
This commit removes the *Page from the MCP Server and instead interacts with
the session to create or remove+create the page as needed, and lets the Session
own the *Page.
It also adds a bit of defensiveness around parameter parsing, e.g. calling
{"method": "tools/call"} (without an id) now errors instead of crashing.
It relies on default field values, e.g. for mutex: std.Thread.Mutex = .{}, but
doesn't initialize the structure, just the pointer on the heap resulting in a
crash.