Skip to content

fix(chat): the composer stays put — the hidden speaker label was stretching the page - #100

Merged
ndemianc merged 1 commit into
developfrom
fix/chat-page-scroll
Oct 3, 2026
Merged

ndemianc merged 1 commit into
developfrom
fix/chat-page-scroll

Conversation

@ndemianc

@ndemianc ndemianc commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

The bug

Past a conversation's first long turn, scrolling beyond the end of the transcript moved the whole chat: transcript, composer and status row slid up together over a blank page. The page is a 100vh shell and only #log is meant to scroll, so the composer should never move at all.

The cause

The page itself had become scrollable.

T4 (59777da) takes the speaker label — You / LevelCode AI — off the screen and keeps it for screen readers, with the usual rule for that: position: absolute, 1px, clipped. Nothing above the label was positioned, so its containing block was the page, not the transcript. Each label was laid out at its unscrolled place in the log, where the log's scrolling and clipping do not reach it.

A labelled message that started more than a viewport down therefore had its label below the fold. The page grew to reach that one pixel, and once the log was at its end, the wheel scrolled the page. In practice: from the second turn of any conversation whose first one was long.

The fix

#log { position: relative; } — the transcript becomes the containing block for everything inside it: this label, and anything taken out of flow later. The label itself is unchanged: out of flow, clipped, and in the accessibility tree.

Not done: overflow: hidden on the page. It would hide the symptom and leave the cause. And in a window too short for the composer and the status row — the shell needs 208px, measured in a 140px viewport — scrolling the page is the only way to reach the bottom controls.

Verification

In Chromium against the shipped chat.html, served the way the host serves it (__CSP__ and __NONCE__ filled in, acquireVsCodeApi stubbed) and driven with the host's own messages: two agent turns, the first with a long code block, in a 1118px viewport.

before after
label's offsetParent <body> #log
last label, its message scrolled to −22px 1962px — pinned to the page −23px — 1px above its message
page height 1963px 1118px (the viewport)
composer after 4 wheel ticks at the end of the log 400px higher, blank page beneath unmoved
transcript content height 2919px 2919px
  • Scrolling up inside the transcript still pauses auto-scroll and shows "jump to latest". No console errors.
  • test/webviewCss.test.js gains the guard no DOM test can give, since the fake DOM lays nothing out: #log is positioned and stays positioned, with the shell's premise pinned beside it. It fails on develop's chat.html, on position: static, and on a later rule that un-positions the log.
  • ./scripts/test-extensions.sh: 48 suites, 925 cases, 0 failing.
  • docs/CHAT-TYPOGRAPHY.md D6 records the correction and the measurements.

Not run in the editor. The worktree this was written in has no vscode/ build, so the fix has been seen in Chromium, not in the app.

Not reproduced: a single run that works first and answers last. Tool activity folds into a compact group, so that transcript stays short; the trigger confirmed here is a later turn that starts below the fold.

…tching the page

Past a conversation's first long turn, scrolling beyond the end of the transcript
moved the whole chat: transcript, composer and status row slid up together over a
blank page. The page is a 100vh shell and only #log is meant to scroll, so the
composer should never move at all.

The page itself had become scrollable. T4 (59777da) takes the speaker label —
"You" / "LevelCode AI" — off the screen and keeps it for screen readers, with the
usual rule for that: position: absolute, 1px, clipped. Nothing above the label
was positioned, so its containing block was the page, not the transcript. Each
label was laid out at its UNSCROLLED place in the log, where the log's scrolling
and clipping do not reach it. A labelled message that started more than a
viewport down had its label below the fold; the page grew to reach that one
pixel; and once the log was at its end, the wheel scrolled the page. In practice:
from the second turn of any conversation whose first one was long.

#log is now positioned (position: relative), which makes it the containing block
for everything inside it — this label, and anything taken out of flow later. The
label is as it was: out of flow, clipped, and in the accessibility tree.

Not done: locking the page with overflow: hidden. It would hide the symptom and
leave the cause. And in a window too short for the composer and the status row —
the shell needs 208px, measured in a 140px viewport — scrolling the page is the
only way to reach the bottom controls.

Verified in Chromium against the shipped chat.html, served the way the host
serves it (CSP and nonce filled in, acquireVsCodeApi stubbed) and driven with the
host's own messages: two agent turns, the first with a long code block, in a
1118px viewport.

                                          before     after
    label's offsetParent                  <body>     #log
    last label, its message at -22px      1962px     -23px
    page height                           1963px     1118px
    composer after 4 wheel ticks          -400px     unmoved
    transcript content height             2919px     2919px

Scrolling up inside the transcript still pauses auto-scroll and shows "jump to
latest". No console errors.

webviewCss.test.js gains the guard no DOM test can give, since the fake DOM lays
nothing out: #log is positioned and stays positioned, with the shell's premise
pinned beside it. It fails on develop's chat.html, on position: static, and on a
later rule that un-positions the log. 48 suites, 925 cases, 0 failing
(scripts/test-extensions.sh). docs/CHAT-TYPOGRAPHY.md D6 records the correction
and the measurements.

Not run in the editor: this worktree has no vscode/ build. Not reproduced: a
single run that works first and answers last. Tool activity folds into a compact
group, so that transcript stays short; the trigger confirmed here is a later turn
that starts below the fold.
Copilot AI balanced review requested due to automatic review settings October 3, 2026 16:20
@ndemianc
ndemianc merged commit bd4daf5 into develop Oct 3, 2026
3 checks passed
@ndemianc
ndemianc deleted the fix/chat-page-scroll branch October 3, 2026 16:22

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

The regression assertion accepts positioning values that would remove the transcript from the flex layout.

Review effort: Balanced
Findings: 1 Medium severity · 2 Low severity

Open (3)
What changed in this PR

Fixes chat-page scrolling by anchoring absolutely positioned transcript labels to #log.

Changes:

  • Positions the transcript as the labels’ containing block.
  • Adds a CSS regression test.
  • Documents the cause and Chromium measurements.
File Description
extensions/​levelcode-ai/​media/​chat.html Anchors hidden speaker labels within the transcript.
extensions/​levelcode-ai/​test/​webviewCss.test.js Adds the regression guard.
docs/​CHAT-TYPOGRAPHY.md Records the fix and measurements.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +540 to +543
assert.ok(position && position !== 'static',
'#log must be positioned (position: relative). It is the containing block for the clipped speaker\n'
+ 'label; without it every label is laid out against the page, the page grows past the viewport, and\n'
+ 'the whole chat — composer included — scrolls away. Got: ' + (position || 'no position at all'));
Comment thread docs/CHAT-TYPOGRAPHY.md
Comment on lines +192 to +193
`#log { position: relative }` makes the log the containing block for everything inside it: this
label, and anything out of flow added later. Measured the same way (Chromium, the shipped
Comment on lines +154 to +155
over a blank page. Positioned, the log is the containing block for every descendant: the label,
and whatever is added later. webviewCss.test.js fails if this goes. */
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.

2 participants