feat(web): tool call panel — desktop right sidebar, mobile bottom sheet #147

Merged
dominik.polakovics merged 1 commit from afk/145 into main 2026-07-12 10:51:03 +02:00

Tool-call detail moves out of the chat column into a dedicated panel. Chips and group summaries become buttons that open it; nothing expands inline anymore. Implements every settled decision in the issue.

What's in here

Containers

  • Desktop (≥1024px, the rail breakpoint): in-layout right sidebar, clamp(320px, 30vw, 480px), complementary region (plain <aside>, not modal). The page cap widens by exactly the sidebar width so the chat column keeps 760px where the viewport allows and squeezes (flex + min-width 0) where it doesn't; closed, the layout is byte-identical to before.
  • Mobile: modal bottom sheet (InstallSheet idiom — scrim and card as siblings, dismiss on the scrim only), role="dialog" aria-modal, grabber bar, 82dvh cap, content scrolls inside.
  • Crossing 1024px while open: shared state, pure render switch — the panel stays open and re-renders in the other chrome. No history entry (no pushState).

Navigation

  • Group tap → list page (one row per tool, title + status mark, thinking stays excluded per #68); row tap → detail with ‹ back. Lone chip → directly at detail, no back. Retarget swaps content in place. Detail = today's chip expansion, roomier (the 40vh cap is lifted in the panel).

Liveness

  • Renders from the same messages() store; identity by immutable seq (the decision-12 stable-key trick), so SSE refetches preserve panel and page state — new tools append to an open list, statuses flip, an open detail's output grows in place. The panel never opens or retargets itself; it resets with the stream on route navigation/transcript rotation.

Dismissal

  • Scrim tap, ✕ everywhere, Esc on desktop (guarded on defaultPrevented so the rename input / composer autocomplete consume Esc first), and drag-down: lib/sheetGesture.ts is a vertical, dismiss-only transposition of #140's drawer state machine, importing its tuning constants — inner scroller wins unless scrollTop 0, editables excluded, axis dominance, flick-or-position commit, touchcancel resets. The sheet stops touch propagation so the app-shell drawer gesture can't claim drags starting on the modal.

Chat side

  • Group coalescing (decisions 7–12) untouched; ToolChip/ToolGroupView are now buttons wearing their old classes (visuals preserved), with a subtle accent selected style on the line whose content the panel shows, cleared on close. The pending-dialog card is untouched.

Verification

  • 727 tests / 43 files green (baseline 678/41): 18 new gesture-machine tests, 23 ToolPanel tests (pages, a11y, Esc, scrim, liveness, breakpoint flip, drag binding with fabricated touch events), reworked + new RunChat integration tests (open/retarget/close, decision-12 analog across SSE refetch, selected highlight, reset on rotation, breakpoint crossing, Esc coordination, no inline expansion remains).
  • tsc --noEmit, eslint src, prettier --check, and the production npm run build all clean.
  • Browsers can't run in this sandbox, so no screenshots; surfaces worth eyeballing: chat at ≥1024px with panel open/closed (column squeeze), sheet drag on a real phone (feel constants shared with the drawer), dark mode panel chrome, and a long mono output scrolling horizontally inside the detail panes.

Out of scope (follow-up #146): rich view kinds (diff/command/write), the ToolInfo.view union, raised detail truncation limit.

Closes #145

🤖 Generated with Claude Code

https://claude.ai/code/session_01FD7GzR4PjrjKmCqQ1WiFt8

Tool-call detail moves out of the chat column into a dedicated panel. Chips and group summaries become buttons that open it; nothing expands inline anymore. Implements every settled decision in the issue. ## What's in here **Containers** - Desktop (≥1024px, the rail breakpoint): in-layout right sidebar, `clamp(320px, 30vw, 480px)`, complementary region (plain `<aside>`, not modal). The page cap widens by exactly the sidebar width so the chat column keeps 760px where the viewport allows and squeezes (flex + min-width 0) where it doesn't; closed, the layout is byte-identical to before. - Mobile: modal bottom sheet (InstallSheet idiom — scrim and card as siblings, dismiss on the scrim only), `role="dialog" aria-modal`, grabber bar, 82dvh cap, content scrolls inside. - Crossing 1024px while open: shared state, pure render switch — the panel stays open and re-renders in the other chrome. No history entry (no pushState). **Navigation** - Group tap → list page (one row per tool, title + status mark, thinking stays excluded per #68); row tap → detail with ‹ back. Lone chip → directly at detail, no back. Retarget swaps content in place. Detail = today's chip expansion, roomier (the 40vh cap is lifted in the panel). **Liveness** - Renders from the same `messages()` store; identity by immutable seq (the decision-12 stable-key trick), so SSE refetches preserve panel and page state — new tools append to an open list, statuses flip, an open detail's output grows in place. The panel never opens or retargets itself; it resets with the stream on route navigation/transcript rotation. **Dismissal** - Scrim tap, ✕ everywhere, Esc on desktop (guarded on `defaultPrevented` so the rename input / composer autocomplete consume Esc first), and drag-down: `lib/sheetGesture.ts` is a vertical, dismiss-only transposition of #140's drawer state machine, importing its tuning constants — inner scroller wins unless scrollTop 0, editables excluded, axis dominance, flick-or-position commit, touchcancel resets. The sheet stops touch propagation so the app-shell drawer gesture can't claim drags starting on the modal. **Chat side** - Group coalescing (decisions 7–12) untouched; `ToolChip`/`ToolGroupView` are now buttons wearing their old classes (visuals preserved), with a subtle accent `selected` style on the line whose content the panel shows, cleared on close. The pending-dialog card is untouched. ## Verification - 727 tests / 43 files green (baseline 678/41): 18 new gesture-machine tests, 23 ToolPanel tests (pages, a11y, Esc, scrim, liveness, breakpoint flip, drag binding with fabricated touch events), reworked + new RunChat integration tests (open/retarget/close, decision-12 analog across SSE refetch, selected highlight, reset on rotation, breakpoint crossing, Esc coordination, no inline expansion remains). - `tsc --noEmit`, `eslint src`, `prettier --check`, and the production `npm run build` all clean. - Browsers can't run in this sandbox, so no screenshots; surfaces worth eyeballing: chat at ≥1024px with panel open/closed (column squeeze), sheet drag on a real phone (feel constants shared with the drawer), dark mode panel chrome, and a long mono output scrolling horizontally inside the detail panes. Out of scope (follow-up #146): rich view kinds (diff/command/write), the `ToolInfo.view` union, raised detail truncation limit. Closes #145 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FD7GzR4PjrjKmCqQ1WiFt8
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
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!147
No description provided.