cmux NEXT chat: shared chat faces (author, who's here, typing, answered by), render only

Merged
TE
11d ago
+716-1

Leo, 2026-10-26, on hearing that Chief multiplayer is Lawrence's: "we can still do the regular chat multiplayer". Earlier: "also chief chats we need multiplayer".

Design: manaflow-ai/cmuxterm-hq#4389 (plans/cmux-next/chat-multiplayer/DESIGN.md).

What this adds (render only)

One set of components, multiplayer/Faces.tsx, for several people in one chat. It serves both the regular agent chat and Chief chats:

  • Author chip: the author's face (their picture, else initials on their own hue) and name over each teammate's prompt. The reader's own prompts draw as before.
  • Presence: who else has the chat open, as a stack of up to 3 faces with +N in the chat header.
  • Typing: "Ana is typing…" over the composer, with a three-dot wave. It is a moving wave like a spinner, never a pulsing dot, and reduced motion stills it.
  • Answered by: "Answered by X" on a permission receipt.

An adapter feeds it plain fields: participants.ts reads acpmux records in hb-acpmux's agreed shapes (person_message provenance, answeredBy {id, name} and _acpmux/presence). Names lose hidden and bidi characters and are capped at 64 characters; ids are kept whole. Home's adapter (chiefAdapter.ts) and the Chief mount come in the next PR, to keep this one under 800 lines.

No acpmux on base sends these fields yet, so a live chat renders exactly as today. The sync PRs light it up. Avatar pictures come from Home's avatar service when one is mounted; otherwise the face shows initials.

Gallery

Two entries, both on synthetic data:

  • agent-pane.shared-chat: the whole pane. Variants shared, crowded and alone (alone is the before).
  • agent-pane.multiplayer-looks: three looks plus two people typing at once. Variants picked, names, faces and two-typing.

The pick is picked: face and name over the prompt, overlapping faces in the header, and face + words + dot wave for typing.

Real app (cmux NEXT, fleet build, Blacksmith macOS)

Real captures of the agent chat with the showcase mock daemon, before (36b8f277c12d, the base) and after (ffb62262fc02, this PR's head). The real app has no teammate yet, so no author chip, face or typing line can show here. These captures show that a chat with nobody else draws exactly as before. The shared-chat look is in the gallery captures below; the live two-person capture over cmux host share follows once presence (#25303, #25375) is on the base.

Before (36b8f277c12d)

After (ffb62262fc02)

LightDark
Beforebefore lightbefore dark
Afterafter lightafter dark

Captures (Linux Chromium, synthetic data, head 6316d03)

Before is alone, the same chat with nobody else. It renders exactly as base does today, since no field is set. After is shared, where three teammates are in the chat, and crowded, where four are here and two are typing.

before (alone)after (shared)after (crowded)
dark
light

Looks (agent-pane.multiplayer-looks). The pick is picked:

pickednamesfacestwo typing
dark
light

Risk

Low. Render only, behind fields that nothing sets on base.

  • Touches App.tsx in three places (author chip, header stack, typing line over the composer), each rendering only when its field is set.
  • Adds AUTHOR_HEIGHT to the user row estimate only when author is set.
  • No timers or observers. The typing dots are a CSS animation under prefers-reduced-motion: no-preference. The only effect is useAvatarPicture's existing subscription, cleaned up on unmount.

Tests

  • multiplayer/participants.test.ts: an author comes only from person_message; answeredBy is a person, not a device or session; presence leaves out the reader by its own id; names lose hidden characters and are bounded, while ids stay whole; only inline pictures are used; graphemes.
  • multiplayer/Faces.test.tsx: the chip; the stack with +N and the screen-reader label; nothing shown when alone; typing wording for one, two and several people.
  • Ran on huge-blue: vp fmt, vp lint (0 warnings), tsc --noEmit, and the tests above. gallery-coverage and pane-english fail the same way on base; nothing in those failures is from this change.

Agent session: cosmic-gray

🤖 Generated with Claude Code