Inline rich tool rendering in chat + flush-right resizable file sidebar (desktop) #154

Closed
opened 2026-07-13 09:43:33 +02:00 by dominik.polakovics · 1 comment

Why

The desktop tool panel from #145/#147 (chips open a right sidebar) turned out not to match how the chat is read: tool detail belongs inline in the conversation flow. We want the pre-#147 inline shape back — but keeping the rich renderers from #146 — plus a new, properly-docked sidebar reserved for file content.

Spec

1. Inline rich tool rendering in chat (desktop ≥1024px)

Restore the collapsed-chip shape removed in f7d3fbd, upgraded with the rich views:

  • One-line chip per tool call; consecutive tool calls collapse into a group row (same grouping as before #147). Clicking a chip/group toggles inline expansion in place — desktop chips no longer open the panel.
  • The expanded body renders the same rich views as the current panel (web/src/components/ToolPanel.tsx:386-435): diff, write, command, plus the new read kind (§3), falling back to raw input/output <pre> when no view exists. Extract these renderers into a shared component so inline, sidebar, and mobile sheet use one implementation.
  • Expanded body has max-height ~45vh with internal vertical scroll.
  • No horizontal scrolling anywhere: long lines soft-wrap (white-space: pre-wrap; overflow-wrap: anywhere) in diff/command/read/write views and the raw fallback <pre> blocks. Apply the same wrapping in the sidebar and the mobile sheet.

2. Flush-right resizable file sidebar (desktop)

A right sidebar exclusively for file views (read, diff, write):

  • Affordance: file chips carry a small "open in sidebar" icon button at the right of the collapsed row, and the same icon appears next to the path header in the expanded inline view. Chip body click still toggles inline expand. Command/fallback tools get no sidebar affordance.
  • Content: the same rich file view (diff / write content / read excerpt), detail-only — no list page on desktop. Opening another file replaces the sidebar content.
  • Docked flush to the right window border. Today the gap exists because .chat-page carries .page (max-width: 760px + panel; margin: 0 auto, web/src/base.css:157-161, 3801-3805) so the whole row centers. Change: when the sidebar is open, the page row goes full-width, the sidebar sits at the true right edge (no auto-margin, no right padding gap), and the chat column stays capped at 760px, centered in the remaining space to the left.
  • Drag-to-resize: a drag handle on the sidebar's left edge (pointer events, no external lib; note there is no existing splitter code — sheetGesture.ts/drawerGesture.ts are dismiss gestures, not resize). Clamp width to min 320px, max 60vw. Persist width in localStorage (single global key); default remains clamp(320px, 30vw, 480px).
  • Escape closes the sidebar (existing behavior); open state stays an in-memory signal — no history entry (unchanged from #145). The chip whose file is shown keeps a selected/pressed state.

3. Server: new read ToolView kind

  • Add ToolViewRead = "read" to the view union in internal/provider/provider.go (fields: Path, Text — the file excerpt from the tool result, DetailTruncateLimit applies).
  • Map it in internal/provider/claudecode/chat_types.go for the Read tool. Codex has no distinct read tool (reads happen via shell → already command), so no codex mapping.
  • Extend the client union in web/src/api.ts:546-549 with { kind: 'read'; path: string; text: string } and render it like a file view (path header + content, no +/- coloring).
  • Redaction (internal/chat/redact.go) must cover the new kind's Path/Text (verify it already masks all View fields generically).

4. Mobile (<1024px): unchanged flow

  • Chips/groups keep opening the bottom sheet (list → detail) exactly as today — no inline expansion on mobile.
  • The sheet picks up the shared renderers, the read view, and line wrapping.

Out of scope

  • Whole-file viewing / fetching file content from the worktree (sidebar shows only the diff/excerpt the tool reported).
  • Any change to the pending-dialog card, drawer gestures, or sheet drag-to-dismiss.

Touch points

  • web/src/routes/RunChat.tsx (ToolChip/ToolGroupView at 1423-1468, panelSel wiring at 163-165, 706-753)
  • web/src/components/ToolPanel.tsx (extract renderers; desktop becomes file-detail-only)
  • web/src/base.css (chat-page cap at 157-161 / 3801-3805, .tool-panel-side at 3641-3648, wrap rules)
  • internal/provider/provider.go, internal/provider/claudecode/chat_types.go, internal/chat/redact.go, web/src/api.ts
## Why The desktop tool panel from #145/#147 (chips open a right sidebar) turned out not to match how the chat is read: tool detail belongs inline in the conversation flow. We want the pre-#147 inline shape back — but keeping the rich renderers from #146 — plus a new, properly-docked sidebar reserved for file content. ## Spec ### 1. Inline rich tool rendering in chat (desktop ≥1024px) Restore the collapsed-chip shape removed in f7d3fbd, upgraded with the rich views: - One-line chip per tool call; consecutive tool calls collapse into a group row (same grouping as before #147). Clicking a chip/group toggles **inline expansion in place** — desktop chips no longer open the panel. - The expanded body renders the same rich views as the current panel (`web/src/components/ToolPanel.tsx:386-435`): diff, write, command, plus the new `read` kind (§3), falling back to raw input/output `<pre>` when no view exists. Extract these renderers into a shared component so inline, sidebar, and mobile sheet use one implementation. - Expanded body has **max-height ~45vh with internal vertical scroll**. - **No horizontal scrolling anywhere**: long lines soft-wrap (`white-space: pre-wrap; overflow-wrap: anywhere`) in diff/command/read/write views and the raw fallback `<pre>` blocks. Apply the same wrapping in the sidebar and the mobile sheet. ### 2. Flush-right resizable file sidebar (desktop) A right sidebar exclusively for **file views** (`read`, `diff`, `write`): - **Affordance:** file chips carry a small "open in sidebar" icon button at the right of the collapsed row, and the same icon appears next to the path header in the expanded inline view. Chip body click still toggles inline expand. Command/fallback tools get no sidebar affordance. - **Content:** the same rich file view (diff / write content / read excerpt), detail-only — no list page on desktop. Opening another file replaces the sidebar content. - **Docked flush to the right window border.** Today the gap exists because `.chat-page` carries `.page` (`max-width: 760px + panel; margin: 0 auto`, `web/src/base.css:157-161, 3801-3805`) so the whole row centers. Change: when the sidebar is open, the page row goes full-width, the sidebar sits at the true right edge (no auto-margin, no right padding gap), and the **chat column stays capped at 760px, centered in the remaining space to the left**. - **Drag-to-resize:** a drag handle on the sidebar's left edge (pointer events, no external lib; note there is no existing splitter code — `sheetGesture.ts`/`drawerGesture.ts` are dismiss gestures, not resize). Clamp width to min 320px, max 60vw. Persist width in `localStorage` (single global key); default remains `clamp(320px, 30vw, 480px)`. - Escape closes the sidebar (existing behavior); open state stays an in-memory signal — **no history entry** (unchanged from #145). The chip whose file is shown keeps a selected/pressed state. ### 3. Server: new `read` ToolView kind - Add `ToolViewRead = "read"` to the view union in `internal/provider/provider.go` (fields: `Path`, `Text` — the file excerpt from the tool result, `DetailTruncateLimit` applies). - Map it in `internal/provider/claudecode/chat_types.go` for the `Read` tool. Codex has no distinct read tool (reads happen via shell → already `command`), so no codex mapping. - Extend the client union in `web/src/api.ts:546-549` with `{ kind: 'read'; path: string; text: string }` and render it like a file view (path header + content, no +/- coloring). - Redaction (`internal/chat/redact.go`) must cover the new kind's `Path`/`Text` (verify it already masks all `View` fields generically). ### 4. Mobile (<1024px): unchanged flow - Chips/groups keep opening the **bottom sheet** (list → detail) exactly as today — no inline expansion on mobile. - The sheet picks up the shared renderers, the `read` view, and line wrapping. ## Out of scope - Whole-file viewing / fetching file content from the worktree (sidebar shows only the diff/excerpt the tool reported). - Any change to the pending-dialog card, drawer gestures, or sheet drag-to-dismiss. ## Touch points - `web/src/routes/RunChat.tsx` (ToolChip/ToolGroupView at 1423-1468, panelSel wiring at 163-165, 706-753) - `web/src/components/ToolPanel.tsx` (extract renderers; desktop becomes file-detail-only) - `web/src/base.css` (chat-page cap at 157-161 / 3801-3805, `.tool-panel-side` at 3641-3648, wrap rules) - `internal/provider/provider.go`, `internal/provider/claudecode/chat_types.go`, `internal/chat/redact.go`, `web/src/api.ts`
Author
Owner

This was generated by AI while landing a PR.

Landing audit — PASS

Verification signal relied on: Forgejo Actions ci / nativesuccess (5m3s, run 168). Not re-run. That job covers the whole surface of this diff: SPA lint/format/vitest/build, then go build/go test (tag ui) and golangci-lint. No flake/dependency files are touched, so nothing here sits behind the conditional nix gate.

Merge state: no conflict — afk/154 is a single commit on the current origin/main tip, merge-tree clean.

Conventions: title is Conventional Commits (feat:); body carries a working Closes #154; head branch is the run's afk/154 claim.

Diff vs. issue #154: all four sections implemented as specified.

  • §1 inline rich rendering — chips/groups toggle in place on desktop; expansion keyed by seq (Set<number>, decision-12 keying), so it survives an SSE refetch and resets with the stream in resetStream. Renderers extracted to ToolViews.tsx, and the liveness invariant holds: ToolViewBody reads everything through props.message, never a captured snapshot.
  • §2 flush-right file sidebar — affordance gated on isFileView (diff/write/read) and on desktop; the guard effect clears a group or non-file selection after a breakpoint crossing, so the sidebar can't be asked to render what it doesn't show.
  • §3 read view kind — ToolViewRead in the union, mapped for claude-code only; readExcerpt is conservative (passes text through unchanged unless every non-empty line carries a cat -n prefix), TruncateDetail applied, view cleared on an errored read. Redaction masks Path/Text generically, pinned by a new test.
  • §4 mobile — the chip click still opens the sheet; inline bodies render only under desktopPanel().
  • CSS: the button→frame split is sound. .chat-tool/.chat-tool-group are plain block frames (border/radius only, no display:flex), so moving them to the wrapper div while .tool-summary/.tool-group-summary (the flex rows) stay on the button preserves the collapsed look.

Non-blocking nits (not fixed here)

  1. internal/provider/claudecode/chat_types.go — inside case "Read", path := str(in["file_path"]) shadows the imported path package. Compiles and lints clean (the case body never uses the package), but it is a trap for the next editor of that clause.
  2. web/src/components/AppShell.tsx:144 — the drawer-gesture comment still lists .tool-body among the horizontal scrollers. .tool-body is now pre-wrap with no overflow-x, so it no longer is one. Behaviour is unaffected (the check reads computed overflow-x, it is not a hard-coded selector list), but the comment is stale.
  3. ToolChip sets aria-expanded={props.desktop && props.expanded}, which renders aria-expanded="false" on mobile chips, where the chip opens a sheet rather than expanding. Previously the attribute was absent there. Harmless, slightly inaccurate semantics.

Verdict: PASS. Merging.

> *This was generated by AI while landing a PR.* ## Landing audit — PASS **Verification signal relied on:** Forgejo Actions `ci / native` — **success** (5m3s, run 168). Not re-run. That job covers the whole surface of this diff: SPA lint/format/vitest/build, then `go build`/`go test` (tag `ui`) and `golangci-lint`. No flake/dependency files are touched, so nothing here sits behind the conditional nix gate. **Merge state:** no conflict — `afk/154` is a single commit on the current `origin/main` tip, `merge-tree` clean. **Conventions:** title is Conventional Commits (`feat:`); body carries a working `Closes #154`; head branch is the run's `afk/154` claim. **Diff vs. issue #154:** all four sections implemented as specified. - §1 inline rich rendering — chips/groups toggle in place on desktop; expansion keyed by `seq` (`Set<number>`, decision-12 keying), so it survives an SSE refetch and resets with the stream in `resetStream`. Renderers extracted to `ToolViews.tsx`, and the liveness invariant holds: `ToolViewBody` reads everything through `props.message`, never a captured snapshot. - §2 flush-right file sidebar — affordance gated on `isFileView` (diff/write/read) and on desktop; the guard effect clears a group or non-file selection after a breakpoint crossing, so the sidebar can't be asked to render what it doesn't show. - §3 `read` view kind — `ToolViewRead` in the union, mapped for claude-code only; `readExcerpt` is conservative (passes text through unchanged unless *every* non-empty line carries a `cat -n` prefix), `TruncateDetail` applied, view cleared on an errored read. Redaction masks `Path`/`Text` generically, pinned by a new test. - §4 mobile — the chip click still opens the sheet; inline bodies render only under `desktopPanel()`. - CSS: the button→frame split is sound. `.chat-tool`/`.chat-tool-group` are plain block frames (border/radius only, no `display:flex`), so moving them to the wrapper `div` while `.tool-summary`/`.tool-group-summary` (the flex rows) stay on the button preserves the collapsed look. ### Non-blocking nits (not fixed here) 1. `internal/provider/claudecode/chat_types.go` — inside `case "Read"`, `path := str(in["file_path"])` shadows the imported `path` package. Compiles and lints clean (the case body never uses the package), but it is a trap for the next editor of that clause. 2. `web/src/components/AppShell.tsx:144` — the drawer-gesture comment still lists `.tool-body` among the horizontal scrollers. `.tool-body` is now `pre-wrap` with no `overflow-x`, so it no longer is one. Behaviour is unaffected (the check reads computed `overflow-x`, it is not a hard-coded selector list), but the comment is stale. 3. `ToolChip` sets `aria-expanded={props.desktop && props.expanded}`, which renders `aria-expanded="false"` on mobile chips, where the chip opens a sheet rather than expanding. Previously the attribute was absent there. Harmless, slightly inaccurate semantics. Verdict: **PASS**. Merging.
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#154
No description provided.