Skip to content

cmux NEXT Home: Chiefs' avatars move, slow at idle and livelier while working - #30534

Merged
teamleaderleo merged 7 commits into
feat-cmux-nextfrom
lane/chief-avatar-motion
Nov 1, 2026
Merged

teamleaderleo merged 7 commits into
feat-cmux-nextfrom
lane/chief-avatar-motion

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Leo, 2026-11-01: "i also want them animated like how chatgpt animates their dots".

A Chief's own avatar in Home's list and pinned grid now moves: a slow loop at idle, and a quicker, deeper one while the Chief works (its turn is running, InboxRow.isTyping). Conversations without their own avatar, people and the CH monogram rows only move when they are a Chief's.

Versions (gallery)

ChiefAvatarMotionGalleryTests draws each style at idle and while working, one period in eight frames, to Packages/Shared/CmuxMessagesLab/.build/chief-avatar-motion-gallery.png (motion exaggerated 4x so a point reads on the sheet):

Style Idle Working
Float (default) bobs ±1 pt, 3.2 s ±1.6 pt, 0.9 s
Breathe grows up to 3.5%, 3.6 s a double beat of 7%, 1.1 s (closest to ChatGPT's dot)
Sway tilts ±2.5°, 4.0 s ±5°, 1.0 s
Still none none

I picked Float. It reads as "alive" at row size without changing the avatar's outline, and in a dense list it is the least distracting. Switch styles in Debug Settings > Home > Chief avatar motion (home.avatarMotion). Each avatar starts at its own phase (from its conversation id), so a list of Chiefs never moves in lockstep.

Stop conditions

Each one removes the loop and leaves the avatar at rest:

  • Reduce Motion (system, or the cmux pin) and calm mode: Home's existing reduceMotionDidChange path.
  • ui.animationSpeed off, or Home's motion switch off: the host's gate is Motion.home.animatesLoops, re-read on .speed changes.
  • App inactive, or the window not key, minimized, hidden or covered. The vendored list already tracks its window's key state (setWindowActive), and that tracking re-applies every avatar. Applying also reads isVisible, isMiniaturized and occlusionState. No new notification observers were added.
  • Off screen: a row or tile going back to the pool (scrolled away, or the conversation removed) stops its loop in recycle.
  • Style Still.

Performance

  • 0 main-thread work per frame. Each avatar's motion is one CAKeyframeAnimation (48 steps, repeatCount = .infinity) added to its layer and run by the render server. There is no timer, display link or redraw.
  • A reload that keeps the same mode and style keeps the running loop (asserted by test), so a list update adds no animation work.
  • Per configure, the cost is one animation(forKey:) lookup and an equality check, O(1) per visible row.
  • No same-harness before/after frame numbers yet. That is a follow-up on the Home perf harness.

Design

  • No module statics or KVC: the host's gate and style live on the list's controller (CmuxSidebarView.avatarLoopGate and avatarMotionStyle are instance properties that forward to it), and each SidebarRowLayer records what its avatar shows.
  • The vendored edits are marked // cmux:. Patches/ was regenerated by reverse-applying the base patch to recover the pin, then diffing again (no MessagesLab checkout on this host); sync-messageslab.sh --check was not run.

Tests

ChiefAvatarMotionTests (MessagesLab package) covers:

  • idle vs working loops on rows and tiles;
  • no motion without an avatar of its own, or with no motion asked;
  • the same animation object kept across a reload;
  • Reduce Motion, and the host's gate stopping and restarting loops;
  • resigning key stopping them, and the real off-screen check;
  • a pooled row stopping, and Still never starting;
  • every loop closing (first frame equals last frame) and seeds differing in phase.

Observed / Not run

  • Fleet: MessagesLab ChiefAvatarMotionTests, ChiefAvatarMotionGalleryTests, and CmuxNext ChiefHeaderMonogramTests, HomeChatHeaderDefaultTests, PerChiefPetTests at the head (results in a comment below).
  • Not run: a DEV build watching the list live; the fresh-home launch (Chief area) follows in a comment.

No capture: the motion is in the gallery sheet; Leo tries the build.

Changelog

Added: Chiefs' avatars in Home's list move gently, and a little more while the Chief is working.

Agent session: agent session

🤖 Generated with Claude Code


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


Summary by cubic

Chiefs' avatars in Home's list and pinned grid now move — a slow loop at idle and a livelier one while the Chief works (its turn is running). Each avatar's motion is one CAKeyframeAnimation run by the render server, so there's no main-thread work per frame.

Motion styles

  • Float (default) bobs the avatar ±1 pt at 3.2 s; working is ±1.6 pt at 0.9 s.
  • Breathe grows it up to 3.5%; working is a double beat of 7% (closest to ChatGPT's dot).
  • Sway tilts ±2.5°; working is ±5°.
  • Switch styles in Debug Settings > Home > Chief avatar motion; a style change restyles every avatar at once. Each avatar starts at its own phase (from its conversation id), so a list of Chiefs doesn't move in lockstep.

Stop conditions

  • Reduce Motion, calm mode, ui.animationSpeed off, and Home's motion switch stop the loops.
  • Loops also stop when the app is inactive, the window is not key, minimized, hidden, or off screen, and when a row returns to the pool.
  • A reload that keeps the same avatar keeps its running loop, so list updates add no animation work.
  • Style Still never starts a loop.

Written for commit bcd2475. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 7 commits November 1, 2026 08:10
…le it works

Leo 2026-11-01: "i also want them animated like how chatgpt animates their dots".
A Chief's own avatar in Home's list and pinned grid runs one Core Animation keyframe
loop (float by default; breathe, sway and still in Debug Settings > Home > Avatar
motion and the gallery). It stops under Reduce Motion, Home's motion gate (calm mode,
speed off, the Home switch), when the app goes inactive or the window is hidden,
miniaturized or occluded, and when the row returns to the pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3a05ae6e302bba4a7be8
…d row layers

No module statics or KVC keys: the host's gate and style are the sidebar controller's,
and each row layer records what its avatar shows. The vendored files' patches are
regenerated against the pinned upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3a05ae6e302bba4a7be8
…nd on screen

The list's own key tracking (setWindowActive) re-applies every avatar, so the loop
stops when the app goes inactive, another window takes the keyboard, or the window
is minimized or hidden, with no new notification observers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3a05ae6e302bba4a7be8
…r its occlusion state, and a style change applies at once

Review follow-ups at 8748e77: the window server reports occlusion after the window
becomes key, so a launch or an un-minimize could leave every avatar still; a key,
shown, unminimized window is enough. The Debug style setter re-applies every avatar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3a05ae6e302bba4a7be8
…gh the list's controller

The layer walk starts at the sidebar view's own layer, which is nil until an ancestor
makes it layer-backed; the controller holds every row and tile. Fleet red at bcd2475:
ChiefAvatarMotionTests.reduceMotionAndTheHostsGateStopEveryLoop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3a05ae6e302bba4a7be8

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/Shared/CmuxMessagesLab/Tests/MessagesLabSidebarTests/ChiefAvatarMotionGalleryTests.swift">

<violation number="1" location="Packages/Shared/CmuxMessagesLab/Tests/MessagesLabSidebarTests/ChiefAvatarMotionGalleryTests.swift:45">
P3: Between `lockFocus()` and `unlockFocus()` the `try #require` calls can throw and skip `unlockFocus()`. Move the throwing lookups before `lockFocus()`, or wrap the drawing in a `defer`-balanced block.</violation>
</file>

<file name="Packages/Shared/CmuxMessagesLab/Tests/MessagesLabSidebarTests/ChiefAvatarMotionTests.swift">

<violation number="1" location="Packages/Shared/CmuxMessagesLab/Tests/MessagesLabSidebarTests/ChiefAvatarMotionTests.swift:125">
P2: The 'real check' only tests the negative case. Add a case that orders the fixture window on screen with `onScreenPin` nil and asserts that the loop runs, so the visibility gate is covered both ways.</violation>
</file>

<file name="Packages/Shared/CmuxMessagesLab/Sources/MessagesLabSidebar/Vendor/appkit-native/SidebarView.swift">

<violation number="1" location="Packages/Shared/CmuxMessagesLab/Sources/MessagesLabSidebar/Vendor/appkit-native/SidebarView.swift:1636">
P2: Covered or hidden Home windows can keep their infinite avatar loops because refresh only follows key-window changes. Observe window occlusion/visibility changes and reapply the avatar gate when they occur.</violation>
</file>

<file name="Packages/Shared/CmuxMessagesLab/Sources/MessagesLabSidebar/Cmux/SidebarAvatarMotion.swift">

<violation number="1" location="Packages/Shared/CmuxMessagesLab/Sources/MessagesLabSidebar/Cmux/SidebarAvatarMotion.swift:48">
P2: `windowKey` defaults to `true`, so the first `setWindowActive(true)` from `viewDidMoveToWindow` is a no-op and never refreshes avatars. Rows configured before the view has a window stay at rest until something else re-applies them. Default it to `false` so the first key report always triggers `refreshAvatarMotion()`.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextHome/Sidebar/HomeSidebarView.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextHome/Sidebar/HomeSidebarView.swift:205">
P2: Changing `home.avatarMotion` does not update avatars already on screen; this read only runs during `reshow()`, not when the tunable changes. Observe the tunable or make the style setter refresh all visible avatar layers.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

f.controller.avatarMotion.onScreenPin = nil
f.controller.setWindowActive(false)
f.controller.setWindowActive(true)
#expect(f.loop("scout") == nil, "a window that is not on screen never loops")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The 'real check' only tests the negative case. Add a case that orders the fixture window on screen with onScreenPin nil and asserts that the loop runs, so the visibility gate is covered both ways.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/Shared/CmuxMessagesLab/Tests/MessagesLabSidebarTests/ChiefAvatarMotionTests.swift, line 125:

<comment>The 'real check' only tests the negative case. Add a case that orders the fixture window on screen with `onScreenPin` nil and asserts that the loop runs, so the visibility gate is covered both ways.</comment>

<file context>
@@ -0,0 +1,156 @@
+        f.controller.avatarMotion.onScreenPin = nil
+        f.controller.setWindowActive(false)
+        f.controller.setWindowActive(true)
+        #expect(f.loop("scout") == nil, "a window that is not on screen never loops")
+    }
+
</file context>

func scaleChanged() { avatars.removeAll(); invalidateAll() }
func setWindowActive(_ active: Bool) {
requestedWindowActive = active // cmux
if avatarMotion.windowKey != active { avatarMotion.windowKey = active; refreshAvatarMotion() } // cmux: avatars move only in the key window

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Covered or hidden Home windows can keep their infinite avatar loops because refresh only follows key-window changes. Observe window occlusion/visibility changes and reapply the avatar gate when they occur.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/Shared/CmuxMessagesLab/Sources/MessagesLabSidebar/Vendor/appkit-native/SidebarView.swift, line 1636:

<comment>Covered or hidden Home windows can keep their infinite avatar loops because refresh only follows key-window changes. Observe window occlusion/visibility changes and reapply the avatar gate when they occur.</comment>

<file context>
@@ -1629,6 +1633,7 @@ final class SidebarController: NSViewController, NSSearchFieldDelegate, NSMenuDe
     func scaleChanged() { avatars.removeAll(); invalidateAll() }
     func setWindowActive(_ active: Bool) {
         requestedWindowActive = active // cmux
+        if avatarMotion.windowKey != active { avatarMotion.windowKey = active; refreshAvatarMotion() } // cmux: avatars move only in the key window
         let active = active || neutralSelection // cmux: neutral selection looks the same in every window state
         guard active != windowActive else { return }
</file context>

/// How avatars move (the host's Debug setting).
var style = CmuxSidebarAvatarMotionStyle.float
/// The list's window is key (the vendored list's key tracking sets it).
var windowKey = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: windowKey defaults to true, so the first setWindowActive(true) from viewDidMoveToWindow is a no-op and never refreshes avatars. Rows configured before the view has a window stay at rest until something else re-applies them. Default it to false so the first key report always triggers refreshAvatarMotion().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/Shared/CmuxMessagesLab/Sources/MessagesLabSidebar/Cmux/SidebarAvatarMotion.swift, line 48:

<comment>`windowKey` defaults to `true`, so the first `setWindowActive(true)` from `viewDidMoveToWindow` is a no-op and never refreshes avatars. Rows configured before the view has a window stay at rest until something else re-applies them. Default it to `false` so the first key report always triggers `refreshAvatarMotion()`.</comment>

<file context>
@@ -0,0 +1,152 @@
+    /// How avatars move (the host's Debug setting).
+    var style = CmuxSidebarAvatarMotionStyle.float
+    /// The list's window is key (the vendored list's key tracking sets it).
+    var windowKey = true
+    /// Tests' pin for "the window is on screen" (an offscreen test window never is).
+    var onScreenPin: Bool?
</file context>
Suggested change
var windowKey = true
var windowKey = false

}

private func reshow() {
list.avatarMotionStyle = HomeTunables.avatarMotion.value.style

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Changing home.avatarMotion does not update avatars already on screen; this read only runs during reshow(), not when the tunable changes. Observe the tunable or make the style setter refresh all visible avatar layers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextHome/Sidebar/HomeSidebarView.swift, line 205:

<comment>Changing `home.avatarMotion` does not update avatars already on screen; this read only runs during `reshow()`, not when the tunable changes. Observe the tunable or make the style setter refresh all visible avatar layers.</comment>

<file context>
@@ -200,6 +202,7 @@ public final class HomeSidebarView: NSView, WindowControlsAvoiding {
     }
 
     private func reshow() {
+        list.avatarMotionStyle = HomeTunables.avatarMotion.value.style
         var entries = (model.pinned + model.rows).map(Self.entry)
         if let draftRowTitle {
</file context>

let strip = CGFloat(Self.frames) * Self.cell
let sheet = NSImage(size: NSSize(width: CGFloat(modes.count) * (strip + gap) + gap,
height: CGFloat(styles.count) * (Self.cell + gap) + gap))
sheet.lockFocus()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Between lockFocus() and unlockFocus() the try #require calls can throw and skip unlockFocus(). Move the throwing lookups before lockFocus(), or wrap the drawing in a defer-balanced block.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/Shared/CmuxMessagesLab/Tests/MessagesLabSidebarTests/ChiefAvatarMotionGalleryTests.swift, line 45:

<comment>Between `lockFocus()` and `unlockFocus()` the `try #require` calls can throw and skip `unlockFocus()`. Move the throwing lookups before `lockFocus()`, or wrap the drawing in a `defer`-balanced block.</comment>

<file context>
@@ -0,0 +1,70 @@
+        let strip = CGFloat(Self.frames) * Self.cell
+        let sheet = NSImage(size: NSSize(width: CGFloat(modes.count) * (strip + gap) + gap,
+                                         height: CGFloat(styles.count) * (Self.cell + gap) + gap))
+        sheet.lockFocus()
+        NSColor(white: 0.96, alpha: 1).setFill()
+        NSRect(origin: .zero, size: sheet.size).fill()
</file context>

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (claude-opus-5-5): SHIP at bcd2475

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

For awareness, @lawrencecchen: this touches the vendored MessagesLab sidebar (a // cmux: hook in configure/configureTile/recycle/setWindowActive, with Patches/ regenerated) and HomeSidebarView, so that a Chief's own avatar loops in Home's list. There are no new observers or statics: the loop rides the list's own key tracking and Home's motion gate. Follow-ups are in #30552.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 0096dea: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr30534 -f ref=lane/chief-avatar-motion -f platform=macos -f backend_mode=local

Default changes

This pull request changes 3 defaults at 0096dea07c0b (what everyone gets without configuring anything):

  • tunable-source.CmuxNextHome/HomeTunables.swift:all: "656de05e469c9a01e3cebf88fef1c55fb04193a607ca3c30e6f5783347c23c5f" -> "2ef18b380e7696b9b169709d5bfcc991bf5b8cbd0b274663c23b01c0f52f0a67"
  • tunable-source.CmuxNextHome/HomeTunables.swift:avatarMotion: (none) -> "5b5e921725a2994a774c213cd7f0c62bbd3a99e843c6c23bf6e31dab54dc27d2"
  • tunable.home.avatarMotion: (none) -> "float"

Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Review (<reviewer>): SHIP at 0096dea07c0b497c1fe564b9cf9836513c43cc32, from a member, not a self-review (the newest verdict naming the head decides; a later BLOCK beats an earlier SHIP), or with an approving review on this commit from @lawrencecchen or @austinywang (the call owners in .github/defaults-call-owners.json, never the author). A receipt posted as a review re-runs this check; after a plain comment, re-run it. The needs a call label alone does not pass, and a new push needs a new receipt.

Merge receipt: merged as f8b2423a76d0 with these defaults changed, WITHOUT a SHIP receipt or an approving call owner review on its head.

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
@teamleaderleo
teamleaderleo merged commit f8b2423 into feat-cmux-next Nov 1, 2026
124 of 131 checks passed
@teamleaderleo
teamleaderleo deleted the lane/chief-avatar-motion branch November 1, 2026 09:52
teamleaderleo added a commit that referenced this pull request Nov 3, 2026
…and never bounces (#33395)

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

Leo 2026-11-03: "don't bounce the circle. i mean like if you ask codex, ... their dot thing has a
few different animations for different scenarios." #30534 read his first ask ("animated like how
chatgpt animates their dots") as a bob of the whole avatar. Each state now has its own look on the
avatar's place: idle glints now and then, working runs an arc round the edge, an unread reply draws
a still accent ring, a failed send a still red ring; a turn ending pops 4% and a failure shakes a few
degrees (240 ms cues). Ring is the default; Glow, Tilt and Still are the gallery's variants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a1179b2311aff0f037aa

* Home avatar looks: a state change cues by conversation, not by the layer that drew it

Fleet red at b2874ee: ChiefAvatarMotionTests.aStateChangePlaysOneShortCue (a changed row is drawn
on another layer, which had no state before it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a1179b2311aff0f037aa

* Home avatar looks gallery: strips start at each loop's own start; turns and pops drawn 3x

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a1179b2311aff0f037aa

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 0096dea0 Deployed Nov 1, 2026 by teamleaderleo via wait for same-key tree publisher #94997
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant