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>
This commit is contained in:
Garry Tan
2026-03-21 13:31:35 -07:00
parent 64e8cd4fa5
commit 594e0c7bae
3 changed files with 135 additions and 40 deletions
+28 -39
View File
@@ -164,54 +164,43 @@ The window has a subtle green shimmer line at the top edge and a floating "gstac
### Chrome extension (Side Panel)
A Chrome extension that shows a live activity feed of browse commands in a Side Panel, plus @ref overlays on the page. Works in any Chrome-based browser (Chrome, Comet, Edge).
A Chrome extension that shows a live activity feed of browse commands in a Side Panel, plus @ref overlays on the page.
#### Step-by-step install
#### Automatic install (recommended)
**1. Open the extensions page**
When you run `$B connect`, the extension **auto-loads** into the Playwright-controlled Chrome window. No manual steps needed — the Side Panel is immediately available.
Type `chrome://extensions` in Chrome's address bar and press Enter.
**2. Enable Developer mode**
In the top-right corner of the extensions page, toggle the **Developer mode** switch ON. You'll see new buttons appear: "Load unpacked", "Pack extension", and "Update".
**3. Load the extension**
Click **Load unpacked**. A file picker dialog opens.
A file picker opens. You need to navigate to the `extension/` folder inside gstack, but macOS hides folders starting with `.` by default. The easiest way: press **Cmd+Shift+G** in the file picker to open "Go to folder", then paste one of these paths:
- Global install: `~/.claude/skills/gstack/extension`
- Project install: `<your-repo>/.claude/skills/gstack/extension`
- Dev/source: `<gstack-repo>/extension`
Press Enter, then click **Select** (select the `extension/` folder itself, not a file inside it).
Alternatively, press **Cmd+Shift+.** (period) in the file picker to reveal hidden files, then navigate to `.claude/skills/gstack/extension` manually.
**4. Pin the extension**
Click the puzzle piece icon (Extensions) in Chrome's toolbar. Find "gstack browse" and click the pin icon so it's always visible.
**5. Configure the port**
Click the gstack icon in the toolbar. A popup appears with a port input field.
Find your browse server port — run `$B status` or check `.gstack/browse.json` in your project root:
```bash
cat .gstack/browse.json | grep port
# or
$B status # shows the port in the output
$B connect # launches Chrome with extension pre-loaded
# Click the gstack icon in toolbar → Open Side Panel
```
Enter the port number and press Enter. The popup saves it and starts polling.
The port is auto-configured. You're done.
**6. Open the Side Panel**
#### Manual install (for your regular Chrome)
Click the gstack icon again, then click **Open Side Panel**. The Side Panel slides open on the right side of Chrome showing a live activity feed.
If you want the extension in your everyday Chrome (not the Playwright-controlled one), run:
Alternatively, right-click the gstack icon and choose "Open side panel."
```bash
bin/gstack-extension # opens chrome://extensions, copies path to clipboard
```
Or do it manually:
1. **Go to `chrome://extensions`** in Chrome's address bar
2. **Toggle "Developer mode" ON** (top-right corner)
3. **Click "Load unpacked"** — a file picker opens
4. **Navigate to the extension folder:** Press **Cmd+Shift+G** in the file picker to open "Go to folder", then paste one of these paths:
- Global install: `~/.claude/skills/gstack/extension`
- Dev/source: `<gstack-repo>/extension`
Press Enter, then click **Select**.
(Tip: macOS hides folders starting with `.` — press **Cmd+Shift+.** in the file picker to reveal them if you prefer to navigate manually.)
5. **Pin it:** Click the puzzle piece icon (Extensions) in the toolbar → pin "gstack browse"
6. **Set the port:** Click the gstack icon → enter the port from `$B status` or `.gstack/browse.json`
7. **Open Side Panel:** Click the gstack icon → "Open Side Panel"
#### What you get
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# gstack-extension — helper to install the Chrome extension
#
# When using $B connect, the extension auto-loads. This script is for
# installing it in your regular Chrome (not the Playwright-controlled one).
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Find the extension directory
EXT_DIR=""
if [ -f "$REPO_ROOT/extension/manifest.json" ]; then
EXT_DIR="$REPO_ROOT/extension"
elif [ -f "$HOME/.claude/skills/gstack/extension/manifest.json" ]; then
EXT_DIR="$HOME/.claude/skills/gstack/extension"
fi
if [ -z "$EXT_DIR" ]; then
echo "Error: extension/ directory not found."
echo "Expected at: $REPO_ROOT/extension/ or ~/.claude/skills/gstack/extension/"
exit 1
fi
# Copy path to clipboard
echo -n "$EXT_DIR" | pbcopy 2>/dev/null
# Get browse server port
PORT=""
STATE_FILE="$REPO_ROOT/.gstack/browse.json"
if [ -f "$STATE_FILE" ]; then
PORT=$(grep -o '"port":[0-9]*' "$STATE_FILE" | grep -o '[0-9]*')
fi
echo "gstack Chrome Extension Setup"
echo "=============================="
echo ""
echo "Extension path (copied to clipboard):"
echo " $EXT_DIR"
echo ""
if [ -n "$PORT" ]; then
echo "Browse server port: $PORT"
echo ""
fi
echo "Quick install (if using \$B connect):"
echo " The extension auto-loads when you run \$B connect."
echo " No manual installation needed!"
echo ""
echo "Manual install (for your regular Chrome):"
echo ""
echo " 1. Opening chrome://extensions now..."
# Open chrome://extensions
osascript -e 'tell application "Google Chrome" to open location "chrome://extensions"' 2>/dev/null || \
open "chrome://extensions" 2>/dev/null || \
echo " Could not open Chrome. Navigate to chrome://extensions manually."
echo " 2. Toggle 'Developer mode' ON (top-right)"
echo " 3. Click 'Load unpacked'"
echo " 4. In the file picker: Cmd+Shift+G → paste (path is in your clipboard) → Enter → Select"
echo " 5. Click the gstack puzzle icon in toolbar → enter port: ${PORT:-<check \$B status>}"
echo " 6. Click 'Open Side Panel'"
+42 -1
View File
@@ -70,6 +70,39 @@ export class BrowserManager {
getConnectionMode(): 'launched' | 'cdp' { return this.connectionMode; }
/**
* Find the gstack Chrome extension directory.
* Checks: repo root /extension, global install, dev install.
*/
private findExtensionPath(): string | null {
const fs = require('fs');
const path = require('path');
const candidates = [
// Relative to this source file (dev mode: browse/src/ -> ../../extension)
path.resolve(__dirname, '..', '..', 'extension'),
// Global gstack install
path.join(process.env.HOME || '', '.claude', 'skills', 'gstack', 'extension'),
// Git repo root (detected via BROWSE_STATE_FILE location)
(() => {
const stateFile = process.env.BROWSE_STATE_FILE || '';
if (stateFile) {
const repoRoot = path.resolve(path.dirname(stateFile), '..');
return path.join(repoRoot, '.claude', 'skills', 'gstack', 'extension');
}
return '';
})(),
].filter(Boolean);
for (const candidate of candidates) {
try {
if (fs.existsSync(path.join(candidate, 'manifest.json'))) {
return candidate;
}
} catch {}
}
return null;
}
/**
* Get the ref map for external consumers (e.g., /refs endpoint).
*/
@@ -125,12 +158,20 @@ export class BrowserManager {
this.refMap.clear();
this.nextTabId = 1;
// Find the gstack extension directory for auto-loading
const extensionPath = this.findExtensionPath();
const launchArgs = ['--restore-last-session'];
if (extensionPath) {
launchArgs.push(`--disable-extensions-except=${extensionPath}`);
launchArgs.push(`--load-extension=${extensionPath}`);
}
// Launch real Chrome via Playwright's channel protocol
// This uses the system Chrome binary, headed, with real window
this.browser = await chromium.launch({
channel: 'chrome',
headless: false,
args: ['--restore-last-session'],
args: launchArgs,
});
this.connectionMode = 'cdp';
this.intentionalDisconnect = false;