From f62377748b493fd016ac72a8304178bfb4a015a8 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 21 Mar 2026 12:17:38 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20connection=20status=20pill=20=E2=80=94?= =?UTF-8?q?=20floating=20indicator=20when=20gstack=20controls=20Chrome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Small pill in bottom-right corner of every page: "● gstack · 3 refs" Shows when connected via CDP, fades to 30% opacity after 3s, full on hover. Disappears entirely when disconnected. Background worker now notifies content scripts on connect/disconnect state changes so the pill appears/disappears without polling. Co-Authored-By: Claude Opus 4.6 (1M context) --- extension/background.js | 41 ++++++++++++++++++++++++--------- extension/content.css | 47 ++++++++++++++++++++++++++++++++++++- extension/content.js | 51 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 13 deletions(-) diff --git a/extension/background.js b/extension/background.js index 33c70c38..6b5b08b7 100644 --- a/extension/background.js +++ b/extension/background.js @@ -51,23 +51,42 @@ async function checkHealth() { } 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: ' ' }); - } + const wasDisconnected = !isConnected; + isConnected = true; + chrome.action.setBadgeBackgroundColor({ color: '#4ade80' }); + chrome.action.setBadgeText({ text: ' ' }); + // Broadcast health to popup and side panel chrome.runtime.sendMessage({ type: 'health', data: healthData }).catch(() => {}); + + // Notify content scripts on connection change + if (wasDisconnected) { + notifyContentScripts('connected'); + } } function setDisconnected() { - if (isConnected) { - isConnected = false; - chrome.action.setBadgeText({ text: '' }); - } + const wasConnected = isConnected; + isConnected = false; + chrome.action.setBadgeText({ text: '' }); + chrome.runtime.sendMessage({ type: 'health', data: null }).catch(() => {}); + + // Notify content scripts on disconnection + if (wasConnected) { + notifyContentScripts('disconnected'); + } +} + +async function notifyContentScripts(type) { + try { + const tabs = await chrome.tabs.query({}); + for (const tab of tabs) { + if (tab.id) { + chrome.tabs.sendMessage(tab.id, { type }).catch(() => {}); + } + } + } catch {} } // ─── Refs Relay ───────────────────────────────────────────────── diff --git a/extension/content.css b/extension/content.css index 880990e8..a5d3dd22 100644 --- a/extension/content.css +++ b/extension/content.css @@ -1,9 +1,54 @@ -/* gstack browse — ref overlay styles */ +/* gstack browse — ref overlay + status pill styles */ #gstack-ref-overlays { font-family: 'SF Mono', 'Fira Code', monospace !important; } +/* Connection status pill — bottom-right corner */ +#gstack-status-pill { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 2147483646; + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: rgba(10, 10, 10, 0.85); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid rgba(74, 222, 128, 0.3); + border-radius: 20px; + color: #e0e0e0; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + font-size: 11px; + font-weight: 500; + letter-spacing: 0.02em; + pointer-events: none; + transition: opacity 0.5s ease; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4); +} + +#gstack-status-pill:hover { + opacity: 1 !important; + pointer-events: auto; +} + +.gstack-pill-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #4ade80; + box-shadow: 0 0 6px rgba(74, 222, 128, 0.5); + flex-shrink: 0; +} + +@media (prefers-reduced-motion: reduce) { + #gstack-status-pill { + transition: none; + } +} + .gstack-ref-badge { position: absolute; background: rgba(220, 38, 38, 0.9); diff --git a/extension/content.js b/extension/content.js index 11f2563f..2ceb75bf 100644 --- a/extension/content.js +++ b/extension/content.js @@ -7,6 +7,43 @@ */ let overlayContainer = null; +let statusPill = null; +let pillFadeTimer = null; +let refCount = 0; + +// ─── Connection Status Pill ────────────────────────────────── + +function showStatusPill(connected, refs) { + refCount = refs || 0; + + if (!statusPill) { + statusPill = document.createElement('div'); + statusPill.id = 'gstack-status-pill'; + document.body.appendChild(statusPill); + } + + if (!connected) { + statusPill.style.display = 'none'; + return; + } + + const refText = refCount > 0 ? ` · ${refCount} refs` : ''; + statusPill.innerHTML = ` gstack${refText}`; + statusPill.style.display = 'flex'; + statusPill.style.opacity = '1'; + + // Fade to subtle after 3s + clearTimeout(pillFadeTimer); + pillFadeTimer = setTimeout(() => { + statusPill.style.opacity = '0.3'; + }, 3000); +} + +function hideStatusPill() { + if (statusPill) { + statusPill.style.display = 'none'; + } +} function ensureContainer() { if (overlayContainer) return overlayContainer; @@ -74,7 +111,7 @@ function renderRefPanel(refs) { container.appendChild(panel); } -// Listen for ref data from background worker +// Listen for messages from background worker chrome.runtime.onMessage.addListener((msg) => { if (msg.type === 'refs' && msg.data) { const refs = msg.data.refs || []; @@ -82,15 +119,27 @@ chrome.runtime.onMessage.addListener((msg) => { if (refs.length === 0) { clearOverlays(); + showStatusPill(true, 0); return; } // CDP mode: could use bounding boxes (future) // For now: floating panel for all modes renderRefPanel(refs); + showStatusPill(true, refs.length); } if (msg.type === 'clearRefs') { clearOverlays(); + showStatusPill(true, 0); + } + + if (msg.type === 'connected') { + showStatusPill(true, refCount); + } + + if (msg.type === 'disconnected') { + hideStatusPill(); + clearOverlays(); } });