diff --git a/extension/background.js b/extension/background.js index 6b5b08b7..e17834db 100644 --- a/extension/background.js +++ b/extension/background.js @@ -3,10 +3,13 @@ * * Polls /health every 10s to detect browse server. * Fetches /refs on snapshot completion, relays to content script. - * Updates badge: green (connected), gray (disconnected). + * Proxies commands from sidebar → browse server. + * Updates badge: amber (connected), gray (disconnected). */ +const DEFAULT_PORT = 34567; // Well-known port used by `$B connect` let serverPort = null; +let authToken = null; let isConnected = false; let healthInterval = null; @@ -14,7 +17,7 @@ let healthInterval = null; async function loadPort() { const data = await chrome.storage.local.get('port'); - serverPort = data.port || null; + serverPort = data.port || DEFAULT_PORT; return serverPort; } @@ -41,6 +44,8 @@ async function checkHealth() { if (!resp.ok) { setDisconnected(); return; } const data = await resp.json(); if (data.status === 'healthy') { + // Capture auth token from health response + if (data.token) authToken = data.token; setConnected(data); } else { setDisconnected(); @@ -53,7 +58,7 @@ async function checkHealth() { function setConnected(healthData) { const wasDisconnected = !isConnected; isConnected = true; - chrome.action.setBadgeBackgroundColor({ color: '#4ade80' }); + chrome.action.setBadgeBackgroundColor({ color: '#F59E0B' }); chrome.action.setBadgeText({ text: ' ' }); // Broadcast health to popup and side panel @@ -68,6 +73,7 @@ function setConnected(healthData) { function setDisconnected() { const wasConnected = isConnected; isConnected = false; + authToken = null; chrome.action.setBadgeText({ text: '' }); chrome.runtime.sendMessage({ type: 'health', data: null }).catch(() => {}); @@ -89,6 +95,31 @@ async function notifyContentScripts(type) { } catch {} } +// ─── Command Proxy ───────────────────────────────────────────── + +async function executeCommand(command, args) { + const base = getBaseUrl(); + if (!base || !authToken) { + return { error: 'Not connected to browse server' }; + } + + try { + const resp = await fetch(`${base}/command`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}`, + }, + body: JSON.stringify({ command, args }), + signal: AbortSignal.timeout(30000), + }); + const data = await resp.json(); + return data; + } catch (err) { + return { error: err.message || 'Command failed' }; + } +} + // ─── Refs Relay ───────────────────────────────────────────────── async function fetchAndRelayRefs() { @@ -135,11 +166,41 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { fetchAndRelayRefs().then(() => sendResponse({ ok: true })); return true; } + + // Sidebar → browse server command proxy + if (msg.type === 'command') { + executeCommand(msg.command, msg.args).then(result => sendResponse(result)); + return true; + } + + // Sidebar → Claude Code (file-based message queue) + if (msg.type === 'sidebar-command') { + const base = getBaseUrl(); + if (!base || !authToken) { + sendResponse({ error: 'Not connected' }); + return true; + } + fetch(`${base}/sidebar-command`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}`, + }, + body: JSON.stringify({ message: msg.message }), + }) + .then(r => r.json()) + .then(data => sendResponse(data)) + .catch(err => sendResponse({ error: err.message })); + return true; + } }); // ─── Side Panel ───────────────────────────────────────────────── -chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }).catch(() => {}); +// Click extension icon → open side panel directly (no popup) +if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}); +} // ─── Startup ──────────────────────────────────────────────────── diff --git a/extension/manifest.json b/extension/manifest.json index 3a28d5ed..ea710e14 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -6,7 +6,6 @@ "permissions": ["sidePanel", "storage", "activeTab"], "host_permissions": ["http://127.0.0.1:*/"], "action": { - "default_popup": "popup.html", "default_icon": { "16": "icons/icon-16.png", "48": "icons/icon-48.png", diff --git a/extension/sidepanel.css b/extension/sidepanel.css index 87ecf7b7..c5aea6ac 100644 --- a/extension/sidepanel.css +++ b/extension/sidepanel.css @@ -1,32 +1,54 @@ -/* gstack browse — Side Panel dark theme */ -/* Design tokens from cookie picker, extended */ +/* gstack browse — Side Panel + * Design system: DESIGN.md (Industrial/Utilitarian, amber accent, zinc neutrals) + */ * { margin: 0; padding: 0; box-sizing: border-box; } :root { - --bg-body: #0a0a0a; - --bg-header: #0f0f0f; - --bg-surface: #1a1a1a; - --bg-hover: #151515; - --border: #222; - --border-inactive: #333; - --border-hover: #555; - --text-heading: #fff; + /* Brand — amber accent, rare and meaningful */ + --amber-400: #FBBF24; + --amber-500: #F59E0B; + --amber-600: #D97706; + + /* Neutrals — cool zinc */ + --zinc-50: #FAFAFA; + --zinc-400: #A1A1AA; + --zinc-600: #52525B; + --zinc-800: #27272A; + + /* Surfaces */ + --bg-base: #0C0C0C; + --bg-surface: #141414; + --bg-hover: #1a1a1a; + --border: #262626; + --border-subtle: #1f1f1f; + + /* Text hierarchy */ + --text-heading: #FAFAFA; --text-body: #e0e0e0; - --text-label: #888; - --text-meta: #666; - --text-disabled: #555; - --green: #4ade80; - --red: #f87171; - --blue: #60a5fa; - --purple: #a78bfa; - --amber: #fbbf24; + --text-label: #A1A1AA; + --text-meta: #52525B; + --text-disabled: #3f3f46; + + /* Semantic */ + --success: #22C55E; + --warning: #F59E0B; + --error: #EF4444; + --info: #3B82F6; + + /* Typography */ --font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; - --font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + + /* Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; } body { - background: var(--bg-body); + background: var(--bg-base); color: var(--text-body); font-family: var(--font-system); font-size: 13px; @@ -36,55 +58,145 @@ body { overflow: hidden; } -/* ─── Header ──────────────────────────────────────────── */ -header { - height: 40px; - background: var(--bg-header); - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 12px; - flex-shrink: 0; +/* Grain texture overlay */ +body::after { + content: ''; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + pointer-events: none; + z-index: 9999; + opacity: 0.03; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); } -.header-left { display: flex; align-items: center; gap: 8px; } -.monogram { - width: 22px; - height: 22px; - background: var(--green); - color: #000; - font-weight: 700; - font-size: 13px; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; -} -.title { color: var(--text-heading); font-weight: 600; font-size: 14px; letter-spacing: -0.3px; } -.header-right { display: flex; align-items: center; gap: 6px; } -.header-port { color: var(--text-meta); font-family: var(--font-mono); font-size: 11px; } /* ─── Status Dot ──────────────────────────────────────── */ .dot { width: 8px; height: 8px; - border-radius: 50%; + border-radius: var(--radius-full); background: var(--text-disabled); flex-shrink: 0; + transition: background 150ms; } -.dot.connected { background: var(--green); } +.dot.connected { background: var(--success); } .dot.reconnecting { - background: var(--amber); - animation: pulse 1.5s ease-in-out infinite; + background: var(--amber-500); + animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } } +/* ─── Chat Messages ───────────────────────────────────── */ +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; +} +.chat-welcome { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; + color: var(--text-label); + gap: 8px; + padding: 24px; +} +.chat-welcome-icon { + width: 40px; + height: 40px; + background: var(--amber-500); + color: #000; + font-weight: 800; + font-size: 22px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 8px; +} +.chat-welcome .muted { color: var(--text-meta); font-size: 12px; } + +.chat-bubble { + max-width: 90%; + padding: 8px 12px; + border-radius: var(--radius-lg); + font-size: 13px; + line-height: 1.5; + word-break: break-word; + animation: slideIn 150ms ease-out; +} +.chat-bubble.user { + align-self: flex-end; + background: var(--amber-500); + color: #000; + border-bottom-right-radius: var(--radius-sm); +} +.chat-bubble.assistant { + align-self: flex-start; + background: var(--bg-surface); + color: var(--text-body); + border: 1px solid var(--border); + border-bottom-left-radius: var(--radius-sm); +} +.chat-bubble.assistant pre { + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 6px 8px; + margin: 6px 0; + overflow-x: auto; + font-family: var(--font-mono); + font-size: 12px; + white-space: pre-wrap; +} +.chat-bubble .chat-time { + font-size: 10px; + opacity: 0.5; + margin-top: 4px; + display: block; +} + +/* ─── Debug Toggle ────────────────────────────────────── */ +.debug-toggle { + background: none; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-meta); + font-family: var(--font-mono); + font-size: 10px; + padding: 2px 6px; + cursor: pointer; + transition: all 150ms; +} +.debug-toggle:hover { + color: var(--text-label); + border-color: var(--zinc-600); +} +.debug-toggle.active { + color: var(--amber-400); + border-color: var(--amber-500); +} +.debug-tabs { + border-top: 1px solid var(--border); +} +.close-debug { + width: 36px; + flex: none !important; + font-size: 16px; + color: var(--text-meta) !important; +} +.close-debug:hover { color: var(--text-label) !important; } + /* ─── Tab Bar ─────────────────────────────────────────── */ .tabs { height: 36px; - background: var(--bg-header); + background: var(--bg-surface); border-bottom: 1px solid var(--border); display: flex; flex-shrink: 0; @@ -98,12 +210,12 @@ header { font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; - transition: all 0.15s; + transition: all 150ms; } -.tab:hover:not(.disabled) { color: #ccc; } +.tab:hover:not(.disabled) { color: var(--zinc-50); } .tab.active { color: var(--text-heading); - border-bottom-color: var(--green); + border-bottom-color: var(--amber-500); } .tab.disabled { color: var(--text-disabled); @@ -125,10 +237,10 @@ header { .activity-entry { padding: 8px 12px; border-left: 3px solid var(--border); - border-bottom: 1px solid var(--border); + border-bottom: 1px solid var(--border-subtle); cursor: pointer; - transition: background 0.15s; - animation: slideIn 0.15s ease; + transition: background 150ms; + animation: slideIn 150ms ease-out; } .activity-entry:hover { background: var(--bg-hover); } @@ -142,17 +254,17 @@ header { } /* Left border colors by type */ -.activity-entry.nav { border-left-color: var(--blue); } -.activity-entry.interaction { border-left-color: var(--green); } -.activity-entry.observe { border-left-color: var(--purple); } -.activity-entry.error { border-left-color: var(--red); } +.activity-entry.nav { border-left-color: var(--info); } +.activity-entry.interaction { border-left-color: var(--success); } +.activity-entry.observe { border-left-color: var(--amber-400); } +.activity-entry.error { border-left-color: var(--error); } .activity-entry.pending { - border-left-color: var(--amber); - animation: slideIn 0.15s ease, borderPulse 1.5s ease-in-out infinite; + border-left-color: var(--amber-500); + animation: slideIn 150ms ease-out, borderPulse 2s ease-in-out infinite; } @keyframes borderPulse { - 0%, 100% { border-left-color: rgba(251, 191, 36, 0.4); } - 50% { border-left-color: rgba(251, 191, 36, 1); } + 0%, 100% { border-left-color: rgba(245, 158, 11, 0.3); } + 50% { border-left-color: rgba(245, 158, 11, 1); } } .entry-header { @@ -188,8 +300,8 @@ header { align-items: center; gap: 4px; } -.entry-status .ok { color: var(--green); } -.entry-status .err { color: var(--red); } +.entry-status .ok { color: var(--success); } +.entry-status .err { color: var(--error); } .entry-status .duration { color: var(--text-meta); } /* Expanded state */ @@ -202,7 +314,7 @@ header { .activity-entry.expanded .entry-detail { display: block; } .activity-entry.expanded .entry-args { white-space: normal; } .entry-result { - color: #aaa; + color: var(--zinc-400); font-family: var(--font-mono); font-size: 12px; white-space: pre-wrap; @@ -216,11 +328,11 @@ header { align-items: center; gap: 8px; padding: 0 12px; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid var(--border-subtle); font-size: 12px; } .ref-id { - color: var(--green); + color: var(--amber-400); font-family: var(--font-mono); font-weight: 600; min-width: 32px; @@ -271,29 +383,97 @@ header { .empty-state code { background: var(--bg-surface); padding: 2px 6px; - border-radius: 3px; + border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: 12px; } /* ─── Gap Banner ──────────────────────────────────────── */ .gap-banner { - background: rgba(251, 191, 36, 0.1); - border-bottom: 1px solid var(--amber); - color: var(--amber); + background: rgba(245, 158, 11, 0.08); + border-bottom: 1px solid var(--amber-500); + color: var(--amber-400); font-size: 11px; padding: 6px 12px; - animation: bannerSlide 0.2s ease; + animation: bannerSlide 250ms ease-out; } @keyframes bannerSlide { from { transform: translateY(-100%); } to { transform: translateY(0); } } +/* ─── Command Bar ─────────────────────────────────────── */ +.command-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: var(--bg-surface); + border-top: 1px solid var(--border); + flex-shrink: 0; +} +.command-prompt { + color: var(--amber-500); + font-family: var(--font-mono); + font-size: 14px; + font-weight: 700; + flex-shrink: 0; + user-select: none; +} +.command-input { + flex: 1; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 8px 10px; + color: var(--text-heading); + font-family: var(--font-system); + font-size: 13px; + outline: none; + transition: border-color 150ms; +} +.command-input:focus { border-color: var(--amber-500); } +.command-input::placeholder { color: var(--text-disabled); font-size: 12px; } +.command-input.sent { + border-color: var(--success); + transition: border-color 150ms; +} +.command-input.error { + border-color: var(--error); + animation: shake 300ms ease; +} +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-4px); } + 75% { transform: translateX(4px); } +} +.send-btn { + width: 32px; + height: 32px; + background: var(--amber-500); + border: none; + border-radius: var(--radius-md); + color: #000; + font-size: 16px; + font-weight: 700; + cursor: pointer; + flex-shrink: 0; + transition: all 150ms; + display: flex; + align-items: center; + justify-content: center; +} +.send-btn:hover { background: var(--amber-400); } +.send-btn:active { transform: scale(0.93); } +.send-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + /* ─── Footer ──────────────────────────────────────────── */ footer { height: 32px; - background: var(--bg-header); + background: var(--bg-surface); border-top: 1px solid var(--border); display: flex; align-items: center; @@ -307,11 +487,37 @@ footer { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - max-width: 60%; + max-width: 50%; } +.footer-right { + display: flex; + align-items: center; + gap: 6px; +} +.footer-port { + color: var(--text-meta); + font-family: var(--font-mono); + font-size: 11px; + cursor: pointer; + transition: color 150ms; +} +.footer-port:hover { color: var(--text-label); } +.port-input { + width: 56px; + padding: 2px 6px; + background: var(--bg-base); + border: 1px solid var(--zinc-600); + border-radius: var(--radius-sm); + color: var(--text-heading); + font-family: var(--font-mono); + font-size: 11px; + outline: none; + transition: border-color 150ms; +} +.port-input:focus { border-color: var(--amber-500); } /* ─── Accessibility ───────────────────────────────────── */ :focus-visible { - outline: 2px solid var(--green); + outline: 2px solid var(--amber-500); outline-offset: 1px; } diff --git a/extension/sidepanel.html b/extension/sidepanel.html index ca519ea3..16cb6483 100644 --- a/extension/sidepanel.html +++ b/extension/sidepanel.html @@ -5,27 +5,19 @@
- -Waiting for commands...
Run a browse command to see activity here.
@@ -33,7 +25,7 @@ - +No refs yet
@@ -43,20 +35,29 @@ - -Full session view requires Conductor.
-Activity tab shows browse commands.
-