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) + } +}