import '../../styles/panel.css'; import { getServerUrl, setServerUrl, fetchSessions, checkServerHealth } from '../../lib/config'; import { getChatHistory, sendChat, clearChat } from '../../lib/api'; const $ = (s) => document.querySelector(s); const $$ = (s) => document.querySelectorAll(s); const $serverStatus = $('#server-status'); const $sessionCount = $('#session-count'); const $errorCount = $('#error-count'); const $sessionsList = $('#sessions-list'); const $btnDashboard = $('#btn-dashboard'); const $serverUrl = $('#server-url'); const $btnSaveUrl = $('#btn-save-url'); const $chatOffline = $('#chat-offline'); const $chatArea = $('#chat-area'); const $chatFeed = $('#chat-feed'); const $chatStreaming = $('#chat-streaming'); const $chatInput = $('#chat-input'); const $chatSend = $('#chat-send'); const $chatStop = $('#chat-stop'); let serverOnline = false; let messages = []; let loading = false; let abortController = null; let currentStreamingEl = null; function dot(color) { return ``; } function renderSessions(sessions) { if (sessions.length === 0) { $sessionsList.innerHTML = ''; return; } $sessionsList.innerHTML = `
Connected tabs
${sessions.map((s) => `
${s.title || s.url || s.id} ${s.id.slice(0, 8)}
`).join('')}
`; } function formatText(text) { let html = text .replace(/&/g, '&').replace(//g, '>'); html = html .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^# (.+)$/gm, '

$1

') .replace(/^\s*[-*] (.+)$/gm, '
• $1
') .replace(/^\s*(\d+)[.)] (.+)$/gm, '
$1 $2
') .replace(/\n/g, '
'); html = html .replace(/\s*/g, '
') .replace(/\s*(
${escapeHtml(msg.content)}
`; return el; } const isUser = msg.role === 'user'; const avatar = isUser ? '★' : '◆'; const label = isUser ? 'CDT' : 'GEN'; let displayContent = msg.content; let parsedToolCalls = null; let parsedSegments = null; try { const parsed = JSON.parse(msg.content); if (parsed && Array.isArray(parsed.segments)) { parsedSegments = parsed.segments; displayContent = parsed.content || ''; } else if (parsed && Array.isArray(parsed.tool_calls)) { parsedToolCalls = parsed.tool_calls; displayContent = parsed.content || ''; } } catch {} const cleanContent = displayContent.replace(/]*>[\s\S]*?<\/think>/gi, ''); let bodyHtml = ''; if (parsedSegments) { bodyHtml = parsedSegments.map((seg) => { if (seg.type === 'text' && seg.content) { const c = seg.content.replace(/]*>[\s\S]*?<\/think>/gi, ''); if (!c) return ''; return renderContent(c).map((p) => { if (p.type === 'code') { return `
${p.lang || ''}
${escapeHtml(p.content)}
`; } return `${formatText(p.content)}`; }).join(''); } if (seg.type === 'tool') { const name = seg.call?.name || 'tool'; const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', list_files: '📁', search_files: '🔍', web_fetch: '🌐' }[name] || '🔧'; const done = seg.result; const isErr = done && done.is_error; const preview = (() => { try { const args = typeof seg.call.args === 'string' ? JSON.parse(seg.call.args) : seg.call.args; return args.command || args.task || args.path || args.url || JSON.stringify(args).slice(0, 60); } catch { return ''; } })(); const resultText = done ? (done.content || '').slice(0, 500) : ''; return `
${icon}${name}${done ? `${isErr ? '✗' : '✓'}` : ''}
${preview ? `
${escapeHtml(preview)}
` : ''}${resultText ? `
${escapeHtml(resultText)}
` : ''}
`; } return ''; }).join(''); } else { if (cleanContent) { bodyHtml = renderContent(cleanContent).map((p) => { if (p.type === 'code') { return `
${p.lang || ''}
${escapeHtml(p.content)}
`; } return `${formatText(p.content)}`; }).join(''); } if (parsedToolCalls && parsedToolCalls.length > 0) { bodyHtml = parsedToolCalls.map((tc) => { const name = tc.name || 'tool'; const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', web_fetch: '🌐' }[name] || '🔧'; return `
${icon}${name}
`; }).join('') + bodyHtml; } } if (!bodyHtml) bodyHtml = ''; el.innerHTML = `
${avatar}
${label}
${bodyHtml}
`; return el; } function renderMessages() { $chatFeed.innerHTML = ''; messages.forEach((msg) => { $chatFeed.appendChild(createMessageEl(msg)); }); scrollToBottom(); } function scrollToBottom() { requestAnimationFrame(() => { $chatFeed.scrollTop = $chatFeed.scrollHeight; }); } function switchTab(tabName) { $$('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tabName)); $$('.tab-content').forEach((s) => s.classList.toggle('active', s.id === `tab-${tabName}`)); } function updateChatVisibility() { if (serverOnline) { $chatOffline.style.display = 'none'; $chatArea.style.display = 'flex'; } else { $chatOffline.style.display = 'flex'; $chatArea.style.display = 'none'; } } async function loadChatHistory() { try { const data = await getChatHistory(); if (data.messages && data.messages.length > 0) { messages = data.messages; } else { messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }]; } renderMessages(); } catch { messages = [{ id: 'welcome', role: 'system', content: 'Ready. Type a message to start.' }]; renderMessages(); } } async function handleSend() { const text = $chatInput.value.trim(); if (!text || loading) return; if (text === '/clear') { try { await clearChat(); } catch {} messages = [{ id: 'clear-' + Date.now(), role: 'system', content: 'Conversation cleared.' }]; renderMessages(); $chatInput.value = ''; return; } $chatInput.value = ''; $chatInput.style.height = 'auto'; const userMsg = { id: Date.now().toString(), role: 'user', content: text }; messages.push(userMsg); $chatFeed.appendChild(createMessageEl(userMsg)); scrollToBottom(); loading = true; $chatSend.style.display = 'none'; $chatStop.style.display = 'flex'; const controller = new AbortController(); abortController = controller; let segments = []; let thinking = ''; let textStartIdx = 0; let streamText = ''; const updateLastText = (text) => { if (!text) return; const last = segments.length > 0 ? segments[segments.length - 1] : null; if (last && last.type === 'text') { last.content = text; } else { segments.push({ type: 'text', content: text }); } }; currentStreamingEl = document.createElement('div'); currentStreamingEl.className = 'chat-msg assistant streaming'; $chatFeed.appendChild(currentStreamingEl); scrollToBottom(); try { const finalContent = await sendChat(text, true, (partial, event) => { if (event && (event.thinking !== undefined || event.thinking_start || event.thinking_end)) { if (event.thinking !== undefined) thinking += event.thinking; return; } if (event && event.tool_call) { updateLastText(partial.slice(textStartIdx)); textStartIdx = partial.length; segments.push({ type: 'tool', call: event.tool_call, result: null }); } else if (event && event.tool_result) { const segIdx = segments.findIndex((s) => s.type === 'tool' && s.call && s.call.tool_call_id === event.tool_result.tool_call_id); if (segIdx >= 0) segments[segIdx].result = event.tool_result; } else { updateLastText(partial.slice(textStartIdx)); } streamText = partial; const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join(''); const toolSegs = segments.filter((s) => s.type === 'tool'); let html = ''; if (thinking) { html += `
Thinking…
`; } segments.forEach((seg) => { if (seg.type === 'text' && seg.content) { const c = seg.content.replace(/]*>[\s\S]*?<\/think>/gi, ''); if (c) html += `
${formatText(c)}
`; } if (seg.type === 'tool') { const name = seg.call?.name || 'tool'; const icon = { terminal: '⌨', crush_run: '⚡', read_file: '📄', web_fetch: '🌐' }[name] || '🔧'; const done = seg.result; const isErr = done && done.is_error; const preview = (() => { try { const args = typeof seg.call.args === 'string' ? JSON.parse(seg.call.args) : seg.call.args; return args.command || args.task || args.path || JSON.stringify(args).slice(0, 60); } catch { return ''; } })(); html += `
${icon}${name}${done ? `${isErr ? '✗' : '✓'}` : ''}
${preview ? `
${escapeHtml(preview)}
` : ''}
`; } }); if (!html) { html = ''; } currentStreamingEl.innerHTML = `
GEN
${html}
`; scrollToBottom(); }, controller.signal); if (currentStreamingEl && currentStreamingEl.parentNode) { currentStreamingEl.remove(); } const allText = segments.filter((s) => s.type === 'text').map((s) => s.content).join(''); const toolSegs = segments.filter((s) => s.type === 'tool'); const aiMsg = { id: (Date.now() + 1).toString(), role: 'assistant', content: toolSegs.length > 0 ? JSON.stringify({ segments: segments.map((s) => s.type === 'text' ? { type: 'text', content: s.content } : { type: 'tool', call: s.call, result: s.result ? { content: s.result.content || '', is_error: s.result.is_error || false, tool_call_id: s.call?.tool_call_id } : null }), content: allText, }) : (allText || finalContent), }; messages.push(aiMsg); $chatFeed.appendChild(createMessageEl(aiMsg)); scrollToBottom(); } catch (err) { if (currentStreamingEl && currentStreamingEl.parentNode) { currentStreamingEl.remove(); } if (err.name !== 'AbortError') { const errMsg = { id: (Date.now() + 1).toString(), role: 'system', content: `Error: ${err.message}` }; messages.push(errMsg); $chatFeed.appendChild(createMessageEl(errMsg)); scrollToBottom(); } } finally { loading = false; abortController = null; currentStreamingEl = null; $chatSend.style.display = 'flex'; $chatStop.style.display = 'none'; } } $$('.tab').forEach((tab) => { tab.addEventListener('click', () => switchTab(tab.dataset.tab)); }); $chatInput.addEventListener('input', () => { $chatInput.style.height = 'auto'; $chatInput.style.height = Math.min($chatInput.scrollHeight, 100) + 'px'; }); $chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }); $chatSend.addEventListener('click', handleSend); $chatStop.addEventListener('click', () => { if (abortController) abortController.abort(); }); $chatFeed.addEventListener('click', (e) => { const btn = e.target.closest('.chat-copy-btn'); if (btn) { navigator.clipboard.writeText(decodeURIComponent(btn.dataset.code)); const orig = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = orig; }, 1200); } }); $btnSaveUrl.addEventListener('click', async () => { const url = $serverUrl.value.trim().replace(/\/$/, ''); if (url) { await setServerUrl(url); refresh(); } }); async function refresh() { const url = await getServerUrl(); $serverUrl.value = url; $btnDashboard.href = url; try { const sessions = await fetchSessions(); serverOnline = true; $serverStatus.innerHTML = `${dot('green')} Online`; $sessionCount.textContent = sessions.length; renderSessions(sessions); } catch { serverOnline = false; $serverStatus.innerHTML = `${dot('red')} Offline`; $sessionCount.textContent = '—'; $sessionsList.innerHTML = ''; } updateChatVisibility(); chrome.runtime.sendMessage({ type: 'get_state' }, (state) => { if (chrome.runtime.lastError || !state) return; $errorCount.textContent = state.errorCount || 0; }); } refresh(); loadChatHistory(); setInterval(refresh, 10000);