fix(resume-picker): keep --instruction on a picked run, tolerate blank instructions, read UTF-8 keys, fit narrow terminals, cap the window at 8 rows

This commit is contained in:
Ahmed Allam 2026-10-04 16:46:28 +00:00 • committed by Ahmed Allam
parent e954301a09
commit cd3bc8011e
6 changed files with 90 additions and 7 deletions

View file

@ -365,7 +365,7 @@ Strix Cloud:
except ValueError as error:
parser.error(f"--workspace-file: {error}")
args.user_explicit_instruction = args.instruction if args.resume else None
args.user_explicit_instruction = args.instruction if args.resume is not None else None
# What the user actually asked for, kept apart from args.instruction because
# prepare_run prepends the diff-scope preamble to that. This is the text the
# transcript shows as their opening message.

View file

@ -82,6 +82,18 @@ _FINDINGS_WIDTH = 8
_MIN_TARGET_WIDTH = 12
_MAX_RUN_WIDTH = 40
_CHROME_LINES = 8
_MAX_VISIBLE = 8
def _utf8_length(lead: bytes) -> int:
byte = lead[0] if lead else 0
if byte >= 0xF0:
return 4
if byte >= 0xE0:
return 3
if byte >= 0xC0:
return 2
return 1
class PickerUnavailableError(RuntimeError):
@ -106,8 +118,9 @@ else:
fd = stream.fileno()
saved = termios.tcgetattr(fd)
try:
tty.setcbreak(fd)
char = os.read(fd, 1).decode("utf-8", "replace")
tty.setcbreak(fd, termios.TCSANOW)
first = os.read(fd, 1)
char = (first + os.read(fd, _utf8_length(first) - 1)).decode("utf-8", "replace")
if char != "\x1b":
return char
sequence = ""
@ -200,7 +213,7 @@ class ResumePicker:
return filter_runs(self.runs, self.filter)
def _visible(self) -> int:
return max(3, min(len(self.rows), self.console.height - _CHROME_LINES))
return max(3, min(len(self.rows), _MAX_VISIBLE, self.console.height - _CHROME_LINES))
def _columns(self) -> tuple[int, int, int]:
width = max(40, self.console.width - 1)
@ -256,6 +269,8 @@ class ResumePicker:
"\u2191\u2193 move enter resume type to search esc cancel", style="dim"
)
lines.extend([Text(), footer])
for line in lines:
line.truncate(self.console.width - 1)
return lines
@staticmethod

View file

@ -72,8 +72,8 @@ def _describe_target(record: dict[str, Any]) -> str:
mount = record.get("workspace_mount")
if isinstance(mount, str) and mount:
return f"{mount} (workspace)"
instruction = record.get("user_instruction") or record.get("instruction")
return str(instruction or "").strip().splitlines()[0] if instruction else ""
instruction = str(record.get("user_instruction") or record.get("instruction") or "").strip()
return instruction.splitlines()[0] if instruction else ""
def _load_json(path: Path, *, default: Any) -> Any:

View file

@ -51,10 +51,22 @@ def test_bare_resume_defers_to_the_picker(tmp_path: Path, monkeypatch: pytest.Mo
assert args.resume is None
assert args.resume_picker is True
assert args.needs_setup is False
assert args.resume is None
assert args.targets_info == []
def test_bare_resume_keeps_the_new_instruction_as_resume_guidance(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write_run(tmp_path / "strix_runs", "example-com_1111")
monkeypatch.setattr(sys, "argv", ["strix", "--resume", "--instruction", "focus on auth"])
args = cli_main.parse_arguments()
assert args.resume_picker is True
assert args.user_explicit_instruction == "focus on auth"
def test_bare_resume_with_no_runs_is_an_error(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:

View file

@ -1,6 +1,8 @@
from __future__ import annotations
import io
import os
import sys
from datetime import UTC, datetime, timedelta
import pytest
@ -18,6 +20,7 @@ from strix.interface.resume_picker import (
ResumePicker,
filter_runs,
pick_run,
read_raw_key,
relative_time,
translate_key,
)
@ -219,3 +222,39 @@ def test_pick_run_needs_a_terminal() -> None:
console, _ = _console()
with pytest.raises(PickerUnavailableError):
pick_run(RUNS, runs_dir="strix_runs", console=console, stdin=io.StringIO())
def test_narrow_terminals_never_wrap_a_row() -> None:
console, _ = _console(width=48)
picker = ResumePicker(RUNS, console=console, runs_dir="strix_runs", now=NOW)
for line in picker.render():
assert len(line.plain) <= 47, line.plain
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX key reader")
def test_posix_key_reader_handles_multibyte_and_arrows() -> None:
master, slave = os.openpty()
try:
with os.fdopen(slave, "r+b", buffering=0) as stream:
os.write(master, "\u00e9".encode())
assert read_raw_key(stream) == "\u00e9" # type: ignore[arg-type]
os.write(master, b"\x1b[B")
assert read_raw_key(stream) == KEY_DOWN # type: ignore[arg-type]
os.write(master, b"\x1b[6~")
assert read_raw_key(stream) == "pagedown" # type: ignore[arg-type]
os.write(master, b"\r")
assert translate_key(read_raw_key(stream)) == KEY_ENTER # type: ignore[arg-type]
finally:
os.close(master)
def test_tall_terminals_still_show_a_short_window() -> None:
console, _ = _console(height=60)
runs = [
_run(f"run_{index:02d}", f"https://host{index}.example", minutes_ago=index)
for index in range(40)
]
picker = ResumePicker(runs, console=console, runs_dir="strix_runs", now=NOW)
plain = [line.plain for line in picker.render()]
assert sum(1 for line in plain if "run_" in line) == 8
assert any("more below" in line for line in plain)

View file

@ -12,6 +12,8 @@ from strix.report.runs import list_run_summaries
if TYPE_CHECKING:
from pathlib import Path
import pytest
def _write_run(
base: Path,
@ -122,3 +124,18 @@ def test_runs_without_agent_state_or_with_a_broken_record_are_listed_not_resumab
def test_missing_runs_dir_lists_nothing(tmp_path: Path) -> None:
assert list_run_summaries(cwd=tmp_path) == []
def test_blank_instruction_does_not_break_the_listing(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
run_dir = tmp_path / "strix_runs" / "blank_0001"
run_dir.mkdir(parents=True)
(run_dir / "run.json").write_text(
json.dumps({"status": "completed", "instruction": " \n "}), encoding="utf-8"
)
[summary] = list_run_summaries()
assert summary.target == ""