feat: add performance controls for large browser ops

- Add throttled large-operation handling for tab, group, and session commands.
- Introduce performance profiles, audible-tab aware gentle mode, and job progress tracking.
- Support background session restores with status/cancel commands and lazy placeholders.
- Expose new perf and extension CLI groups plus matching Python SDK methods.
- Preserve pinned tabs during session snapshots and debounce auto-save updates.
- Bump browser-cli and extension versions to 0.10.0 and add pytest-cov to dev deps.
- Add coverage for performance controls, background jobs, lazy restores, and tab metadata.
This commit is contained in:
2026-05-20 22:13:57 +02:00
parent e1e4adbb25
commit 545abeb515
18 changed files with 1054 additions and 148 deletions
+78 -48
View File
@@ -1,5 +1,5 @@
// @ts-nocheck
import { executeScript, getActiveTab, getAliases, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl, resolveTabForDirectAction, tabInfo } from '../core';
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();
@@ -17,24 +17,33 @@ export async function tabsList() {
return tabs;
}
export async function tabsClose({ tabId, inactive, duplicates }) {
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);
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];
}
} else if (inactive) {
const all = await chrome.tabs.query({});
toClose = all.filter(t => !t.active).map(t => t.id);
} else if (tabId) {
toClose = [tabId];
}
if (toClose.length) await chrome.tabs.remove(toClose);
return { closed: toClose.length };
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 }) {
@@ -127,41 +136,62 @@ export async function tabsHtml({ tabId }) {
}
}
export async function tabsDedupe() {
return tabsClose({ duplicates: true });
export async function tabsDedupe(args = {}) {
return tabsClose({ ...args, duplicates: true });
}
export async function tabsSort({ by }) {
const windows = await chrome.windows.getAll({ populate: true });
let moved = 0;
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);
});
for (let i = 0; i < sorted.length; i++) {
await chrome.tabs.move(sorted[i].id, { index: i });
moved++;
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 };
return { moved };
});
}
export async function tabsMergeWindows() {
const current = await chrome.windows.getCurrent();
const all = await chrome.windows.getAll({ populate: true });
let moved = 0;
for (const w of all) {
if (w.id === current.id) continue;
const ids = w.tabs.map(t => t.id);
await chrome.tabs.move(ids, { windowId: current.id, index: -1 });
moved += ids.length;
}
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 }) {