feat: tab group Vomnibar (zg / ZG), multi-tab selection, color swatches#4914
Open
MarvinHauke wants to merge 12 commits into
Open
feat: tab group Vomnibar (zg / ZG), multi-tab selection, color swatches#4914MarvinHauke wants to merge 12 commits into
MarvinHauke wants to merge 12 commits into
Conversation
- za: collapse current tab group (was zc) - zn/zN: next/previous tab group with circular wrap (were zk/zj) - goToTabGroup now wraps around to the first/last group when no target exists in the current direction - add .serena to .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, << and >> (moveTabLeft/moveTabRight) moved a tab by a fixed number of positions in the tab strip, ignoring tab group structure entirely. This rewrite introduces group-aware movement where tab groups behave as atomic blocks: - Moving INTO an open group: if the neighboring tab belongs to an expanded group, the tab joins that group via chrome.tabs.group(). Chrome keeps the tab at its current adjacent position and adds it as a member, so no explicit repositioning is needed. - Moving past a collapsed group: if the neighboring tab belongs to a collapsed group, the tab skips over the entire group and lands immediately on the far side. The target index is computed as afterGroup.index - direction to account for the index shift that occurs when Chrome removes the tab from its original position before reinserting it. - Exiting a group at its boundary: if the active tab is the first/last member of a group and the user presses << or >> toward the boundary, chrome.tabs.ungroup() removes it from the group without any additional move call — the tab stays at its current index, which is already outside the remaining group members. - Moving within a group (not at a boundary): behaves identically to the previous implementation, shifting one position at a time. For counts > 1 (e.g. 3>>) the logic is applied iteratively, re-fetching the tab after each step to get the updated index. Fallback: pinned tabs and environments where chrome.tabGroups is unavailable (e.g. older browsers) retain the original flat-index logic unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…leter.js
## Motivation
main.js was growing too large as tab group features accumulated. All group-related
background logic is now consolidated in a dedicated module, following the same pattern
used by marks.js, exclusions.js, and tab_operations.js.
## Changes
### background_scripts/tab_groups.js (new)
Exports four public BackgroundCommand handlers:
- collapseTabGroup: collapse the current tab's group, moving focus to an adjacent tab
- previousTabGroup / nextTabGroup: circular navigation between groups
- moveTab: group-aware implementation of << and >> (replaces the inline version in main.js)
Two private helpers (goToTabGroup, moveTabOneStep) remain internal to the module.
The only external dependency is bg_utils.js (for isFirefox()). Circular imports are
avoided by inlining the two main.js helpers that were previously needed:
- visibleTabsQueryArgs → { currentWindow: true } (pinned tabs are never hidden)
- getTabIndex(tab, tabs) → tabs.findIndex((t) => t.id === tab.id) (id-based, safer)
### background_scripts/completion/group_completer.js (new)
Implements TabGroupCompleter following the Completer contract from completers.js
(filter({ queryTerms }) → Suggestion[]). Queries chrome.tabGroups for the current
window, maps each group to a Suggestion with tabId set to the first tab in the group
so Vomnibar can jump to it on selection. Falls back gracefully when chrome.tabGroups
is unavailable (Firefox without tab group support).
### background_scripts/main.js
- Added imports for tab_groups.js and group_completer.js
- Added tabGroups entry to completionSources and completers (key: "tabGroups")
- Delegated collapseTabGroup, previousTabGroup, nextTabGroup, moveTabLeft, moveTabRight
to the tabGroups module
- Removed the now-redundant inline implementations of moveTab, moveTabOneStep,
collapseTabGroup, previousTabGroup, nextTabGroup, goToTabGroup (~80 lines removed)
### content_scripts/vomnibar.js
Added activateTabGroupSelection() method with completer: "tabGroups", selectFirst: true.
### background_scripts/all_commands.js
Added Vomnibar.activateTabGroupSelection command definition.
### background_scripts/commands.js
Added "ZG" key binding for Vomnibar.activateTabGroupSelection.
Note: ZG is a stub — the full group management UI (creating groups, adding selected
tabs to groups) will be wired up in a follow-up commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces two new background commands that extend Chrome's native tab
highlighting to allow selecting multiple tabs at once — a prerequisite for
the upcoming "group selected tabs" feature (zg).
## How it works
Chrome maintains a "highlighted" state on tabs independently of the active
tab. chrome.tabs.highlight({ windowId, tabs: [indices] }) replaces the
highlighted set while keeping the specified active tab as the first entry.
The highlighted tabs appear visually selected (blue) in Chrome's native tab
strip, with no content-script UI changes needed.
## Behavior
ctrl+shift+j (bound as <c-J>): adds the tab immediately after the highest
currently-highlighted index to the selection. Stops at the last tab.
ctrl+shift+k (bound as <c-K>): adds the tab immediately before the lowest
currently-highlighted index to the selection. Stops at the first tab.
The active tab index is always kept in the highlighted set as the first
element (Chrome requirement — removing it from the set would change the
active tab).
## Files
- background_scripts/tab_groups.js: selectNextTabForGroup,
selectPreviousTabForGroup exported and implemented
- background_scripts/main.js: two new BackgroundCommands entries
- background_scripts/all_commands.js: two new command definitions
- background_scripts/commands.js: <c-J> and <c-K> key bindings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ctrl+shift+j (<c-J>) is intercepted by Chrome on Windows and Linux as the DevTools Console shortcut, preventing Vimium's content script from ever seeing the keydown event. alt+shift+j/k (<a-J> / <a-K>) is not reserved by Chrome on any platform and reliably reaches the content script. Key encoding in Vimium: shift on a single-char key uppercases the char (j → J), and alt adds the "a" modifier prefix → <a-J>. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rebind selectNextTabForGroup to `zz` and selectPreviousTabForGroup to `ZZ` (was <a-J>/<a-K>, which Karabiner Elements intercepts on macOS). Both shortcuts now sit cleanly in the z-prefix group alongside za/zn/zN. Rewrite both functions with vim visual-mode semantics: tab.index is the fixed anchor; the selection cursor extends or shrinks relative to it. - zz: extends right if right side is selected; shrinks left if left side is selected; starts extending right if nothing is selected yet. - ZZ: extends left if left side is selected; shrinks right if right side is selected; starts extending left if nothing is selected yet. Also switches chrome.tabs.query from currentWindow:true to windowId:tab.windowId for reliable results from MV3 service workers, and shortens the command descriptions to "Select next/previous tab". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## New commands
- `ZG` — navigate to a tab group: opens Vomnibar listing all groups in
the current window; selecting one uncollapses the group and switches
to its first tab.
- `zg` — assign highlighted tabs to a group: two-step Vomnibar flow:
1. Shows all existing groups; type to filter or enter a name.
- Enter on an exact name match → assigns highlighted tabs to that group.
- Enter on a non-matching name → moves to step 2 (color picker).
2. Shows the 9 Chrome tab group colors; selecting one creates a new
named, colored group containing the highlighted tabs.
Selecting an existing group directly from the dropdown assigns tabs
immediately without opening step 2.
## Completers (group_completer.js)
- `TabGroupCompleter` (ZG): lists groups with `showResultsWithNoQuery`
so they appear as soon as the Vomnibar opens.
- `TabGroupAssignCompleter` (zg step 1): same, plus emits a trailing
"Create…" suggestion when the query is non-empty.
- `TabGroupColorCompleter` (zg step 2): lists all 9 Chrome colors with
`showResultsWithNoQuery`.
- All three use `chrome.windows.getLastFocused()` for the window ID —
`WINDOW_ID_CURRENT` / `currentWindow: true` are unreliable in MV3
background service workers.
- Every suggestion pre-sets `s.html` so `generateHtml()` returns it
directly, embedding a colored swatch (`<span class="group-color-swatch">`)
inline-styled with the actual Chrome group color hex.
## Suggestion class (completers.js)
- Declared `groupData` as a class field alongside the existing `html`,
`searchUrl`, etc. — required because the constructor calls
`Object.seal(this)`, preventing ad-hoc property additions.
- Extended `MultiCompleter.filter`'s empty-query guard to also allow
completers that set `showResultsWithNoQuery = true`, not just
`TabCompleter`.
## Vomnibar page (vomnibar_page.js)
- `VomnibarUI` constructor: added `this.pendingGroupName` to carry the
group name across the two-step flow (survives `reset()`, which does
not clear it).
- `handleEnterKey`: smart Enter for `tabGroupAssign` — exact title
match assigns; anything else enters step 2 (color picker).
- `handleOpenRequested`: when the selected completion is a "createGroup"
action, switches completer to `groupColors` in-place instead of
closing the Vomnibar.
- `openCompletion`: dispatches on `groupData.action` first
(`addToGroup`, `setColor`), then falls back to `tabId != null` for
tab switching (replacing the fragile `description == "tab"` check that
broke ZG), then URL launch.
## Background (main.js)
- Added `addTabsToGroup` handler: groups all highlighted tabs in the
focused window into an existing group.
- Added `createTabGroupWithColor` handler: groups highlighted tabs into
a new group and sets its title and color.
- `selectSpecificTab`: uncollapses the tab's group before switching to
it, so the ZG destination is always visible.
## Wiring (mode_normal.js, vomnibar.js, commands.js, all_commands.js)
- Added `Vomnibar.activateTabGroupSelection` and
`Vomnibar.activateGroupAssign` to the manually maintained
`NormalModeCommands` dispatch table — both were missing, causing `ZG`
and `zg` to silently do nothing.
- Registered both methods on the `Vomnibar` object in `vomnibar.js`.
- Bound `zg` key in `commands.js` and declared both commands in
`all_commands.js`.
## CSS (vomnibar_page.css)
- Added `.group-color-swatch`: 10×10px rounded square, vertically
centered, right margin 5px. Background is always set inline from the
`COLOR_CSS` map in `group_completer.js`, so swatches work in both
light and dark Vomnibar themes without any media query override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the two most logic-heavy parts of the tab group feature: - tests/unit_tests/tab_groups_test.js (10 cases) selectNextTabForGroup (zz) and selectPreviousTabForGroup (ZZ): extend, shrink, and no-op behavior for all anchor/selection combos. - tests/unit_tests/group_completer_test.js (16 cases) TabGroupCompleter: empty-query results, title/color filtering, tabId assignment, color swatch hex in html, unnamed group fallback title. TabGroupAssignCompleter: no Create entry on empty query, Create entry appended on non-empty query, correct groupId, filtering by title. TabGroupColorCompleter: all 9 colors returned, prefix filtering, setColor action on every result, GROUP_COLORS order preserved, swatch hex in html. Also removes the redundant bare import of completers.js in main.js (already loaded by the named import below it) and expands the tab group completer import to multi-line style for consistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3964651 to
b11ffcb
Compare
Previously, pressing >> on a free tab when a collapsed group was the last item in the tab bar silently did nothing (the function returned early because there was no tab after the group to anchor against). The user had to manually uncollapse the group, then shift through every tab inside it to reach the end — unintuitive. The fix: when afterGroup is undefined (group is flush against the window edge), move the tab to the edge group tab's index instead of returning. The index arithmetic is the same as the non-edge case; the group shifts by one to accommodate the incoming tab. Open-group behaviour (>> absorbs the tab into the group) is unchanged. Also adds two unit tests covering both the right-edge and left-edge cases via the public moveTab function. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3zz / 3ZZ now extend or shrink the visual tab selection by N tabs in one keystroke, matching the existing count behavior of >> / <<. Remove noRepeat from both commands so the count prefix reaches the handler, then loop the single-step logic N times via private helper functions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related
Builds on / complements #4858, which adds
zc/zj/zknavigation and skipping of collapsed groups. This PR adds higher-level group management via the Vomnibar and visual multi-tab selection.New commands
ZGVomnibar.activateTabGroupSelectionzgVomnibar.activateGroupAssignzacollapseTabGroupzn/zNnextTabGroup/previousTabGroupzz/ZZselectNextTabForGroup/selectPreviousTabForGroupFeature details
ZG— navigate to a tab groupOpens the Vomnibar listing all groups in the current window. Selecting one uncollapses the group and switches to its first tab. Groups appear immediately without needing to type (no-query results).
zg— assign highlighted tabs to a group (two-step flow)zz/ZZ— multi-tab visual selectionVim visual-mode semantics: the current tab is the anchor.
zzextends the selection right (or shrinks from the left if already extended left).ZZextends the selection left (or shrinks from the right).Use together with
zgto assign multiple tabs to a group in one flow.Color swatches in Vomnibar
Group suggestions show a small colored square next to the color name so groups and colors are visually distinguishable at a glance — both in the group list (ZG / zg step 1) and the color picker (zg step 2). Works in light and dark Vomnibar themes.
Implementation notes
chrome.windows.getLastFocused()for the window ID —WINDOW_ID_CURRENTandcurrentWindow: trueare unreliable in MV3 background service workers.Suggestionhas a newgroupDataclass field (required because the constructor callsObject.seal(this)).MultiCompleter.filter's empty-query guard is extended with ashowResultsWithNoQueryopt-in flag used by all three group completers.background_scripts/tab_groups.js; completers inbackground_scripts/completion/group_completer.js.🤖 Generated with Claude Code