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 { | |||
| 711 | 711 | if seq := tui.ReportCWD(i.cfg.CWD); seq != "" { | |
| 712 | 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 | 719 | i.applyInputCursorColor() | |
| 716 | 720 | ||
| 717 | 721 | // 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 rate | 721 | // 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 | + | } | |||