# Repository Guidelines

Repo: https://github.com/platonai/Browser4

## Architecture

```
browser4-cli (Rust)  ──MCP over HTTP──▶  browser4-rest (Kotlin/Spring)  ──▶  PulsarWebDriver (Kotlin/CDP)
     ▲                                           │
     │                                           ▼
     └──── e2e tests ────▶  Fixture HTTP server (Rust test harness)
```

- **CLI:** `cli/browser4-cli/` — Rust binary, MCP tool calls over HTTP
- **Backend:** `browser4-rest/` — Spring Boot, `MCPToolController` dispatches tools
- **Browser driver:** `browser4-core/browser4-browser/` — `PulsarWebDriver` wraps CDP
- **Agent tools:** `browser4-agentic/` — `AgentToolManager` maps MCP tool names → driver methods

### Dispatch chain (CLI → browser)

1. CLI builds MCP tool call: `{tool: "browser_type", arguments: {ref: "#el", text: "hi"}}`
2. `MCPToolController.callTool()` → `dispatchToToolExecutor()`
3. `normalizeFrontendToolCall()` applies `FRONTEND_TOOL_NAME_ALIASES` (e.g., `browser_type` → `fill`)
4. `DefaultArgumentNormalizer` maps `ref` → `selector`, strips `sessionId`, converts snake_case
5. `resolveMcpToolCall()` → `ToolCall("tab", "fill", args)`
6. `AgentToolManager.execute()` → `executor.callFunctionOn(toolCall, driver)` → `PulsarWebDriver.fill()`

### Batch commands

`handleCommandBatch()` → `handleBatchTool()` in `MCPToolController`. CLI's `compile_batch_request()` builds step arrays with `op: "tool"`. `preFocusSelector` only added for `keydown`/`keyup` (not fill/type/press).

### Known CDP pitfalls

- **crbug.com/444929150:** `Input.dispatchMouseEvent` type `mouseWheel` race condition in headless Chrome. Fix: dispatch to `{passive: false}` wheel listener.
- **Cursor positioning:** `DOM.focus()` + `Input.dispatchMouseEvent` (click) may leave cursor at 0. Fix: `setSelectionRange(99999, 99999)` after focus+click.
- **`Input.insertText` racing:** 0ms delay between chars drops `input` events. Fix: use same inter-char delay as `type()` via `randomDelayMillis("type")` (90-240ms).

### Direct CDP methods: mandatory review gates

Any method that implements behavior by calling CDP directly (or by evaluating
JavaScript that synthesizes browser events) — `drag`, `press`, `click`, dialog
handling, screenshots, scroll, storage restore, etc. — must satisfy ALL of the
following before it is considered done. "It works on my machine" is not
sufficient; each gate below has burned this project before.

1. **Anti-bot detection** — the page can observe the automation, and many sites
   actively fingerprint it. Consider:
   - `event.isTrusted` is `false` for JS-synthesized events; if a trusted path
     exists (CDP `Input.*`, `userGesture=true`), prefer it or document why not.
   - Fixed inter-event delays and exact element-center coordinates are
     fingerprints; randomize delays (via `randomDelayMillis` or an equivalent
     bucket) and jitter coordinates like real input would.
   - The full input story: real drags/clicks have trusted `mousedown` →
     `mousemove` → `mouseup` context; a synthetic event sequence with no
     trusted accompaniment is itself detectable.
2. **Negative impact** — check the failure modes, not just the happy path:
   - No silent failures: a method must fail loudly rather than dispatch onto
     an unrelated element (occlusion, `pointer-events:none`, layout shift,
     frames). Validate before dispatching so retries stay idempotent.
   - Error messages must surface the real cause to the CLI/user instead of
     being swallowed or wrapped by retry machinery.
   - Retry safety: blocks inside `invokeOnPage`/`invokeOnElement` must be
     idempotent or the deterministic failures must bypass the retry.
   - Don't regress error semantics of the upstream method you override.
3. **Cross-platform compatibility** — headless vs GUI Chrome, Docker/CI,
   Windows/Linux/macOS all behave differently:
   - Scroll commits and layout updates are asynchronous (`scrollIntoViewIfNeeded`
     may prefer smooth animation); poll for the observable state instead of
     assuming a single read is current.
   - Event dispatch timing, renderer scheduling, and viewport geometry differ
     under load and in containers; never rely on a single hardcoded delay.
   - CDP API availability varies by Chrome version; provide fallbacks or
     explicit errors, never a silent no-op.
4. **Real-webpage testing** — unit tests alone are insufficient:
   - Add a fixture page (under
     `browser4-tests/pulsar-tests-common/src/main/resources/static/b4/`) that
     exercises the real behavior: event order, payloads, occluded/disabled/
     off-viewport/frame targets, async layout changes.
   - Prove it against a real browser through the e2e harness
     (`cargo test --test e2e -- --scenario=...`, `requires_browser4: true`),
     not only against mocks.

## Project Structure

| Module | Description |
|---|---|
| `browser4-core` | Core engine: sessions, scheduling, DOM, browser control |
| `browser4-dependencies` | BOM and dependency alignment |
| `browser4-tools` | Operational tools and launch helpers |
| `browser4-agentic` | AI agents, MCP, skill registration |
| `browser4-agent-tools` | High-level agent tools: scraping, crawling, stateful page interaction |
| `browser4-rest` | Spring Boot REST layer & command endpoints |
| `cli/browser4-cli` | Rust CLI binary |
| `skills/browser4-cli` | AI agent skill definitions |
| `browser4-apps/browser4-standalone` | Product packaging, unified launcher (`target/Browser4.jar`) |
| `examples/browser4-examples` | Runnable examples |
| `browser4-tests` | E2E, integration, scenario tests |
| `browser4-tests/pulsar-tests-common` | Shared test base classes and utilities |
| `cdp-protocol` | Chrome DevTools Protocol JSON definitions |
| `coworker/` | File-queue automation for task-driven AI workflows |

## Build & Test

### Quick commands

```bash
# Build (skip tests)
./mvnw -DskipTests                           # Linux/macOS
.\mvnw.cmd -q -D"skipTests"                  # Windows PowerShell

# Rust unit tests (fast, no backend needed)
cd cli/browser4-cli && cargo test --bin browser4-cli

# Kotlin tests
mvn test -pl browser4-rest -am
mvn test -pl browser4-rest -am -Dtest=MCPToolControllerTest

# E2E tests (needs running backend or mock server)
cargo test --test e2e -- --nocapture
cargo test --test e2e -- --nocapture --scenario=test_e2e_batch_*

# Scoped test runs (Windows)
bin/test.ps1 fast|it|e2e|rest|skills|mcp|cli|browser4|mock-site
```

Maven profile switches in root `pom.xml`: `-DrunITs=true`, `-DrunE2ETests=true`, `-DrunCoreTests=true`, `-DrunRestTests=true`.

### E2E test filtering

```
cargo test --test e2e -- --help           # All options
--scenario <pattern>                      # Glob filter
--group <name>                            # Group filter (repeatable)
--level BASIC|EXTENDED|ALL                # Test depth
--fail-fast / --failed                    # Stop early / rerun failures
--list / --list-groups                    # Discover without running
--enable-install-scenario                 # Opt into install tests
--enable-batch-scenario                   # Opt into batch tests
--force-rebuild-bundle                    # Force local Maven + runtime rebuild
--force-remote-bundle                     # Download pre-built bundle instead
```

### Test locations

| Scope | Path |
|---|---|
| Unit tests | `src/test/kotlin/...` |
| Integration | `browser4-tests/pulsar-it-tests/` |
| E2E | `browser4-tests/pulsar-e2e-tests/` |
| REST integration/E2E | `browser4-tests/browser4-rest-tests/` |
| Shared utilities | `browser4-tests/pulsar-tests-common/` |
| Rust E2E | `cli/browser4-cli/tests/e2e/` |

## Code Style

### Kotlin
- Immutable `data class`; explicit return types; null-safety (`require`/`check`/`?:`)
- Public APIs require KDoc
- Store AI-generated task docs in `docs-dev/copilot/`

### Logging
```kotlin
logger.info("Task {} finished in {} ms", taskId, cost)  // placeholders, never concatenation
```

### Naming
- Test methods: camelCase + `@DisplayName("...")` — **NOT** backtick naming
- Test classes: `<Name>Test.kt` (unit), `<Name>IT.kt` (integration), `<Name>E2ETest.kt` (e2e)

## Testing Guidelines

**Default policy:** Don't run full suites. Compile with tests skipped, run smallest relevant scope. Upgrade scope when risk increases (cross-module, public API/DTO/serialization, Spring wiring, dependency bumps, concurrency/I/O, browser lifecycle).

**Tag-driven scheduling** — use tags from `docs/TESTING.md`:
- Scope: `Unit`, `Integration`, `E2E`, `SDK`
- Speed: `Fast` (<5s), `Slow` (5–30s), `Heavy` (>30s)
- Gates: `Requires*`, `ManualOnly`

**Coverage targets:** Global ≥70%, Core ≥80%, Utilities ≥90%, Controllers ≥85%

**CI has two gates with different Maven test scopes** — pick the tags for the gate you are writing tests for:

| Gate | Workflow | Maven `excluded_groups` | Scope |
|---|---|---|---|
| PR Quality Gate | `.github/workflows/pr.yml` | `ManualOnly,RequiresAI,E2E,E2ETest,Slow,Heavy,HeavyTest,Integration,IntegrationTest,RequiresServer,RequiresBrowser,RequiresDocker,TestInfraCheck` | fast/unit only (`run_pulsar_tests: 'false'`) |
| CI/CD Pipeline (main + release tags) | `.github/workflows/ci.yml` | `ManualOnly,RequiresAI,E2E,E2ETest,Slow,HeavyTest,TestInfraCheck` | adds integration/infra tests that need Chrome, Docker and the started app |

Both gates pass `-Dsurefire.excludes=**integration**` (class-file pattern, not tags) and both derive success from the surefire XML totals — a test class is skipped by **tag**, never by name. `SDK` is excluded by neither gate (no test carries that tag today); `Heavy` is excluded only by the PR gate, `HeavyTest` by both. `.github/workflows/ci.yml` also builds all-main-modules, starts a Dockerized app on port 8182 and runs `cargo test` in `cli/browser4-cli`.
See [CI stabilization notes](docs-dev/copilot/ci-stabilization-4.13.x.md) before changing either list.

## Configuration

- Default port: **18182**
- Config files: `application.properties` → `application-*.properties` → `application-private.properties` (git-ignored, secrets here or env vars)
- Key properties: `openrouter.api.key`, `browser.profile.mode` (DEFAULT|SYSTEM_DEFAULT|SEQUENTIAL|TEMPORARY), `browser.display.mode` (GUI|HEADLESS|SUPERVISED)
- Display mode precedence: session capabilities (`headed` / `displayMode`, e.g. from `open --headed`/`--headless`) override the server-wide `browser.display.mode` default at browser launch (`AbstractPulsarSession.createBoundDriver`); server default applies only when the session has no display preference
- LLM providers configured via env vars: `DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `VOLCENGINE_API_KEY`, `OPENAI_API_KEY`

## Development Patterns

### Adding a `browser4-cli` command

1. **`commands.rs`** — Add `CommandDef`: CLI name kebab-case, MCP tool `browser_`-prefixed snake_case, map args in `tool_params_fn`
2. **`MCPToolController.kt`** — Add frontend alias so `browser_my_tool` resolves to internal name `my_tool`
3. **Backend tool** — Reuse existing when possible; if new capability needed add `@MCP` method in `WebDriver.kt`, implement in concrete driver. Add explicit `BrowserTabToolExecutor` case only for non-trivial parameter mapping
4. **`main.rs`** — Update only for: custom dispatch, dynamic tool-name selection, stale-session recovery, `no_snapshot_commands()`, or custom batch handling in `compile_batch_request()`
5. **Docs** — Update `skills/browser4-cli/SKILL.md`; extensive docs → `skills/browser4-cli/references/<topic>.md`
6. **Tests** — `commands.rs` unit tests → controller mapping tests → `e2e.rs` → `MCPToolControllerE2ETest.kt`
7. **Common failures:** missing backend alias, omitted `sessionId`, forgetting `no_snapshot_commands()`/`batch_supported`, element-ref parameter name mismatches, snake_case/camelCase normalization

### REST-based commands (swarm, crawl)

- `tool_name_fn` returns `""`; dispatch entirely in `main.rs` via custom `handle_*`
- HTTP in `http.rs`: `submit_*` → POST `/api/<resource>`, `get_*_result` → GET `/api/<resource>/{id}/result`
- Backend: `@RestController` + `@Service` + `ConcurrentHashMap` task store + `CoroutineScope`
- Return task UUID from POST; CLI polls for completion. No MCP alias needed.

### Snapshot-related commands

Category `Category::Snapshot`:
- `htmlsnapshot get` / `query` — HTML retrieval with pagination (`-limit`, `-offset`)
- `htmlsnapshot grep` — regex search (`-i`, `-v`, `-F`, `-w`, `-A/B/C`, `--selector`)
- `htmlsnapshot inspect` — CSS selector discovery for recurring patterns
- `htmlsnapshot summary` — compressed Web Page Summary Index
- `snapshot` — live a11y snapshot with `--boxes`, `--stdout`, `--limit`

### Modifying install/uninstall/upgrade code

After changing `cli/browser4-cli/src/daemon.rs`, run install-scenario e2e tests:
```bash
cargo test --test e2e -- --nocapture --level ALL --enable-install-scenario --scenario '*install*'
```

## PowerShell Cross-Platform Compatibility

- Use `$IsWindows`, `$IsLinux`, `$IsMacOS` for platform branching
- Avoid Windows-only cmdlets; use `Join-Path` / `[System.IO.Path]::Combine()`
- Prefer `$env:HOME` over `$env:USERPROFILE`
- Shebang: `#!/usr/bin/env pwsh`
- **When fixing one `.ps1`, check siblings for the same issue**

## Test Script Portability

Scripts under `browser4-tests/tests-production/` and `bin/test-production.ps1` test globally-installed `browser4-cli`. They must **never** depend on: git, repo root, source code, Maven/Cargo build outputs. Use `$PSScriptRoot` for sibling references. Repo-awareness must be opt-in with clear error messages when absent.

## Coworker Automation

File-queue system for task-driven AI workflows (`coworker/`). Task files (Markdown with optional `Title:`/`Prompt:` headers) route through state directories: `0draft/` → `1ready/` → `2working/` → `3complete/` (or `3aborted/`, `4review/`, `5approved/`). See [Coworker SKILL.md](coworker/SKILL.md).

## Definition of Done

- [ ] Build and related tests pass
- [ ] No new high-noise logs or warnings
- [ ] New/changed logic has tests (main path + edge case)
- [ ] No secrets or private endpoints committed
- [ ] No arbitrary version changes (follow parent BOM)
- [ ] Documentation updated for public behavior changes
- [ ] Performance impact assessed if >5%

## Common Issues

| Issue | Solution |
|---|---|
| `.ps1` scripts don't run on Linux | `sudo apt-get install -y powershell`, then `pwsh script.ps1` |
| `mvnw` no execute permission | `chmod +x mvnw` |
| JDK version mismatch | JDK 17+ in `JAVA_HOME` |
| Windows parameter escaping | `-D"key.with.dots=value"` |
| Port 8182 in use | Override `server.port` in root `application.properties` |
| BrowserProtocol retry log storms | Use existing retry utilities, lower log level |

## Documentation Update Rule

When a user says **"update documents"** (or "update docs", "refresh documentation"), update ALL of the following that reference the changed feature:

1. **`README.md`** and **`README.zh.md`** — root-level project readmes
2. **`skills/browser4-cli/SKILL.md`** — primary CLI skill document
3. **`skills/browser4-cli/references/*.md`** — reference docs (htmlsnapshot.md, x-sql.md, etc.)
4. **`cli/browser4-cli/README.md`** — CLI-specific README
5. **`cli/browser4-cli/src/help.rs`** — CLI help text generation
6. **`cli/browser4-cli/src/tips.rs`** — CLI tips/hints shown to users
7. **Any other `.md` files** in `docs/`, `skills/`, or project root that reference the changed command or feature

When adding a new CLI option or changing command behavior, always check these locations.

## Documentation References

- [Testing Taxonomy](docs/TESTING.md)
- [Build from Source](docs/build-from-source.md)
- [Configuration Guide](docs/config.md)
- [CLI Skill Guide](skills/browser4-cli/SKILL.md)
- [ARIA Snapshots](docs/aria-snapshots.md)
- [HTML Snapshot](docs/htmlsnapshot-inspect-summary.md)
- [Eval Command Output](docs/eval-command-output.md)
- [Load Options Guide](docs/load-options-guide.md)
- [Crawl Checkpoint & Resume](docs/crawl-checkpoint-resume.md)
- [Mock Site](docs/mocksite.md)
- [QL Functions Guide](docs/ql-functions-guide.md)
- [Coworker Automation](coworker/SKILL.md)

---

*Last updated: 2026-08-25*
