[BUG] VS Code extension: chat input shows ghost/offset text when prompt overflows — mentionMirror scroll clamps before re-render
Description
In the VS Code extension's chat panel, once a prompt is taller than the input box (max-height: 200px, about 8 lines), the visible text sometimes ends up one or more lines away from the real, editable text. Selecting all (Ctrl+A) shows the problem: the selection highlight sits on one set of lines, and a second, unhighlighted "ghost" copy of the text is drawn offset by about a line. Clicking, arrow keys, and selection all act on text you cannot see where it is drawn, which makes editing a long prompt very confusing.
Environment
- Claude Code VS Code extension 2.1.278 (win32-x64)
- VS Code 1.138.0
- Windows 11 (10.0.26200)
- Chat panel in the secondary sidebar (not the terminal UI)
Steps to reproduce
- Open the Claude Code chat panel.
- Paste or type a prompt long enough to make the input scroll (more than ~8 wrapped lines).
- Press Ctrl+A.
Expected: the selection highlight covers the visible text exactly.
Actual: the visible text is shifted about one line from the selection. Unhighlighted ghost text shows between the highlighted lines. It does not happen every time, but it happens often after a paste or a large edit.
Workaround: scrolling the input one notch with the mouse wheel lines the layers up again.
Root cause (from the shipped webview/index.js / index.css)
The input is the same transparent-input-plus-mirror design described in #69513, but the cause here is different:
.messageInput_cKsPxgis acontentEditable="plaintext-only"div withcolor: #0000andoverflow-y: auto. It holds the real text, caret, and selection..mentionMirror_cKsPxgis anaria-hiddendiv positioned absolutely behind it (inset: 0,overflow: hidden, same padding,max-height: 200px). It draws the visible text.
Scroll position is synced in exactly one place, a scroll listener registered once:
o(() => {
let S1 = f1.current, D0 = T0.current; // input, mirror
if (!S1 || !D0) return;
let t5 = () => { D0.scrollTop = S1.scrollTop; D0.scrollLeft = S1.scrollLeft };
return S1.addEventListener("scroll", t5), () => S1.removeEventListener("scroll", t5);
}, []);
When input grows the content (paste, typing at the end), the contenteditable scrolls to the caret and fires scroll before React re-renders the mirror's children with the new text. At that moment the mirror's scrollHeight is still the old, shorter height, so the browser clamps D0.scrollTop to the old maximum. React then renders the longer text, but no second scroll event fires, so the mirror stays scrolled short by the height of the added content, usually about one line.
Suggested fix
Also re-sync after the mirror renders, for example in a layout effect that depends on the rendered mirror content:
useLayoutEffect(() => {
const input = f1.current, mirror = T0.current;
if (input && mirror) { mirror.scrollTop = input.scrollTop; mirror.scrollLeft = input.scrollLeft; }
}, [Y5 /* mirror children */, n4 /* argument hint */]);
A ResizeObserver on the mirror that re-applies scrollTop would also cover changes in wrap width, such as resizing the panel.
A screenshot of the Ctrl+A state is available on request.