fix(opencode): the 2.x home placeholder is not typed input, cut example included (Issue #60, Linux item 1)
An adapter can now name its placeholder as a pattern the box's first line matches in full (input_placeholder_pattern); input_placeholder stays the literal lib.sh greps for. opencode: `Ask anything… "<example>"`, the closing quote optional because a box under ~65 columns cuts the example. Fixtures: the 140-column herdr capture and a 60-column one with the cut example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d49abb371
commit
14d7a46181
6 files changed
+170
-2
No files matched your search
@@ -50,6 +50,12 @@ class OpenCodeAgentAdapter(BaseAgentAdapter):
|
|||||||
def input_placeholder(self) -> Optional[str]:
|
def input_placeholder(self) -> Optional[str]:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_placeholder_pattern(self) -> Optional[str]:
|
||||||
|
# 2.x home screen: `Ask anything… "<example>"`, the example picked per
|
||||||
|
# launch and cut without its closing quote in a narrow box (Issue #60).
|
||||||
|
return r'Ask anything(?:…|\.\.\.)(?: "[^"]*"?)?'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def input_rule_pattern(self) -> Optional[str]:
|
def input_rule_pattern(self) -> Optional[str]:
|
||||||
# Kept for lib.sh's readiness corroboration; the input box itself is a
|
# Kept for lib.sh's readiness corroboration; the input box itself is a
|
||||||
|
|||||||
@@ -208,6 +208,13 @@ class BaseAgentAdapter:
|
|||||||
def input_placeholder(self) -> Optional[str]:
|
def input_placeholder(self) -> Optional[str]:
|
||||||
return None
|
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
|
@property
|
||||||
def input_rule_pattern(self) -> Optional[str]:
|
def input_rule_pattern(self) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -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.
|
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
|
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
|
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.
|
region and yields a non-empty "unknown" result rather than a guess.
|
||||||
Raises ValueError for agents without input area facts (like hermes' peers).
|
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:
|
if first is not None:
|
||||||
head = re.sub(r'^\s*' + re.escape(prompt) + r'\s*', '', region_lines[first], count=1)
|
head = re.sub(r'^\s*' + re.escape(prompt) + r'\s*', '', region_lines[first], count=1)
|
||||||
placeholder = adapter.input_placeholder
|
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 = ''
|
head = ''
|
||||||
region_lines[first] = head
|
region_lines[first] = head
|
||||||
|
|
||||||
|
|||||||
+40
@@ -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 <session> --source visible`, 140x40 (cwd line masked)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
▄
|
||||||
|
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
|
||||||
|
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
|
||||||
|
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
|
||||||
|
|
||||||
|
|
||||||
|
┃
|
||||||
|
┃ Ask anything… "Fix a TODO in the codebase"
|
||||||
|
┃
|
||||||
|
┃ Build auto · Nemotron 3.5 Super VL Free OpenCode
|
||||||
|
╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||||
|
/…/<repo>/<worktree>:<branch> shift+tab agents ctrl+p commands
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2.0.32
|
||||||
@@ -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
|
||||||
|
╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||||
|
/…/<worktree>:<branch> shift+tab agents ctrl+p commands
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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… "<example>"`, 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 · <model>`.
|
||||||
|
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
|
||||||
Reference in new issue
Block a user