Chat UI redesign: quick-return mobile header, in-stream needs-input note, jump-to-latest pill, merged composer #35

Closed
opened 2026-07-08 00:19:10 +02:00 by dominik.polakovics · 0 comments

Goal

Improve the chat view (web/src/routes/RunChat.tsx, route /runs/:id) so that on mobile you can read long assistant messages comfortably, and the composer reads as one polished control. Phone-first; the app is a ~760px centered column at all widths, with a single breakpoint at 640px.

Scope tags: 📱 = mobile only (<640px) · 🌐 = all widths
Primary files: web/src/routes/RunChat.tsx · web/src/base.css (chat block ~L1597–1900) · web/src/lib/chatStream.ts
Constraints: no new dependencies — plain global CSS + SolidJS signals. A single scroll listener on .chat-stream should drive both §2 (header) and §4 (pill).

Current pain points

  • The header (ChatHeader, RunChat.tsx:323) is a flex-wrap row that wraps to 2+ lines on narrow screens.
  • The "needs input" note lives in the sticky composer, so it always occupies vertical space, even while reading history.
  • The send button is a detached 44×44 square beside the textarea — reads as two controls, not one.
  • Nothing hides while reading, so chrome eats space on a phone.

1 · One-line header + ••• dropdown 📱

Collapse the wrapping header to a single non-wrapping row:

[← back] · [title · repo (truncates)] · [● state dot] · [•••]

  • The convo state chip (working / needs-input / question) shrinks to a small colored dot next to the title, reusing the existing convo colors; it expands back to the full text chip at ≥640px.
  • ••• opens an anchored dropdown containing: Show/Hide thinking (toggle, reflects current state) · Open / Attach (the OpenAffordance, when available) · Stop run… (destructive/red, keeps its existing two-step confirm). Items are contextual (Stop only for live runs, Open only when available); ••• is always present for a stable layout.
  • ≥640px: keep the current inline header unchanged.

Acceptance

  • <640px: header is a single row that never wraps — back, truncated title, state dot, •••.
  • State appears as a colored dot <640px and as the full text chip ≥640px.
  • ••• dropdown exposes the thinking-toggle, open/attach (when present), and Stop (with its confirm).
  • ≥640px header is visually unchanged from today.

2 · Quick-return (hide-on-scroll) header 📱

Auto-hide the header while reading, reveal on reverse:

  • Swipe up (advance / read onward) → header slides out. Swipe down (backtrack) → header slides in immediately. At the very top → header pinned visible.
  • Reveal is hair-trigger; hide fires only after ~10px of committed upward scroll and once past the header's own height (prevents flicker / rubber-band bounce).
  • Header overlays the top of the stream and animates via translateYcontent must not reflow / jump.
  • Only user-initiated scroll toggles it. Programmatic scrolling (streaming autoscroll, auto-follow-to-bottom, tap-to-latest) must never change header visibility.
  • Composer stays visible (header-only for v1).
  • Net-new: there is currently no scroll listener; add one on .chat-stream.

Acceptance

  • Swipe up past the threshold hides the header (translateY, no reflow); any swipe down reveals it instantly.
  • Header is always visible at the very top.
  • Streaming / auto-follow / pill-tap scrolling never hides the header.
  • Composer remains visible throughout.

3 · Relocate the needs-input note into the stream 🌐

Move only the needs_input note (currently RunChat.tsx:874, "Claude needs input — reply below, or open it in claude.ai.") out of the composer:

  • Render it as the last item in .chat-stream, a subtle centered status line (styled like chat-lifecycle), not a chat bubble.
  • Reword to "Claude is waiting for your reply." (drop "reply below" and the claude.ai link).
  • Reactive: shows when run state is needs_input, hides when the agent resumes working.
  • Being the last stream item, it is only visible at the bottom; scroll up and it slides off with the rest.
  • Composer in needs_input collapses to just the input row. The other composer notes (ended / transcript-gone / question-state / working-hint) are unchanged.

Acceptance

  • The needs-input note no longer renders in the composer.
  • On needs_input, "Claude is waiting for your reply." appears as the last stream item (status-line style, not a bubble) and clears when working resumes.
  • It is only visible when scrolled to the bottom.
  • Other composer notes are unchanged.

4 · Jump-to-latest pill 🌐 (new)

Closes the discoverability gap created by §2 + §3 — while reading scrolled-up you'd otherwise get no signal that Claude finished / needs you:

  • Bottom-center, floating ~12px above the composer, over the stream.
  • Appears only when the stream is scrolled up (not near bottom) and newer content exists below.
  • Nice-looking: fully-rounded pill, soft elevation/shadow, leading chevron + short label; slide-up + fade in; fades out on reaching bottom.
  • When the new content is a needs-you signal, the pill is emphasized (accent tint) to draw the eye.
  • Tap → smooth-scroll to bottom (which, via the §2 swipe-down rule, brings the header back and reveals the note). Reuses the §2 scroll listener.

Acceptance

  • Pill appears bottom-center only when scrolled up with new content below; hidden at/near the bottom.
  • Emphasized styling when the new content corresponds to needs_input.
  • Tapping smooth-scrolls to the latest message.
  • Animates in/out; never overlaps the composer.

5 · Merge the send button into the textfield 🌐

  • Textarea + send share one rounded-rect container (~14px radius — not a full pill, since it grows multiline). Textarea is borderless inside, with right-padding so text never runs under the button.
  • Send = accent circular icon button pinned bottom-right INSIDE the field, staying pinned as the field grows downward. Visual ~36px with a full 44px hit area.
  • Preserve all existing behavior: auto-grow, disabled-when-empty (dimmed), ⌘/Ctrl+Enter to send / Enter = newline, and the morph into the pulsing Interrupt button while the agent works.

Acceptance

  • Textarea and send button read as one control (shared rounded container; button inside, pinned bottom-right; text never underlaps).
  • Button hit area ≥44px (visual ~36px); auto-grow, disabled-when-empty, keyboard send, and the Interrupt-morph are all preserved.

Out of scope / follow-ups

  • Hiding the composer too for max-reading ("mode B") — deferred.
  • Hide-on-scroll on desktop (≥640px) — deferred (trivially extendable later).
  • Bottom-sheet menu instead of a dropdown — deferred.
## Goal Improve the chat view (`web/src/routes/RunChat.tsx`, route `/runs/:id`) so that on mobile you can read long assistant messages comfortably, and the composer reads as one polished control. Phone-first; the app is a ~760px centered column at all widths, with a single breakpoint at **640px**. **Scope tags:** 📱 = mobile only (<640px) · 🌐 = all widths **Primary files:** `web/src/routes/RunChat.tsx` · `web/src/base.css` (chat block ~L1597–1900) · `web/src/lib/chatStream.ts` **Constraints:** no new dependencies — plain global CSS + SolidJS signals. A single scroll listener on `.chat-stream` should drive both §2 (header) and §4 (pill). ## Current pain points - The header (`ChatHeader`, RunChat.tsx:323) is a `flex-wrap` row that **wraps to 2+ lines** on narrow screens. - The "needs input" note lives in the sticky composer, so it **always occupies vertical space**, even while reading history. - The send button is a **detached 44×44 square** beside the textarea — reads as two controls, not one. - Nothing hides while reading, so chrome eats space on a phone. --- ## 1 · One-line header + `•••` dropdown 📱 Collapse the wrapping header to a single non-wrapping row: `[← back] · [title · repo (truncates)] · [● state dot] · [•••]` - The `convo` state chip (working / needs-input / question) shrinks to a **small colored dot** next to the title, reusing the existing convo colors; it expands back to the full text chip at ≥640px. - `•••` opens an **anchored dropdown** containing: **Show/Hide thinking** (toggle, reflects current state) · **Open / Attach** (the `OpenAffordance`, when available) · **Stop run…** (destructive/red, keeps its existing two-step confirm). Items are contextual (Stop only for live runs, Open only when available); `•••` is always present for a stable layout. - ≥640px: keep the current inline header unchanged. **Acceptance** - [ ] <640px: header is a single row that never wraps — back, truncated title, state dot, `•••`. - [ ] State appears as a colored dot <640px and as the full text chip ≥640px. - [ ] `•••` dropdown exposes the thinking-toggle, open/attach (when present), and Stop (with its confirm). - [ ] ≥640px header is visually unchanged from today. ## 2 · Quick-return (hide-on-scroll) header 📱 Auto-hide the header while reading, reveal on reverse: - **Swipe up** (advance / read onward) → header slides out. **Swipe down** (backtrack) → header slides in **immediately**. At the **very top** → header pinned visible. - Reveal is hair-trigger; **hide** fires only after ~10px of committed upward scroll and once past the header's own height (prevents flicker / rubber-band bounce). - Header **overlays** the top of the stream and animates via `translateY` — **content must not reflow / jump**. - **Only user-initiated scroll toggles it.** Programmatic scrolling (streaming autoscroll, auto-follow-to-bottom, tap-to-latest) must never change header visibility. - Composer stays visible (header-only for v1). - Net-new: there is currently **no** scroll listener; add one on `.chat-stream`. **Acceptance** - [ ] Swipe up past the threshold hides the header (translateY, no reflow); any swipe down reveals it instantly. - [ ] Header is always visible at the very top. - [ ] Streaming / auto-follow / pill-tap scrolling never hides the header. - [ ] Composer remains visible throughout. ## 3 · Relocate the needs-input note into the stream 🌐 Move **only** the `needs_input` note (currently RunChat.tsx:874, *"Claude needs input — reply below, or open it in claude.ai."*) out of the composer: - Render it as the **last item in `.chat-stream`**, a subtle centered **status line** (styled like `chat-lifecycle`), **not** a chat bubble. - Reword to **"Claude is waiting for your reply."** (drop "reply below" and the claude.ai link). - **Reactive**: shows when run state is `needs_input`, hides when the agent resumes working. - Being the last stream item, it is **only visible at the bottom**; scroll up and it slides off with the rest. - Composer in `needs_input` collapses to just the input row. The other composer notes (ended / transcript-gone / question-state / working-hint) are **unchanged**. **Acceptance** - [ ] The needs-input note no longer renders in the composer. - [ ] On `needs_input`, "Claude is waiting for your reply." appears as the last stream item (status-line style, not a bubble) and clears when working resumes. - [ ] It is only visible when scrolled to the bottom. - [ ] Other composer notes are unchanged. ## 4 · Jump-to-latest pill 🌐 *(new)* Closes the discoverability gap created by §2 + §3 — while reading scrolled-up you'd otherwise get **no** signal that Claude finished / needs you: - **Bottom-center**, floating ~12px above the composer, over the stream. - Appears **only** when the stream is scrolled up (not near bottom) **and** newer content exists below. - **Nice-looking**: fully-rounded pill, soft elevation/shadow, leading `↓` chevron + short label; slide-up + fade in; fades out on reaching bottom. - When the new content is a **needs-you** signal, the pill is **emphasized** (accent tint) to draw the eye. - Tap → **smooth-scroll to bottom** (which, via the §2 swipe-down rule, brings the header back and reveals the note). Reuses the §2 scroll listener. **Acceptance** - [ ] Pill appears bottom-center only when scrolled up with new content below; hidden at/near the bottom. - [ ] Emphasized styling when the new content corresponds to `needs_input`. - [ ] Tapping smooth-scrolls to the latest message. - [ ] Animates in/out; never overlaps the composer. ## 5 · Merge the send button into the textfield 🌐 - Textarea + send share **one rounded-rect container** (~14px radius — not a full pill, since it grows multiline). Textarea is borderless inside, with **right-padding** so text never runs under the button. - **Send = accent circular icon button pinned bottom-right INSIDE the field**, staying pinned as the field grows downward. Visual ~36px with a full **44px** hit area. - Preserve all existing behavior: auto-grow, disabled-when-empty (dimmed), **⌘/Ctrl+Enter** to send / **Enter** = newline, and the morph into the pulsing **Interrupt** button while the agent works. **Acceptance** - [ ] Textarea and send button read as one control (shared rounded container; button inside, pinned bottom-right; text never underlaps). - [ ] Button hit area ≥44px (visual ~36px); auto-grow, disabled-when-empty, keyboard send, and the Interrupt-morph are all preserved. --- ## Out of scope / follow-ups - Hiding the **composer** too for max-reading ("mode B") — deferred. - Hide-on-scroll on **desktop** (≥640px) — deferred (trivially extendable later). - **Bottom-sheet** menu instead of a dropdown — deferred.
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#35
No description provided.