mirror of
https://github.com/usestrix/strix.git
synced 2026-10-05 02:41:38 +00:00
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:
parent
e954301a09
commit
cd3bc8011e
6 changed files with 90 additions and 7 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 == ""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue