cmux NEXT Home: a Chief's avatar shows its state like the Codex pet, and never bounces

Merged
TE
3d ago
+660-187

Leo, 2026-11-03: "oh you guys completely misinterpreted the "animated" thing... don't bounce the circle. i mean like if you ask codex, codex (idk if you wanna ask sol 6.1 xhigh or astra high) their dot thing has a few different animations for different scenarios."

His first ask (2026-11-01: "i also want them animated like how chatgpt animates their dots") became #30534, which bobs the whole avatar up and down (Float, the default), faster while the Chief works. That is the bounce he turned down. The reference he points at is the Codex app's pet: one distinct animation per scenario, not one motion at two speeds.

What changes

A Chief's avatar in the Home list and pinned grid shows what its Chief is doing, one look per state. The avatar never leaves its place: no bob, bounce or slide. The only transforms are a small scale or turn about its centre.

StateWhenRing (default)
idlenothing runningat rest; a soft glint sweeps across the face once every 7 s (a pet's blink), clipped to the avatar picture's own shape
workingits turn is running (InboxRow.isTyping)a quarter arc in the accent runs round the avatar's edge, 1.6 s a turn, linear
waitinga reply is unreada still thin ring in the accent
faileda send failed (InboxRow.hasFailedSend)a still thin red ring

A state change plays one cue of at most 240 ms (design/motion-and-polish.md). When a turn ends (working to idle or waiting), the avatar pops 4%. A failure gives it a quick shake of a few degrees. A reply that arrives with no turn seen fades its ring in. A reload of the same state replays nothing.

Versions (gallery)

The list ships with four styles, and ChiefAvatarMotionGalleryTests draws each one in every state plus the two cues, 8 frames each. It writes them to Packages/Shared/CmuxMessagesLab/.build/chief-avatar-motion-gallery.png:

  • Ring (default): the table above.
  • Glow: a soft glow behind the avatar's shape. None at idle; accent while working, with a glint sweeping every 1.8 s; accent while waiting; red when failed. Same cues.
  • Tilt: the avatar turns its head. A 3° glance every 6 s at idle and a ±4° rock (1.4 s) while working. Ring's still rings and cues.
  • Still: no motion and no ring.

I picked Ring. Each state reads differently at row size, even at a glance and in a dense list. Working also matches the rest of the app's "running" language (an arc going round), so it reads as working without a second mark. Switch styles in Debug Settings > Home > Chief avatar motion (home.avatarMotion). A saved Float, Breathe or Sway value no longer matches a choice and falls back to Ring.

What it does not touch

  • The shared working Spinner (the thin 3/4 arc, #32754) is unchanged. This is the Chief's avatar, not the agent working-state mark.
  • The Chief pet's sprite rows (ChiefPetSprite) are unchanged. A follow-up could also step the animated characters' state rows (Mochi, Captain, Sprout, Bolt) in the list.
  • #32861 (lucasr1b, open) sets home.avatarMotion to Still so avatars stop bouncing. This PR removes the bounce itself and changes the same default line, so the two conflict on that line. A comment on #32861 says so.

Stop conditions and cleanup

Every loop and cue is removed by each of the following. The still waiting and failed rings stay, as static indicators:

  • Reduce Motion (system or the cmux pin), calm mode, ui.animationSpeed off, and Home's motion switch, through the existing avatarLoopGate and reduceMotionDidChange path. Working's arc stands still as a quarter ring, so the state still reads.
  • App inactive, or the window not key, hidden or minimized: the list's existing setWindowActive tracking. No new observers.
  • Row back in the pool: SidebarAvatarMotion.stop removes everything, rings included.
  • Style Still: nothing is drawn.

Performance

  • No main-thread work per frame. Each look is Core Animation on the avatar layer and at most two named sublayers of it (a ring CAShapeLayer and a masked glint band), run by the render server. There is no timer or display link.
  • A reload that keeps the state keeps the running loop (asserted by test), so a list update adds no animation work. Per configure, the cost is one look comparison and a frame fit, O(1) per visible row.
  • No vendored file changes, so Patches/ is untouched.
  • No same-harness before/after frame numbers. The previous loop was also render-server only.

Tests

ChiefAvatarMotionTests (MessagesLab), rewritten for the states:

  • each state has its own look, and no style in any state animates the avatar's position;
  • a state change plays a short cue (pop ≤ 1.04 scale, shake), and a reload or Reduce Motion plays none;
  • Reduce Motion and the host's gate stop every loop and keep the still rings;
  • an inactive app or hidden window stops every loop;
  • a pooled row clears, and Still draws nothing;
  • a style change restyles every avatar at once;
  • loops close and are phase-seeded per conversation.

Observed / Not run

  • Observed: python3 scripts/cmux-next/prepush-gate.py: no new violations. Fleet compile and the MessagesLab suites: results in a comment below.
  • Not run: a DEV build watching the list live. Leo tries the build.

Spec: design/motion-and-polish.md (240 ms cue cap, every loop stops). Prior art: #30534 (replaced here), #32861 (open, same default line).

No capture: the looks are in the gallery sheet; Leo tries the build.

Changelog

Changed: A Chief's avatar in Home shows what the Chief is doing (a glint at rest, an arc while it works, a ring when a reply waits or a send failed) instead of bobbing.

Agent session: agent session

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Changes the Chief avatar in the Home list and pinned grid to show what its Chief is doing—one look per state, like the Codex pet—instead of bobbing. The avatar never leaves its place; the only transforms are a small scale or turn about its centre.

  • Idle glints across the face occasionally; working runs a quarter arc round the edge; an unread reply draws a still accent ring; a failed send a still red ring.
  • A state change plays one cue of at most 240 ms: a 4% pop when a turn ends, a quick shake on failure, and a ring fade-in when a reply arrives with no turn seen.
  • Under Reduce Motion no loops run, but the working arc stands still as a quarter ring and the still rings stay, so the state still reads.
  • Replaces Float, Breathe and Sway with Ring (new default), Glow, Tilt and Still; saved old setting values fall back to Ring.
  • A state change cues per conversation id, not per row layer, so a row redrawn on another layer still plays its cue.