Skip to content

feat: Grep and Glob results render as clickable locations - #288

Merged
oratis merged 1 commit into
mainfrom
claude/grep-glob-locations
Sep 3, 2026
Merged

feat: Grep and Glob results render as clickable locations#288
oratis merged 1 commit into
mainfrom
claude/grep-glob-locations

Conversation

@oratis

@oratis oratis commented Sep 3, 2026

Copy link
Copy Markdown
Owner

What

The fourth render intent from the DSH adoption plan (§1.6), specified there and deferred by #271: Grep/Glob cards in the desktop rendered as a grey text blob; now they render as a list of openable file locations — line number and matched text included in content mode — each row wired to the same file-panel opener the card header already uses.

Why parsing the result text was the wrong design (and what this does instead)

The deferral reason was real: re-deriving entries from the formatted result text means parsing path:line:text, whose : separator is ambiguous the moment a path contains one. Grep itself dodges that ambiguity internally with ripgrep's --null output — throwing the exact rows away and re-parsing the lossy format back would be guesswork presented as UI.

So the entries are attached at the source: both tools publish the rows they already hold as ToolResult.data.locations ({path, display?, line?, preview?}, absolute path for opening, printed form for display, capped at MAX_TOOL_LOCATIONS = 200). The render intent stays a pure function of the call's arguments; the entries ride the result's existing structured-data channel, which the app-server has always serialized verbatim — ToolCompletedEvent.result now declares data? instead of narrowing it away, documenting the wire rather than widening it.

Clients share one validating extractor, readToolLocations, so a malformed payload degrades to "no locations" everywhere instead of crashing one renderer.

Honest boundaries

  • Replay degrades, by design. A session restored from its log has only the text the model saw (ToolResultBlock.content), so replayed cards fall back to today's plain-text body — same characters, no buttons. Persisting presentation data into thread items is a possible follow-up; it was left out to keep the stored formats untouched.
  • Withheld stays withheld. Locations are built from the post-contract-filter rows; a test pins that a denied path never leaks through the structured channel.
  • Grep's head_limit now slices rows before formatting so entries line up with shown lines — the emitted text is unchanged byte for byte.

Verification

  • typecheck · lint · format:check · build · docs:check all green.
  • core 1181 passed (rg-backed Grep tests run for real via a local ripgrep, DC_REQUIRE_RIPGREP=1) · desktop 105 passed.
  • Vite preview harness (preview-toolcards.html), checked in the in-app browser: line-numbered Grep rows with match previews, Glob's relative rows, clicks reporting the absolute path, and a restored-session card (result text, no data) falling back to plain text.

🤖 Generated with Claude Code

The DSH round specified a fourth render intent — locations — and deferred
it: deriving entries by parsing the formatted result text is unsound,
because the ':' separator is ambiguous the moment a path contains one.
That is the same reason the withhold filter runs on parsed rows.

So the entries are attached at the source instead. Grep and Glob already
hold exact rows (Grep from ripgrep's --null output) and now publish them
as ToolResult.data.locations — absolute path for opening, the printed
form for display, line and matched text where the mode knows them,
capped at MAX_TOOL_LOCATIONS. The intent stays a pure function of the
call's arguments; the entries ride the result's existing data channel,
which the server has always serialized verbatim (the protocol type now
says so instead of narrowing it away).

The desktop reads them through one shared validating extractor
(readToolLocations) and renders a search's card as one openable row per
hit, wired to the same file-panel opener the card header already uses.
A session restored from its log has only the text the model saw, so
replayed cards degrade to today's plain-text body — same characters,
minus the click.

Grep's head_limit now slices rows before formatting (output unchanged
byte for byte) so entries line up with shown lines, and the contract
tests pin that withheld paths never leak through the structured channel.

Verified in the vite preview harness: line-numbered Grep rows with match
previews, Glob's relative rows, clicks reporting absolute paths, and the
restored-session card falling back to plain text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oratis
oratis merged commit 9785b86 into main Sep 3, 2026
5 checks passed
@oratis
oratis deleted the claude/grep-glob-locations branch September 3, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant