mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-06 05:35:46 +02:00
feat: self-healing sidebar — reconnect banner, state machine, copy button
Sidebar UI now handles disconnection gracefully: - Connection state machine: connected → reconnecting → dead - Amber pulsing banner during reconnect (2s retry, 30 attempts) - Red "Server offline" banner with Reconnect + Copy /connect-chrome buttons - Green "Reconnected" toast that fades after 3s on successful reconnect - Copy button lets user paste /connect-chrome into any Claude Code session Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,68 @@
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
/* ─── Connection Banner ─────────────────────────────────────────── */
|
||||
|
||||
.conn-banner {
|
||||
padding: 6px 10px;
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.conn-banner.reconnecting {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-bottom: 1px solid rgba(245, 158, 11, 0.2);
|
||||
color: var(--amber-400);
|
||||
}
|
||||
|
||||
.conn-banner.dead {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.conn-banner.reconnected {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-bottom: 1px solid rgba(34, 197, 94, 0.2);
|
||||
color: var(--success);
|
||||
animation: fadeOut 3s ease forwards;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to { opacity: 0; height: 0; padding: 0; overflow: hidden; }
|
||||
}
|
||||
|
||||
.conn-banner-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.conn-btn {
|
||||
font-size: 9px;
|
||||
font-family: var(--font-mono);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-label);
|
||||
transition: all 150ms;
|
||||
}
|
||||
|
||||
.conn-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
.conn-copy {
|
||||
color: var(--text-meta);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-base);
|
||||
color: var(--text-body);
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
<link rel="stylesheet" href="sidepanel.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Connection status banner -->
|
||||
<div class="conn-banner" id="conn-banner" style="display:none">
|
||||
<span class="conn-banner-text" id="conn-banner-text">Reconnecting...</span>
|
||||
<div class="conn-banner-actions" id="conn-banner-actions" style="display:none">
|
||||
<button class="conn-btn" id="conn-reconnect">Reconnect</button>
|
||||
<button class="conn-btn conn-copy" id="conn-copy" title="Copy command">/connect-chrome</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Tab (default, full height) -->
|
||||
<main id="tab-chat" class="tab-content active">
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
|
||||
+77
-2
@@ -16,6 +16,10 @@ let serverUrl = null;
|
||||
let serverToken = null;
|
||||
let chatLineCount = 0;
|
||||
let chatPollInterval = null;
|
||||
let connState = 'disconnected'; // disconnected | connected | reconnecting | dead
|
||||
let reconnectAttempts = 0;
|
||||
let reconnectTimer = null;
|
||||
const MAX_RECONNECT_ATTEMPTS = 30; // 30 * 2s = 60s before showing "dead"
|
||||
|
||||
// Auth headers for sidebar endpoints
|
||||
function authHeaders() {
|
||||
@@ -24,6 +28,58 @@ function authHeaders() {
|
||||
return h;
|
||||
}
|
||||
|
||||
// ─── Connection State Machine ─────────────────────────────────────
|
||||
|
||||
function setConnState(state) {
|
||||
const prev = connState;
|
||||
connState = state;
|
||||
const banner = document.getElementById('conn-banner');
|
||||
const bannerText = document.getElementById('conn-banner-text');
|
||||
const bannerActions = document.getElementById('conn-banner-actions');
|
||||
|
||||
if (state === 'connected') {
|
||||
if (prev === 'reconnecting' || prev === 'dead') {
|
||||
// Show "reconnected" toast that fades
|
||||
banner.style.display = '';
|
||||
banner.className = 'conn-banner reconnected';
|
||||
bannerText.textContent = 'Reconnected';
|
||||
bannerActions.style.display = 'none';
|
||||
setTimeout(() => { banner.style.display = 'none'; }, 5000);
|
||||
} else {
|
||||
banner.style.display = 'none';
|
||||
}
|
||||
reconnectAttempts = 0;
|
||||
if (reconnectTimer) { clearInterval(reconnectTimer); reconnectTimer = null; }
|
||||
} else if (state === 'reconnecting') {
|
||||
banner.style.display = '';
|
||||
banner.className = 'conn-banner reconnecting';
|
||||
bannerText.textContent = `Reconnecting... (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`;
|
||||
bannerActions.style.display = 'none';
|
||||
} else if (state === 'dead') {
|
||||
banner.style.display = '';
|
||||
banner.className = 'conn-banner dead';
|
||||
bannerText.textContent = 'Server offline';
|
||||
bannerActions.style.display = '';
|
||||
if (reconnectTimer) { clearInterval(reconnectTimer); reconnectTimer = null; }
|
||||
} else {
|
||||
banner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function startReconnect() {
|
||||
if (reconnectTimer) return;
|
||||
setConnState('reconnecting');
|
||||
reconnectTimer = setInterval(() => {
|
||||
reconnectAttempts++;
|
||||
if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
|
||||
setConnState('dead');
|
||||
return;
|
||||
}
|
||||
setConnState('reconnecting');
|
||||
tryConnect();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// ─── Chat ───────────────────────────────────────────────────────
|
||||
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
@@ -451,21 +507,25 @@ async function fetchRefs() {
|
||||
// ─── Server Discovery ───────────────────────────────────────────
|
||||
|
||||
function updateConnection(url, token) {
|
||||
const wasConnected = !!serverUrl;
|
||||
serverUrl = url;
|
||||
serverToken = token || null;
|
||||
if (url) {
|
||||
document.getElementById('footer-dot').className = 'dot connected';
|
||||
const port = new URL(url).port;
|
||||
document.getElementById('footer-port').textContent = `:${port}`;
|
||||
setConnState('connected');
|
||||
connectSSE();
|
||||
// Start chat polling
|
||||
if (chatPollInterval) clearInterval(chatPollInterval);
|
||||
chatPollInterval = setInterval(pollChat, 1000);
|
||||
pollChat(); // immediate first poll
|
||||
pollChat();
|
||||
} else {
|
||||
document.getElementById('footer-dot').className = 'dot';
|
||||
document.getElementById('footer-port').textContent = '';
|
||||
if (chatPollInterval) { clearInterval(chatPollInterval); chatPollInterval = null; }
|
||||
if (wasConnected) {
|
||||
startReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +558,21 @@ portInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') { portInput.style.display = 'none'; portLabel.style.display = ''; }
|
||||
});
|
||||
|
||||
// ─── Reconnect / Copy Buttons ────────────────────────────────────
|
||||
|
||||
document.getElementById('conn-reconnect').addEventListener('click', () => {
|
||||
reconnectAttempts = 0;
|
||||
startReconnect();
|
||||
});
|
||||
|
||||
document.getElementById('conn-copy').addEventListener('click', () => {
|
||||
navigator.clipboard.writeText('/connect-chrome').then(() => {
|
||||
const btn = document.getElementById('conn-copy');
|
||||
btn.textContent = 'copied!';
|
||||
setTimeout(() => { btn.textContent = '/connect-chrome'; }, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
// Try to connect immediately, retry every 2s until connected
|
||||
function tryConnect() {
|
||||
chrome.runtime.sendMessage({ type: 'getPort' }, (resp) => {
|
||||
|
||||
Reference in New Issue
Block a user