fix: ESC/Ctrl-C in picker falls back to numbered list instead of cancelling (#2390)

The TTY key loop treated explicit user cancellation (ESC/Ctrl-C) the same
as a TTY failure — both called fallback() which renders a numbered-list
picker. Now the key loop distinguishes between the two: cancel() exits
cleanly, fallback() is only used when /dev/tty is unavailable.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
L 2026-03-09 17:28:02 -04:00 committed by GitHub
parent b8ca943592
commit d9a25a4720
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 10 additions and 3 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.15.29",
"version": "0.15.30",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -120,6 +120,7 @@ type WriteFn = (s: string) => void;
interface KeyLoopCallbacks<T> {
fallback: () => T;
cancel: () => T;
init: (w: WriteFn, cols: number) => void;
handleKey: (
key: string,
@ -223,6 +224,7 @@ function withTTYKeyLoop<T>(callbacks: KeyLoopCallbacks<T>): T {
// ── key loop ────────────────────────────────────────────────────────────
const buf = Buffer.alloc(8);
let finalResult: T | undefined;
let cancelled = false;
try {
while (true) {
@ -238,8 +240,9 @@ function withTTYKeyLoop<T>(callbacks: KeyLoopCallbacks<T>): T {
const key = buf.slice(0, n).toString("binary");
// Ctrl-C / Escape — universal cancel
// Ctrl-C / Escape — explicit user cancel (not a TTY failure)
if (key === "\x03" || key === "\x1b") {
cancelled = true;
break;
}
@ -253,7 +256,10 @@ function withTTYKeyLoop<T>(callbacks: KeyLoopCallbacks<T>): T {
restore();
}
return finalResult !== undefined ? finalResult : callbacks.fallback();
if (finalResult !== undefined) {
return finalResult;
}
return cancelled ? callbacks.cancel() : callbacks.fallback();
}
// ── TTY picker ────────────────────────────────────────────────────────────────
@ -316,6 +322,7 @@ export function pickToTTYWithActions(config: PickConfig): PickResult {
return withTTYKeyLoop<PickResult>({
fallback,
cancel: () => cancel,
init(w, cols) {
maxW = cols - 1;