detached 9cc2fd6

fix(tui): force repaint on layout-shifting events

Calls Invalidate() on tool result, assistant message commit, and turn end to prevent stale code fragments from bleeding into the status bar when the diff renderer misses row changes.
commit
9cc2fd6e8acc00d84555ac1937a061e036ca745f
author
patriceckhart · · 2026-04-24 13:00 UTC
parent
b25a2bc
signature
unsigned
refs
tag: v0.1.7
1 file · +16 -4
M internal/agent/modes/interactive.go +16 -4
@@ -2598,6 +2598,10 @@ func (i *Interactive) handleEvent(ev core.AgentEvent) {
25982598 return
25992599 }
26002600 i.resetStreamingStateLocked()
2601+ // Force full repaint: assistant text committed, layout shifts.
2602+ if i.rend != nil {
2603+ i.rend.Invalidate()
2604+ }
26012605 case core.EvToolUseStart:
26022606 // Live streaming: pre-create the view so the user sees the
26032607 // tool call being composed in real time. Any subsequent
@@ -2657,6 +2661,12 @@ func (i *Interactive) handleEvent(ev core.AgentEvent) {
26572661 }
26582662 tc.Result = text.String()
26592663 }
2664+ // Force full repaint: tool completion shifts the layout
2665+ // (collapsed body replaces streaming) and the diff renderer
2666+ // may leave stale code fragments on status bar rows.
2667+ if i.rend != nil {
2668+ i.rend.Invalidate()
2669+ }
26602670 if i.cfg.OnToolResult != nil {
26612671 i.cfg.OnToolResult(e.ID, e.Result)
26622672 }
@@ -2666,11 +2676,13 @@ func (i *Interactive) handleEvent(ev core.AgentEvent) {
26662676 i.lastCtxInput = e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens
26672677 }
26682678 case core.EvTurnEnd:
2679+ // Force full repaint: the turn ending shifts layout (streaming
2680+ // buffer flushed, status bar updates) and the diff renderer
2681+ // may leave stale content on reused rows.
2682+ if i.rend != nil {
2683+ i.rend.Invalidate()
2684+ }
26692685 if e.Stop == provider.StopAborted {
2670- // Aborted turn: discard the partial streaming text (it
2671- // is not persisted in the transcript) and clear any
2672- // transient error. Also drop anything still buffered in
2673- // the pacer so nothing keeps drawing after the cancel.
26742686 i.resetStreamingStateLocked()
26752687 i.statusErr = ""
26762688 i.statusOK = "cancelled"
@@ -2598,6 +2598,10 @@ func (i *Interactive) handleEvent(ev core.AgentEvent) {
2598 return2598 return
2599 }2599 }
2600 i.resetStreamingStateLocked()2600 i.resetStreamingStateLocked()
2601+ // Force full repaint: assistant text committed, layout shifts.
2602+ if i.rend != nil {
2603+ i.rend.Invalidate()
2604+ }
2601 case core.EvToolUseStart:2605 case core.EvToolUseStart:
2602 // Live streaming: pre-create the view so the user sees the2606 // Live streaming: pre-create the view so the user sees the
2603 // tool call being composed in real time. Any subsequent2607 // tool call being composed in real time. Any subsequent
@@ -2657,6 +2661,12 @@ func (i *Interactive) handleEvent(ev core.AgentEvent) {
2657 }2661 }
2658 tc.Result = text.String()2662 tc.Result = text.String()
2659 }2663 }
2664+ // Force full repaint: tool completion shifts the layout
2665+ // (collapsed body replaces streaming) and the diff renderer
2666+ // may leave stale code fragments on status bar rows.
2667+ if i.rend != nil {
2668+ i.rend.Invalidate()
2669+ }
2660 if i.cfg.OnToolResult != nil {2670 if i.cfg.OnToolResult != nil {
2661 i.cfg.OnToolResult(e.ID, e.Result)2671 i.cfg.OnToolResult(e.ID, e.Result)
2662 }2672 }
@@ -2666,11 +2676,13 @@ func (i *Interactive) handleEvent(ev core.AgentEvent) {
2666 i.lastCtxInput = e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens2676 i.lastCtxInput = e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens
2667 }2677 }
2668 case core.EvTurnEnd:2678 case core.EvTurnEnd:
2679+ // Force full repaint: the turn ending shifts layout (streaming
2680+ // buffer flushed, status bar updates) and the diff renderer
2681+ // may leave stale content on reused rows.
2682+ if i.rend != nil {
2683+ i.rend.Invalidate()
2684+ }
2669 if e.Stop == provider.StopAborted {2685 if e.Stop == provider.StopAborted {
2670- // Aborted turn: discard the partial streaming text (it
2671- // is not persisted in the transcript) and clear any
2672- // transient error. Also drop anything still buffered in
2673- // the pacer so nothing keeps drawing after the cancel.
2674 i.resetStreamingStateLocked()2686 i.resetStreamingStateLocked()
2675 i.statusErr = ""2687 i.statusErr = ""
2676 i.statusOK = "cancelled"2688 i.statusOK = "cancelled"

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close