feat(tui): render grep results as structured per-file matches#39
Merged
feat(tui): render grep results as structured per-file matches#39
Conversation
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.
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%.
4 tasks
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.
5 tasks
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the next per-tool result view after Edit (#24) and Read (#30): successful
grepcalls 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.ToolResultView::GrepMatches(Vec<GrepFileGroup>+truncatedflag) so parsing stays in thegreptool and rendering stays in the TUI block layer.GrepTool::result_viewparses 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 defaultTextbody — preferring "user sees raw output" over "user sees a partial structured render that silently drops information".-separator in grep's text output) render dim so match rows stand out at a glance.read_excerpt's row shape so a future sharednumbered_rowprimitive can hoist both call sites cleanly.Changes
tool.rsToolResultView::GrepMatches,GrepFileGroup,GrepMatchLine. New registry-routing test pins the full structured shape so a mutation returning empty groups or flippingis_matchdoesn't sneak through.tool/grep.rsGrepTool::result_viewfor content mode;parse_content_view+parse_match_lineparsers 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.rsmod grepdeclaration + dispatch arm inToolResultBlock::render. Module-level doc updated to list grep in the structured-output set.tui/components/chat/blocks/tool/grep.rs(new)footer_texthelper 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+N linesfooter, and the server-side(limit reached)truncation marker without a phantom hidden-row count.CLAUDE.mdTest plan
cargo fmt --all --checkcargo buildcompiles cleanlycargo clippy --all-targets -- -D warnings— zero warningscargo test— 990 passed (was 985 pre-PR; +5 tests)pnpm spellcheck— cleancargo 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% linetui/components/chat/blocks/tool/grep.rs— 100% region, 99.58% lineOut of scope
Address in separate PRs:
numbered_rowprimitive used byread_excerpt,grep, and the future diff renderer. Pure refactor; no visual change.