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 = `
${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 ``;
}
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 ``;
}
return '';
}).join('');
} else {
if (cleanContent) {
bodyHtml = renderContent(cleanContent).map((p) => {
if (p.type === 'code') {
return ``;
}
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 ``;
}).join('') + bodyHtml;
}
}
if (!bodyHtml) bodyHtml = '';
el.innerHTML = `
${avatar}
`;
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 += ``;
}
});
if (!html) {
html = '';
}
currentStreamingEl.innerHTML = `
◆
${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);