interactive: drain key bursts before redrawing
A drag-drop into VS Code's terminal arrives as a stream of single KeyRune events (no bracketed paste). The main loop redrew between every rune, so on a long session the path appeared to type in over several seconds instead of landing instantly. Bump the input channel buffer to 256 and, after handling one key, non-blockingly drain every other key already queued before issuing one invalidate. The whole burst processes in a single main-loop pass and the screen repaints once at the end.
- commit
- d3a4767ee6a0fad351261aa251a10ac82d7f27b9
- author
- patriceckhart · · 2026-04-28 12:21 UTC
- parent
- ef7327c
- signature
- unsigned
- refs
- tag: v0.1.38
1 file · +21 -2
M internal/agent/modes/interactive.go +21 -2
| @@ -382,8 +382,11 @@ func (i *Interactive) Run(ctx context.Context) error { | |||
| 382 | 382 | i.dialog.Open(i.cfg.ZotHome) | |
| 383 | 383 | } | |
| 384 | 384 | ||
| 385 | - | // Input goroutine. | |
| 386 | - | keys := make(chan tui.Key, 8) | |
| 385 | + | // Input goroutine. Buffered generously so a drag-drop that the | |
| 386 | + | // terminal delivers as a burst of single-character key events | |
| 387 | + | // (no bracketed paste) can be drained in one main-loop pass | |
| 388 | + | // instead of triggering a redraw per character. | |
| 389 | + | keys := make(chan tui.Key, 256) | |
| 387 | 390 | go func() { | |
| 388 | 391 | reader := tui.NewReaderWithPeek(term.ReadByte, term.PeekByteTimeout) | |
| 389 | 392 | for { | |
| @@ -476,6 +479,22 @@ func (i *Interactive) Run(ctx context.Context) error { | |||
| 476 | 479 | if done := i.handleKey(ctx, k); done { | |
| 477 | 480 | return nil | |
| 478 | 481 | } | |
| 482 | + | // Drain any keystrokes that arrived during this iteration. | |
| 483 | + | // VS Code (and other terminals that don't bracket drops as | |
| 484 | + | // paste) deliver a path one rune at a time — without this | |
| 485 | + | // loop the editor would render between every rune and a | |
| 486 | + | // long path on a heavy transcript would visibly type in. | |
| 487 | + | drain: | |
| 488 | + | for { | |
| 489 | + | select { | |
| 490 | + | case k2 := <-keys: | |
| 491 | + | if done := i.handleKey(ctx, k2); done { | |
| 492 | + | return nil | |
| 493 | + | } | |
| 494 | + | default: | |
| 495 | + | break drain | |
| 496 | + | } | |
| 497 | + | } | |
| 479 | 498 | i.invalidate() | |
| 480 | 499 | case ev := <-authEvents: | |
| 481 | 500 | i.handleAuthEvent(ev) | |
| @@ -382,8 +382,11 @@ func (i *Interactive) Run(ctx context.Context) error { | |||||
| 382 | i.dialog.Open(i.cfg.ZotHome) | 382 | i.dialog.Open(i.cfg.ZotHome) | ||
| 383 | } | 383 | } | ||
| 384 | 384 | ||||
| 385 | - | // Input goroutine. | 385 | + | // Input goroutine. Buffered generously so a drag-drop that the |
| 386 | - | keys := make(chan tui.Key, 8) | 386 | + | // terminal delivers as a burst of single-character key events |
| 387 | + | // (no bracketed paste) can be drained in one main-loop pass | |||
| 388 | + | // instead of triggering a redraw per character. | |||
| 389 | + | keys := make(chan tui.Key, 256) | |||
| 387 | go func() { | 390 | go func() { | ||
| 388 | reader := tui.NewReaderWithPeek(term.ReadByte, term.PeekByteTimeout) | 391 | reader := tui.NewReaderWithPeek(term.ReadByte, term.PeekByteTimeout) | ||
| 389 | for { | 392 | for { | ||
| @@ -476,6 +479,22 @@ func (i *Interactive) Run(ctx context.Context) error { | |||||
| 476 | if done := i.handleKey(ctx, k); done { | 479 | if done := i.handleKey(ctx, k); done { | ||
| 477 | return nil | 480 | return nil | ||
| 478 | } | 481 | } | ||
| 482 | + | // Drain any keystrokes that arrived during this iteration. | |||
| 483 | + | // VS Code (and other terminals that don't bracket drops as | |||
| 484 | + | // paste) deliver a path one rune at a time — without this | |||
| 485 | + | // loop the editor would render between every rune and a | |||
| 486 | + | // long path on a heavy transcript would visibly type in. | |||
| 487 | + | drain: | |||
| 488 | + | for { | |||
| 489 | + | select { | |||
| 490 | + | case k2 := <-keys: | |||
| 491 | + | if done := i.handleKey(ctx, k2); done { | |||
| 492 | + | return nil | |||
| 493 | + | } | |||
| 494 | + | default: | |||
| 495 | + | break drain | |||
| 496 | + | } | |||
| 497 | + | } | |||
| 479 | i.invalidate() | 498 | i.invalidate() | ||
| 480 | case ev := <-authEvents: | 499 | case ev := <-authEvents: | ||
| 481 | i.handleAuthEvent(ev) | 500 | i.handleAuthEvent(ev) | ||