diff --git a/clients/terminal/tui/renderers/filesystem.py b/clients/terminal/tui/renderers/filesystem.py index abb5b24..d7dbfc1 100644 --- a/clients/terminal/tui/renderers/filesystem.py +++ b/clients/terminal/tui/renderers/filesystem.py @@ -87,8 +87,12 @@ return self._render_read(text, args, theme) if action == "info": return self._render_info(text, theme) - # write / edit / append / move / copy / delete / mkdir / exists / find / - # find_up / query / unknown → plain, unchanged. + if action == "find": + return self._render_find(text, theme) + if action == "find_up": + return self._render_find_up(text, theme) + # write / edit / append / move / copy / delete / mkdir / exists / query / + # unknown → plain, unchanged. return Text(text, style=theme.text_dim.hex) # ── diff ─────────────────────────────────────────────────────────────── @@ -180,6 +184,49 @@ out.append(line, style=theme.text.hex) return out + # ── find ─────────────────────────────────────────────────────────────── + + def _render_find(self, text: str, theme: Theme) -> RenderableType: + lines = text.splitlines() + if not lines: + return Text(text, style=theme.text_dim.hex) + + out = Text() + for idx, line in enumerate(lines): + if idx: + out.append("\n") + if idx == 0 and line.startswith("["): + style = ( + theme.warning.hex + if _TRUNCATED_MARKER in line + else theme.text_dim.hex + ) + out.append(line, style=style) + continue + if line.startswith("No matches for "): + out.append(line, style=theme.text_dim.hex) + continue + # Match line: "{path} ({size})" or "" instead of size. + path_part, sep, tail = line.rpartition(" (") + if sep: + out.append(path_part, style=theme.text.hex) + out.append(" (", style=theme.text_dim.hex) + if tail == ")": + out.append("", style=theme.info.hex) + out.append(")", style=theme.text_dim.hex) + else: + out.append(tail, style=theme.text_dim.hex) + else: + out.append(line, style=theme.text.hex) + return out + + # ── find_up ──────────────────────────────────────────────────────────── + + def _render_find_up(self, text: str, theme: Theme) -> RenderableType: + if text.startswith("not found (searched:"): + return Text(text, style=theme.text_dim.hex) + return Text(text, style=theme.accent.hex) + # ── list ─────────────────────────────────────────────────────────────── def _render_list(self, text: str, theme: Theme) -> RenderableType: diff --git a/tests/clients/test_filesystem_renderer.py b/tests/clients/test_filesystem_renderer.py index 193b7cd..6de92cc 100644 --- a/tests/clients/test_filesystem_renderer.py +++ b/tests/clients/test_filesystem_renderer.py @@ -270,6 +270,102 @@ assert "a.py:1: x y" in body.plain +# ── find ────────────────────────────────────────────────────────────────────── + + +def test_find_highlights_header_and_match_lines() -> None: + set_active_theme("gnexus-dark") + theme = get_active_theme() + renderer = FilesystemToolResultRenderer() + msg = { + "type": "tool_call", + "tool": "filesystem", + "args": {"action": "find", "pattern": "*.py"}, + "result": ( + "[2 matches for '*.py' in /proj]\n" + "/proj/a.py (1.2K)\n" + "/proj/src ()\n" + ), + "success": True, + } + body = _body(renderer.render(msg)) + assert isinstance(body, Text) + # Header plaque is dim. + assert theme.text_dim.hex in _span_styles_at(body, "2 matches") + # Match path in text, size in dim. + assert theme.text.hex in _span_styles_at(body, "/proj/a.py") + assert theme.text_dim.hex in _span_styles_at(body, "1.2K") + # "" marker in info color. + assert theme.info.hex in _span_styles_at(body, "") + + +def test_find_truncated_header_uses_warning_color() -> None: + set_active_theme("gnexus-dark") + theme = get_active_theme() + renderer = FilesystemToolResultRenderer() + msg = { + "type": "tool_call", + "tool": "filesystem", + "args": {"action": "find", "pattern": "*"}, + "result": "[200 matches for '*' in /proj ⚠ showing first 200]\n/p/a (0B)\n", + "success": True, + } + body = _body(renderer.render(msg)) + assert isinstance(body, Text) + assert theme.warning.hex in _span_styles_at(body, "showing first 200") + + +def test_find_no_matches_renders_dim() -> None: + set_active_theme("gnexus-dark") + theme = get_active_theme() + renderer = FilesystemToolResultRenderer() + msg = { + "type": "tool_call", + "tool": "filesystem", + "args": {"action": "find", "pattern": "*.x"}, + "result": "No matches for '*.x' in /proj", + "success": True, + } + body = _body(renderer.render(msg)) + assert isinstance(body, Text) + assert _span_styles_at(body, "No matches") == {theme.text_dim.hex} + + +# ── find_up ─────────────────────────────────────────────────────────────────── + + +def test_find_up_found_renders_path_in_accent() -> None: + set_active_theme("gnexus-dark") + theme = get_active_theme() + renderer = FilesystemToolResultRenderer() + msg = { + "type": "tool_call", + "tool": "filesystem", + "args": {"action": "find_up", "pattern": "pyproject.toml"}, + "result": "/proj/pyproject.toml", + "success": True, + } + body = _body(renderer.render(msg)) + assert isinstance(body, Text) + assert _span_styles_at(body, "/proj/pyproject.toml") == {theme.accent.hex} + + +def test_find_up_not_found_renders_dim() -> None: + set_active_theme("gnexus-dark") + theme = get_active_theme() + renderer = FilesystemToolResultRenderer() + msg = { + "type": "tool_call", + "tool": "filesystem", + "args": {"action": "find_up", "pattern": "x.txt"}, + "result": "not found (searched: /a/x.txt, /x.txt)", + "success": True, + } + body = _body(renderer.render(msg)) + assert isinstance(body, Text) + assert _span_styles_at(body, "not found") == {theme.text_dim.hex} + + # ── fallback ──────────────────────────────────────────────────────────────────