cmux NEXT Inspector: a resize places the card in the frame that draws it

Merged
TE
14d ago
+52-4

Leo: "why's the diff viewer not opening up in the right split tab? what about the existing diff viewer elsewhere that was previously made? also wtf this sidebar like ik we're fixing it not being a whole fucking sidebar and shit but why does it FLICKER AND WHY DOES IT STAY ON IN THESE RANDOM SITUATIONS WITHOUT BEING CLOSABLE!?"

The height (#24125) and the close button and Esc (#24126) have landed. This PR fixes the flicker.

What flickered

I recorded the Inspector card on every painted frame in Chromium while a chat streamed text, edits, background tasks and context updates. I resized the pane, switched chats, opened a diff in place and opened a diff beside the chat.

  • Ruled out: nothing remounts the card. It has the same element and sections on every frame, and its opacity and visibility never change. #24127's stable task keys already stopped the task rows remounting.
  • The flicker: the card jumped for a frame whenever the pane's width crossed the point where it switches between floating over the chat and pushing it aside. That happens when a split opens, a diff opens beside the chat, or the window is dragged.
    • The pane's width came from a ResizeObserver and was stored as ordinary React state, so React drew it a frame or more after the browser drew the new width.
    • Narrowing (1600 → 1200px): for one frame the card still floated, covering 4 transcript rows. Then the chat jumped aside under it.
    • Widening (1200 → 1600px): for one frame the chat was still pushed aside. Then it jumped back under the floating card.

Fix

usePaneWidth now applies the observer's update with flushSync. The observer runs after the frame's layout and before its paint, so the card's new place is drawn in the same frame as the new width.

The width is read once per resize, which the observer already does. Nothing new measures per update, and nothing is added per streamed update. The observer is the existing one with its existing cleanup (disconnect when the pane or panel goes away), and no new timer or loop was added.

Test

In chat-inspector-placement.test.ts, the first painted frame at the new width must already have the card placed for that width and covering no row: 1600 → 1200 and 1200 → 1600, in Chromium and WebKit.

  • On base, all 4 cases fail. Narrowing gives placement: overlay, covers: 4 where it should push. Widening gives placement: push where it should float.
  • With the fix, all 20 tests in the file pass. chat-inspector-overlay-toggle, -motion, -overlay-transcript and chat-inspector.test.tsx pass too.

Capture

Frame strip from a CDP screencast of every composited frame across 1600 → 1200px:

frame 0frame 1frame 2
before (base)floats at 16001200px wide, card still floating over the transcript's right edgecard pushed aside, chat jumps
afterfloats at 16001200px, card already pushed, chat already in its new columnsame

The PNG strips are on huge-blue in the lane's scratch folder, and the gallery advisory run posts its own capture. Webview only, with no native change.

🤖 Generated with Claude Code

Agent session: huge-blue


Summary by cubic

Fixes the Inspector card flickering for one frame when a pane resize crosses the point where it switches between floating over the chat and pushing it aside. The pane's width was read in a ResizeObserver and applied as React state, so the card was drawn a frame or more after the new width: narrowing left the card floating over transcript rows, and widening kept the chat pushed for a frame.

Bug Fixes

  • Applies the observer's width update with flushSync so the card is placed in the same frame that draws the new width.
  • Adds placement tests asserting the first frame at the new width has the card in its new place with no covered rows, for narrowing and widening in Chromium and WebKit.