Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions native/cmd/lich-native/terminals.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions native/cmd/lich-native/terminals_rides_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading