/* global chrome, loadConfig, saveConfig, baseUrlFrom, ensureHostPermission, loginAndValidate, validateTokenSession, fetchCatalogCached, invalidateCatalogCache, streamTest, cancelByConversationId, extractConversationId, AGENT_MODES, toPrompt, defaultInstruction, markdownToHtml, CSAI_LIMITS, isExtensionContextError */ let config = {}; let entries = []; let runs = []; let selectedEntryId = null; let selectedRunId = null; let inspectedTabId = null; let token = ''; let tokenExpiresAt = ''; let bgPort = null; let activeAbort = null; let activeRunId = null; let validateAbort = null; let validating = false; let captureEnabled = true; let searchDebounceTimer = null; let connectRetryTimer = null; let tokenCheckTimer = null; let probeInFlight = null; let lastProbeAt = 0; const TOKEN_TICK_INTERVAL_MS = 30 * 1000; const PROBE_MIN_GAP_MS = 5000; let outputFlushScheduled = false; let pendingProgressRunId = null; let pendingFinalRunId = null; let finalRenderedLen = 0; let markdownRenderToken = 0; const $ = (id) => document.getElementById(id); function extensionAlive() { try { return !!(typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id); } catch (_) { return false; } } function stopOnContextLoss() { if (tokenCheckTimer) { clearInterval(tokenCheckTimer); tokenCheckTimer = null; } if (connectRetryTimer) { clearTimeout(connectRetryTimer); connectRetryTimer = null; } bgPort = null; showContextBanner(true); } function onContextLoss(err) { if (!isExtensionContextError(err)) return false; stopOnContextLoss(); return true; } function showContextBanner(show) { const el = $('ctx-banner'); if (el) el.classList.toggle('hidden', !show); } function scheduleConnect(delayMs) { if (connectRetryTimer) clearTimeout(connectRetryTimer); connectRetryTimer = setTimeout(connectBackground, delayMs); } function runtimeSendMessage(msg) { if (!extensionAlive()) { showContextBanner(true); return Promise.reject(new Error('Extension context lost')); } return chrome.runtime.sendMessage(msg); } function setStatus(text, kind) { const el = $('status'); el.textContent = text; el.className = 'status' + (kind ? ' status-' + kind : ''); if (kind !== 'error') updateConnSummary(); } function connectionEndpointLabel() { const scheme = $('https').checked ? 'https' : 'http'; const host = ($('host').value || '').trim() || '127.0.0.1'; const port = ($('port').value || '').trim() || '8080'; return `${scheme}://${host}:${port}`; } function setConnExpanded(expanded) { const toolbar = $('toolbar'); if (!toolbar) return; toolbar.classList.toggle('toolbar--conn-collapsed', !expanded); const btn = $('btn-conn-toggle'); if (btn) { btn.setAttribute('aria-expanded', expanded ? 'true' : 'false'); btn.textContent = expanded ? 'Collapse' : 'Connection'; } } function updateConnSummary() { const summary = $('conn-summary'); if (!summary) return; summary.textContent = connectionEndpointLabel(); summary.classList.toggle('conn-summary--ok', !!token && !isTokenExpiredByTime(tokenExpiresAt)); } function refreshAuthStatus() { if (!token) { setStatus('Not validated', ''); updateConnSummary(); return; } if (isTokenExpiredByTime(tokenExpiresAt)) { setStatus('Session expired — please Validate again', 'error'); updateConnSummary(); return; } const hint = formatTokenExpiryHint(tokenExpiresAt); const kind = tokenExpiresWithin(tokenExpiresAt, TOKEN_WARN_BEFORE_MS) ? 'warn' : 'ok'; setStatus(hint, kind); } async function clearAuthSession(message) { token = ''; tokenExpiresAt = ''; try { await saveConfig({ token: '', tokenExpiresAt: '' }); } catch (err) { if (onContextLoss(err)) return; throw err; } invalidateCatalogCache(); setStatus(message || 'Not validated', message ? 'error' : ''); setConnExpanded(true); updateConnSummary(); updateSendButtons(); } async function handleAuthFailure(err, fallbackMessage) { if (err && isAuthHttpStatus(err.status)) { await clearAuthSession(fallbackMessage || 'Token invalid or expired — please Validate again'); return true; } return false; } async function ensureAuthReady() { if (!token) { setStatus('Validate first', 'warn'); setConnExpanded(true); return false; } if (isTokenExpiredByTime(tokenExpiresAt)) { await clearAuthSession('Session expired — please Validate again'); return false; } try { await probeTokenOnServer(true); return !!token; } catch (_) { return false; } } function startTokenWatch() { if (tokenCheckTimer) clearInterval(tokenCheckTimer); tokenCheckTimer = setInterval(async () => { if (!extensionAlive()) { stopOnContextLoss(); return; } if (!token) return; if (isTokenExpiredByTime(tokenExpiresAt)) { await clearAuthSession('Session expired — please Validate again'); return; } await probeTokenOnServer(); }, TOKEN_TICK_INTERVAL_MS); } async function probeTokenOnServer(force) { if (!extensionAlive()) { stopOnContextLoss(); return; } if (!token || isTokenExpiredByTime(tokenExpiresAt)) return; const now = Date.now(); if (!force && now - lastProbeAt < PROBE_MIN_GAP_MS) return; if (probeInFlight) return probeInFlight; probeInFlight = (async () => { try { const baseUrl = baseUrlFrom(await loadConfig()); await validateTokenSession(baseUrl, token); lastProbeAt = Date.now(); refreshAuthStatus(); } catch (err) { if (onContextLoss(err)) return; lastProbeAt = Date.now(); if (await handleAuthFailure(err, 'Server restarted or token invalid — please Validate again')) { return; } if (isNetworkFetchError(err)) { setStatus('Cannot reach server — confirm CyberStrikeAI is running', 'warn'); updateConnSummary(); return; } setStatus('Validation failed: ' + err.message, 'warn'); updateConnSummary(); } finally { probeInFlight = null; } })(); return probeInFlight; } function setupAuthProbeHooks() { document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && token) { probeTokenOnServer(true).catch(() => {}); } }); window.addEventListener('focus', () => { if (token) probeTokenOnServer(true).catch(() => {}); }); } function onConnToggle() { const toolbar = $('toolbar'); const collapsed = toolbar && toolbar.classList.contains('toolbar--conn-collapsed'); setConnExpanded(!!collapsed); } function updateCaptureToggleUi() { const btn = $('btn-capture-toggle'); if (btn) { btn.classList.toggle('btn--capture-on', captureEnabled); btn.classList.toggle('btn--capture-off', !captureEnabled); btn.setAttribute('aria-pressed', captureEnabled ? 'true' : 'false'); btn.textContent = captureEnabled ? '● Capturing' : '○ Paused'; } const sidebar = $('sidebar'); if (sidebar) sidebar.classList.toggle('capture-paused', !captureEnabled); const hint = $('capture-paused-hint'); if (hint) hint.classList.toggle('hidden', captureEnabled); } async function syncCaptureEnabled() { try { await saveConfig({ captureEnabled }); } catch (err) { if (onContextLoss(err)) return; throw err; } try { await runtimeSendMessage({ type: 'set-capture-enabled', enabled: captureEnabled }); } catch (_) {} updateCaptureToggleUi(); } async function onCaptureToggle() { captureEnabled = !captureEnabled; await syncCaptureEnabled(); } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>'); } function truncateOutput(s, max) { if (!s || s.length <= max) return s || ''; return s.slice(0, max) + '\n\n… [output truncated at ' + max + ' chars]'; } function selectedEntry() { return entries.find((e) => e.id === selectedEntryId) || null; } function selectedRun() { return runs.find((r) => r.id === selectedRunId) || null; } function statusDotClass(status) { switch (status) { case 'running': return 'dot-running'; case 'done': return 'dot-done'; case 'error': return 'dot-error'; case 'cancelled': case 'cancelling': return 'dot-cancelled'; default: return ''; } } function createRun(entry, meta) { const run = { id: 'run_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), title: entry ? entry.title : 'Unknown', status: 'running', meta: meta || '', progress: '', final: '', entry: entry ? { ...entry } : null, conversationId: '', }; runs.unshift(run); if (runs.length > CSAI_LIMITS.MAX_RUNS) runs.length = CSAI_LIMITS.MAX_RUNS; selectedRunId = run.id; return run; } function buildRunLi(r) { const li = document.createElement('li'); li.dataset.id = r.id; if (r.id === selectedRunId) li.classList.add('selected'); li.innerHTML = `` + `
` + `
${escapeHtml(r.title)}
` + `
${escapeHtml(r.meta)} · ${escapeHtml(r.status)}
` + `
`; li.addEventListener('click', () => selectRun(r.id)); return li; } function renderRunList() { const ul = $('run-list'); ul.innerHTML = ''; for (const r of runs) { ul.appendChild(buildRunLi(r)); } } function prependRunItem(run) { const ul = $('run-list'); ul.insertBefore(buildRunLi(run), ul.firstChild); while (ul.children.length > runs.length) { ul.removeChild(ul.lastChild); } } function updateRunListItem(runId) { const r = runs.find((x) => x.id === runId); const ul = $('run-list'); const li = ul.querySelector(`li[data-id="${CSS.escape(runId)}"]`); if (!r || !li) { renderRunList(); return; } const selected = r.id === selectedRunId; li.className = selected ? 'selected' : ''; li.innerHTML = `` + `
` + `
${escapeHtml(r.title)}
` + `
${escapeHtml(r.meta)} · ${escapeHtml(r.status)}
` + `
`; li.onclick = () => selectRun(r.id); } function currentSearchQuery() { return ($('search').value || '').trim().toLowerCase(); } function entryMatchesSearch(e, q) { return !q || (e.url || '').toLowerCase().includes(q) || (e.title || '').toLowerCase().includes(q); } function buildRequestLi(e) { const li = document.createElement('li'); li.dataset.id = e.id; if (e.id === selectedEntryId) li.classList.add('selected'); const st = e.responseStatus || 0; const stClass = st >= 200 && st < 400 ? 'status-ok' : 'status-err'; li.innerHTML = `${escapeHtml(e.method)}` + `${st || '—'}` + `${escapeHtml(e.title || e.url)}`; li.addEventListener('click', () => selectEntry(e.id)); return li; } function renderRequestList() { const q = currentSearchQuery(); const ul = $('request-list'); ul.innerHTML = ''; const filtered = entries.filter((e) => entryMatchesSearch(e, q)); for (const e of filtered) { ul.appendChild(buildRequestLi(e)); } updateSendButtons(); } function prependRequestEntry(entry) { const q = currentSearchQuery(); if (!entryMatchesSearch(entry, q)) return; const ul = $('request-list'); ul.insertBefore(buildRequestLi(entry), ul.firstChild); while (ul.children.length > CSAI_LIMITS.MAX_CAPTURED) { ul.removeChild(ul.lastChild); } updateSendButtons(); } function updateRequestSelection() { $('request-list').querySelectorAll('li').forEach((li) => { li.classList.toggle('selected', li.dataset.id === selectedEntryId); }); } function updateSendButtons() { const ok = !!token; $('btn-send').disabled = !selectedEntryId || !ok; $('btn-latest-xhr').disabled = !ok; } function selectEntry(id) { selectedEntryId = id; const e = selectedEntry(); updateRequestSelection(); if (!e) return; $('view-request').textContent = formatRawRequest(e); $('view-response').textContent = formatRawResponse(e); } function updateRunSelection(prevId, nextId) { const ul = $('run-list'); if (prevId && prevId !== nextId) { const prevLi = ul.querySelector(`li[data-id="${CSS.escape(prevId)}"]`); if (prevLi) prevLi.classList.remove('selected'); } if (nextId) { const nextLi = ul.querySelector(`li[data-id="${CSS.escape(nextId)}"]`); if (nextLi) nextLi.classList.add('selected'); } } function trimInactiveRunFinals(activeId) { const max = CSAI_LIMITS.MAX_FINAL_ARCHIVE_CHARS; for (const r of runs) { if (r.id === activeId || r.status === 'running') continue; if (r.final && r.final.length > max) { r.final = r.final.slice(0, max) + '\n\n… [inactive run truncated at ' + max + ' chars]'; } } } function selectRun(id) { const prevId = selectedRunId; selectedRunId = id; if (prevId !== id) { updateRunSelection(prevId, id); trimInactiveRunFinals(id); } const r = selectedRun(); if (!r) return; flushOutputViewNow(); refreshFinalView(r.final, { streaming: r.status === 'running' && activeRunId === r.id }); if (r.entry) { $('view-request').textContent = formatRawRequest(r.entry); $('view-response').textContent = formatRawResponse(r.entry); } } function formatRawRequest(e) { return formatRequestDisplay(e); } function formatRawResponse(e) { return formatResponseDisplay(e); } function resetFinalDom(finalText) { const raw = $('final-raw'); raw.textContent = finalText || ''; finalRenderedLen = (finalText || '').length; } function appendFinalDom(delta) { if (!delta) return; const raw = $('final-raw'); raw.append(document.createTextNode(delta)); finalRenderedLen += delta.length; raw.scrollTop = raw.scrollHeight; } function refreshFinalView(finalText, options) { const opts = options || {}; const streaming = !!opts.streaming; const text = finalText || ''; const raw = $('final-raw'); const md = $('final-md'); const wantMd = $('render-md').checked && text.trim() && text.length <= CSAI_LIMITS.MAX_MARKDOWN_CHARS; if (streaming) { md.classList.add('hidden'); raw.classList.remove('hidden'); if (text.length < finalRenderedLen) { resetFinalDom(text); } else if (text.length > finalRenderedLen) { appendFinalDom(text.slice(finalRenderedLen)); } return; } resetFinalDom(text); markdownRenderToken += 1; const token = markdownRenderToken; if (wantMd) { const renderMd = () => { if (token !== markdownRenderToken) return; md.classList.remove('hidden'); raw.classList.add('hidden'); md.srcdoc = markdownToHtml(text); }; if (typeof requestIdleCallback === 'function') { requestIdleCallback(renderMd, { timeout: 500 }); } else { setTimeout(renderMd, 0); } return; } md.classList.add('hidden'); raw.classList.remove('hidden'); if (text.length > CSAI_LIMITS.MAX_MARKDOWN_CHARS && $('render-md').checked) { raw.textContent = text + '\n\n… [Markdown disabled: content exceeds ' + CSAI_LIMITS.MAX_MARKDOWN_CHARS + ' chars]'; } } function finalizeFinalView(finalText) { flushOutputViewNow(); refreshFinalView(finalText, { streaming: false }); } function scheduleOutputFlush() { if (outputFlushScheduled) return; outputFlushScheduled = true; requestAnimationFrame(flushOutputView); } function flushOutputView() { outputFlushScheduled = false; if (pendingProgressRunId != null) { const rid = pendingProgressRunId; pendingProgressRunId = null; if (rid === selectedRunId) { const r = runs.find((x) => x.id === rid); if (r) { const el = $('progress'); el.textContent = r.progress; el.scrollTop = el.scrollHeight; } } } if (pendingFinalRunId != null) { const rid = pendingFinalRunId; pendingFinalRunId = null; if (rid === selectedRunId) { const r = runs.find((x) => x.id === rid); if (r) { refreshFinalView(r.final, { streaming: isRunStreaming(rid) }); } } } } function flushOutputViewNow() { outputFlushScheduled = false; if (pendingProgressRunId != null || pendingFinalRunId != null) { const p = pendingProgressRunId; const f = pendingFinalRunId; pendingProgressRunId = null; pendingFinalRunId = null; if (p === selectedRunId) { const r = runs.find((x) => x.id === p); if (r) { $('progress').textContent = r.progress; $('progress').scrollTop = $('progress').scrollHeight; } } if (f === selectedRunId) { const r = runs.find((x) => x.id === f); if (r) { refreshFinalView(r.final, { streaming: isRunStreaming(f) }); } } } } function isRunStreaming(runId) { if (runId !== activeRunId) return false; const r = runs.find((x) => x.id === runId); return !!(r && r.status === 'running'); } function appendToRun(runId, field, text) { const r = runs.find((x) => x.id === runId); if (!r || !text) return; if (field === 'progress') { r.progress = truncateOutput(r.progress + text, CSAI_LIMITS.MAX_PROGRESS_CHARS); } else { r[field] = (r[field] || '') + text; } if (runId === selectedRunId) { if (field === 'progress') { pendingProgressRunId = runId; scheduleOutputFlush(); } else if (field === 'final') { pendingFinalRunId = runId; scheduleOutputFlush(); } } } function setRunStatus(runId, status) { const r = runs.find((x) => x.id === runId); if (!r) return; if (r.status === status) return; r.status = status; updateRunListItem(runId); if (runId === selectedRunId && status !== 'running') { finalizeFinalView(r ? r.final : ''); } } function setRunConversationId(runId, cid) { const r = runs.find((x) => x.id === runId); if (r && cid) r.conversationId = cid; } async function initConfig() { try { config = await loadConfig(); } catch (err) { if (onContextLoss(err)) return; throw err; } token = config.token || ''; tokenExpiresAt = config.tokenExpiresAt || ''; $('host').value = config.host; $('port').value = config.port; $('https').checked = config.https; $('filter-api').checked = config.filterApiOnly; captureEnabled = config.captureEnabled !== false; $('render-md').checked = config.renderMarkdown; $('debug-events').checked = config.showDebugEvents; if (token && isTokenExpiredByTime(tokenExpiresAt)) { await clearAuthSession('Session expired — please Validate again'); } else { refreshAuthStatus(); if (token) { startTokenWatch(); probeTokenOnServer().catch(() => {}); } } setConnExpanded(!token); updateCaptureToggleUi(); syncCaptureEnabled().catch(() => {}); updateSendButtons(); } async function persistConnection() { try { await saveConfig({ host: $('host').value.trim(), port: $('port').value.trim(), https: $('https').checked, filterApiOnly: $('filter-api').checked, renderMarkdown: $('render-md').checked, showDebugEvents: $('debug-events').checked, }); } catch (err) { onContextLoss(err); } } async function onValidate() { if (validating) { validateAbort?.abort(); validating = false; $('btn-validate').textContent = 'Validate'; setStatus('Cancelled', 'warn'); return; } validating = true; validateAbort = new AbortController(); $('btn-validate').textContent = 'Cancel'; setStatus('Validating...', 'pending'); await persistConnection(); try { config = await loadConfig(); } catch (err) { if (onContextLoss(err)) return; throw err; } const baseUrl = baseUrlFrom(config); const password = $('password').value; try { await ensureHostPermission(baseUrl); const auth = await loginAndValidate(baseUrl, password, validateAbort.signal); token = auth.token; tokenExpiresAt = auth.expiresAt || ''; await saveConfig({ token, tokenExpiresAt }); invalidateCatalogCache(); refreshAuthStatus(); startTokenWatch(); setConnExpanded(false); updateSendButtons(); } catch (err) { if (onContextLoss(err)) return; if (err.name === 'AbortError') { setStatus('Cancelled', 'warn'); } else { token = ''; tokenExpiresAt = ''; try { await saveConfig({ token: '', tokenExpiresAt: '' }); } catch (saveErr) { onContextLoss(saveErr); } setStatus('Failed: ' + err.message, 'error'); setConnExpanded(true); } updateSendButtons(); } finally { validating = false; validateAbort = null; $('btn-validate').textContent = 'Validate'; } } function fillAgentSelect(sel, selected) { sel.innerHTML = ''; for (const m of AGENT_MODES) { const opt = document.createElement('option'); opt.value = m.id; opt.textContent = m.label; sel.appendChild(opt); } sel.value = selected || 'eino_single'; } function fillSelect(sel, items, value) { sel.innerHTML = ''; for (const item of items) { const opt = document.createElement('option'); opt.value = item.id; opt.textContent = item.label; sel.appendChild(opt); } const has = items.some((i) => i.id === value); sel.value = has ? value : (items[0] && items[0].id) || ''; } async function openSendDialog(entryOverride) { const e = entryOverride || selectedEntry(); if (!e) return; if (!(await ensureAuthReady())) return; const dlg = $('send-dialog'); config = await loadConfig(); const baseUrl = baseUrlFrom(config); $('dlg-instruction').value = config.lastInstruction || defaultInstruction(); fillAgentSelect($('dlg-agent'), config.lastAgentMode); $('dlg-project').innerHTML = ''; $('dlg-role').innerHTML = ''; dlg.showModal(); try { const { projects, roles } = await fetchCatalogCached(baseUrl, token); fillSelect($('dlg-project'), projects, config.lastProjectId); fillSelect($('dlg-role'), roles, config.lastRole); } catch (err) { if (await handleAuthFailure(err, 'Token invalid or expired — please Validate again')) { dlg.close(); return; } fillSelect($('dlg-project'), [{ id: '', label: '(load failed)' }], ''); fillSelect($('dlg-role'), [{ id: '', label: 'Default' }], ''); } dlg.dataset.entryId = e.id; } async function onSendConfirmed() { const entryId = $('send-dialog').dataset.entryId; const e = entries.find((x) => x.id === entryId) || selectedEntry(); if (!e || !(await ensureAuthReady())) return; const instruction = $('dlg-instruction').value.trim() || defaultInstruction(); const projectId = $('dlg-project').value || ''; const role = $('dlg-role').value || ''; const agentMode = $('dlg-agent').value || 'eino_single'; try { await saveConfig({ lastInstruction: instruction, lastProjectId: projectId, lastRole: role, lastAgentMode: agentMode, }); } catch (err) { if (onContextLoss(err)) return; throw err; } const modeLabel = (AGENT_MODES.find((m) => m.id === agentMode) || {}).label || agentMode; const roleLabel = role || 'Default'; const run = createRun(e, modeLabel + ' · ' + roleLabel); activeRunId = run.id; prependRunItem(run); finalRenderedLen = 0; markdownRenderToken += 1; selectRun(run.id); const baseUrl = baseUrlFrom(await loadConfig()); const prompt = toPrompt(e, instruction); appendToRun(run.id, 'progress', `[*] ${e.title}\n[server] ${baseUrl}\n[mode] ${modeLabel}\n`); if (projectId) appendToRun(run.id, 'progress', `[project] ${projectId}\n`); appendToRun(run.id, 'progress', '\n'); try { await streamTest( baseUrl, token, { message: prompt, role, projectId, agentMode }, { setAbortController(ctrl) { activeAbort = ctrl; }, onEvent(type, message, ev) { handleStreamEvent(run.id, type, message, ev); }, onDone() { appendToRun(run.id, 'progress', '\n[done]\n'); setRunStatus(run.id, 'done'); activeAbort = null; activeRunId = null; }, } ); } catch (err) { if (err.name === 'AbortError') { appendToRun(run.id, 'progress', '\n[info] Local stream stopped.\n'); setRunStatus(run.id, 'cancelled'); } else { await handleAuthFailure(err, 'Token invalid or expired — please Validate again'); appendToRun(run.id, 'progress', '\n[error] ' + err.message + '\n'); setRunStatus(run.id, 'error'); } activeAbort = null; activeRunId = null; } } function handleStreamEvent(runId, type, message, ev) { const debug = $('debug-events').checked; switch (type) { case 'response_start': appendToRun(runId, 'progress', '\n\n[Main reply]\n'); break; case 'response_delta': if (message) appendToRun(runId, 'final', message); break; case 'response': if (message) appendToRun(runId, 'final', message); break; case 'eino_agent_reply_stream_start': appendToRun(runId, 'progress', '\n\n[Sub-agent reply]\n'); break; case 'eino_agent_reply_stream_delta': if (message) appendToRun(runId, 'progress', message); break; case 'progress': appendToRun(runId, 'progress', '\n[progress] ' + message + '\n'); setRunStatus(runId, 'running'); break; case 'error': appendToRun(runId, 'progress', '\n[error] ' + message + '\n'); setRunStatus(runId, 'error'); break; case 'cancelled': appendToRun(runId, 'progress', '\n[cancelled] ' + message + '\n'); setRunStatus(runId, 'cancelled'); break; case 'conversation': case 'message_saved': { const cid = extractConversationId(ev); if (cid) setRunConversationId(runId, cid); if (type === 'conversation' && debug && message) { appendToRun(runId, 'progress', '\n[conversation] ' + message + '\n'); } break; } case 'tool_call': case 'tool_result': case 'tool_result_delta': if (debug && message) appendToRun(runId, 'progress', '\n[' + type + '] ' + message + '\n'); break; default: if (debug && message && !type.endsWith('_stream_delta') && !type.endsWith('_stream_start')) { appendToRun(runId, 'progress', '\n[' + type + '] ' + message + '\n'); } break; } } async function onStop() { const runId = activeRunId || selectedRunId; const run = runs.find((x) => x.id === runId); if (!run) return; if (activeAbort) { activeAbort.abort(); activeAbort = null; } setRunStatus(run.id, 'cancelling'); appendToRun(run.id, 'progress', '\n[info] Stop requested (local stream + server cancel).\n'); const cid = run.conversationId; if (!cid || !token) { if (!cid) { appendToRun(run.id, 'progress', '[info] conversationId not ready yet; only local stream was stopped.\n'); } setRunStatus(run.id, 'cancelled'); activeRunId = null; return; } const baseUrl = baseUrlFrom(await loadConfig()); try { await cancelByConversationId(baseUrl, token, cid); appendToRun(run.id, 'progress', '[info] Server cancel acknowledged.\n'); setRunStatus(run.id, 'cancelled'); } catch (err) { appendToRun(run.id, 'progress', '[error] Server cancel failed: ' + err.message + '\n'); setRunStatus(run.id, 'cancelled'); } activeRunId = null; } function clearCurrentRunOutput() { pendingProgressRunId = null; pendingFinalRunId = null; outputFlushScheduled = false; finalRenderedLen = 0; markdownRenderToken += 1; const r = selectedRun(); if (r) { r.progress = ''; r.final = ''; $('progress').textContent = ''; refreshFinalView(''); } else { $('progress').textContent = ''; refreshFinalView(''); } } function clearAllRuns() { runs = []; selectedRunId = null; pendingProgressRunId = null; pendingFinalRunId = null; outputFlushScheduled = false; finalRenderedLen = 0; markdownRenderToken += 1; renderRunList(); $('progress').textContent = ''; refreshFinalView(''); } function connectBackground() { connectRetryTimer = null; if (!extensionAlive()) { showContextBanner(true); bgPort = null; return; } showContextBanner(false); try { if (!chrome.devtools || !chrome.devtools.inspectedWindow) { scheduleConnect(1000); return; } inspectedTabId = chrome.devtools.inspectedWindow.tabId; bgPort = chrome.runtime.connect({ name: 'cyberstrike-panel' }); } catch (_) { if (!extensionAlive()) { showContextBanner(true); bgPort = null; return; } scheduleConnect(1000); return; } bgPort.postMessage({ type: 'subscribe', tabId: inspectedTabId, filterApiOnly: $('filter-api').checked, }); bgPort.onMessage.addListener((msg) => { if (msg.type === 'list') { entries = msg.entries || []; renderRequestList(); } else if (msg.type === 'entry') { entries.unshift(msg.entry); if (entries.length > CSAI_LIMITS.MAX_CAPTURED) entries.length = CSAI_LIMITS.MAX_CAPTURED; prependRequestEntry(msg.entry); } }); bgPort.onDisconnect.addListener(() => { bgPort = null; if (!extensionAlive()) { showContextBanner(true); return; } scheduleConnect(1000); }); } async function onFilterApiChange() { await persistConnection(); if (bgPort && inspectedTabId != null) { bgPort.postMessage({ type: 'subscribe', tabId: inspectedTabId, filterApiOnly: $('filter-api').checked, }); runtimeSendMessage({ type: 'set-filter-api', tabId: inspectedTabId, filterApiOnly: $('filter-api').checked, }).catch(() => {}); } } async function clearCaptures() { if (inspectedTabId == null) return; await runtimeSendMessage({ type: 'clear-captures', tabId: inspectedTabId }); entries = []; selectedEntryId = null; renderRequestList(); } async function sendLatestXhr() { if (inspectedTabId == null) return; if (!(await ensureAuthReady())) return; const resp = await runtimeSendMessage({ type: 'get-latest-api', tabId: inspectedTabId, }); if (resp && resp.entry) { selectedEntryId = resp.entry.id; selectEntry(resp.entry.id); openSendDialog(resp.entry); } } function onSearchInput() { clearTimeout(searchDebounceTimer); searchDebounceTimer = setTimeout(renderRequestList, 150); } function onCopyOutput() { const activeTab = document.querySelector('.tab.active'); const tabName = activeTab ? activeTab.dataset.tab : 'output'; let text = ''; if (tabName === 'request') text = $('view-request').textContent; else if (tabName === 'response') text = $('view-response').textContent; else { const r = selectedRun(); text = (r && r.final) ? r.final : $('final-raw').textContent; } navigator.clipboard.writeText(text || '').catch(() => {}); } function setupTabs() { document.querySelectorAll('.tab').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('.tab').forEach((b) => b.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach((p) => p.classList.add('hidden')); btn.classList.add('active'); $('tab-' + btn.dataset.tab).classList.remove('hidden'); }); }); } $('btn-validate').addEventListener('click', onValidate); $('btn-conn-toggle').addEventListener('click', onConnToggle); $('host').addEventListener('input', updateConnSummary); $('port').addEventListener('input', updateConnSummary); $('https').addEventListener('change', updateConnSummary); $('btn-send').addEventListener('click', () => openSendDialog()); $('btn-latest-xhr').addEventListener('click', sendLatestXhr); $('btn-stop').addEventListener('click', onStop); $('btn-copy').addEventListener('click', onCopyOutput); $('btn-clear').addEventListener('click', clearCurrentRunOutput); $('btn-clear-runs').addEventListener('click', clearAllRuns); $('btn-clear-captures').addEventListener('click', clearCaptures); $('search').addEventListener('input', onSearchInput); $('btn-capture-toggle').addEventListener('click', onCaptureToggle); $('filter-api').addEventListener('change', onFilterApiChange); $('render-md').addEventListener('change', () => { const r = selectedRun(); const text = r ? r.final : $('final-raw').textContent; const streaming = r && r.status === 'running' && activeRunId === r.id; refreshFinalView(text, { streaming }); persistConnection(); }); $('debug-events').addEventListener('change', persistConnection); $('send-form').addEventListener('submit', (ev) => { ev.preventDefault(); $('send-dialog').close(); onSendConfirmed(); }); $('dlg-cancel').addEventListener('click', () => $('send-dialog').close()); setupTabs(); setupAuthProbeHooks(); initConfig().then(() => { if (extensionAlive()) connectBackground(); else showContextBanner(true); });