Skip to content

fix(web): settle the resting composer layout with a pixel of slack - #9482

Merged
juliusmarminge merged 1 commit into
pingdotgg:mainfrom
matheustimbo:upstream-fix/settle-resting-composer-layout
Sep 4, 2026
Merged

fix(web): settle the resting composer layout with a pixel of slack#9482
juliusmarminge merged 1 commit into
pingdotgg:mainfrom
matheustimbo:upstream-fix/settle-resting-composer-layout

Conversation

@matheustimbo

@matheustimbo matheustimbo commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What Changed

resolveRestingComposerControlsLayout takes an optional previous layout and applies 1px of hysteresis in the re-showing direction only. Hiding a block stays unconditional — the controls would clip otherwise. ChatComposer passes the current layout through.

3 files, +107/-4.

Why

The composer re-measures its resting controls on every render (useLayoutEffect(measure), no dependency array), and the model picker's natural width is recovered from a truncated label whose scrollWidth is integral while the rendered box is fractional. With the host sitting exactly on a threshold, that sub-pixel difference flipped a block in and out of the overflow menu on every render until React gave up with "Maximum update depth exceeded" (error #185) while opening a thread.

Requiring real slack before a block comes back makes that noise unable to drive the layout.

Follow-up to #9393, which removed the strip-reservation feedback path but left this one — it runs entirely inside the composer's own measure -> resolve -> render cycle.

Fixes #9481.

The new test settles when the measured picker width jitters below a pixel feeds alternating 149 / 149.5 measurements and asserts the layout converges: 2 distinct layouts before the fix, 1 after. The existing threshold tests call the resolver without previous and are unchanged.

UI Changes

None visible. A block now needs 1px more room before it leaves the overflow menu.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes (no visual change)
  • I included a video for animation/interaction changes (not applicable)

Note

Low Risk
Localized layout math and state threading in the composer measure loop; behavior change is a 1px margin when controls leave overflow, with no auth or data impact.

Overview
Fixes an infinite re-render loop when opening a thread by stabilizing resting composer overflow layout against sub-pixel measurement jitter.

resolveRestingComposerControlsLayout now accepts an optional previous layout. Hiding controls into overflow stays immediate (to avoid clipping). Bringing blocks back from overflow—or making the cluster visible again—requires about 1px of slack (RESTING_CONTROLS_SLACK_PX) so threshold flicker from the model picker’s fractional vs integral width does not flip hiddenCount every render.

ChatComposer passes the current layout into the resolver on each measure. New unit tests cover hysteresis thresholds, no-previous behavior, sub-pixel jitter convergence, and cluster visibility slack.

Reviewed by Cursor Bugbot for commit ec1d9c1. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add 1px slack hysteresis to resolveRestingComposerControlsLayout to prevent control oscillation

  • Introduces RESTING_CONTROLS_SLACK_PX (1px) in composerFooterLayout.ts; the resolver now requires one pixel of spare host width before restoring previously overflowed blocks or re-showing a previously hidden cluster.
  • resolveRestingComposerControlsLayout accepts an optional previous layout and retains the prior hidden count when the newly expanded layout does not clear the slack margin. Calls without previous state use the original width check.
  • ChatComposer.tsx passes the hook's current layout as previous into each measurement, guarded by the existing state-change check.
  • Adds hysteresis tests in composerFooterLayout.test.ts covering exact-fit retention, 1px re-show, scratch resolution, fractional-width stabilization, and cluster visibility.
  • Behavioral Change: controls that were hidden now stay hidden until host width exceeds their minimum by at least 1px; reviewers should verify the previous param threading in useRestingComposerControlsLayout does not cause stale layouts during rapid resize.

Macroscope summarized ec1d9c1.

The composer re-measures its resting controls on every render, and the
model picker's natural width is recovered from a truncated label whose
scrollWidth is integral while the rendered box is fractional. With the
host sitting exactly on a threshold, that sub-pixel difference flipped a
block in and out of the overflow menu on every render until React gave
up with "Maximum update depth exceeded" (error pingdotgg#185) while opening a
thread.

Hiding a block stays unconditional, or the controls would clip. Showing
one again now has to clear a pixel of slack, so the noise can no longer
drive the layout back and forth. Follow-up to pingdotgg#9393, which removed the
strip-reservation feedback but left this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Sep 3, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at ec1d9c1

Macroscope's review found this PR approvable — This is a localized, test-covered fix that stabilizes the existing resting composer layout by adding a one-pixel re-show threshold. Its production impact is confined to overflow control placement in the composer, with no new capability, schema change, or sensitive-area impact.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarminge enabled auto-merge (squash) September 4, 2026 01:22
@juliusmarminge
juliusmarminge merged commit 0ba06a1 into pingdotgg:main Sep 4, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: React #185 loop returns in the resting composer controls after #9393

2 participants