From f0c2278e58e3d1f22b31573b6987e3bd8b7c4047 Mon Sep 17 00:00:00 2001 From: Nikola Pajic <2182745+pajcho@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:08:30 +0200 Subject: [PATCH 1/2] Skip auto-closing tabs in excluded tab groups Adds a per-group exclusion list and a global "Exclude all tab groups" toggle to the Tab Cleaner page. Resolves group titles only when needed during cleanup, and refreshes the popup list live when groups are created, renamed, or removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- background.js | 62 ++++++++++++++++++++++++++------ manifest.json | 2 +- popup.html | 29 +++++++++++++++ popup.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 11 deletions(-) diff --git a/background.js b/background.js index 3573677..9f2bd35 100644 --- a/background.js +++ b/background.js @@ -44,8 +44,19 @@ chrome.alarms.create("tabCleanup", { periodInMinutes: CHECK_INTERVAL_MIN }); chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name !== "tabCleanup") return; - const { exclusions = [], enabled = true, timeoutMin = DEFAULT_TIMEOUT_MIN } = - await chrome.storage.local.get(["exclusions", "enabled", "timeoutMin"]); + const { + exclusions = [], + enabled = true, + timeoutMin = DEFAULT_TIMEOUT_MIN, + excludeAllGroups = false, + excludedGroupTitles = [], + } = await chrome.storage.local.get([ + "exclusions", + "enabled", + "timeoutMin", + "excludeAllGroups", + "excludedGroupTitles", + ]); if (!enabled) return; @@ -53,6 +64,23 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { const timeoutMs = timeoutMin * 60 * 1000; const tabs = await chrome.tabs.query({}); + // Resolve titles for any groups containing tabs (only when needed) + const groupTitleById = {}; + if (excludeAllGroups || excludedGroupTitles.length) { + const groupIds = new Set(); + for (const tab of tabs) { + if (typeof tab.groupId === "number" && tab.groupId !== -1) { + groupIds.add(tab.groupId); + } + } + for (const gid of groupIds) { + try { + const g = await chrome.tabGroups.get(gid); + groupTitleById[gid] = g.title || ""; + } catch {} + } + } + // Never close the last tab in a window const windowTabCounts = {}; for (const tab of tabs) { @@ -97,6 +125,15 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { } catch {} } + // Skip tabs in excluded groups + if (typeof tab.groupId === "number" && tab.groupId !== -1) { + if (excludeAllGroups) continue; + const title = groupTitleById[tab.groupId]; + if (title !== undefined && excludedGroupTitles.includes(title)) { + continue; + } + } + // Check inactivity const lastActive = tabActivity[tab.id] || 0; if (now - lastActive >= timeoutMs) { @@ -213,13 +250,18 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // Set defaults on install chrome.runtime.onInstalled.addListener(() => { - chrome.storage.local.get(["enabled", "timeoutMin", "exclusions"], (data) => { - const defaults = {}; - if (data.enabled === undefined) defaults.enabled = true; - if (data.timeoutMin === undefined) defaults.timeoutMin = DEFAULT_TIMEOUT_MIN; - if (data.exclusions === undefined) defaults.exclusions = []; - if (Object.keys(defaults).length) { - chrome.storage.local.set(defaults); + chrome.storage.local.get( + ["enabled", "timeoutMin", "exclusions", "excludeAllGroups", "excludedGroupTitles"], + (data) => { + const defaults = {}; + if (data.enabled === undefined) defaults.enabled = true; + if (data.timeoutMin === undefined) defaults.timeoutMin = DEFAULT_TIMEOUT_MIN; + if (data.exclusions === undefined) defaults.exclusions = []; + if (data.excludeAllGroups === undefined) defaults.excludeAllGroups = false; + if (data.excludedGroupTitles === undefined) defaults.excludedGroupTitles = []; + if (Object.keys(defaults).length) { + chrome.storage.local.set(defaults); + } } - }); + ); }); diff --git a/manifest.json b/manifest.json index f891098..1a5afa6 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "🚀 SuperLevels", "version": "2.0", "description": "The super Chrome extension by @levelsio — dark mode, GDPR cookie dismisser, YouTube unhook, tab cleaner, cookie editor, redirect tracer, live CSS editor, X/Twitter dim mode, JS toggle, JSON formatter, music recognizer, picture-in-picture, Google Maps links, and view image.", - "permissions": ["tabs", "storage", "alarms", "cookies", "activeTab", "webRequest", "webNavigation", "tabCapture", "contentSettings", "scripting"], + "permissions": ["tabs", "tabGroups", "storage", "alarms", "cookies", "activeTab", "webRequest", "webNavigation", "tabCapture", "contentSettings", "scripting"], "host_permissions": [""], "background": { "service_worker": "background.js" diff --git a/popup.html b/popup.html index f555a2e..cf7b865 100644 --- a/popup.html +++ b/popup.html @@ -123,6 +123,26 @@ font-size: 16px; cursor: pointer; padding: 0 4px; line-height: 1; } .exc-list .item button:hover { color: #ff6b81; } + .group-list.dimmed { opacity: 0.5; pointer-events: none; } + .group-list .group-info { + display: flex; align-items: center; gap: 8px; + flex: 1; min-width: 0; + } + .group-list .group-color { + width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; + } + .group-list .group-title { + flex: 1; min-width: 0; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } + .group-list .group-title.untitled { color: #666; font-style: italic; } + .group-list .item { padding: 5px 10px; } + .group-list .switch { width: 32px; height: 18px; flex-shrink: 0; } + .group-list .slider { border-radius: 18px; } + .group-list .slider::before { + width: 12px; height: 12px; left: 3px; bottom: 3px; + } + .group-list .switch input:checked + .slider::before { transform: translateX(14px); } .closed-section { margin-top: 14px; } .closed-header { display: flex; align-items: center; justify-content: space-between; @@ -766,6 +786,15 @@

Excluded Hosts

+

Excluded Tab Groups

+
+ + +
+
diff --git a/popup.js b/popup.js index ebd1d1f..853633e 100644 --- a/popup.js +++ b/popup.js @@ -93,6 +93,103 @@ function renderExclusionList(exclusions) { }); } +// ── Tab Group Exclusions ── +const excludeAllGroupsEl = document.getElementById("excludeAllGroups"); +const groupListEl = document.getElementById("groupList"); + +const GROUP_COLOR_HEX = { + grey: "#5f6368", + blue: "#1a73e8", + red: "#d93025", + yellow: "#f9ab00", + green: "#1e8e3e", + pink: "#d01884", + purple: "#a142f4", + cyan: "#007b83", + orange: "#fa903e", +}; + +chrome.storage.local.get(["excludeAllGroups", "excludedGroupTitles"], (data) => { + excludeAllGroupsEl.checked = !!data.excludeAllGroups; + applyDimmed(); + renderGroupList(data.excludedGroupTitles || []); +}); + +function refreshGroupList() { + chrome.storage.local.get(["excludedGroupTitles"], (data) => { + renderGroupList(data.excludedGroupTitles || []); + }); +} + +if (chrome.tabGroups) { + chrome.tabGroups.onCreated.addListener(refreshGroupList); + chrome.tabGroups.onUpdated.addListener(refreshGroupList); + chrome.tabGroups.onRemoved.addListener(refreshGroupList); +} + +excludeAllGroupsEl.addEventListener("change", () => { + chrome.storage.local.set({ excludeAllGroups: excludeAllGroupsEl.checked }, applyDimmed); +}); + +function applyDimmed() { + groupListEl.classList.toggle("dimmed", excludeAllGroupsEl.checked); +} + +async function renderGroupList(excluded) { + let visible = []; + try { + if (chrome.tabGroups && chrome.tabGroups.query) { + visible = await chrome.tabGroups.query({}); + } + } catch {} + + // Only titled groups can be excluded individually; the global toggle catches untitled ones. + const visibleTitled = visible.filter((g) => g.title && g.title.trim()); + const visibleTitles = new Set(visibleTitled.map((g) => g.title)); + const orphans = excluded.filter((t) => t && !visibleTitles.has(t)); + const hasUntitled = visible.some((g) => !g.title || !g.title.trim()); + + if (!visibleTitled.length && !orphans.length) { + let msg = "No tab groups found"; + if (hasUntitled) msg += " — name your groups to exclude individually"; + groupListEl.innerHTML = `
${msg}
`; + return; + } + + const renderRow = (title, color, isExcluded) => `
+
+ + ${esc(title)} +
+ +
`; + + const visibleHtml = visibleTitled + .map((g) => renderRow(g.title, GROUP_COLOR_HEX[g.color] || "#888", excluded.includes(g.title))) + .join(""); + const orphanHtml = orphans.map((t) => renderRow(t, "#444", true)).join(""); + + groupListEl.innerHTML = visibleHtml + orphanHtml; + + groupListEl.querySelectorAll('input[type="checkbox"][data-title]').forEach((cb) => { + cb.addEventListener("change", () => { + const title = cb.dataset.title; + chrome.storage.local.get(["excludedGroupTitles"], (data) => { + let list = data.excludedGroupTitles || []; + if (cb.checked) { + if (!list.includes(title)) list = [...list, title]; + } else { + list = list.filter((t) => t !== title); + } + chrome.storage.local.set({ excludedGroupTitles: list }, () => renderGroupList(list)); + }); + }); + }); +} + // ── Closed Tabs History ── const closedSection = document.getElementById("closedSection"); From bd8a3599f28f61b35ecfe949bdd6e4f9b21ba086 Mon Sep 17 00:00:00 2001 From: Nikola Pajic <2182745+pajcho@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:12:57 +0200 Subject: [PATCH 2/2] Add top margin above Excluded Tab Groups heading Co-Authored-By: Claude Opus 4.7 (1M context) --- popup.html | 1 + 1 file changed, 1 insertion(+) diff --git a/popup.html b/popup.html index cf7b865..d08903e 100644 --- a/popup.html +++ b/popup.html @@ -123,6 +123,7 @@ font-size: 16px; cursor: pointer; padding: 0 4px; line-height: 1; } .exc-list .item button:hover { color: #ff6b81; } + .exc-list + h2 { margin-top: 14px; } .group-list.dimmed { opacity: 0.5; pointer-events: none; } .group-list .group-info { display: flex; align-items: center; gap: 8px;