Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f5daf7b
feat: CDP inspector module — persistent sessions, CSS cascade, style …
garrytan Mar 30, 2026
e084ca9
feat: browse server inspector endpoints + inspect/style/cleanup/prett…
garrytan Mar 30, 2026
f395f58
feat: sidebar CSS inspector — element picker, box model, rule cascade…
garrytan Mar 30, 2026
da5d008
docs: document inspect, style, cleanup, prettyscreenshot browse commands
garrytan Mar 30, 2026
dcf1b0d
feat: auto-track user-created tabs and handle tab close
garrytan Mar 30, 2026
54fec2d
feat: per-tab agent isolation via BROWSE_TAB environment variable
garrytan Mar 30, 2026
a36a3ac
feat: sidebar per-tab chat context, tab bar sync, stop button, UX polish
garrytan Mar 30, 2026
812882d
test: per-tab isolation, BROWSE_TAB pinning, tab tracking, sidebar UX
garrytan Mar 30, 2026
fe4441b
Merge remote-tracking branch 'origin/main' into garrytan/sidebar-css-…
garrytan Mar 30, 2026
91d2f73
fix: resolve merge conflicts — keep security defenses + per-tab isola…
garrytan Mar 30, 2026
f4ab540
chore: bump version and changelog (v0.13.9.0)
garrytan Mar 30, 2026
6238edd
fix: add inspector message types to background.js allowlist
garrytan Mar 30, 2026
8d65628
feat: basic element picker in content.js for CSP-restricted pages
garrytan Mar 30, 2026
7e31568
feat: cleanup + screenshot buttons in sidebar inspector toolbar
garrytan Mar 30, 2026
9698095
test: inspector allowlist, CSP fallback, cleanup/screenshot buttons
garrytan Mar 30, 2026
fc60651
docs: update project documentation for v0.13.9.0
garrytan Mar 30, 2026
92adc71
feat: cleanup + screenshot buttons in chat toolbar (not just inspector)
garrytan Mar 30, 2026
b09224e
test: chat toolbar buttons, shared helpers, quick-action-btn styles
garrytan Mar 30, 2026
2d2a808
feat: aggressive cleanup heuristics — overlays, scroll unlock, blur r…
garrytan Mar 30, 2026
6c3fdf3
fix: disable action buttons when disconnected, no error spam
garrytan Mar 30, 2026
80bb252
test: cleanup heuristics, button disabled state, overlay selectors
garrytan Mar 30, 2026
fa03b66
feat: LLM-based page cleanup — agent analyzes page semantically
garrytan Mar 30, 2026
0940d21
feat: aggressive cleanup heuristics + preserve top nav bar
garrytan Mar 30, 2026
18d6e10
test: LLM-based cleanup architecture, deterministic heuristics, stick…
garrytan Mar 30, 2026
561ce40
fix: resolve merge conflicts — rebase onto main v0.13.10.0
garrytan Mar 30, 2026
3df29fc
fix: prevent repeat chat message rendering on reconnect/replay
garrytan Mar 30, 2026
6c4b408
fix: agent stops when done, no focus stealing, opus for prompt inject…
garrytan Mar 30, 2026
c77f064
test: agent conciseness, focus stealing, opus model, switchTab opts
garrytan Mar 30, 2026
0ccb9c1
test: sidebar CSS interaction E2E — HN comment highlight round-trip
garrytan Mar 30, 2026
e9bfa7f
fix: resolve merge conflicts — bump to v0.14.1.0 on top of main's v0.…
garrytan Mar 30, 2026
2f3581d
fix: sidebar CSS E2E test — correct idle timeout (ms not s), pipe stdio
garrytan Mar 30, 2026
6442d9c
fix: resolve merge conflicts — bump to v0.14.2.0 on top of main's v0.…
garrytan Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion BROWSER.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ This document covers the command reference and internals of gstack's headless br
| Read | `text`, `html`, `links`, `forms`, `accessibility` | Extract content |
| Snapshot | `snapshot [-i] [-c] [-d N] [-s sel] [-D] [-a] [-o] [-C]` | Get refs, diff, annotate |
| Interact | `click`, `fill`, `select`, `hover`, `type`, `press`, `scroll`, `wait`, `viewport`, `upload` | Use the page |
| Inspect | `js`, `eval`, `css`, `attrs`, `is`, `console`, `network`, `dialog`, `cookies`, `storage`, `perf` | Debug and verify |
| Inspect | `js`, `eval`, `css`, `attrs`, `is`, `console`, `network`, `dialog`, `cookies`, `storage`, `perf`, `inspect [selector] [--all]` | Debug and verify |
| Style | `style <sel> <prop> <val>`, `style --undo [N]`, `cleanup [--all]`, `prettyscreenshot` | Live CSS editing and page cleanup |
| Visual | `screenshot [--viewport] [--clip x,y,w,h] [sel\|@ref] [path]`, `pdf`, `responsive` | See what Claude sees |
| Compare | `diff <url1> <url2>` | Spot differences between environments |
| Dialogs | `dialog-accept [text]`, `dialog-dismiss` | Control alert/confirm/prompt handling |
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## [0.14.2.0] - 2026-03-30 — Sidebar CSS Inspector + Per-Tab Agents

The sidebar is now a visual design tool. Pick any element on the page and see the full CSS rule cascade, box model, and computed styles right in the Side Panel. Edit styles live and see changes instantly. Each browser tab gets its own independent agent, so you can work on multiple pages simultaneously without cross-talk. Cleanup is LLM-powered... the agent snapshots the page, understands it semantically, and removes the junk while keeping the site's identity.

### Added

- **CSS Inspector in the sidebar.** Click "Pick Element", hover over anything, click it, and the sidebar shows the full CSS rule cascade with specificity badges, source file:line, box model visualization (gstack palette colors), and computed styles. Like Chrome DevTools, but inside the sidebar.
- **Live style editing.** `$B style .selector property value` modifies CSS rules in real time via CDP. Changes show instantly on the page. Undo with `$B style --undo`.
- **Per-tab agents.** Each browser tab gets its own Claude agent process via `BROWSE_TAB` env var. Switch tabs in the browser and the sidebar swaps to that tab's chat history. Ask questions about different pages in parallel without agents fighting over which tab is active.
- **Tab tracking.** User-created tabs (Cmd+T, right-click "Open in new tab") are automatically tracked via `context.on('page')`. The sidebar tab bar updates in real time. Click a tab in the sidebar to switch the browser. Close a tab and it disappears.
- **LLM-powered page cleanup.** The cleanup button sends a prompt to the sidebar agent (which IS an LLM). The agent runs a deterministic first pass, snapshots the page, analyzes what's left, and removes clutter intelligently while preserving site branding. Works on any site without brittle CSS selectors.
- **Pretty screenshots.** `$B prettyscreenshot --cleanup --scroll-to ".pricing" ~/Desktop/hero.png` combines cleanup, scroll positioning, and screenshot in one command.
- **Stop button.** A red stop button appears in the sidebar when an agent is working. Click it to cancel the current task.
- **CSP fallback for inspector.** Sites with strict Content Security Policy (like SF Chronicle) now get a basic picker via the always-loaded content script. You see computed styles, box model, and same-origin CSS rules. Full CDP mode on sites that allow it.
- **Cleanup + Screenshot buttons in chat toolbar.** Not hidden in debug... right there in the chat. Disabled when disconnected so you don't get error spam.

### Fixed

- **Inspector message allowlist.** The background.js allowlist was missing all inspector message types, silently rejecting them. The inspector was broken for all pages, not just CSP-restricted ones. (Found by Codex review.)
- **Sticky nav preservation.** Cleanup no longer removes the site's top nav bar. Sorts sticky elements by position and preserves the first full-width element near the top.
- **Agent won't stop.** System prompt now tells the agent to be concise and stop when done. No more endless screenshot-and-highlight loops.
- **Focus stealing.** Agent commands no longer pull Chrome to the foreground. Internal tab pinning uses `bringToFront: false`.
- **Chat message dedup.** Old messages from previous sessions no longer repeat on reconnect.

### Changed

- **Sidebar banner** now says "Browser co-pilot" instead of the old mode-specific text.
- **Input placeholder** is "Ask about this page..." (more inviting than the old placeholder).
- **System prompt** includes prompt injection defense and allowed-commands whitelist from the security audit.

## [0.14.1.0] - 2026-03-30 — Comparison Board is the Chooser

The design comparison board now always opens automatically when reviewing variants. No more inline image + "which do you prefer?" — the board has rating controls, comments, remix/regenerate buttons, and structured feedback output. That's the experience. All 3 design skills (/plan-design-review, /design-shotgun, /design-consultation) get this fix.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ gstack/
│ ├── src/ # CLI + commands (generate, variants, compare, serve, etc.)
│ ├── test/ # Integration tests
│ └── dist/ # Compiled binary
├── extension/ # Chrome extension (side panel + activity feed)
├── extension/ # Chrome extension (side panel + activity feed + CSS inspector)
├── lib/ # Shared libraries (worktree.ts)
├── docs/designs/ # Design documents
├── setup-deploy/ # /setup-deploy skill (one-time deploy config)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan-
| `/freeze` | **Edit Lock** — restrict file edits to one directory. Prevents accidental changes outside scope while debugging. |
| `/guard` | **Full Safety** — `/careful` + `/freeze` in one command. Maximum safety for prod work. |
| `/unfreeze` | **Unlock** — remove the `/freeze` boundary. |
| `/connect-chrome` | **Chrome Controller** — launch your real Chrome controlled by gstack with the Side Panel extension. Watch every action live. |
| `/connect-chrome` | **Chrome Controller** — launch Chrome with the Side Panel extension. Watch every action live, inspect CSS on any element, clean up pages, and take screenshots. Each tab gets its own agent. |
| `/setup-deploy` | **Deploy Configurator** — one-time setup for `/land-and-deploy`. Detects your platform, production URL, and deploy commands. |
| `/gstack-upgrade` | **Self-Updater** — upgrade gstack to latest. Detects global vs vendored install, syncs both, shows what changed. |

Expand Down
4 changes: 4 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
### Interaction
| Command | Description |
|---------|-------------|
| `cleanup [--ads] [--cookies] [--sticky] [--social] [--all]` | Remove page clutter (ads, cookie banners, sticky elements, social widgets) |
| `click <sel>` | Click element |
| `cookie <name>=<value>` | Set cookie on current page domain |
| `cookie-import <json>` | Import cookies from JSON file |
Expand All @@ -686,6 +687,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
| `press <key>` | Press key — Enter, Tab, Escape, ArrowUp/Down/Left/Right, Backspace, Delete, Home, End, PageUp, PageDown, or modifiers like Shift+Enter |
| `scroll [sel]` | Scroll element into view, or scroll to page bottom if no selector |
| `select <sel> <val>` | Select dropdown option by value, label, or visible text |
| `style <sel> <prop> <value> | style --undo [N]` | Modify CSS property on element (with undo support) |
| `type <text>` | Type into focused element |
| `upload <sel> <file> [file2...]` | Upload file(s) |
| `useragent <string>` | Set user agent |
Expand All @@ -701,6 +703,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
| `css <sel> <prop>` | Computed CSS value |
| `dialog [--clear]` | Dialog messages |
| `eval <file>` | Run JavaScript from file and return result as string (path must be under /tmp or cwd) |
| `inspect [selector] [--all] [--history]` | Deep CSS inspection via CDP — full rule cascade, box model, computed styles |
| `is <prop> <sel>` | State check (visible/hidden/enabled/disabled/checked/editable/focused) |
| `js <expr>` | Run JavaScript expression and return result as string |
| `network [--clear]` | Network requests |
Expand All @@ -712,6 +715,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
|---------|-------------|
| `diff <url1> <url2>` | Text diff between pages |
| `pdf [path]` | Save as PDF |
| `prettyscreenshot [--scroll-to sel|text] [--cleanup] [--hide sel...] [--width px] [path]` | Clean screenshot with optional cleanup, scroll positioning, and element hiding |
| `responsive [prefix]` | Screenshots at mobile (375x812), tablet (768x1024), desktop (1280x720). Saves as {prefix}-mobile.png etc. |
| `screenshot [--viewport] [--clip x,y,w,h] [selector|@ref] [path]` | Save screenshot (supports element crop via CSS/@ref, --clip region, --viewport) |

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.14.1.0
0.14.2.0
28 changes: 28 additions & 0 deletions browse/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,30 @@ $B click @c1 # cursor-interactive ref (from -C)

Refs are invalidated on navigation — run `snapshot` again after `goto`.

## CSS Inspector & Style Modification

### Inspect element CSS
```bash
$B inspect .header # full CSS cascade for selector
$B inspect # latest picked element from sidebar
$B inspect --all # include user-agent stylesheet rules
$B inspect --history # show modification history
```

### Modify styles live
```bash
$B style .header background-color #1a1a1a # modify CSS property
$B style --undo # revert last change
$B style --undo 2 # revert specific change
```

### Clean screenshots
```bash
$B cleanup --all # remove ads, cookies, sticky, social
$B cleanup --ads --cookies # selective cleanup
$B prettyscreenshot --cleanup --scroll-to ".pricing" --width 1440 ~/Desktop/hero.png
```

## Full Command List

### Navigation
Expand Down Expand Up @@ -542,6 +566,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
### Interaction
| Command | Description |
|---------|-------------|
| `cleanup [--ads] [--cookies] [--sticky] [--social] [--all]` | Remove page clutter (ads, cookie banners, sticky elements, social widgets) |
| `click <sel>` | Click element |
| `cookie <name>=<value>` | Set cookie on current page domain |
| `cookie-import <json>` | Import cookies from JSON file |
Expand All @@ -554,6 +579,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
| `press <key>` | Press key — Enter, Tab, Escape, ArrowUp/Down/Left/Right, Backspace, Delete, Home, End, PageUp, PageDown, or modifiers like Shift+Enter |
| `scroll [sel]` | Scroll element into view, or scroll to page bottom if no selector |
| `select <sel> <val>` | Select dropdown option by value, label, or visible text |
| `style <sel> <prop> <value> | style --undo [N]` | Modify CSS property on element (with undo support) |
| `type <text>` | Type into focused element |
| `upload <sel> <file> [file2...]` | Upload file(s) |
| `useragent <string>` | Set user agent |
Expand All @@ -569,6 +595,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
| `css <sel> <prop>` | Computed CSS value |
| `dialog [--clear]` | Dialog messages |
| `eval <file>` | Run JavaScript from file and return result as string (path must be under /tmp or cwd) |
| `inspect [selector] [--all] [--history]` | Deep CSS inspection via CDP — full rule cascade, box model, computed styles |
| `is <prop> <sel>` | State check (visible/hidden/enabled/disabled/checked/editable/focused) |
| `js <expr>` | Run JavaScript expression and return result as string |
| `network [--clear]` | Network requests |
Expand All @@ -580,6 +607,7 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
|---------|-------------|
| `diff <url1> <url2>` | Text diff between pages |
| `pdf [path]` | Save as PDF |
| `prettyscreenshot [--scroll-to sel|text] [--cleanup] [--hide sel...] [--width px] [path]` | Clean screenshot with optional cleanup, scroll positioning, and element hiding |
| `responsive [prefix]` | Screenshots at mobile (375x812), tablet (768x1024), desktop (1280x720). Saves as {prefix}-mobile.png etc. |
| `screenshot [--viewport] [--clip x,y,w,h] [selector|@ref] [path]` | Save screenshot (supports element crop via CSS/@ref, --clip region, --viewport) |

Expand Down
24 changes: 24 additions & 0 deletions browse/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,30 @@ After `resume`, you get a fresh snapshot of wherever the user left off.

{{SNAPSHOT_FLAGS}}

## CSS Inspector & Style Modification

### Inspect element CSS
```bash
$B inspect .header # full CSS cascade for selector
$B inspect # latest picked element from sidebar
$B inspect --all # include user-agent stylesheet rules
$B inspect --history # show modification history
```

### Modify styles live
```bash
$B style .header background-color #1a1a1a # modify CSS property
$B style --undo # revert last change
$B style --undo 2 # revert specific change
```

### Clean screenshots
```bash
$B cleanup --all # remove ads, cookies, sticky, social
$B cleanup --ads --cookies # selective cleanup
$B prettyscreenshot --cleanup --scroll-to ".pricing" --width 1440 ~/Desktop/hero.png
```

## Full Command List

{{COMMAND_REFERENCE}}
81 changes: 80 additions & 1 deletion browse/src/browser-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,17 @@ export class BrowserManager {
};
await this.context.addInitScript(indicatorScript);

// Track user-created tabs automatically (Cmd+T, link opens in new tab, etc.)
this.context.on('page', (page) => {
const id = this.nextTabId++;
this.pages.set(id, page);
this.activeTabId = id;
this.wirePageEvents(page);
// Inject indicator on the new tab
page.evaluate(indicatorScript).catch(() => {});
console.log(`[browse] New tab detected (id=${id}, total=${this.pages.size})`);
});

// Persistent context opens a default page — adopt it instead of creating a new one
const existingPages = this.context.pages();
if (existingPages.length > 0) {
Expand Down Expand Up @@ -410,10 +421,62 @@ export class BrowserManager {
}
}

switchTab(id: number): void {
switchTab(id: number, opts?: { bringToFront?: boolean }): void {
if (!this.pages.has(id)) throw new Error(`Tab ${id} not found`);
this.activeTabId = id;
this.activeFrame = null; // Frame context is per-tab
// Only bring to front when explicitly requested (user-initiated tab switch).
// Internal tab pinning (BROWSE_TAB) should NOT steal focus.
if (opts?.bringToFront !== false) {
const page = this.pages.get(id);
if (page) page.bringToFront().catch(() => {});
}
}

/**
* Sync activeTabId to match the tab whose URL matches the Chrome extension's
* active tab. Called on every /sidebar-tabs poll so manual tab switches in
* the browser are detected within ~2s.
*/
syncActiveTabByUrl(activeUrl: string): void {
if (!activeUrl || this.pages.size <= 1) return;
// Try exact match first, then fuzzy match (origin+pathname, ignoring query/fragment)
let fuzzyId: number | null = null;
let activeOriginPath = '';
try {
const u = new URL(activeUrl);
activeOriginPath = u.origin + u.pathname;
} catch {}

for (const [id, page] of this.pages) {
try {
const pageUrl = page.url();
// Exact match — best case
if (pageUrl === activeUrl && id !== this.activeTabId) {
this.activeTabId = id;
this.activeFrame = null;
return;
}
// Fuzzy match — origin+pathname (handles query param / fragment differences)
if (activeOriginPath && fuzzyId === null && id !== this.activeTabId) {
try {
const pu = new URL(pageUrl);
if (pu.origin + pu.pathname === activeOriginPath) {
fuzzyId = id;
}
} catch {}
}
} catch {}
}
// Fall back to fuzzy match
if (fuzzyId !== null) {
this.activeTabId = fuzzyId;
this.activeFrame = null;
}
}

getActiveTabId(): number {
return this.activeTabId;
}

getTabCount(): number {
Expand Down Expand Up @@ -876,6 +939,22 @@ export class BrowserManager {

// ─── Console/Network/Dialog/Ref Wiring ────────────────────
private wirePageEvents(page: Page) {
// Track tab close — remove from pages map, switch to another tab
page.on('close', () => {
for (const [id, p] of this.pages) {
if (p === page) {
this.pages.delete(id);
console.log(`[browse] Tab closed (id=${id}, remaining=${this.pages.size})`);
// If the closed tab was active, switch to another
if (this.activeTabId === id) {
const remaining = [...this.pages.keys()];
this.activeTabId = remaining.length > 0 ? remaining[remaining.length - 1] : 0;
}
break;
}
}
});

// Clear ref map on navigation — refs point to stale elements after page change
// (lastSnapshot is NOT cleared — it's a text baseline for diffing)
page.on('framenavigated', (frame) => {
Expand Down
Loading
Loading