fix(tui): repair footer artifacts during agent startup and runtime#346
fix(tui): repair footer artifacts during agent startup and runtime#346abezzub-dr wants to merge 4 commits into
Conversation
Moat tells the child process the terminal is one row shorter than the host so the child doesn't paint its own bottom-pinned UI on the same row as moat's footer. Previously, manager.StartAttached and exec.go's ResizeTTY calls passed the full host terminal height. The status bar's DECSTBM scroll region was correctly set to lines 1..height-1, but the child still saw all height rows and drew its bottom UI at row height — colliding with the footer. Recent Claude Code versions paint a multi-line bottom UI (input prompt, model/workspace status, permissions hint), and every repaint clobbered moat's footer. Both processes then redrew on top of each other, producing character-interleaved artifacts in the content area. Add a reservedRows parameter to Manager.StartAttached. The manager subtracts it from the auto-detected InitialHeight. The CLI passes 1 when a status bar is present and subtracts 1 from the height in both ResizeTTY calls (initial post-start and SIGWINCH).
Some node-based TUIs (Claude Code among them) emit `\x1b[r` once during TTY normalization to reset the DECSTBM scroll region to full screen. That wipes out moat's scroll region — the bottom row is no longer reserved for the footer, and any subsequent footer redraw at row H becomes regular text that scrolls up with content. The visible symptom is moat's footer text appearing as scattered standalone lines inside Claude's content area. Detect `\x1b[r` in the byte stream, let it pass through to the terminal (the child may have meaningful side effects on it), then immediately reassert moat's scroll region wrapped in DECSC/DECRC so the cursor is not disturbed. Skip the reassert in compositor mode since the emulator owns its own screen state. Diagnosed from a TTY trace showing exactly one `\x1b[r` near startup and one at exit per session — this is a one-shot restore, not an ongoing fight with the child.
Pre-run hooks (e.g. `pnpm install`) and moat-init's own setup steps print to the same TTY as the user's command. TUIs that paint with relative cursor advances (\x1b[NC) instead of overwriting cells leave any prior characters bleeding through their layout — Claude Code's startup banner is a striking example, with pnpm install lines visible inside the logo glyphs. moat-init.sh now emits ESC[2J ESC[H after the pre_run hook (gated on a TTY and a successful hook — `set -e` aborts above on hook failure, so errors stay visible). The writer detects ESC[2J in the data stream and redraws the footer immediately, since the 50ms debounce isn't reliable during a busy agent startup.
|
I've noticed that Claude Code's rendering tends to get all sorts of messed up inside of This is probably entirely unrelated, but I thought to give the In any case, I'd love to see if this PR might fix my issue - is there anything help I could offer to help bring this over the line @abezzub-dr @dpup? |
|
Did you try after #349? In a previous commit I added a TUI trace to the The problem for me hasn't been consistent. It almost seems like a per-terminal problem. |
|
@dpup I have not, going to build locally and give that a whirl! |
|
@dpup my early testing would seem to indicate that the changes from #349 have addressed the issue for me, though this is based on somewhat limited hands-on time with this so far - happy to keep testing and report back if the issue re-surfaces. As an aside, I should've mentioned that I'm personally using |
|
Nice. I'm using cmux which is based on ghostty |
|
If it happens again try to grab a TUI trace. It might have sensitive data, depending on your project so you could also ask Claude to investigate. |
Summary
Fix three independent bugs that caused moat's footer to bleed into the agent's output. The visible symptoms were character-interleaved text in Claude's content area, the moat footer appearing as scattered standalone lines mid-conversation, and pnpm-install output bleeding through Claude's startup banner.
The bugs were diagnosed from a TTY trace (
moat claude --tty-trace) rather than guessed at — happy to share the trace if useful for review.Three fixes, each addressing a distinct cause
1. Reserve the status bar row in the PTY size advertised to the child (
ab5fa6f)Manager.StartAttachedandexec.go'sResizeTTYcalls passed the full host terminal height to the child. moat's DECSTBM scroll region was correctly set to lines1..H-1, but the child still saw allHrows and drew its bottom-pinned UI at rowH— colliding with moat's footer. Recent Claude Code versions paint a multi-line bottom UI (input prompt, model/workspace status, permissions hint), which made every repaint clobber the footer; both processes then redrew on top of each other, producing the character-interleaved artifacts.StartAttachedgains areservedRows uintparameter. The manager subtracts it from the auto-detectedInitialHeight. The CLI passes1when a status bar is in use and subtracts1from the height in bothResizeTTYcalls (initial post-start and SIGWINCH).2. Reassert the scroll region after the child resets DECSTBM (
ad972c5)Some node-based TUIs (Claude Code among them) emit
\x1b[ronce during TTY normalization to reset the scroll region to full screen. After that, moat's region is gone — any subsequent footer redraw at rowHbecomes regular text that scrolls up with content. The visible symptom is moat's footer text appearing as scattered standalone lines inside the agent's content area.The writer detects
\x1b[rin the byte stream, lets it pass through (some terminals normalize state on it), then immediately reasserts moat's scroll region wrapped in DECSC/DECRC so the cursor isn't disturbed. Skipped in compositor mode since the emulator owns its own state.3. Clear the screen between pre_run hooks and the user's command (
c51e191)Pre-run hooks (e.g.
pnpm install) and moat-init's own setup steps print to the same TTY as the user's command. TUIs that paint with relative cursor advances (\x1b[NC) instead of overwriting cells leave any prior characters bleeding through their layout — Claude Code's startup banner is a striking example, with pnpm install lines visible inside the logo glyphs.moat-init.shnow emits\x1b[2J\x1b[Hafter the pre_run hook, gated on a TTY (and on hook success —set -eaborts above on hook failure, so errors stay visible). The writer detects\x1b[2Jin the data stream and redraws the footer immediately, since the 50ms debounce isn't reliable during a busy startup.Test plan
go test -race ./internal/tui/passes (28 existing + 8 new tests, including DECSTBM reassert, erase-screen redraw, split-across-writes prefix detection, and compositor-mode skips)go test -race ./internal/run/ ./cmd/moat/cli/passesgolangci-lint run --new-from-rev=mainreports 0 new issuesmoat claude pricing-agentagainst a TTY trace: no character-interleaving in content, no scattered footer lines, banner renders on a clean screengo test -tags=e2e ./internal/e2e/(5 e2e callers updated to pass0reservedRows)--rebuildflag — since the script is embedded into the image at build time)