Skip to content

feat(tui): render grep results as structured per-file matches#39

Merged
hakula139 merged 2 commits intomainfrom
feat/grep-result-view
Apr 25, 2026
Merged

feat(tui): render grep results as structured per-file matches#39
hakula139 merged 2 commits intomainfrom
feat/grep-result-view

Conversation

@hakula139
Copy link
Copy Markdown
Owner

@hakula139 hakula139 commented Apr 25, 2026

Summary

Adds the next per-tool result view after Edit (#24) and Read (#30): successful grep calls in content mode now render as per-file groups of line-numbered match rows in the TUI, while the model-facing tool output stays unchanged.

  • Adds a pure-data ToolResultView::GrepMatches (Vec<GrepFileGroup> + truncated flag) so parsing stays in the grep tool and rendering stays in the TUI block layer.
  • GrepTool::result_view parses content mode only; non-content modes (files_with_matches, count), outputs with skipped-large-file warnings, and any line the parser doesn't fully recognise fall through to the default Text body — preferring "user sees raw output" over "user sees a partial structured render that silently drops information".
  • Per-file path headers + line-numbered match rows; context lines (the - separator in grep's text output) render dim so match rows stand out at a glance.
  • Match-line styling is fg-only (no background tint) — leaves the design space open for an upcoming GitHub-block diff redesign tracked separately.
  • Renderer mirrors read_excerpt's row shape so a future shared numbered_row primitive can hoist both call sites cleanly.

Changes

File Description
tool.rs Adds ToolResultView::GrepMatches, GrepFileGroup, GrepMatchLine. New registry-routing test pins the full structured shape so a mutation returning empty groups or flipping is_match doesn't sneak through.
tool/grep.rs GrepTool::result_view for content mode; parse_content_view + parse_match_line parsers with fall-through-on-failure semantics; tests cover empty / single-file / multi-file / context / -- separator / truncation footer / skipped-warning / invalid-line paths.
tui/components/chat/blocks/tool.rs New mod grep declaration + dispatch arm in ToolResultBlock::render. Module-level doc updated to list grep in the structured-output set.
tui/components/chat/blocks/tool/grep.rs (new) Per-file groups with line-numbered match rows; uniform line-number column width across groups so a 4-digit number doesn't shift the column under a 1-digit sibling group above it. footer_text helper handles all four (hidden, truncated) combinations. Renderer-level unit tests cover the empty-groups guard and the path-boundary budget guard.
tui/components/chat.rs Three render tests covering path-header + numbered rows, body-budget truncation with +N lines footer, and the server-side (limit reached) truncation marker without a phantom hidden-row count.
CLAUDE.md Crate tree picks up the new renderer module.

Test plan

  • cargo fmt --all --check
  • cargo build compiles cleanly
  • cargo clippy --all-targets -- -D warnings — zero warnings
  • cargo test — 990 passed (was 985 pre-PR; +5 tests)
  • pnpm spellcheck — clean
  • cargo llvm-cov --ignore-filename-regex 'main\.rs' — 97.84% line coverage (was 97.35% pre-PR), per-file:
    • tool/grep.rs — 98.29% region, 98.60% line
    • tui/components/chat/blocks/tool/grep.rs — 100% region, 99.58% line

Out of scope

Address in separate PRs:

  • Inter-tool spacing — borderless spacer line between a tool result and the next non-result block.
  • Inline code style — drop background tint, use a Catppuccin accent fg.
  • GitHub-block diff redesign — line numbers in a left column + full-width red / green background tint per row. Splits into three sub-PRs (data shape, numbers column, bg tint).
  • DRY refactor — extract a shared numbered_row primitive used by read_excerpt, grep, and the future diff renderer. Pure refactor; no visual change.

Adds the next per-tool result view after Edit (#24) and Read (#30):
successful `grep` calls in content mode now render as per-file groups
of line-numbered match rows. Model-facing tool output is unchanged.

`GrepTool::result_view` parses content mode only. Non-content modes
(`files_with_matches`, `count`), outputs with skipped-large-file
warnings, and any line the parser doesn't fully recognise fall through
to the default `Text` body — preferring "user sees raw output" over
"user sees a partial structured render that silently drops
information".

The renderer mirrors `read_excerpt`'s row shape so a future shared
`numbered_row` primitive can hoist both call sites cleanly. Match
rows render at full text style; context lines (the `-` separator in
grep's text output) render dim. No background tint yet — leaves the
design space open for the upcoming GitHub-block diff redesign.
@hakula139 hakula139 added the enhancement New feature or request label Apr 25, 2026
@hakula139 hakula139 self-assigned this Apr 25, 2026
@hakula139 hakula139 added the enhancement New feature or request label Apr 25, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Address PR review: trim verbose docs/comments to single-line where
the WHY fits, rename `unparseable` test to clear cspell, replace
`let-else { panic! }` test patterns with direct `assert_eq!` to drop
phantom uncovered lines, and add renderer-level unit tests for the
empty-groups guard and the path-boundary budget guard.

Patch coverage: tool/grep.rs 98.29%, blocks/tool/grep.rs 100%.
@hakula139 hakula139 merged commit 6f6800b into main Apr 25, 2026
4 checks passed
@hakula139 hakula139 deleted the feat/grep-result-view branch April 25, 2026 16:45
hakula139 added a commit that referenced this pull request Apr 26, 2026
Two cleanups surfaced by #39:

- `grep_title` filtered with `l.contains(':')`, which matched
  context rows too. Reuse `parse_match_line` and count only
  `is_match` rows.
- `format_count` emitted `paths\n\nFound N ...`; the 5-line render
  budget cut the summary but kept the trailing blank, leaving a
  stray blank before the footer. Lead with the summary so the
  renderer's title-strip removes it, mirroring the
  `files_with_matches` shape.
hakula139 added a commit that referenced this pull request Apr 26, 2026
## Summary

A bundled polish pass on the TUI rendering surfaces. Three of the items
were deferred from the grep result-view PR (#39); the fourth
(inline-code style) is a follow-up to the surface-fill experiment in #27
that turned out to read as a heavy block on transparent terminals.

- Grep title counted both match and context lines, advertising "20
matches" on a 4-match `context=2` search; now only `is_match` rows
count.
- Grep count-mode reshaped to lead with the summary line, killing a
stray trailing blank that the 5-line render budget exposed when the body
overflowed.
- Distinct tool invocations now break apart with a borderless spacer —
the `▎` bar no longer runs unbroken from one group's result into the
next group's call. `Call → Result` and parallel `Call → Call` runs stay
tight.
- Inline code drops the surface background fill in favor of an fg-only
Catppuccin peach accent, matching Claude Code's treatment.

## Changes

| File | Description |
| ---- | ----------- |
| `tool/grep.rs` | `grep_title` reuses `parse_match_line` for content
mode (counts only matches); `format_count` leads with the summary line
so the renderer's title-strip pass consumes it. |
| `tui/components/chat/blocks.rs` | New `BlockKind` enum +
`block_kind()` trait method on `ChatBlock`. |
| `tui/components/chat/blocks/tool.rs` | `ToolCallBlock` and
`ToolResultBlock` override `block_kind`. |
| `tui/components/chat.rs` | Stacker tracks `prev_kind` and inserts a
borderless spacer between any `Result` and the following non-`Result`
block. |
| `tui/theme.rs` | `inline_code()` returns `fg(user)` only — drops
`bg(surface)`. `surface` slot doc updated to reflect it has no live
consumer. |
| `tui/markdown/render.rs` | Tests updated to pin the new `inline_code`
style (`fg=user`, `bg=None`). |

## Test plan

- [x] `cargo fmt --all --check`
- [x] `cargo build` compiles cleanly
- [x] `cargo clippy --all-targets -- -D warnings` — zero warnings
- [x] `cargo test` — 993 passed (was 990 pre-PR; +3 tests: grep title
with context, count-mode summary-first, tool-result → next-call spacer)

## Out of scope

Address in separate PRs:

- DRY refactor — extract a shared `numbered_row` primitive used by
`read_excerpt`, `grep`, and the future diff renderer. Pure refactor; no
visual change.
- GitHub-block diff redesign — line numbers in a left column +
full-width red / green background tint per row. Splits into three
sub-PRs (data shape, numbers column, bg tint) on top of the
`numbered_row` primitive.
hakula139 added a commit that referenced this pull request Apr 26, 2026
)

## Summary

`read_excerpt` and `grep` (#39) each hand-rolled the same `[bar]
[number] │ [text]` row shape under their tool-result body — line-number
right-padding, text byte-budget truncation, continuation prefix
construction, span vector for the `wrap_line` call. The `Out of scope`
section of #40 deferred extracting this; this PR pulls it into a shared
`numbered_row::Renderer`. Pure refactor; no visual change. Sets up item
3 of `tui-visual-polish.md` (the GitHub-block diff redesign), which
becomes the third caller and is the place to grow `sign` and `bg_tint`
extension points when 3b / 3c land.

- `Renderer::new(ctx, border_style, number_width)` caches the
continuation prefix, its bordered span vector, and the column width once
per call site.
- `Renderer::render(out, number, text, text_style)` carries the per-row
variance: number, text, and text style (the latter is what lets grep
keep its match-vs-context dimming branch — context rows pass
`theme.dim()`, match rows pass `theme.text()`).
- Both call sites drop their inline row construction in favor of the
helper. Path-header rows in `grep` still use the direct `wrap_line` path
since they don't share the numbered shape.

## Changes

| File | Description |
| ---- | ----------- |
| `tui/components/chat/blocks/tool/numbered_row.rs` (new) | `Renderer`
struct + `render` method. 4 unit tests pinning row span order,
number-column right-padding, byte-budget ellipsis, and
continuation-prefix alignment under wrap. |
| `tui/components/chat/blocks/tool.rs` | Declares the new `numbered_row`
module. |
| `tui/components/chat/blocks/tool/read_excerpt.rs` | Body loop
collapses to a single `rows.render(...)` call. Imports trim
`MAX_TOOL_OUTPUT_LINE_BYTES`, `truncate_to_bytes`, `expand_tabs`. |
| `tui/components/chat/blocks/tool/grep.rs` | Same trim.
Match-vs-context style branch stays at the call site as the per-row
`text_style` argument. |
| `CLAUDE.md` | Crate tree picks up the new module. |

## Test plan

- [x] `cargo fmt --all --check`
- [x] `cargo build` compiles cleanly
- [x] `cargo clippy --all-targets -- -D warnings` — zero warnings
- [x] `cargo test` — 997 pass (was 993 pre-PR; +4 tests on the new
module)
- [x] `cargo llvm-cov --ignore-filename-regex 'main\.rs'` — 97.86% line
coverage (was 97.80%); per-file:
  - `numbered_row.rs` — 100% line / 100% function / 100% region
  - `read_excerpt.rs` — 99.19% line / 100% function / 100% region
  - `grep.rs` — 99.51% line / 100% function / 100% region
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant