Skip to content

Fold the phone chat composer into one pill - #472

Merged
MaggieAppleton merged 6 commits into
mainfrom
mobile/chat-phone
Oct 11, 2026
Merged

MaggieAppleton merged 6 commits into
mainfrom
mobile/chat-phone

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Why

On a phone, Chat's composer was about 125px tall plus a "Chopin unavailable" tab above it. With the keyboard open, that left roughly 80px of transcript (M-35). The @ and # pickers were narrow desktop dropdowns (M-36).

What changed

Phone only: @media (pointer: coarse) and (max-width: 600px), (pointer: coarse) and (max-height: 500px), so landscape phones are included. It lives in one block at the end of composer.css and markdown.css and can switch to :root[data-phone] once #475 lands.

  • One-pill composer. Order: reference + (Mention docs), a 16px draft that grows up to 5 lines, the mode switch (icon only, keeps its Talk to Chopin name), Stop/Resume when active, and a 36px filled send circle, with 0.5rem before send so the mode switch keeps its own 44px target. Buttons keep 44px hit areas. The pill is 44px at rest and 54px with padding. Bottom padding uses env(safe-area-inset-bottom) but drops it while --keyboard-inset covers the home indicator. The mode switch is hidden only when the Planner is off on the server, because then it can never be used. The disabled send is solid --color-inset with no outline ring, instead of faded teal.
  • Status. "Chopin unavailable" and research notices become one quiet 26px line above the pill. Errors keep their red surface and Retry. Offline, Connecting and Reconnect become a slim first row inside the pill. That live region stays mounted, out of flow, so screen readers still hear its first update.
  • Keyboard. viewport.ts already sizes #root to the visual viewport, so the composer sits on the keyboard and the transcript stays pinned through its existing ResizeObserver. The draft is now capped at 30% of the exposed height so a long draft can't push the transcript out of view.
  • Pickers. Mention, reference and command lists become a full-width list just above the pill. Rows are 44px, max height is 40% of the exposed height, and scrolling stays inside the list. The pickers' inline maxHeight moved to CSS. Desktop values are unchanged.
  • Messages. Chat prose uses --text-base, the same size as the document's prose. Code blocks wrap instead of scrolling sideways. Links already wrapped. There are no hover-only message actions to fix.
  • New tokens: --composer-pill-control, --composer-pill-icon, --radius-composer-pill.

M-36 ("@Cho" + Enter sent "Cho") is display behaviour, not a lost mention. The wire frame was {"text":"@cho","to":"room"} and data-chat-raw is @cho. referenceRenderModel deliberately draws @login as a capitalised name (hi @octocat shows as "hi Octocat"). With the Planner off there is no candidate for @cho, so no picker opens and Enter sends. I left this unchanged. Whether unknown handles should keep their @ is a product call.

M-37 context chip: skipped. On a phone the header already names the document directly above Chat. A chip would mean passing the title into Chat and would repeat the header.

Screenshots (iPhone 15 Pro; BEFORE = main on the reference server, AFTER = this branch)

The keyboard is emulated as a 313px visual viewport.

Chat at rest
Chat at rest, before and after

Keyboard open with a draft
Keyboard open with a draft, before and after

Mention picker
Mention picker, before and after

Reference picker
Reference picker, before and after

Desktop 1440×900 composer, before (top) and after (bottom): identical. The bounding boxes of the composer, draft, footer, mode switch, send and pickers match exactly.
Desktop composer, before and after

Testing

  • bun run ci: passes (tokens, design contract, design record, impeccable).
  • bun run types: passes.
  • bun test in apps/web: 631 pass.
  • New e2e/chat-phone.e2e.ts:
    • Pill height ≤ 64px, the controls sit in one row, send is 36px, the status line is ≤ 28px, and disabled send is opaque.
    • At phone width, the mention and reference pickers are full-width lists above the pill with rows ≥ 44px and contained overscroll.
    • With the keyboard emulated through e2e/visual-viewport.ts at 508px: the composer stays in view on the keyboard and the transcript keeps ≥ 200px (measured 250px at 390×844, 150px at 375×667).
    • At desktop, the mode label, footer row and dropdown-width picker are unchanged.
  • New: a transcript sitting at the bottom stays pinned when another person's message arrives while the keyboard is open.
  • e2e/chat-composer.e2e.ts: the touch case now expects a one-line rest instead of two.
  • I didn't run E2E locally because ports 8788, 8789, 8792 and 8797 are held by other processes, so CI is the check.

Needs a real device

  • iOS Safari keyboard behaviour with the pill.
  • Whether env(safe-area-inset-bottom) - var(--keyboard-inset) behaves as intended.
  • lh support (Safari 16.4+).
  • Most of the remaining keyboard-up space still goes to the 121px header and top tabs. That belongs to slice 2, the bottom tab bar.

🤖 Generated with Claude Code

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Changes needed

Reviewed head dcf64eed. I checked it in headless Chromium on iPhone 15 Pro, iPhone SE (375×667), iPhone 15 Pro landscape, and desktop at 1440×900 and 1280×800. The keyboard was emulated with e2e/visual-viewport.ts. BEFORE is the reference server on main.

The design works well and is close to prototype 07-chat.png. At rest the composer drops from 172px to 80px. Every control is still in the pill: mode switch, Mention docs, Stop/Resume, send and Reconnect. The draft is 16px. Send is a 36px circle with a 44px ::after hit area, which I probed with elementFromPoint. Desktop is unchanged: the composer, draft, footer, mode, send, notice and picker bounding boxes are identical before and after at 1440 and 1280, and the picker max-height is 256px in both.

Checked with the keyboard open (323px exposed):

  • The composer sits on the keyboard.
  • A 9-line draft caps at 97px and scrolls (30% of 323).
  • 12 incoming messages from a second user kept the transcript pinned to the bottom.
  • The @ and # lists are full-width above the pill, with 44px rows and overscroll-behavior: contain.
  • ArrowDown ×9 through 14 references keeps the active row in view inside the list (picker height 163px is 40% of 407). aria-activedescendant updates and Enter inserts @cy .

Must fix

  1. CI e2e is red because of the new test. e2e/chat-phone.e2e.ts:85 fails all three attempts: row.height is 43.98, expected ≥ 44. The width poll finishes while the data-menu-enter scale is still just under 1. Poll the row height too (expect.poll(...).toBeGreaterThanOrEqual(44)), or measure offsetHeight, which ignores transforms. The other failure, responsive-workspace.e2e.ts:291 (a 100ms reduced-motion swap), looks like an unrelated flake. Re-run it.

Should fix

  1. The mode switch's touch target is under 44px when the Planner is on. The mode switch and send are 36px boxes with no gap between them (SE: mode 278–314, send 314–350). Send comes later in the DOM, so its 44px ::after takes the 4px next to it. A tap 3px left of send's edge lands on Send. The mode switch keeps about 36–40px of width. Fix: add column-gap: 0.5rem between the trailing controls, or margin-inline-start: 0.5rem on .send-action inside the composer.css phone block.
  2. The pinned-transcript behaviour has no test. The keyboard test checks geometry only. Add one incoming message while the viewport is 508px tall and assert that the transcript stays scrolled to the bottom. It works today; this guards it.

Nice to have

  1. Disabled send still draws the 1px outline ring (oklch(0 0 0 / 0.15)), so it reads as a bordered grey button. The prototype has none. Drop it in the phone :disabled rule.
  2. --color-gray-200 is a palette token. A semantic token such as --color-selected, or a new one, would match the tokens rule better.
  3. The 2px brand :focus-within outline with its 2px offset draws a heavy double ring around the whole pill while typing. Consider a 1px inset brand border on the phone, keeping :focus-visible rings on the buttons.
  4. The phone gate is max-width: 600px, so phone landscape (734×343) keeps the roughly 125px desktop composer in a 343px-tall split. That's fine as a follow-up once slice 2's phone-class hook lands.

M-36 investigation

The conclusion is sound. With the Planner off, @cho + Enter sends the raw text. I reproduced it: raw @cho#@cy renders as "Cho#Cy". The user still sees the problem, though. An unknown handle shows as a capitalised name with no @, which reads like a lost mention. Please file that as its own issue (rendering unknown handles) rather than closing M-36.

The + is a direct # reference button, not a menu. No capability is lost, and the spec's @ button inside the pill is not needed. The M-37 skip is reasonable.

MaggieAppleton and others added 6 commits October 11, 2026 10:15
On a coarse-pointer phone the composer rested at about 125px plus a status
tab, leaving roughly 80px of transcript above the keyboard. It now rests as
one 44px pill: reference button, auto-growing draft and a 36px send circle,
with the mode switch icon-only and connection state as a slim row inside it.
"Chopin unavailable" is a quiet line above the pill. Mention, reference and
command pickers become a full-width list resting on the composer with 44px
rows. Chat prose uses the document's body size on phones and code wraps.
Desktop layout is unchanged.

Why: apps/web/src/chat/composer.css | the phone connection status stays mounted out of flow so its live region still hears its first word
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Space the send circle so the mode switch keeps its own 44px target, draw
disabled send without an outline on an inset tone, hug the pill with its
focus edge, and apply the phone composer to coarse landscape phones too.
Add an e2e check that a transcript at the bottom stays pinned when a
message arrives with the keyboard open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The focus test reserves outline geometry for theme.css, so the pill keeps
the shared ring. The disabled send hides its edge by colour instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton merged commit 6140138 into main Oct 11, 2026
3 checks passed
@MaggieAppleton
MaggieAppleton deleted the mobile/chat-phone branch October 11, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant