Skip to content

feat: tab group Vomnibar (zg / ZG), multi-tab selection, color swatches#4914

Open
MarvinHauke wants to merge 12 commits into
philc:masterfrom
MarvinHauke:feature/tab-groups
Open

feat: tab group Vomnibar (zg / ZG), multi-tab selection, color swatches#4914
MarvinHauke wants to merge 12 commits into
philc:masterfrom
MarvinHauke:feature/tab-groups

Conversation

@MarvinHauke

Copy link
Copy Markdown

Related

Builds on / complements #4858, which adds zc/zj/zk navigation and skipping of collapsed groups. This PR adds higher-level group management via the Vomnibar and visual multi-tab selection.

New commands

Key Command Description
ZG Vomnibar.activateTabGroupSelection Open Vomnibar to navigate to a tab group
zg Vomnibar.activateGroupAssign Assign highlighted tab(s) to a group
za collapseTabGroup Collapse / uncollapse current tab's group
zn / zN nextTabGroup / previousTabGroup Cycle between groups
zz / ZZ selectNextTabForGroup / selectPreviousTabForGroup Visual multi-tab selection (Vim visual-mode semantics)

Feature details

ZG — navigate to a tab group

Opens 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)

  1. Vomnibar lists all existing groups. Type to filter by name or color.
    • Enter on an exact name match → assigns all highlighted tabs to that group immediately.
    • Enter on a non-matching name → proceeds to step 2 with the typed name.
    • Select "Create …" from dropdown → same as above.
  2. Vomnibar lists the 9 Chrome tab group colors. Selecting one creates a new named, colored group containing the highlighted tabs.

zz / ZZ — multi-tab visual selection

Vim visual-mode semantics: the current tab is the anchor.

  • zz extends the selection right (or shrinks from the left if already extended left).
  • ZZ extends the selection left (or shrinks from the right).

Use together with zg to 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

  • All group completers use chrome.windows.getLastFocused() for the window ID — WINDOW_ID_CURRENT and currentWindow: true are unreliable in MV3 background service workers.
  • Suggestion has a new groupData class field (required because the constructor calls Object.seal(this)).
  • MultiCompleter.filter's empty-query guard is extended with a showResultsWithNoQuery opt-in flag used by all three group completers.
  • Tab group logic lives in background_scripts/tab_groups.js; completers in background_scripts/completion/group_completer.js.

🤖 Generated with Claude Code

tushar.muralidharan and others added 10 commits June 14, 2026 23:58
- 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>
MarvinHauke and others added 2 commits June 15, 2026 00:26
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant