Files
CyberStrikeAI/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.js
T
2026-07-08 14:19:04 +08:00

1147 lines
32 KiB
JavaScript

/* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
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 =
`<span class="dot ${statusDotClass(r.status)}"></span>` +
`<div class="run-body">` +
`<div class="run-title">${escapeHtml(r.title)}</div>` +
`<div class="meta">${escapeHtml(r.meta)} · ${escapeHtml(r.status)}</div>` +
`</div>`;
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 =
`<span class="dot ${statusDotClass(r.status)}"></span>` +
`<div class="run-body">` +
`<div class="run-title">${escapeHtml(r.title)}</div>` +
`<div class="meta">${escapeHtml(r.meta)} · ${escapeHtml(r.status)}</div>` +
`</div>`;
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 =
`<span class="method">${escapeHtml(e.method)}</span>` +
`<span class="${stClass}">${st || '—'}</span>` +
`<span class="req-path">${escapeHtml(e.title || e.url)}</span>`;
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 = '<option>Loading…</option>';
$('dlg-role').innerHTML = '<option>Loading…</option>';
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);
});