Skip to content

CPLAT-12781: TUI robustness - move blocking tmux/git work off the UI loop, fix CJK input wrap - #175

Merged
gavin-jeong merged 4 commits into
masterfrom
CPLAT-12781-tui-robustness
Oct 14, 2026
Merged

gavin-jeong merged 4 commits into
masterfrom
CPLAT-12781-tui-robustness

Conversation

@gavin-jeong

Copy link
Copy Markdown
Collaborator

JIRA: https://sendbird.atlassian.net/browse/CPLAT-12781

Summary

Fixes UI-thread blocking, a CJK layout bug, swallowed errors and a render-time state write in the TUI, found in a robustness audit. One commit per fix.

  • Kitty visibility poll blocked Update and redrew at idle. kittyTickMsg ran tmux display-message synchronously in Update every 250ms and forced a View() each time. A tea.Cmd now polls tmux and only returns kittyVisibilityMsg when visibility changes, so an idle app receives no messages. (internal/tui/kitty_watch.go, app.go)
  • Korean input broke the input modal. inputmodal.go truncated and positioned by rune count; Hangul is 2 cells per rune, so lines overflowed, lipgloss re-wrapped them and the modal grew. Lines now soft-wrap into visual rows by cell width, and scrolling and the cursor follow visual rows. (internal/tui/inputmodal.go)
  • git worktree add, tmux.FindPane and send-keys ran inside Update. executeNewWorktreeSession, executeCmdWorktreeNew, executeWorktree, bulkKill, killLiveSession and openLiveInput now do their git/tmux work in tea.Cmds that return worktreeDoneMsg / sessionsKilledMsg / liveInputPaneMsg. The live-input lookup carries a generation, so a stale result cannot open input for the wrong pane. (internal/tui/worktree_async.go, internal/tui/pane_actions.go)
  • send-keys C-c errors were discarded. A failed kill still reported "Killed". Errors now appear in the status line (Kill failed: … / Killed N, M failed: …), and the bulk count only includes panes that actually got the interrupt. (internal/tui/pane_actions.go)
  • renderSessionSplit wrote state during render. It resized the list and sessSplit.Preview.Width/Height and re-wrapped content inside View. That work moves to syncSessionSplitLayout, which runs after every Update. View now only reads state. (internal/tui/app.go)

app.go shrinks by ~230 lines; the moved code lives in focused files.

Not in this PR

Other synchronous tmux calls in Update remain: bulkInput (FindPane per selected session), FindPane at app.go jump/preview paths, share_ref.go, and the send-keys pair in the \-continuation path whose .Run() errors are still ignored.

Tests

  • New: kitty_watch_test.go, inputmodal_test.go (Hangul wrap + fixed modal height; the height test fails on master with 29 vs 26 lines), pane_actions_test.go (no tmux call on the Update path, send-keys error surfaced, stale live-input result dropped, worktree created by the Cmd and not in Update, using a temp git repo), session_split_layout_test.go (View does not resize the preview; fails on master).
  • outputs_actions_test.go fixture now settles the layout before it injects digest rows, as the first real Update would.
  • go vet ./... clean; go test ./... passes (10 packages); -race on the new tests passes.

kittyTickMsg ran tmux display-message synchronously inside Update every
250ms and forced a View() each time even when idle. A tea.Cmd now polls
tmux and only returns a message when the visibility actually changes.
Hangul/CJK runes are two cells wide. The modal truncated by rune count
and never wrapped the cursor line, so Korean input overflowed the inner
width, lipgloss re-wrapped it, and the modal grew and misplaced rows.
Soft-wrap each line into visual rows by cell width and scroll by visual
row so the cursor row is always the one drawn.
git worktree add, tmux.FindPane and send-keys ran synchronously inside
Update, freezing input for up to seconds on large repos. They now run in
tea.Cmds that return worktreeDoneMsg / sessionsKilledMsg /
liveInputPaneMsg. The live-input lookup carries a generation so a stale
result cannot open input for the wrong pane.

send-keys C-c errors were discarded, so a failed kill still read
"Killed"; they are now collected and shown in the status line, and the
bulk count only includes panes that actually received the interrupt.
renderSessionSplit resized the list, wrote sessSplit.Preview.Width/Height
and re-wrapped preview content while rendering, so View was not pure and
two renders could differ. The mutating half moves to
syncSessionSplitLayout, which Update runs after every message; View only
reads.
@Kairo-Kim Kairo-Kim added the auto-review/approved Auto-approved by the Slack auto-reviewer bot label Oct 14, 2026

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@gavin-jeong
gavin-jeong merged commit d219fd8 into master Oct 14, 2026
5 checks passed
@gavin-jeong
gavin-jeong deleted the CPLAT-12781-tui-robustness branch October 14, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review/approved Auto-approved by the Slack auto-reviewer bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants