From 25209dbc6954a087ae8efcf64dc5dd6d9f7c2664 Mon Sep 17 00:00:00 2001 From: omartelo Date: Sun, 1 Nov 2026 19:04:56 -0300 Subject: [PATCH] native: a terminal's replies to the program open no echo window Measured after #1011, a busy session with its agent streaming still drew 37 frames a second, not 24: the terminal's answers to the program's queries (cursor position, device attributes, which Claude Code asks for as it draws) went through send and counted as someone typing, so the output that followed always drew at once instead of riding the ring's frame. Replies now go through their own write and leave the echo window closed. --- native/cmd/lich-native/terminals.go | 16 +++++++++++++-- .../cmd/lich-native/terminals_rides_test.go | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/native/cmd/lich-native/terminals.go b/native/cmd/lich-native/terminals.go index d312d80b..f7391a9d 100644 --- a/native/cmd/lich-native/terminals.go +++ b/native/cmd/lich-native/terminals.go @@ -239,7 +239,20 @@ func (ts *terminals) openLink(url string) { }() } +// send writes what the person typed, pasted or scrolled to session id's +// PTY: what comes back next is its echo, drawn at once. func (ts *terminals) send(id string, b []byte) { + if len(b) == 0 { + return + } + ts.sent.Store(time.Now().UnixNano()) + ts.reply(id, b) +} + +// reply writes the terminal's answer to a query of the program's (cursor +// position, device attributes) to session id's PTY. A program polling the +// terminal is no one typing, so its answers never open the echo window. +func (ts *terminals) reply(id string, b []byte) { if len(b) == 0 { return } @@ -249,7 +262,6 @@ func (ts *terminals) send(id string, b []byte) { if t != nil && t.readOnly.Load() { return } - ts.sent.Store(time.Now().UnixNano()) if err := stream.Send(context.Background(), id, b); err != nil { log.Printf("send to %s: %v", id, err) } @@ -314,7 +326,7 @@ func (ts *terminals) attach(ctx context.Context, t *termView, cols, rows int) er cellW, cellH := t.r.Cell() vt, err := gioterm.New(cols, rows, cellW, cellH, func(b []byte) { if t.seeded.Load() { - ts.send(t.id, b) + ts.reply(t.id, b) } }) if err != nil { diff --git a/native/cmd/lich-native/terminals_rides_test.go b/native/cmd/lich-native/terminals_rides_test.go index c9644889..2cf98088 100644 --- a/native/cmd/lich-native/terminals_rides_test.go +++ b/native/cmd/lich-native/terminals_rides_test.go @@ -70,3 +70,23 @@ func TestAKeystrokesEchoNeverWaits(t *testing.T) { t.Errorf("output past the echo window asked for %d frames, want to ride the loop", *frames) } } + +// A program polling the terminal (Claude Code asks for the cursor position +// as it draws) gets its answers written to the PTY all the time; those are +// no one typing, and must not keep output off the looping frame. +func TestAQuerysReplyOpensNoEchoWindow(t *testing.T) { + ts, th, frames := ridingTerminals(t) + // Read-only: the writes stop short of the stream this test has none of. + ts.byID["s"].readOnly.Store(true) + spin(th, time.Now()) + ts.reply("s", []byte("\x1b[24;80R")) + ts.onOutput("s", []byte("a")) + if *frames != 0 { + t.Errorf("output after a reply asked for %d frames, want to ride the loop", *frames) + } + ts.send("s", []byte("k")) + ts.onOutput("s", []byte("k")) + if *frames != 1 { + t.Errorf("output after a keystroke asked for %d frames, want one at once", *frames) + } +}