Files
gstack/extension/background.js
T
Garry Tan 410d0abd9b 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>
2026-03-21 10:23:47 -07:00

131 lines
3.9 KiB
JavaScript

/**
* gstack browse — background service worker
*
* Polls /health every 10s to detect browse server.
* Fetches /refs on snapshot completion, relays to content script.
* Updates badge: green (connected), gray (disconnected).
*/
let serverPort = null;
let isConnected = false;
let healthInterval = null;
// ─── Port Discovery ────────────────────────────────────────────
async function loadPort() {
const data = await chrome.storage.local.get('port');
serverPort = data.port || null;
return serverPort;
}
async function savePort(port) {
serverPort = port;
await chrome.storage.local.set({ port });
}
function getBaseUrl() {
return serverPort ? `http://127.0.0.1:${serverPort}` : null;
}
// ─── Health Polling ────────────────────────────────────────────
async function checkHealth() {
const base = getBaseUrl();
if (!base) {
setDisconnected();
return;
}
try {
const resp = await fetch(`${base}/health`, { signal: AbortSignal.timeout(3000) });
if (!resp.ok) { setDisconnected(); return; }
const data = await resp.json();
if (data.status === 'healthy') {
setConnected(data);
} else {
setDisconnected();
}
} catch {
setDisconnected();
}
}
function setConnected(healthData) {
if (!isConnected) {
isConnected = true;
chrome.action.setBadgeText({ text: '' });
chrome.action.setBadgeBackgroundColor({ color: '#4ade80' });
// Small green dot via badge
chrome.action.setBadgeText({ text: ' ' });
}
// Broadcast health to popup and side panel
chrome.runtime.sendMessage({ type: 'health', data: healthData }).catch(() => {});
}
function setDisconnected() {
if (isConnected) {
isConnected = false;
chrome.action.setBadgeText({ text: '' });
}
chrome.runtime.sendMessage({ type: 'health', data: null }).catch(() => {});
}
// ─── Refs Relay ─────────────────────────────────────────────────
async function fetchAndRelayRefs() {
const base = getBaseUrl();
if (!base || !isConnected) return;
try {
const resp = await fetch(`${base}/refs`, { signal: AbortSignal.timeout(3000) });
if (!resp.ok) return;
const data = await resp.json();
// Send to all tabs' content scripts
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (tab.id) {
chrome.tabs.sendMessage(tab.id, { type: 'refs', data }).catch(() => {});
}
}
} catch {}
}
// ─── Message Handling ──────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'getPort') {
sendResponse({ port: serverPort, connected: isConnected });
return true;
}
if (msg.type === 'setPort') {
savePort(msg.port).then(() => {
checkHealth();
sendResponse({ ok: true });
});
return true;
}
if (msg.type === 'getServerUrl') {
sendResponse({ url: getBaseUrl() });
return true;
}
if (msg.type === 'fetchRefs') {
fetchAndRelayRefs().then(() => sendResponse({ ok: true }));
return true;
}
});
// ─── Side Panel ─────────────────────────────────────────────────
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }).catch(() => {});
// ─── Startup ────────────────────────────────────────────────────
loadPort().then(() => {
checkHealth();
healthInterval = setInterval(checkHealth, 10000);
});