Internal development workflow document. This file exists for local developer and coding-agent operations and is not the primary public project guide.
You are a developer agent working on Tandem Browser: an Electron browser built for human-AI symbiosis. The user (the human) and Wingman (the AI) browse the web together. You write the code.
Read PROJECT.md first. It contains the full overview of what Tandem is,
how it works, and why it exists. Read ARCHITECTURE.md before changing
code — it explains the layer model, the manager system, and where everything
lives.
- Repo:
hydro13/tandem-browser(GitHub: hydro13) - Stack: Electron + TypeScript + Express.js API (
localhost:8765) + MCP server with 257 tools (count maintained byscripts/check-consistency.js) - Goal: An agent-first browser where any AI (via MCP, HTTP API, or WebSocket) and a human browse together
- Philosophy: Local-first, privacy-first, no cloud dependencies in the browser itself
- Tests: Vitest; unit tests live in
src/**/tests/ - Versioning: See
package.jsonandCHANGELOG.mdfor the current release and full history
Counts in docs: trust only the automated ones. The MCP tool count and the
version number are enforced across docs by scripts/check-consistency.js
(part of npm run verify) — those you can trust. Any other count you find in
documentation (managers, files, modules) may have drifted; the authoritative
sources are src/registry.ts for the manager list, src/mcp/tools/ for the
MCP tool surface, and src/api/routes/ for the HTTP API surface. When a doc
and the code disagree, the code is right — fix the doc while you are there.
tandem-browser/
├── src/ # TypeScript application code (main process)
│ ├── main.ts # Electron main process entry
│ ├── registry.ts # ManagerRegistry — source of truth for managers
│ ├── bootstrap/ # Manager instantiation and wiring
│ ├── api/ # Express API server; routes in api/routes/
│ ├── mcp/ # MCP server; tools in mcp/tools/ (one file per domain)
│ ├── security/ # 8-layer shield (see ARCHITECTURE.md)
│ ├── extensions/ # Browser extension system
│ ├── snapshot/ # Accessibility tree with @refs
│ ├── platform/ # Platform adapters (macOS baseline, Windows track)
│ └── <domain>/ # ~50 single-responsibility domains, one dir each
├── shell/ # Browser UI (Electron renderer)
├── cli/ # tandem CLI (@hydro13/tandem-cli)
├── docs/ # Public site + project documentation
│ ├── implementations/ # Completed implementation plans
│ ├── plans/ # Not-yet-implemented design docs
│ ├── templates/ # design-template.md, manager-pattern.md
│ ├── research/ # Analyses and feature inventories
│ └── archive/ # Historical documents
├── scripts/ # Build, launch, and consistency scripts
├── skill/ # Agent skill file (SKILL.md)
├── tests/ # Smoke tests (unit tests live in src/**/tests/)
├── ARCHITECTURE.md # System structure — read before changing code
├── PROJECT.md # Product vision
├── AGENTS.md # This file
├── TODO.md # Current priorities
└── CHANGELOG.md
- Read the section of
TODO.mdrelevant to your task so you know how it fits the current priorities (you do not need to read the whole file) - Read
ARCHITECTURE.mdfor the patterns your change must follow - For larger features, check
docs/plans/anddocs/implementations/for an existing design doc and read it if present - Explore the slice you are changing and its blast radius: the manager,
its tests, its wiring (
src/registry.ts,src/bootstrap/), and its callers (API routes and MCP tools that use it). Verify what else observes the behavior you are about to change before you change it.
- Load what you need to be correct — no less. Artificially avoiding files that your change touches causes regressions; reading everything causes drowning.
- Delegate bulk reading (repo-wide sweeps, long documents, large test suites) to subagents where your environment supports them; keep summaries in your main context, not raw file dumps.
- Do not answer structural questions from memory or from docs alone: verify names, signatures, and wiring against the source.
- Always compile:
npx tscmust be error-free before you finish - Start the app:
npm startand verify startup without crashes - Test API endpoints: Use
curlfor every new or changed endpoint - Test the UI: Take a screenshot and verify it looks correct
- Run tests:
npx vitest run; all existing tests must keep passing - Full gate:
npm run verify(compile + lint + test + consistency check) is what CI runs — run it before opening a PR - Report: Provide a summary of what you tested and the outcomes
- TODO.md: Check off completed work and add newly discovered items
- CHANGELOG.md: Add an entry for each completed phase or feature (add at the top; you do not need to read the whole file)
- Code comments: Use JSDoc for public functions. Comment only what the code cannot say itself (constraints, invariants, non-obvious why) — not what it does.
- Multi-session work: If a task spans multiple sessions, keep a short
HANDOFF.mdnext to the feature's docs (state, decisions made, next steps) so the next session can resume without re-deriving context. Keep it current; delete it when the track completes.
- Commit after each completed sub-feature, not as one giant commit
- Default to a branch + pull request workflow, even for maintainer-owned work
- Do not push directly to
mainunless Robin explicitly asks for a direct bypass or the change is an urgent maintainer-only fix - While Tandem is still effectively solo-maintained, prefer keeping required CI
checks (
verify,CodeQL) and using PRs as the review step; do not assume a second human reviewer will exist - Any merged
feat:change must bump the app version before the PR is merged (minor), and any mergedfix:change must bump apatchversion - Keep
CHANGELOG.md,package.json, the in-app version, and the repo/docs version references on the same release number; do not leave new product surface under an old version header - Before merging a PR, quickly review the diff for version bumps, changelog noise, release impact, and whether README / CONTRIBUTING / TODO need updates
- Commit message examples:
feat: tab management with groupsfix: stealth UA mismatchdocs: update API endpointstest: add curl coverage for /tabs endpoints
- Merge to
mainonly after the PR checks are green or Robin explicitly chooses to bypass them
- Tandem is currently a source-only developer preview
- Do not publish official end-user binaries by default
- Do not attach old local
release/artifacts to GitHub releases - Source tags / GitHub releases are fine; binary releases should wait until packaging, naming, signing, notarization, and update metadata are deliberate and repeatable
- TypeScript strict mode: no
anyunless truly necessary, and document why - Error handling: every API endpoint should catch errors and return JSON
- No hardcoded paths: use
path.join(),app.getPath(), and related APIs - Separation of concerns: each file should have one clear responsibility
- Naming: camelCase for variables/functions, PascalCase for classes, kebab-case for files
- In persistent artifacts (docs, commit messages, code comments, handoff
files): refer to function, class, or section names — never line numbers,
because line numbers rot. Example: "see
function startAPI()inmain.ts", not "seeserver.tsline 287". - In ephemeral conversation (chat replies, PR review threads)
file:linereferences are fine — modern tooling renders them clickable and they are read immediately, not archived. - Use
grep -n "functionName"to locate code you have not loaded yet.
- Treat
docs/platform-support.mdas the public platform capability matrix. Update it whenever a platform capability changes status. - macOS Apple Silicon is the protected baseline. Any shared-code change must preserve existing macOS behavior and call out the macOS safety check in the PR description.
- Windows 11 x64 is the active target platform. Keep Windows work phased, independently revertable, and isolated behind platform adapters where code changes are needed.
- Do not add new
process.platformbranches in shared application code. Introduce platform-specific behavior throughsrc/platform/adapters. Existing branches are grandfathered until their phase migrates them. - Do not put Unix shell syntax in
package.jsonscripts. Cross-platform launch and maintenance behavior must go through Node helpers. - Do not claim Windows support in README, website,
docs/llms.txt, or release notes until a real signed Windows installer and required Windows CI exist. - Do not break local agent bootstrap.
~/.tandem/api-tokenremains a readable compatibility contract for local MCP/HTTP clients until a replacement bootstrap flow is explicitly designed and shipped. - Keep shared helpers Electron-safe. Utilities used by tests, MCP helpers, or
Node scripts must not require Electron
appat module import time. - All repository content must be English, including public docs, local plans, comments, commit messages, changelog entries, and handoff files.
- Do not add dependencies without calling it out; every new package needs approval
- Do not break existing endpoints; preserve backward compatibility
- Do not send data to external servers; everything stays local
- Do not use
rm -rfor destructive filesystem operations - Do not hardcode secrets or tokens; use environment variables or config files
- Do not rewrite entire files if a focused edit is sufficient
- Electron security: keep
nodeIntegration: falseandcontextIsolation: true - Webview partition: always use
persist:tandem - API binding: bind only to
127.0.0.1, never0.0.0.0 - Stealth patches: do not remove or disable them
Tandem is a browser that a single human uses on their own machine, with an AI assistant that has the same access the human does. The assistant acts for the present human, on the human's own logged-in accounts, with the human in the loop (see the captcha hand-off and the dead-man switch below). A person using their own browser with an assistant is a legitimate, allowed use — it does nothing a human browsing their own accounts may not do.
The only thing this layer hides is that an AI/Electron is driving the browser, so a site sees a normal Chrome used by a normal person — which is exactly what it is. If a site could tell it was an "AI browser," it would wrongly flag or ban the human's real accounts; that is the harm this prevents.
What this layer is not, and must never become:
- Not scraping or mass automation. One user, one machine, human present.
- Not hardware-fingerprint spoofing. The real, stable hardware fingerprint is
fine and expected — sites may fingerprint normally. The old WebGL / canvas /
audio / screen / CPU / font / timing spoofing was removed on purpose (see
the Stealth section in
TODO.md). Do not re-add it. - Not cheating security challenges. Cloudflare challenges are solved natively; captchas are handed off to the human to solve.
So the rule is simple: hide the AI-integration, present a real human's real browser. Nothing more. When in doubt, that boundary is the answer.
The web page must not be able to tell an AI/Electron is involved. It should see a normal Chrome and normal, human-initiated interaction — because the human is present and the assistant acts on their behalf. Wingman looks over Robin's shoulder; the page simply sees Robin's own browser.
- Layer 1 (visible to the website): normal Chromium webview, real session, human behavior
- Layer 2 (invisible): Electron main process + shell UI where Wingman operates
| Task | Forbidden | Required |
|---|---|---|
| Screenshot | Canvas API in the webview | webContents.capturePage() in the main process |
| Click | el.click() / dispatchEvent() in the webview |
webContents.sendInputEvent() at OS level |
| Type | el.value = "text" in the webview |
sendInputEvent({type:'char'}) per character |
| Read page | Injected DOM crawler script | Minimal executeJavaScript() from the main process |
| Draw overlay | Canvas inside the webview | Canvas in the shell above the webview |
| Voice | Web Speech API in the webview | Web Speech API in the shell |
| Wingman panel | iframe/element in the webview | Separate Electron panel outside the webview |
| Activity tracking | MutationObserver in the page | Electron webview events |
Event.isTrusted: programmatic events becomefalse, so usesendInputEventdocument.hasFocus(): if the Wingman panel steals focus, mock focus must stay consistent- Input cadence: assistant-driven clicks/typing that are inhumanly fast or regular can get the user's own account flagged for robotic behaviour, so input is paced from the user's real patterns (see Humanization below) — not to defeat detection, but so the assistant's actions look like the user's own, because they are
- Injected DOM elements: anything not from the site itself must never live in the webview
- WebSocket to localhost: the page must never call our API directly from the webview
window.process/window.require: Electron giveaways must beundefinednavigator.userAgentData: must match real Chrome (hides Electron, not hardware)localhost:8765port scans: use strict CORS and no cross-origin responses
Hide the signals that reveal an AI/Electron is involved — webdriver,
window.process / window.require, Electron in the UA, a missing
window.chrome. Present a normal Chrome.
Do not hide or fake the machine. The real, stable hardware fingerprint (GPU, canvas, audio, screen, CPU, fonts, timing) is left untouched and is exactly what a real human's browser exposes. Spoofing it is both an unwinnable arms race and self-defeating: it made Tandem's fingerprint inconsistent (real on some sites, fake on others), which is itself a tell, and it broke Cloudflare challenges. If you are ever tempted to add a fingerprint-masking patch, that is the wrong direction — the goal is a real human's real browser, not a disguised one.
- Hide Electron-specific / automation APIs; leave everything else real.
- No custom headers, no custom properties that mark the browser as non-Chrome.
- Consider a Chrome Extension model if Electron cannot stay free of automation giveaways.
Background browsing is never fully autonomous:
- On detection or captcha, the tab becomes visible to Robin
- Robin resolves it, then the tab returns to the background
- Robin is always the dead-man switch and the bodyguard
Tandem learns Robin’s real behavior patterns and replicates them for automated actions.
Observation layer (always active, passive):
- Track through Electron main-process events, never inside the webview
- Mouse movement paths, click delays, scroll patterns, typing rhythm
- Storage:
~/.tandem/behavior/(raw data + compiled profile)
The profile contains:
- Typing bigram timing (interval per key combination)
- Click hesitation distribution (hover → click delay)
- Scroll patterns (speed, pauses, reading time)
- Mouse path curves (Bezier templates)
- Day-cycle variation (night = slower)
- Per-site behavior clusters
During automated actions:
- Sample from Robin’s real distributions, not hardcoded ranges
- Mouse movement: Bezier curves based on learned paths
- Typing: Robin’s own key-combination rhythm plus variation
- Fallback if the profile is still sparse: Gaussian random 80-300ms click delays and 30-120ms typing delays
Golden rule: the resulting behavior should be statistically indistinguishable from Robin’s real browsing.
The Wingman panel has a Chat tab that lets Robin and Wingman communicate. It
connects directly via WebSocket to the OpenClaw gateway
(ws://127.0.0.1:18789).
- Open a WebSocket to
ws://127.0.0.1:18789 - Wait for the
connect.challengeevent - Send the
connectrequest with the gateway token from~/.openclaw/openclaw.json - Load history via
chat.historywith session keyagent:main:main - Send messages via
chat.send - Receive streaming updates via
chatevents (delta→final)
We tried three other approaches that did not work:
- Cron polling
localhost:8765/chat: too slow, and it wastes API tokens on every poll - Iframe embedding or OpenClaw webchat: blocked by
X-Frame-Options: DENYandContent-Security-Policy: frame-ancestors 'none', plus auth token issues - Webview with localStorage token injection: separate partition
(
persist:openclaw-chat) does not share storage with the main partition, and the token structure is too fragile
Direct WebSocket is the only correct approach. It is simple, fast, and
real-time. The gateway token lives in ~/.openclaw/openclaw.json under
gateway.auth.token.
Chat WebSocket code lives in shell/chat/openclaw-backend.js (it moved out of
shell/index.html during the shell refactor). Look for ocChat or the
// === OpenClaw WebSocket Chat === marker.
Electron on macOS gets killed by Gatekeeper (SIGKILL after roughly 4 seconds) if quarantine flags are present. Always do this before launching:
xattr -cr node_modules/electron/dist/Electron.appRun it after every npm install or whenever Electron is re-downloaded. Bake it
into start scripts.
1. Orient: TODO.md section, ARCHITECTURE.md, any design doc in docs/plans/
2. Explore the affected slice and its blast radius (callers, wiring, tests)
3. Write the code
4. Run npx tsc and fix all type errors
5. Run npx vitest run and keep all tests passing
6. Run npm start and test manually (not npm run dev)
7. Use curl to test every new or changed endpoint
8. Run npm run verify (the CI gate) before opening the PR
9. Update CHANGELOG.md and TODO.md
10. Commit (see commit format below), push, open a PR
11. Report: built / tested / problems / next step
Work in units that end in a verifiable state: each commit should compile, pass tests, and leave the app startable. Verify each unit before starting the next.
<type>: <short description> (<scope>)
What was built/changed:
- New files: src/sidebar/manager.ts, src/sidebar/types.ts
- Modified files: src/registry.ts, src/main.ts, src/api/server.ts
- New API endpoints: GET /sidebar/config, POST /sidebar/state, etc.
- Deleted files: (if applicable)
Why this approach:
- Short explanation of the architecture choices
Tested:
- npx tsc: zero errors
- npx vitest run: all tests pass
- Manual: [what was tested]
| Type | Version bump | Use |
|---|---|---|
feat: |
minor (0.15.0 → 0.16.0) |
new feature |
feat!: |
major (0.15.0 → 1.0.0) |
breaking change |
fix: |
patch (0.15.0 → 0.15.1) |
bug fix |
chore: |
none | dependencies, build, tooling |
docs: |
none | documentation |
refactor: |
none | code restructuring |
test: |
none | tests |
The auto-versioning hook strips leading emoji, so both forms work:
✅ feat: sidebar manager + config API
✅ 🗂️ feat: sidebar manager ← hook strips the emoji prefix
✅ feat: sidebar manager 🗂️ ← also fine
For every feat: or fix: commit, add this structure at the top:
## [v0.16.0] - 2026-02-28
### Added
- **Sidebar Infrastructure** (`src/sidebar/`) — SidebarManager with JSON config storage
- 12 sidebar items: 6 utility panels + 6 messenger webviews
- 6 REST API endpoints (GET/POST /sidebar/config, /state, /reorder, etc.)
- 3 sidebar modes: hidden / narrow / wide
- Config persisted in `~/.tandem/sidebar-config.json`
### Changed
- `src/registry.ts` — added `sidebarManager` to ManagerRegistry
- `src/main.ts` — SidebarManager instantiation in `startAPI()` + will-quit cleanup
- `src/api/server.ts` — added `registerSidebarRoutes`
### Technical Details
- Manager pattern: load/save via `tandemDir()` + `ensureDir()`
- 12 default items: workspaces, news, pinboards, bookmarks, history, downloads + 6 messengersAfter each session, provide:
## Built
- [feature 1]: what it does
- [feature 2]: what it does
## Tested
- ✅ npx tsc — no errors
- ✅ npx vitest run — all tests pass
- ✅ npm start — app starts without crashes
- ✅ curl localhost:8765/new-endpoint — response OK
- ⚠️ [any issues found]
## Documentation
- TODO.md updated
- CHANGELOG.md updated
## Next Step
- [what is next according to TODO.md]
Robin is the product owner. He:
- Decides design choices when there are multiple valid options
- Must be informed about new dependencies
- Tests the UI visually while you test the code
- Speaks Dutch; you may reply to Robin in Dutch, but all repository content must remain in English
- No exceptions for repository language: code, comments, commit messages, docs, plans, TODOs, changelog entries, and handoff files must all be written in English
- Proceed without asking on reversible, in-scope decisions. Record every such decision in your report so Robin can course-correct afterwards.
- Ask first before anything irreversible or scope-changing: adding dependencies, deleting data, publishing or releasing, weakening the security posture, or genuine product choices where multiple designs are valid and the choice shapes the user experience.
- When you do need input, batch your questions and keep working on whatever is not blocked. Do not stall an entire session on one open question.