diff --git a/.agents/skills/lib_py/agents/adapters/opencode.py b/.agents/skills/lib_py/agents/adapters/opencode.py index 2480d22..2935c6b 100644 --- a/.agents/skills/lib_py/agents/adapters/opencode.py +++ b/.agents/skills/lib_py/agents/adapters/opencode.py @@ -50,6 +50,12 @@ class OpenCodeAgentAdapter(BaseAgentAdapter): def input_placeholder(self) -> Optional[str]: return '' + @property + def input_placeholder_pattern(self) -> Optional[str]: + # 2.x home screen: `Ask anything… ""`, the example picked per + # launch and cut without its closing quote in a narrow box (Issue #60). + return r'Ask anything(?:…|\.\.\.)(?: "[^"]*"?)?' + @property def input_rule_pattern(self) -> Optional[str]: # Kept for lib.sh's readiness corroboration; the input box itself is a diff --git a/.agents/skills/lib_py/agents/base.py b/.agents/skills/lib_py/agents/base.py index cd27ee4..dbb234f 100644 --- a/.agents/skills/lib_py/agents/base.py +++ b/.agents/skills/lib_py/agents/base.py @@ -208,6 +208,13 @@ class BaseAgentAdapter: def input_placeholder(self) -> Optional[str]: return None + @property + def input_placeholder_pattern(self) -> Optional[str]: + """Regex the input box's first line matches in full while it shows + only a placeholder whose text varies (#60); input_placeholder stays + the literal lib.sh greps for.""" + return None + @property def input_rule_pattern(self) -> Optional[str]: return None diff --git a/.agents/skills/lib_py/agents/input_region.py b/.agents/skills/lib_py/agents/input_region.py index b44b723..71de286 100644 --- a/.agents/skills/lib_py/agents/input_region.py +++ b/.agents/skills/lib_py/agents/input_region.py @@ -17,7 +17,8 @@ def extract_input_region(pane_text: str, agent_name: str) -> str: prompt line is input: a multi-line draft is never cut to its first line. Then `input_frame_chars` are stripped, and the prompt and the placeholder - (whole-line) are stripped from the box's first non-blank line only. There + (whole-line: `input_placeholder` exactly or `input_placeholder_pattern` in + full) are stripped from the box's first non-blank line only. There is no whole-pane fallback: a pane with none of these shapes has no input region and yields a non-empty "unknown" result rather than a guess. Raises ValueError for agents without input area facts (like hermes' peers). @@ -79,7 +80,8 @@ def extract_input_region(pane_text: str, agent_name: str) -> str: if first is not None: head = re.sub(r'^\s*' + re.escape(prompt) + r'\s*', '', region_lines[first], count=1) placeholder = adapter.input_placeholder - if placeholder and head.strip() == placeholder: + pattern = adapter.input_placeholder_pattern + if (placeholder and head.strip() == placeholder) or (pattern and re.fullmatch(pattern, head.strip())): head = '' region_lines[first] = head diff --git a/tests/fixtures/panes/opencode_idle_2x_home.txt b/tests/fixtures/panes/opencode_idle_2x_home.txt new file mode 100644 index 0000000..31ccdd7 --- /dev/null +++ b/tests/fixtures/panes/opencode_idle_2x_home.txt @@ -0,0 +1,40 @@ +# captured 2026-10-18 from an idle opencode 2.0.32 home screen (no message sent), input box empty: herdr 0.9.3 `herdr agent read --source visible`, 140x40 (cwd line masked) + + + + + + + + + + + + + + + ▄ + █▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ + █ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ + ▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + + + ┃ + ┃ Ask anything… "Fix a TODO in the codebase" + ┃ + ┃ Build auto · Nemotron 3.5 Super VL Free OpenCode + ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + /…//: shift+tab agents ctrl+p commands + + + + + + + + + + + + + 2.0.32 diff --git a/tests/fixtures/panes/opencode_idle_2x_home_narrow.txt b/tests/fixtures/panes/opencode_idle_2x_home_narrow.txt new file mode 100644 index 0000000..0c03362 --- /dev/null +++ b/tests/fixtures/panes/opencode_idle_2x_home_narrow.txt @@ -0,0 +1,31 @@ +# captured 2026-10-18 from an idle opencode 2.0.32 home screen (no message sent), input box empty, width 60: tmux 60x30 `capture-pane -p`; the example is cut without its closing quote (cwd line masked) + + + + + + + + + + + ▄ + █▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ + █ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ + ▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + + + ┃ + ┃ Ask anything… "What is the tech stack of this proje + ┃ + ┃ Build · Nemotron 3.5 Super VL Free OpenCode + ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + /…/: shift+tab agents ctrl+p commands + + + + + + + + diff --git a/tests/test_issue60_linux_fixes.py b/tests/test_issue60_linux_fixes.py new file mode 100644 index 0000000..bb0a124 --- /dev/null +++ b/tests/test_issue60_linux_fixes.py @@ -0,0 +1,82 @@ +"""Issue #60, Linux check: the defects the opencode 2.x check on Linux found. + +1. The 2.x home screen's placeholder (`Ask anything… ""`, cut + without its closing quote in a narrow box) is not typed input. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +SKILLS = REPO_ROOT / ".agents" / "skills" +PANES = REPO_ROOT / "tests" / "fixtures" / "panes" +if str(SKILLS) not in sys.path: + sys.path.insert(0, str(SKILLS)) + +from lib_py import stall_watch # noqa: E402 +from lib_py.agents.input_region import extract_input_region # noqa: E402 +from lib_py.agents.registry import get_adapter # noqa: E402 + +# 140 columns (herdr capture) and 60 columns, where the example loses its +# closing quote. +HOME_SCREENS = ("opencode_idle_2x_home.txt", "opencode_idle_2x_home_narrow.txt") +PLACEHOLDER_LINE = re.compile(r'^(\s*┃\s*)Ask anything… ".*$', re.M) + + +def _pane(name: str) -> str: + return "".join(l for l in (PANES / name).read_text(encoding="utf-8").splitlines(True) if not l.startswith("# ")) + + +# --------------------------------------------------------------------------- +# 1. the 2.x home screen's placeholder +# --------------------------------------------------------------------------- +@pytest.mark.parametrize("name", HOME_SCREENS) +def test_the_2x_home_screen_reads_as_an_empty_box(name): + assert extract_input_region(_pane(name), "opencode") == "" + + +def test_the_narrow_capture_has_a_cut_example(): + # The case the pattern must keep: no closing quote on screen. + (line,) = [l for l in _pane(HOME_SCREENS[1]).splitlines() if "Ask anything" in l] + assert '"' in line and not line.rstrip().endswith('"') + + +def test_the_narrow_screen_stops_at_its_status_line_and_reads_ready(): + text = _pane(HOME_SCREENS[1]) + # Under ~60 columns the status line drops `auto`: `Build · `. + assert re.search(r"^\s*┃\s*Build · ", text, re.M) and "Build auto" not in text + assert re.search(get_adapter("opencode").ready_tokens, text) + + +@pytest.mark.parametrize("name", HOME_SCREENS) +@pytest.mark.parametrize("typed", [ + "please continue", + 'Ask anything… "x" and fix it', + "Ask anything about the build", + 'Ask anything… "a"b', +]) +def test_typed_text_is_read_as_typed(name, typed): + text, n = PLACEHOLDER_LINE.subn(lambda m: m.group(1) + typed, _pane(name), count=1) + assert n == 1 + assert extract_input_region(text, "opencode") == typed + + +def test_the_1x_capture_still_reads_empty(): + assert extract_input_region(_pane("opencode_idle.txt"), "opencode") == "" + + +@pytest.mark.parametrize("name", HOME_SCREENS) +def test_stall_watch_sees_the_2x_home_box_empty(name, monkeypatch): + src = stall_watch.HerdrSource("t", "opencode") + monkeypatch.setattr(src, "_herdr", lambda *args: _pane(name)) + assert src.input_empty() is True + + +def test_other_adapters_have_no_placeholder_pattern(): + for agent in ("claude", "agy", "grok", "codex", "hermes"): + assert get_adapter(agent).input_placeholder_pattern is None, agent