Skip to content

chat: resizing the pane holds the conversation still - #14

Merged
foxnne merged 1 commit into
mainfrom
chat/resize-smooth
Oct 16, 2026
Merged

foxnne merged 1 commit into
mainfrom
chat/resize-smooth

Conversation

@foxnne

@foxnne foxnne commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

chat: resizing the pane holds the conversation still

A drag of the pane's edge, sideways or up and down, moved the whole conversation by
hundreds of pixels for a frame, stopped following the bottom on alternate frames, and laid
every message out again on every frame. Three causes, all in View.zig:

  • Every message's kept height was keyed by width, so the first frame of a width change
    turned every off-screen spacer back into a message. dvui had forgotten those rows' sizes,
    so each laid out empty for a frame, and everything below moved up by their total height.
    The fix from chat: scrolling up holds still while messages above lay out again #3 for scrolling up kept the height only when it was still valid, which a new
    width never is. Now a message coming back from a spacer always keeps its last height for
    that frame.
  • Off-screen messages are now spacers at their last height even at another width. They are
    laid out at the new width when they come into view, and the anchor holds the reader through
    that, as it does for any reflow. A drag lays out only what is on screen.
  • Following used last frame's viewport height. When the pane grew taller, that put the
    viewport past the end. The newest line sat a frame behind the edge, and dvui's bounce back
    from the overshoot read as the person scrolling up. The height now comes from a box made
    before the scroll area.

Also: a bubble's hug width was cached by version alone, clamped to the width at that time.
A message that arrived while the pane was narrow stayed narrow after the pane widened. It is
now kept by version and width.

How it was seen: fizzy from main (Debug, macOS) with a sandbox profile. A temporary driver,
left out of this PR, opened a Sample chat and streamed 8 scripted turns (17 messages) into it
the way an agent does. Tapes dragged the sidebar split from 864 to 569 to 1049 px and back,
and dragged the bottom panel down. A per-frame log recorded each message row's screen rect
and the scroll state.

before after
sideways drag, at the bottom: largest frame-to-frame offset of the last message from the bottom edge 2698 px (first frame of the drag, and again when it resumed after a pause) 0 px
sideways drag, scrolled up: largest offset of the anchored message from its place 1376 px 0 px
messages laid out per drag frame 17 (all) 3.6 (on screen)
transcript draw per drag frame, Debug, median / max 4.9 / 20.4 ms 1.8 / 3.3 ms
vertical drag: times following flipped off and on 8 of 9 frames 0
vertical drag: gap under the last message 3 to 15 px 0 px
person bubble after narrowing, a new message, widening 285 px (stuck at the narrow clamp) 291 px (same as an identical message sent while wide)

Still one frame late, and not changed here: a message whose wrap changes during a drag is
laid out at its old height for one frame (dvui places a child at last frame's size). That
shows as a single step, not a bounce.

Streaming jitter, the other half of the report, is the markdown plugin's inline render
emitting a block's estimated height for a frame after each re-parse. That fix is
fizzyedit/fizzy#454. Nothing in this PR is needed for it.

🤖 Generated with Claude Code

A width change turned every off-screen spacer back into a message that laid out empty for a
frame, moving the conversation by their total height. Off-screen messages now stay spacers at
their last height through a width change, and one coming back keeps its height for its first
frame. Following the bottom uses this frame's viewport height, so a taller pane no longer
overshoots into dvui's bounce back and drops following. A bubble's hug width is kept by width
as well as version, so it grows back when the pane widens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne marked this pull request as ready for review October 16, 2026 15:15
@foxnne
foxnne merged commit 9e70b20 into main Oct 16, 2026
6 checks passed
foxnne added a commit to fizzyedit/fizzy that referenced this pull request Oct 16, 2026
#454)

A chat streaming a reply in (fizzyedit/chat) jittered: the conversation
dropped by a line
and came back, many times a turn. The cause is in the markdown service's
inline render
(`drawInline`, which a chat bubble uses).

`renderTopLevel` pins a block's emitted height to its height-table
entry. That keeps a
virtualized preview still while blocks come back from spacers.
`drawInline` passes no
viewport, so nothing is virtualized and every block is laid out every
frame, but the pin
still applied. A streamed reply changes its text on every chunk, and the
chat's type-in
changes it on every frame. Each change re-parses, which clears the
table:

1. Frame A: the edited block has no entry and is laid out at its real
height. If its text
layout is new (a paragraph that just started), the measurement is filed
`.deferred`,
   which keeps the estimate.
2. Frame B: the block is pinnable (`.deferred`), so the pin emits the
estimate, which is a
   line or so short.
3. Frame C: real again.

The caller sees H, then H minus a line, then H. With the chat following
the bottom, the whole
conversation moved down by that line and back.

The fix gates both pins on `virtualize`, so a render with no viewport
emits what it lays out.
The preview pane, which always has a viewport, is unchanged.

How it was seen: fizzy from main (Debug, macOS) in a sandbox profile,
with this markdown dylib
in place of the bundled one. A temporary driver in the chat plugin
streamed 8 scripted turns,
in 4 to 40 byte chunks every 20 to 140 ms. A per-frame log recorded each
message row's rect,
min size and the scroll position.

| over 20 s of streaming | before | after |
|---|---|---|
| frames the conversation moved back down | 15 | 0 |
| content-height changes that were shrinks | 22 of 78 | 0 of 36 |
| frames a message's row was a frame behind its own size | 75 | 35 (only
growth, one frame late, as dvui lays out) |

Example, one reply's row height across three frames with the same text:
98.1, then 80.0, then
98.1 before; 98.1 throughout after.

Test: the headless integration test's inline-render check now streams a
reply in 9 bytes a
frame and fails if the height ever drops. Without the fix it fails
("shrank from 36.4 to
19.6 at byte 18"); with it, `zig build test-integration` passes 53/53
steps, and so do
`zig build test` and `scripts/check-fmt.sh`. `check-web`,
`test-sdk-version` and the full
app build were left to CI: no SDK or build file changes.

No plan step; a fix found while measuring the chat. The chat's own
resize fixes are
fizzyedit/chat#14 and don't depend on this.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant