Repository navigation
Description
This was generated by AI during triage.
Symptom
While CSM is starting up, the content of Claude panes renders garbled — rows colliding, chunks duplicated, content drawn in the wrong place. It is not a subtle offset; it looks corrupted. It corrects itself once something forces the pane to repaint.
Reported by the maintainer against current main.
Not #116, and not #38
#116 warns explicitly that a similar-looking startup symptom has a separate cause, so recording the distinction up front:
- Initial render: input sits one character into the placeholder until the window is resized #116 is a one-character offset — the input line sits one column into the placeholder. Cosmetic, self-correcting, and its two leading theories (font loading,
document.fonts.load()) are already disproved there. - Terminal display corruption: rendered output overlaps/duplicates mid-session #38 is mid-session corruption, correlated with resize-during-TUI-redraw on an already-running pane.
- This is startup/restore-specific and much coarser than one column.
Why Claude panes specifically, and why "garbled" rather than "offset"
Claude Code is an ink TUI: it redraws by absolute cursor positioning, not by clearing the screen (the same property #38 identifies as its likely mechanism). If the PTY believes the terminal is one width and xterm draws at another, ink writes each frame to addresses that do not match the visual grid. A plain shell mostly survives that with ugly wrapping; a TUI that positions its own cursor produces colliding and duplicated rows. So a width disagreement shows up as corruption in a Claude pane and as nothing much in a pwsh pane — which matches the report naming Claude windows.
So the question is why the PTY width and the rendered width disagree during startup.
Mechanism — a fit that never happens
Three things compound, all visible in the current code:
1. Panes are attached to the grid without being re-fit, except in Single layout.
RefreshTerminalLayout adds each session's wrapper to TerminalGrid.Children. Only the Single branch follows that up with Bridge?.FitTerminal(). Every multi-pane branch (TwoColumn, TwoByTwo, SixColumn, SixByThree, …) attaches wrappers with no fit call at all.
This matters most during restore, because RefreshTerminalLayout runs again after every session launch. A pane launched early is detached and re-attached on each subsequent pass, and gets no fit on any of them. LaunchSessionAsync does call bridge.FitTerminal() once — but only for the session it just launched, at a moment when later panes have not yet changed the layout.
2. The page-side fallbacks are acknowledged to be too early. terminal-init.js re-fits at setTimeout 50ms and 250ms to cover the case where the initial fit() ran while the container was Collapsed (0×0). The code comment already concedes these are "guesses at 'fonts are probably ready by now' and are easily too early during a heavy restore with many WebView2s initialising." Restore is fully sequential and measured at ~131s for 25 sessions (#82), so 250ms is not remotely long enough for a pane that gets laid out much later. The ResizeObserver is not a safety net either when a WebView is detached from the visual tree rather than merely sized to zero.
3. A wrong-but-stable width is never reported. The page only tells the host about a size via term.onResize, which xterm fires only when the dimensions change. TerminalBridge._lastSize starts at (80, 24) and the PTY is started at that size. So if a late fit() computes the same wrong cols that xterm already had, no resize message is posted, the PTY keeps its stale width, and nothing self-corrects. #116 flags this same trap independently — it is worth treating as the root pathology rather than a detail.
Together: the PTY is sized from a default, the pane is attached without a fit, the timeout fallbacks expire long before the pane is really laid out, and the one mechanism that would report the truth is suppressed because the value didn't change. The user's repaint is what finally forces a fit that does change cols, which posts a resize, which makes ink redraw at the correct width.
Desired behavior
- A Claude pane renders correctly on first paint after restore, with no user-initiated repaint, resize, or layout change.
- The PTY's width always reflects the width xterm is actually drawing at, including when a pane is attached, re-attached, or becomes visible long after its page loaded.
- The correct size is established by an explicit signal at the moment the pane has real layout — not by a timeout guessing when that might be.
Key interfaces
TerminalBridge.FitTerminal()already does the right thing: it posts{"type":"fit"}atDispatcherPriority.Backgroundso WPF has measured the WebView2 first. The bug is the call sites, not the method. Every path that attaches a wrapper to the grid should call it, not just theSinglebranch.TerminalBridge.TerminalSize/_lastSize— the(80, 24)default is what the PTY starts at. Worth deciding whether a PTY should start before a real fit has been reported at all, or whether the first genuine fit should always be forced through to_pty.Resize.- The
resizemessage contract between page and host. Becauseterm.onResizeis change-triggered, the page needs a way to assert its current size unconditionally — e.g. thefithandler posting the resultingcols/rowsregardless of whether xterm considered it a change. This is the single highest-value fix and is independent of the call-site gap. - The
setTimeout(50)/setTimeout(250)fits interminal-init.js— candidates for removal once an explicit signal exists, per the repo's documented preference for measured signals over guessed delays (theClaudeLaunchStaggerMshistory is the cautionary tale in the other direction: a timer's continuation absorbs whatever stall the restore loop is having, so a delay-based fix is unreliable exactly when the system is loaded).
Reproduction notes
Likely needs a multi-pane layout and several restored sessions to show up — a single-session restore hits the one branch that does fit. Suggested repro: save a session set of 4+ Claude sessions in a TwoByTwo or larger layout, restart CSM, and watch the panes as the restore rail advances. Sessions launched early in the sequence should be the worst affected, since they are re-attached the most times without a fit.
Acceptance criteria
- After a restore of 4+ Claude sessions in a multi-pane layout, every pane renders correctly with no user interaction.
- Every code path that attaches a terminal wrapper to the grid results in a fit, verified for each layout mode — not only
Single. - A fit that computes the same
colsxterm already had still results in the host knowing the true size (no silent divergence when the value is unchanged). - The PTY's
cols/rowsand xterm'scols/rowsagree for every pane immediately after restore completes. Initial render: input sits one character into the placeholder until the window is resized #116 proposes logging both sides; that instrumentation would confirm this directly and is worth landing first. - Sessions launched early in a long sequential restore are no worse off than those launched last.
- Correctness does not depend on any
setTimeoutduration.
Out of scope
- The one-character placeholder offset (Initial render: input sits one character into the placeholder until the window is resized #116) — may well be fixed by the same width-reporting change, but it is tracked separately and should be verified separately.
- Mid-session corruption after a resize (Terminal display corruption: rendered output overlaps/duplicates mid-session #38).
- Making restore faster or parallel (perf: session restore on startup is fully sequential (~131s for 25 sessions; launch cost, not sleeps) #82), though a slow sequential restore is what makes this visible.
- Font-metrics theories — already disproved in Initial render: input sits one character into the placeholder until the window is resized #116; do not re-tread them.
Related
- Initial render: input sits one character into the placeholder until the window is resized #116 — one-char offset at startup; shares the change-triggered-
onResizetrap - Terminal display corruption: rendered output overlaps/duplicates mid-session #38 — mid-session corruption from width disagreement
- perf: session restore on startup is fully sequential (~131s for 25 sessions; launch cost, not sleeps) #82 — sequential restore, ~131s for 25 sessions, which is what stretches this window open
- fix(terminal): re-fit once the font has loaded, not on a timing guess #113 — the
document.fonts.readyre-fit, kept but a no-op here
Activity