Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8a57f17
fix(errors): name the selector when it matches nothing instead of "El…
Sep 29, 2026
a268db0
fix(targeting): one shared resolver for every tool, find/click_text r…
Sep 30, 2026
b50b4ab
fix(hang): a frozen page fails in seconds and navigate/reload recover it
Sep 30, 2026
454ce57
feat(input): coordinate actions, triple click, key sequences, zoomed …
Sep 30, 2026
e5b62cb
feat(window): browser_resize_window, background tabs, window focus, f…
Sep 30, 2026
06c0980
fix(console): capture the page's own console; filters for console and…
Sep 30, 2026
e20f258
feat(upload): upload images without a local file, to inputs or drop z…
Sep 30, 2026
f842293
feat(gif): record the agent's actions in a tab as an animated GIF
Sep 30, 2026
3b91458
feat(multi-browser): several browsers at once, each session picks one
Sep 30, 2026
e58787f
feat(shortcuts): save and replay named action sequences with {{variab…
Sep 30, 2026
a9e27d7
chore(release): 2.4.0 — GIF export in parts, README for the new capab…
Sep 30, 2026
aa79782
test(gif): multi-part export is reassembled and written, bytes never …
Sep 30, 2026
f03d62f
refactor: browser_find in its own module (inspection.js back under 60…
Sep 30, 2026
91a7b15
fix(review): lock-safe frozen-tab recovery, DPR-correct screenshot ma…
Sep 30, 2026
f88520b
chore(lint): no useless assignment in isVisible
Sep 30, 2026
5217539
Merge pull request #20 from compnew2006/feat/cic-parity-2
compnew2006 Sep 30, 2026
e5e9c40
fix(ci): Babel 8 ESLint parser so npm ci and lint pass
Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<p align="center">
<a href="https://github.com/compnew2006/browser-controller/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/compnew2006/browser-controller/ci.yml?branch=main&label=CI&style=flat-square" alt="CI" /></a>
<a href="https://github.com/compnew2006/browser-controller/releases"><img src="https://img.shields.io/badge/version-2.3.0-blue?style=flat-square" alt="v2.3.0" /></a>
<a href="https://github.com/compnew2006/browser-controller/releases"><img src="https://img.shields.io/badge/version-2.4.0-blue?style=flat-square" alt="v2.4.0" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow?style=flat-square" alt="License: MIT" /></a>
<img src="https://img.shields.io/badge/node-%E2%89%A520-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node >= 20" />
<img src="https://img.shields.io/badge/TypeScript-strict-blue?style=flat-square" alt="TypeScript strict" />
Expand Down Expand Up @@ -44,6 +44,12 @@ It already has your browser open right there. It just can't see it.
- **Real, trusted input.** Clicks, typing and key presses go through the Chrome DevTools Protocol, so the page sees `isTrusted` events, focus really moves, default actions run (Tab moves focus, Enter submits, arrows drive autocomplete menus) and focus/blur fire even while the window is in the background — legacy grids and lookup widgets behave as they do for a person. One debugger session per tab is reused and detached after 30 s idle (the yellow "being debugged" banner shows only while it is attached). Pass `trusted: false` for the old synthetic events with no banner; they are also the automatic fallback when the debugger can't attach.
- **Batches.** `browser_batch` runs a list of tool calls in one round-trip and stops at the first failure — a click → type → Tab → wait → read sequence is one call instead of five.
- **Console-style JavaScript.** `browser_evaluate` accepts code as you'd type it in DevTools: top-level `await`, several statements, the last expression's value is returned, DOM nodes come back as readable descriptions — and page CSP doesn't block it.
- **Refs that stay right.** Snapshot/find refs resolve through one shared page runtime: the registered element first, then the first *visible* selector match (across open and closed shadow roots and same-origin iframes), then a verified fallback that only re-binds when role, tag and name identify one element — an ambiguous match is reported as gone instead of clicked. Hidden duplicates are skipped.
- **Shadow DOM everywhere.** `snapshot`, `text`, `find`, `click_text`, `wait` and every locator see web components (open and closed roots, slots), so sites like caniuse read like any other page.
- **Frozen tabs don't freeze the agent.** Every page call has an 8 s budget; a tab that stops answering is reported as `TAB_WEDGED` in seconds, later calls fail fast after a 1.5 s probe, and `browser_navigate` / `browser_tabs reload` replace the frozen tab in place (the result carries the new `tabId`).
- **Coordinates when you need them.** Click, hover and wheel-scroll at `x`/`y`, triple-click, ctrl/shift-click, key sequences with `repeat`, and zoomed `region` screenshots that tell you how image pixels map to those coordinates.
- **Record and replay.** `browser_gif` records a flow as an animated GIF (clicks marked) for the user; `browser_shortcuts` saves a flow with `{{variables}}` and replays it in one call.
- **Several browsers.** Every connected Chrome profile is its own connection; `browser_list_browsers` / `browser_select_browser` pick one per session (one browser behaves exactly as before).
- **Honest errors.** Every tool failure reaches your agent as a real `isError` result with the full payload — no "success" responses hiding failures mid-workflow.

---
Expand Down Expand Up @@ -284,35 +290,36 @@ See [`agent-config/`](agent-config/) for manual installation or to customize the

## What It Can Do

25 tools. Every page-interaction tool takes a **`tabId`** (the one exception is `browser_navigate`, where it's optional).
30 tools. Every page-interaction tool takes a **`tabId`** (the one exception is `browser_navigate`, where it's optional).

**See**

| Tool | What it does |
|------|-------------|
| `browser_observe` | Compact atomic semantic observation with snapshot/document identity, geometry, state, and dynamic allowed actions |
| `browser_snapshot` | Accessibility tree with element refs. Compact mode (default) returns only interactive elements. Traverses open shadow DOM + same-origin iframes. |
| `browser_screenshot` | Capture a tab as an image over CDP — `maxWidth` / `scale` / `jpeg` to cut tokens, `fullPage` for the whole page |
| `browser_text` | Extract raw text from page or element |
| `browser_find` | Query elements by natural language — walks same-origin iframes too |
| `browser_snapshot` | Accessibility tree with element refs. Compact mode (default) returns only interactive elements; `filter` / `depth` / `ref` (subtree) / `maxChars` (default 20k) keep it small. Traverses open + closed shadow DOM, slots and same-origin iframes. |
| `browser_screenshot` | Capture a tab as an image over CDP — `maxWidth` / `scale` / `jpeg` to cut tokens, `fullPage` for the whole page, `region` to zoom; reports the pixel → x/y mapping |
| `browser_text` | Extract text from page or element (incl. shadow DOM); `mode:"article"` = main content only; `offset` paging |
| `browser_find` | Query elements by natural language ("search input", "Save button") — tokenized, role-aware, shadow DOM + same-origin iframes |

**Interact**

| Tool | What it does |
|------|-------------|
| `browser_act` | Safely click/type/select/focus/hover/keypress/scroll/upload against a `browser_observe` snapshot |
| `browser_click` | Real (trusted) click by ref or CSS selector — pierces same-origin iframes |
| `browser_click_text` | Click by visible text. Works through React portals and overlays |
| `browser_type` | Real key presses into inputs and contenteditable fields; returns the resulting value |
| `browser_press_key` | Real key presses and combos (`Enter`, `Tab`, `ctrl+a`) |
| `browser_click` | Real (trusted) click by ref, CSS selector or `x`/`y` — `clickCount` 1-3, `modifiers`; pierces same-origin iframes and shadow DOM |
| `browser_click_text` | Click by visible text (case-insensitive, shadow DOM too) with a real click on the owning control. Works through React portals and overlays |
| `browser_type` | Real key presses into inputs and contenteditable fields (or the focused field); returns the resulting value |
| `browser_press_key` | Real key presses, combos (`Enter`, `Tab`, `ctrl+a`), sequences (`"ArrowDown ArrowDown Enter"`) and `repeat` |
| `browser_batch` | Run several tool calls in one round-trip; stops at the first failure |
| `browser_scroll` | Scroll pages and virtual containers |
| `browser_hover` | Trigger tooltips and dropdowns |
| `browser_shortcuts` | Save a flow with `{{variables}}`, replay it in one call |
| `browser_scroll` | Scroll pages and virtual containers, or wheel-scroll at `x`/`y` |
| `browser_hover` | Trigger tooltips and dropdowns (ref, selector or `x`/`y`) |
| `browser_select` | Pick from native `<select>` dropdowns |
| `browser_wait` | Wait for elements to appear or disappear |
| `browser_fill_form` | Fill multiple form fields in one call (React/Vue-safe setters) |
| `browser_wait` | Wait for elements (any visible match), text, a URL change, or a delay |
| `browser_fill_form` | Fill multiple form fields in one call (React/Vue-safe setters; selects by value or label) |
| `browser_drag` | Drag element-to-element (uses CDP for reliability) |
| `browser_upload_file` | Upload files through `<input type="file">` (uses CDP, strict-CSP safe) |
| `browser_upload_file` | Upload files through `<input type="file">` (CDP, strict-CSP safe), or bytes / a screenshot into an input or a drop zone |

<details>
<summary><b>Uploading files — no file dialog</b></summary>
Expand All @@ -332,15 +339,18 @@ Paths are absolute and local to the machine running the browser. Omit `ref`/`sel

| Tool | What it does |
|------|-------------|
| `browser_navigate` | Go to a URL in a tab (`tabId` optional, defaults to active) |
| `browser_tabs` | List / create / close / focus / **lock** / **unlock** tabs |
| `browser_navigate` | Go to a URL in a tab (`tabId` optional, defaults to active); replaces a frozen tab |
| `browser_tabs` | List / create (`active:false` for background) / close / focus / reload / **lock** / **unlock** tabs |
| `browser_resize_window` | Resize or maximize the window holding a tab (responsive testing) |
| `browser_list_browsers` / `browser_select_browser` | See the connected browsers (profiles) and route this session to one |

**Debug & Advanced**

| Tool | What it does |
|------|-------------|
| `browser_console` | Console output (log, warn, error) — per-tab, capped at 200 entries |
| `browser_network` | XHR/fetch requests with status codes — per-tab, optional `limit` |
| `browser_console` | The page's console output (log, info, warn, error, debug, uncaught errors) — per-tab, capped at 200 entries; `pattern` / `level` / `limit` |
| `browser_network` | Requests with status codes and failures — per-tab; `urlPattern`, regex `filter`, `failed`, `limit` |
| `browser_gif` | Record the agent's actions in a tab as an animated GIF (clicks marked); export writes the file |
| `browser_evaluate` | Run JavaScript like the DevTools console: top-level `await`, last value returned, not blocked by CSP |
| `browser_handle_dialog` | Dismiss/accept an open alert/confirm/prompt via CDP (works on frozen pages) |
| `browser_run_action` | Run a self-contained JS action object via CDP |
Expand Down
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export default [
parserOptions: {
requireConfigFile: false,
babelOptions: {
presets: [['@babel/preset-typescript', { allowDeclareFields: true }]],
// Babel 8 always allows `declare` fields (the option was removed).
presets: ['@babel/preset-typescript'],
},
},
},
Expand Down
36 changes: 36 additions & 0 deletions extension/console-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* MAIN-world console capture. content.js runs in the extension's isolated
* world, where patching `console` only sees the extension's own calls — the
* page's console.log/warn/info/debug/error never reached browser_console.
* This script patches the PAGE's console and hands each entry to content.js
* as a JSON string on a private DOM event (object details don't cross
* worlds). Uncaught errors / rejections are still captured by content.js.
*/
(function () {
'use strict';
if (window.__bcConsoleMain) return;
Object.defineProperty(window, '__bcConsoleMain', { value: true });
const EVENT = '__bc_console_entry';
const LEVELS = ['log', 'info', 'warn', 'error', 'debug'];
const fmt = (a) => {
if (typeof a === 'string') return a;
if (a instanceof Error) return `${a.name}: ${a.message}`;
if (a && typeof a === 'object') {
try { return JSON.stringify(a); } catch { return Object.prototype.toString.call(a); }
}
return String(a);
};
for (const level of LEVELS) {
const orig = console[level];
if (typeof orig !== 'function') continue;
const patched = function (...args) {
try {
let text = args.map(fmt).join(' ');
if (text.length > 2000) text = text.slice(0, 2000) + '…[truncated]';
document.dispatchEvent(new CustomEvent(EVENT, { detail: JSON.stringify({ level, text }) }));
} catch { /* never break the page's logging */ }
return orig.apply(this, args);
};
try { console[level] = patched; } catch { /* frozen console */ }
}
})();
9 changes: 9 additions & 0 deletions extension/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
console.info = (...a) => capture('info', ...a);
console.debug = (...a) => capture('debug', ...a);

// Page console entries from console-main.js (MAIN world), JSON on a DOM event.
document.addEventListener('__bc_console_entry', (e) => {
let entry;
try { entry = JSON.parse(e.detail); } catch { return; }
if (!entry || typeof entry.text !== 'string') return;
if (entry.text.indexOf('ResizeObserver loop') !== -1) return;
try { chrome.runtime.sendMessage({ type: 'console', level: String(entry.level || 'log'), text: entry.text.slice(0, 2100) }); } catch {}
});

window.addEventListener('error', (e) => {
// Silence the well-known ResizeObserver loop warning: it's a benign browser
// notice (element resized during its own observation callback), not a real
Expand Down
17 changes: 17 additions & 0 deletions extension/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,23 @@ export function registerEventListeners() {
{ urls: ['<all_urls>'] },
);

// Requests that never completed (DNS failure, blocked, aborted, CORS…):
// onCompleted never fires for them, so without this they were invisible.
chrome.webRequest.onErrorOccurred.addListener(
(details) => {
if (details.tabId == null || details.tabId < 0) return;
const buf = getTabBuffer(networkByTab, details.tabId);
pushCapped(buf, {
method: details.method,
url: details.url,
error: details.error,
type: details.type,
timestamp: details.timeStamp,
});
},
{ urls: ['<all_urls>'] },
);

// A tab closing should release its lock and drop its buffers.
// NOTE: no hideLockShield here — the tab/page is already gone, so a shield
// inject would just throw (swallowed) and there is nothing to remove.
Expand Down
Loading
Loading