Skip to content

feat(tabs): grouped vertical rail from owner tab layouts - #517

Merged
Ark0N merged 1 commit into
Ark0N:masterfrom
aakhter:pr/grouped-vertical-rail
Oct 4, 2026
Merged

Ark0N merged 1 commit into
Ark0N:masterfrom
aakhter:pr/grouped-vertical-rail

Conversation

@aakhter

@aakhter aakhter commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

This is the first of a short stack that gives the vertical tab rail named groups, built on the owner tab-layout API that already shipped as backend only (GET/PUT /api/tab-layout).

It adds a small pure module, tab-layout-browser.js, that reads GET /api/tab-layout, projects the owner's groups onto the live sessions and open web tabs, and renders them as collapsible sections in the vertical rail. The browser never writes the layout in this PR. Creating, renaming and moving tabs between groups come in follow-ups.

No change for existing users. Grouping only applies when the rail is vertical and the owner has at least one group. With no layout, a failed read, zero groups, the horizontal strip, phones or the sidebar, the rail markup is byte-identical to today. A test renders the old and new code side by side and compares them.

Grouping only changes how rows are drawn.

  • sessionOrder, Alt+N, Ctrl+Tab and the palette keep reading the server-projected order.
  • Each grouped row reuses the flat row's markup, so its badge still names its Alt+N key.
  • In webview-tabs.js the only change is a small extracted renderWebviewTab(), with identical markup.

Collapse is a per-device preference in localStorage. A collapsed group always keeps the active row visible, selecting a hidden session reveals just that row, and lineage arcs to a hidden session anchor to its group header.

Drag-reorder is off in the grouped rail until grouped editing lands, because a flat-order drop can't express a move between groups.

There are no server changes, no new settings and no new SSE events. The page refreshes the layout on the existing tab:layoutChanged event (owner-scoped, newest version wins).

Tests

  • Two new test files cover the projection, the markup, collapse storage, load sequencing and the app integration, including the byte-identity check.
  • @vitest-environment jsdom currently fails on master (No such built-in module: node:), so the rail test runs the real modules in an explicit JSDOM window rather than relying on it.
  • typecheck, lint, format, frontend-syntax, public-assets, browser-excludes and build all pass, plus 28 related test files.
  • Checked in Chromium against an isolated instance: flat rail with no groups; a group created through the API appears live over SSE; collapse persists across reload; removing the group returns the exact flat markup.

Open questions for review

  • With the default activity sort, rows sort within each group, so Alt+N badges may not count 1, 2, 3 down a group.
  • Group headers are plain buttons for now; tree/treeitem semantics come in the next slice.

The vertical tab rail now reads the owner's tab layout (GET /api/tab-layout)
and draws its groups as collapsible sections. This is the first frontend
consumer of the tab-layout backend and it is read-only: nothing in the
browser writes the layout yet.

- tab-layout-browser.js (new, pure, loaded before app.js): projects the
  layout onto the live sessions and open web tabs, renders the grouped
  markup, stores collapse per device, and sequences loads newest-wins with
  a bounded retry on failure.
- app.js: loads the layout on init and on tab:layoutChanged, renders the
  grouped rail from the same per-row markup the flat rail uses, falls
  through to a full render whenever the grouping structure changes, and
  withholds drag-reorder in the grouped rail.
- Grouping is opt-in by construction. With no layout, a failed read, a
  layout without groups, or a horizontal strip, the rail renders exactly
  as before (byte-identical markup).
- Grouping is a render layer only: sessionOrder, Alt+N, Ctrl+Tab and the
  palette keep reading the server-projected order, and row badges keep
  their Alt+N slot.
- A collapsed group still shows the active row; lineage arcs to a hidden
  session anchor to its group header.
- webview-tabs.js: renderWebviewTab() extracted so a single web tab can be
  placed into its group with unchanged markup.
@aakhter

aakhter commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

A quick map for when you get to this one, since it now heads a short stack. Each PR adds one commit on top of the previous one:

  1. feat(tabs): grouped vertical rail from owner tab layouts #517 (this one): read-only grouped rail from the existing tab-layout API. No server changes, and no change at all for users without groups.
  2. feat(tabs): full-row activation and tree semantics for the grouped rail #519: full-row activation pinned in Chromium, plus ARIA tree semantics for the grouped rail.
  3. feat(tabs): edit groups in the vertical rail #525 (draft): editing groups from the browser (create, rename, move, reorder, delete, drag).
  4. fix(tabs): grouped rail interaction fixes for inline rename #526 (draft): inline rename write ordering and long-prefix layout fixes.

#525 and #526 are drafts only because GitHub currently won't let my account open regular PRs or mark drafts ready. Both are ready for review. Reviewing in order makes the most sense, and I'll rebase the rest onto master as each one lands.

@Ark0N
Ark0N merged commit 23d145b into Ark0N:master Oct 4, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 4, 2026
…d touch fixes (#517, #519 review)

Maintainer merge-time fixes for the grouped vertical rail (#517) and its
tree semantics (#519), from the two PR reviews.

#517 minors
- A collapsed group hid rows that need the user with no signal on its
  header. The header now takes the most urgent alert among the session
  rows its collapse hides, in the tab alert language (tab-alert-action
  red ring, tab-alert-idle yellow ring, the existing ::before rules
  extended to the header). New pure hiddenGroupAlerts() over a per-section
  `hidden` list; _syncTabGroupHeaderAlerts() patches it on BOTH render
  paths, since alerts change without a rebuild. The kept selection draws
  its own ring and is not counted.
- Every layout read rebuilt the whole tab strip, and failed reads retried
  every 5 s forever. _applyTabLayout() now rebuilds only when the
  structure key changed. The key drops the layout version (bumped on
  every session create/close and order PUT) and instead carries group
  names and the rows each collapse hides, so a version bump that moves
  nothing costs nothing and a rename still rebuilds. The load coordinator
  backs off (5, 10, 20, 40 s, capped at 60 s) and stops after 4 retries;
  the next SSE init or tab:layoutChanged tries again, a success resets.
- A malformed stored collapse value disabled collapse on that device for
  good. A parse or shape error now reads as nothing collapsed and is
  rewritten to []; ok:false stays reserved for a store that throws.
- Ctrl+Shift+{ / } still reordered across groups, where the server
  re-ranks per group, sends no session:orderChanged and leaves this
  client's sessionOrder and Alt+N targets diverged. The move is now a
  no-op unless the neighbour is in the active session's own section
  (_canSwapActiveTabWith, reading the projection's new sectionByRef, which
  also covers rows a collapse hides). Within a group the swap still works
  and the server agrees with it; the flat rail and the strip are
  unchanged.

#517 nits
- Keyboard group toggle dropping focus: already fixed by #519's
  focus-by-identity; the Enter toggle test now pins focus on the header.
- Header <button> inside role=tablist: moot, #519 made the header a
  treeitem inside role=tree.
- Byte-identity test not comparing against master: skipped in the suite
  (a test cannot read another revision's files portably). Checked by
  hand instead: the flat strip and flat rail markup of this branch before
  and after this commit are identical in all 16 cases (both orientations,
  manual and activity sort, no layout and zero groups, full and
  incremental paths).
- Doubled blank line in docs/architecture-invariants.md: removed.

#519 minors
- A tap on a tree header or unselected row dismissed the touch keyboard:
  the roving tabindex parks those at -1, so the [tabindex] arm of
  MOBILE_KEYBOARD_DISMISS_EXEMPT_SELECTOR missed them. The selector now
  lists [role="treeitem"].
- The tree key handler acted on keys pressed on a focused control inside
  a row (Enter on the overflow button re-selected and reloaded the active
  session instead of reopening its menu). It now returns unless the key
  landed on the treeitem itself.

#519 nits
- aria-posinset/setsize went stale when the activity-sorted grouped rail
  re-sorted rows on the incremental path. The position pass is extracted
  (_applyTabTreePositions) and re-run, with aria-selected and the header
  alerts, at the end of the incremental branch while the rail is a tree.
- An expanded group with no open rows was announced as an expanded parent
  owning an empty group. A group with no open rows is now a tree leaf: no
  aria-expanded, no aria-owns, its rows container presentation; Left and
  Right do nothing on it, and its chevron keys off the section's
  collapsed class instead of aria-expanded.

Tests: tab-layout-browser (malformed storage, backoff with a bounded
drain, structure key, hidden alerts, leaf groups, sectionByRef),
tab-layout-rail (header alerts on both paths, render-on-change, backoff
without rebuilds, malformed storage, Ctrl+Shift section gate, in-row
control keys, leaf header keys, posinset after an incremental re-sort,
the dismiss selector matching tree items), and three new Chromium tests
in tab-activation.browser (Enter on a focused overflow button, the touch
keyboard staying up on tree taps, the collapsed header's red ring). Every
new test fails on the pre-fix sources. Docs: architecture-invariants
owner-tab-layouts and keyboard-dismissal sections, one clause in
CLAUDE.md's dismissal rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 4, 2026
…#520, #521, #522, #523, #524, #530, #531)

One consolidated minor changeset with the Thanks block first; the four contributor changesets (#520, #521, #522, #523) are folded into it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Merged and shipped in 1.34.0, together with #519. Thanks @aakhter! Giving the tab-layout API its first frontend without changing a single byte of the flat rail, and proving it with a side-by-side render, made this safe to land, as did the stack map you left.

I applied the review items while landing (7917273):

  • A collapsed group's header now shows the red or yellow ring of the most urgent hidden row, using the existing tab alert styles, so a row that needs you is never hidden silently.
  • Layout reads rebuild the rail only when something it draws changed (group names, or which rows a collapse hides), not on every version bump. Failed reads back off at 5, 10, 20 and 40 s and then stop, and the next reconnect or tab:layoutChanged tries again.
  • A malformed stored collapse value now reads as nothing collapsed and is rewritten, instead of disabling collapse on that device for good.
  • Ctrl+Shift+{ / } in the grouped rail moves a tab only within its own group, which the server accepts. Drag stays off.
  • The doubled blank line in the invariants doc is gone. The keyboard-toggle focus nit was already fixed by feat(tabs): full-row activation and tree semantics for the grouped rail #519, and now has an assertion.

#525 and #526 are next in the queue. A rebase onto master is welcome whenever you have time.

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.

2 participants