mirror of https://github.com/garrytan/gstack.git
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:
parent
7e8684f923
commit
530a4ef22c
|
|
@ -47,6 +47,68 @@
|
||||||
--radius-full: 9999px;
|
--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 {
|
body {
|
||||||
background: var(--bg-base);
|
background: var(--bg-base);
|
||||||
color: var(--text-body);
|
color: var(--text-body);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,15 @@
|
||||||
<link rel="stylesheet" href="sidepanel.css">
|
<link rel="stylesheet" href="sidepanel.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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) -->
|
<!-- Chat Tab (default, full height) -->
|
||||||
<main id="tab-chat" class="tab-content active">
|
<main id="tab-chat" class="tab-content active">
|
||||||
<div class="chat-messages" id="chat-messages">
|
<div class="chat-messages" id="chat-messages">
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ let serverUrl = null;
|
||||||
let serverToken = null;
|
let serverToken = null;
|
||||||
let chatLineCount = 0;
|
let chatLineCount = 0;
|
||||||
let chatPollInterval = null;
|
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
|
// Auth headers for sidebar endpoints
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
|
|
@ -24,6 +28,58 @@ function authHeaders() {
|
||||||
return h;
|
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 ───────────────────────────────────────────────────────
|
// ─── Chat ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const chatMessages = document.getElementById('chat-messages');
|
const chatMessages = document.getElementById('chat-messages');
|
||||||
|
|
@ -451,21 +507,25 @@ async function fetchRefs() {
|
||||||
// ─── Server Discovery ───────────────────────────────────────────
|
// ─── Server Discovery ───────────────────────────────────────────
|
||||||
|
|
||||||
function updateConnection(url, token) {
|
function updateConnection(url, token) {
|
||||||
|
const wasConnected = !!serverUrl;
|
||||||
serverUrl = url;
|
serverUrl = url;
|
||||||
serverToken = token || null;
|
serverToken = token || null;
|
||||||
if (url) {
|
if (url) {
|
||||||
document.getElementById('footer-dot').className = 'dot connected';
|
document.getElementById('footer-dot').className = 'dot connected';
|
||||||
const port = new URL(url).port;
|
const port = new URL(url).port;
|
||||||
document.getElementById('footer-port').textContent = `:${port}`;
|
document.getElementById('footer-port').textContent = `:${port}`;
|
||||||
|
setConnState('connected');
|
||||||
connectSSE();
|
connectSSE();
|
||||||
// Start chat polling
|
|
||||||
if (chatPollInterval) clearInterval(chatPollInterval);
|
if (chatPollInterval) clearInterval(chatPollInterval);
|
||||||
chatPollInterval = setInterval(pollChat, 1000);
|
chatPollInterval = setInterval(pollChat, 1000);
|
||||||
pollChat(); // immediate first poll
|
pollChat();
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('footer-dot').className = 'dot';
|
document.getElementById('footer-dot').className = 'dot';
|
||||||
document.getElementById('footer-port').textContent = '';
|
document.getElementById('footer-port').textContent = '';
|
||||||
if (chatPollInterval) { clearInterval(chatPollInterval); chatPollInterval = null; }
|
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 = ''; }
|
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
|
// Try to connect immediately, retry every 2s until connected
|
||||||
function tryConnect() {
|
function tryConnect() {
|
||||||
chrome.runtime.sendMessage({ type: 'getPort' }, (resp) => {
|
chrome.runtime.sendMessage({ type: 'getPort' }, (resp) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue