Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
fc4045e
build(tauri): upgrade to tauri 2.11.5 and @tauri-apps/api 2.11.1
xintaofei Sep 7, 2026
7c1377b
refactor(links): extract link classification and add the browser link…
xintaofei Sep 7, 2026
9b17313
feat(browser): scaffold Rust-owned browser tabs on a wry child surface
xintaofei Sep 7, 2026
e1ab171
feat(browser): isolated-world channel, macOS WebKit shim and popup ad…
xintaofei Sep 7, 2026
1de808b
feat(browser): browser tab records, live-state store and event bridge
xintaofei Sep 7, 2026
f5b719d
feat(browser): tab view with native surface host, occlusion leases an…
xintaofei Sep 7, 2026
b6425e2
fix(browser): bind occlusion leases to overlay DOM and sweep orphaned…
xintaofei Sep 7, 2026
2bfd94d
feat(browser): route transcript and terminal links through the link d…
xintaofei Sep 7, 2026
5bb2044
feat(browser): turn modifier-clicks into background tabs and guard ca…
xintaofei Sep 7, 2026
3e7dc92
feat(browser): place modifier-click tabs right after their opener
xintaofei Sep 7, 2026
4317fef
feat(browser): settings section, real DevTools/surface preferences an…
xintaofei Sep 7, 2026
0e4001d
fix(browser): keep owned windows visible and raise them on show
xintaofei Sep 7, 2026
56612bd
fix(browser): never read URLs through wry and surface failed navigations
xintaofei Sep 7, 2026
b21da17
fix(browser): make retry restart a navigation that never committed
xintaofei Sep 7, 2026
3cccf7f
feat(browser): give tabs their own profile and follow the app proxy
xintaofei Sep 7, 2026
067c242
fix(browser): show the error page when the requested page never arrives
xintaofei Sep 7, 2026
d4a89a4
feat(browser): keep browser tabs across restarts and unload idle ones
xintaofei Sep 8, 2026
257be7f
feat(browser): let pages download files into the downloads folder
xintaofei Sep 8, 2026
2366060
fix(browser): a download must not leave an error page behind
xintaofei Sep 8, 2026
8e02441
feat(browser): find in page with ⌘F
xintaofei Sep 8, 2026
fd6d0b3
fix(browser): close the races review found in persistence, downloads …
xintaofei Sep 8, 2026
3046674
fix(browser): order the create and destroy calls of one tab id
xintaofei Sep 8, 2026
4b5bac0
feat(browser): site rules, typed load errors and freeze frames under …
xintaofei Sep 8, 2026
ffd5ba4
fix(browser): close the gaps review found in site rules, load state a…
xintaofei Sep 8, 2026
198ccd4
fix(browser): hold only web clicks for the capabilities answer
xintaofei Sep 8, 2026
3d2c809
fix(browser): tie-break rules by restrictiveness, key rules by what t…
xintaofei Sep 8, 2026
c2e33ff
fix(browser): trim patterns as ASCII on both sides; make visibility c…
xintaofei Sep 8, 2026
1324cdf
fix(browser): trim exactly the characters the backend trims
xintaofei Sep 8, 2026
a9125df
fix(browser): name the rule score type
xintaofei Sep 8, 2026
27e7dbf
Merge branch 'main' into task/166
xintaofei Sep 8, 2026
562fa3c
feat(browser): optional action menu on terminal link clicks
xintaofei Sep 8, 2026
2c2b894
feat(browser): show HTML files through a codeg-doc document guest
xintaofei Sep 8, 2026
4005f85
fix(browser): close the gaps review found in the document guest
xintaofei Sep 8, 2026
80e1e9e
fix(browser): resolve every link on a document path before approving it
xintaofei Sep 8, 2026
3c64454
feat(browser): bridge dev-server ports into web-mode browser tabs
xintaofei Sep 8, 2026
ce440c8
fix(browser): keep bridge listeners apart and pair the grants
xintaofei Sep 8, 2026
4b83d7c
fix(browser): judge bridge requests without Fetch Metadata too
xintaofei Sep 8, 2026
0938ddc
fix(browser): believe forwarding headers only behind a declared proxy
xintaofei Sep 8, 2026
79f2546
feat(browser): keep tabs in separate profiles and present a sign-in i…
xintaofei Sep 8, 2026
67f6f7a
fix(browser): admit the identity switch only on allowed navigations a…
xintaofei Sep 8, 2026
5022222
fix(browser): count profile use while a deletion drains and pin popup…
xintaofei Sep 8, 2026
58c3a77
fix(browser): keep a profile in use until the engine has finished cle…
xintaofei Sep 8, 2026
a5c881d
fix(browser): decide a suspended tab's fate against the list it lands in
xintaofei Sep 8, 2026
94a79fa
fix(browser): compile the child surface on Windows
xintaofei Sep 9, 2026
43ea19c
feat(browser): give the tabs their Windows half
xintaofei Sep 10, 2026
3d4b7d8
feat(browser): say so when a tab loses its page channel
xintaofei Sep 10, 2026
2448e40
test(browser): give the idle sweep its own clock
xintaofei Sep 10, 2026
ccda865
feat(browser): answer the engine's multiple-downloads permission
xintaofei Sep 10, 2026
c8716f2
feat(browser): let Windows show local HTML through the document guest
xintaofei Sep 10, 2026
7c5a39d
fix(browser): show a download that landed on a network path
xintaofei Sep 10, 2026
856be89
fix(browser): keep a document guest's own addresses to its own host
xintaofei Sep 11, 2026
f09e7ed
test(browser): match the reworded degraded-channel bar
xintaofei Sep 11, 2026
9ed2274
fix(browser): read a windows path by either separator, a unix one by …
xintaofei Sep 11, 2026
65588b0
fix(office-watch): send the kill from the thread that asked for it
xintaofei Sep 11, 2026
9410210
fix(browser): register the send primitive before the world it belongs in
xintaofei Sep 11, 2026
b5b2838
fix(browser): take the sign-in identity back when a navigation never …
xintaofei Sep 11, 2026
c593f5e
fix(browser): let one frame hold one page-channel context
xintaofei Sep 11, 2026
6eafbb9
fix(browser): give the document on screen a world, whatever brought i…
xintaofei Sep 11, 2026
e6598f0
feat(browser): give the Linux surface a page channel and a shim
xintaofei Sep 11, 2026
f240695
feat(browser): let a Linux page open a window, on the same terms as e…
xintaofei Sep 11, 2026
ed7dec2
feat(browser): say when an owned window still answers, and let the pa…
xintaofei Sep 11, 2026
cb286a7
fix(browser): mark a download's navigation wherever a watcher will re…
xintaofei Sep 11, 2026
ec8daa5
fix(browser): install the channel wherever a surface can carry one
xintaofei Sep 11, 2026
3e9ba07
fix(browser): let the subframe primitive ask which frame it is, not w…
xintaofei Sep 11, 2026
0f3d36b
fix(browser): distrust the world a turned-away look can see
xintaofei Sep 11, 2026
aaa0d7b
fix(browser): keep the headless build out of GTK
xintaofei Sep 11, 2026
81026e5
fix(browser): ask whether a surface carries a channel, not which plat…
xintaofei Sep 11, 2026
410cb02
chore(browser): declare the WebKitGTK level the shim is built against
xintaofei Sep 11, 2026
4abefef
feat(browser): give an agent a tree of the page it can name elements in
xintaofei Sep 11, 2026
4e296bf
fix(browser): refuse a ref once the page it was read from has moved on
xintaofei Sep 11, 2026
848b8cb
feat(browser): let a person hand one page to an agent, and nothing else
xintaofei Sep 11, 2026
433dedc
feat(browser): show a person what an agent is doing to their page
xintaofei Sep 12, 2026
5005da1
feat(browser): let an agent ask for a page, and be told to ask the pe…
xintaofei Sep 12, 2026
f390e89
feat(browser): pin the program behind a loopback grant, not the process
xintaofei Sep 12, 2026
a220426
fix(browser): resolve CI failures in clippy, tsc, and prettier
xintaofei Sep 12, 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
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ jobs:
- name: Lint
run: pnpm eslint .

- name: Browser agent bundle matches its source
# `src-tauri/src/browser/js/agent.bundle.js` is committed so that a
# cargo build needs no node — `include_str!` reads it at compile time.
# A committed artifact is only worth anything if it is the one the
# source produces, and nothing else in the build would notice a stale
# one: the Rust side would happily embed last week's bundle.
run: pnpm browser:agent:check

- name: Browser agent typecheck
# `browser-agent/` is excluded from the app's tsconfig (its vendored
# half answers to Playwright's compiler settings, not ours), so the
# repo-wide typecheck in `pnpm build` never sees our entry point.
run: pnpm browser:agent:types

- name: Unit tests (vitest)
run: pnpm test

Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ src-tauri/resources/opencode/models-dev.json
src-tauri/resources/codex/bundled-catalog.json
docs/
pnpm-lock.yaml
browser-agent/vendor/
src-tauri/src/browser/js/
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ ENV CODEG_RUNTIME=docker
ENV CODEG_RESTART_DELAY_MS=2000

EXPOSE 3080
# Port bridge for dev servers (CODEG_BRIDGE_PORTS; default CODEG_PORT+1..+10).
EXPOSE 3081-3090
VOLUME /data

# Run under the built-in supervisor (PID 1) so an in-place upgrade can swap
Expand Down
86 changes: 86 additions & 0 deletions browser-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# browser-agent

The script a browser tab's **isolated world** runs when an agent needs to read
the page. Built by esbuild into `src-tauri/src/browser/js/agent.bundle.js`,
which is committed.

```
pnpm browser:agent # build the bundle
pnpm browser:agent:check # is the committed bundle the one this source makes?
pnpm browser:agent:types # typecheck (esbuild does not)
pnpm browser:agent:probe # drive the bundle in real Chrome
```

## What it is

`vendor/playwright/` is Playwright's aria tree, copied byte-for-byte at v1.63.0
(see `vendor/playwright/VENDOR.md`). `src/index.ts` calls it in **`ai` mode** —
the mode Playwright MCP uses — and puts `snapshot` and `elementForRef` on
`globalThis.__codegAgent` for Rust to call through world-scoped eval.

`ai` mode is why there is no second pass over the DOM here. It gives a ref to
every element that is _visible and receives pointer events_, so a `<div>` with
`cursor: pointer` and a click handler and no ARIA role is namable, and carries
`[cursor=pointer]` to say why. An earlier plan for this package described
writing that promotion ourselves; upstream had already made it unnecessary.

## Two things it does that upstream does not

**Refs are answerable across a navigation.** Playwright's ref counter lives in
the module, so a fresh document starts again at `e1` — two pages use the same
names for different elements. Each world here draws a random `generation` and
reports it with every snapshot; `elementForRef` refuses a ref that quotes an
older one. A new document destroys the world, so the next snapshot is a new
generation and every ref an agent still holds is refused rather than resolved
onto whatever now happens to be `e1`.

A new document is not the only kind of navigation, and the other kind is the
common one here: `pushState`, `replaceState` and hash changes leave the
document, the world and the generation exactly as they were while the page
becomes a different page. That is a route change in a single-page app — which
is most of what a dev server serves — and the elements a framework keeps
across one, the header and its buttons, are exactly the ones that would still
resolve. So a snapshot also records the address it was taken at, and a ref is
refused once the page has moved. The error runs in the safe direction: a
caller told to snapshot again loses a round trip, a caller handed the wrong
element loses the user's page.

**Where that stops, and who takes over.** An address is not an identity. A
route that goes A → B → A arrives back at a string that matches, on a page
whose framework may have kept the DOM node and given it new meaning, and this
world cannot see that it happened: the page's own `history.pushState` is
invisible from an isolated world, because patching `History.prototype` here
patches _this_ world's prototype while the page calls a different function
object — the same isolation that keeps `__codegAgent` out of the page's reach.

So the world enforces three floors it can check by looking — a new document, a
moved address, a departed element — and `snapshot({ epoch })` mixes a host
token into the generation an agent echoes back. Deciding _when_ refs die is
the host's, because the host is the only party that sees the navigation.

The probe measures the world's side of that, including the A → B → A case that
the floors do not catch, and the premise underneath it: it patches
`History.prototype.pushState` in the world, has the _page_ navigate, and
checks that the patch never fired while the address moved anyway. It cannot
measure the host's side, because there is no host here yet.

**The tree can be capped.** `maxChars` cuts on a line boundary, so an agent
never reads half a node, and the result says `truncated` so it knows to narrow
the question rather than believe the page ended. A cap that lands inside the
very first line has no boundary to use; the cap wins there and the line is cut
where it falls.

## What is not here

Nothing reads this bundle yet. The Rust seam is where **authorization** lives —
`none / read / control`, per tab and origin, with no automatic grants — and
until that exists there must be no code path that reads a page on an agent's
behalf. The bundle and the grant model land together, in the package that adds
the `browser_*` tools.

## Where it runs

The world is separate from the page: `__codegAgent` is not reachable from page
script, and page code cannot forge a ref or observe a snapshot. The probe
asserts this. What the probe cannot assert is the engine — it drives Chrome,
while the three platforms ship WKWebView, WebView2 and WebKitGTK.
Loading
Loading