fix(resume-picker): treat ctrl+c as cancel instead of leaking a KeyboardInterrupt traceback

This commit is contained in:
Ahmed Allam 2026-10-04 17:34:55 +00:00 • committed by Ahmed Allam
parent 7a31053348
commit 28fa7f7734
2 changed files with 23 additions and 7 deletions

View file

@ -331,7 +331,7 @@ class ResumePicker:
"""Apply one key: (done, run), where done with no run means cancelled."""
self.notice = ""
if key == KEY_INTERRUPT:
raise KeyboardInterrupt
return True, None
if key == KEY_ESCAPE:
if not self.filter:
return True, None
@ -380,7 +380,11 @@ class ResumePicker:
try:
self.draw()
while True:
done, chosen = self.handle(next_key())
try:
key = next_key()
except KeyboardInterrupt:
key = KEY_INTERRUPT
done, chosen = self.handle(key)
if done:
return chosen
self.draw()

View file

@ -182,11 +182,6 @@ def test_no_match_renders_a_hint_and_enter_does_nothing() -> None:
assert picker.handle(KEY_ENTER) == (False, None)
def test_ctrl_c_raises_keyboard_interrupt() -> None:
with pytest.raises(KeyboardInterrupt):
_picker().handle(KEY_INTERRUPT)
def test_long_lists_scroll_to_keep_the_cursor_visible() -> None:
runs = [
_run(f"run_{index:02d}", f"https://host{index}.example", minutes_ago=index)
@ -218,6 +213,23 @@ def test_run_redraws_in_place_and_restores_the_cursor() -> None:
assert "example-com_223e" in output
def test_ctrl_c_cancels_like_escape() -> None:
console, _ = _console()
picker = ResumePicker(RUNS, console=console, runs_dir="strix_runs", now=NOW)
assert picker.handle(KEY_INTERRUPT) == (True, None)
def test_sigint_while_waiting_for_a_key_cancels_and_restores_the_cursor() -> None:
console, buffer = _console()
picker = ResumePicker(RUNS, console=console, runs_dir="strix_runs", now=NOW)
def interrupted() -> str:
raise KeyboardInterrupt
assert picker.run(interrupted) is None
assert buffer.getvalue().endswith("\x1b[?25h")
def test_pick_run_needs_a_terminal() -> None:
console, _ = _console()
with pytest.raises(PickerUnavailableError):