From 256b6e876d3991c5b7ca1f05209f8c965869b446 Mon Sep 17 00:00:00 2001 From: Celine Debled Date: Wed, 5 Aug 2026 10:29:47 +0200 Subject: [PATCH 1/4] Add reference/pandascript section documenting primitives, calling conventions and errors --- src/content/reference/_meta.ts | 7 ++ src/content/reference/pandascript.mdx | 145 ++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/content/reference/_meta.ts create mode 100644 src/content/reference/pandascript.mdx diff --git a/src/content/reference/_meta.ts b/src/content/reference/_meta.ts new file mode 100644 index 0000000..6295200 --- /dev/null +++ b/src/content/reference/_meta.ts @@ -0,0 +1,7 @@ +import type { MetaRecord } from 'nextra' + +const meta: MetaRecord = { + pandascript: 'PandaScript', +} + +export default meta diff --git a/src/content/reference/pandascript.mdx b/src/content/reference/pandascript.mdx new file mode 100644 index 0000000..d9a998b --- /dev/null +++ b/src/content/reference/pandascript.mdx @@ -0,0 +1,145 @@ +--- +title: PandaScript Reference +description: Primitives, calling conventions, extraction schema and errors of the PandaScript runtime. +--- + +# PandaScript Reference + +A PandaScript is a plain JavaScript automation script that Lightpanda runs directly, with no LLM call. Learn the execution model and see complete examples in the [PandaScript usage page](/usage/pandascript). + +```sh copy +lightpanda run .js +``` + +The script context installs two globals, `Page` and [`console`](#console), and nothing else from the browser toolset. A script creates a page with `new Page()`, then calls every primitive below as a method on that page. Every `timeout` is in milliseconds. + +## Navigation + +| Primitive | Arguments | Returns | Comment | +|-----------|-----------|---------|---------| +| `page.goto` | `goto(url[, { timeout = 10000 }])` | The same page object it was called on | Returns at the `load` event, a fast snapshot that skips content rendered by post-load JavaScript. Follow it with [`waitForState`](#waiting) when the page keeps rendering. | +| `page.close` | `close()` | Nothing | Stales the handle, so later calls on it fail. The page itself is reclaimed on the next `goto` or at script end. | + +## Extraction and page scripting + +`evaluate` runs a JavaScript string inside the page, where `window` and `document` exist, and `extract` resolves its schema selectors there too. That code runs in the page, so it cannot see the variables of your agent script. + +| Primitive | Arguments | Returns | Comment | +|-----------|-----------|---------|---------| +| `page.extract` | `extract(schema)` or `extract({ schema })` | An object or an array, shaped by the schema | Schema forms are listed [below](#extraction-schema). | +| `page.evaluate` | `evaluate(script[, { url, timeout = 10000, save }])` | The page value as a string, JSON-encoded when it is an object or array | `url` navigates before running the script, and `timeout` bounds that navigation, not the script. | + +### Extraction schema + +`page.extract(...)` takes a schema object mapping output field names to CSS-selector specs. The schema forms are: + +| Schema value | Meaning | Comment | +|--------------|---------|---------| +| `""` | Text of the first matching element, or `null` | | +| `""` | Text of the element currently matched | Only inside a `fields` block | +| `[""]` | Text of all matching elements | Only the first element is read, so `["a", "b"]` extracts `"a"` and silently drops `"b"` | +| `{ selector: "", attr: "" }` | Attribute from the first match | `href` and `src` resolve to absolute URLs | +| `[{ selector: "", attr: "" }]` | Attribute from all matches | | +| `[{ selector: "", fields: { ... } }]` | Array of records, with fields resolved relative to each matched element | Fields accept any shape above, so arrays nest for per-item sub-lists | +| `[{ selector: "", limit: N }]` | At most `N` matches | Object-array form only, the `[""]` shorthand has no equivalent | + +**Passing the schema.** These three calls are equivalent: + +```js copy +page.extract({ title: "h1" }); +page.extract({ schema: { title: "h1" } }); +page.extract('{ "title": "h1" }'); +``` + +The object form accepts only the `schema` key, so the REPL's `save` option is rejected in scripts. + +**What you get back.** Every extracted value is a string (trimmed text or a raw attribute) or `null`; parse numbers yourself. The result mirrors the top-level schema: + +| Schema | Result | +|--------|--------| +| An object, `{ title: "h1" }` | An object keyed by your fields, even with a single field | +| A bare array, `[{ selector: "a" }]` | The array itself | +| An array field matching nothing | `[]` | +| Every field in the schema missing | Throws `no schema selector matched any element` | + +## Interaction + +Every interaction below reports the page URL and title reached after the action, so a script can tell whether it triggered a navigation. A `click` returns, in full: + +``` +Clicked element (selector: a.login). Page url: https://example.com/app, title: Dashboard +``` + +`scroll` is the exception and reports only its position. + +| Primitive | Arguments | Returns | +|-----------|-----------|---------| +| `page.click` | `click(selector)` or `click({ selector })` | `Clicked element ()` | +| `page.fill` | `fill(selector, value)` or `fill({ selector, value })` | `Filled element () with ""` | +| `page.hover` | `hover(selector)` or `hover({ selector })` | `Hovered element ()` | +| `page.press` | `press(selector, key)` or `press({ key[, selector] })` | `Pressed key ''` | +| `page.selectOption` | `selectOption(selector, value)` or `selectOption({ selector, value })` | `Selected option '' ()` | +| `page.setChecked` | `setChecked(selector[, checked = true])` or `setChecked({ selector, checked = true })` | `Set element () to checked` or `to unchecked` | +| `page.scroll` | `scroll({ x = 0, y = 0 })`, never a selector | `Scrolled to x: , y: ` | + +## Waiting + +| Primitive | Arguments | Returns | Comment | +|-----------|-----------|---------|---------| +| `page.waitForSelector` | `waitForSelector(selector[, { timeout = 5000 }])` | `Element found. backendNodeId: ` | A script cannot act on that ID, since every primitive takes a CSS selector. | +| `page.waitForScript` | `waitForScript(script[, { timeout = 5000 }])` | `Script returned truthy.` | Re-evaluates the script in the page context until it returns truthy. | +| `page.waitForState` | `waitForState(state[, { timeout = 5000 }])` | `Page reached .` | Takes one of the states below. | + +The states `waitForState` accepts: + +| `state` | Resolves when | +|---------|---------------| +| `"load"` | The `load` event fires: the frame and all its subresources, including async scripts, have finished loading. | +| `"domcontentloaded"` | The HTML is parsed and deferred scripts have run. Subresources (images, stylesheets, async scripts) may still be loading. | +| `"networkalmostidle"` | At most 2 requests stay in flight for 500 ms straight. | +| `"networkidle"` | Zero requests stay in flight for 500 ms straight. | +| `"done"` | The page goes fully idle: no scheduled JavaScript work and no network activity. Every `waitForState` call eventually resolves to this, even if the requested state is never reached. | + +## Console + +`console` is the second and last global. It carries five methods, split across the two output streams: + +| Methods | Write to | +|---------|----------| +| `console.log`, `console.info`, `console.debug` | stdout | +| `console.warn`, `console.error` | stderr | + +They print for you to read. A script's own output is whatever it `return`s from the top level, which is where objects and arrays get JSON-formatted. + +## Calling conventions + +**Positional and options arguments.** Each primitive takes its leading arguments positionally, with an optional trailing options object for the rest: + +- Mix a positional with an options object: `waitForSelector("#row", { timeout: 2000 })`. +- Or pass one object with everything: `waitForSelector({ selector: "#row", timeout: 2000 })`. This is equivalent, and it's the shape `/save` records into saved scripts. +- An option can never be passed as a bare positional: `waitForSelector("#row", 2000)` is an error. +- A `null` positional omits that argument: `press(null, "Enter")` presses on the document, not necessarily the focused element. +- Setting the same field both positionally and in the options object is an error: `goto(url, { url: ... })` throws `invalid arguments`. +- Arguments must be JSON-serializable. `undefined`, functions, and symbols throw `invalid arguments`; a cyclic object throws V8's own `TypeError: Converting circular structure to JSON` instead, since it fails before Lightpanda's own check runs. + +**Selectors, not node IDs.** Script primitives address elements by CSS selector only. `tree`, `findElement`, and `nodeDetails` hand out `backendNodeId`s but aren't installed in the script context, and a raw node ID wouldn't survive replay anyway. When you're exploring in the REPL and have a `backendNodeId` (the leading number on a `/tree` line, or a `/findElement` hit), run `/nodeDetails backendNodeId=` to get a durable CSS `selector`, then paste that into your script. + +**Secrets via `$LP_*` placeholders.** String arguments can contain `$LP_*` placeholders, resolved inside the Lightpanda process. This keeps credentials out of recorded scripts and LLM prompts. Recordings scrub resolved `LP_*` values back to placeholders. + +## Errors + +Primitive failures throw JavaScript exceptions. The complete list: + +| Error | Meaning | +|-------|---------| +| `Page must be called with new` | `Page(...)` was called without `new`. Use `new Page()`. | +| `extract is not defined` (or `click`, `fill`, ...) | Primitives are methods on the page object, not globals. Use `page.extract(...)`, not a bare `extract(...)`. | +| `page is not navigated or has been closed` | A method ran on a fresh `new Page()`, or on a closed page, before `await page.goto(url)`. | +| `page handle is no longer valid` | A re-`goto` on the same page object that failed or timed out: the old frame is torn down before the new navigation's outcome is known, so a rejected re-navigation leaves the handle bound to a removed frame. A **successful** re-goto rebinds the same object to the new frame and keeps working; sibling pages from other `new Page()` calls are unaffected either way. | +| `ReferenceError: document is not defined` | You tried to use browser DOM APIs in the agent context. Use `extract(...)` or page `evaluate(...)`. | +| `ReferenceError: require is not defined` | Agent scripts are not Node.js scripts. | +| `no page loaded - run page.goto(url) first` | A page-dependent primitive ran before navigation. | +| `invalid arguments` | A primitive received the wrong number or shape of arguments, or a value that stringifies to `undefined` (like a bare function or symbol). A cyclic object throws a different, native error instead. | +| `extract: no schema selector matched any element` | Every field in the schema missed. Fix the selectors; an empty page section yields `null`/`[]` per field, not this error. | +| `navigation timed out` | `goto` did not finish loading before its timeout. Also `navigation failed`, `navigation cancelled`, and `navigation abandoned` for the other navigation outcomes. | +| ` failed: ` | Fallback for any other tool failure, e.g. `click failed: NodeNotFound` when a selector matches nothing. | From 4069b1df6c4782581b1c5b70fc1a3b804c2c7f69 Mon Sep 17 00:00:00 2001 From: Celine Debled Date: Wed, 5 Aug 2026 10:29:47 +0200 Subject: [PATCH 2/4] Slim usage/pandascript.mdx down to a practical guide, linking to the new reference for exhaustive detail --- src/content/usage/pandascript.mdx | 145 ++++++++---------------------- 1 file changed, 37 insertions(+), 108 deletions(-) diff --git a/src/content/usage/pandascript.mdx b/src/content/usage/pandascript.mdx index 6fc3bda..b7255ee 100644 --- a/src/content/usage/pandascript.mdx +++ b/src/content/usage/pandascript.mdx @@ -2,7 +2,6 @@ title: PandaScript description: Built-in automation script for Lightpanda --- -import { Callout, Tabs } from 'nextra/components' # PandaScript @@ -17,7 +16,7 @@ It's reproducible, deterministic and token-free (no LLM required). To run a PandaScript: ```sh copy -lightpanda agent .js +lightpanda run .js ``` ## Runtime Environment @@ -36,28 +35,26 @@ web page's JavaScript context. - The global `evaluate(...)` primitive runs JavaScript in the page context, distinct from the agent context's own native `eval`. - Agent variables persist for the lifetime of one script run, across - navigations and primitive calls. A later `lightpanda agent script.js` run - starts with a fresh agent context. -- The installed primitives are synchronous and blocking. Do not write an - `async`/`await` automation contract around them. Scripts compile as classic - scripts, so top-level `await` is a `SyntaxError`; promise callbacks - (`.then`) only run after the script body finishes, never in between - primitive calls. + navigations and primitive calls. A later `lightpanda run script.js` starts + with a fresh agent context. +- `goto` is the one asynchronous primitive: always `await page.goto(...)`. + Every other page method (`extract`, `evaluate`, `click`, `fill`, and the + rest) is synchronous and blocking. The script body runs inside an async + wrapper, so top-level `await` is allowed, unlike a plain classic script. - Tool failures throw JavaScript `Error` exceptions and stop execution unless you catch them. -- The script's completion value (its last top-level expression) is printed - automatically (objects and arrays as JSON; other values coerced). End a - script with the bare expression you want as output, e.g. a final - `extract({ ... });` or `results;`. `console.log(...)` is for extra or debug - output and does not JSON-format objects. +- The script's output is whatever it `return`s from the top level (objects + and arrays printed as JSON; other values coerced). End a script with + `return extract({ ... });` or `return results;`. A bare trailing expression + is not printed. `console.log(...)` is for extra or debug output and does + not JSON-format objects. -The agent context includes a small `console` object: +The agent context includes a small `console` object, split across the two +output streams. Find every method in the +[PandaScript reference](/reference/pandascript#console). ```js console.log("printed to stdout"); -console.info("printed to stdout"); -console.debug("printed to stdout"); -console.warn("printed to stderr"); console.error("printed to stderr"); ``` @@ -85,7 +82,7 @@ const data = page.extract({ }] }); -data; // printed automatically as JSON +return data; // printed automatically as JSON ``` Destructure when a single field is all you need: @@ -111,54 +108,15 @@ for (const story of stories) { returns an object or array, that text is JSON. Primitive arguments must be JSON-serializable. Strings, numbers, booleans, -arrays, plain objects, and `null` work. `undefined`, functions, symbols, and -cyclic objects do not. +arrays, plain objects, and `null` work. Find what the other values throw in the +[PandaScript reference](/reference/pandascript#calling-conventions). ## Installed Primitives -Only recorded browser primitives are installed globally: - -| Primitive | Arguments | Runs in | -|-----------|-----------|---------| -| `new Page()` ||Browser session | -| `page.goto` | `goto(url[, { timeout }])` | Browser page | -| `page.extract` | `extract(schema)` or `extract({ schema })` | Browser page via extractor; returns a JS object or array | -| `page.evaluate` | `evaluate(script[, { url, timeout, save }])` | Browser page JS context | -| `page.click` | `click(selector)` or `click({ selector })` | Browser page | -| `page.fill` | `fill(selector, value)` or `fill({ selector, value })` | Browser page | -| `page.scroll` | `scroll()` or `scroll({ x, y })` | Browser page | -| `page.waitForSelector` | `waitForSelector(selector[, { timeout }])` | Browser page | -| `page.waitForScript` | `waitForScript(script[, { timeout }])` | Browser page JS context | -| `page.waitForState` | `waitForState(state[, { timeout }])` | Browser page | -| `page.hover` | `hover(selector)` or `hover({ selector })` | Browser page | -| `page.press` | `press(selector, key)` or `press({ key[, selector] })` | Browser page | -| `page.selectOption` | `selectOption(selector, value)` or `selectOption({ selector, value })` | Browser page | -| `page.setChecked` | `setChecked(selector[, checked])` or `setChecked({ selector, checked })` | Browser page | - -`goto` returns at the `load` event (a fast snapshot). When a page's content is -still loading (rendered by post-load JS), call `waitForState("networkidle")` -before reading. `waitForState`'s `state` accepts `"load"`, -`"domcontentloaded"`, `"networkalmostidle"`, `"networkidle"`, or `"done"`. -`goto`'s `timeout` defaults to 10000 ms; the `waitFor*` timeouts default to -5000 ms. - -The `[, { … }]` is an optional trailing options object: leading arguments are -positional (`waitForSelector("#row", { timeout: 2000 })`), and the options ride -in a final object. Passing a single object with everything -(`waitForSelector({ selector: "#row", timeout: 2000 })`) is equivalent; that's -the shape `/save` records into saved scripts. An option can't be a bare -positional, though: `waitForSelector("#row", 2000)` is an error. A `null` -positional omits that field (`press(null, "Enter")` presses on the focused -element), and setting the same field positionally and in the options object -(`goto(url, { url: ... })`) is an `invalid arguments` error. - -Script primitives address elements by CSS selector only. The tools that hand -out `backendNodeId`s (`tree`, `findElement`, `nodeDetails`) aren't installed in -the script context, and a raw node ID wouldn't survive replay anyway. When -you're exploring in the REPL and have a `backendNodeId` (e.g. the leading -number on a `/tree` line, or a `/findElement` hit) run `/nodeDetails -backendNodeId=` to get a durable CSS `selector`, then paste that into your -script. +Only recorded browser primitives are installed globally: `new Page()`, then +`goto`, `extract`, `evaluate`, the interaction methods and the `waitFor*` +methods as methods on the page. Find the full table, the calling conventions +and the timeouts in the [PandaScript reference](/reference/pandascript#primitives). ## Navigation @@ -174,11 +132,10 @@ await page.goto({ }); ``` -The call returns a status string and throws if navigation fails. A timeout -does **not** throw: the call returns `"Navigation started but the page did not -finish loading before the timeout."` and the page stays in whatever state it -reached. Check the return value, or follow with `waitForState(...)` / -`waitForSelector(...)`, when completeness matters. +The call resolves to the same page object (`await page.goto(url) === page`), +and rejects if navigation fails or times out (`"navigation timed out"`). +Follow with `waitForState(...)` / `waitForSelector(...)` when completeness +matters. ## Structured Extraction @@ -200,27 +157,8 @@ const result = page.extract({ }); ``` -The schema forms are: - -| Schema value | Meaning | -|--------------|---------| -| `""` | Text of the first matching element, or `null` | -| `""` | Text of the current matched element inside a `fields` block | -| `[""]` | Text of all matching elements | -| `{ selector: "", attr: "" }` | Attribute from the first match | -| `[{ selector: "", attr: "" }]` | Attribute from all matches | -| `[{ selector: "", fields: { ... } }]` | Array of records, with fields resolved relative to each matched element | -| `limit: N` | Cap array extraction to `N` matches | - -Return shape follows the top-level schema: - -- `extract({ title: "h1" })` returns `{ title: "..." }`. -- `extract({ title: "h1", links: [{ selector: "a" }] })` returns an object - with both fields. -- `extract({ links: [{ selector: "a" }] })` returns `{ links: [...] }`. An - object schema always returns an object, even with a single field. -- `extract([{ selector: "a" }])` is shorthand for a single anonymous array - extraction and returns the array directly. +Find every schema form in the +[PandaScript reference](/reference/pandascript#extraction-schema). Every value is a string (trimmed text or a raw attribute) or `null`; parse numbers in script logic. An array field that matches nothing yields `[]` @@ -235,19 +173,6 @@ the detail). The local agent context keeps the data across navigations, so the assembly happens in plain JavaScript. See the [complete example](#complete-example) below. -When passing an object directly to `extract(...)`, the runtime serializes it as -the extractor schema. These forms are equivalent: - -```js copy -page.extract({ title: "h1" }); -page.extract({ schema: { title: "h1" } }); -page.extract('{ "title": "h1" }'); -``` - -The wrapped form accepts only `schema`: the REPL's `save=` option does not -exist in scripts (`extract({ schema: ..., save: ... })` is rejected). Keep -results in local variables instead. - Use local variables to keep extracted data available to later script logic: ```js copy @@ -315,9 +240,10 @@ page.scroll(); `setChecked` defaults `checked` to `true` when the field is omitted (`setChecked("#chk")` checks the box). `press`'s leading positional is the optional `selector`, not `key`: a bare `press("Enter")` binds `"Enter"` to -`selector` and fails. Press on the focused element with -`press({ key: "Enter" })` or `press(null, "Enter")`; target an element with -`press("#search", "Enter")` or `press({ key: "Enter", selector: "#search" })`. +`selector` and fails. Press on the document (not necessarily the focused +element) with `press({ key: "Enter" })` or `press(null, "Enter")`; target an +element with `press("#search", "Enter")` or +`press({ key: "Enter", selector: "#search" })`. `$LP_*` placeholders in string arguments are resolved inside the Lightpanda process. This keeps credentials out of recorded scripts and LLM prompts. In @@ -342,10 +268,13 @@ Common failures: |-------|---------| | `ReferenceError: document is not defined` | You tried to use browser DOM APIs in the agent context. Use `extract(...)` or page `evaluate(...)`. | | `ReferenceError: require is not defined` | Agent scripts are not Node.js scripts. | -| `no page loaded - run goto(url) first` | A page-dependent primitive ran before navigation. | +| `no page loaded - run page.goto(url) first` | A page-dependent primitive ran before navigation. | | `invalid arguments` | A primitive received the wrong number or shape of arguments, or a non-JSON-serializable value. | | `extract: no schema selector matched any element` | Every field in the schema missed. Fix the selectors; an empty page section yields `null`/`[]` per field, not this error. | +Find the complete list in the +[PandaScript reference](/reference/pandascript#errors). + ## Complete Example This script opens Hacker News, extracts five stories, visits each comments @@ -388,5 +317,5 @@ for (const story of stories) { story.comments = comments; } -stories; // printed automatically as JSON +return stories; // printed automatically as JSON ``` From 4d804c29af508b4dde521963352c25edde300f52 Mon Sep 17 00:00:00 2001 From: Celine Debled Date: Thu, 6 Aug 2026 17:22:08 +0200 Subject: [PATCH 3/4] Address review: document goto's waitUntil option, waitForSelector/waitForScript's state-dependent timeout, fix broken #primitives anchor --- src/content/reference/pandascript.mdx | 6 +++--- src/content/usage/pandascript.mdx | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/content/reference/pandascript.mdx b/src/content/reference/pandascript.mdx index d9a998b..5e3f69a 100644 --- a/src/content/reference/pandascript.mdx +++ b/src/content/reference/pandascript.mdx @@ -17,7 +17,7 @@ The script context installs two globals, `Page` and [`console`](#console), and n | Primitive | Arguments | Returns | Comment | |-----------|-----------|---------|---------| -| `page.goto` | `goto(url[, { timeout = 10000 }])` | The same page object it was called on | Returns at the `load` event, a fast snapshot that skips content rendered by post-load JavaScript. Follow it with [`waitForState`](#waiting) when the page keeps rendering. | +| `page.goto` | `goto(url[, { timeout = 10000, waitUntil = "load" }])` | The same page object it was called on | Resolves once `waitUntil` is reached. `waitUntil` takes the same states as [`waitForState`](#waiting): `load` (default, a fast snapshot that skips content rendered by post-load JavaScript), `domcontentloaded`, `networkalmostidle`, `networkidle`, or `done`. | | `page.close` | `close()` | Nothing | Stales the handle, so later calls on it fail. The page itself is reclaimed on the next `goto` or at script end. | ## Extraction and page scripting @@ -86,8 +86,8 @@ Clicked element (selector: a.login). Page url: https://example.com/app, title: D | Primitive | Arguments | Returns | Comment | |-----------|-----------|---------|---------| -| `page.waitForSelector` | `waitForSelector(selector[, { timeout = 5000 }])` | `Element found. backendNodeId: ` | A script cannot act on that ID, since every primitive takes a CSS selector. | -| `page.waitForScript` | `waitForScript(script[, { timeout = 5000 }])` | `Script returned truthy.` | Re-evaluates the script in the page context until it returns truthy. | +| `page.waitForSelector` | `waitForSelector(selector[, { timeout }])` | `Element found. backendNodeId: ` | `timeout` defaults to 5000ms once the page has reached `load`, or 15000ms (the navigation budget plus 5000) if it hasn't yet. A script cannot act on that ID, since every primitive takes a CSS selector. | +| `page.waitForScript` | `waitForScript(script[, { timeout }])` | `Script returned truthy.` | Same default-`timeout` rule as `waitForSelector`. Re-evaluates the script in the page context until it returns truthy. | | `page.waitForState` | `waitForState(state[, { timeout = 5000 }])` | `Page reached .` | Takes one of the states below. | The states `waitForState` accepts: diff --git a/src/content/usage/pandascript.mdx b/src/content/usage/pandascript.mdx index b7255ee..e0a29e9 100644 --- a/src/content/usage/pandascript.mdx +++ b/src/content/usage/pandascript.mdx @@ -116,7 +116,7 @@ arrays, plain objects, and `null` work. Find what the other values throw in the Only recorded browser primitives are installed globally: `new Page()`, then `goto`, `extract`, `evaluate`, the interaction methods and the `waitFor*` methods as methods on the page. Find the full table, the calling conventions -and the timeouts in the [PandaScript reference](/reference/pandascript#primitives). +and the timeouts in the [PandaScript reference](/reference/pandascript). ## Navigation @@ -132,10 +132,11 @@ await page.goto({ }); ``` -The call resolves to the same page object (`await page.goto(url) === page`), -and rejects if navigation fails or times out (`"navigation timed out"`). -Follow with `waitForState(...)` / `waitForSelector(...)` when completeness -matters. +The call resolves to the same page object (`await page.goto(url) === page`) +once it reaches `waitUntil` (`load` by default), and rejects if navigation +fails or times out (`"navigation timed out"`). Pass `waitUntil: "networkidle"` +(or another state) to wait past `load` directly, or follow with +`waitForState(...)` / `waitForSelector(...)` when completeness matters. ## Structured Extraction From fca036866414325d8d86c81ffebcfd52c2180a6f Mon Sep 17 00:00:00 2001 From: Celine Debled Date: Tue, 25 Aug 2026 09:55:42 +0200 Subject: [PATCH 4/4] Update cross-links to the merged PandaScript reference --- src/content/reference/cli/agent.mdx | 2 +- src/content/reference/cli/index.mdx | 2 +- src/content/usage/agent.mdx | 13 +++---------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/content/reference/cli/agent.mdx b/src/content/reference/cli/agent.mdx index d6db259..ce33ea2 100644 --- a/src/content/reference/cli/agent.mdx +++ b/src/content/reference/cli/agent.mdx @@ -130,7 +130,7 @@ Inside the REPL, slash commands work alongside natural-language input: | `/usage` | Show token usage and cache stats for this session | | `/clear` | Clear conversation history and usage (keeps page and cookies) | | `/reset` | Reset conversation and browser session (drops page and cookies) | -| `/save [filename.js] [prompt]` | Save this session to a file as a [PandaScript](/usage/pandascript) | +| `/save [filename.js] [prompt]` | Save this session to a file as a [PandaScript](/reference/pandascript) | | `/load ` | Load and run a script from disk | | `/model [name]` | Change the model | | `/provider [name]` | Change the provider, or `null` to disable the LLM | diff --git a/src/content/reference/cli/index.mdx b/src/content/reference/cli/index.mdx index 75354c1..02dce09 100644 --- a/src/content/reference/cli/index.mdx +++ b/src/content/reference/cli/index.mdx @@ -58,7 +58,7 @@ Arguments: The command also accepts the [common options](/reference/cli/common-options). -Find the script format in the [PandaScript guide](/usage/pandascript). +Find the script format in the [PandaScript reference](/reference/pandascript). ## version diff --git a/src/content/usage/agent.mdx b/src/content/usage/agent.mdx index bbb56bb..fec2552 100644 --- a/src/content/usage/agent.mdx +++ b/src/content/usage/agent.mdx @@ -151,16 +151,9 @@ line by line, Enter submits at each newline. ### Extracting data `/extract` takes a JSON schema where each value tells the extractor what to -lift off the page. The result is printed to stdout as a single JSON object. - -Supported value forms: - -- `""`: `textContent.trim()` of the first match. -- `""`: the matched element's own text (only inside a `fields` block). -- `[""]`: text of every match. Sugar for `[{"selector": ""}]`. -- `{"selector": "", "attr": ""}`: attribute of the first match. -- `[{"selector": "", "fields": {…}}]`: array of records, each - `fields` value resolved relative to the matched element. +lift off the page: the same schema `page.extract` uses in +[PandaScript](/reference/pandascript#extraction-schema). The result is +printed to stdout as a single JSON object. A selector that matches nothing yields a null or empty field, not an error. That's deliberate, but it means a stale selector fails quietly. If a run