// ../shared/src/background/index.ts
var STORAGE_KEYS = {
connectedTabId: "yetibrowser:connectedTabId",
wsPort: "yetibrowser:wsPort",
wsPortMode: "yetibrowser:wsPortMode"
};
var DEFAULT_WS_PORT = 9010;
var DEFAULT_PORT_MODE = "auto";
var FALLBACK_WS_PORTS = [
9010,
9011,
9012,
9013,
9014,
9015,
9016,
9017,
9018,
9019,
9020
];
globalThis.addEventListener(
"error",
(event) => {
const message = typeof event.message === "string" ? event.message : "";
if (message.includes("Error in connection establishment: net::ERR_CONNECTION_REFUSED")) {
event.preventDefault();
if ("stopImmediatePropagation" in event && typeof event.stopImmediatePropagation === "function") {
event.stopImmediatePropagation();
}
}
},
{ capture: true }
);
globalThis.onerror = (message) => {
if (typeof message === "string" && message.includes("Error in connection establishment: net::ERR_CONNECTION_REFUSED")) {
return true;
}
return false;
};
var originalConsoleError = console.error.bind(console);
console.error = (...args) => {
const first = args[0];
if (typeof first === "string" && first.includes("WebSocket connection to 'ws://localhost:") && first.includes("Error in connection establishment: net::ERR_CONNECTION_REFUSED")) {
return;
}
originalConsoleError(...args);
};
var CONNECT_ATTEMPT_TIMEOUT_MS = 1e3;
var AUTO_SCAN_FAST_DELAY_MS = 100;
var AUTO_SCAN_SLOW_DELAY_MS = 750;
var AUTO_RECOVERY_DELAY_MS = 250;
var MANUAL_RETRY_BASE_DELAY_MS = 250;
var MANUAL_RETRY_MAX_DELAY_MS = 3e3;
var FAILURE_RESET_WINDOW_MS = 1e4;
var connectedTabId = null;
var wsPort = DEFAULT_WS_PORT;
var portMode = DEFAULT_PORT_MODE;
var socket = null;
var socketStatus = "disconnected";
var keepAliveTimer = null;
function setSocketStatus(status) {
if (socketStatus !== status) {
socketStatus = status;
void updateBadge();
}
}
var WebSocketConnectionManager = class {
reconnectTimer = null;
attemptTimer = null;
attemptId = 0;
expectingClose = false;
running = false;
nextAutoIndex = 0;
pendingInitialAutoPort = null;
lastSuccessfulPort = null;
preferLastSuccessful = false;
manualPort = DEFAULT_WS_PORT;
failurePort = null;
failureCount = 0;
lastFailureAt = 0;
scanIterations = 0;
activeSocket = null;
initialize(mode, port) {
if (mode === "manual") {
this.manualPort = isValidPort(port) ? port : DEFAULT_WS_PORT;
this.pendingInitialAutoPort = null;
} else {
this.manualPort = DEFAULT_WS_PORT;
this.resetAutoSequence(port);
}
this.resetFailureCounters();
this.lastSuccessfulPort = mode === "auto" && isAutoPort(port) ? port : null;
this.preferLastSuccessful = false;
}
start() {
if (this.running) {
return;
}
this.running = true;
setSocketStatus("connecting");
this.scheduleReconnect(0, { reason: "startup", preferLastSuccessful: portMode === "auto" });
}
setManualMode(port, options = {}) {
this.manualPort = port;
this.resetFailureCounters();
this.preferLastSuccessful = false;
this.pendingInitialAutoPort = null;
this.lastSuccessfulPort = null;
if (!options.fromStorage) {
chrome.storage.local.set({
[STORAGE_KEYS.wsPort]: port,
[STORAGE_KEYS.wsPortMode]: "manual"
});
}
this.triggerReconnect({ immediate: true, reason: "manual" });
}
setAutoMode(startPort, options = {}) {
const target = isAutoPort(startPort) ? startPort : DEFAULT_WS_PORT;
this.resetAutoSequence(target);
this.resetFailureCounters();
this.preferLastSuccessful = false;
if (!options.fromStorage) {
chrome.storage.local.set({
[STORAGE_KEYS.wsPort]: target,
[STORAGE_KEYS.wsPortMode]: "auto"
});
}
this.triggerReconnect({ immediate: true, reason: "auto", resetAuto: true });
}
updateManualPortFromStorage(port, options) {
this.manualPort = port;
this.resetFailureCounters();
if (options.scheduleReconnect) {
this.triggerReconnect({ immediate: true, reason: "storage-manual" });
}
}
updateAutoPortFromStorage(port, options) {
if (!isAutoPort(port)) {
return;
}
this.resetAutoSequence(port);
this.resetFailureCounters();
if (options.scheduleReconnect) {
this.triggerReconnect({ immediate: true, reason: "storage-auto", resetAuto: true });
}
}
triggerReconnect(options = {}) {
if (!this.running) {
this.start();
}
if (options.resetAuto && portMode === "auto") {
this.resetAutoSequence(wsPort);
}
if (options.preferLastSuccessful !== void 0) {
this.preferLastSuccessful = options.preferLastSuccessful;
}
setSocketStatus("connecting");
this.teardownActiveSocket();
const delay2 = options.immediate ? 0 : this.computeDelay();
this.scheduleReconnect(delay2, {
reason: options.reason,
preferLastSuccessful: this.preferLastSuccessful
});
}
scheduleReconnect(delayMs, options = {}) {
if (!this.running) {
return;
}
if (options.resetAuto && portMode === "auto") {
this.resetAutoSequence(wsPort);
}
if (options.preferLastSuccessful !== void 0) {
this.preferLastSuccessful = options.preferLastSuccessful;
}
if (this.reconnectTimer !== null) {
clearTimeout(this.reconnectTimer);
}
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.beginConnection();
}, Math.max(0, delayMs));
}
beginConnection() {
if (!this.running) {
return;
}
const attemptToken = ++this.attemptId;
const port = this.selectNextPort();
wsPort = port;
setSocketStatus("connecting");
console.log(`[yetibrowser] connecting to ws://localhost:${port} (mode: ${portMode})`);
let candidate;
try {
candidate = new WebSocket(`ws://localhost:${port}`);
} catch (error) {
console.error("[yetibrowser] failed to create WebSocket", error);
this.handleEarlyFailure(port);
this.scheduleReconnect(this.computeDelay(), { reason: "constructor-failed" });
return;
}
this.registerAttemptTimeout(attemptToken, candidate, port);
candidate.addEventListener("open", () => this.handleOpen(attemptToken, candidate, port));
candidate.addEventListener("message", (event) => this.handleMessage(attemptToken, candidate, event.data));
candidate.addEventListener("close", (event) => this.handleClose(attemptToken, candidate, port, event));
candidate.addEventListener("error", (event) => {
if (typeof event.preventDefault === "function") {
event.preventDefault();
}
if ("stopImmediatePropagation" in event && typeof event.stopImmediatePropagation === "function") {
event.stopImmediatePropagation();
}
this.handleError(attemptToken, candidate, port, event);
});
}
handleOpen(token, candidate, port) {
if (this.attemptId !== token) {
return;
}
this.expectingClose = false;
this.clearAttemptTimer();
this.attachSocket(candidate);
this.lastSuccessfulPort = port;
this.preferLastSuccessful = true;
this.scanIterations = 0;
this.resetFailureCounters();
if (portMode === "auto") {
this.pendingInitialAutoPort = null;
}
setSocketStatus("open");
persistWsPortIfNeeded(port).catch((error) => {
console.error("[yetibrowser] failed to persist websocket port", error);
});
sendHello(candidate);
startKeepAlive();
}
handleMessage(token, candidate, data) {
if (this.attemptId !== token || this.activeSocket !== candidate) {
return;
}
handleSocketMessage(data).catch((error) => {
console.error("[yetibrowser] failed to handle message", error);
});
}
handleClose(token, candidate, port, event) {
if (this.attemptId !== token) {
return;
}
this.clearAttemptTimer();
const wasActive = this.activeSocket === candidate;
if (wasActive) {
console.warn(
"[yetibrowser] MCP socket closed",
JSON.stringify({ code: event.code, reason: event.reason, wasClean: event.wasClean })
);
this.detachSocket();
}
const intentional = this.expectingClose;
this.expectingClose = false;
if (intentional) {
return;
}
this.recordFailure(port);
setSocketStatus("connecting");
const preferLast = wasActive && portMode === "auto";
this.scheduleReconnect(this.computeDelay(), { reason: "close", preferLastSuccessful: preferLast });
}
handleError(token, candidate, port, event) {
if (this.attemptId !== token) {
return;
}
this.recordFailure(port);
if (this.activeSocket === candidate) {
console.debug("[yetibrowser] socket error", {
port,
readyState: candidate.readyState,
type: event.type
});
}
}
handleEarlyFailure(port) {
this.recordFailure(port);
this.preferLastSuccessful = false;
}
registerAttemptTimeout(token, candidate, port) {
this.clearAttemptTimer();
this.attemptTimer = setTimeout(() => {
if (this.attemptId !== token) {
return;
}
if (candidate.readyState !== WebSocket.CONNECTING) {
return;
}
console.warn(`[yetibrowser] connection attempt on port ${port} timed out`);
try {
candidate.close();
} catch (error) {
console.error("[yetibrowser] failed to close timed-out socket", error);
}
}, CONNECT_ATTEMPT_TIMEOUT_MS);
}
clearAttemptTimer() {
if (this.attemptTimer !== null) {
clearTimeout(this.attemptTimer);
this.attemptTimer = null;
}
}
attachSocket(instance) {
this.activeSocket = instance;
socket = instance;
}
detachSocket() {
stopKeepAlive();
this.activeSocket = null;
socket = null;
}
teardownActiveSocket() {
this.clearAttemptTimer();
if (this.activeSocket) {
this.expectingClose = true;
try {
this.activeSocket.close();
} catch (error) {
console.error("[yetibrowser] failed to close socket", error);
}
this.detachSocket();
}
}
selectNextPort() {
if (portMode === "manual") {
return this.manualPort;
}
if (this.pendingInitialAutoPort !== null) {
const initial = this.pendingInitialAutoPort;
this.pendingInitialAutoPort = null;
return initial;
}
if (this.preferLastSuccessful && this.lastSuccessfulPort !== null) {
this.preferLastSuccessful = false;
return this.lastSuccessfulPort;
}
const port = FALLBACK_WS_PORTS[this.nextAutoIndex];
this.nextAutoIndex = (this.nextAutoIndex + 1) % FALLBACK_WS_PORTS.length;
return port;
}
resetAutoSequence(startPort) {
const target = isAutoPort(startPort) ? startPort : DEFAULT_WS_PORT;
const startIndex = FALLBACK_WS_PORTS.indexOf(target);
this.pendingInitialAutoPort = target;
this.nextAutoIndex = (startIndex + 1) % FALLBACK_WS_PORTS.length;
}
resetFailureCounters() {
this.failurePort = null;
this.failureCount = 0;
this.lastFailureAt = 0;
this.scanIterations = 0;
}
recordFailure(port) {
const now = Date.now();
if (this.failurePort !== port || now - this.lastFailureAt > FAILURE_RESET_WINDOW_MS) {
this.failurePort = port;
this.failureCount = 0;
}
this.failureCount += 1;
this.lastFailureAt = now;
if (this.lastSuccessfulPort === null) {
this.scanIterations += 1;
}
if (portMode === "auto" && this.failureCount >= 2) {
this.preferLastSuccessful = false;
if (this.lastSuccessfulPort === port) {
this.lastSuccessfulPort = null;
}
}
}
computeDelay() {
if (portMode === "manual") {
const attempt = Math.min(
this.failureCount + 1,
Math.ceil(MANUAL_RETRY_MAX_DELAY_MS / MANUAL_RETRY_BASE_DELAY_MS)
);
return Math.min(MANUAL_RETRY_BASE_DELAY_MS * attempt, MANUAL_RETRY_MAX_DELAY_MS);
}
if (this.lastSuccessfulPort === null) {
return this.scanIterations >= FALLBACK_WS_PORTS.length * 3 ? AUTO_SCAN_SLOW_DELAY_MS : AUTO_SCAN_FAST_DELAY_MS;
}
return this.failureCount >= 3 ? AUTO_SCAN_SLOW_DELAY_MS : AUTO_RECOVERY_DELAY_MS;
}
};
var connectionManager = new WebSocketConnectionManager();
chrome.runtime.onInstalled.addListener(() => {
console.log("[yetibrowser] extension installed");
});
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (!message || typeof message !== "object") {
return;
}
if (message.type === "yetibrowser/connect") {
const { tabId } = message;
setConnectedTab(tabId).then(() => sendResponse({ ok: true })).catch((error) => sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
return true;
}
if (message.type === "yetibrowser/disconnect") {
clearConnectedTab().then(() => sendResponse({ ok: true })).catch((error) => sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }));
return true;
}
if (message.type === "yetibrowser/getState") {
sendResponse({
tabId: connectedTabId,
wsPort,
socketConnected: socket?.readyState === WebSocket.OPEN,
portMode,
socketStatus
});
return;
}
if (message.type === "yetibrowser/setPortConfig") {
const { mode, port } = message;
void setPortConfiguration(mode, port).then(() => sendResponse({ ok: true })).catch(
(error) => sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) })
);
return true;
}
if (message.type === "yetibrowser/reconnect") {
triggerManualReconnect();
sendResponse({ ok: true });
return;
}
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== "local") {
return;
}
if (STORAGE_KEYS.connectedTabId in changes) {
const value = changes[STORAGE_KEYS.connectedTabId]?.newValue;
connectedTabId = typeof value === "number" ? value : null;
console.log("[yetibrowser] connected tab changed", connectedTabId);
}
const modeChange = changes[STORAGE_KEYS.wsPortMode];
const portChange = changes[STORAGE_KEYS.wsPort];
if (modeChange) {
const parsed = modeChange.newValue === "manual" ? "manual" : DEFAULT_PORT_MODE;
if (parsed !== portMode) {
if (parsed === "manual") {
const rawPort = portChange?.newValue;
const manualPort = typeof rawPort === "number" && Number.isFinite(rawPort) ? rawPort : wsPort;
portMode = "manual";
wsPort = isValidPort(manualPort) ? manualPort : DEFAULT_WS_PORT;
connectionManager.setManualMode(wsPort, { fromStorage: true });
setSocketStatus("connecting");
} else {
const rawPort = portChange?.newValue;
const autoPort = typeof rawPort === "number" && Number.isFinite(rawPort) ? rawPort : wsPort;
portMode = "auto";
wsPort = isAutoPort(autoPort) ? autoPort : DEFAULT_WS_PORT;
connectionManager.setAutoMode(wsPort, { fromStorage: true });
setSocketStatus("connecting");
}
}
}
if (portChange) {
const parsed = typeof portChange.newValue === "number" && Number.isFinite(portChange.newValue) ? portChange.newValue : DEFAULT_WS_PORT;
if (parsed === wsPort) {
return;
}
wsPort = parsed;
if (portMode === "manual") {
connectionManager.updateManualPortFromStorage(parsed, { scheduleReconnect: !modeChange });
} else if (portMode === "auto") {
connectionManager.updateAutoPortFromStorage(parsed, { scheduleReconnect: !modeChange });
}
}
});
void bootstrap();
async function bootstrap() {
const stored = await chrome.storage.local.get(STORAGE_KEYS);
const storedTabId = stored[STORAGE_KEYS.connectedTabId];
const storedPort = stored[STORAGE_KEYS.wsPort];
const storedMode = stored[STORAGE_KEYS.wsPortMode];
if (typeof storedTabId === "number") {
connectedTabId = storedTabId;
await initializeTab(storedTabId);
}
if (storedMode === "manual" || storedMode === "auto") {
portMode = storedMode;
}
if (typeof storedPort === "number" && Number.isFinite(storedPort)) {
wsPort = portMode === "auto" ? isAutoPort(storedPort) ? storedPort : DEFAULT_WS_PORT : storedPort;
}
connectionManager.initialize(portMode, wsPort);
connectionManager.start();
}
function triggerManualReconnect() {
const options = portMode === "auto" ? { immediate: true, reason: "manual", resetAuto: true, preferLastSuccessful: false } : { immediate: true, reason: "manual" };
connectionManager.triggerReconnect(options);
}
function isAutoPort(port) {
return FALLBACK_WS_PORTS.includes(port);
}
async function persistWsPortIfNeeded(port) {
if (portMode !== "auto") {
return;
}
const stored = await chrome.storage.local.get(STORAGE_KEYS.wsPort);
if (stored[STORAGE_KEYS.wsPort] === port) {
return;
}
await chrome.storage.local.set({ [STORAGE_KEYS.wsPort]: port });
}
async function setPortConfiguration(mode, port) {
if (mode === "manual") {
if (!isValidPort(port)) {
throw new Error("Port must be an integer between 1 and 65535");
}
portMode = "manual";
wsPort = port;
connectionManager.setManualMode(port, { fromStorage: false });
return;
}
const candidate = typeof port === "number" && Number.isInteger(port) ? port : wsPort;
portMode = "auto";
wsPort = isAutoPort(candidate) ? candidate : DEFAULT_WS_PORT;
connectionManager.setAutoMode(wsPort, { fromStorage: false });
}
function isValidPort(value) {
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535;
}
function sendHello(targetSocket = socket) {
if (!targetSocket || targetSocket.readyState !== WebSocket.OPEN) {
return;
}
const message = {
type: "hello",
client: "yetibrowser-extension",
version: chrome.runtime.getManifest().version
};
targetSocket.send(JSON.stringify(message));
}
function startKeepAlive() {
stopKeepAlive();
keepAliveTimer = setInterval(() => {
if (!socket || socket.readyState !== WebSocket.OPEN) {
return;
}
const message = {
type: "event",
event: "heartbeat",
payload: Date.now()
};
socket.send(JSON.stringify(message));
}, 2e4);
}
function stopKeepAlive() {
if (keepAliveTimer !== null) {
clearInterval(keepAliveTimer);
keepAliveTimer = null;
}
}
async function handleSocketMessage(data) {
if (!socket) {
return;
}
let message;
try {
message = JSON.parse(String(data));
} catch (error) {
console.error("[yetibrowser] invalid message from server", error);
return;
}
if (message.type !== "call") {
console.warn("[yetibrowser] unsupported message type", message);
return;
}
try {
const result = await dispatchCommand(message.command, message.payload);
respond({
type: "result",
id: message.id,
command: message.command,
ok: true,
result
});
} catch (error) {
const messageText = error instanceof Error ? error.message : String(error);
respond({
type: "result",
id: message.id,
command: message.command,
ok: false,
error: messageText
});
}
}
function respond(message) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
return;
}
socket.send(JSON.stringify(message));
}
async function dispatchCommand(command, payload) {
switch (command) {
case "ping":
return { ok: true };
case "getUrl":
return { url: (await ensureTab()).url ?? "about:blank" };
case "getTitle":
return { title: (await ensureTab()).title ?? "" };
case "snapshot": {
const snapshot = await captureSnapshot();
return snapshot;
}
case "navigate": {
const { url } = payload;
await navigateTo(url);
return { ok: true };
}
case "goBack":
await goBack();
return { ok: true };
case "goForward":
await goForward();
return { ok: true };
case "wait": {
const { seconds } = payload;
await delay(seconds * 1e3);
return { ok: true };
}
case "pressKey": {
const { key } = payload;
await simulateKeyPress(key);
return { ok: true };
}
case "click": {
const { selector, description } = payload;
await clickElement(selector, description);
return { ok: true };
}
case "hover": {
const { selector, description } = payload;
await hoverElement(selector, description);
return { ok: true };
}
case "type": {
const { selector, text, submit, description } = payload;
await typeIntoElement(selector, text, submit ?? false, description);
return { ok: true };
}
case "selectOption": {
const { selector, values, description } = payload;
await selectOptions(selector, values, description);
return { ok: true };
}
case "screenshot": {
const { fullPage } = payload;
const { data, mimeType } = await takeScreenshot(fullPage ?? false);
return { data, mimeType };
}
case "getConsoleLogs": {
const logs = await readConsoleLogs();
return logs;
}
case "pageState": {
const state = await capturePageState();
return state;
}
case "waitFor": {
const { selector, timeoutMs, visible } = payload;
await waitForSelector(selector, timeoutMs ?? 5e3, visible ?? false);
return { ok: true };
}
case "fillForm": {
const { fields } = payload;
const result = await fillFormFields(fields ?? []);
return result;
}
case "evaluate": {
const { script, args, timeoutMs } = payload;
const evaluationPromise = evaluateInPage(script, args);
const value = typeof timeoutMs === "number" && timeoutMs > 0 ? await Promise.race([
evaluationPromise,
new Promise(
(_, reject) => setTimeout(
() => reject(new Error(`Script evaluation timed out after ${timeoutMs}ms`)),
timeoutMs
)
)
]) : await evaluationPromise;
return { value };
}
case "handleDialog": {
const { action, promptText } = payload;
await handleJavaScriptDialog(action, promptText);
return { ok: true };
}
case "drag": {
const { fromSelector, toSelector, steps, description } = payload;
await dragElement(fromSelector, toSelector, steps ?? 12, description);
return { ok: true };
}
default:
throw new Error(`Unsupported command ${command}`);
}
}
async function ensureTab() {
if (connectedTabId === null) {
throw new Error("No tab connected. Open the YetiBrowser popup and connect the target tab.");
}
try {
return await chrome.tabs.get(connectedTabId);
} catch (error) {
console.warn("[yetibrowser] failed to get connected tab", error);
await clearConnectedTab();
throw new Error("Connected tab is no longer available. Reconnect from the popup and try again.");
}
}
async function setConnectedTab(tabId) {
await chrome.storage.local.set({ [STORAGE_KEYS.connectedTabId]: tabId });
connectedTabId = tabId;
await initializeTab(tabId);
void updateBadge();
}
async function clearConnectedTab() {
await chrome.storage.local.remove(STORAGE_KEYS.connectedTabId);
connectedTabId = null;
void updateBadge();
}
async function navigateTo(url) {
const tab = await ensureTab();
await chrome.tabs.update(tab.id, { url });
await waitForTabComplete(tab.id);
await initializeTab(tab.id);
}
async function goBack() {
const tab = await ensureTab();
try {
await chrome.tabs.goBack(tab.id);
} catch (error) {
console.warn("[yetibrowser] unable to navigate back", error);
}
await waitForTabComplete(tab.id);
await initializeTab(tab.id);
}
async function goForward() {
const tab = await ensureTab();
try {
await chrome.tabs.goForward(tab.id);
} catch (error) {
console.warn("[yetibrowser] unable to navigate forward", error);
}
await waitForTabComplete(tab.id);
await initializeTab(tab.id);
}
async function waitForTabComplete(tabId) {
const tab = await chrome.tabs.get(tabId);
if (tab.status === "complete") {
return;
}
await new Promise((resolve) => {
const listener = (updatedTabId, info) => {
if (updatedTabId === tabId && info.status === "complete") {
chrome.tabs.onUpdated.removeListener(listener);
resolve();
}
};
chrome.tabs.onUpdated.addListener(listener);
});
}
async function captureSnapshot() {
const tab = await ensureTab();
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: collectSnapshot
});
const scriptResult = results[0]?.result;
if (!scriptResult || typeof scriptResult === "string") {
const fallback = {
title: tab.title ?? "",
url: tab.url ?? "about:blank",
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
entries: []
};
return {
formatted: typeof scriptResult === "string" ? scriptResult : formatSnapshot(fallback),
raw: fallback
};
}
const snapshot = scriptResult.snapshot;
if (!snapshot.capturedAt) {
snapshot.capturedAt = (/* @__PURE__ */ new Date()).toISOString();
}
return {
formatted: formatSnapshot(snapshot),
raw: snapshot
};
}
async function capturePageState() {
const fallback = {
forms: [],
localStorage: [],
sessionStorage: [],
cookies: [],
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
};
const response = await runInPage(() => {
const computeSelector = (element) => {
if (element.id) {
return `#${element.id}`;
}
const parts = [];
let current = element;
while (current && parts.length < 5) {
const tag = current.tagName.toLowerCase();
const classes = (current.className || "").toString().split(/\s+/).filter(Boolean).slice(0, 2).map((cls) => cls.replace(/[^a-zA-Z0-9_-]/g, "")).filter(Boolean);
let part = tag;
if (classes.length) {
part += `.${classes.join(".")}`;
}
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1;
part += `:nth-of-type(${index})`;
}
}
parts.push(part);
current = current.parentElement;
}
return parts.reverse().join(" > ");
};
const readStorage = (storage) => {
const entries = [];
const limit = Math.min(storage.length, 100);
for (let index = 0; index < limit; index += 1) {
const key = storage.key(index);
if (!key) {
continue;
}
try {
const value = storage.getItem(key) ?? "";
entries.push({ key, value });
} catch (error) {
entries.push({ key, value: "<unavailable>" });
}
}
return entries;
};
const readCookies = () => {
const raw = document.cookie;
if (!raw) {
return [];
}
return raw.split(";").slice(0, 50).map((part) => {
const [name, ...rest] = part.split("=");
return { key: name.trim(), value: rest.join("=").trim() };
});
};
const forms = Array.from(document.querySelectorAll("form")).slice(0, 25).map((form) => {
const fields = [];
const elements = Array.from(form.elements ?? []).slice(0, 50);
for (const element of elements) {
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) {
continue;
}
const selector = computeSelector(element);
const base = {
selector,
name: element.getAttribute("name") ?? void 0
};
if (element instanceof HTMLInputElement) {
base.type = element.type;
if (element.type === "password") {
base.value = "[redacted]";
} else if (element.type === "file") {
base.value = element.files?.length ? `${element.files.length} file(s)` : "";
} else {
base.value = element.value;
}
base.label = element.labels?.[0]?.innerText.trim() || element.placeholder || void 0;
} else if (element instanceof HTMLTextAreaElement) {
base.type = "textarea";
base.value = element.value;
base.label = element.labels?.[0]?.innerText.trim() || element.placeholder || void 0;
} else if (element instanceof HTMLSelectElement) {
base.type = "select";
base.value = Array.from(element.selectedOptions).map((option) => option.value || option.label).join(", ");
base.label = element.labels?.[0]?.innerText.trim() || void 0;
}
fields.push(base);
}
return {
selector: computeSelector(form),
name: form.getAttribute("name") ?? void 0,
method: form.getAttribute("method")?.toUpperCase() ?? void 0,
action: form.getAttribute("action") ?? void 0,
fields
};
});
const snapshot = {
forms,
localStorage: readStorage(window.localStorage),
sessionStorage: readStorage(window.sessionStorage),
cookies: readCookies(),
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
};
return { ok: true, value: snapshot };
}, []);
return response ?? fallback;
}
function collectSnapshot() {
function computeSelector(element) {
if (element.id) {
return `#${element.id}`;
}
const parts = [];
let current = element;
while (current && parts.length < 5) {
const tag = current.tagName.toLowerCase();
const classes = (current.className || "").toString().split(/\s+/).filter(Boolean).slice(0, 2).map((cls) => cls.replace(/[^a-zA-Z0-9_-]/g, "")).filter(Boolean);
let part = tag;
if (classes.length) {
part += `.${classes.join(".")}`;
}
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1;
part += `:nth-of-type(${index})`;
}
}
parts.push(part);
current = current.parentElement;
}
return parts.reverse().join(" > ");
}
function makeName(element) {
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
return element.placeholder || element.value || element.name || element.id || element.type;
}
if (element instanceof HTMLSelectElement) {
return element.options[element.selectedIndex]?.label || element.name || element.id || "select";
}
const text = element.textContent?.trim();
if (text) {
return text.slice(0, 160);
}
return element.getAttribute("aria-label") || element.getAttribute("title") || element.tagName.toLowerCase();
}
const targets = Array.from(
document.querySelectorAll("a, button, input, textarea, select, [role='button'], [role='link']")
);
const entries = targets.slice(0, 100).map((element) => ({
selector: computeSelector(element),
role: element.getAttribute("role") ?? element.tagName.toLowerCase(),
name: makeName(element)
}));
return {
snapshot: {
title: document.title,
url: location.href,
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
entries
}
};
}
function formatSnapshot(snapshot) {
const lines = [];
lines.push(`title: ${snapshot.title}`);
lines.push(`url: ${snapshot.url}`);
lines.push(`capturedAt: ${snapshot.capturedAt}`);
lines.push("elements:");
for (const entry of snapshot.entries) {
lines.push(` - selector: "${entry.selector.replace(/"/g, '\\"')}"`);
lines.push(` role: ${entry.role}`);
lines.push(` name: "${entry.name.replace(/"/g, '\\"')}"`);
}
return lines.join("\n");
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function runInPage(func, args) {
const tab = await ensureTab();
const [execution] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func,
args,
world: "MAIN"
});
const response = execution?.result;
if (!response) {
throw new Error("Injected script did not return a result");
}
if ("error" in response) {
throw new Error(response.error);
}
return response.value;
}
async function waitForSelector(selector, timeoutMs, requireVisible) {
await runInPage(
(sel, timeout, visible) => {
const deadline = timeout > 0 ? Date.now() + timeout : Number.POSITIVE_INFINITY;
const visibilityCheck = (element) => {
if (!visible) {
return true;
}
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
}
const style = window.getComputedStyle(element);
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity) === 0) {
return false;
}
return true;
};
const locate = () => {
const element = document.querySelector(sel);
if (!element) {
return null;
}
if (!visibilityCheck(element)) {
return null;
}
return element;
};
return new Promise((resolve) => {
const existing = locate();
if (existing) {
resolve({ ok: true });
return;
}
const abort = () => {
observer.disconnect();
clearInterval(intervalId);
};
const observer = new MutationObserver(() => {
const match = locate();
if (match) {
abort();
resolve({ ok: true });
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: visible,
attributeFilter: visible ? ["style", "class", "hidden", "aria-hidden"] : void 0
});
const intervalId = window.setInterval(() => {
const match = locate();
if (match) {
abort();
resolve({ ok: true });
return;
}
if (Date.now() > deadline) {
abort();
resolve({ error: `Timed out after ${timeout}ms waiting for selector ${sel}` });
}
}, 50);
if (!Number.isFinite(deadline)) {
const frameCheck = () => {
const match = locate();
if (match) {
abort();
resolve({ ok: true });
return;
}
window.requestAnimationFrame(frameCheck);
};
window.requestAnimationFrame(frameCheck);
}
});
},
[selector, timeoutMs, requireVisible]
);
}
async function fillFormFields(fields) {
let filled = 0;
const errors = [];
const submitSelectors = /* @__PURE__ */ new Set();
for (const field of fields) {
try {
const description = field.description ?? field.selector;
const targetType = field.type ?? "auto";
if (Array.isArray(field.values) && field.values.length > 0) {
await selectOptions(field.selector, field.values, field.description);
filled++;
} else if (targetType === "select" && typeof field.value !== "undefined") {
await selectOptions(field.selector, [String(field.value)], field.description);
filled++;
} else if (typeof field.value === "boolean" || targetType === "checkbox") {
const desired = typeof field.value === "boolean" ? field.value : coerceBoolean(field.value);
await setCheckboxState(field.selector, desired, description);
filled++;
} else if (targetType === "radio") {
await setRadioState(field.selector, field.value, description);
filled++;
} else {
const text = field.value === null || typeof field.value === "undefined" ? "" : String(field.value);
await typeIntoElement(field.selector, text, false, field.description);
filled++;
}
if (field.submit) {
submitSelectors.add(field.selector);
}
} catch (error) {
errors.push(
error instanceof Error ? error.message : `Failed to fill ${field.selector}: ${String(error)}`
);
}
}
for (const selector of submitSelectors) {
try {
await submitContainingForm(selector);
} catch (error) {
errors.push(
error instanceof Error ? error.message : `Failed to submit form for ${selector}: ${String(error)}`
);
}
}
return { filled, attempted: fields.length, errors };
}
function coerceBoolean(value) {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "number") {
return value !== 0;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
return ["true", "1", "yes", "on"].includes(normalized);
}
return false;
}
async function setCheckboxState(selector, checked, description) {
await runInPage(
(sel, state, label) => {
const element = document.querySelector(sel);
if (!(element instanceof HTMLInputElement) || element.type !== "checkbox") {
return { error: `Element is not a checkbox: ${label ?? sel}` };
}
if (element.checked === state) {
return { ok: true };
}
element.checked = state;
element.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
element.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
return { ok: true };
},
[selector, checked, description ?? null]
);
}
async function setRadioState(selector, value, description) {
await runInPage(
(sel, selected, label) => {
const element = document.querySelector(sel);
if (!(element instanceof HTMLInputElement) || element.type !== "radio") {
return { error: `Element is not a radio button: ${label ?? sel}` };
}
if (typeof selected === "string" || typeof selected === "number") {
const stringValue = String(selected);
element.checked = element.value === stringValue;
} else if (typeof selected === "boolean") {
element.checked = selected;
} else {
element.checked = true;
}
element.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
element.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
return { ok: true };
},
[selector, value, description ?? null]
);
}
async function submitContainingForm(selector) {
await runInPage(
(sel) => {
const element = document.querySelector(sel);
if (!element) {
return { error: `Element not found: ${sel}` };
}
const form = element instanceof HTMLFormElement ? element : element.closest("form") ?? void 0;
if (!form) {
return { error: "No containing form to submit" };
}
if (typeof form.requestSubmit === "function") {
form.requestSubmit();
} else {
form.submit();
}
return { ok: true };
},
[selector]
);
}
async function evaluateInPage(script, args) {
return await runInPage(
async (source, functionArgs) => {
let fn;
try {
fn = globalThis.eval(`(${source})`);
} catch (error) {
return {
error: `Failed to parse script: ${error instanceof Error ? error.message : String(error)}`
};
}
if (typeof fn !== "function") {
return { error: "Script must evaluate to a function" };
}
try {
const result = await fn(...functionArgs);
let cloned;
if (typeof structuredClone === "function") {
cloned = structuredClone(result);
} else {
cloned = JSON.parse(JSON.stringify(result));
}
return { ok: true, value: cloned };
} catch (error) {
return { error: error instanceof Error ? error.message : String(error) };
}
},
[script, args ?? []]
);
}
async function handleJavaScriptDialog(action, promptText) {
const tab = await ensureTab();
const target = { tabId: tab.id };
await attachDebugger(target);
try {
await sendDebuggerCommand(target, "Page.enable");
await sendDebuggerCommand(target, "Page.handleJavaScriptDialog", {
accept: action === "accept",
promptText
});
} catch (error) {
throw new Error(
error instanceof Error ? error.message : `Failed to handle dialog: ${String(error)}`
);
} finally {
await detachDebugger(target);
}
}
async function dragElement(fromSelector, toSelector, steps, description) {
const resolvedSteps = typeof steps === "number" && Number.isFinite(steps) ? Math.max(1, Math.floor(steps)) : 12;
await runInPage(
(sourceSelector, targetSelector, stepCount, label) => {
const source = document.querySelector(sourceSelector);
const target = document.querySelector(targetSelector);
if (!source || !(source instanceof HTMLElement)) {
return { error: `Drag source not found: ${label ?? sourceSelector}` };
}
if (!target || !(target instanceof HTMLElement)) {
return { error: `Drop target not found: ${label ?? targetSelector}` };
}
const startRect = source.getBoundingClientRect();
const endRect = target.getBoundingClientRect();
const startX = startRect.left + startRect.width / 2;
const startY = startRect.top + startRect.height / 2;
const endX = endRect.left + endRect.width / 2;
const endY = endRect.top + endRect.height / 2;
const dataTransfer = typeof DataTransfer === "function" ? new DataTransfer() : void 0;
const pointerId = 1;
const firePointerEvent = (type, x, y, buttons) => {
const targetElement = document.elementFromPoint(x, y);
(targetElement ?? document.body).dispatchEvent(
new PointerEvent(type, {
bubbles: true,
cancelable: true,
pointerId,
pointerType: "mouse",
clientX: x,
clientY: y,
buttons
})
);
};
const fireDragEvent = (element, type, x, y) => {
if (typeof DragEvent !== "function") {
return;
}
element.dispatchEvent(
new DragEvent(type, {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
dataTransfer
})
);
};
firePointerEvent("pointerover", startX, startY, 0);
firePointerEvent("pointerenter", startX, startY, 0);
firePointerEvent("pointerdown", startX, startY, 1);
source.dispatchEvent(
new MouseEvent("mousedown", {
bubbles: true,
cancelable: true,
clientX: startX,
clientY: startY,
buttons: 1
})
);
fireDragEvent(source, "dragstart", startX, startY);
const totalSteps = typeof stepCount === "number" && Number.isFinite(stepCount) && stepCount > 0 ? Math.floor(stepCount) : 12;
for (let i = 1; i <= totalSteps; i++) {
const progress = i / totalSteps;
const currentX = startX + (endX - startX) * progress;
const currentY = startY + (endY - startY) * progress;
firePointerEvent("pointermove", currentX, currentY, 1);
fireDragEvent(target, "dragover", currentX, currentY);
}
fireDragEvent(target, "drop", endX, endY);
firePointerEvent("pointerup", endX, endY, 0);
target.dispatchEvent(
new MouseEvent("mouseup", {
bubbles: true,
cancelable: true,
clientX: endX,
clientY: endY,
buttons: 0
})
);
firePointerEvent("pointerout", endX, endY, 0);
firePointerEvent("pointerleave", endX, endY, 0);
return { ok: true };
},
[fromSelector, toSelector, resolvedSteps, description ?? null]
);
}
async function simulateKeyPress(key) {
await runInPage(
(keyValue) => {
const active = document.activeElement;
if (!active) {
return { error: "No element is focused" };
}
const init = { key: keyValue, bubbles: true, cancelable: true };
active.dispatchEvent(new KeyboardEvent("keydown", init));
active.dispatchEvent(new KeyboardEvent("keypress", init));
active.dispatchEvent(new KeyboardEvent("keyup", init));
return { ok: true };
},
[key]
);
}
async function clickElement(selector, description) {
await runInPage(
(sel, label) => {
const element = document.querySelector(sel);
if (!element || !(element instanceof HTMLElement)) {
return { error: `Element not found: ${label ?? sel}` };
}
element.focus({ preventScroll: false });
element.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
element.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
element.click();
return { ok: true };
},
[selector, description ?? null]
);
}
async function hoverElement(selector, description) {
await runInPage(
(sel, label) => {
const element = document.querySelector(sel);
if (!element || !(element instanceof HTMLElement)) {
return { error: `Element not found: ${label ?? sel}` };
}
element.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, cancelable: true }));
element.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true }));
element.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true }));
return { ok: true };
},
[selector, description ?? null]
);
}
async function typeIntoElement(selector, text, submit, description) {
await runInPage(
(sel, value, shouldSubmit, label) => {
const element = document.querySelector(sel);
if (!element || !(element instanceof HTMLElement)) {
return { error: `Element not found: ${label ?? sel}` };
}
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
element.focus({ preventScroll: false });
element.value = value;
element.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
element.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
} else if (element.isContentEditable) {
element.focus({ preventScroll: false });
element.textContent = value;
element.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
} else {
return { error: "Element is not editable" };
}
if (shouldSubmit) {
const init = { key: "Enter", bubbles: true, cancelable: true };
element.dispatchEvent(new KeyboardEvent("keydown", init));
element.dispatchEvent(new KeyboardEvent("keyup", init));
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
element.form?.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
}
}
return { ok: true };
},
[selector, text, submit, description ?? null]
);
}
async function selectOptions(selector, values, description) {
await runInPage(
(sel, valueList, label) => {
const element = document.querySelector(sel);
if (!(element instanceof HTMLSelectElement)) {
return { error: `Element is not a <select>: ${label ?? sel}` };
}
const targets = new Set(valueList);
let matched = 0;
for (const option of Array.from(element.options)) {
const shouldSelect = targets.has(option.value) || targets.has(option.label) || targets.has(option.textContent ?? "");
if (shouldSelect) {
option.selected = true;
matched++;
if (!element.multiple) {
break;
}
} else if (!element.multiple) {
option.selected = false;
}
}
if (matched === 0) {
return { error: "None of the provided values matched" };
}
element.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
element.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
return { ok: true };
},
[selector, values, description ?? null]
);
}
async function takeScreenshot(fullPage) {
const tab = await ensureTab();
let base64;
if (canUseDebugger()) {
try {
base64 = await captureScreenshotWithDebugger(tab.id, fullPage);
} catch (error) {
console.warn("[yetibrowser] debugger capture failed, falling back", error);
}
}
if (!base64) {
base64 = await captureVisibleTabFallback(tab.windowId);
}
return await encodeScreenshot(base64);
}
function canUseDebugger() {
const manifest = chrome.runtime.getManifest();
const permissions = Array.isArray(manifest.permissions) ? manifest.permissions : [];
const optionalPermissions = Array.isArray(manifest.optional_permissions) ? manifest.optional_permissions : [];
if (!permissions.includes("debugger") && !optionalPermissions.includes("debugger")) {
return false;
}
return typeof chrome.debugger?.attach === "function";
}
var DEBUGGER_PROTOCOL_VERSION = "1.3";
async function captureScreenshotWithDebugger(tabId, fullPage) {
const target = { tabId };
await attachDebugger(target);
let metricsOverridden = false;
try {
await sendDebuggerCommand(target, "Page.enable");
if (fullPage) {
try {
const metrics = await sendDebuggerCommand(
target,
"Page.getLayoutMetrics"
);
const width = Math.ceil(metrics.contentSize?.width ?? 0);
const height = Math.ceil(metrics.contentSize?.height ?? 0);
if (width > 0 && height > 0) {
await sendDebuggerCommand(target, "Emulation.setDeviceMetricsOverride", {
mobile: false,
deviceScaleFactor: 1,
width,
height,
screenWidth: width,
screenHeight: height,
viewport: {
x: 0,
y: 0,
width,
height,
scale: 1
}
});
metricsOverridden = true;
}
} catch (error) {
console.warn("[yetibrowser] layout metrics unavailable, skipping full-page override", error);
}
}
const screenshot = await sendDebuggerCommand(target, "Page.captureScreenshot", {
format: "png",
captureBeyondViewport: fullPage
});
if (metricsOverridden) {
await sendDebuggerCommand(target, "Emulation.clearDeviceMetricsOverride");
}
return screenshot.data;
} finally {
await detachDebugger(target);
}
}
async function captureVisibleTabFallback(windowId) {
if (windowId === void 0) {
throw new Error("Unable to determine window for active tab");
}
const dataUrl = await new Promise((resolve, reject) => {
chrome.tabs.captureVisibleTab(windowId, { format: "png" }, (result) => {
const error = chrome.runtime.lastError;
if (error) {
reject(new Error(error.message));
return;
}
if (!result) {
reject(new Error("Failed to capture screenshot"));
return;
}
resolve(result);
});
});
const [, base64] = dataUrl.split(",");
if (!base64) {
throw new Error("Unexpected screenshot data format");
}
return base64;
}
async function attachDebugger(target) {
await new Promise((resolve, reject) => {
chrome.debugger.attach(target, DEBUGGER_PROTOCOL_VERSION, () => {
const error = chrome.runtime.lastError;
if (error) {
if (error.message?.includes("Another debugger is already attached")) {
resolve();
return;
}
reject(new Error(error.message));
return;
}
resolve();
});
});
}
async function detachDebugger(target) {
await new Promise((resolve) => {
chrome.debugger.detach(target, () => {
const error = chrome.runtime.lastError;
if (error && !error.message?.includes("No debugger is connected")) {
console.warn("[yetibrowser] failed to detach debugger", error);
}
resolve();
});
});
}
async function encodeScreenshot(base64Png) {
const bytes = Uint8Array.from(atob(base64Png), (char) => char.charCodeAt(0));
const blob = new Blob([bytes], { type: "image/png" });
const bitmap = await createImageBitmap(blob);
const maxWidth = 1280;
const scale = bitmap.width > maxWidth ? maxWidth / bitmap.width : 1;
const targetWidth = Math.round(bitmap.width * scale);
const targetHeight = Math.round(bitmap.height * scale);
const canvas = new OffscreenCanvas(targetWidth, targetHeight);
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Unable to create drawing context for screenshot");
}
ctx.drawImage(bitmap, 0, 0, targetWidth, targetHeight);
let outputBlob;
let mimeType = "image/webp";
try {
outputBlob = await canvas.convertToBlob({ type: "image/webp", quality: 0.85 });
} catch (error) {
console.warn("[yetibrowser] webp conversion failed, falling back to jpeg", error);
outputBlob = await canvas.convertToBlob({ type: "image/jpeg", quality: 0.85 });
mimeType = "image/jpeg";
}
const arrayBuffer = await outputBlob.arrayBuffer();
const outputBytes = new Uint8Array(arrayBuffer);
let binary = "";
outputBytes.forEach((byte) => {
binary += String.fromCharCode(byte);
});
return { data: btoa(binary), mimeType };
}
async function sendDebuggerCommand(target, method, params) {
return await new Promise((resolve, reject) => {
chrome.debugger.sendCommand(target, method, params, (result) => {
const error = chrome.runtime.lastError;
if (error) {
reject(new Error(error.message));
return;
}
resolve(result ?? {});
});
});
}
async function readConsoleLogs() {
const tab = await ensureTab();
await ensurePageHelpers(tab.id);
const logs = await runInPage(() => {
const win = window;
const entries = Array.isArray(win.__yetibrowser?.logs) ? win.__yetibrowser.logs : [];
return { ok: true, value: entries.slice(-200) };
}, []);
return logs ?? [];
}
async function initializeTab(tabId) {
try {
await ensurePageHelpers(tabId);
} catch (error) {
console.warn("[yetibrowser] failed to initialize tab helpers", error);
}
void updateBadge();
}
async function ensurePageHelpers(tabId) {
await chrome.scripting.executeScript({
target: { tabId },
world: "MAIN",
func: () => {
const win = window;
const install = () => {
const maxEntries = 500;
const state = win.__yetibrowser ?? { logs: [] };
const logs = Array.isArray(state.logs) ? state.logs : [];
const originals = {
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console)
};
const serialize = (value) => {
if (typeof value === "string") {
return value;
}
if (value instanceof Error) {
return value.message;
}
try {
const serialized = JSON.stringify(value);
return serialized ?? String(value);
} catch (error) {
return String(value);
}
};
const extractStack = (values) => {
for (const value of values) {
if (value instanceof Error && value.stack) {
return value.stack;
}
if (typeof value === "string" && value.includes("\n at ")) {
return value;
}
}
return void 0;
};
const pushEntry = (level, args, explicitStack) => {
const message = args.map((arg) => serialize(arg)).filter((part) => part.length > 0).join(" ") || level;
const stack = explicitStack ?? extractStack(args);
logs.push({ level, message, timestamp: Date.now(), stack });
if (logs.length > maxEntries) {
logs.shift();
}
};
const wrap = (level) => (...args) => {
pushEntry(level, args);
originals[level](...args);
};
console.log = wrap("log");
console.info = wrap("info");
console.warn = wrap("warn");
console.error = wrap("error");
window.addEventListener("error", (event) => {
const details = [event.message];
if (event.filename) {
const locationParts = [event.filename];
if (typeof event.lineno === "number") {
locationParts.push(String(event.lineno));
}
if (typeof event.colno === "number") {
locationParts.push(String(event.colno));
}
details.push(locationParts.join(":"));
}
const stack = event.error instanceof Error ? event.error.stack ?? void 0 : void 0;
pushEntry("error", details, stack);
originals.error(event.message, event.error ?? event);
});
window.addEventListener("unhandledrejection", (event) => {
const reason = event.reason;
let message;
let stack;
if (reason instanceof Error) {
message = reason.message;
stack = reason.stack ?? void 0;
} else {
message = serialize(reason);
}
pushEntry("error", ["Unhandled promise rejection", message], stack);
originals.error("Unhandled promise rejection", reason);
});
logs.push({ level: "debug", message: "[yetibrowser] console hooks installed", timestamp: Date.now() });
win.__yetibrowser = {
initialized: true,
logs
};
};
if (!win.__yetibrowser?.initialized) {
install();
return;
}
if (!Array.isArray(win.__yetibrowser.logs)) {
install();
}
}
});
const tab = await chrome.tabs.get(tabId);
if (tab.url?.startsWith("chrome://") || tab.url?.startsWith("edge://") || tab.url?.startsWith("about:")) {
console.warn("[yetibrowser] unable to inject helpers into special page", tab.url);
}
}
async function updateBadge() {
const isConnected = connectedTabId !== null && socketStatus === "open";
try {
const text = isConnected ? "\u25CF" : "";
await chrome.action.setBadgeText({ text });
if (isConnected) {
try {
await chrome.action.setBadgeBackgroundColor({ color: "#111827" });
} catch (error) {
console.warn("[yetibrowser] failed to set badge background", error);
}
if (chrome.action.setBadgeTextColor) {
try {
await chrome.action.setBadgeTextColor({ color: "#facc15" });
} catch (error) {
console.warn("[yetibrowser] failed to set badge text color", error);
}
}
}
} catch (error) {
console.warn("[yetibrowser] failed to set badge", error);
}
}
//# sourceMappingURL=background.js.map