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

Closed
opened 2026-07-12 04:03:27 +02:00 by dominik.polakovics · 1 comment

Blocked by #145 — do not start until the tool call panel is merged. This slice fills the panel's detail page with per-tool-kind rendering. Grilled 2026-07-12; decisions below are settled.

Seam: server-side view union

ToolInfo gains an optional provider-neutral discriminated union, produced by each provider's chat mapper (which already owns per-provider tool knowledge — toolTitle/toolInputText in claudecode, payload mappers in codex). The web client renders by view.kind and stays provider-blind; no tool-name knowledge leaks into TS.

view?: { kind: 'diff',    path: string, text: string }   // unified-diff text
      | { kind: 'command', command: string }             // output rides ToolInfo.output
      | { kind: 'write',   path: string, text: string }  // file content
// absent view → existing raw input/output fallback

input/output string fields stay populated regardless (raw fallback + older clients).

Diffs are computed server-side

The mapper runs a small pure-Go line diff (Edit's old/new are exact strings, so this is cheap) and ships one unified-diff text string. The client colors lines by +/-/@@ prefix — no diff library in the web bundle, and truncating the tail of a long diff still leaves a valid renderable head.

Provider mapping

  • claudecode: Edit/NotebookEditdiff; Writewrite; Bashcommand (command string from input). Everything else → no view.
  • codex: apply_patchdiff (input is already patch-shaped text with *** Update File: envelopes — normalize to unified-diff text); exec_command/shellcommand (cmd from arguments JSON). Everything else → no view.
  • Future providers (gemini, #126) map in their own adapter; the union is the conformance surface.

Truncation

  • New detailTruncateLimit ≈ 20000 for view.text and output — sized for a panel, not a chip. Chip-facing title and the legacy input keep the existing 2000.
  • Truncation is explicit: a marker the panel renders as "… truncated" (with total size if cheap), never a silent cut.

Panel renderers (web)

  • diff: path header, line-colored unified diff (+ green / − red / @@ dim).
  • command: $-prefixed command block, output as terminal text below.
  • write: path header + content (rendered like an all-added diff). No syntax highlighting — the app has no highlighter; separate concern if ever.
  • No view → today's raw input/output <pre> blocks.

Out of scope

Copy-to-clipboard buttons, wrap toggles, word-level diff highlighting, a fetch-full-I/O endpoint for payloads beyond the 20KB cap (revisit only if the cap proves tight in practice).

**Blocked by #145 — do not start until the tool call panel is merged.** This slice fills the panel's detail page with per-tool-kind rendering. Grilled 2026-07-12; decisions below are settled. ## Seam: server-side view union `ToolInfo` gains an optional provider-neutral discriminated union, produced by each provider's chat mapper (which already owns per-provider tool knowledge — `toolTitle`/`toolInputText` in claudecode, payload mappers in codex). The web client renders by `view.kind` and stays provider-blind; no tool-name knowledge leaks into TS. ``` view?: { kind: 'diff', path: string, text: string } // unified-diff text | { kind: 'command', command: string } // output rides ToolInfo.output | { kind: 'write', path: string, text: string } // file content // absent view → existing raw input/output fallback ``` `input`/`output` string fields stay populated regardless (raw fallback + older clients). ## Diffs are computed server-side The mapper runs a small pure-Go line diff (Edit's old/new are exact strings, so this is cheap) and ships **one unified-diff text string**. The client colors lines by `+`/`-`/`@@` prefix — no diff library in the web bundle, and truncating the tail of a long diff still leaves a valid renderable head. ## Provider mapping - **claudecode**: `Edit`/`NotebookEdit` → `diff`; `Write` → `write`; `Bash` → `command` (command string from input). Everything else → no view. - **codex**: `apply_patch` → `diff` (input is already patch-shaped text with `*** Update File:` envelopes — normalize to unified-diff text); `exec_command`/`shell` → `command` (cmd from arguments JSON). Everything else → no view. - Future providers (gemini, #126) map in their own adapter; the union is the conformance surface. ## Truncation - New `detailTruncateLimit ≈ 20000` for `view.text` and `output` — sized for a panel, not a chip. Chip-facing `title` and the legacy `input` keep the existing 2000. - Truncation is explicit: a marker the panel renders as "… truncated" (with total size if cheap), never a silent cut. ## Panel renderers (web) - `diff`: path header, line-colored unified diff (+ green / − red / @@ dim). - `command`: `$`-prefixed command block, output as terminal text below. - `write`: path header + content (rendered like an all-added diff). No syntax highlighting — the app has no highlighter; separate concern if ever. - No `view` → today's raw input/output `<pre>` blocks. ## Out of scope Copy-to-clipboard buttons, wrap toggles, word-level diff highlighting, a fetch-full-I/O endpoint for payloads beyond the 20KB cap (revisit only if the cap proves tight in practice).
Author
Owner

This was generated by AI while landing a PR.

Landing audit — PR #148: PASS

  • CI: ci / native success (run #161, 4m54s) — relied on, not re-run.
  • Mergeable: clean merge into current origin/main; #147 already landed so the diff reduced to exactly the two #146 commits (4779374 + 1eb4e66), as the PR body predicted.
  • Title: Conventional Commits (feat:). AFK contract: head afk/146, body carries working Closes #146.
  • Diff scope: 13 files, all on-design — server ToolView union + internal/unidiff + provider mappers (claudecode/codex) + redact sweep + web render-by-kind + tests.

Correctness spot-checks (all hold): provider-neutrality preserved (SPA switches on view.kind only; unmapped/partial inputs → nil view → raw fallback, no panic); diff prefix precedence correct (+++/---/@@ before +/-); .tool-diff span { display:block } backs the per-line spans inside <pre>; TruncateDetail (20KB, rune-boundary, explicit marker); redact masks the new View.Path/Text/Command; unidiff LCS has size guards.

Verdict: PASS — awaiting free-text merge go-ahead.

> *This was generated by AI while landing a PR.* **Landing audit — PR #148: PASS** - **CI**: `ci / native` success (run #161, 4m54s) — relied on, not re-run. - **Mergeable**: clean merge into current `origin/main`; #147 already landed so the diff reduced to exactly the two #146 commits (`4779374` + `1eb4e66`), as the PR body predicted. - **Title**: Conventional Commits (`feat:`). **AFK contract**: head `afk/146`, body carries working `Closes #146`. - **Diff scope**: 13 files, all on-design — server `ToolView` union + `internal/unidiff` + provider mappers (claudecode/codex) + redact sweep + web render-by-kind + tests. **Correctness spot-checks (all hold):** provider-neutrality preserved (SPA switches on `view.kind` only; unmapped/partial inputs → nil view → raw fallback, no panic); diff prefix precedence correct (`+++`/`---`/`@@` before `+`/`-`); `.tool-diff span { display:block }` backs the per-line spans inside `<pre>`; `TruncateDetail` (20KB, rune-boundary, explicit marker); redact masks the new `View.Path/Text/Command`; `unidiff` LCS has size guards. Verdict: **PASS** — awaiting free-text merge go-ahead.
Sign in to join this conversation.
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#146
No description provided.