feat: rich tool views — ToolInfo view union, server-computed diffs, panel renderers #148

Merged
dominik.polakovics merged 3 commits from afk/146 into main 2026-07-12 11:09:39 +02:00

Fills the #145 panel's detail page with per-tool-kind rendering, per the settled #146 design.

Stacked on #147 (issue #145's tool call panel): this branch contains origin/afk/145 merged in, since the panel is the render surface. Merge #147 first — this PR's diff then reduces to the #146 work (commits 4779374 + 1eb4e66).

Server

  • provider.ToolView — optional provider-neutral union on ToolInfo (diff | command | write), produced by each provider's chat mapper; input/output stay populated as the raw fallback. Flows through httpapi unchanged.
  • New internal/unidiff: dependency-free line-LCS unified diff (headerless @@ hunks, 3 context lines, whole-file-hunk guard above 512KB/5000 lines). Diffs are computed server-side from Edit's exact old/new strings; the client ships no diff library.
  • claudecode: Edit/NotebookEdit → diff, Write → write, Bash → command. codex: apply_patch → diff (normalizePatch translates the V4A *** envelope to ---/+++ file headers inline — multi-file patches render every file; Path carries the first), exec_command/shell → command (bash -lc/sh -c wrappers unwrap to the script).
  • New DetailTruncateLimit (20KB) for view.text and output, with an explicit in-text … truncated (N KB total) marker — never a silent cut; a cut diff keeps a valid renderable head. Chip-facing title/input keep the 2000 cap.
  • redactChat masks View.Path/Text/Command beside the existing free-form fields.

Web

  • Panel detail page switches on view.kind, provider-blind: diff = path header + prefix-colored lines (+++/---/@@ precede +/- — headers never color as content); write = all-added paint with prefix test short-circuited; command = unselectable $ prompt + output below; no view = the raw input/output fallback, unchanged.
  • Judgment call: for diff/write the raw output block renders only on status=error (a successful edit's "file updated" is noise; a failure's error text must surface). Command output always shows.

Verification

  • go build ./..., go vet ./..., gofmt -l clean; go test ./... green except the pre-existing internal/tmuxx integration failures (real tmux unavailable in the sandbox — identical on the base branch).
  • web: 733 vitest tests green (29 in ToolPanel), npm run build (tsc + vite) green, eslint clean, provider-neutrality guard green.
  • unidiff pins exact byte-for-byte outputs (insertion/deletion/replacement/multi-hunk/merge/guards); mapper tests pin exact hunk text, the V4A→unified translation table (Add/Update/Delete/Move/multi-file/unknown-verb), and the 20KB marker contract.

Closes #146

🤖 Generated with Claude Code

https://claude.ai/code/session_01ULDG5tDsA35vVqzryfztsw

Fills the #145 panel's detail page with per-tool-kind rendering, per the settled #146 design. **Stacked on #147** (issue #145's tool call panel): this branch contains `origin/afk/145` merged in, since the panel is the render surface. Merge #147 first — this PR's diff then reduces to the #146 work (commits `4779374` + `1eb4e66`). ## Server - `provider.ToolView` — optional provider-neutral union on `ToolInfo` (`diff` | `command` | `write`), produced by each provider's chat mapper; `input`/`output` stay populated as the raw fallback. Flows through httpapi unchanged. - New `internal/unidiff`: dependency-free line-LCS unified diff (headerless `@@` hunks, 3 context lines, whole-file-hunk guard above 512KB/5000 lines). Diffs are computed server-side from Edit's exact old/new strings; the client ships no diff library. - claudecode: `Edit`/`NotebookEdit` → diff, `Write` → write, `Bash` → command. codex: `apply_patch` → diff (`normalizePatch` translates the V4A `***` envelope to `---`/`+++` file headers inline — multi-file patches render every file; `Path` carries the first), `exec_command`/`shell` → command (`bash -lc`/`sh -c` wrappers unwrap to the script). - New `DetailTruncateLimit` (20KB) for `view.text` and `output`, with an explicit in-text `… truncated (N KB total)` marker — never a silent cut; a cut diff keeps a valid renderable head. Chip-facing `title`/`input` keep the 2000 cap. - `redactChat` masks `View.Path/Text/Command` beside the existing free-form fields. ## Web - Panel detail page switches on `view.kind`, provider-blind: diff = path header + prefix-colored lines (`+++`/`---`/`@@` precede `+`/`-` — headers never color as content); write = all-added paint with prefix test short-circuited; command = unselectable `$ ` prompt + output below; no view = the raw input/output fallback, unchanged. - Judgment call: for diff/write the raw output block renders only on `status=error` (a successful edit's "file updated" is noise; a failure's error text must surface). Command output always shows. ## Verification - `go build ./...`, `go vet ./...`, `gofmt -l` clean; `go test ./...` green except the pre-existing `internal/tmuxx` integration failures (real tmux unavailable in the sandbox — identical on the base branch). - web: 733 vitest tests green (29 in ToolPanel), `npm run build` (tsc + vite) green, eslint clean, provider-neutrality guard green. - unidiff pins exact byte-for-byte outputs (insertion/deletion/replacement/multi-hunk/merge/guards); mapper tests pin exact hunk text, the V4A→unified translation table (Add/Update/Delete/Move/multi-file/unknown-verb), and the 20KB marker contract. Closes #146 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ULDG5tDsA35vVqzryfztsw
feat(web): tool call panel — desktop right sidebar, mobile bottom sheet
All checks were successful
ci / native (pull_request) Successful in 7m48s
f7d3fbdac0
Tool-call detail moves out of the chat column into a dedicated panel
(issue #145). Chips and group summaries stop expanding inline and become
buttons that open it; the pending-dialog card stays inline.

- ToolPanel component: one <aside>, two containers — in-layout right
  sidebar at >=1024px (rail breakpoint, chat column squeezes via a new
  .chat-column flex row) and a modal bottom sheet below it (InstallSheet
  scrim idiom, grabber, 82dvh, internal scroll). Crossing the breakpoint
  while open is a pure render switch on shared state.
- Two pages: a group opens at a list (one row per tool, thinking stays
  excluded per #68); a row pushes the detail page with a back
  affordance; a lone chip opens directly at detail with no back. Detail
  is today's chip expansion, roomier (40vh cap lifted).
- Liveness: panel renders from the same messages() store, identity by
  immutable seq (decision-12 trick) so SSE refetches preserve panel
  state; new tools append, statuses flip, open output grows in place.
  The panel never opens or retargets itself; state resets with the
  stream and clears on route navigation. No history entry.
- Dismissal: scrim tap, ✕ everywhere, Esc on desktop
  (defaultPrevented-guarded so rename/autocomplete Esc wins), and
  drag-down via lib/sheetGesture — a vertical, dismiss-only
  transposition of #140's drawer gesture sharing its tuning constants
  (inner scroller wins unless scrollTop 0, editables excluded,
  flick-or-position commit, touchcancel resets).
- The source chip/group line gets a selected highlight while its
  content is open.

Closes #145

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FD7GzR4PjrjKmCqQ1WiFt8
Give ToolInfo an optional provider-neutral rich-view union (issue #146),
produced by each provider's chat mapper — the one place per-provider tool
knowledge lives — and rendered by the SPA purely by view.kind:

- provider.ToolView: {kind: diff|command|write} with path/text/command;
  absent view falls back to the raw Input/Output chip fields, so unmapped
  tools keep rendering. Rides provider.Message straight through httpapi.
- internal/unidiff (new): dependency-free line-LCS unified diff — headerless
  @@ hunks with 3 context lines, size-guarded to a whole-file hunk against
  quadratic blow-up. Edit's old/new strings are exact, so the mapper diffs
  them server-side and ships ONE unified-diff text; the client just colors
  line prefixes, no diff library in the web bundle.
- claudecode: Edit/NotebookEdit -> diff, Write -> write, Bash -> command.
- codex: apply_patch -> diff (normalizePatch translates the V4A envelope's
  *** verbs into ---/+++ file headers inline — one patch can span several
  files; Path carries only the first), exec_command/shell -> command.
- Tool output and view.text are sized for the panel, not the chip: new
  DetailTruncateLimit (20KB) with an explicit in-text "… truncated (N KB
  total)" marker — never a silent cut; truncating a diff's tail leaves a
  valid renderable head. Chip-facing Title/Input keep the 2000-byte cap.
- redactChat masks View.Path/Text/Command beside Input/Output — the union
  carries the same operator-visible free-form content secrets can ride.

Closes #146

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULDG5tDsA35vVqzryfztsw
feat(web): render rich tool views in the panel detail page
All checks were successful
ci / native (pull_request) Successful in 4m54s
1eb4e6658c
The #145 panel's detail page renders by the server's view.kind (issue
#146) and stays provider-blind — no tool-name knowledge in TS:

- diff: path header + line-colored unified diff. Prefix precedence is
  load-bearing: +++/---/@@ header lines test before the single-char +/-
  markers, so a "+++ b/foo" header never reads as an added line. Tints are
  color-mix over the theme vars, adapting to light/dark on their own.
- write: path header + content painted as an all-added diff (forceAdd
  short-circuits the prefix test — raw content starting with "-" or "@@"
  is never mistyped). Raw output rides below only on status=error; a
  successful write's "file updated" line is noise, a failed one's error
  text must surface.
- command: dim unselectable "$ " prompt + the command, output as terminal
  text below whatever the status.
- No view: byte-for-byte the raw input/output fallback.

Server truncates view.text/output at 20KB with an in-text marker, so the
client renders text as-is — no truncation logic here.

Part of #146

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULDG5tDsA35vVqzryfztsw
Merge remote-tracking branch 'origin/main' into afk/146
All checks were successful
ci / native (pull_request) Successful in 4m57s
911f2a14ab
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!148
No description provided.