Skip to content

fix(ui): stop Settings, Agent Detail and the Dashboard header widening the page (#2197) - #2782

Merged
vybe merged 2 commits into
devfrom
fix/2197-body-horizontal-overflow
Sep 22, 2026
Merged

vybe merged 2 commits into
devfrom
fix/2197-body-horizontal-overflow

Conversation

@dolho

@dolho dolho commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2781 (refactor/1925-overflowtabs-adoption) → #2780 → #2778. Review the last commit, or rebase onto dev once the stack lands.

Fixes #2197

The report named the wrong element

The issue blames the tab strips, and they are not what was wrong. OverflowTabs was collapsing correctly the whole time, 375px included — its "More" trigger fits its container, asserted directly.

What the original sweep found instead was the primitive's hidden mirror row: a width: max-content measuring row inside a 0×0; overflow: hidden box. It is invisible to the user and to scrollWidth alike, but a probe that does not skip clipped subtrees reports it as by far the widest element on the page — which is how NAV.-mb-px.flex and BUTTON.ml-auto :: "More" ended up named as the worst offenders. My own first probe reproduced the same artifact before I filtered for clipping.

What actually widened the page

Page At 375px Cause
Agent Detail +496px three AgentHeader rows, each a non-wrapping flex row of two rigid clusters — identity + Workspace/Talk/Running, toggles + Tags + live CPU/MEM/uptime, lifetime cost. ~790px of min-content combined.
Settings +157px the Default Model <select>: flex-1 with the default min-width: auto. A select's min-content is its widest option (~417px), so one control scrolled the page. min-w-0 is the entire fix.

Both now wrap (flex-wrap + gap-y-*), with min-w-0 where a long name should truncate rather than push. space-x-* becomes gap-* on every row that can now wrap — a wrapped space-x row mis-indents every line after the first.

The Dashboard overlap is the same shape, one level up

#1830 made the stats cluster elastic so it degrades instead of being clipped. But elastic with no floor means it collapses to zero, so the pressure never reaches the controls and the row never wraps. Measured at 640px before the fix:

stats cluster   64→64   (width 0, scroll 78)   ← gave all its ground
controls        64→677  (width 613)            ← pinned at max-content, row ends at 609

So the stats cluster's clipped, invisible children still sat under the controls' first button — the 8px the spec reports — while the controls ran ~70px past the row's right edge. Three changes: the row wraps, the stats cluster carries the ladder's own agents-only floor (min-w-[4.5rem] ≈ 71px) so the overflow becomes a wrap, and the controls may shrink and wrap their own buttons instead of being pinned wider than the row.

Measured ladder after the fix — single row above ~900px, unchanged:

width header height stats controls past row edge overlap
1600 30px 813 692 0 0
1440 30px 653 692 0 0
1024 30px 237 692 0 0
900 30px 113 692 0 0
768 91px 673 673 0 0
640 91px 545 545 0 0

At 640 the stats cluster now gets the full width, so it shows more than before, not less.

Verification

e2e/dashboard-stats-overflow.spec.js (#1830) — red on dev (8px overlap, all three modes), green here.

New e2e/body-horizontal-overflow.spec.js measures the one thing the contract actually states — the document scroll width — across /settings, /, /operations, /library and a live agent's detail page at 375/640/768/1024, and on failure names the widest unclipped element. That is deliberate: an element-shaped assertion is exactly what produced a report naming the wrong element.

Proven to bite. With the three fixes reverted it fails with:

Settings @ 375px — body scrolls horizontally by 157px.
  Widest unclipped: SELECT.block.flex-1.px-3 +84px "Claude Opus 5 — Most cap" | BUTTON…"Save" +157px
Agent Detail @ 375px — body scrolls horizontally by 496px.
  Widest unclipped: DIV.flex.items-center.space-x-3 +331px "Workspace Talk Running"

And a vacuous arm was caught doing it. The widest markup on both pages is gated behind a later render — Settings' admin-only <select> behind /api/users/me, Agent Detail's live stats behind the stats poll — so on a fixed settle the Settings arm passed with its own fix reverted. The ready predicates are load-bearing, not politeness, and the negative control is what surfaced that, not reading the test.

Full sweep in both themes (dark class asserted) at 375 and 640: scrollWidth <= clientWidth on every page. Frontend unit suite 132 files / 2916 tests green. agent-detail-tabs-overflow.spec.js (#1114) and the dashboard grid/list/mode-switcher/type-filter/hotkey specs all pass.

One pre-existing failure, not from this stack

smoke.spec.js › @smoke api keys page loads fails on plain dev too (verified by building origin/dev and running it against the same backend) — /api-keys no longer renders the expected copy. Out of scope here; worth its own issue.

Note for the reviewer

The "collapsed tab strip fits its container at 375px" arm reads [data-measure-tab] on Settings, which only renders through OverflowTabs as of #2781. It is green in this stack and would time out if this PR were rebased ahead of that one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q19uRCksdn4DiRAJ55rfpZ

@dolho

dolho commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Review — /review pass

No critical findings. The strongest thing in this PR is that it disproves the issue's own diagnosis with a measurement rather than fixing what was reported: OverflowTabs was collapsing correctly the whole time, and the "worst offender" was the primitive's hidden mirror row inside a 0×0; overflow:hidden box — invisible to the user and to scrollWidth alike, and reported as the widest element by any probe that does not skip clipped subtrees. That correction is worth more than the fix.

Verified the ladder live rather than trusting the class names (grid mode, entitled build):

width header height stats controls past row edge overlap
1600–900 30px 813 → 113 692 0 0
768–640 91px full width full width 0 0

So "unchanged above ~900px" is measured, not asserted, and the wrap point is where the ladder actually runs out.

[I1] The min-w-[4.5rem] floor is a magic number one level away from the thing it mirrors (confidence 7/10)

The floor is "the ladder's own agents-only width (~71px)", but the ladder lives in @container statsbar rules in Dashboard.vue's <style> (330px / 200px thresholds) and the floor is a Tailwind arbitrary value in the class string. Nothing fails if one moves. It is the right value; consider a comment on the @container block pointing back at the floor, so the next person editing either sees the pair.

[I2] flex-wrap on justify-between changes alignment when it wraps (confidence 8/10, cosmetic)

Once AgentHeader row 1 wraps, the actions cluster is alone on its line, so justify-between degenerates to flex-start and the buttons left-align where they used to sit right. Visible in the 375px screenshots and arguably fine on a phone — but it is a real visual change beyond "nothing shifts", and justify-end on the wrapped arm (or ml-auto on the cluster) would keep the previous alignment if that is preferred.

Checked and clean

  • The spec measures the contract's actual statement — document scroll width — and names the widest unclipped element on failure. Given the issue's own mis-attribution, an element-shaped assertion would have been the wrong gate.
  • The ready predicates are load-bearing and proven so: the Settings arm passed with its own fix reverted on a fixed settle, because the admin-only <select> renders after /api/users/me. Catching that by running the negative control rather than re-reading the test is the right instinct, and the write-up says so.
  • Negative control reproduces the exact offenders (+157px naming the <select>, +496px naming the action cluster) — so the gate bites.
  • dashboard-stats-overflow.spec.js (bug: Dashboard stats bar — right controls overlap host telemetry below ~1170px effective width #1830) goes from red on dev to green here, which is the issue's stated regression gate.
  • Scope: 4 files, no unrelated churn.

Honest residual worth stating in the PR body

The sweep covers five pages. Agent Detail is fixed by fixing AgentHeader, but other agent-scoped tabs (Files, Schedules, Canvas) are not measured, so "the page body never scrolls" is proven for what is swept and assumed elsewhere.

@dolho
dolho force-pushed the fix/2197-body-horizontal-overflow branch from 336fb2f to f94b4c0 Compare September 14, 2026 14:42
@dolho
dolho force-pushed the refactor/1925-overflowtabs-adoption branch from 4e6fff4 to 4458af1 Compare September 18, 2026 08:48
@dolho
dolho force-pushed the fix/2197-body-horizontal-overflow branch from f94b4c0 to 14fc78f Compare September 18, 2026 08:48
@dolho
dolho marked this pull request as ready for review September 18, 2026 08:48
@dolho

dolho commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

/review — post-rebase (stacked on refactor/1925-overflowtabs-adoption)

Scope: CLEAN — the PR reframes the issue's diagnosis (rigid flex rows + a flex-1 <select> with min-width:auto, not the tab strips) with the mirror-row measurement artefact explained; a correction, not drift. Plan completion vs #2197: 4 done / 1 changed (AC2 — strips were never the cause) / AC5 both-themes manual.

Critical

None.

Informational

  • [I1] #2536 spec's premise no longer holds as written (8/10) — e2e/dashboard-mode-switcher.spec.js:8 still says "non-shrinking flex row (justify-between + flex-shrink-0)" while Dashboard.vue:68 is now flex flex-wrap items-center gap-2 min-w-0. The pinned-x invariant is single-line-scoped (holds ≥~900px, where the smoke runs); update the comment.
  • [I2] Regression spec is @interactive, so the merge gate never runs it (9/10) — consistent with the e2e policy; "red without the fix" is proven locally, not by CI. A vitest source guard (AgentHeader.vue rows carry flex-wrap; the Settings select carries min-w-0) would make it per-PR-gated.
  • [I3] justify-between on a wrapped row left-aligns the actions on line 2 (6/10) — AgentHeader.vue:60; cosmetic.

Clean

No palette classes added (min-w-[4.5rem], flex-wrap, gap-* only); 4.5rem tied to the @container statsbar (max-width: 200px) rung by comment; no v-html.

dolho and others added 2 commits September 22, 2026 18:35
…g the page (#2197)

Three rigid flex rows scrolled the page body below 1024px, against the
design-system rule that wide content scrolls inside its own container.

The report blamed the tab strips, and that is not what was wrong. `OverflowTabs`
was collapsing correctly the whole time, at 375px included: the hidden mirror row
it measures against lives in a 0x0 `overflow: hidden` box and is invisible to the
user and to `scrollWidth` alike, but a probe that does not skip clipped subtrees
reports it as the widest thing on the page — which is how a measurement sweep
came to name `NAV.-mb-px.flex` and the "More" button. Measured with clipped
subtrees excluded, the real offenders are:

* **Agent Detail** — three `AgentHeader` rows, each a non-wrapping flex row of
  two rigid clusters: identity + Workspace/Talk/Running, toggles + Tags + the
  live CPU/MEM/uptime readout, and the lifetime-cost line. Together ~790px of
  min-content, so the page widened at every width below ~1024px (+496px at 375).
* **Settings** — the Default Model `<select>`, `flex-1` with the default
  `min-width: auto`. A select's min-content is its widest OPTION ("Claude Opus 5
  — Most capable Opus (latest) (recommended)", ~417px), so one control scrolled
  the page by 157px at 375. `min-w-0` is the whole fix.

The Dashboard overlap is a different failure with the same root shape. #1830 made
the stats cluster elastic so it degrades instead of being clipped — but elastic
with no floor means it collapses to ZERO, so the pressure never reached the
controls and the row never wrapped. Measured at 640px: stats 0px wide (its
clipped children still sitting under the controls' first button — the 8px the
spec reports), controls pinned at their 692px max-content and running ~70px past
the row's right edge. The row now wraps, the stats cluster carries the ladder's
own agents-only floor (~71px) so the overflow becomes a wrap, and the controls
may shrink and wrap their own buttons rather than being pinned wider than the row.

Rendering above ~900px is unchanged — measured single 30px row at 1600/1440/1280/
1024/900, wrapping to 91px at 768 and below, zero overlap at every width.

`space-x-*` becomes `gap-*` on each row that can now wrap: a wrapped `space-x`
row mis-indents every line after the first.

Verified against a local dev instance in BOTH themes at 375 and 640 (light and
dark, `dark` class asserted): `scrollWidth <= clientWidth` on Settings, Agent
Detail, Dashboard, Operations and Library at 375/640/768/1024.

`e2e/dashboard-stats-overflow.spec.js` (#1830), red on `dev`, is green. The new
`e2e/body-horizontal-overflow.spec.js` measures the one thing the contract states
— the document scroll width — and names the widest UNCLIPPED element on failure,
because an element-shaped assertion is exactly what produced a report naming the
wrong element. Proven to bite: with these three fixes reverted it fails Settings
at +157px naming the `<select>`, and Agent Detail at +496px naming the action
cluster.

Its `ready` predicates are load-bearing, not politeness. The widest markup on
both pages is gated behind a later render — Settings' admin-only `<select>`
behind `/api/users/me`, Agent Detail's live stats behind the stats poll — and on
a fixed settle the Settings arm PASSED with the fix reverted. That vacuous green
was caught by running the negative control, not by reading the test.

Fixes #2197

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q19uRCksdn4DiRAJ55rfpZ
…ith the code (#2197) — mechanical, per the merge-train note on the PR

Dashboard.vue said the controls cluster is flex-shrink-0 (this PR removed it); AgentHeader.vue said min-w-0 truncates the name (the h1 carries no truncate). Prose only, no class changed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vybe
vybe changed the base branch from refactor/1925-overflowtabs-adoption to dev September 22, 2026 17:36
@vybe

vybe commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

merge-train: rebased onto dev and retargeted — mechanical. git rebase --onto origin/dev bb1b1018f (the pre-squash tip of #2781, merged as 10d3d8a); the single #2197 commit replayed cleanly. One prose-only commit on top, from validation: Dashboard.vue's Create-Agent rationale still said the controls cluster is flex-shrink-0 (this PR removed it), and AgentHeader.vue's row-1 note said min-w-0 truncates the name (the <h1> carries no truncate). Both comments now describe the code; no class changed. Left as-is, advisory: row 2's ml-4 / mx-4 inside the now-wrapping cluster can indent a wrapped second line — the space-x→gap class this PR fixed elsewhere.

@vybe
vybe force-pushed the fix/2197-body-horizontal-overflow branch from 69c677e to c35446d Compare September 22, 2026 17:36

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: validated (lane B), rebased onto dev, all checks green on the retargeted run.

@vybe
vybe merged commit 48a80d4 into dev Sep 22, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants