mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
feat: sidebar per-tab chat context, tab bar sync, stop button, UX polish
Extension changes:
- sidepanel.js: per-tab chat history (tabChatHistories map), switchChatTab()
swaps entire chat view, browserTabActivated handler for instant tab sync,
stop button wired to /sidebar-agent/stop, pollTabs renders tab bar
- sidepanel.html: updated banner text ("Browser co-pilot"), stop button markup,
input placeholder "Ask about this page..."
- sidepanel.css: tab bar styles, stop button styles, loading state fixes
- background.js: chrome.tabs.onActivated sends browserTabActivated to sidepanel
with tab URL for instant tab switch detection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -378,6 +378,22 @@ chrome.runtime.onInstalled.addListener(async () => {
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// ─── Tab Switch Detection ────────────────────────────────────────
|
||||
// Notify sidepanel instantly when the user switches tabs in the browser.
|
||||
// This is faster than polling — the sidebar swaps chat context immediately.
|
||||
|
||||
chrome.tabs.onActivated.addListener((activeInfo) => {
|
||||
chrome.tabs.get(activeInfo.tabId, (tab) => {
|
||||
if (chrome.runtime.lastError || !tab) return;
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'browserTabActivated',
|
||||
tabId: activeInfo.tabId,
|
||||
url: tab.url || '',
|
||||
title: tab.title || '',
|
||||
}).catch(() => {}); // sidepanel may not be open
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Startup ────────────────────────────────────────────────────
|
||||
|
||||
// Load auth token BEFORE first health poll (token no longer in /health response)
|
||||
|
||||
+83
-17
@@ -262,16 +262,27 @@ body::after {
|
||||
}
|
||||
.agent-tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
background: var(--bg-base);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono);
|
||||
overflow: hidden;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(245, 158, 11, 0.06);
|
||||
border-left: 2px solid var(--amber-500);
|
||||
border-radius: 0 4px 4px 0;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-system);
|
||||
margin: 2px 0;
|
||||
}
|
||||
.tool-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tool-description {
|
||||
color: var(--text-body);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
/* Legacy classes kept for compat */
|
||||
.tool-name {
|
||||
color: var(--amber-500);
|
||||
font-weight: 600;
|
||||
@@ -285,9 +296,10 @@ body::after {
|
||||
}
|
||||
.agent-text {
|
||||
color: var(--text-body);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.agent-text pre {
|
||||
background: var(--bg-base);
|
||||
@@ -637,6 +649,22 @@ body::after {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.stop-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: var(--error);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
}
|
||||
.stop-btn:hover { background: #dc2626; }
|
||||
.stop-btn:active { transform: scale(0.93); }
|
||||
|
||||
/* ─── Footer ──────────────────────────────────────────── */
|
||||
footer {
|
||||
@@ -686,17 +714,55 @@ footer {
|
||||
|
||||
/* ─── Experimental Banner ─────────────────────────────── */
|
||||
.experimental-banner {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
color: #F59E0B;
|
||||
padding: 8px 12px;
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: 1px solid rgba(59, 130, 246, 0.15);
|
||||
color: var(--zinc-400);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
margin: 8px 12px;
|
||||
font-size: 11px;
|
||||
margin: 6px 12px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ─── Browser Tab Bar ─────────────────────────────────── */
|
||||
.browser-tabs {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-base);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
flex-shrink: 0;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.browser-tabs::-webkit-scrollbar { display: none; }
|
||||
.browser-tab {
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-system);
|
||||
color: var(--text-meta);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
transition: background 100ms, color 100ms;
|
||||
}
|
||||
.browser-tab:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-label);
|
||||
}
|
||||
.browser-tab.active {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-body);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
/* ─── Inspector Tab ──────────────────────────────────── */
|
||||
|
||||
.inspector-toolbar {
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Browser tab bar -->
|
||||
<div class="browser-tabs" id="browser-tabs" style="display:none"></div>
|
||||
|
||||
<!-- Chat Tab (default, full height) -->
|
||||
<main id="tab-chat" class="tab-content active">
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
@@ -127,12 +130,13 @@
|
||||
|
||||
<!-- Experimental chat banner (shown when chatEnabled) -->
|
||||
<div id="experimental-banner" class="experimental-banner" style="display: none;">
|
||||
⚠ Standalone mode — this is a separate agent from your workspace
|
||||
Browser co-pilot — controls this browser, reports back to your workspace
|
||||
</div>
|
||||
|
||||
<!-- Command Bar -->
|
||||
<div class="command-bar">
|
||||
<input type="text" class="command-input" id="command-input" placeholder="Message Claude Code..." autocomplete="off" spellcheck="false">
|
||||
<button class="stop-btn" id="stop-agent-btn" title="Stop agent" style="display: none;">■</button>
|
||||
<input type="text" class="command-input" id="command-input" placeholder="Ask about this page..." autocomplete="off" spellcheck="false">
|
||||
<button class="send-btn" id="send-btn" title="Send">↑</button>
|
||||
</div>
|
||||
|
||||
|
||||
+266
-6
@@ -17,6 +17,10 @@ let serverToken = null;
|
||||
let chatLineCount = 0;
|
||||
let chatPollInterval = null;
|
||||
let connState = 'disconnected'; // disconnected | connected | reconnecting | dead
|
||||
let lastOptimisticMsg = null; // track optimistically rendered user msg to avoid dupes
|
||||
let sidebarActiveTabId = null; // which browser tab's chat we're showing
|
||||
const chatLineCountByTab = {}; // tabId -> last seen chatLineCount
|
||||
const chatDomByTab = {}; // tabId -> saved innerHTML
|
||||
let reconnectAttempts = 0;
|
||||
let reconnectTimer = null;
|
||||
const MAX_RECONNECT_ATTEMPTS = 30; // 30 * 2s = 60s before showing "dead"
|
||||
@@ -103,8 +107,12 @@ function addChatEntry(entry) {
|
||||
const welcome = chatMessages.querySelector('.chat-welcome');
|
||||
if (welcome) welcome.remove();
|
||||
|
||||
// User messages → chat bubble
|
||||
// User messages → chat bubble (skip if we already rendered it optimistically)
|
||||
if (entry.role === 'user') {
|
||||
if (lastOptimisticMsg === entry.message) {
|
||||
lastOptimisticMsg = null; // consumed — don't skip next identical msg
|
||||
return;
|
||||
}
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'chat-bubble user';
|
||||
bubble.innerHTML = `${escapeHtml(entry.message)}<span class="chat-time">${formatChatTime(entry.ts)}</span>`;
|
||||
@@ -136,6 +144,13 @@ function addChatEntry(entry) {
|
||||
|
||||
function handleAgentEvent(entry) {
|
||||
if (entry.type === 'agent_start') {
|
||||
// If we already showed thinking dots optimistically in sendMessage(),
|
||||
// don't duplicate. Just ensure fast polling is on.
|
||||
if (agentContainer && document.getElementById('agent-thinking')) {
|
||||
startFastPoll();
|
||||
updateStopButton(true);
|
||||
return;
|
||||
}
|
||||
// Create a new agent response container
|
||||
agentText = '';
|
||||
agentContainer = document.createElement('div');
|
||||
@@ -150,6 +165,8 @@ function handleAgentEvent(entry) {
|
||||
thinking.innerHTML = '<span class="thinking-dot"></span><span class="thinking-dot"></span><span class="thinking-dot"></span>';
|
||||
agentContainer.appendChild(thinking);
|
||||
agentContainer.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
startFastPoll();
|
||||
updateStopButton(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,6 +174,8 @@ function handleAgentEvent(entry) {
|
||||
// Remove thinking indicator
|
||||
const thinking = document.getElementById('agent-thinking');
|
||||
if (thinking) thinking.remove();
|
||||
updateStopButton(false);
|
||||
stopFastPoll();
|
||||
// Add timestamp
|
||||
if (agentContainer) {
|
||||
const ts = document.createElement('span');
|
||||
@@ -172,6 +191,8 @@ function handleAgentEvent(entry) {
|
||||
if (entry.type === 'agent_error') {
|
||||
const thinking = document.getElementById('agent-thinking');
|
||||
if (thinking) thinking.remove();
|
||||
updateStopButton(false);
|
||||
stopFastPoll();
|
||||
if (!agentContainer) {
|
||||
agentContainer = document.createElement('div');
|
||||
agentContainer.className = 'agent-response';
|
||||
@@ -200,7 +221,11 @@ function handleAgentEvent(entry) {
|
||||
toolEl.className = 'agent-tool';
|
||||
const toolName = entry.tool || 'Tool';
|
||||
const toolInput = entry.input || '';
|
||||
toolEl.innerHTML = `<span class="tool-name">${escapeHtml(toolName)}</span> <span class="tool-input">${escapeHtml(toolInput)}</span>`;
|
||||
|
||||
// Use the verbose description as the primary text
|
||||
// The tool name becomes a subtle badge
|
||||
const toolIcon = toolName === 'Bash' ? '▸' : toolName === 'Read' ? '📄' : toolName === 'Grep' ? '🔍' : toolName === 'Glob' ? '📁' : '⚡';
|
||||
toolEl.innerHTML = `<span class="tool-icon">${toolIcon}</span> <span class="tool-description">${escapeHtml(toolInput)}</span>`;
|
||||
agentContainer.appendChild(toolEl);
|
||||
agentContainer.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
return;
|
||||
@@ -251,8 +276,34 @@ async function sendMessage() {
|
||||
commandInput.disabled = true;
|
||||
sendBtn.disabled = true;
|
||||
|
||||
// Show user bubble + thinking dots IMMEDIATELY — don't wait for poll.
|
||||
// This eliminates up to 1000ms of perceived latency.
|
||||
lastOptimisticMsg = msg;
|
||||
const welcome = chatMessages.querySelector('.chat-welcome');
|
||||
if (welcome) welcome.remove();
|
||||
const userBubble = document.createElement('div');
|
||||
userBubble.className = 'chat-bubble user';
|
||||
userBubble.innerHTML = `${escapeHtml(msg)}<span class="chat-time">${formatChatTime(new Date().toISOString())}</span>`;
|
||||
chatMessages.appendChild(userBubble);
|
||||
|
||||
agentText = '';
|
||||
agentContainer = document.createElement('div');
|
||||
agentContainer.className = 'agent-response';
|
||||
agentTextEl = null;
|
||||
chatMessages.appendChild(agentContainer);
|
||||
const thinking = document.createElement('div');
|
||||
thinking.className = 'agent-thinking';
|
||||
thinking.id = 'agent-thinking';
|
||||
thinking.innerHTML = '<span class="thinking-dot"></span><span class="thinking-dot"></span><span class="thinking-dot"></span>';
|
||||
agentContainer.appendChild(thinking);
|
||||
agentContainer.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
updateStopButton(true);
|
||||
|
||||
// Speed up polling while agent is working
|
||||
startFastPoll();
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage({ type: 'sidebar-command', message: msg }, resolve);
|
||||
chrome.runtime.sendMessage({ type: 'sidebar-command', message: msg, tabId: sidebarActiveTabId }, resolve);
|
||||
});
|
||||
|
||||
commandInput.disabled = false;
|
||||
@@ -260,7 +311,7 @@ async function sendMessage() {
|
||||
commandInput.focus();
|
||||
|
||||
if (result?.ok) {
|
||||
// Immediately poll to show the user's own message
|
||||
// Poll immediately to sync server state
|
||||
pollChat();
|
||||
} else {
|
||||
commandInput.classList.add('error');
|
||||
@@ -286,6 +337,7 @@ commandInput.addEventListener('keydown', (e) => {
|
||||
});
|
||||
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
document.getElementById('stop-agent-btn').addEventListener('click', stopAgent);
|
||||
|
||||
// Poll for new chat messages
|
||||
let initialLoadDone = false;
|
||||
@@ -293,16 +345,25 @@ let initialLoadDone = false;
|
||||
async function pollChat() {
|
||||
if (!serverUrl || !serverToken) return;
|
||||
try {
|
||||
const resp = await fetch(`${serverUrl}/sidebar-chat?after=${chatLineCount}`, {
|
||||
// Request chat for the currently displayed tab
|
||||
const tabParam = sidebarActiveTabId !== null ? `&tabId=${sidebarActiveTabId}` : '';
|
||||
const resp = await fetch(`${serverUrl}/sidebar-chat?after=${chatLineCount}${tabParam}`, {
|
||||
headers: authHeaders(),
|
||||
signal: AbortSignal.timeout(3000),
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
|
||||
// Detect tab switch from server — swap chat context
|
||||
if (data.activeTabId !== undefined && data.activeTabId !== sidebarActiveTabId) {
|
||||
switchChatTab(data.activeTabId);
|
||||
return; // switchChatTab triggers a fresh poll
|
||||
}
|
||||
|
||||
// First successful poll — hide loading spinner
|
||||
if (!initialLoadDone) {
|
||||
initialLoadDone = true;
|
||||
sidebarActiveTabId = data.activeTabId ?? null;
|
||||
const loading = document.getElementById('chat-loading');
|
||||
const welcome = document.getElementById('chat-welcome');
|
||||
if (loading) loading.style.display = 'none';
|
||||
@@ -319,6 +380,181 @@ async function pollChat() {
|
||||
}
|
||||
chatLineCount = data.total;
|
||||
}
|
||||
|
||||
// Clean up orphaned thinking indicators after replay.
|
||||
const thinking = document.getElementById('agent-thinking');
|
||||
if (thinking && data.agentStatus !== 'processing') {
|
||||
thinking.remove();
|
||||
if (agentContainer) {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'agent-text';
|
||||
notice.style.color = 'var(--text-meta)';
|
||||
notice.style.fontStyle = 'italic';
|
||||
notice.textContent = '(session ended)';
|
||||
agentContainer.appendChild(notice);
|
||||
agentContainer = null;
|
||||
agentTextEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide stop button based on agent status
|
||||
updateStopButton(data.agentStatus === 'processing');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** Switch the sidebar to show a different tab's chat context */
|
||||
function switchChatTab(newTabId) {
|
||||
if (newTabId === sidebarActiveTabId) return;
|
||||
|
||||
// Save current tab's chat DOM + scroll position
|
||||
if (sidebarActiveTabId !== null) {
|
||||
chatDomByTab[sidebarActiveTabId] = chatMessages.innerHTML;
|
||||
chatLineCountByTab[sidebarActiveTabId] = chatLineCount;
|
||||
}
|
||||
|
||||
sidebarActiveTabId = newTabId;
|
||||
|
||||
// Restore saved chat for new tab, or show welcome
|
||||
if (chatDomByTab[newTabId]) {
|
||||
chatMessages.innerHTML = chatDomByTab[newTabId];
|
||||
chatLineCount = chatLineCountByTab[newTabId] || 0;
|
||||
} else {
|
||||
chatMessages.innerHTML = `
|
||||
<div class="chat-welcome" id="chat-welcome">
|
||||
<div class="chat-welcome-icon">G</div>
|
||||
<p>Send a message about this page.</p>
|
||||
<p class="muted">Each tab has its own conversation.</p>
|
||||
</div>`;
|
||||
chatLineCount = 0;
|
||||
}
|
||||
|
||||
// Reset agent state for this tab
|
||||
agentContainer = null;
|
||||
agentTextEl = null;
|
||||
agentText = '';
|
||||
|
||||
// Immediately poll the new tab's chat
|
||||
pollChat();
|
||||
}
|
||||
|
||||
function updateStopButton(agentRunning) {
|
||||
const stopBtn = document.getElementById('stop-agent-btn');
|
||||
if (!stopBtn) return;
|
||||
stopBtn.style.display = agentRunning ? '' : 'none';
|
||||
}
|
||||
|
||||
async function stopAgent() {
|
||||
if (!serverUrl) return;
|
||||
try {
|
||||
await fetch(`${serverUrl}/sidebar-agent/stop`, { method: 'POST', headers: authHeaders() });
|
||||
} catch {}
|
||||
// Immediately clean up UI
|
||||
const thinking = document.getElementById('agent-thinking');
|
||||
if (thinking) thinking.remove();
|
||||
if (agentContainer) {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'agent-text';
|
||||
notice.style.color = 'var(--text-meta)';
|
||||
notice.style.fontStyle = 'italic';
|
||||
notice.textContent = 'Stopped';
|
||||
agentContainer.appendChild(notice);
|
||||
agentContainer = null;
|
||||
agentTextEl = null;
|
||||
}
|
||||
updateStopButton(false);
|
||||
stopFastPoll();
|
||||
}
|
||||
|
||||
// ─── Adaptive poll speed ─────────────────────────────────────────
|
||||
// 300ms while agent is working (fast first-token), 1000ms when idle.
|
||||
const FAST_POLL_MS = 300;
|
||||
const SLOW_POLL_MS = 1000;
|
||||
|
||||
function startFastPoll() {
|
||||
if (chatPollInterval) clearInterval(chatPollInterval);
|
||||
chatPollInterval = setInterval(pollChat, FAST_POLL_MS);
|
||||
}
|
||||
|
||||
function stopFastPoll() {
|
||||
if (chatPollInterval) clearInterval(chatPollInterval);
|
||||
chatPollInterval = setInterval(pollChat, SLOW_POLL_MS);
|
||||
}
|
||||
|
||||
// ─── Browser Tab Bar ─────────────────────────────────────────────
|
||||
let tabPollInterval = null;
|
||||
let lastTabJson = '';
|
||||
|
||||
async function pollTabs() {
|
||||
if (!serverUrl || !serverToken) return;
|
||||
try {
|
||||
// Tell the server which Chrome tab the user is actually looking at.
|
||||
// This syncs manual tab switches in the browser → server activeTabId.
|
||||
let activeTabUrl = null;
|
||||
try {
|
||||
const chromeTabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
activeTabUrl = chromeTabs?.[0]?.url || null;
|
||||
} catch {}
|
||||
|
||||
const resp = await fetch(`${serverUrl}/sidebar-tabs${activeTabUrl ? '?activeUrl=' + encodeURIComponent(activeTabUrl) : ''}`, {
|
||||
headers: authHeaders(),
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
if (!data.tabs) return;
|
||||
|
||||
// Only re-render if tabs changed
|
||||
const json = JSON.stringify(data.tabs);
|
||||
if (json === lastTabJson) return;
|
||||
lastTabJson = json;
|
||||
|
||||
renderTabBar(data.tabs);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function renderTabBar(tabs) {
|
||||
const bar = document.getElementById('browser-tabs');
|
||||
if (!bar) return;
|
||||
|
||||
if (!tabs || tabs.length <= 1) {
|
||||
bar.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
bar.style.display = '';
|
||||
bar.innerHTML = '';
|
||||
|
||||
for (const tab of tabs) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'browser-tab' + (tab.active ? ' active' : '');
|
||||
el.title = tab.url || '';
|
||||
|
||||
// Show favicon-style domain + title
|
||||
let label = tab.title || '';
|
||||
if (!label && tab.url) {
|
||||
try { label = new URL(tab.url).hostname; } catch { label = tab.url; }
|
||||
}
|
||||
if (label.length > 20) label = label.slice(0, 20) + '…';
|
||||
|
||||
el.textContent = label || `Tab ${tab.id}`;
|
||||
el.dataset.tabId = tab.id;
|
||||
|
||||
el.addEventListener('click', () => switchBrowserTab(tab.id));
|
||||
bar.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
async function switchBrowserTab(tabId) {
|
||||
if (!serverUrl) return;
|
||||
try {
|
||||
await fetch(`${serverUrl}/sidebar-tabs/switch`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ id: tabId }),
|
||||
});
|
||||
// Switch chat context + re-poll tabs
|
||||
switchChatTab(tabId);
|
||||
pollTabs();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -960,12 +1196,17 @@ function updateConnection(url, token) {
|
||||
connectSSE();
|
||||
connectInspectorSSE();
|
||||
if (chatPollInterval) clearInterval(chatPollInterval);
|
||||
chatPollInterval = setInterval(pollChat, 1000);
|
||||
chatPollInterval = setInterval(pollChat, SLOW_POLL_MS);
|
||||
pollChat();
|
||||
// Poll browser tabs every 2s (lightweight, just tab list)
|
||||
if (tabPollInterval) clearInterval(tabPollInterval);
|
||||
tabPollInterval = setInterval(pollTabs, 2000);
|
||||
pollTabs();
|
||||
} else {
|
||||
document.getElementById('footer-dot').className = 'dot';
|
||||
document.getElementById('footer-port').textContent = '';
|
||||
if (chatPollInterval) { clearInterval(chatPollInterval); chatPollInterval = null; }
|
||||
if (tabPollInterval) { clearInterval(tabPollInterval); tabPollInterval = null; }
|
||||
if (wasConnected) {
|
||||
startReconnect();
|
||||
}
|
||||
@@ -1060,6 +1301,25 @@ chrome.runtime.onMessage.addListener((msg) => {
|
||||
inspectorPickerActive = false;
|
||||
inspectorPickBtn.classList.remove('active');
|
||||
}
|
||||
// Instant tab switch — background.js fires this on chrome.tabs.onActivated
|
||||
if (msg.type === 'browserTabActivated') {
|
||||
// Tell the server which tab is now active, then switch chat context
|
||||
if (serverUrl && serverToken) {
|
||||
fetch(`${serverUrl}/sidebar-tabs?activeUrl=${encodeURIComponent(msg.url || '')}`, {
|
||||
headers: authHeaders(),
|
||||
signal: AbortSignal.timeout(2000),
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.tabs) {
|
||||
renderTabBar(data.tabs);
|
||||
// Find the server-side tab ID for this Chrome tab
|
||||
const activeTab = data.tabs.find(t => t.active);
|
||||
if (activeTab && activeTab.id !== sidebarActiveTabId) {
|
||||
switchChatTab(activeTab.id);
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Chat Gate ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user