mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-17 09:41:28 +08:00
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>
This commit is contained in:
130
extension/background.js
Normal file
130
extension/background.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user