mirror of https://github.com/garrytan/gstack.git
4 Commits
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
d0782c4c4d
|
feat(v1.4.0.0): /make-pdf — markdown to publication-quality PDFs (#1086)
* feat(browse): full $B pdf flag contract + tab-scoped load-html/js/pdf
Grow $B pdf from a 2-line wrapper (hard-coded A4) into a real PDF engine
frontend so make-pdf can shell out to it without duplicating Playwright:
- pdf: --format, --width/--height, --margins, --margin-*, --header-template,
--footer-template, --page-numbers, --tagged, --outline, --print-background,
--prefer-css-page-size, --toc. Mutex rules enforced. --from-file <json>
dodges Windows argv limits (8191 char CreateProcess cap).
- load-html: add --from-file <json> mode for large inline HTML. Size + magic
byte checks still apply to the inline content, not the payload file path.
- newtab: add --json returning {"tabId":N,"url":...} for programmatic use.
- cli: extract --tab-id flag and route as body.tabId to the HTTP layer so
parallel callers can target specific tabs without racing on the active
tab (makes make-pdf's per-render tab isolation possible).
- --toc: non-fatal 3s wait for window.__pagedjsAfterFired. Paged.js ships
later; v1 renders TOC statically via the markdown renderer.
Codex round 2 flagged these P0 issues during plan review. All resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(resolvers): add MAKE_PDF_SETUP + makePdfDir host paths
Skill templates can now embed {{MAKE_PDF_SETUP}} to resolve $P to the
make-pdf binary via the same discovery order as $B / $D: env override
(MAKE_PDF_BIN), local skill root, global install, or PATH.
Mirrors the pattern established by generateBrowseSetup() and
generateDesignSetup() in scripts/resolvers/design.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(make-pdf): new /make-pdf skill + orchestrator binary
Turn markdown into publication-quality PDFs. $P generate input.md out.pdf
produces a PDF with 1in margins, intelligent page breaks, page numbers,
running header, CONFIDENTIAL footer, and curly quotes/em dashes — all on
Helvetica so copy-paste extraction works ("S ai li ng" bug avoided).
Architecture (per Codex round 2):
markdown → render.ts (marked + sanitize + smartypants) → orchestrator
→ $B newtab --json → $B load-html --tab-id → $B js (poll Paged.js)
→ $B pdf --tab-id → $B closetab
browseClient.ts shells out to the compiled browse CLI rather than
duplicating Playwright. --tab-id isolation per render means parallel
$P generate calls don't race on the active tab. try/finally tab cleanup
survives Paged.js timeouts, browser crashes, and output-path failures.
Features in v1:
--cover left-aligned cover page (eyebrow + title + hairline rule)
--toc clickable static TOC (Paged.js page numbers deferred)
--watermark <text> diagonal DRAFT/CONFIDENTIAL layer
--no-chapter-breaks opt out of H1-starts-new-page
--page-numbers "N of M" footer (default on)
--tagged --outline accessible PDF + bookmark outline (default on)
--allow-network opt in to external image loading (default off for privacy)
--quiet --verbose stderr control
Design decisions locked from the /plan-design-review pass:
- Helvetica everywhere (Chromium emits single-word Tj operators for
system fonts; bundled webfonts emit per-glyph and break extraction).
- Left-aligned body, flush-left paragraphs, no text-indent, 12pt gap.
- Cover shares 1in margins with body pages; no flexbox-center, no
inset padding.
- The reference HTMLs at .context/designs/*.html are the implementation
source of truth for print-css.ts.
Tests (56 unit + 1 E2E combined-features gate):
- smartypants: code/URL-safe, verified against 10 fixtures
- sanitizer: strips <script>/<iframe>/on*/javascript: URLs
- render: HTML assembly, CJK fallback, cover/TOC/chapter wrap
- print-css: all @page rules, margin variants, watermark
- pdftotext: normalize()+copyPasteGate() cross-OS tolerance
- browseClient: binary resolution + typed error propagation
- combined-features gate (P0): 2-chapter fixture with smartypants +
hyphens + ligatures + bold/italic + inline code + lists + blockquote
passes through PDF → pdftotext → expected.txt diff
Deferred to Phase 4 (future PR): Paged.js vendored for accurate TOC page
numbers, highlight.js for syntax highlighting, drop caps, pull quotes,
two-column, CMYK, watermark visual-diff acceptance.
Plan: .context/ceo-plans/2026-04-19-perfect-pdf-generator.md
References: .context/designs/make-pdf-*.html
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(build): wire make-pdf into build/test/setup/bin + add marked dep
- package.json: compile make-pdf/dist/pdf as part of bun run build; add
"make-pdf" to bin entry; include make-pdf/test/ in the free test pass;
add marked@18.0.2 as a dep (markdown parser, ~40KB).
- setup: add make-pdf/dist/pdf to the Apple Silicon codesign loop.
- .gitignore: add make-pdf/dist/ (matches browse/dist/ and design/dist/).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(make-pdf): matrix copy-paste gate on Ubuntu + macOS
Runs the combined-features P0 gate on pull requests that touch make-pdf/
or browse's PDF surface. Installs poppler (macOS) / poppler-utils (Ubuntu)
per OS. Windows deferred to tolerant mode (Xpdf / Poppler-Windows
extraction variance not yet calibrated against the normalized comparator —
Codex round 2 #18).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(skills): regenerate SKILL.md for make-pdf addition + browse pdf flags
bun run gen:skill-docs picks up:
- the new /make-pdf skill (make-pdf/SKILL.md)
- updated browse command descriptions for 'pdf', 'load-html', 'newtab'
reflecting the new flag contract and --from-file mode
Source of truth stays the .tmpl files + COMMAND_DESCRIPTIONS;
these are regenerated artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tests): repair stale test expectations + emit _EXPLAIN_LEVEL / _QUESTION_TUNING from preamble
Three pre-existing test failures on main were blocking /ship:
- test/skill-validation.test.ts "Step 3.4 test coverage audit" expected the
literal strings "CODE PATH COVERAGE" and "USER FLOW COVERAGE" which were
removed when the Step 7 coverage diagram was compressed. Updated assertions
to check the stable `Code paths:` / `User flows:` labels that still ship.
- test/skill-validation.test.ts "ship step numbering" allowed-substeps list
didn't include 15.0 (WIP squash) and 15.1 (bisectable commits) which were
added for continuous checkpoint mode. Extended the allowlist.
- test/writing-style-resolver.test.ts and test/plan-tune.test.ts expected
`_EXPLAIN_LEVEL` and `_QUESTION_TUNING` bash variables in the preamble but
generate-preamble-bash.ts had been refactored and those lines were dropped.
Without them, downstream skills can't read `explain_level` or
`question_tuning` config at runtime — terse mode and /plan-tune features
were silently broken.
Added the two bash echo blocks back to generatePreambleBash and refreshed
the golden-file fixtures to match. All three preamble-related golden
baselines (claude/codex/factory) are synchronized with the new output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v1.4.0.0)
New /make-pdf skill + $P binary.
Turn any markdown file into a publication-quality PDF. Default output is
a 1in-margin Helvetica letter with page numbers in the footer. `--cover`
adds a left-aligned cover page, `--toc` generates a clickable table of
contents, `--watermark DRAFT` overlays a diagonal watermark. Copy-paste
extraction from the PDF produces clean words, not "S a i l i n g"
spaced out letter by letter. CI gate (macOS + Ubuntu) runs a combined-
features fixture through pdftotext on every PR.
make-pdf shells out to browse rather than duplicating Playwright.
$B pdf grew into a real PDF engine with full flag contract (--format,
--margins, --header-template, --footer-template, --page-numbers,
--tagged, --outline, --toc, --tab-id, --from-file). $B load-html and
$B js gained --tab-id. $B newtab --json returns structured output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(changelog): rewrite v1.4.0.0 headline — positive voice, no VC framing
The original headline led with "a PDF you wouldn't be embarrassed to send
to a VC": double-negative voice and audience-too-narrow. /make-pdf works
for essays, letters, memos, reports, proposals, and briefs. Framing the
whole release around founders-to-investors misses the wider audience.
New headline: "Turn any markdown file into a PDF that looks finished."
New tagline: "This one reads like a real essay or a real letter."
Positive voice. Broader aperture. Same energy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
|
|
8ca950f6f1
|
feat: content security — 4-layer prompt injection defense for pair-agent (#815)
* feat: token registry for multi-agent browser access Per-agent scoped tokens with read/write/admin/meta command categories, domain glob restrictions, rate limiting, expiry, and revocation. Setup key exchange for the /pair-agent ceremony (5-min one-time key → 24h session token). Idempotent exchange handles tunnel drops. 39 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: integrate token registry + scoped auth into browse server Server changes for multi-agent browser access: - /connect endpoint: setup key exchange for /pair-agent ceremony - /token endpoint: root-only minting of scoped sub-tokens - /token/:clientId DELETE: revoke agent tokens - /agents endpoint: list connected agents (root-only) - /health: strips root token when tunnel is active (P0 security fix) - /command: scope/rate/domain checks via token registry before dispatch - Idle timer skips shutdown when tunnel is active Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: ngrok tunnel integration + @ngrok/ngrok dependency BROWSE_TUNNEL=1 env var starts an ngrok tunnel after Bun.serve(). Reads NGROK_AUTHTOKEN from env or ~/.gstack/ngrok.env. Reads NGROK_DOMAIN for dedicated domain (stable URL). Updates state file with tunnel URL. Feasibility spike confirmed: SDK works in compiled Bun binary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: tab isolation for multi-agent browser access Add per-tab ownership tracking to BrowserManager. Scoped agents must create their own tab via newtab before writing. Unowned tabs (pre-existing, user-opened) are root-only for writes. Read access always allowed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: tab enforcement + POST /pair endpoint + activity attribution Server-side tab ownership check blocks scoped agents from writing to unowned tabs. Special-case newtab records ownership for scoped tokens. POST /pair endpoint creates setup keys for the pairing ceremony. Activity events now include clientId for attribution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: pair-agent CLI command + instruction block generator One command to pair a remote agent: $B pair-agent. Creates a setup key via POST /pair, prints a copy-pasteable instruction block with curl commands. Smart tunnel fallback (tunnel URL > auto-start > localhost). Flags: --for HOST, --local HOST, --admin, --client NAME. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: tab isolation + instruction block generator tests 14 tests covering tab ownership lifecycle (access checks, unowned tabs, transferTab) and instruction block generator (scopes, URLs, admin flag, troubleshooting section). Fix server-auth test that used fragile sliceBetween boundaries broken by new endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.15.9.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: CSO security fixes — token leak, domain bypass, input validation 1. Remove root token from /health endpoint entirely (CSO #1 CRITICAL). Origin header is spoofable. Extension reads from ~/.gstack/.auth.json. 2. Add domain check for newtab URL (CSO #5). Previously only goto was checked, allowing domain-restricted agents to bypass via newtab. 3. Validate scope values, rateLimit, expiresSeconds in createToken() (CSO #4). Rejects invalid scopes and negative values. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: /pair-agent skill — syntactic sugar for browser sharing Users remember /pair-agent, not $B pair-agent. The skill walks through agent selection (OpenClaw, Hermes, Codex, Cursor, generic), local vs remote setup, tunnel configuration, and includes platform-specific notes for each agent type. Wraps the CLI command with context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: remote browser access reference for paired agents Full API reference, snapshot→@ref pattern, scopes, tab isolation, error codes, ngrok setup, and same-machine shortcuts. The instruction block points here for deeper reading. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: improved instruction block with snapshot→@ref pattern The paste-into-agent instruction block now teaches the snapshot→@ref workflow (the most powerful browsing pattern), shows the server URL prominently, and uses clearer formatting. Tests updated to match. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: smart ngrok detection + auto-tunnel in pair-agent The pair-agent command now checks ngrok's native config (not just ~/.gstack/ngrok.env) and auto-starts the tunnel when ngrok is available. The skill template walks users through ngrok install and auth if not set up, instead of just printing a dead localhost URL. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: on-demand tunnel start via POST /tunnel/start pair-agent now auto-starts the ngrok tunnel without restarting the server. New POST /tunnel/start endpoint reads authtoken from env, ~/.gstack/ngrok.env, or ngrok's native config. CLI detects ngrok availability and calls the endpoint automatically. Zero manual steps when ngrok is installed and authed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pair-agent skill must output the instruction block verbatim Added CRITICAL instruction: the agent MUST output the full instruction block so the user can copy it. Previously the agent could summarize over it, leaving the user with nothing to paste. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: scoped tokens rejected on /command — auth gate ordering bug The blanket validateAuth() gate (root-only) sat above the /command endpoint, rejecting all scoped tokens with 401 before they reached getTokenInfo(). Moved /command above the gate so both root and scoped tokens are accepted. This was the bug Wintermute hit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: pair-agent auto-launches headed mode before pairing When pair-agent detects headless mode, it auto-switches to headed (visible Chromium window) so the user can watch what the remote agent does. Use --headless to skip this. Fixed compiled binary path resolution (process.execPath, not process.argv[1] which is virtual /$bunfs/ in Bun compiled binaries). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: comprehensive tests for auth ordering, tunnel, ngrok, headed mode 16 new tests covering: - /command sits above blanket auth gate (Wintermute bug) - /command uses getTokenInfo not validateAuth - /tunnel/start requires root, checks native ngrok config, returns already_active - /pair creates setup keys not session tokens - Tab ownership checked before command dispatch - Activity events include clientId - Instruction block teaches snapshot→@ref pattern - pair-agent auto-headed mode, process.execPath, --headless skip - isNgrokAvailable checks all 3 sources (gstack env, env var, native config) - handlePairAgent calls /tunnel/start not server restart Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: chain scope bypass + /health info leak when tunneled 1. Chain command now pre-validates ALL subcommand scopes before executing any. A read+meta token can no longer escalate to admin via chain (eval, js, cookies were dispatched without scope checks). tokenInfo flows through handleMetaCommand into the chain handler. Rejects entire chain if any subcommand fails. 2. /health strips sensitive fields (currentUrl, agent.currentMessage, session) when tunnel is active. Only operational metadata (status, mode, uptime, tabs) exposed to the internet. Previously anyone reaching the ngrok URL could surveil browsing activity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: tout /pair-agent as headline feature in CHANGELOG + README Lead with what it does for the user: type /pair-agent, paste into your other agent, done. First time AI agents from different companies can coordinate through a shared browser with real security boundaries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: expand /pair-agent, /design-shotgun, /design-html in README Each skill gets a real narrative paragraph explaining the workflow, not just a table cell. design-shotgun: visual exploration with taste memory. design-html: production HTML with Pretext computed layout. pair-agent: cross-vendor AI agent coordination through shared browser. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: split handleCommand into handleCommandInternal + HTTP wrapper Chain subcommands now route through handleCommandInternal for full security enforcement (scope, domain, tab ownership, rate limiting, content wrapping). Adds recursion guard for nested chains, rate-limit exemption for chain subcommands, and activity event suppression (1 event per chain, not per sub). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add content-security.ts with datamarking, envelope, and filter hooks Four-layer prompt injection defense for pair-agent browser sharing: - Datamarking: session-scoped watermark for text exfiltration detection - Content envelope: trust boundary wrapping with ZWSP marker escaping - Content filter hooks: extensible filter pipeline with warn/block modes - Built-in URL blocklist: requestbin, pipedream, webhook.site, etc. BROWSE_CONTENT_FILTER env var controls mode: off|warn|block (default: warn) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: centralize content wrapping in handleCommandInternal response path Single wrapping location replaces fragmented per-handler wrapping: - Scoped tokens: content filters + datamarking + enhanced envelope - Root tokens: existing basic wrapping (backward compat) - Chain subcommands exempt from top-level wrapping (wrapped individually) - Adds 'attrs' to PAGE_CONTENT_COMMANDS (ARIA value exposure defense) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: hidden element stripping for scoped token text extraction Detects CSS-hidden elements (opacity, font-size, off-screen, same-color, clip-path) and ARIA label injection patterns. Marks elements with data-gstack-hidden, extracts text from a clean clone (no DOM mutation), then removes markers. Only active for scoped tokens on text command. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: snapshot split output format for scoped tokens Scoped tokens get a split snapshot: trusted @refs section (for click/fill) separated from untrusted web content in an envelope. Ref names truncated to 50 chars in trusted section. Root tokens unchanged (backward compat). Resume command also uses split format for scoped tokens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add SECURITY section to pair-agent instruction block Instructs remote agents to treat content inside untrusted envelopes as potentially malicious. Lists common injection phrases to watch for. Directs agents to only use @refs from the trusted INTERACTIVE ELEMENTS section, not from page content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add 4 prompt injection test fixtures - injection-visible.html: visible injection in product review text - injection-hidden.html: 7 CSS hiding techniques + ARIA injection + false positive - injection-social.html: social engineering in legitimate-looking content - injection-combined.html: all attack types + envelope escape attempt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: comprehensive content security tests (47 tests) Covers all 4 defense layers: - Datamarking: marker format, session consistency, text-only application - Content envelope: wrapping, ZWSP marker escaping, filter warnings - Content filter hooks: URL blocklist, custom filters, warn/block modes - Instruction block: SECURITY section content, ordering, generation - Centralized wrapping: source-level verification of integration - Chain security: recursion guard, rate-limit exemption, activity suppression - Hidden element stripping: 7 CSS techniques, ARIA injection, false positives - Snapshot split format: scoped vs root output, resume integration Also fixes: visibility:hidden detection, case-insensitive ARIA pattern matching. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pair-agent skill compliance + fix all 16 pre-existing test failures Root cause: pair-agent was added without completing the gen-skill-docs compliance checklist. All 16 failures traced back to this. Fixes: - Sync package.json version to VERSION (0.15.9.0) - Add "(gstack)" to pair-agent description for discoverability - Add pair-agent to Codex path exception (legitimately documents ~/.codex/) - Add CLI_COMMANDS (status, pair-agent, tunnel) to skill parser allowlist - Regenerate SKILL.md for all hosts (claude, codex, factory, kiro, etc.) - Update golden file baselines for ship skill - Fix relink tests: pass GSTACK_INSTALL_DIR to auto-relink calls so they use the fast mock install instead of scanning real ~/.claude/skills/gstack Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.15.12.0) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: E2E exit reason precedence + worktree prune race condition Two fixes for E2E test reliability: 1. session-runner.ts: error_max_turns was misclassified as error_api because is_error flag was checked before subtype. Now known subtypes like error_max_turns are preserved even when is_error is set. The is_error override only applies when subtype=success (API failure). 2. worktree.ts: pruneStale() now skips worktrees < 1 hour old to avoid deleting worktrees from concurrent test runs still in progress. Previously any second test execution would kill the first's worktrees. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore token in /health for localhost extension auth The CSO security fix stripped the token from /health to prevent leaking when tunneled. But the extension needs it to authenticate on localhost. Now returns token only when not tunneled (safe: localhost-only path). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: verify /health token is localhost-only, never served through tunnel Updated tests to match the restored token behavior: - Test 1: token assignment exists AND is inside the !tunnelActive guard - Test 1b: tunnel branch (else block) does not contain AUTH_TOKEN Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add security rationale for token in /health on localhost Explains why this is an accepted risk (no escalation over file-based token access), CORS protection, and tunnel guard. Prevents future CSO scans from stripping it without providing an alternative auth path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: verify tunnel is alive before returning URL to pair-agent Root cause: when ngrok dies externally (pkill, crash, timeout), the server still reports tunnelActive=true with a dead URL. pair-agent prints an instruction block pointing at a dead tunnel. The remote agent gets "endpoint offline" and the user has to manually restart everything. Three-layer fix: - Server /pair endpoint: probes tunnel URL before returning it. If dead, resets tunnelActive/tunnelUrl and returns null (triggers CLI restart). - Server /tunnel/start: probes cached tunnel before returning already_active. If dead, falls through to restart ngrok automatically. - CLI pair-agent: double-checks tunnel URL from server before printing instruction block. Falls through to auto-start on failure. 4 regression tests verify all three probe points + CLI verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add POST /batch endpoint for multi-command batching Remote agents controlling GStack Browser through a tunnel pay 2-5s of latency per HTTP round-trip. A typical "navigate and read" takes 4 sequential commands = 10-20 seconds. The /batch endpoint collapses N commands into a single HTTP round-trip, cutting a 20-tab crawl from ~60s to ~5s. Sequential execution through the full security pipeline (scope, domain, tab ownership, content wrapping). Rate limiting counts the batch as 1 request. Activity events emitted at batch level, not per-command. Max 50 commands per batch. Nested batches rejected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add source-level security tests for /batch endpoint 8 tests verifying: auth gate placement, scoped token support, max command limit, nested batch rejection, rate limiting bypass, batch-level activity events, command field validation, and tabId passthrough. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: correct CHANGELOG date from 2026-04-06 to 2026-04-05 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: consolidate Hermes into generic HTTP option in pair-agent Hermes doesn't have a host-specific config — it uses the same generic curl instructions as any other agent. Removing the dedicated option simplifies the menu and eliminates a misleading distinction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump VERSION to 0.15.14.0, add CHANGELOG entry for batch endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate pair-agent/SKILL.md after main merge Vendoring deprecation section from main's template wasn't reflected in the generated file. Fixes check-freshness CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: checkTabAccess uses options object, add own-only tab policy Refactors checkTabAccess(tabId, clientId, isWrite) to use an options object { isWrite?, ownOnly? }. Adds tabPolicy === 'own-only' support in the server command dispatch — scoped tokens with this policy are restricted to their own tabs for all commands, not just writes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add --domain flag to pair-agent CLI for domain restrictions Allows passing --domain to pair-agent to restrict the remote agent's navigation to specific domains (comma-separated). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * revert: remove batch commands CHANGELOG entry and VERSION bump The batch endpoint work belongs on the browser-batch-multitab branch (port-louis), not this branch. Reverting VERSION to 0.15.14.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: adopt main's headed-mode /health token serving Our merge kept the old !tunnelActive guard which conflicted with main's security-audit-r2 tests that require no currentUrl/currentMessage in /health. Adopts main's approach: serve token conditionally based on headed mode or chrome-extension origin. Updates server-auth tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: improve snapshot flags docs completeness for LLM judge Adds $B placeholder explanation, explicit syntax line, and detailed flag behavior (-d depth values, -s CSS selector syntax, -D unified diff format and baseline persistence, -a screenshot vs text output relationship). Fixes snapshot flags reference LLM eval scoring completeness < 4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
|
|
|
a1a933614c
|
feat: sidebar CSS inspector + per-tab agents (v0.13.9.0) (#650)
* feat: CDP inspector module — persistent sessions, CSS cascade, style modification
New browse/src/cdp-inspector.ts with full CDP inspection engine:
- inspectElement() via CSS.getMatchedStylesForNode + DOM.getBoxModel
- modifyStyle() via CSS.setStyleTexts with headless page.evaluate fallback
- Persistent CDP session lifecycle (create, reuse, detach on nav, re-create)
- Specificity sorting, overridden property detection, UA rule filtering
- Modification history with undo support
- formatInspectorResult() for CLI output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: browse server inspector endpoints + inspect/style/cleanup/prettyscreenshot CLI
Server endpoints: POST /inspector/pick, GET /inspector, POST /inspector/apply,
POST /inspector/reset, GET /inspector/history, GET /inspector/events (SSE).
CLI commands: inspect (CDP cascade), style (live CSS mod), cleanup (page clutter
removal), prettyscreenshot (clean screenshot pipeline).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: sidebar CSS inspector — element picker, box model, rule cascade, quick edit
Extension changes for the visual CSS inspector:
- inspector.js: element picker with hover highlight, CSS selector generation,
basic mode fallback (getComputedStyle + CSSOM), page alteration handlers
- inspector.css: picker overlay styles (blue highlight + tooltip)
- background.js: inspector message routing (picker <-> server <-> sidepanel)
- sidepanel: Inspector tab with box model viz (gstack palette), matched rules
with specificity badges, computed styles, click-to-edit quick edit,
Send to Agent/Code button, empty/loading/error states
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: document inspect, style, cleanup, prettyscreenshot browse commands
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: auto-track user-created tabs and handle tab close
browser-manager.ts changes:
- context.on('page') listener: automatically tracks tabs opened by the user
(Cmd+T, right-click open in new tab, window.open). Previously only
programmatic newTab() was tracked, so user tabs were invisible.
- page.on('close') handler in wirePageEvents: removes closed tabs from the
pages map and switches activeTabId to the last remaining tab.
- syncActiveTabByUrl: match Chrome extension's active tab URL to the correct
Playwright page for accurate tab identity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: per-tab agent isolation via BROWSE_TAB environment variable
Prevents parallel sidebar agents from interfering with each other's tab context.
Three-layer fix:
- sidebar-agent.ts: passes BROWSE_TAB=<tabId> env var to each claude process,
per-tab processing set allows concurrent agents across tabs
- cli.ts: reads process.env.BROWSE_TAB and includes tabId in command request body
- server.ts: handleCommand() temporarily switches activeTabId when tabId is present,
restores after command completes (safe: Bun event loop is single-threaded)
Also: per-tab agent state (TabAgentState map), per-tab message queuing,
per-tab chat buffers, verbose streaming narration, stop button endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: sidebar per-tab chat context, tab bar sync, stop button, UX polish
Extension changes:
- sidepanel.js: per-tab chat history (tabChatHistories map), switchChatTab()
swaps entire chat view, browserTabActivated handler for instant tab sync,
stop button wired to /sidebar-agent/stop, pollTabs renders tab bar
- sidepanel.html: updated banner text ("Browser co-pilot"), stop button markup,
input placeholder "Ask about this page..."
- sidepanel.css: tab bar styles, stop button styles, loading state fixes
- background.js: chrome.tabs.onActivated sends browserTabActivated to sidepanel
with tab URL for instant tab switch detection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: per-tab isolation, BROWSE_TAB pinning, tab tracking, sidebar UX
sidebar-agent.test.ts (new tests):
- BROWSE_TAB env var passed to claude process
- CLI reads BROWSE_TAB and sends tabId in body
- handleCommand accepts tabId, saves/restores activeTabId
- Tab pinning only activates when tabId provided
- Per-tab agent state, queue, concurrency
- processingTabs set for parallel agents
sidebar-ux.test.ts (new tests):
- context.on('page') tracks user-created tabs
- page.on('close') removes tabs from pages map
- Tab isolation uses BROWSE_TAB not system prompt hack
- Per-tab chat context in sidepanel
- Tab bar rendering, stop button, banner text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve merge conflicts — keep security defenses + per-tab isolation
Merged main's security improvements (XML escaping, prompt injection defense,
allowed commands whitelist, --model opus, Write tool, stderr capture) with
our branch's per-tab isolation (BROWSE_TAB env var, processingTabs set,
no --resume). Updated test expectations for expanded system prompt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v0.13.9.0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add inspector message types to background.js allowlist
Pre-existing bug found by Codex: ALLOWED_TYPES in background.js was missing
all inspector message types (startInspector, stopInspector, elementPicked,
pickerCancelled, applyStyle, toggleClass, injectCSS, resetAll, inspectResult).
Messages were silently rejected, making the inspector broken on ALL pages.
Also: separate executeScript and insertCSS into individual try blocks in
injectInspector(), store inspectorMode for routing, and add content.js
fallback when script injection fails (CSP, chrome:// pages).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: basic element picker in content.js for CSP-restricted pages
When inspector.js can't be injected (CSP, chrome:// pages), content.js
provides a basic picker using getComputedStyle + CSSOM:
- startBasicPicker/stopBasicPicker message handlers
- captureBasicData() with ~30 key CSS properties, box model, matched rules
- Hover highlight with outline save/restore (never leaves artifacts)
- Click uses e.target directly (no re-querying by selector)
- Sends inspectResult with mode:'basic' for sidebar rendering
- Escape key cancels picker and restores outlines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: cleanup + screenshot buttons in sidebar inspector toolbar
Two action buttons in the inspector toolbar:
- Cleanup (🧹): POSTs cleanup --all to server, shows spinner, chat
notification on success, resets inspector state (element may be removed)
- Screenshot (📸): POSTs screenshot to server, shows spinner, chat
notification with saved file path
Shared infrastructure:
- .inspector-action-btn CSS with loading spinner via ::after pseudo-element
- chat-notification type in addChatEntry() for system messages
- package.json version bump to 0.13.9.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: inspector allowlist, CSP fallback, cleanup/screenshot buttons
16 new tests in sidebar-ux.test.ts:
- Inspector message allowlist includes all inspector types
- content.js basic picker (startBasicPicker, captureBasicData, CSSOM,
outline save/restore, inspectResult with mode basic, Escape cleanup)
- background.js CSP fallback (separate try blocks, inspectorMode, fallback)
- Cleanup button (POST /command, inspector reset after success)
- Screenshot button (POST /command, notification rendering)
- Chat notification type and CSS styles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: update project documentation for v0.13.9.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: cleanup + screenshot buttons in chat toolbar (not just inspector)
Quick actions toolbar (🧹 Cleanup, 📸 Screenshot) now appears above the chat
input, always visible. Both inspector and chat buttons share runCleanup() and
runScreenshot() helper functions. Clicking either set shows loading state on
both simultaneously.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: chat toolbar buttons, shared helpers, quick-action-btn styles
Tests that chat toolbar exists (chat-cleanup-btn, chat-screenshot-btn,
quick-actions container), CSS styles (.quick-action-btn, .quick-action-btn.loading),
shared runCleanup/runScreenshot helper functions, and cleanup inspector reset.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: aggressive cleanup heuristics — overlays, scroll unlock, blur removal
Massively expanded CLEANUP_SELECTORS with patterns from uBlock Origin and
Readability.js research:
- ads: 30+ selectors (Google, Amazon, Outbrain, Taboola, Criteo, etc.)
- cookies: OneTrust, Cookiebot, TrustArc, Quantcast + generic patterns
- overlays (NEW): paywalls, newsletter popups, interstitials, push prompts,
app download banners, survey modals
- social: follow prompts, share tools
- Cleanup now defaults to --all when no args (sidebar button fix)
- Uses !important on all display:none (overrides inline styles)
- Unlocks body/html scroll (overflow:hidden from modal lockout)
- Removes blur/filter effects (paywall content blur)
- Removes max-height truncation (article teaser truncation)
- Collapses empty ad placeholder whitespace (empty divs after ad removal)
- Skips gstack-ctrl indicator in sticky removal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: disable action buttons when disconnected, no error spam
- setActionButtonsEnabled() toggles .disabled class on all cleanup/screenshot
buttons (both chat toolbar and inspector toolbar)
- Called with false in updateConnection when server URL is null
- Called with true when connection established
- runCleanup/runScreenshot silently return when disconnected instead of
showing 'Not connected' error notifications
- CSS .disabled style: pointer-events:none, opacity:0.3, cursor:not-allowed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: cleanup heuristics, button disabled state, overlay selectors
17 new tests:
- cleanup defaults to --all on empty args
- CLEANUP_SELECTORS overlays category (paywall, newsletter, interstitial)
- Major ad networks in selectors (doubleclick, taboola, criteo, etc.)
- Major consent frameworks (OneTrust, Cookiebot, TrustArc, Quantcast)
- !important override for inline styles
- Scroll unlock (body overflow:hidden)
- Blur removal (paywall content blur)
- Article truncation removal (max-height)
- Empty placeholder collapse
- gstack-ctrl indicator skip in sticky cleanup
- setActionButtonsEnabled function
- Buttons disabled when disconnected
- No error spam from cleanup/screenshot when disconnected
- CSS disabled styles for action buttons
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: LLM-based page cleanup — agent analyzes page semantically
Instead of brittle CSS selectors, the cleanup button now sends a prompt to
the sidebar agent (which IS an LLM). The agent:
1. Runs deterministic $B cleanup --all as a quick first pass
2. Takes a snapshot to see what's left
3. Analyzes the page semantically to identify remaining clutter
4. Removes elements intelligently, preserving site branding
This means cleanup works correctly on any site without site-specific selectors.
The LLM understands that "Your Daily Puzzles" is clutter, "ADVERTISEMENT" is
junk, but the SF Chronicle masthead should stay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: aggressive cleanup heuristics + preserve top nav bar
Deterministic cleanup improvements (used as first pass before LLM analysis):
- New 'clutter' category: audio players, podcast widgets, sidebar puzzles/games,
recirculation widgets (taboola, outbrain, nativo), cross-promotion banners
- Text-content detection: removes "ADVERTISEMENT", "Article continues below",
"Sponsored", "Paid content" labels and their parent wrappers
- Sticky fix: preserves the topmost full-width element near viewport top (site
nav bar) instead of hiding all sticky/fixed elements. Sorts by vertical
position, preserves the first one that spans >80% viewport width.
Tests: clutter category, ad label removal, nav bar preservation logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: LLM-based cleanup architecture, deterministic heuristics, sticky nav
22 new tests covering:
- Cleanup button uses /sidebar-command (agent) not /command (deterministic)
- Cleanup prompt includes deterministic first pass + agent snapshot analysis
- Cleanup prompt lists specific clutter categories for agent guidance
- Cleanup prompt preserves site identity (masthead, headline, body, byline)
- Cleanup prompt instructs scroll unlock and $B eval removal
- Loading state management (async agent, setTimeout)
- Deterministic clutter: audio/podcast, games/puzzles, recirculation
- Ad label text patterns (ADVERTISEMENT, Sponsored, Article continues)
- Ad label parent wrapper hiding for small containers
- Sticky nav preservation (sort by position, first full-width near top)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent repeat chat message rendering on reconnect/replay
Root cause: server persists chat to disk (chat.jsonl) and replays on restart.
Client had no dedup, so every reconnect re-rendered the entire history.
Messages from an old HN session would repeat endlessly on the SF Chronicle tab.
Fix: renderedEntryIds Set tracks which entry IDs have been rendered. addChatEntry
skips entries already in the set. Entries without an id (local notifications)
bypass the check. Clear chat resets the set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: agent stops when done, no focus stealing, opus for prompt injection safety
Three fixes for sidebar agent UX:
- System prompt: "Be CONCISE. STOP as soon as the task is done. Do NOT keep
exploring or doing bonus work." Prevents agent from endlessly taking
screenshots and highlighting elements after answering the question.
- switchTab(id, opts): new bringToFront option. Internal tab pinning
(BROWSE_TAB) uses bringToFront: false so agent commands never steal
window focus from the user's active app.
- Keep opus model (not sonnet) for prompt injection resistance on untrusted
web pages. Remove Write from allowedTools (agent only needs Bash for $B).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: agent conciseness, focus stealing, opus model, switchTab opts
Tests for the three UX fixes:
- System prompt contains STOP/CONCISE/Do NOT keep exploring
- sidebar agent uses opus (not sonnet) for prompt injection resistance
- switchTab has bringToFront option, defaults to true (opt-out)
- handleCommand tab pinning uses bringToFront: false (no focus steal)
- Updated stale tests: switchTab signature, allowedTools excludes Write,
narration -> conciseness, tab pinning restore calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: sidebar CSS interaction E2E — HN comment highlight round-trip
New E2E test (periodic tier, ~$2/run) that exercises the full sidebar
agent pipeline with CSS interaction:
1. Agent navigates to Hacker News
2. Clicks into the top story's comments
3. Reads comments and identifies the most insightful one
4. Highlights it with a 4px solid orange outline via style injection
Tests: navigation, snapshot, text reading, LLM judgment, CSS modification.
Requires real browser + real Claude (ANTHROPIC_API_KEY).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sidebar CSS E2E test — correct idle timeout (ms not s), pipe stdio
Root cause of test failure: BROWSE_IDLE_TIMEOUT is in milliseconds, not
seconds. '600' = 0.6 seconds, server died immediately after health check.
Fixed to '600000' (10 minutes).
Also: use 'pipe' stdio instead of file descriptors (closing fds kills child
on macOS/bun), catch ConnectionRefused on poll retry, 4 min poll timeout
for the multi-step opus task.
Test passes: agent navigates to HN, reads comments, identifies most
insightful one, highlights it with orange CSS, stops. 114s, $0.00.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
|
|
7665adf4fe
|
feat: headed mode + sidebar agent + Chrome extension (v0.12.0) (#517)
* feat: CDP connect — control real Chrome/Comet via Playwright
Add `connectCDP()` to BrowserManager: connects to a running browser via
Chrome DevTools Protocol. All existing browse commands work unchanged
through Playwright's abstraction layer.
- chrome-launcher.ts: browser discovery, CDP probe, auto-relaunch with rollback
- browser-manager.ts: connectCDP(), mode guards (close/closeTab/recreateContext/handoff),
auto-reconnect on browser restart, getRefMap() for extension API
- server.ts: CDP branch in start(), /health gains mode field, /refs endpoint,
idle timer only resets on /command (not passive endpoints)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: browse connect/disconnect/focus CLI commands
- connect: pre-server command that discovers browser, starts server in CDP mode
- disconnect: drops CDP connection, restarts in headless mode
- focus: brings browser window to foreground via osascript (macOS)
- status: now shows Mode: cdp | launched | headed
- startServer() accepts extra env vars for CDP URL/port passthrough
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: CDP-aware skill templates — skip cookie import in real browser mode
Skills now check `$B status` for CDP mode and skip:
- /qa: cookie import prompt, user-agent override, headless workarounds
- /design-review: cookie import for authenticated pages
- /setup-browser-cookies: returns "not needed" in CDP mode
Regenerated SKILL.md files from updated templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: activity streaming — SSE endpoint for Chrome extension Side Panel
Real-time browse command feed via Server-Sent Events:
- activity.ts: ActivityEntry type, CircularBuffer (capacity 1000), privacy
filtering (redacts passwords, auth tokens, sensitive URL params),
cursor-based gap detection, async subscriber notification
- server.ts: /activity/stream SSE, /activity/history REST, handleCommand
instrumented with command_start/command_end events
- 18 unit tests for filterArgs privacy, emitActivity, subscribe lifecycle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: Chrome extension Side Panel + Conductor API proposal
Chrome extension (Manifest V3, sideload):
- Side Panel with live activity feed, @ref overlays, dark terminal aesthetic
- Background worker: health polling, SSE relay, ref fetching
- Popup: port config, connection status, side panel launcher
- Content script: floating ref panel with @ref badges
Conductor API proposal (docs/designs/CONDUCTOR_SESSION_API.md):
- SSE endpoint for full Claude Code session mirroring in Side Panel
- Discovery via HTTP endpoint (not filesystem — extensions can't read files)
TODOS.md: add $B watch, multi-agent tabs, cross-platform CDP, Web Store publishing.
Mark CDP mode as shipped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: detect Conductor runtime, skip osascript quit for sandboxed apps
macOS App Management blocks Electron apps (Conductor) from quitting
other apps via osascript. Now detects the runtime environment:
- terminal/claude-code/codex: can manage apps freely
- conductor: prints manual restart instructions + polls for 60s
detectRuntime() checks env vars and parent process. When Chrome needs
restart but we can't quit it, prints step-by-step instructions and
waits for the user to restart Chrome with --remote-debugging-port.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: detect Conductor via actual env vars (CONDUCTOR_WORKSPACE_NAME)
Previous detection checked CONDUCTOR_WORKSPACE_ID which doesn't exist.
Conductor sets CONDUCTOR_WORKSPACE_NAME, CONDUCTOR_BIN_DIR, CONDUCTOR_PORT,
and __CFBundleIdentifier=com.conductor.app. Check these FIRST because
Conductor sessions also have ANTHROPIC_API_KEY (which was matching claude-code).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: connection status pill — floating indicator when gstack controls Chrome
Small pill in bottom-right corner of every page: "● gstack · 3 refs"
Shows when connected via CDP, fades to 30% opacity after 3s, full on hover.
Disappears entirely when disconnected.
Background worker now notifies content scripts on connect/disconnect state
changes so the pill appears/disappears without polling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: Chrome requires --user-data-dir for remote debugging
Chrome refuses --remote-debugging-port without an explicit --user-data-dir.
Add userDataDir to BrowserBinary registry (macOS Application Support paths)
and pass it in both auto-launch and manual restart instructions.
Fix double-quoting in CLI manual restart instructions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: Chrome must be fully quit before launching with --remote-debugging-port
Chrome refuses to enable CDP on its default profile when another instance
is running (even with explicit --user-data-dir). The only reliable path:
fully quit Chrome first, then relaunch with the flag.
Updated instructions to emphasize this clearly with verification step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: bin/chrome-cdp — quit Chrome and relaunch with CDP in one command
Quits Chrome gracefully, waits for full exit, relaunches with
--remote-debugging-port, polls until CDP is ready. Usage: chrome-cdp [port]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use Playwright channel:chrome instead of broken connectOverCDP
Playwright's connectOverCDP hangs with Chrome 146 due to CDP protocol
version mismatch. Switch to channel:'chrome' which uses Playwright's
native pipe protocol to launch the system Chrome binary directly.
This is simpler and more reliable:
- No CDP port discovery needed
- No --remote-debugging-port or --user-data-dir hassles
- $B connect just works — launches real Chrome headed window
- All Playwright APIs (snapshot, click, fill) work unchanged
bin/chrome-cdp updated with symlinked profile approach (kept for
manual CDP use cases, but $B connect no longer needs it).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: green border + gstack label on controlled Chrome window
Injects a 2px green border and small "gstack" label on every page
loaded in the controlled Chrome window via context.addInitScript().
Users can instantly tell which Chrome window Claude controls.
Also fixes close() for channel:chrome mode (uses browser.close()
not browser.disconnect() which doesn't exist).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: cleanup chrome-launcher runtime detection, remove puppeteer-core dep
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style(design): redesign controlled Chrome indicator
Replace crude green border + label with polished indicator:
- 2px shimmer gradient at top edge (green→cyan→green, 3s loop)
- Floating pill bottom-right with frosted glass bg, fades to 25%
opacity after 4s so it doesn't compete with page content
- prefers-reduced-motion disables shimmer animation
- Much more subtle — looks like a developer tool, not broken CSS
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: document real browser mode + Chrome extension in BROWSER.md and README.md
BROWSER.md: new sections for connect/disconnect/focus commands,
Chrome extension Side Panel install, CDP-aware skills, activity streaming.
Updated command reference table, key components, env vars, source map.
README.md: updated /browse description, added "Real browser mode" to
What's New section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: step-by-step Chrome extension install guide in BROWSER.md
Replace terse bullet points with numbered walkthrough covering:
developer mode toggle, load unpacked, macOS file picker tip (Cmd+Shift+G),
pin extension, configure port, open side panel. Added troubleshooting section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add Cmd+Shift+. tip for hidden folders in macOS file picker
macOS hides folders starting with . by default. Added both shortcuts:
Cmd+Shift+G (paste path directly) and Cmd+Shift+. (show hidden files).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: integrate hidden folder tips into the install flow naturally
Move Cmd+Shift+G and Cmd+Shift+. tips inline with the file picker
step instead of as a separate tip block after it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: auto-load Chrome extension when $B connect launches Chrome
Extension auto-loads via --load-extension flag — no manual chrome://extensions
install needed. findExtensionPath() checks repo root, global install, and dev
paths. Also adds bin/gstack-extension helper for manual install in regular
Chrome, and rewrites BROWSER.md install docs with auto-load as primary path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /connect-chrome skill — one command to launch Chrome with Side Panel
New skill that runs $B connect, verifies the connection, guides the user
to open the Side Panel, and demos the live activity feed. Extension auto-loads
via --load-extension so no manual chrome://extensions install needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use launchPersistentContext for Chrome extension loading
Playwright's chromium.launch() silently ignores --load-extension.
Switch to launchPersistentContext with ignoreDefaultArgs to remove
--disable-extensions flag. Use bundled Chromium (real Chrome blocks
unpacked extensions). Fixed port 34567 for CDP mode so the extension
auto-connects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: sync extension to DESIGN.md — amber accent, zinc neutrals, grain texture
Import design system from gstack-website. Update all extension colors:
green (#4ade80) → amber (#F59E0B/#FBBF24), zinc gray neutrals, grain
texture overlay. Regenerate icons as amber "G" monogram on dark background.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: sidebar chat with Claude Code — icon opens side panel directly
Replace popup flyout with direct side panel open on icon click. Primary
UI is now a chat interface that sends messages to Claude Code via file
queue. Activity/Refs tabs moved behind a debug toggle in the footer.
Command bar with history, auto-poll for responses, amber design system.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: sidebar agent — Claude-powered chat backend via file queue
Add /sidebar-command, /sidebar-response, and /sidebar-chat endpoints
to the browse server. sidebar-agent.ts watches the command queue file,
spawns claude -p with browse context for each message, and streams
responses back to the sidebar chat.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove duplicate gstack pill overlay, hide crash restore bubble
The addInitScript indicator and the extension's content script were both
injecting bottom-right pills, causing duplicates. Remove the pill from
addInitScript (extension handles it). Replace --restore-last-session with
--hide-crash-restore-bubble to suppress the "Chromium didn't shut down
correctly" dialog.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: state file authority — CDP server cannot be silently replaced
Hardens the connect/disconnect lifecycle:
- ensureServer() refuses to auto-start headless when CDP server is alive
- $B connect does full cleanup: SIGTERM → 2s → SIGKILL, profile locks, state
- shutdown() cleans Chromium SingletonLock/Socket/Cookie files
- uncaughtException/unhandledRejection handlers do emergency cleanup
This prevents the bug where a headless server overwrites the CDP server's
state file, causing $B commands to hit the wrong browser.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: sidebar agent streaming events + session state management
Enhance sidebar-agent.ts with:
- Live streaming of claude -p events (tool_use, text, result) to sidebar
- Session state file for BROWSE_STATE_FILE propagation to claude subprocess
- Improved logging (stderr, exit codes, event types)
- stdin.end() to prevent claude waiting for input
- summarizeToolInput() with path shortening for compact sidebar display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: sidebar chat UI — streaming events, agent status, reconnect retry
Sidebar panel improvements:
- Chat tab renders streaming agent events (tool_use, text, result)
- Thinking dots animation while agent processes
- Agent error display with styled error blocks
- tryConnect() with 2s retry loop for initial connection
- Debug tabs (Activity/Refs) hidden behind gear toggle
- Clear chat button
- Compact tool call display with path shortening
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: server-integrated sidebar agent with sessions and message queue
Move the sidebar agent from a separate bun process into server.ts:
- Agent spawns claude -p directly when messages arrive via /sidebar-command
- In-memory chat buffer backed by per-session chat.jsonl on disk
- Session manager: create, load, persist, list sessions
- Message queue (cap 5) with agent status tracking (idle/processing/hung)
- Stop/kill endpoints with queue dismiss support
- /health now returns agent status + session info
- All sidebar endpoints require Bearer auth
- Agent killed on server shutdown
- 120s timeout detects hung claude processes
Eliminates: file-queue polling, separate sidebar-agent.ts process,
stale auth tokens, state file conflicts between processes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: extension auth + token flow for server-integrated agent
Update Chrome extension to use Bearer auth on all sidebar endpoints:
- background.js captures auth token from /health, exposes via getToken msg
- background.js sets openPanelOnActionClick for direct side panel access
- sidepanel.js gets token from background, sends in all fetch headers
- Health broadcasts include token so sidebar auto-authenticates
- Removes popup from manifest — icon click opens side panel directly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: self-healing sidebar — reconnect banner, state machine, copy button
Sidebar UI now handles disconnection gracefully:
- Connection state machine: connected → reconnecting → dead
- Amber pulsing banner during reconnect (2s retry, 30 attempts)
- Red "Server offline" banner with Reconnect + Copy /connect-chrome buttons
- Green "Reconnected" toast that fades after 3s on successful reconnect
- Copy button lets user paste /connect-chrome into any Claude Code session
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: crash handling — save session, kill agent, distinct exit codes
Hardened shutdown/crash behavior:
- Browser disconnect exits with code 2 (distinct from crash code 1)
- emergencyCleanup kills agent subprocess and saves session state
- Clean shutdown saves session before exit (chat history persists)
- Clear user message on browser disconnect: "Run $B connect to reconnect"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: worktree-per-session isolation for sidebar agent
Each sidebar session gets an isolated git worktree so the agent's file
operations don't conflict with the user's working directory:
- createWorktree() creates detached HEAD worktree in ~/.gstack/worktrees/
- Falls back to main cwd for non-git repos or on creation failure
- Handles collision cleanup from prior crashes
- removeWorktree() cleans up on session switch and shutdown
- worktreePath persisted in session.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(qa): ISSUE-001 — disconnect blocked by CDP guard in ensureServer
$B disconnect was routed through ensureServer() which refused to start a
headless server when a CDP state file existed. Disconnect is now handled
before ensureServer() (like connect), with force-kill + cleanup fallback
when the CDP server is unresponsive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve claude binary path for daemon-spawned agent
The browse server runs as a daemon and may not inherit the user's shell
PATH. Add findClaudeBin() that checks ~/.local/bin/claude (standard
install location), which claude, and common system paths. Shows a clear
error in the sidebar chat if claude CLI is not found.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve claude symlinks + check Conductor bundled binary
posix_spawn fails on symlinks in compiled bun binaries. Now:
- Checks Conductor app's bundled binary first (not a symlink)
- Scans ~/.local/share/claude/versions/ for direct versioned binaries
- Uses fs.realpathSync() to resolve symlinks before spawning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: compiled bun binary cannot posix_spawn — use external agent process
Compiled bun binaries fail posix_spawn on ALL executables (even /bin/bash).
The server now writes to an agent queue file, and a separate non-compiled
bun process (sidebar-agent.ts) reads the queue, spawns claude, and POSTs
events back via /sidebar-agent/event.
Changes:
- server.ts: spawnClaude writes to queue file instead of spawning directly
- server.ts: new /sidebar-agent/event endpoint for agent → server relay
- server.ts: fix result event field name (event.text vs event.result)
- sidebar-agent.ts: rewritten to poll queue file, relay events via HTTP
- cli.ts: $B connect auto-starts sidebar-agent as non-compiled bun process
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: loading spinner on sidebar open while connecting to server
Shows an amber spinner with "Connecting..." when the sidebar first opens,
replacing the empty state. After the first successful /sidebar-chat poll:
- If chat history exists: renders it immediately
- If no history: shows the welcome message
Prevents the jarring empty-then-populated flash on sidebar open.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: zero-friction side panel — auto-open on install, pill is clickable
Three changes to eliminate manual side panel setup:
- Auto-open side panel on extension install/update (onInstalled listener)
- gstack pill (bottom-right) is now clickable — opens the side panel
- Pill has pointer-events: auto so clicks always register (was: none)
User no longer needs to find the puzzle piece icon, pin the extension,
or know the side panel exists. It opens automatically on first launch
and can be re-opened by clicking the floating gstack pill.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: kill CDP naming, delete chrome-launcher.ts dead code
The connectCDP() method and connectionMode: 'cdp' naming was a legacy
artifact — real Chrome was tried but failed (silently blocks
--load-extension), so the implementation already used Playwright's
bundled Chromium via launchPersistentContext(). The naming was
misleading.
Changes:
- Delete chrome-launcher.ts (361 LOC) — only import was in unreachable
attemptReconnect() method
- Delete dead attemptReconnect() and reconnecting field
- Delete preExistingTabIds (was for protecting real Chrome tabs we
never connect to)
- Rename connectCDP() → launchHeaded()
- Rename connectionMode: 'cdp' → 'headed' across all files
- Replace BROWSE_CDP_URL/BROWSE_CDP_PORT env vars with BROWSE_HEADED=1
- Regenerate SKILL.md files for updated command descriptions
- Move BrowserManager unit tests to browser-manager-unit.test.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: converge handoff into connect — extension loads on handoff
Handoff now uses launchPersistentContext() with extension auto-loading,
same as the connect/launchHeaded() path. This means when the agent
gets stuck (2FA, CAPTCHA) and hands off to the user, the Chrome
extension + side panel are available automatically.
Before: handoff used chromium.launch() + newContext() — no extension
After: handoff uses chromium.launchPersistentContext() — extension loads
Also sets connectionMode to 'headed' and disables dialog auto-accept
on handoff, matching connect behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: gate sidebar chat behind --chat flag
$B connect (default): headed Chromium + extension with Activity + Refs
tabs only. No separate agent spawned. Clean, no confusion.
$B connect --chat: same + Chat tab with standalone claude -p agent.
Shows experimental banner: "Standalone mode — this is a separate
agent from your workspace."
Implementation:
- cli.ts: parse --chat, set BROWSE_SIDEBAR_CHAT env, conditionally
spawn sidebar-agent
- server.ts: gate /sidebar-* routes behind chatEnabled, return 403
when disabled, include chatEnabled in /health response
- sidepanel.js: applyChatEnabled() hides/shows Chat tab + banner
- background.js: forward chatEnabled from health response
- sidepanel.html/css: experimental banner with amber styling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: file drop relay + $B inbox command
Sidebar agent now writes structured messages to .context/sidebar-inbox/
when processing user input. The workspace agent can read these via
$B inbox to see what the user reported from the browser.
File drop format:
.context/sidebar-inbox/{timestamp}-observation.json
{ type, timestamp, page: {url}, userMessage, sidebarSessionId }
Atomic writes (tmp + rename) prevent partial reads. $B inbox --clear
removes messages after display.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: $B watch — passive observation mode
Claude enters read-only mode and captures periodic snapshots (every 5s)
while the user browses. Mutation commands (click, fill, etc.) are
blocked during watch. $B watch stop exits and returns a summary with
the last snapshot.
Requires headed mode ($B connect). This is the inverse of the scout
pattern — the workspace agent watches through the browser instead of
the sidebar relaying to it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add coverage for sidebar-agent, file-drop, and watch mode
33 new tests covering:
- Sidebar agent queue parsing (valid/malformed/empty JSONL)
- writeToInbox file drop (directory creation, atomic writes, JSON format)
- Inbox command (display, sorting, --clear, malformed file handling)
- Watch mode state machine (start/stop cycles, snapshots, duration)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: TODOS cleanup + Chrome vs Chromium exploration doc
- Update TODOS.md: mark CDP mode, $B watch, sidebar scout as SHIPPED
- Delete dead "cross-platform CDP browser discovery" TODO
- Rename dependencies from "CDP connect" to "headed mode"
- Add docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md memorializing
the architecture exploration and decision to use Playwright Chromium
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add Conductor Chrome sidebar integration design doc
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sidebar-agent validates cwd before spawning claude
The queue entry may reference a worktree that was cleaned up between
sessions. Now falls back to process.cwd() if the path doesn't exist,
preventing silent spawn failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: gen-skill-docs resolver merge + preamble tier gate + plan file discovery
The local RESOLVERS record in gen-skill-docs.ts was shadowing the imported
canonical resolvers, causing stale test coverage and preamble generators
to be used instead of the authoritative versions in resolvers/.
Changes:
- Merge imported RESOLVERS with local overrides (spread + override pattern)
- Fix preamble tier gate: tier 1 skills no longer get AskUserQuestion format
- Make plan file discovery host-agnostic (search multiple plan dirs)
- Add missing E2E tier entries for ship/review plan completion tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: ungate sidebar agent + raise timeout to 5 minutes (v0.12.0)
Sidebar chat is now always available in headed mode — no --chat flag needed.
Agent tasks get 5 minutes instead of 2, enabling multi-page workflows like
navigating directories and filling forms across pages.
Changes:
- cli.ts: remove --chat flag, always set BROWSE_SIDEBAR_CHAT=1, always spawn agent
- server.ts: remove chatEnabled gate (403 response), raise AGENT_TIMEOUT_MS to 300s
- sidebar-agent.ts: raise child process timeout from 120s to 300s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: headed mode + sidebar agent documentation (v0.12.0)
- README: sidebar agent section, personal automation example (school parent
portal), two auth paths (manual login + cookie import), DevTools MCP mention
- BROWSER.md: sidebar agent section with usage, timeout, session isolation,
authentication, and random delay documentation
- connect-chrome template: add sidebar chat onboarding step
- CHANGELOG: v0.12.0 entry covering headed mode, sidebar agent, extension
- VERSION: bump to 0.12.0.0
- TODOS: Chrome DevTools MCP integration as P0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: regenerate SKILL.md files
Generated from updated templates + resolver merge. Key changes:
- Tier 1 skills no longer include AskUserQuestion format section
- Ship/review skills now include coverage gate with thresholds
- Connect-chrome skill includes sidebar chat onboarding step
- Plan file discovery uses host-agnostic paths
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: regenerate Codex connect-chrome skill
Updated preamble with proactive prompt and sidebar chat onboarding step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: network idle, state persistence, iframe support, chain pipe format (v0.12.1.0) (#516)
* feat: network idle detection + chain pipe format
- Upgrade click/fill/select from domcontentloaded to networkidle wait
(2s timeout, best-effort). Catches XHR/fetch triggered by interactions.
- Add pipe-delimited format to chain as JSON fallback:
$B chain 'goto url | click @e5 | snapshot -ic'
- Add post-loop networkidle wait in chain when last command was a write.
- Frame-aware: commands use target (getActiveFrameOrPage) for locator ops,
page-only ops (goto/back/forward/reload) guard against frame context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: $B state save/load + $B frame — new browse commands
- state save/load: persist cookies + URLs to .gstack/browse-states/{name}.json
File perms 0o600, name sanitized to [a-zA-Z0-9_-]. V1 skips localStorage
(breaks on load-before-navigate). Load replaces session via closeAllPages().
- frame: switch command context to iframe via CSS selector, @ref, --name, or
--url. 'frame main' returns to main frame. Execution target abstraction
(getActiveFrameOrPage) across read-commands, snapshot, and write-commands.
- Frame context cleared on tab switch, navigation, resume, and handoff.
- Snapshot shows [Context: iframe src="..."] header when in frame.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add tests for network idle, chain pipe format, state, and frame
- Network idle: click on fetch button waits for XHR, static click is fast
- Chain pipe: pipe-delimited commands, quoted args, JSON still works
- State: save/load round-trip, name sanitization, missing state error
- Frame: switch to iframe + back, snapshot context header, fill in frame,
goto-in-frame guard, usage error
New fixtures: network-idle.html (fetch + static buttons), iframe.html (srcdoc)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: review fixes — iframe ref scoping, detached frame recovery, state validation
- snapshot.ts: ref locators, cursor-interactive scan, and cursor locator
now use target (frame-aware) instead of page — fixes @ref clicking in iframes
- browser-manager.ts: getActiveFrameOrPage auto-recovers from detached frames
via isDetached() check
- meta-commands.ts: state load resets activeFrame, elementHandle disposed after
contentFrame(), state file schema validation (cookies + pages arrays),
filter empty pipe segments in chain tokenizer
- write-commands.ts: upload command uses target.locator() for frame support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: regenerate SKILL.md files + rebuild binary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v0.12.1.0)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|