Skip to content

v1.5.1 regression: TUI row offset drift when scrolling (all terminals) #607

Description

@maxfi

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

  1. Upgrade from v0.27.5 to v1.5.1
  2. Open Agent Deck TUI with several sessions
  3. Scroll up and down the session list (j/k or mouse wheel)
  4. 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

  1. Investigate CSIuReader timing — compare Bubble Tea rendering behavior with and without the tea.WithInput wrapper
  2. Extend full_repaint to fire on tea.KeyMsg / tea.MouseMsg — not just ticks
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions