Files
ollama-sidebar/background.js
T
2026-05-25 01:50:39 -04:00

172 lines
6.4 KiB
JavaScript

// ── Side panel ───────────────────────────────────────────────────────────────
if (chrome.sidePanel) {
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
}
chrome.action.onClicked.addListener((tab) => {
if (chrome.sidePanel && tab.id) chrome.sidePanel.open({ tabId: tab.id }).catch(() => {});
});
// ── Active stream registry (for cancellation) ────────────────────────────────
const activeStreams = new Map(); // requestId → AbortController
// ── Message router ────────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'OLLAMA_GET') {
handleGet(message, sendResponse);
return true;
}
if (message.action === 'OLLAMA_FETCH') {
handleStream(message);
sendResponse({ started: true });
return false;
}
if (message.action === 'CANCEL_STREAM') {
const ctrl = activeStreams.get(message.requestId);
if (ctrl) { ctrl.abort(); activeStreams.delete(message.requestId); }
sendResponse({ ok: true });
return false;
}
if (message.action === 'GET_PAGE_CONTENT') {
handleGetPageContent(message.tabId, sendResponse);
return true;
}
});
// ── Helpers ───────────────────────────────────────────────────────────────────
function broadcast(msg) {
chrome.runtime.sendMessage(msg).catch(() => {});
}
// ── Page content relay ────────────────────────────────────────────────────────
function handleGetPageContent(tabId, sendResponse) {
chrome.tabs.sendMessage(tabId, { type: 'GET_PAGE_CONTENT' }, (response) => {
if (chrome.runtime.lastError) {
sendResponse({ error: chrome.runtime.lastError.message });
return;
}
sendResponse(response || { error: 'Content script did not respond' });
});
}
// ── Non-streaming GET ─────────────────────────────────────────────────────────
async function handleGet(message, sendResponse) {
try {
const res = await fetch(message.url, { method: 'GET', headers: message.headers || {} });
if (!res.ok) { sendResponse({ ok: false, status: res.status, error: 'HTTP ' + res.status }); return; }
const data = await res.json();
sendResponse({ ok: true, data });
} catch (e) {
sendResponse({ ok: false, error: e.message });
}
}
// ── Streaming POST ────────────────────────────────────────────────────────────
// Requests go through the background service worker so they carry no page
// Origin header — avoiding 403s from reverse proxies that block extension origins.
async function handleStream(message) {
const { url, headers, body, requestId } = message;
const controller = new AbortController();
activeStreams.set(requestId, controller);
try {
const res = await fetch(url, {
method: 'POST',
headers: headers || {},
body,
signal: controller.signal
});
if (!res.ok) {
let errBody = '';
try { errBody = await res.text(); } catch (_) {}
broadcast({ action: 'STREAM_ERROR', requestId,
error: 'HTTP ' + res.status + (errBody ? ': ' + errBody.slice(0, 200) : '') });
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
let chunk;
try { chunk = JSON.parse(trimmed); } catch (_) { continue; }
if (chunk.error) {
broadcast({ action: 'STREAM_ERROR', requestId, error: chunk.error });
return;
}
if (chunk.message) {
if (chunk.message.thinking) {
broadcast({ action: 'STREAM_THINKING', requestId, text: chunk.message.thinking });
}
if (chunk.message.content) {
broadcast({ action: 'STREAM_CHUNK', requestId, text: chunk.message.content });
}
}
if (chunk.done) {
if (chunk.eval_count || chunk.total_duration) {
broadcast({
action: 'STREAM_METRICS', requestId,
metrics: {
eval_count: chunk.eval_count || 0,
eval_duration: chunk.eval_duration || 0,
prompt_eval_count: chunk.prompt_eval_count || 0,
total_duration: chunk.total_duration || 0
}
});
}
broadcast({ action: 'STREAM_DONE', requestId });
return;
}
}
}
// Drain remaining buffer
if (buf.trim()) {
try {
const last = JSON.parse(buf.trim());
if (last.message && last.message.content) {
broadcast({ action: 'STREAM_CHUNK', requestId, text: last.message.content });
}
if (last.error) { broadcast({ action: 'STREAM_ERROR', requestId, error: last.error }); return; }
if (last.done && (last.eval_count || last.total_duration)) {
broadcast({ action: 'STREAM_METRICS', requestId,
metrics: { eval_count: last.eval_count || 0, eval_duration: last.eval_duration || 0,
prompt_eval_count: last.prompt_eval_count || 0, total_duration: last.total_duration || 0 } });
}
} catch (_) {}
}
broadcast({ action: 'STREAM_DONE', requestId });
} catch (e) {
if (e.name === 'AbortError') {
broadcast({ action: 'STREAM_DONE', requestId, aborted: true });
} else {
broadcast({ action: 'STREAM_ERROR', requestId, error: e.message });
}
} finally {
activeStreams.delete(requestId);
}
}