detached c55f92f

Clear interactive frame on exit

commit
c55f92fb9eac251006d9db375bab9a6b2fbdad63
author
patriceckhart · · 2026-08-11 07:10 UTC
committer
brice · · 2026-08-12 12:07 UTC
parent
84a9305
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +53 -1
M packages/agent/modes/interactive.go +5 -1
@@ -1092,7 +1092,11 @@ func (i *Interactive) Run(ctx context.Context) error {
10921092 if seq := tui.ReportCWD(i.cfg.CWD); seq != "" {
10931093 _, _ = term.Write([]byte(seq))
10941094 }
1095- defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff + tui.ResetCursorColor() + tui.ResetCursorShape() + tui.SeqShowCursor))
1095+ // Erase the live frame on exit without moving the cursor. The shell can
1096+ // then draw its prompt on the row where zot ended instead of leaving the
1097+ // inactive TUI visible above it. Do not erase scrollback: users should
1098+ // still be able to review the session after closing zot.
1099+ defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff + tui.ResetCursorColor() + tui.ResetCursorShape() + tui.SeqClearScreenNoHome + tui.SeqShowCursor))
10961100 i.applyInputCursorColor()
10971101
10981102 // Streaming pacer: drains buffered text deltas at a steady rate
@@ -1092,7 +1092,11 @@ func (i *Interactive) Run(ctx context.Context) error {
1092 if seq := tui.ReportCWD(i.cfg.CWD); seq != "" {1092 if seq := tui.ReportCWD(i.cfg.CWD); seq != "" {
1093 _, _ = term.Write([]byte(seq))1093 _, _ = term.Write([]byte(seq))
1094 }1094 }
1095- defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff + tui.ResetCursorColor() + tui.ResetCursorShape() + tui.SeqShowCursor))1095+ // Erase the live frame on exit without moving the cursor. The shell can
1096+ // then draw its prompt on the row where zot ended instead of leaving the
1097+ // inactive TUI visible above it. Do not erase scrollback: users should
1098+ // still be able to review the session after closing zot.
1099+ defer term.Write([]byte(tui.SeqResetScrollRegion + tui.SeqDeleteKittyImages + tui.SeqEnhancedKeyboardOff + tui.SeqBracketedPasteOff + tui.ResetCursorColor() + tui.ResetCursorShape() + tui.SeqClearScreenNoHome + tui.SeqShowCursor))
1096 i.applyInputCursorColor()1100 i.applyInputCursorColor()
1097 1101
1098 // Streaming pacer: drains buffered text deltas at a steady rate1102 // 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/bnema/zut/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/bnema/zut/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