feat(chat): quick-return mobile header, in-stream needs-input note, jump-to-latest pill, merged composer #43

Merged
dominik.polakovics merged 1 commit from afk/35 into main 2026-07-08 09:24:54 +02:00

Summary

Redesigns the embedded chat view (web/src/routes/RunChat.tsx, route /runs/:id) so long assistant messages read comfortably on a phone and the composer reads as one polished control. Phone-first, single 640px breakpoint, no new dependencies — plain global CSS + SolidJS signals, with a single .chat-stream scroll listener driving both the header (§2) and the pill (§4).

Closes #35

What changed (per the issue's five sections)

  • §1 · One-line header + ••• dropdown (📱 <640px): the wrapping header collapses to a single non-wrapping row [← back · title · ● state dot · •••]. The convo chip shrinks to a colored dot; Show/Hide thinking · Open/Attach · Stop run… (two-step confirm, contextual) move into an anchored dropdown reusing TopBar's menu + scrim + Escape pattern. ••• is always present. ≥640px the inline header is unchanged (desktop controls kept via display:contents).
  • §2 · Quick-return header (📱): auto-hides on committed (~10px) upward scroll past the header height, reveals hair-trigger on any downward scroll, pinned at the very top. Overlays the stream via translateY with no reflow (the stream reserves the band via padding-top). Only user-initiated scroll toggles it — a programmatic-scroll guard keeps streaming autoscroll / follow-to-bottom / tap-to-latest from moving it. Logic lives in a pure, unit-tested quickReturnReduce().
  • §3 · needs-input note relocated into the stream (🌐): now a centered status line "Claude is waiting for your reply." (styled like .chat-lifecycle, not a bubble), reactive on needs_input and only visible at the bottom. The composer collapses to just its input row.
  • §4 · Jump-to-latest pill (🌐, new): bottom-center over the stream, appears when scrolled up with newer content below, accent-emphasized on needs_input, taps smooth-scroll to the latest (which brings the header back). Pure, unit-tested pillState().
  • §5 · Merged composer field (🌐): textarea + send share one ~14px rounded-rect; the accent circular send/interrupt button is pinned bottom-right inside the field (36px visual, 44px hit area, stays pinned as the field grows). Auto-grow, disabled-when-empty, ⌘/Ctrl+Enter, and the working-state Interrupt morph are all preserved. CSS-only and scoped so DialogPanel is untouched.

Design & review

Built via a design pass (scroll-controller / test-impact / DOM+CSS lenses, reconciled) and an adversarial multi-dimension review whose 8 confirmed findings were all fixed: the mobile header title/spacer flex split, the hidden overlay header vs. the error banner (z-index), scrim dimming the header (portaled), three menu a11y nits, and two test-coverage gaps. One finding (350ms smooth-scroll guard) was verified as a non-issue.

The ErrorBanner now sits above a new .chat-body positioning wrapper (so the mobile overlay header can't cover it); on desktop it renders just above the header row rather than just below — a small, benign, documented placement change.

Testing

  • npm run lint · npm run format:check · npm test · npm run build — all green.
  • 399 tests pass (was 378): +9 pure-helper tests (quickReturnReduce, pillState) and +12 component tests covering the ••• dropdown (open/close/Escape, contextual Stop, thinking toggle), the state dot, the in-stream needs-input line (present / last-child / reactive / rewording), and the pill DOM contract (mounted / hidden-inert / reveal / emphasis / onJump).
  • Note: jsdom has no layout or media-query engine, so the pixel-level scroll-hide behavior and the 640px breakpoint switch are exercised through the pure reducers + the adversarial CSS review rather than the rendered component; a quick manual pass in a mobile viewport is worthwhile on review.

No API / SSE / schema / backend change. This evolves the chat UI within ADR-0016/0019/0022's design language with no ADR conflict (it preserves the ADR-0022 Send↔Interrupt morph and the ADR-0019 two-step Stop); a codifying ADR could be a maintainer follow-up if desired.

🤖 Generated with Claude Code

## Summary Redesigns the embedded chat view (`web/src/routes/RunChat.tsx`, route `/runs/:id`) so long assistant messages read comfortably on a phone and the composer reads as one polished control. Phone-first, single **640px** breakpoint, **no new dependencies** — plain global CSS + SolidJS signals, with a single `.chat-stream` scroll listener driving both the header (§2) and the pill (§4). Closes #35 ## What changed (per the issue's five sections) - **§1 · One-line header + `•••` dropdown** (📱 <640px): the wrapping header collapses to a single non-wrapping row `[← back · title · ● state dot · •••]`. The convo chip shrinks to a colored dot; **Show/Hide thinking · Open/Attach · Stop run…** (two-step confirm, contextual) move into an anchored dropdown reusing `TopBar`'s menu + scrim + Escape pattern. `•••` is always present. **≥640px the inline header is unchanged** (desktop controls kept via `display:contents`). - **§2 · Quick-return header** (📱): auto-hides on committed (~10px) upward scroll past the header height, reveals hair-trigger on any downward scroll, pinned at the very top. Overlays the stream via `translateY` with **no reflow** (the stream reserves the band via `padding-top`). **Only user-initiated scroll toggles it** — a programmatic-scroll guard keeps streaming autoscroll / follow-to-bottom / tap-to-latest from moving it. Logic lives in a pure, unit-tested `quickReturnReduce()`. - **§3 · needs-input note relocated into the stream** (🌐): now a centered status line **"Claude is waiting for your reply."** (styled like `.chat-lifecycle`, not a bubble), reactive on `needs_input` and only visible at the bottom. The composer collapses to just its input row. - **§4 · Jump-to-latest pill** (🌐, new): bottom-center over the stream, appears when scrolled up with newer content below, **accent-emphasized on `needs_input`**, taps smooth-scroll to the latest (which brings the header back). Pure, unit-tested `pillState()`. - **§5 · Merged composer field** (🌐): textarea + send share one ~14px rounded-rect; the accent **circular send/interrupt button is pinned bottom-right inside the field** (36px visual, 44px hit area, stays pinned as the field grows). Auto-grow, disabled-when-empty, ⌘/Ctrl+Enter, and the working-state **Interrupt morph** are all preserved. CSS-only and scoped so `DialogPanel` is untouched. ## Design & review Built via a design pass (scroll-controller / test-impact / DOM+CSS lenses, reconciled) and an **adversarial multi-dimension review** whose 8 confirmed findings were all fixed: the mobile header title/spacer flex split, the hidden overlay header vs. the error banner (z-index), scrim dimming the header (portaled), three menu a11y nits, and two test-coverage gaps. One finding (350ms smooth-scroll guard) was verified as a non-issue. The `ErrorBanner` now sits above a new `.chat-body` positioning wrapper (so the mobile overlay header can't cover it); on desktop it renders just above the header row rather than just below — a small, benign, documented placement change. ## Testing - `npm run lint` · `npm run format:check` · `npm test` · `npm run build` — all green. - **399 tests pass** (was 378): +9 pure-helper tests (`quickReturnReduce`, `pillState`) and +12 component tests covering the `•••` dropdown (open/close/Escape, contextual Stop, thinking toggle), the state dot, the in-stream needs-input line (present / last-child / reactive / rewording), and the pill DOM contract (mounted / hidden-inert / reveal / emphasis / `onJump`). - Note: jsdom has no layout or media-query engine, so the pixel-level scroll-hide behavior and the 640px breakpoint switch are exercised through the pure reducers + the adversarial CSS review rather than the rendered component; a quick manual pass in a mobile viewport is worthwhile on review. No API / SSE / schema / backend change. This evolves the chat UI within ADR-0016/0019/0022's design language with no ADR conflict (it preserves the ADR-0022 Send↔Interrupt morph and the ADR-0019 two-step Stop); a codifying ADR could be a maintainer follow-up if desired. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Redesign the RunChat view (/runs/:id) for comfortable phone reading and a
polished composer, per issue #35. Single 640px breakpoint; no new deps — plain
global CSS + SolidJS signals; one .chat-stream scroll listener drives both the
header (§2) and the pill (§4).

- §1 One-line header + ••• dropdown (<640px): the wrapping header collapses to
  [back · title · state dot · •••]; the convo chip shrinks to a colored dot and
  the thinking toggle / open affordance / two-step Stop move into an anchored
  dropdown reusing TopBar's menu + scrim + Escape pattern. ≥640px is unchanged
  (desktop controls stay via display:contents).
- §2 Quick-return header (<640px): auto-hides on committed upward scroll,
  reveals hair-trigger on any downward scroll, pinned at the very top. Overlay +
  translateY with no reflow (the stream reserves the band via padding-top); only
  user-initiated scroll toggles it — a programmatic-scroll guard keeps streaming
  autoscroll / follow-to-bottom / tap-to-latest from moving it. Pure
  quickReturnReduce() reducer, unit-tested.
- §3 needs-input note relocated out of the composer into the stream as a
  centered status line ("Claude is waiting for your reply."), reactive on state
  and only visible at the bottom; the composer collapses to its input row.
- §4 Jump-to-latest pill: bottom-center over the stream, appears when scrolled
  up with newer content below, accent-emphasized on needs_input, smooth-scrolls
  to the latest (which brings the header back). Pure pillState() derivation,
  unit-tested.
- §5 Composer send merged into the field: one rounded-rect container with a
  borderless textarea and the accent circular send/interrupt button pinned
  bottom-right (36px visual, 44px hit area, stays pinned as the field grows).
  Auto-grow, disabled-when-empty, ⌘/Ctrl+Enter, and the working-state Interrupt
  morph are all preserved (CSS-only, scoped so DialogPanel is untouched).

Evolves the chat UI within ADR-0016/0019/0022's design language (no ADR
conflict). New pure helpers in lib/chatStream.ts; +21 tests (399 total).

Closes #35

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015BHusQyDZJTHJgy8c6tKDE
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!43
No description provided.