Summary
Scrolling the session list produces rendering artifacts (duplicated/offset rows) on all terminals — Ghostty 1.3.1, macOS Terminal.app, and Warp. This is a regression from v0.27.5, where the TUI rendered correctly.
full_repaint = true in [display] reduces the severity but does not eliminate it — drift accumulates between the 2-second tick clears.
Steps to Reproduce
- Upgrade from v0.27.5 to v1.5.1
- Open Agent Deck TUI with several sessions
- Scroll up and down the session list (j/k or mouse wheel)
- Observe rows rendering with increasing vertical offset / duplication
Environment
- Agent Deck: v1.5.1
- Previously working: v0.27.5
- Terminals tested: Ghostty 1.3.1, macOS Terminal.app, Warp (all affected)
- tmux: 3.6a
- OS: Ubuntu (server via SSH), macOS (client)
full_repaint = true: Enabled, reduces but doesn't fix
Analysis
This is not the same as #327 (Ghostty-specific grapheme width mismatch). That issue was terminal-specific; this reproduces on all terminals.
Rendering deps unchanged
Bubble Tea, lipgloss, and go-runewidth versions are identical between v0.27.5 and v1.5.1:
bubbletea v1.3.10
lipgloss v1.1.0
go-runewidth v0.0.21
Suspected root cause: CSIuReader stdin wrapper
v1.5.1 added tea.WithInput(ui.NewCSIuReader(os.Stdin)) to the tea.NewProgram call at cmd/agent-deck/main.go:624. This wraps stdin with a buffering/translation layer for CSI u keyboard compatibility.
The csiuReader.Read() at internal/ui/keyboard_compat.go:292 adds a translation pass between raw terminal bytes and Bubble Tea. This changes the Read() timing characteristics — if buffering causes reads to be delayed or batched, it could affect how Bubble Tea handles WindowSizeMsg events during scrolling, causing the renderer to use stale height/width values for one or more frames and producing the offset drift.
The codebase's own comments at home.go:8840 describe exactly this failure mode:
"Uses lipgloss.Width() for measurement to stay consistent with lipgloss.JoinHorizontal's internal width calculation. Using a different measurement can disagree by even 1 character, causing JoinHorizontal to pad all lines to the wider measurement. This makes the joined output exceed terminal width, lines wrap, and Bubble Tea's renderer loses cursor tracking — producing duplicated/stacked content (the 'scrolling artifact' bug)."
Why full_repaint doesn't fully fix it
full_repaint only fires tea.ClearScreen on the tick handler (every 2s at home.go:4299), not on key/scroll/mouse events. Drift accumulates between clears.
Suggested fix directions
- Investigate CSIuReader timing — compare Bubble Tea rendering behavior with and without the
tea.WithInput wrapper
- Extend
full_repaint to fire on tea.KeyMsg / tea.MouseMsg — not just ticks
- Gate CSIuReader behind a config flag or terminal detection so it only activates on terminals that need it
Diagnostic suggestion
Building from source with tea.WithInput(ui.NewCSIuReader(os.Stdin)) removed from main.go:624 would confirm or rule out the CSIuReader as the cause.
Summary
Scrolling the session list produces rendering artifacts (duplicated/offset rows) on all terminals — Ghostty 1.3.1, macOS Terminal.app, and Warp. This is a regression from v0.27.5, where the TUI rendered correctly.
full_repaint = truein[display]reduces the severity but does not eliminate it — drift accumulates between the 2-second tick clears.Steps to Reproduce
Environment
full_repaint = true: Enabled, reduces but doesn't fixAnalysis
This is not the same as #327 (Ghostty-specific grapheme width mismatch). That issue was terminal-specific; this reproduces on all terminals.
Rendering deps unchanged
Bubble Tea, lipgloss, and go-runewidth versions are identical between v0.27.5 and v1.5.1:
bubbletea v1.3.10lipgloss v1.1.0go-runewidth v0.0.21Suspected root cause: CSIuReader stdin wrapper
v1.5.1 added
tea.WithInput(ui.NewCSIuReader(os.Stdin))to thetea.NewProgramcall atcmd/agent-deck/main.go:624. This wraps stdin with a buffering/translation layer for CSI u keyboard compatibility.The
csiuReader.Read()atinternal/ui/keyboard_compat.go:292adds a translation pass between raw terminal bytes and Bubble Tea. This changes the Read() timing characteristics — if buffering causes reads to be delayed or batched, it could affect how Bubble Tea handlesWindowSizeMsgevents during scrolling, causing the renderer to use stale height/width values for one or more frames and producing the offset drift.The codebase's own comments at
home.go:8840describe exactly this failure mode:Why
full_repaintdoesn't fully fix itfull_repaintonly firestea.ClearScreenon the tick handler (every 2s athome.go:4299), not on key/scroll/mouse events. Drift accumulates between clears.Suggested fix directions
tea.WithInputwrapperfull_repaintto fire ontea.KeyMsg/tea.MouseMsg— not just ticksDiagnostic suggestion
Building from source with
tea.WithInput(ui.NewCSIuReader(os.Stdin))removed frommain.go:624would confirm or rule out the CSIuReader as the cause.