Repository navigation
cmux NEXT Home: Chiefs' avatars move, slow at idle and livelier while working - #30534
Conversation
…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
…(a String) 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
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
| var windowKey = true | |
| var windowKey = false |
| } | ||
|
|
||
| private func reshow() { | ||
| list.avatarMotionStyle = HomeTunables.avatarMotion.value.style |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Review (claude-opus-5-5): SHIP at bcd2475
|
For awareness, @lawrencecchen: this touches the vendored MessagesLab sidebar (a |
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Default changesThis pull request changes 3 defaults at
Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Merge receipt: merged as Regenerate the snapshot with Shipped in:
|
…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>
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)
ChiefAvatarMotionGalleryTestsdraws each style at idle and while working, one period in eight frames, toPackages/Shared/CmuxMessagesLab/.build/chief-avatar-motion-gallery.png(motion exaggerated 4x so a point reads on the sheet):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:
reduceMotionDidChangepath.ui.animationSpeedoff, or Home's motion switch off: the host's gate isMotion.home.animatesLoops, re-read on.speedchanges.setWindowActive), and that tracking re-applies every avatar. Applying also readsisVisible,isMiniaturizedandocclusionState. No new notification observers were added.recycle.Performance
CAKeyframeAnimation(48 steps,repeatCount = .infinity) added to its layer and run by the render server. There is no timer, display link or redraw.animation(forKey:)lookup and an equality check, O(1) per visible row.Design
CmuxSidebarView.avatarLoopGateandavatarMotionStyleare instance properties that forward to it), and eachSidebarRowLayerrecords what its avatar shows.// 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 --checkwas not run.Tests
ChiefAvatarMotionTests(MessagesLab package) covers:Observed / Not run
ChiefAvatarMotionTests,ChiefAvatarMotionGalleryTests, and CmuxNextChiefHeaderMonogramTests,HomeChatHeaderDefaultTests,PerChiefPetTestsat the head (results in a comment below).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
Need help on this PR? Tag
@codesmith-botwith 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
Stop conditions
ui.animationSpeedoff, and Home's motion switch stop the loops.Written for commit bcd2475. Summary will update on new commits.