Skip to content

feat(glob): pattern body header, parenthetical footer, structured total#45

Merged
hakula139 merged 10 commits into
mainfrom
feat/glob-followups
Apr 26, 2026
Merged

feat(glob): pattern body header, parenthetical footer, structured total#45
hakula139 merged 10 commits into
mainfrom
feat/glob-followups

Conversation

@hakula139

@hakula139 hakula139 commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Summary

Lands the four P2 follow-ups deferred from PR #44 as four atomic commits. The user-facing wins are the glob block staying self-describing once the status header scrolls away (a dim pattern (visible of total) row above the path list) and a footer shape that mirrors grep's (limit reached) parenthetical. The two internal cleanups — metadata-driven total and a shared bordered_row helper — drop a string round-trip and three open-coded row-and-wrap pairs.

  • New ToolMetadata::truncated_total lets glob_files ship the unbounded match count alongside content instead of the renderer reverse-engineering it from the (Showing X of Y matches. ...) prose footer it just emitted. The model still sees that footer; it's the renderer that stops parsing it. parse_truncation_footer is replaced by a lightweight is_truncation_footer shape gate so unknown trailing prose still falls through to text rather than misparsing as a path. Resumed sessions whose JSONL predates the field fall back to files.len() — same forward-compat shape used for diff_chunks.
  • ToolResultView::GlobFiles gains a pattern: String. Renderer emits pattern (visible of total) above the path list when results are non-empty. Header sits outside MAX_TOOL_OUTPUT_LINES — that budget gates content density, not metadata rows. Empty results suppress the header (the No files found row already labels the result, and (0 of 0) would just be noise).
  • Footer wording converges on grep's parenthetical shape: ... +N files (showing X of Y) for the combined case, ... showing N of N for the no-elision-but-tool-truncated case, ... +N files unchanged for TUI-only elision. The prior ... +N files of Y total form mixed counted-noun and footnote grammar in one footer.
  • New bordered_row module replaces five open-coded Line::from(vec![Span::styled(STATUS_LINE_CONT, ...), Span::styled(text, ...)]) + wrap_line pairs across text.rs, grep.rs, and glob.rs. Sibling to numbered_row, not a mode of it — the column shape is different enough that one renderer for both modes would dilute each contract. Always-wrap behaviour is a small bug fix: long footers in narrow terminals previously skipped wrap_line entirely and would have overflowed the bar.

Changes

File Description
tool.rs New ToolMetadata::truncated_total: Option<usize> field, with_truncated_total fluent helper, and pattern: String on ToolResultView::GlobFiles. Registry-routing test pins the full structured shape.
tool/glob.rs glob_files returns GlobOutput { content, truncated_total }; the run-loop attaches the total via metadata. parse_files_view reads the total from metadata, drops the parse_truncation_footer count parser, and keeps a lightweight is_truncation_footer shape gate. Tests cover the boundary, embedded-blank-line, inconsistent-metadata, and unrecognised-trailing-prose paths.
tui/components/chat/blocks/tool.rs Dispatch arm threads pattern through to glob::render. New mod bordered_row; declaration.
tui/components/chat/blocks/tool/bordered_row.rs (new) Shared [bar] [text] row primitive — render(out, ctx, border_style, text, text_style) always wraps. 3 unit tests pin span shape, wrap-and-bar continuation, and text-style preservation across wraps.
tui/components/chat/blocks/tool/glob.rs New pattern parameter, dim header row above the file list, parenthetical footer wording. Refactored to call bordered_row::render. Tests cover the header shape, header suppression on empty, and the new footer wording.
tui/components/chat/blocks/tool/grep.rs Refactored to call bordered_row::render for path headers and the combined footer. No behaviour change — wording stayed ... +N lines (limit reached) since grep's truncation is by line count, not match count, so it can't yet disclose the equivalent of showing X of Y.
tui/components/chat/blocks/tool/text.rs Refactored to call bordered_row::render for body rows and the +N lines footer.
tui/components/chat.rs Chat-level integration test pushes the new GlobFiles shape (with pattern) and asserts both the header and the new footer wording.
CLAUDE.md Crate tree picks up bordered_row.rs; glob.rs description refreshed for the new header / footer.
.cspell/words.txt Adds misparse (used in a doc comment in tool/glob.rs).

Test plan

  • cargo fmt --all --check
  • cargo build compiles cleanly
  • cargo clippy --all-targets -- -D warnings — zero warnings
  • cspell — clean
  • cargo test — 1066 passed (was 1061 pre-PR; +5 net: dropped 3 parse_truncation_footer tests, added 3 is_truncation_footer + 3 bordered_row + 2 producer-side coverage tests)

Out of scope

Cross-renderer footer convergence is partial: glob now uses (showing X of Y), grep stays on (limit reached). Grep's truncation is by line count (its head_limit), not match count, so disclosing the equivalent of Y would require the tool to track an unbounded total it doesn't currently compute. The metadata slot (truncated_total) is now in place for when grep can populate it — converging then becomes a one-renderer change.

Replace the renderer-side `parse_truncation_footer` round-trip with a
structured `ToolMetadata::truncated_total` field that the producer
populates directly. The model still receives the `(Showing X of Y ...)`
prose footer in `content` (it needs the total to adapt strategy), but
the result-view parser no longer reverse-engineers the count from a
string the same module emits.

`glob_files` now returns a `GlobOutput { content, truncated_total }`;
the run-loop attaches the total via `ToolOutput::with_truncated_total`.
A lightweight `is_truncation_footer` gate replaces the count-parsing
helper so unknown trailing prose still falls through to text rather
than misparsing as a path. Resumed sessions whose JSONL predates the
field fall back to `files.len()` as the total — same forward-compat
shape used for `diff_chunks`.
Add `pattern: String` to `ToolResultView::GlobFiles` and emit a dim
`pattern (visible of total)` row above the path list. Keeps the block
self-describing once the status header (`Glob(**/*.rs)`) scrolls out
of view — long chats currently strand the body without a way back to
"what did this glob match again?".

The header sits outside `MAX_TOOL_OUTPUT_LINES`: that budget gates
content density (path rows), not metadata rows. Empty results suppress
the header — the explicit `No files found` row already labels the
result, and `(0 of 0)` would just be noise. The `(visible of total)`
shape is uniform across truncation states; even `(5 of 5)` honestly
tells the reader "nothing hidden", and asymmetric formatting costs
more cognitive load than it saves redundancy.
Rework `glob`'s truncation footer to mirror the existing grep shape:
lead with what the TUI elided (`+N files`), follow with a parenthetical
disclosing the cap context (`(showing X of Y)`). Replaces the prior
`... +N files of Y total` form, which mixed counted-noun and footnote
grammar in one footer.

The no-elision-but-truncated branch becomes `... showing N of N`
mirroring grep's `... limit reached` — a single descriptor of the cap
context, no `+N` to qualify.
Three tool-result renderers each open-coded the same `Line::from(vec![
Span::styled(STATUS_LINE_CONT, border) , Span::styled(text, style) ])`
+ `wrap_line` pair across body rows, headers, and footers. Five call
sites in total — past the rule-of-three threshold for extraction.

The new `bordered_row` module is a sibling to `numbered_row`, not a
mode of it: numbered rows carry column-width state for line-number
padding and a separator span, which an unnumbered mode would dilute.
Always-wrap behavior is a small bug fix — long footers in narrow
terminals previously overflowed the bar; they now wrap with the
correct continuation prefix.
Add bordered_row.rs to the per-variant tool body tree, and refresh the
glob.rs description to reflect the new pattern body header and
parenthetical footer wording. Add `misparse` to the cspell wordlist
for a doc comment in tool/glob.rs.
@hakula139 hakula139 added the enhancement New feature or request label Apr 26, 2026
@hakula139 hakula139 self-assigned this Apr 26, 2026
@hakula139 hakula139 added the enhancement New feature or request label Apr 26, 2026
@codecov

codecov Bot commented Apr 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Codecov flagged three new code paths from this branch as uncovered:
the `with_truncated_total` helper, the `if let Some(total)` attach in
`run`, and the `extract_input_field(input, "pattern")?` guard in
`parse_files_view`. Add `run_truncated_attaches_total_to_metadata`
(end-to-end pin for the producer-side wiring) and
`result_view_falls_back_when_input_has_no_pattern` (defensive arm for
malformed input) to close the gap.
Footer arithmetic combined the unbounded universe with TUI-side
hiding (`visible = total - hidden_in_tui`), producing a number that
matched neither what the user saw nor a meaningful intermediate.
Header already discloses both counts (`pattern (visible of total)`);
footer just flags the cap with grep's `(limit reached)` token.
`parse_files_view` is a misnomer post the metadata refactor — it
no longer parses the unbounded total, only validates input and
classifies trailing prose. Rename to `build_files_view` to match.
Drop `.trim()` from `is_truncation_footer` (and its test): the
caller already passes a clean footer chunk from the
`content.trim_end()` + `rsplit_once("\n\n")` pipeline. Align the
truncation-test offset with its `glob_files` sibling so the magic
numbers do not drift.
Sessions recorded before `truncated_total` landed in metadata still
carry the count in the `(Showing X of Y matches. ...)` prose footer
that `glob_files` bakes into `content`. Without recovery, resumed
truncated views silently under-report `total = files.len()` and the
header reads `(N of N)` instead of `(N of Y)`.
Sweep over comments added/expanded in this PR. Drops restated-WHAT
lines, narrative-of-change comments, speculative future-use notes,
and exhaustive caller lists in module docs. Keeps load-bearing
rationale (defensive arms, suppressed headers, intentional
trade-offs) but trims to one line each.
@hakula139 hakula139 merged commit 497e9d1 into main Apr 26, 2026
4 checks passed
@hakula139 hakula139 deleted the feat/glob-followups branch April 26, 2026 15:15
hakula139 added a commit that referenced this pull request Apr 28, 2026
PRs #44 / #45 / #46 omit the `crates/<crate>/src/` prefix on
crate-source paths in the Changes table (writing `tool.rs`, not
`crates/oxide-code/src/tool.rs`) and keep the full path for repo-root
files. Pin that as a rule in both the PR template and CLAUDE.md so
future PRs match without re-deriving the convention from prior art.

Also add `pnpm lint` and `pnpm spellcheck` to the verification
checklist. The original PR landed with a `cspell` failure on the
new code because the node-check CI job wasn't in the local
pre-commit loop.
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