LearnNewsExamplesServices
Frontmatter
id19982
titleDock maximize leaves the tab overflow partitioned for the old width
stateClosed
labels
bugaidashboard
assigneesneo-opus-ada
createdAtOct 22, 2026, 11:03 AM
updatedAtOct 22, 2026, 4:48 PM
githubUrlhttps://github.com/neomjs/neo/issues/19982
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 22, 2026, 4:48 PM

Dock maximize leaves the tab overflow partitioned for the old width

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

Context

Found in manual testing of apps/workstation on dev (2026-10-22). @tobiu reproduced it, and his screenshots of the three states follow in a comment.

  1. In the shipped layout, the pane with the Runtime Health Envelope tab holds 12 tabs. At its default width only the active header fits, and the rest sit behind the ⋯ overflow action.
  2. Click into the pane body. Focus reveals the focus-gated header actions. Then press maximize.
  3. The pane fills the workspace, but its header shows 2 tabs, and most of the strip stays empty.
  4. Activate the second tab. The header re-partitions, and 8 tabs show.

Steps 1–4 are observed. The stale partition also reproduces headless (Chromium 1440×900, 8bb3574d0f, whose src/ matches dev): 1.5 s after maximize, the header still shows 2 of the 12 tabs (#19984's probe). The mechanism below is read from source, and the e2e criterion measures it.

The Problem

The overflow partition is runtime projection state. It is recomputed only when one of its triggers fires: an owner resize, an activation, or a tab-set change. Maximize does fire resize, but the pass it triggers measures the strip in the middle of the maximize motion:

  • Maximize#applyPresentation() writes the workspace rect through wrapperStyle. The node's layout box grows, and the resize DOM event fires.
  • playFlip() then runs DockFlip.play({geometryOnly: true}). That installs an inverse translate(…) scale(sx, sy), so the node visually starts from its old rect, and then transitions the transform to identity.
  • Overflow#project() reads the strip extent through getDomRect(), which is getBoundingClientRect(), the visual box. A pass that lands inside that window reads a transform-scaled extent, close to the pre-maximize width, and partitions for it.
  • A transform settling never changes the layout box, so no further resize fires. Nothing re-projects until another trigger arrives, which is the activation in step 4.

Inferred and still to be confirmed by the e2e:

  • Restore: the mirror case. The pass measures at an inverse scale above 1, so the shrunken pane could keep more headers than fit.
  • Other dock operations: every DockFlip-animated operation that resizes a tabs node opens the same window.

The Architectural Reality

  • src/tab/plugin/Overflow.mjs: onResize() calls project(). project() step 2 measures owner.getDomRect([owner, ...actions, control]) and derives extent from those visual rects. The plugin has no motion awareness. The dock routes the overflow's own start/idle into MotionSignal (LayoutAdapter, overflowProjectionStart / overflowProjectionIdle), but nothing routes a settled motion back into a re-projection.
  • src/dashboard/dock/plugin/Maximize.mjs: applyPresentation() sets wrapperStyle, awaits promiseUpdate(), then calls playFlip().
  • src/main/addon/DockFlip.mjs: play() inverts with translate() scale() and resolves after the transition.
  • Design authority: the JSDoc of Neo.main.DomAccess#getLayoutRect: "Layout consumers that persist sizes (grid column generation, buffered mounting math) must read the LAYOUT box instead, which transforms never affect." Its examples name the DockFlip inverse-transform window. The grid moved to Component#getLayoutRect() in #16375 (grid/Container.mjs, grid/header/Toolbar.mjs), and so did DockSplitter. The tab Overflow plugin still reads visual rects, and its @summary collapses tabs only "when headers exceed the available main-axis extent". A stale partition hides tabs that fit.

The Fix

Make the overflow partition transform-immune. In Overflow#project(), read the natural widths (step 1) and the strip extent (step 2) through Component#getLayoutRect() instead of getDomRect(). The resize pass that maximize already triggers then partitions against the final layout box, wherever it lands in the motion. The same change covers restore and every other DockFlip window. That is why the reader is the right place to fix this, not each motion owner.

Trap: getLayoutRect() x/y values are offset-parent-relative. The coordinate arm (actionStart − ownerStart) is only valid when both nodes share an offset parent. Otherwise use the size arm (ownerSize − actionSize) alone, which is already the ceiling.

Acceptance Criteria

  • After a maximize settles (dock motion signal and overflow projection both idle), a fresh overflow projection leaves the maximized header's partition unchanged. Every tab that fits the strip is visible.
  • The same holds after restore: a fresh projection on the restored pane changes nothing, and no header extends under the action cluster.
  • Overflow#project() measures with getLayoutRect(), never with visual rects.
  • E2E (the reproduction): a new whitebox Neural Link spec, test/playwright/e2e/workstation/WorkstationMaximizeTabOverflowNL.spec.mjs, drives the real animated maximize and then restore on the shipped layout's 12-tab pane. After each settles, it compares the visible-header set with the set a forced re-projection produces. It must be red on dev, where the base run shows the stale partition rather than just a precondition, and green with the fix. It joins the hosted Workstation NL tier (#19655).
  • WorkstationTabOverflowCapNL, WorkstationFocusedTabOverflowNL and WorkstationNL ("maximize uses measured workspace geometry…") stay green.

Out of Scope

  • Revealing the focus-gated header actions on hover: #19983.
  • Overflow-control visuals and the packing algorithm (Overflow.computeOverflow). The decision is correct; its input is wrong.

Avoided Traps

  • Re-projecting from Maximize after playFlip() settles. This fixes one motion owner and leaves restore and every other DockFlip consumer with the same misread.
  • Debouncing onResize past the motion. This adds a timing guess, and the transform window has no fixed length when frames are starved.

Related

  • #16375: the grid's transform-immune measurement, the precedent.
  • #19655: the hosted Workstation NL tier the new spec joins.
  • #17928: the dock maximize affordance.
  • #19984: the same maximized pane, where the overflow menu closes as it opens (a different cause).

Live latest-open sweep: checked latest 20 open issues at 2026-10-22T09:01:52Z; no equivalent found. A2A claim sweep: last 15 messages; no overlapping lane-claim. MC sweep: "tab header overflow not updated after maximize…", 6 results, no prior decision (#14771 / #16424 / #17928 are adjacent, not the same). Own-assignment sweep: 6 open, none overlapping.

Origin Session ID: 70e59a84-efd6-4e7a-9262-c5478bd314ff Retrieval Hint: "tab overflow stale after dock maximize, DockFlip inverse transform, getDomRect vs getLayoutRect"

tobiu
tobiu Oct 22, 2026, 11:07 AM
Image
Image
Image
  • 2026-10-22T09:19:27Z @neo-opus-ada cross-referenced by #19984
  • 2026-10-22T09:26:36Z @neo-opus-ada cross-referenced by #19985
  • 2026-10-22T09:38:37Z @neo-opus-ada cross-referenced by #19986
  • 2026-10-22T09:54:06Z @neo-opus-ada cross-referenced by #19987
  • 2026-10-22T10:06:29Z @neo-opus-ada cross-referenced by #19989
  • 2026-10-22T10:18:24Z @neo-opus-ada cross-referenced by #19990
  • 2026-10-22T10:38:02Z @neo-opus-ada cross-referenced by #19991
  • 2026-10-22T10:45:48Z @neo-opus-ada cross-referenced by #19992
  • 2026-10-22T10:53:07Z @neo-opus-ada cross-referenced by #19993
  • 2026-10-22T11:52:12Z @neo-opus-ada cross-referenced by PR #19996
  • 2026-10-22T13:15:29Z @neo-opus-ada cross-referenced by PR #19997
tobiu referenced in commit a117b6f - "fix(tab): the tab overflow partitions a maximized pane for its settled size (#19982) (#19996) on Oct 22, 2026, 4:48 PM
tobiu closed this issue on Oct 22, 2026, 4:48 PM