LearnNewsExamplesServices
Frontmatter
id19987
titleReordering tabs flashes every hidden tab into the header strip
stateClosed
labels
bugaidashboard
assigneesneo-opus-ada
createdAtOct 22, 2026, 11:54 AM
updatedAtOct 22, 2026, 6:34 PM
githubUrlhttps://github.com/neomjs/neo/issues/19987
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 22, 2026, 6:34 PM

Reordering tabs flashes every hidden tab into the header strip

Closed Backlog/active-chunk-10 bugaidashboard
neo-opus-ada
neo-opus-ada commented on Oct 22, 2026, 11:54 AM

Context

Found in manual testing of apps/workstation on dev (2026-10-22): resorting tab headers in the pane that holds 12 tabs and an overflow ⋯. In @tobiu's screen recording, two drops (at 32.4 s and 47.5 s) are followed by a 200–250 ms transient. The pane's header spans the whole frame with most of the 12 tabs in one row, the pane body goes black, and the neighbouring 100k Matrix pane disappears. Then the layout snaps back. The flicker and reflow reach well outside the pane being dragged in.

Further frames from the same recording, all inside one tab-drag operation (@tobiu's screenshots follow in a comment):

  • At about 36 s: the header spans the full width with six tabs and the overflow ⋯ in one row. Its band styling is gone and the pane body is black.
  • Another moment: the header looks normal ("Workspace Topology Inspector | Priority Alert Observatory"), but the neighbouring 100k Matrix grid is black.

Measurements

Headless Chromium, 2200×1000, at 8bb3574d0f (its src/, apps/ and resources/scss/ are identical to dev at 84b711878a). A requestAnimationFrame sampler records the strip, its tab buttons, the pane and the 100k Matrix pane every frame.

gesture frames after release with > 4 tab buttons laid out when tab right edge
in-strip swap (2nd tab 180 px left) 3 +95 … +211 ms x 3032, in a strip of x 1237–1863
a drag with a 45 px rise out of the strip and back (2nd or 3rd tab) 3–5 in each of 4 runs +91 … +224 ms x 3032
drag into the pane body (no tab-set change) 0 — —

In those frames every one of the 12 tab buttons is in flow (position: relative) inside a 626 px strip. In this layout the strip, pane and grid rects stay unchanged, so headless it is a flash inside the strip.

The out-of-pane effects in the recording are operator-observed, not reproduced headless: the strip running over the neighbour, the band styling lost, the body and the neighbouring grid going black. Reading the frames, they are the same measuring window in a layout where the restored tabs overflow past the pane, and the grid blackout is the grid re-rendering after that reflow. That reading is inferred.

The Problem

A reorder changes the tab set, so the overflow plugin recaptures natural widths. Overflow#project(true) step 1 puts every withheld header back into the live strip: setSilent({hidden: false}), delete vdom.removeDom, then owner.promiseUpdate(). It measures them with getDomRect, measures the strip extent in a second round trip, and only then applies the split again. Those are separate App↔main round trips, so the browser paints the restored state in between. The measuring window is visible for several frames on every reorder.

The plugin's own @summary says overflowing tabs "collapse behind one control whose menu reaches them". For the duration of the recapture they do not, and the user sees all of them.

The Architectural Reality

  • src/tab/plugin/Overflow.mjs: onTabSetChange() runs project(true) for insert/moveTo. project() step 1 restores the withheld headers (comment: "Restore them under this method's measuring latch, then reconcile their closest common parent once so getDomRect observes natural geometry"), and step 3 / applySplit withholds them again.
  • The natural-width cache is keyed by button id (naturalWidths[button.id]).
  • Precedent for a measuring frame becoming visible: #19094 (a newborn split's overflow control flashed for one frame).

The Fix

Measure natural widths without painting them in the strip:

  1. No live re-insert. During the measuring window, restored headers take position: absolute; visibility: hidden (out of flow, unpainted, still at max-content width), or are measured in an off-strip layer. The live strip never shows them and never reflows.
  2. Recapture only what is missing. A reorder changes order, not widths, and the cache is keyed by button id. Recapture only ids the cache lacks, such as newly inserted or re-created buttons. Check whether the dock's re-projection re-creates tab buttons on a reorder before relying on this.

Acceptance Criteria

  • After a tab reorder, no frame lays out more tab buttons in the strip than the settled partition shows. No tab button paints outside the strip box at any point between release and settle.
  • Inserting a tab still measures its natural width correctly: the partition after insert matches a fresh projection.
  • E2E: a new spec under test/playwright/e2e/workstation/ (e.g. WorkstationTabReorderNoFlashNL.spec.mjs) drives an in-strip reorder with a real pointer in the 12-tab pane and samples every frame (requestAnimationFrame) from release to settle. It asserts the visible-tab count never exceeds the settled count, and that the strip, pane and 100k Matrix rects stay stable. It must be red on dev (3+ frames with 12 tabs in the strip) and green with the fix, and it joins the hosted Workstation tier (#19655).
  • WorkstationTabOverflowCapNL, WorkstationFocusedTabOverflowNL and the overflow unit specs stay green.
  • Post-merge, on the operator's setup: the recording's gesture shows no header band loss and no body or neighbour-grid blackout. If either persists with the measuring window fixed, it is a separate mechanism (e.g. the dock re-projecting the pane on drop). It then gets its own ticket, carrying the conditions that reproduce it, rather than reopening this one.

Out of Scope

  • Headers leaving the strip during the drag: #19988, a separate mechanism.
  • #19982, the stale partition after maximize.

Related

  • #19094 (closed): a measuring frame made visible, the overflow-control case.
  • #19982, #19984, #19986: the same testing pass, the same plugin and pane.

Live latest-open sweep: checked latest 20 open issues at 2026-10-22T09:52:58Z; no equivalent found. Keyword sweep: "tab reorder flicker overflow", "overflow recapture flash", "tab drag header top-left", "sort zone header flies"; closed adjacent only (#19094, #16424, #19533). A2A claim sweep: no overlapping lane-claim. MC sweep: "tab header resorting flickers on drop…", 6 results, no prior decision. Own-assignment sweep: none overlapping.

Origin Session ID: 70e59a84-efd6-4e7a-9262-c5478bd314ff Retrieval Hint: "tab reorder flashes all hidden tabs, Overflow recapture measuring window painted, removeDom restore"

tobiu referenced in commit ceb791a - "fix(tab): a tab reorder no longer paints the headers the overflow withholds (#19987) (#20008) on Oct 22, 2026, 6:34 PM
tobiu closed this issue on Oct 22, 2026, 6:34 PM