pi-browser-harness 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/prompt.ts CHANGED
@@ -1,127 +1,27 @@
1
- /**
2
- * System prompt guidance for browser usage.
3
- *
4
- * Injects a compact browser control reference into the pi system prompt.
5
- * This prompt carries only cross-cutting knowledge — the mental model, tool
6
- * routing, workflows, and gotchas. Per-tool specifics (schemas, detailed usage)
7
- * are injected separately via promptGuidelines on each tool registration, so this
8
- * file intentionally does NOT restate full tool descriptions.
9
- */
10
-
11
- export function getBrowserSystemPrompt(): string {
1
+ export function getBrowserSystemPrompt(connected = true): string {
2
+ const connectionNotice = connected
3
+ ? ""
4
+ : "Browser is not connected. Run browser_setup or /browser-setup to connect.\n\n";
12
5
  return `
13
6
  ## Browser Control
14
7
 
15
- You drive a **real Chrome** (the user's running instance, not headless) through
16
- \`browser_*\` tools. These coexist with every standard pi tool — read, bash, edit,
17
- write — so mix them freely: \`browser_*\` for the page, everything else alongside.
18
-
19
- ### The core handle: refs
20
-
21
- Interaction is ref-first. Call \`browser_snapshot\` to get an accessibility tree
22
- where every interactive element has a stable ref (\`[eN]\`) and \`@(x,y)\`. **Target
23
- elements by ref, not by coordinates or guessed selectors** — refs are keyed to
24
- element identity, so they survive the re-renders that React/Vue forms trigger on
25
- every edit and save. Coordinates go stale; guessed selectors miss.
26
-
27
- - Prefer \`browser_fill\` for text — it fires input/change so controlled components
28
- update, and returns the value for verification. Use \`browser_click\` + \`browser_type\`
29
- only for keystroke-sensitive widgets (autocomplete, masked inputs).
30
- - Mutating calls append a compact **"Page changes"** diff — read it to confirm the
31
- change landed (form closed, new \`*[eN]\` appeared).
32
- - \`"ref is stale"\` means the page changed: re-run \`browser_snapshot\` for fresh refs.
33
-
34
- ### Choosing a tool
35
-
36
- | To… | Reach for |
37
- |-----|-----------|
38
- | See the page structure (for interaction) | \`browser_snapshot\` (refs) |
39
- | See the page visually | \`browser_screenshot\` |
40
- | Search the web | \`browser_web_search\` → ranked {title, url, snippet} |
41
- | Read an article's clean text | \`browser_read_page\` (reader mode; url or owned tab) |
42
- | Extract specific DOM values | \`browser_execute_js\` |
43
- | Hit a JSON/API endpoint | \`browser_http_get\` (raw GET, outside the browser) |
44
- | Click / fill / type / select / focus / press a key | \`browser_click\` · \`browser_fill\` · \`browser_type\` · \`browser_select_option\` · \`browser_focus\` · \`browser_press_key\` |
45
- | Upload a file · drag · resize viewport | \`browser_upload_file\` · \`browser_drag_and_drop\` · \`browser_viewport_resize\` |
46
- | Go to a URL / open many / manage tabs | \`browser_navigate\` · \`browser_new_tab\` · \`browser_open_urls\` · \`browser_list_tabs\` · \`browser_switch_tab\` · \`browser_go_back\`/\`go_forward\`/\`reload\` |
47
- | Wait for content / load / a fixed delay | \`browser_wait_for\` (selector/text) · \`browser_wait_for_load\` (readyState) · \`browser_wait\` |
48
- | Inspect page / handle a dialog | \`browser_page_info\` · \`browser_current_tab\` · \`browser_handle_dialog\` |
49
- | Diagnose a broken action | \`browser_console\` (JS errors) · \`browser_get_network_log\` |
50
- | Save output / config downloads | \`browser_print_to_pdf\` · \`browser_download\` |
51
- | Anything the tools can't express | \`browser_run_script\` (see Extending) |
52
-
53
- Each tool carries its own detailed guidelines — this table is only for routing.
54
-
55
- ### Workflows
56
-
57
- **Research (search → read):**
58
- \`\`\`
59
- browser_web_search("query") // ranked links, no page content
60
- → browser_read_page(url) // clean article text for the top hits
61
- // fall back to browser_open_urls + browser_execute_js only when reader mode misses
62
- \`\`\`
63
-
64
- **Form filling (ref-first — the reliable path on SPA forms):**
65
- \`\`\`
66
- browser_wait_for({ selector: "#email" }) // ensure the field is rendered
67
- browser_snapshot() // interactive elements get [eN] refs
68
- → browser_fill({ ref: "e7", value: "a@b.com" })
69
- → browser_select_option({ ref: "e9", label: "India" })
70
- → browser_click({ ref: "e12" }) // re-resolves position even after reflow
71
- // read each "Page changes" diff to confirm the step landed
72
- \`\`\`
73
-
74
- **Navigate + verify:**
75
- \`\`\`
76
- browser_new_tab("https://example.com") → browser_wait_for_load() → browser_screenshot()
77
- \`\`\`
78
-
79
- **Scroll:** \`browser_scroll({ deltaY: 500 })\` — W3C wheel convention: positive = down,
80
- negative = up (default 300, down).
81
-
82
- ### Parallelism
83
-
84
- Observation tools (\`browser_screenshot\`, \`browser_page_info\`, \`browser_execute_js\`,
85
- \`browser_list_tabs\`, \`browser_http_get\`, \`browser_web_search\`, \`browser_read_page\`, …)
86
- run in parallel with each other and with mutations. The harness serializes mutations
87
- (click, type, scroll, navigate, switch_tab) so they never race. Emit independent calls
88
- in one turn:
89
- \`\`\`
90
- browser_screenshot() + browser_page_info() + browser_execute_js("document.title")
91
- \`\`\`
92
-
93
- **Multi-agent:** tab switching by one agent changes the active tab for all agents,
94
- but per-tab data (console, network, dialogs) stays isolated. Call \`browser_current_tab\`
95
- before a mutation to confirm you're on the expected tab. Handle any dialog reported by
96
- \`browser_page_info\` promptly — dialogs block interaction and aren't queued across agents.
97
-
98
- **Session boundary & tab hygiene:** you operate only inside this session's own Chrome
99
- window. \`browser_switch_tab\`/\`browser_close_tab\` refuse tabs this session didn't open,
100
- and \`browser_list_tabs\` defaults to owned tabs — never reach into the user's other tabs
101
- or windows. Close tabs when you're done with them: call \`browser_close_tab\` on any tab
102
- you opened (via \`browser_new_tab\`/\`browser_open_urls\`) once you've extracted what you
103
- need. Don't leave a pile of stale tabs behind — keep only what a later step still needs.
104
- (Leftover tabs are closed automatically when the session ends, but close them yourself
105
- as you go.)
106
-
107
- ### Extending: temporary scripts
108
-
109
- When built-in tools can't express a multi-step workflow, write a script to a temp file
110
- and run it with \`browser_run_script\`. It executes in the harness process with direct
111
- daemon + Node access.
112
- \`\`\`
113
- write(<tempfile>.js, \`
114
- const results = [];
115
- for (const url of params.urls) {
116
- await daemon.cdp("Page.navigate", { url });
117
- await new Promise(r => setTimeout(r, 2000));
118
- results.push({ url, title: await daemon.evaluateJS("document.title") });
119
- }
120
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
121
- \`)
122
- browser_run_script(<tempfile>.js, { urls: [...] })
123
- \`\`\`
124
- Script bindings: params, daemon, require, signal, onUpdate, ctx, console, fetch, JSON,
125
- Buffer, setTimeout, clearTimeout. Use a real temp path from the OS — don't hardcode \`/tmp\`.
8
+ ${connectionNotice}You control the user's running Chrome through \`browser_*\` tools alongside the
9
+ standard pi tools. Per-tool usage lives in tool descriptions, parameter schemas,
10
+ and tool-specific guidelines.
11
+
12
+ - Operate only inside this session's owned Chrome window. Never reach into the
13
+ user's other tabs or windows. Close owned tabs once no later step needs them.
14
+ - Mutating tools (click, type, fill, press_key, scroll, navigate, history, tab
15
+ switch/new/close, upload, download, headers, viewport, drag, web_search) are
16
+ serialized. Observations (snapshot, screenshot, page_info, current_tab, list_tabs,
17
+ console, network_requests, http_get, read_page, execute_js, wait, wait_for) may run
18
+ concurrently with them. \`browser_run_script\` bypasses that lane: keep mutating
19
+ scripts sequential with other browser calls. Parallelize only independent calls.
20
+ - Agents sharing a session share its active tab. Check \`browser_current_tab\`
21
+ before a mutation to confirm the expected tab when another agent may switch it.
22
+ - Read any appended "Page changes" diff to confirm an interaction. Tools without a
23
+ diff, including \`browser_run_script\`, need explicit state verification.
24
+ - If a tool reports "ref is stale", call \`browser_snapshot\` for fresh refs before
25
+ retrying; do not reuse the stale handle.
126
26
  `;
127
27
  }
package/src/registry.ts CHANGED
@@ -16,6 +16,7 @@ import { uploadFileTool, downloadTool, printToPdfTool } from "./domains/files";
16
16
  import { viewportResizeTool } from "./domains/viewport";
17
17
  import { dragAndDropTool } from "./domains/drag";
18
18
  import { httpGetTool, networkRequestsTool } from "./domains/network";
19
+ import { clearHeadersTool, setHeadersTool } from "./domains/headers";
19
20
  import { consoleTool } from "./domains/console";
20
21
  import { snapshotTool } from "./domains/snapshot";
21
22
  import { executeJsTool, runScriptTool } from "./domains/js";
@@ -58,6 +59,8 @@ export const ALL_TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
58
59
  dragAndDropTool,
59
60
  httpGetTool,
60
61
  networkRequestsTool,
62
+ setHeadersTool,
63
+ clearHeadersTool,
61
64
  consoleTool,
62
65
  snapshotTool,
63
66
  executeJsTool,
package/src/state.ts CHANGED
@@ -13,8 +13,38 @@ export const defaultState = (namespace = "default"): BrowserState => ({
13
13
  ownedTargetIds: [],
14
14
  });
15
15
 
16
- export const persistState = (pi: ExtensionAPI, state: BrowserState): void => {
17
- pi.appendEntry<BrowserState>("browser-harness-state", state);
16
+ // Pi session JSONL is append-only and every custom entry shows up in the branch navigator, so a
17
+ // snapshot is only worth appending when it differs from the one the branch already holds (#26).
18
+ // Tab order is not a difference: ownership is a set.
19
+ const stateKey = (state: BrowserState): string =>
20
+ JSON.stringify({
21
+ namespace: state.namespace,
22
+ ownedTargetIds: [...(state.ownedTargetIds ?? [])].sort(),
23
+ harnessWindowTargetId: state.harnessWindowTargetId ?? null,
24
+ harnessWindowId: state.harnessWindowId ?? null,
25
+ });
26
+
27
+ export type StatePersister = {
28
+ // Appends the snapshot unless it equals the last one persisted or restored. Returns whether it wrote.
29
+ readonly persist: (state: BrowserState) => boolean;
30
+ // Records a snapshot that already lives in the session (restored from the branch) without writing it.
31
+ readonly markPersisted: (state: BrowserState) => void;
32
+ };
33
+
34
+ export const createStatePersister = (pi: ExtensionAPI): StatePersister => {
35
+ let last: string | undefined;
36
+ return {
37
+ persist: (state) => {
38
+ const key = stateKey(state);
39
+ if (key === last) return false;
40
+ pi.appendEntry<BrowserState>("browser-harness-state", state);
41
+ last = key;
42
+ return true;
43
+ },
44
+ markPersisted: (state) => {
45
+ last = stateKey(state);
46
+ },
47
+ };
18
48
  };
19
49
 
20
50
  const asPersistedState = (v: unknown): Partial<BrowserState> | undefined => {
package/src/util/paths.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { tmpdir } from "node:os";
3
- import { join } from "node:path";
3
+ import { isAbsolute, join, relative, resolve } from "node:path";
4
+
5
+ // `relative` rather than a string prefix: it is separator-agnostic and carries each platform's
6
+ // own case rules, so a Windows path is not rejected for its backslashes or its drive-letter case.
7
+ export const isPathWithin = (root: string, target: string): boolean => {
8
+ const absRoot = resolve(root);
9
+ const absTarget = resolve(target);
10
+ if (absTarget === absRoot) return true;
11
+ const rel = relative(absRoot, absTarget);
12
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
13
+ };
4
14
 
5
15
  const NAMESPACE_RE = /^[a-zA-Z0-9_-]+$/;
6
16