detached 9fa3d04

Clear interactive frame on exit

commit
9fa3d049c18ec4a809642699c61d3e0cf293f030
author
patriceckhart · · 2026-08-11 07:10 UTC
parent
dad7bb4
signature
unsigned
refs
tag: v0.3.41
2 files · +53 -1
M packages/agent/modes/interactive.go +5 -1
@@ -711,7 +711,11 @@ func (i *Interactive) Run(ctx context.Context) error {
711711 if seq := tui.ReportCWD(i.cfg.CWD); seq != "" {
712712 _, _ = term.Write([]byte(seq))
713713 }
714- defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff + tui.ResetCursorColor() + tui.ResetCursorShape() + tui.SeqShowCursor))
714+ // Erase the live frame on exit without moving the cursor. The shell can
715+ // then draw its prompt on the row where zot ended instead of leaving the
716+ // inactive TUI visible above it. Do not erase scrollback: users should
717+ // still be able to review the session after closing zot.
718+ defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff + tui.ResetCursorColor() + tui.ResetCursorShape() + tui.SeqClearScreenNoHome + tui.SeqShowCursor))
715719 i.applyInputCursorColor()
716720
717721 // Streaming pacer: drains buffered text deltas at a steady rate
@@ -711,7 +711,11 @@ func (i *Interactive) Run(ctx context.Context) error {
711 if seq := tui.ReportCWD(i.cfg.CWD); seq != "" {711 if seq := tui.ReportCWD(i.cfg.CWD); seq != "" {
712 _, _ = term.Write([]byte(seq))712 _, _ = term.Write([]byte(seq))
713 }713 }
714- defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff + tui.ResetCursorColor() + tui.ResetCursorShape() + tui.SeqShowCursor))714+ // Erase the live frame on exit without moving the cursor. The shell can
715+ // then draw its prompt on the row where zot ended instead of leaving the
716+ // inactive TUI visible above it. Do not erase scrollback: users should
717+ // still be able to review the session after closing zot.
718+ defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff + tui.ResetCursorColor() + tui.ResetCursorShape() + tui.SeqClearScreenNoHome + tui.SeqShowCursor))
715 i.applyInputCursorColor()719 i.applyInputCursorColor()
716 720
717 // Streaming pacer: drains buffered text deltas at a steady rate721 // Streaming pacer: drains buffered text deltas at a steady rate
A packages/agent/modes/interactive_terminal_test.go +48 -0
@@ -0,0 +1,48 @@
1+package modes
2+
3+import (
4+ "bytes"
5+ "context"
6+ "errors"
7+ "io"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/patriceckhart/zot/packages/tui"
13+)
14+
15+type cleanupTestTerminal struct {
16+ bytes.Buffer
17+}
18+
19+func (t *cleanupTestTerminal) Size() (int, int) { return 80, 24 }
20+func (t *cleanupTestTerminal) OnResize(func()) {}
21+func (t *cleanupTestTerminal) SetNonblock(bool) error { return nil }
22+func (t *cleanupTestTerminal) ReadByte() (byte, error) { return 0, io.EOF }
23+func (t *cleanupTestTerminal) EnterRaw() (func() error, error) {
24+ return func() error { return nil }, nil
25+}
26+func (t *cleanupTestTerminal) PeekByteTimeout(time.Duration) (byte, bool, error) {
27+ return 0, false, io.EOF
28+}
29+
30+func TestInteractiveRunClearsVisibleFrameOnExit(t *testing.T) {
31+ term := &cleanupTestTerminal{}
32+ interactive := NewInteractive(InteractiveConfig{Terminal: term})
33+ ctx, cancel := context.WithCancel(context.Background())
34+ cancel()
35+
36+ err := interactive.Run(ctx)
37+ if !errors.Is(err, context.Canceled) {
38+ t.Fatalf("Run error = %v, want context.Canceled", err)
39+ }
40+
41+ cleanup := tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages +
42+ tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff +
43+ tui.ResetCursorColor() + tui.ResetCursorShape() +
44+ tui.SeqClearScreenNoHome + tui.SeqShowCursor
45+ if got := term.String(); !strings.HasSuffix(got, cleanup) {
46+ t.Fatalf("terminal output does not end with frame cleanup\ngot suffix: %q\nwant: %q", got, cleanup)
47+ }
48+}
@@ -0,0 +1,48 @@
1+package modes
2+
3+import (
4+ "bytes"
5+ "context"
6+ "errors"
7+ "io"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/patriceckhart/zot/packages/tui"
13+)
14+
15+type cleanupTestTerminal struct {
16+ bytes.Buffer
17+}
18+
19+func (t *cleanupTestTerminal) Size() (int, int) { return 80, 24 }
20+func (t *cleanupTestTerminal) OnResize(func()) {}
21+func (t *cleanupTestTerminal) SetNonblock(bool) error { return nil }
22+func (t *cleanupTestTerminal) ReadByte() (byte, error) { return 0, io.EOF }
23+func (t *cleanupTestTerminal) EnterRaw() (func() error, error) {
24+ return func() error { return nil }, nil
25+}
26+func (t *cleanupTestTerminal) PeekByteTimeout(time.Duration) (byte, bool, error) {
27+ return 0, false, io.EOF
28+}
29+
30+func TestInteractiveRunClearsVisibleFrameOnExit(t *testing.T) {
31+ term := &cleanupTestTerminal{}
32+ interactive := NewInteractive(InteractiveConfig{Terminal: term})
33+ ctx, cancel := context.WithCancel(context.Background())
34+ cancel()
35+
36+ err := interactive.Run(ctx)
37+ if !errors.Is(err, context.Canceled) {
38+ t.Fatalf("Run error = %v, want context.Canceled", err)
39+ }
40+
41+ cleanup := tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages +
42+ tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff +
43+ tui.ResetCursorColor() + tui.ResetCursorShape() +
44+ tui.SeqClearScreenNoHome + tui.SeqShowCursor
45+ if got := term.String(); !strings.HasSuffix(got, cleanup) {
46+ t.Fatalf("terminal output does not end with frame cleanup\ngot suffix: %q\nwant: %q", got, cleanup)
47+ }
48+}

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