Commit Graph
12 Commits
Author SHA1 Message Date
Ryan Vogel 27c09b4d3a feat(browser-extension): sidebar as a plain client, browse tools, and pairing setup
The side panel no longer brings its own browser tools into opencode. It is an
opencode client like the desktop app and TUI; agents drive the browser through
the `browse` MCP server (renamed from opencode-browser), and setup pairs the
extension with opencode instead of using a native messaging host.

Removed / replaced:
- The per-session browser attachment (session-browser, page, cdp, files,
  profiling, analysis, diagnostics, preview): the panel no longer attaches
  `browser.*` tools or writes instruction entries. Instead, each prompt sent
  from the panel is preceded by a synthetic "Browser context" message
  (resume: false) naming the page; a tab shared with agents comes with its
  Chrome tab id. Sent only when the page or its sharing changed.
- The opencode plugin (site_scripts, browsing, browser.tabs.request) and its
  relay-link: these are browse MCP tools now (site_scripts_*, browsing_*,
  tabs_request), sent through the relay as `extension.request`
  (POST /extension/request, 10-minute timeout for the user's answer).
  tabs_request adopts the approved tab into the session.
- The native messaging host and service discovery: the extension keeps a list
  of paired servers (background/servers.ts, chrome.storage.local). connect.html
  redeems a one-time code from opencode's own pairing (POST /api/pair,
  GET /auth/connect/:code -> 30-day token used as the password) and renews the
  token a week before expiry. Loopback links pair right away; others ask.
  Servers can also be added by hand with a code or the service password; the
  directory menu switches, adds, and forgets servers.
- Permissions nativeMessaging, downloads, webNavigation.

Setup (cli/src/setup.ts):
- `install` writes the `browse` MCP entry (renaming the old one), removes the
  old host and plugin, then runs `connect`.
- `connect` finds browsers/profiles with the extension (default browser
  first; macOS-protected profiles are reported as unknown and asked about),
  opens connect.html#server=&code= there via the browser's executable and
  --profile-directory, opens the Web Store listing when missing, and prints
  the link and code when headless (no TTY, SSH, no display, --no-open).
- The relay reports the extension's build in /extension/status; install
  checks the build changed after asking for a reload, and says so (and skips
  connect) when it didn't.

Fixed: the extension's runtime.reload closed its socket with code 1001,
which browsers reject (only 1000 or 3000-4999), so the reload threw and never
ran; every install since reported "Reloaded" while Helium kept the old
build. It closes with 1000 now.

Also: session_adopt / `session adopt --tab-id` adopt a tab by Chrome tab id;
"Share tab" above the composer lets agents use the current tab; agent guide
renamed to skills/browse; store listing and privacy policy updated.

Verified in the user's Helium: `connect --yes` detected Helium/Ryan (default),
Chrome unreadable (macOS privacy), Arc without the extension, opened the
connect page, which showed "Connected · This computer · 4102". Through the
browse MCP server: site_scripts_list, tabs_request (returned the tab id and
adopted it), session_adopt({ tabId }) (success, exclusivity error, unknown-tab
error). The synthetic message with metadata was accepted and stayed queued
(resume: false). A clean headless Chromium loads the build and the connect
page answers. Install now reports a stale reload instead of claiming success.
Typecheck (extension, cli), lint. Upstream relay suite: 871/888; the new
failures are the 9 MCP tests asserting the old server name.
Not yet verified: the panel UI (server menu, Share tab, composer flow).
2026-10-06 16:39:48 -04:00
Ryan Vogel a73b58c031 feat(browser-extension): drive pages with Patchright so they can't detect the debugger
Bot checks (deviceandbrowserinfo.com's isAutomatedWithCDP and its worker
variant) override Error.prepareStackTrace and console.log an Error: with
CDP's Runtime domain enabled, Chrome serializes console arguments for the
debugger and calls the override. Playwright enables Runtime on every page and
worker. Patchright (patchright-core, a Playwright fork pinned to the same
1.63.0) never does.

- playwright-core is replaced by patchright-core (same API; two runtime
  imports, the rest types).
- Without Runtime, a page's console messages and uncaught errors don't reach
  execute. pageConsole.start()/stop() (new page-console.ts) enables Runtime
  on that one page through its own CDP session, drops what Chrome replays
  from before, and feeds the same execute logs. Documented in the skill and
  the MCP execute description.
- patchright-tuning.ts: Patchright finds elements by walking the DOM over CDP
  (for closed shadow roots), and pulled the whole DOM tree before every
  count()/evaluateAll()/all(). Through the extension, getByRole("link").count()
  on a 4,000-element Wikipedia page took ~650-1,900 ms vs Playwright's ~9-29
  ms. count() and querySelectorAll() go back to Playwright's lookup (injected
  script in the isolated world, which pages can't see) and the shadow-root
  scan is off; Playwright never reached into closed shadow roots either. The
  internals are reached through the first page's main frame; if an update
  renames them, lookups stay correct but slower and relay.log records
  patchright.tuning.

Verified in the user's Helium:
- deviceandbrowserinfo.com: isAutomatedWithCDP and isAutomatedWithCDPInWebWorker
  gone; only hasInconsistentWorkerValues remains (Helium reports
  hardwareConcurrency 16 on pages, 18 in workers; neither engine touches it).
  Sannysoft 31/31, BrowserScan 18/18 normal, reCAPTCHA v3 0.9, CreepJS 0%
  headless/stealth, Fingerprint scraping demo not blocked: unchanged.
- Upstream test suite: 880/888, the same 8 intentional failures as before.
- Benchmark page (form, iframe, 200-item list, snapshot, screenshot), 3 runs
  each: medians within noise, identical form results and snapshot hashes.
- Wikipedia: count/all/evaluateAll/textContent/isVisible within a few ms of
  Playwright with the same values; click, hover, fill, waitForSelector same.
- pageConsole: off by default (no page logs, page can't detect a debugger);
  start() captures log/warn/error/pageerror across executes and reloads;
  stop() makes the page undetectable again.
- Mouse/typing/scroll humanization unchanged.
2026-10-06 12:14:58 -04:00
Ryan Vogel bf81e11848 feat(browser-extension): scroll smoothly instead of jumping
Agent scrolling stuttered: Playwright's mouse.wheel is one wheel event (the
page jumps by the whole delta in one frame), and before acting on an
off-screen element Playwright calls DOM.scrollIntoViewIfNeeded, which snaps
the page there instantly.

- Each wheel event is now split into an eased ~60 Hz burst (quick start,
  slowing to a stop) with the original event carrying the last frame.
- DOM.scrollIntoViewIfNeeded on the top frame first scrolls toward the
  element with wheel flicks of 450-900 px from where the pointer rests,
  re-measuring after each and stopping when the element stops moving; the
  original command then only fine-tunes. Logged as input.scroll.
- relay.ts imports human-input by name instead of a star import (repo style).

Verified in the user's Helium: wheel(0, 800) now moves over 28 frames, at most
80 px per frame (was 800 px in one frame); clicking a button 2,600 px down
scrolls over 105 frames, at most 83 px per frame, in 3.1 s. cli typecheck, lint.
2026-10-06 11:54:56 -04:00
Ryan Vogel d494106add feat(browser-extension): make agent mouse and keyboard input look human
Playwright teleports the pointer: one mouseMoved straight to the exact center
of the target, then a press and release ~50 ms apart. The events are trusted,
but behavioral bot detection scores exactly that shape.

The relay now paces Input.* commands per tab (cli/src/human-input.ts):
- each pointer move is preceded by a curved trajectory from the last position
  (cubic Bezier with a sideways bow, minimum-jerk speed, Fitts'-law duration,
  fading tremor, occasional overshoot and correction), sent at ~60 Hz with the
  round trip counted toward each gap so samples don't arrive in bursts;
- clicks land 1-3 px off center (same offset for move, press, release), after
  a 30-130 ms settle, with a 60-140 ms hold;
- key presses are spaced 30-110 ms apart;
- one tab's input runs in order, since Playwright doesn't always wait for the
  move before pressing (that interleaved two paths in the first attempt).
Each movement is logged to relay.log as input.move. OPENCODE_BROWSER_HUMAN_INPUT=0
turns it off. The skill tells agents to use pressSequentially where typing is
watched.

Verified in the user's Helium: a 690 px click went from 1 mousemove in 0 ms
to ~45 moves over ~650 ms (path ratio ~1.03, bow 40-80 px, 15-20 ms gaps),
clicks hit 1-3 px off center, typing 40-110 ms per key; a 4-click httpbin
form still submits correctly (1.7 s -> 3.7 s); bot.sannysoft.com 0 failed,
BrowserScan 18/18 normal, reCAPTCHA v3 score 0.9. cli typecheck, lint.
2026-10-06 11:49:04 -04:00
Ryan Vogel 4633044ef0 fix(browser-extension): don't leave loose agent tabs after a reload, and reload on install
Reloading the extension (or an extension update) emptied its memory and
session storage, so on reconnect it ungrouped every relay group's tabs it no
longer recognized: the groups vanished and the agents' tabs stayed open as
ordinary tabs.

- Which tabs agents opened (tab cleanup) and which groups are agents'
  (collapse/placement) now live in chrome.storage.local, cleared on browser
  startup since tab and group ids don't survive a restart.
- After a reconnect, tabs an agent opened stay in their group (the relay
  reclaims named sessions' tabs, idle cleanup closes the rest); only tabs the
  user lent an agent go back to ordinary tabs.
- install reloads a connected extension through the relay (new POST
  /extension/reload, sending runtime.reload) after replacing the unpacked
  files, waiting up to 5s for a just-restarted relay to see it, so updates
  apply without a trip to chrome://extensions.
- The extension's hello carries its build time and the relay logs it, so a
  browser running an old build shows in relay.log.

Verified: extension and cli typecheck, lint. In the user's Helium,
POST /extension/reload reconnected the extension with the new build within a
second (relay.log). The reload-without-ungrouping path couldn't be checked
headless (Chrome for Testing blocks a command-line extension after
runtime.reload).
2026-10-06 11:35:39 -04:00
Ryan Vogel a8f4716311 feat(browser-extension): close idle agent tabs after 10 minutes
Agent tabs unused for 10 minutes (was 30) now close, checked every minute (was 5), so a tab goes within about 11 minutes. The agent guidance, skill, MCP tool, and CLI flag descriptions say 10.

Verified: extension and cli typecheck, cli build.
2026-10-06 11:31:26 -04:00
Ryan Vogel 3ca1cc981b fix(browser-extension): make updates take effect in running relays and MCP servers
After reinstalling, the running relay and every MCP server opencode had
started kept the old code, and the relay's build check then refused them
("relay build ... is newer than CLI build"; restart the client). Install now:
- writes the runtime's fingerprint (sha256 of dist/cli.mjs) into the
  opencode-browser MCP entry's environment, so the entry changes whenever the
  runtime does and opencode restarts the server with the new code;
- restarts a running relay whose build no longer matches (detected with
  `status --json`'s stale flag), keeping tabs open.

Verified: cli typecheck and build; reinstalling on this machine updated the
MCP entry, restarted the stale relay, opencode reloaded the MCP server (26
tools), and an MCP execute in the user's browser returned HN's newest stories.
2026-10-06 10:59:06 -04:00
Ryan Vogel fe369ae72d feat(browser-extension): name agent tab groups and close idle agent tabs
Agent tab groups had opaque names (every conversation's group was
"opencode"; MCP/CLI sessions showed ids like mcp-48092553), and tabs agents
opened stayed open forever.

Names:
- A conversation's group is named after the conversation title (cut to 28
  characters; "opencode" until it has one), refreshed when it groups a tab.
- An agent session's group uses the session's chosen name ("github"); ids
  the relay or MCP made up (mcp-<hex>, adjective-animal-###) become
  "Agent · <site>", following the tab's navigation. Each session keeps one
  group by id, so renames no longer split its tabs.

Cleanup (background/tab-cleanup.ts):
- Tracks only tabs agents created (browser.tabs.open, relay tabs.create);
  an agent command or the user switching to the tab counts as use. Tabs the
  user shared are never tracked; pinning a tab or dragging it out of the
  group hands it to the user.
- Every 5 minutes, closes tracked tabs unused for 30 minutes
  (chrome.storage.local tabCleanupIdleMinutes; 0 turns it off), skipping
  the active tab of the focused window, tabs playing audio, recording, or
  waiting for a handoff. State survives worker restarts.
- On demand: "Close agent tabs" in the panel's agents menu, the relay
  command tabs.cleanup, POST /tabs/cleanup, the MCP tool tabs_cleanup, and
  `opencode-browser tabs cleanup [--idle-minutes N]`.
- The skill and the in-session guidance tell agents to name sessions,
  close their tabs, and that idle tabs close by themselves.

Verified: extension and cli typecheck, lint. Headless Chrome against an
isolated relay: a "github" session's tab was grouped as "github", an
unnamed session's as "Agent · iana.org", and `tabs cleanup --idle-minutes 0`
closed both and their groups. The conversation group name and the 30-minute
alarm were not exercised end to end.
2026-10-06 10:48:47 -04:00
Ryan Vogel b679f7bb4b fix(browser-extension): install a packed build with node, not the source runner
With OPENCODE_BROWSER_PACKAGE set from a source checkout, install reported "(from source)" and wrote a bun host wrapper for the installed runtime. A package spec now always means a real runtime install run with node.

Verified: cli typecheck; installing a packed 0.0.2 on this machine wrote a node host wrapper and MCP entry pointing at the runtime.
2026-10-06 10:37:50 -04:00
Ryan Vogel 476f398510 feat(browser-extension): bring Browser Control into OpenCode Browser
OpenCode Browser no longer depends on the separate Browser Control product.
Its relay, CLI, MCP server, and agent guide (anomalyco/browser-control
8c80ac2, MIT, Anomaly) now live in packages/browser-extension/cli and ship
as the opencode-browser-cli npm package:

- Renamed throughout: `opencode-browser` command and MCP server,
  OPENCODE_BROWSER_* environment, opencode-browser skill, injected page ids.
- Its own port, 19988, so a Browser Control relay on 19989 can't hold it,
  and it accepts only OpenCode Browser's store and unpacked extension IDs
  (the path-hashed bundled-extension origin is gone).
- All files under one data root (src/paths.ts: ~/.local/share/
  opencode-browser, %LOCALAPPDATA% on Windows, OPENCODE_BROWSER_HOME):
  runtime, logs, relay catalogs and lifecycle, sessions, secrets.
- One entry (src/main.ts, bundled to dist/cli.mjs with npm dependencies
  external): setup commands (install, extension, uninstall, host) go to
  setup.ts, everything else to the relay CLI; `status` prints setup state
  first. The managed relay respawns the same entry.
- install now installs this exact version into <data root>/runtime with
  npm (OPENCODE_BROWSER_PACKAGE overrides the spec), points the native host
  and a new "opencode-browser" MCP entry at it, and warns about leftover
  browser-control MCP entries instead of configuring them. uninstall also
  removes the MCP entry and the data root.
- Logging: relay.log is always on (relay ready, extension connected/
  rejected/closed with codes, every HTTP request with status and ms except
  status/version polls, faults) and rotates to relay.log.1 instead of
  truncating; host.log records each native messaging request with timing.
- cli/ has its own package.json, bun.lock, and tsconfig so its pinned
  dependencies (effect 4.0.1, playwright-core 1.63.0, ...) stay out of the
  monorepo lockfile. The release workflow installs, typechecks, builds, and
  smoke tests it.

Verified: cli typecheck; browser-control's own test suite run against the
port outside the repo: 880/888 pass, the 8 failures assert the removed
layout (dist/mcp.js, path-hashed origins, extension/dist). From an npm pack
tarball in a temp HOME: install (5.7s), the native host answered service and
plugin requests, the MCP server listed 25 tools over stdio, and `execute`
started a managed relay, drove headless Chrome with the extension to
example.com, and returned the title; status and relay.log looked right.
2026-10-06 10:18:42 -04:00
Ryan Vogel 8ee61be65b ci: release OpenCode Browser with publish-browser-extension
cli/package.json is now the one release version: the store zip and the
extension bundled in the npm helper are stamped with it. Bumped to 0.0.2
(0.0.1 is on npm and in the store draft).

.github/workflows/publish-browser-extension.yml runs on a
browser-extension-v<version> tag (checked against cli/package.json) or by
hand: typecheck, build the helper and the store zip, smoke test the helper,
upload the zip as an artifact, then publish opencode-browser-cli to npm with
provenance (trusted publishing, or OPENCODE_BROWSER_NPM_TOKEN; skipped when the
version exists) and attach the zip to a GitHub release. Manual runs only
build unless `publish` is checked.

Verified: actionlint (only the repo's custom runner label is unknown to it);
locally, cli build + `bun run package` produced version 0.0.2 in both the zip
(no key) and the bundled extension, the built helper ran, and `npm pack
--dry-run` packs 0.0.2.
2026-10-03 19:25:25 -04:00
Ryan Vogel 6e6fadae5e feat(browser-extension): add opencode-browser-cli npm helper
`opencode browser install` only exists on this branch, so nobody on a
released opencode can connect the extension. packages/browser-extension/cli
is a standalone npm package (opencode-browser-cli) that does the same setup
without changing opencode:

- install: copies itself to ~/.local/share/opencode-browser (npx caches can
  be cleared), registers the ai.opencode.browser host for every installed
  Chromium browser (macOS/Linux manifests, Windows registry), adds the
  Browser Control MCP to opencode's global config (jsonc edits keep comments),
  starts the service, and copies the bundled unpacked extension to a
  permanent folder for Load unpacked.
- host: answers the extension with the URL and password from the user's
  installed `opencode service start` / `service get password`, and installs
  the extension's plugin.
- status, extension (opens the folder), uninstall.

The bundle includes jsonc-parser and the extension build (manifest key kept
so the unpacked ID matches the helper). The panel, welcome tab, store copy,
and privacy policy now point at `npx opencode-browser-cli install`. The
opencode CLI command is unchanged.

Verified: lint, extension typecheck; `npm pack` then npx from the tarball in
a temp HOME/XDG registered Helium, kept a commented opencode.json and added
the MCP entry, started the service; the wrapper answered service (URL +
password) and plugin (written) messages over native messaging; status and
uninstall worked.
2026-10-02 20:38:40 -04:00