// @ts-nocheck import { executeScript, getActiveTab, getAliases, getLargeOperationThrottle, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl, resolveTabForDirectAction, runLargeOperation, tabInfo, throwIfJobCancelled, updateJobProgress, yieldForLargeOperation } from '../core'; export async function tabsList() { const windows = await chrome.windows.getAll({ populate: true }); const aliases = await getAliases(); const tabs = []; for (const w of windows) { for (const t of w.tabs) { tabs.push({ ...tabInfo(t), windowAlias: aliases[t.windowId] || null, pinned: t.pinned, favIconUrl: t.favIconUrl, }); } } return tabs; } export async function tabsClose({ tabId, inactive, duplicates, gentleMode, __job } = {}) { return runLargeOperation("tabs.close", async () => { let toClose = []; if (duplicates) { const all = await chrome.tabs.query({}); const seen = new Set(); for (const t of all) { if (!t.url) continue; if (seen.has(t.url)) toClose.push(t.id); else seen.add(t.url); } } else if (inactive) { const all = await chrome.tabs.query({}); toClose = all.filter(t => !t.active).map(t => t.id); } else if (tabId) { toClose = [tabId]; } const throttle = await getLargeOperationThrottle(toClose.length, gentleMode); updateJobProgress(__job, { phase: "closing tabs", current: 0, total: toClose.length }); for (let i = 0; i < toClose.length; i += throttle.batchSize) { throwIfJobCancelled(__job); await chrome.tabs.remove(toClose.slice(i, i + throttle.batchSize)); updateJobProgress(__job, { phase: "closing tabs", current: Math.min(i + throttle.batchSize, toClose.length), total: toClose.length }); await yieldForLargeOperation(i + throttle.batchSize, throttle.batchSize, throttle.pauseMs); } return { closed: toClose.length, gentle: throttle.gentle, audible: throttle.audible }; }); } export async function tabsMove({ tabId, groupId, windowId, index, forward, backward }) { const moveProps = {}; if (windowId != null) moveProps.windowId = windowId; if (forward || backward) { const tab = await chrome.tabs.get(tabId); if (forward) moveProps.index = tab.index + 2; // +2 because Chrome shifts after removal else moveProps.index = Math.max(0, tab.index - 1); } else if (index != null) { moveProps.index = index; } else { moveProps.index = -1; } await chrome.tabs.move(tabId, moveProps); if (groupId != null) { await chrome.tabs.group({ tabIds: [tabId], groupId }); } return { tabId }; } export async function tabsActive({ tabId }) { const tab = await chrome.tabs.get(tabId); await chrome.windows.update(tab.windowId, { focused: true }); await chrome.tabs.update(tabId, { active: true }); return { tabId }; } export async function tabsActiveInWindow({ windowId }) { const activeTabs = await chrome.tabs.query({ windowId, active: true }); const tab = activeTabs[0]; if (!tab) { throw new Error(`No active tab found for window ${windowId}`); } return tabInfo(tab); } export async function tabsStatus({ tabId }) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); return tabInfo(tab); } export async function tabsFilter({ pattern }) { const all = await chrome.tabs.query({}); return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo); } export async function tabsCount({ pattern }) { const all = await chrome.tabs.query({}); if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length; return all.length; } export async function tabsQuery({ search }) { const q = search.toLowerCase(); const all = await chrome.tabs.query({}); return all.filter(t => (t.url && t.url.toLowerCase().includes(q)) || (t.title && t.title.toLowerCase().includes(q)) ).map(tabInfo); } export async function tabsHtml({ tabId }) { for (let i = 0; i < 3; i++) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const tabUrl = tab.url || tab.pendingUrl || ""; if (isBrowserErrorUrl(tabUrl)) { return ""; } if (!isScriptableUrl(tabUrl)) { throw new Error(`Cannot get HTML of ${tabUrl} — navigate to a regular web page first`); } try { const results = await executeScript({ target: { tabId: tab.id }, func: () => document.documentElement.outerHTML, }); return results[0]?.result || ""; } catch (e) { if (isErrorPageScriptError(e)) return ""; const transient = e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")); if (i < 2 && transient) { await new Promise(r => setTimeout(r, 300)); continue; } throw e; } } } export async function tabsDedupe(args = {}) { return tabsClose({ ...args, duplicates: true }); } export async function tabsSort({ by, gentleMode, __job } = {}) { return runLargeOperation("tabs.sort", async () => { const windows = await chrome.windows.getAll({ populate: true }); let moved = 0; const totalTabs = windows.reduce((sum, w) => sum + (w.tabs?.length || 0), 0); updateJobProgress(__job, { phase: "sorting tabs", current: 0, total: totalTabs }); for (const w of windows) { const sorted = [...w.tabs].sort((a, b) => { if (by === "title") return (a.title || "").localeCompare(b.title || ""); if (by === "time") return a.id - b.id; // lower id = opened earlier // domain (default) const da = new URL(a.url || a.pendingUrl || "about:blank").hostname; const db = new URL(b.url || b.pendingUrl || "about:blank").hostname; return da.localeCompare(db); }); if (w.tabs.every((tab, index) => tab.id === sorted[index]?.id)) continue; const throttle = await getLargeOperationThrottle(sorted.length, gentleMode); const moveBatchSize = Math.max(1, Math.min(10, throttle.batchSize)); for (let i = 0; i < sorted.length; i++) { throwIfJobCancelled(__job); await chrome.tabs.move(sorted[i].id, { index: i }); moved++; updateJobProgress(__job, { phase: "sorting tabs", current: moved, total: totalTabs }); await yieldForLargeOperation(moved, moveBatchSize, throttle.pauseMs); } } return { moved }; }); } export async function tabsMergeWindows({ gentleMode, __job } = {}) { return runLargeOperation("tabs.merge_windows", async () => { const current = await chrome.windows.getCurrent(); const all = await chrome.windows.getAll({ populate: true }); let moved = 0; const totalTabs = all.filter(w => w.id !== current.id).reduce((sum, w) => sum + (w.tabs?.length || 0), 0); updateJobProgress(__job, { phase: "merging windows", current: 0, total: totalTabs }); for (const w of all) { if (w.id === current.id) continue; const ids = w.tabs.map(t => t.id); const throttle = await getLargeOperationThrottle(ids.length, gentleMode); for (let i = 0; i < ids.length; i += throttle.batchSize) { throwIfJobCancelled(__job); const chunk = ids.slice(i, i + throttle.batchSize); await chrome.tabs.move(chunk, { windowId: current.id, index: -1 }); moved += chunk.length; updateJobProgress(__job, { phase: "merging windows", current: moved, total: totalTabs }); await yieldForLargeOperation(moved, throttle.batchSize, throttle.pauseMs); } } return { moved }; }); } export async function tabsPin({ tabId }) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); await chrome.tabs.update(tab.id, { pinned: true }); return { tabId: tab.id, pinned: true }; } export async function tabsUnpin({ tabId }) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); await chrome.tabs.update(tab.id, { pinned: false }); return { tabId: tab.id, pinned: false }; } export async function tabsScreenshot({ tabId, format = "png", quality } = {}) { let windowId; if (tabId) { const tab = await chrome.tabs.get(tabId); await chrome.tabs.update(tabId, { active: true }); windowId = tab.windowId; } else { const tab = await getActiveTab(); windowId = tab.windowId; } const opts = { format }; if (format === "jpeg" && quality != null) opts.quality = quality; const dataUrl = await chrome.tabs.captureVisibleTab(windowId, opts); return { dataUrl, format }; } export async function tabsWatchUrl({ pattern, timeout = 30000, tabId } = {}) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const deadline = Date.now() + timeout; const regex = new RegExp(pattern); let lastUrl = tab.url || tab.pendingUrl || ""; let lastStatus = tab.status || "unknown"; const matches = (url) => { regex.lastIndex = 0; return Boolean(url && regex.test(url)); }; if (matches(lastUrl)) return tabInfo(tab); while (Date.now() < deadline) { const t = await chrome.tabs.get(tab.id); lastUrl = t.url || t.pendingUrl || ""; lastStatus = t.status || "unknown"; if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t); if (isBrowserErrorUrl(t.url || "")) { throw new Error(`Tab ${tab.id} is showing an error page while waiting for URL to match '${pattern}'`); } await new Promise(r => setTimeout(r, 200)); } throw new Error(`Tab ${tab.id} URL did not match '${pattern}' within ${timeout}ms (last URL: '${lastUrl}', status: '${lastStatus}')`); } export async function tabsMute({ tabId }) { const tab = await resolveTabForDirectAction(tabId, "mute"); await chrome.tabs.update(tab.id, { muted: true }); return { tabId: tab.id, muted: true }; } export async function tabsUnmute({ tabId }) { const tab = await resolveTabForDirectAction(tabId, "unmute"); await chrome.tabs.update(tab.id, { muted: false }); return { tabId: tab.id, muted: false }; }