// @ts-nocheck // Shared helpers for browser-cli extension command handlers. export async function getProfileAlias() { const { profileAlias } = await chrome.storage.local.get("profileAlias"); return profileAlias || "default"; } export function isBrowserErrorUrl(url) { const value = String(url || "").toLowerCase(); return value.startsWith("chrome-error://") || value.startsWith("edge-error://") || value.startsWith("brave-error://") || value.startsWith("about:neterror") || value.startsWith("about:certerror") || value.startsWith("about:blocked") || value.startsWith("about:tabcrashed"); } export function isErrorPageScriptError(error) { const message = String(error?.message || error || "").toLowerCase(); return message.includes("error page") || message.includes("chrome-error://") || message.includes("edge-error://") || message.includes("brave-error://") || message.includes("about:neterror") || message.includes("about:certerror") || message.includes("about:tabcrashed"); } export function isTransientScriptError(error) { const message = String(error?.message || error || ""); return message.includes("Frame with ID") || message.includes("No tab with id") || isErrorPageScriptError(error); } export function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } export const LARGE_OPERATION_BATCH_SIZE = 25; export const LARGE_OPERATION_PAUSE_MS = 25; export const GENTLE_OPERATION_BATCH_SIZE = 8; export const GENTLE_OPERATION_PAUSE_MS = 100; export async function hasAudibleTabs() { const audibleTabs = await chrome.tabs.query({ audible: true }); return audibleTabs.some(tab => !(tab.mutedInfo && tab.mutedInfo.muted)); } let largeOperationQueue = Promise.resolve(); export async function runLargeOperation(name, fn) { const run = largeOperationQueue.then(async () => { console.log(`[browser-cli] large operation start: ${name}`); try { return await fn(); } finally { console.log(`[browser-cli] large operation done: ${name}`); } }); largeOperationQueue = run.catch(() => {}); return run; } export async function getPerformanceProfile() { const { performanceProfile } = await chrome.storage.local.get("performanceProfile"); return performanceProfile || "auto"; } export async function setPerformanceProfile(profile) { const allowed = new Set(["auto", "normal", "gentle", "ultra"]); const performanceProfile = allowed.has(profile) ? profile : "auto"; await chrome.storage.local.set({ performanceProfile }); return { performanceProfile }; } export async function getLargeOperationThrottle(itemCount = 0, mode = "auto") { const audible = await hasAudibleTabs(); const storedProfile = await getPerformanceProfile(); const configuredMode = mode && mode !== "auto" ? mode : storedProfile; const gentle = configuredMode === "gentle" || configuredMode === "ultra" || (configuredMode === "auto" && audible); let batchSize = gentle ? GENTLE_OPERATION_BATCH_SIZE : LARGE_OPERATION_BATCH_SIZE; let pauseMs = gentle ? GENTLE_OPERATION_PAUSE_MS : LARGE_OPERATION_PAUSE_MS; if (configuredMode === "ultra" || itemCount >= 300) { batchSize = Math.max(3, Math.floor(batchSize / 2)); pauseMs *= 2; } else if (itemCount >= 100) { batchSize = Math.max(5, Math.floor(batchSize * 0.75)); pauseMs = Math.max(pauseMs, 75); } return { batchSize, pauseMs, gentle, audible, itemCount, mode: configuredMode }; } export function updateJobProgress(job, { phase, current, total } = {}) { if (!job) return; if (phase) job.phase = phase; if (total != null) job.total = total; if (current != null) job.current = current; if (job.total) job.percent = Math.min(100, Math.round((job.current || 0) * 100 / job.total)); job.updatedAt = Date.now(); } export function throwIfJobCancelled(job) { if (job?.cancelRequested) { throw new Error(`Job '${job.id}' cancelled`); } } export async function yieldForLargeOperation(processed, batchSize = LARGE_OPERATION_BATCH_SIZE, pauseMs = LARGE_OPERATION_PAUSE_MS) { if (processed > 0 && processed % batchSize === 0) { await sleep(pauseMs); } } export async function executeScript(options, retries = 3) { for (let i = 0; i < retries; i++) { try { return await chrome.scripting.executeScript(options); } catch (e) { if (i < retries - 1 && isTransientScriptError(e)) { await sleep(300); continue; } throw e; } } } export function tabInfo(t) { return { id: t.id, windowId: t.windowId, active: t.active, muted: Boolean(t.mutedInfo && t.mutedInfo.muted), groupId: t.groupId >= 0 ? t.groupId : null, title: t.title, url: t.url || t.pendingUrl || "", }; } // ── Groups ──────────────────────────────────────────────────────────────────── export function isScriptableUrl(url) { if (!url) return false; return !url.startsWith("chrome://") && !url.startsWith("brave://") && !url.startsWith("about:") && !url.startsWith("edge://") && !url.startsWith("chrome-extension://"); } export async function getActiveTab() { const activeTabs = await chrome.tabs.query({ active: true }); if (!activeTabs.length) throw new Error("No active tab found"); const windows = await chrome.windows.getAll({ populate: false }); const focusedWindowIds = new Set(windows.filter(window => window.focused).map(window => window.id)); const chooseTab = (predicate) => activeTabs.find(predicate); const byFocusAndScriptable = tab => focusedWindowIds.has(tab.windowId) && isScriptableUrl(tab.url || tab.pendingUrl || ""); const byScriptable = tab => isScriptableUrl(tab.url || tab.pendingUrl || ""); const byFocus = tab => focusedWindowIds.has(tab.windowId); return chooseTab(byFocusAndScriptable) || chooseTab(byScriptable) || chooseTab(byFocus) || activeTabs[0]; } export async function resolveTabForDirectAction(tabId, actionName) { if (tabId != null) { return chrome.tabs.get(tabId); } const allTabs = await chrome.tabs.query({}); if (allTabs.length !== 1) { throw new Error( `Refusing to ${actionName} without explicit tab ID when ${allTabs.length} tabs are open` ); } return allTabs[0]; } export async function resolveGroupId(nameOrId) { const asInt = parseInt(nameOrId); if (!isNaN(asInt)) return asInt; const groups = await chrome.tabGroups.query({}); const match = groups.find(g => g.title && g.title.toLowerCase() === String(nameOrId).toLowerCase()); if (!match) throw new Error(`No tab group found with name '${nameOrId}'`); return match.id; } export function buildTabBlocks(tabs) { const blocks = []; for (const tab of tabs) { const normalizedGroupId = tab.groupId >= 0 ? tab.groupId : null; const lastBlock = blocks[blocks.length - 1]; if (lastBlock?.groupId === normalizedGroupId) { lastBlock.tabIds.push(tab.id); lastBlock.endIndex = tab.index; continue; } blocks.push({ groupId: normalizedGroupId, startIndex: tab.index, endIndex: tab.index, tabIds: [tab.id], }); } return blocks; } export function getSessionTabs(session) { if (!session) return []; if (Array.isArray(session.tabs)) { return session.tabs .map(entry => typeof entry === "string" ? { url: entry } : entry) .filter(entry => entry?.url); } if (Array.isArray(session.urls)) { return session.urls.filter(Boolean).map(url => ({ url })); } return []; } export function normalizeGroupColor(color) { const allowed = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]); return allowed.has(color) ? color : "grey"; } export async function getAliases() { const { windowAliases } = await chrome.storage.local.get("windowAliases"); return windowAliases || {}; } export async function getSessions() { const { sessions } = await chrome.storage.local.get("sessions"); return sessions || {}; }