From f418c70a2eb2f093ddadf0d0261fd7186a040d06 Mon Sep 17 00:00:00 2001 From: "tushar.muralidharan" Date: Mon, 2 Mar 2026 17:29:00 +1100 Subject: [PATCH 1/2] feat: add tab group support (skip collapsed groups, zc/zj/zk navigation) --- background_scripts/all_commands.js | 24 ++++++++++++++++++ background_scripts/commands.js | 3 +++ background_scripts/main.js | 40 +++++++++++++++++++++++++++++- manifest.json | 1 + 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/background_scripts/all_commands.js b/background_scripts/all_commands.js index 607fb73a7..f8579a695 100644 --- a/background_scripts/all_commands.js +++ b/background_scripts/all_commands.js @@ -493,6 +493,30 @@ const allCommands = [ noRepeat: true, }, + { + name: "collapseTabGroup", + desc: "Collapse current tab's group", + group: "tabs", + background: true, + noRepeat: true, + }, + + { + name: "previousTabGroup", + desc: "Switch to previous tab group", + group: "tabs", + background: true, + noRepeat: true, + }, + + { + name: "nextTabGroup", + desc: "Switch to next tab group", + group: "tabs", + background: true, + noRepeat: true, + }, + { name: "removeTab", desc: "Close current tab", diff --git a/background_scripts/commands.js b/background_scripts/commands.js index faaf58497..5c2679fa9 100644 --- a/background_scripts/commands.js +++ b/background_scripts/commands.js @@ -482,6 +482,9 @@ const defaultKeyMappings = { "zi": "zoomIn", "zo": "zoomOut", "z0": "zoomReset", + "zc": "collapseTabGroup", + "zj": "previousTabGroup", + "zk": "nextTabGroup", // Marks "m": "Marks.activateCreateMode", diff --git a/background_scripts/main.js b/background_scripts/main.js index 7d37e6b1b..3dfa9402a 100644 --- a/background_scripts/main.js +++ b/background_scripts/main.js @@ -340,6 +340,22 @@ const BackgroundCommands = { }); }, toggleMuteTab, + // Tab group commands (Chrome only; no-op on Firefox). + async collapseTabGroup({ tab }) { + if (bgUtils.isFirefox() || !chrome.tabGroups || tab.groupId == -1) return; + const tabs = await chrome.tabs.query({ currentWindow: true }); + let nextTab = tabs.find((t) => t.index > tab.index && t.groupId != tab.groupId) || + tabs.findLast((t) => t.index < tab.index && t.groupId != tab.groupId); + if (!nextTab) nextTab = await chrome.tabs.create({}); // All tabs are in this group. + await chrome.tabs.update(nextTab.id, { active: true }); + chrome.tabGroups.update(tab.groupId, { collapsed: true }); + }, + previousTabGroup({ tab }) { + return goToTabGroup(tab, -1); + }, + nextTabGroup({ tab }) { + return goToTabGroup(tab, 1); + }, moveTabLeft: moveTab, moveTabRight: moveTab, @@ -464,11 +480,33 @@ async function removeTabsRelative(direction, { count, tab }) { await chrome.tabs.remove(toRemove.map((t) => t.id)); } +// Jump to the next (direction=1) or previous (direction=-1) tab group. +async function goToTabGroup(tab, direction) { + if (bgUtils.isFirefox() || !chrome.tabGroups) return; + const tabs = await chrome.tabs.query({ currentWindow: true }); + const inDifferentGroup = (t) => t.groupId != -1 && t.groupId != tab.groupId; + const target = direction > 0 + ? tabs.find((t) => t.index > tab.index && inDifferentGroup(t)) + : tabs.findLast((t) => t.index < tab.index && inDifferentGroup(t)); + if (target) { + await chrome.tabGroups.update(target.groupId, { collapsed: false }); + chrome.tabs.update(target.id, { active: true }); + } +} + // Selects a tab before or after the currently selected tab. // - direction: "next", "previous", "first" or "last". function selectTab(direction, { count, tab }) { - chrome.tabs.query(visibleTabsQueryArgs, function (tabs) { + chrome.tabs.query(visibleTabsQueryArgs, async function (tabs) { if (tabs.length > 1) { + // On Chrome, skip tabs in collapsed tab groups. + if (!bgUtils.isFirefox() && chrome.tabGroups) { + const groups = await chrome.tabGroups.query({ windowId: tab.windowId, collapsed: true }); + const collapsedGroupIds = new Set(groups.map((g) => g.id)); + tabs = tabs.filter((t) => t.id === tab.id || !collapsedGroupIds.has(t.groupId)); + if (tabs.length <= 1) return; + } + const toSelect = (() => { switch (direction) { case "next": diff --git a/manifest.json b/manifest.json index 783852cbb..754d7a3f3 100644 --- a/manifest.json +++ b/manifest.json @@ -25,6 +25,7 @@ ], "permissions": [ "tabs", + "tabGroups", "bookmarks", "history", "storage", From 26e0661390157e9b23b4efb7abce229a53213068 Mon Sep 17 00:00:00 2001 From: "tushar.muralidharan" Date: Mon, 4 May 2026 02:47:55 +1000 Subject: [PATCH 2/2] feat: enable tab group commands on Firefox via feature detection --- background_scripts/main.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/background_scripts/main.js b/background_scripts/main.js index 3dfa9402a..0a790f1a1 100644 --- a/background_scripts/main.js +++ b/background_scripts/main.js @@ -340,14 +340,15 @@ const BackgroundCommands = { }); }, toggleMuteTab, - // Tab group commands (Chrome only; no-op on Firefox). async collapseTabGroup({ tab }) { - if (bgUtils.isFirefox() || !chrome.tabGroups || tab.groupId == -1) return; + if (!chrome.tabGroups || tab.groupId == -1) return; const tabs = await chrome.tabs.query({ currentWindow: true }); let nextTab = tabs.find((t) => t.index > tab.index && t.groupId != tab.groupId) || tabs.findLast((t) => t.index < tab.index && t.groupId != tab.groupId); - if (!nextTab) nextTab = await chrome.tabs.create({}); // All tabs are in this group. - await chrome.tabs.update(nextTab.id, { active: true }); + if (!nextTab && !bgUtils.isFirefox()) { + nextTab = await chrome.tabs.create({}); + } + if (nextTab) await chrome.tabs.update(nextTab.id, { active: true }); chrome.tabGroups.update(tab.groupId, { collapsed: true }); }, previousTabGroup({ tab }) { @@ -482,7 +483,7 @@ async function removeTabsRelative(direction, { count, tab }) { // Jump to the next (direction=1) or previous (direction=-1) tab group. async function goToTabGroup(tab, direction) { - if (bgUtils.isFirefox() || !chrome.tabGroups) return; + if (!chrome.tabGroups) return; const tabs = await chrome.tabs.query({ currentWindow: true }); const inDifferentGroup = (t) => t.groupId != -1 && t.groupId != tab.groupId; const target = direction > 0 @@ -499,8 +500,8 @@ async function goToTabGroup(tab, direction) { function selectTab(direction, { count, tab }) { chrome.tabs.query(visibleTabsQueryArgs, async function (tabs) { if (tabs.length > 1) { - // On Chrome, skip tabs in collapsed tab groups. - if (!bgUtils.isFirefox() && chrome.tabGroups) { + // Skip tabs in collapsed tab groups. + if (chrome.tabGroups) { const groups = await chrome.tabGroups.query({ windowId: tab.windowId, collapsed: true }); const collapsedGroupIds = new Set(groups.map((g) => g.id)); tabs = tabs.filter((t) => t.id === tab.id || !collapsedGroupIds.has(t.groupId));