diff --git a/clients/terminal/tui/renderers/diff.py b/clients/terminal/tui/renderers/diff.py index dc04252..b8db8b1 100644 --- a/clients/terminal/tui/renderers/diff.py +++ b/clients/terminal/tui/renderers/diff.py @@ -14,8 +14,13 @@ # ``{marker} {num}│ {content}`` — the line-number prefix the server # (``navi/tools/filesystem.py:_number_diff``) prepends to each diff content line. -# A fixed space follows the marker, then the right-aligned number and ``│ ``. -_DIFF_NUM_RE = re.compile(r"^ (\d+)│ ?(.*)$", re.DOTALL) +# A fixed space follows the marker, then the right-aligned number (with its own +# leading spaces, one per digit of width) and ``│ ``. We capture the number's +# leading spaces separately so the column stays right-aligned when we render +# the number first (``{num} {marker}``) instead of the server's marker-first +# order — and so files with more than 9 lines (width > 1) still match, which the +# old single-space regex silently dropped, falling back to whole-line colour. +_DIFF_NUM_RE = re.compile(r"^( +)(\d+)│ ?(.*)$", re.DOTALL) def _diff_marker_style(marker: str, theme: Theme) -> str: @@ -30,10 +35,13 @@ """Color a unified-diff string: ``+`` green, ``-`` red, ``@@`` dim, rest text. When a content line carries a server-prefixed line number - (``{marker} {num}│ {content}``), the ``{marker} {num}│`` column is rendered - dim and only the content takes the marker color — so the number reads as - meta, not as part of the added/removed text. Lines without the prefix (e.g. - the standalone ``diff`` event, or older callers) are colored whole, as before. + (``{marker} {num}│ {content}``), the number and marker are shown in the + marker color (green/red) and the content is rendered neutral (``theme.text``) + — the number and marker act as a coloured anchor, the code itself reads + plainly. The display order is ``{num} {marker} {content}`` (number first); + the server still emits marker-first in the model-facing text, so the agent's + contract is unchanged. Lines without the prefix (e.g. the standalone + ``diff`` event, or older callers) are colored whole, as before. Shared by the standalone ``diff`` event renderer and the ``filesystem`` tool-call renderer (``edit_lines`` / ``smart_edit`` / ``diff`` actions embed @@ -53,8 +61,15 @@ rest = line[1:] m = _DIFF_NUM_RE.match(rest) if m: - out.append(f"{marker} {m.group(1)}│ ", style=theme.text_dim.hex) - out.append(m.group(2), style=_diff_marker_style(marker, theme)) + # {leading}{num} keeps the server's right-alignment (leading spaces + # come from {num:>width}); we then show the marker and two spaces + # before the content. Number+marker in the marker colour, content + # neutral. + out.append( + f"{m.group(1)}{m.group(2)} {marker} ", + style=_diff_marker_style(marker, theme), + ) + out.append(m.group(3), style=theme.text.hex) else: out.append(line, style=_diff_marker_style(marker, theme)) return out diff --git a/clients/terminal/tui/renderers/filesystem.py b/clients/terminal/tui/renderers/filesystem.py index 24f83bf..8ad364f 100644 --- a/clients/terminal/tui/renderers/filesystem.py +++ b/clients/terminal/tui/renderers/filesystem.py @@ -170,16 +170,22 @@ idx = 2 # Body: file contents (no syntax highlighting — rejected by design). + # Default matches FilesystemTool._read (numbered=true) so a read the model + # issued without an explicit `numbered` arg still gets the number column + # highlighted when the tool numbered the output. The line number is the + # accent colour (an anchor for navigation); the content is neutral text — + # the same "number coloured, content neutral" standard the diff renderer + # uses. body = lines[idx:] if body: out.append("\n") - if args.get("numbered", False): + if args.get("numbered", True): for j, ln in enumerate(body): if j: out.append("\n") num, sep2, content = ln.partition(": ") if sep2: - out.append(num + ": ", style=theme.text_dim.hex) + out.append(num + ": ", style=theme.accent.hex) out.append(content, style=theme.text.hex) else: out.append(ln, style=theme.text.hex) diff --git a/tests/clients/test_filesystem_renderer.py b/tests/clients/test_filesystem_renderer.py index 62d5be3..7ef71bc 100644 --- a/tests/clients/test_filesystem_renderer.py +++ b/tests/clients/test_filesystem_renderer.py @@ -176,8 +176,11 @@ def test_diff_line_number_column_is_dim() -> None: - """Server-prefixed ``{marker} {num}│`` column renders dim; content keeps the - marker color.""" + """Server-prefixed ``{marker} {num}│ {content}`` lines render with the + number+marker in the marker colour (green/red) and the content neutral + (theme.text) — the number is a coloured anchor, the code reads plainly. + Display order is ``{num} {marker} {content}``; the server still emits + marker-first in the model-facing text, so the agent's contract is unchanged.""" set_active_theme("gnexus-dark") theme = get_active_theme() renderer = FilesystemToolResultRenderer() @@ -193,17 +196,46 @@ } body = _body(renderer.render(msg)) assert isinstance(body, Text) - # The number prefix is dim (marker + number + separator). - assert _span_styles_at(body, "- 2│") == {theme.text_dim.hex} - assert _span_styles_at(body, "+ 2│") == {theme.text_dim.hex} - # The content after the prefix keeps the marker color. - assert _span_styles_at(body, "BETA") == {theme.success.hex} - assert _span_styles_at(body, "beta") == {theme.error.hex} + # The number+marker anchor takes the marker colour (red for removed). + assert _span_styles_at(body, "2 - ") == {theme.error.hex} + # ...and green for added. + assert _span_styles_at(body, "2 + ") == {theme.success.hex} + # The content after the anchor is neutral (theme.text), not the marker colour. + assert _span_styles_at(body, "BETA") == {theme.text.hex} + assert _span_styles_at(body, "beta") == {theme.text.hex} + assert _span_styles_at(body, "alpha") == {theme.text.hex} # Hunk header is dim, file headers are plain text. assert theme.text_dim.hex in _span_styles_at(body, "@@ -1,3 +1,3 @@") -def test_edit_without_metadata_falls_back_to_plain() -> None: +def test_diff_numbered_lines_beyond_nine_rows_parses_width() -> None: + """The server right-aligns line numbers to the max width, so a diff of a + >9-line file has multi-space leading on single-digit rows. The number regex + must still match these (the old single-space regex dropped them to the + fallback path and coloured the whole line). Number+marker keep the marker + colour; content is neutral.""" + set_active_theme("gnexus-dark") + theme = get_active_theme() + renderer = FilesystemToolResultRenderer() + msg = { + "type": "tool_call", + "tool": "filesystem", + "args": {"action": "diff"}, + "result": ( + "--- a.py\n+++ b.py\n@@ -1,12 +1,12 @@\n" + " 1│ a\n 10│ j\n 11│ k\n- 12│ old\n+ 12│ NEW\n" + ), + "success": True, + } + body = _body(renderer.render(msg)) + assert isinstance(body, Text) + # Single-digit row (1) and double-digit rows (12) both parse: the anchor + # (num+marker) carries the marker colour, not the whole line. + assert _span_styles_at(body, " 12 - ") == {theme.error.hex} + assert _span_styles_at(body, " 12 + ") == {theme.success.hex} + # Content is neutral. + assert _span_styles_at(body, "old") == {theme.text.hex} + assert _span_styles_at(body, "NEW") == {theme.text.hex} set_active_theme("gnexus-dark") theme = get_active_theme() renderer = FilesystemToolResultRenderer() @@ -520,8 +552,9 @@ } body = _body(renderer.render(msg)) assert isinstance(body, Text) - # Line numbers are dim, line content is text. - assert _span_styles_at(body, "1") == {theme.text_dim.hex} + # Line numbers are the accent colour (navigation anchor); content is neutral. + assert theme.accent.hex in _span_styles_at(body, "1: ") + assert theme.accent.hex in _span_styles_at(body, "2: ") assert _span_styles_at(body, "foo") == {theme.text.hex} assert _span_styles_at(body, "bar") == {theme.text.hex} diff --git a/webclient/src/components/messages/ToolCard.vue b/webclient/src/components/messages/ToolCard.vue index 2b19ab9..14e9bc4 100644 --- a/webclient/src/components/messages/ToolCard.vue +++ b/webclient/src/components/messages/ToolCard.vue @@ -34,7 +34,7 @@