| name | browser |
| description | Drive a real Chrome window — read pages as an accessibility tree, click, type, upload, wait, and run JavaScript. Use for anything on a site that needs a logged-in session, a rendered page, or interaction — reading a page that needs JavaScript, filling a form, checking how something looks, or working through a site the way a person would. |
browser
A real Chrome window, driven from the shell. It stays open between commands and
keeps its logins, so you can watch what is happening and take over by hand.
cd ~/.pi/agent/skills/browser/scripts
node start.mjs
The first run installs Playwright and says so — a few seconds, once per
machine, nothing to do. It also makes a profile of its own: log into whatever
you need, once, by hand in that window, and it is kept from then on.
The loop
Take a snapshot, read it, act on the numbers it printed.
node nav.mjs github.com/BurntSushi/ripgrep
node snapshot.mjs
The snapshot is the page's accessibility tree — every frame, nothing
summarised. Read it or grep it. Interactive elements are numbered:
[20] link "Issues"
[39] combobox "Search Issues"
Those numbers are what the other commands take. They belong to the snapshot
that printed them; after anything changes the page, take another.
Acting
node click.mjs 20
node fill.mjs 39 "memory leak"
node press.mjs Escape
node select.mjs 12 "Option 1"
node hover.mjs 8
node upload.mjs 5 ./report.pdf
Each waits for the element to be attached, visible, still, enabled, and to
actually receive the click — retrying until it does. A button under a cookie
banner fails and says so rather than being clicked in its place. --force
skips those checks.
Waiting
node wait.mjs --text "Hello World!"
node wait.mjs --gone "Loading..."
node wait.mjs --idle
nav.mjs already waits for the page to settle. Use these for content that
arrives later.
Looking
node screenshot.mjs
For layout, whether an image rendered, what a chart shows — things words do not
carry. The snapshot is better for everything else.
Tabs
node tabs.mjs
node nav.mjs example.com --new
node snapshot.mjs --tab 5B2CA09708EF
node tabs.mjs --close 5B2CA09708EF
Without --tab, commands act on the tab that is in front.
--tab takes the id tabs.mjs prints. A position works too, but do not keep
one: closing a tab moves every tab after it down, so a remembered position
starts pointing at a different page.
Dialogs
alert, confirm and prompt stop the page, so the answer has to be arranged
before the thing that opens them — one command, not two.
node dialog.mjs --click 7
node dialog.mjs --click 7 --dismiss
node dialog.mjs --eval 'confirmThing()' --text "answer"
Any other command that happens to open one answers Cancel: Playwright
closes a dialog nobody is listening for, and only this script listens. So if a
click produces "You clicked: Cancel" when you did not ask for it, that is why —
use dialog.mjs for that click instead.
JavaScript
node eval.mjs 'document.title'
node eval.mjs 'const rows = [...document.querySelectorAll("tr")]; return rows.length;'
node eval.mjs 'document.body.innerHTML' --frame 1
The escape hatch for anything the other commands do not cover. A bare
expression or a body with return, both work.
--frame reaches inside an iframe. Chrome does not expose every editor as
interactive — TinyMCE's body is a generic with no properties, so the snapshot
gives it no number — and this is the way in.
What the page logged
node console.mjs --eval 'doTheThing()' --for 3000
node console.mjs --for 5000
Console output, uncaught errors and failed requests exist only while something
is listening, and every command here is a separate process. So this runs the
action itself, with the listeners already on.
More than one agent
They share the browser — there is one window, and one of them navigating a tab
takes it away from the other. Snapshots and handles do not collide: each pi
session gets its own, keyed on PI_SESSION_ID. A handle whose page has moved
on says so rather than acting on whatever is now there, because the number is
written onto the element and a navigation takes it with it.
So open a tab of your own and work in it:
node nav.mjs example.com --new
node snapshot.mjs --tab <that id>
node click.mjs 4
The id survives other agents opening and closing tabs. Handles carry it, so
after a snapshot you can leave --tab off entirely.
Notes
An accessible name can contain characters you cannot see — icon fonts put
private-use glyphs in them, so one real page has button "\uf090 Login". Copy
the number, not the name.
The window is left open when a command finishes. Closing it would throw away
the login state.