Add sidebar active agent indicator - #6480
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds a sidebar active-agent indicator with settings, snapshot, and rendering updates. It also adds a ChangesSidebar agent activity
Spinner Gallery debug window
CLI workspace loading subcommand
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (3 errors, 1 warning)
✅ Passed checks (21 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryAdds a GPU-driven Core Animation sidebar spinner that appears on workspace rows with running coding agents or active manual loaders (
Confidence Score: 5/5Safe to merge after dogfood; the manual-key namespace is correctly isolated from hibernation/PID handling, the spinner pauses when not visible, and all three new socket/CLI paths are covered by regression tests. All findings are style and polish — a missing guard in a didSet, a caseless enum namespace, and a missing accessibilityHidden on an opacity-0 view. None affect correctness, data integrity, or the agent lifecycle state machine. Sources/Sidebar/GPUSpinnerNSView.swift (missing guard in color didSet) and Sources/Sidebar/SidebarWorkspaceTrailingStatusSlot.swift (accessibility). Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant CLI as cmux CLI
participant Socket as Control Socket
participant Coord as ControlCommandCoordinator
participant TC as TerminalController
participant WS as Workspace
participant Sidebar as Sidebar SwiftUI
CLI->>CLI: parseWorkspaceLoadingArguments
CLI->>Socket: workspace_loading manual on --tab wsId
Socket->>Coord: sidebarWorkspaceLoading
Coord->>TC: controlSidebarOnMain
TC->>WS: clearAgentLifecycle key panelId nil
TC->>TC: count manual loaders cap 32
TC->>WS: setAgentLifecycle key panelId running
WS-->>TC: before after state
TC-->>Coord: ControlSidebarWorkspaceLoadingState
Coord-->>Socket: "before=ON after=ON"
Socket-->>CLI: "before=ON after=ON"
WS->>Sidebar: snapshotRefreshPolicy triggers
Sidebar->>Sidebar: visibleActiveCodingAgentCount
Sidebar->>Sidebar: show GPUSpinner on workspace row
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant CLI as cmux CLI
participant Socket as Control Socket
participant Coord as ControlCommandCoordinator
participant TC as TerminalController
participant WS as Workspace
participant Sidebar as Sidebar SwiftUI
CLI->>CLI: parseWorkspaceLoadingArguments
CLI->>Socket: workspace_loading manual on --tab wsId
Socket->>Coord: sidebarWorkspaceLoading
Coord->>TC: controlSidebarOnMain
TC->>WS: clearAgentLifecycle key panelId nil
TC->>TC: count manual loaders cap 32
TC->>WS: setAgentLifecycle key panelId running
WS-->>TC: before after state
TC-->>Coord: ControlSidebarWorkspaceLoadingState
Coord-->>Socket: "before=ON after=ON"
Socket-->>CLI: "before=ON after=ON"
WS->>Sidebar: snapshotRefreshPolicy triggers
Sidebar->>Sidebar: visibleActiveCodingAgentCount
Sidebar->>Sidebar: show GPUSpinner on workspace row
Reviews (26): Last reviewed commit: "Merge origin/main into agent activity in..." | Re-trigger Greptile |
| private static func resolvedCGColor(_ color: NSColor) -> CGColor { | ||
| color.usingColorSpace(.deviceRGB)?.cgColor | ||
| ?? NSColor.secondaryLabelColor.usingColorSpace(.deviceRGB)?.cgColor | ||
| ?? CGColor(gray: 0.6, alpha: 1) | ||
| } |
There was a problem hiding this comment.
NSColor.usingColorSpace(.deviceRGB) snapshots the color into the device color space at call time. On Macs with P3 displays the layer's backing uses displayP3, so a deviceRGB CGColor can be interpreted with slightly shifted primaries. More importantly, dynamic semantic colors like NSColor.secondaryLabelColor change with appearance; snapshotting into a concrete CGColor captures the appearance at the instant of the call. If SwiftUI calls updateNSView before the appearance environment propagates, the snapshot misses the new appearance. Using .sRGB is the conventional choice for layer colors derived from NSColor on macOS.
| private static func resolvedCGColor(_ color: NSColor) -> CGColor { | |
| color.usingColorSpace(.deviceRGB)?.cgColor | |
| ?? NSColor.secondaryLabelColor.usingColorSpace(.deviceRGB)?.cgColor | |
| ?? CGColor(gray: 0.6, alpha: 1) | |
| } | |
| private static func resolvedCGColor(_ color: NSColor) -> CGColor { | |
| color.usingColorSpace(.sRGB)?.cgColor | |
| ?? NSColor.secondaryLabelColor.usingColorSpace(.sRGB)?.cgColor | |
| ?? CGColor(gray: 0.6, alpha: 1) | |
| } |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Sources/Workspace.swift (1)
4445-4491:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid full-map snapshots on each lifecycle event.
Line 4445, Line 4454, Line 4470, and Line 4479 copy
agentLifecycleStatesByPanelId, then Line 4488 does a full equality check. That adds repeated O(N) copy/compare work in a hot sidebar-update path.Use targeted change detection (
oldValue/didMutate) and publish only when a mutation actually occurred.⚙️ Suggested refactor
- let previousStates = agentLifecycleStatesByPanelId - agentLifecycleStatesByPanelId[targetPanelId, default: [:]][key] = lifecycle - publishAgentLifecycleStatesIfNeeded(previousStates) + let previous = agentLifecycleStatesByPanelId[targetPanelId]?[key] + agentLifecycleStatesByPanelId[targetPanelId, default: [:]][key] = lifecycle + if previous != lifecycle { + agentLifecycleStatesPublisher.send(agentLifecycleStatesByPanelId) + } recordAgentLifecycleChange(panelId: targetPanelId) @@ - let previousStates = agentLifecycleStatesByPanelId + var didMutate = false let panelIds = panelId.map { [$0] } ?? Array(agentLifecycleStatesByPanelId.keys) for panelId in panelIds { guard agentLifecycleStatesByPanelId[panelId]?[key] != nil else { continue } agentLifecycleStatesByPanelId[panelId]?.removeValue(forKey: key) if agentLifecycleStatesByPanelId[panelId]?.isEmpty == true { agentLifecycleStatesByPanelId.removeValue(forKey: panelId) } + didMutate = true didClear = true recordAgentLifecycleChange(panelId: panelId) } - publishAgentLifecycleStatesIfNeeded(previousStates) + if didMutate { + agentLifecycleStatesPublisher.send(agentLifecycleStatesByPanelId) + } return didClear @@ - let previousStates = agentLifecycleStatesByPanelId - guard agentLifecycleStatesByPanelId.removeValue(forKey: panelId) != nil else { return } - publishAgentLifecycleStatesIfNeeded(previousStates) + guard agentLifecycleStatesByPanelId.removeValue(forKey: panelId) != nil else { return } + agentLifecycleStatesPublisher.send(agentLifecycleStatesByPanelId) recordAgentLifecycleChange(panelId: panelId) @@ - let previousStates = agentLifecycleStatesByPanelId agentLifecycleStatesByPanelId.removeAll() - publishAgentLifecycleStatesIfNeeded(previousStates) + agentLifecycleStatesPublisher.send(agentLifecycleStatesByPanelId) for panelId in panelIds { recordAgentLifecycleChange(panelId: panelId) } @@ - private func publishAgentLifecycleStatesIfNeeded(_ previousStates: [UUID: [String: AgentHibernationLifecycleState]]) { - guard previousStates != agentLifecycleStatesByPanelId else { return } - agentLifecycleStatesPublisher.send(agentLifecycleStatesByPanelId) - }As per coding guidelines, avoid repeated full scans/copies in hot update paths over scalable collections.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/Workspace.swift` around lines 4445 - 4491, The code is copying the entire agentLifecycleStatesByPanelId dictionary at the start of each method (setAgentLifecycle, clearAgentLifecycle, clearAgentLifecycleStates, clearAllAgentLifecycleStates) and then performing a full equality comparison in publishAgentLifecycleStatesIfNeeded, which is inefficient O(N) work in a hot update path. Instead of copying the full state and comparing dictionaries, refactor by tracking whether an actual mutation occurred in each method and passing a boolean flag to publishAgentLifecycleStatesIfNeeded to indicate whether to publish, eliminating the full dictionary copies and equality checks. Follow the existing pattern already used in clearAgentLifecycle with the didClear flag.Source: Coding guidelines
cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift (1)
97-124: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winExercise non-zero
activeCodingAgentCountin refresh-policy tests.The helper default at Line 124 keeps
activeCodingAgentCountat0in current call sites, so context-menu immediate/deferred behavior for this new field is effectively untested. Please add a helper parameter and at least one test case with differing counts (current != next) so this path is covered.Suggested test-helper adjustment
private static func snapshot( @@ - finderDirectoryPath: String? = nil + finderDirectoryPath: String? = nil, + activeCodingAgentCount: Int = 0 ) -> SidebarWorkspaceSnapshotBuilder.Snapshot { @@ - activeCodingAgentCount: 0, + activeCodingAgentCount: activeCodingAgentCount,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift` around lines 97 - 124, The snapshot helper method currently hardcodes activeCodingAgentCount to 0, leaving the context-menu behavior for this field untested. Add a parameter named activeCodingAgentCount with a default value of 0 to the snapshot helper method, then pass this parameter when initializing the Snapshot object. Additionally, create at least one new test case that calls the snapshot helper with different activeCodingAgentCount values (such as current != next) to exercise and verify the refresh-policy behavior for this field in the context-menu scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift`:
- Around line 474-484: The SettingsCardRow containing the showAgentActivity
toggle for "Show Active Agents in Sidebar" is missing a guard condition based on
hideAll.current, unlike the neighboring rows in the same detail block. Add a
disabled state modifier to the SettingsCardRow that disables it when
hideAll.current is true, ensuring the toggle cannot be interacted with when the
"Hide All Sidebar Details" setting is enabled, matching the pattern applied to
other related controls in this section.
In
`@Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift`:
- Line 99: The newly added setting path "sidebar.showAgentActivity" is placed in
the wrong position within the hand-maintained contract list in
SettingsRowAnchorResolutionTests.swift. This list maintains alphabetical
ordering for the sidebar.show* entries as documented in the comments. Move
"sidebar.showAgentActivity" from its current position after
"sidebar.showNotificationMessage" to its correct alphabetical location before
"sidebar.showBranchDirectory" to maintain the documented alphabetical ordering
discipline for this deliberately maintained contract list.
In `@Sources/SidebarAgentActivitySummary.swift`:
- Around line 7-9: The filter call on panelStates.values creates a temporary
collection for each panel in the reduce operation, causing unnecessary memory
allocations in a performance-sensitive sidebar refresh path. Replace the chained
filter and count operations with a direct count(where:) method that counts
matching elements in a single pass without creating an intermediate collection.
Specifically, change panelStates.values.filter { $0 == .running }.count to
panelStates.values.count(where: { $0 == .running }) to eliminate the temporary
array allocation.
---
Outside diff comments:
In `@cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift`:
- Around line 97-124: The snapshot helper method currently hardcodes
activeCodingAgentCount to 0, leaving the context-menu behavior for this field
untested. Add a parameter named activeCodingAgentCount with a default value of 0
to the snapshot helper method, then pass this parameter when initializing the
Snapshot object. Additionally, create at least one new test case that calls the
snapshot helper with different activeCodingAgentCount values (such as current !=
next) to exercise and verify the refresh-policy behavior for this field in the
context-menu scenarios.
In `@Sources/Workspace.swift`:
- Around line 4445-4491: The code is copying the entire
agentLifecycleStatesByPanelId dictionary at the start of each method
(setAgentLifecycle, clearAgentLifecycle, clearAgentLifecycleStates,
clearAllAgentLifecycleStates) and then performing a full equality comparison in
publishAgentLifecycleStatesIfNeeded, which is inefficient O(N) work in a hot
update path. Instead of copying the full state and comparing dictionaries,
refactor by tracking whether an actual mutation occurred in each method and
passing a boolean flag to publishAgentLifecycleStatesIfNeeded to indicate
whether to publish, eliminating the full dictionary copies and equality checks.
Follow the existing pattern already used in clearAgentLifecycle with the
didClear flag.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 422ff73b-c6b9-43c8-8282-dd62409ab548
📒 Files selected for processing (17)
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swiftPackages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swiftPackages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swiftPackages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swiftResources/Localizable.xcstringsSources/ContentView.swiftSources/SettingsNavigation.swiftSources/SettingsSearchAliases.swiftSources/Sidebar/SidebarAgentActivityIndicator.swiftSources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swiftSources/SidebarAgentActivitySummary.swiftSources/Workspace.swiftSources/WorkspaceSidebarObservation.swiftcmux.xcodeproj/project.pbxprojcmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swiftcmuxTests/WorkspaceSidebarObservationTests.swiftdocs/configuration.md
| SettingsCardRow( | ||
| configurationReview: .json("sidebar.showAgentActivity"), | ||
| String(localized: "settings.app.showAgentActivity", defaultValue: "Show Active Agents in Sidebar"), | ||
| subtitle: String(localized: "settings.app.showAgentActivity.subtitle", defaultValue: "Display a spinner and count on workspaces with running coding agents.") | ||
| ) { | ||
| Toggle("", isOn: Binding(get: { showAgentActivity.current }, set: { showAgentActivity.set($0) })) | ||
| .labelsHidden() | ||
| .controlSize(.small) | ||
| } | ||
| SettingsCardDivider() | ||
|
|
There was a problem hiding this comment.
Disable this row when “Hide All Sidebar Details” is enabled.
This toggle sits in the same detail block as neighboring rows but is the only one not gated by hideAll.current, so users can change a control that has no visible effect while hide-all is active.
Suggested fix
SettingsCardRow(
configurationReview: .json("sidebar.showAgentActivity"),
String(localized: "settings.app.showAgentActivity", defaultValue: "Show Active Agents in Sidebar"),
subtitle: String(localized: "settings.app.showAgentActivity.subtitle", defaultValue: "Display a spinner and count on workspaces with running coding agents.")
) {
Toggle("", isOn: Binding(get: { showAgentActivity.current }, set: { showAgentActivity.set($0) }))
.labelsHidden()
.controlSize(.small)
}
+ .disabled(hideAll.current)
SettingsCardDivider()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| SettingsCardRow( | |
| configurationReview: .json("sidebar.showAgentActivity"), | |
| String(localized: "settings.app.showAgentActivity", defaultValue: "Show Active Agents in Sidebar"), | |
| subtitle: String(localized: "settings.app.showAgentActivity.subtitle", defaultValue: "Display a spinner and count on workspaces with running coding agents.") | |
| ) { | |
| Toggle("", isOn: Binding(get: { showAgentActivity.current }, set: { showAgentActivity.set($0) })) | |
| .labelsHidden() | |
| .controlSize(.small) | |
| } | |
| SettingsCardDivider() | |
| SettingsCardRow( | |
| configurationReview: .json("sidebar.showAgentActivity"), | |
| String(localized: "settings.app.showAgentActivity", defaultValue: "Show Active Agents in Sidebar"), | |
| subtitle: String(localized: "settings.app.showAgentActivity.subtitle", defaultValue: "Display a spinner and count on workspaces with running coding agents.") | |
| ) { | |
| Toggle("", isOn: Binding(get: { showAgentActivity.current }, set: { showAgentActivity.set($0) })) | |
| .labelsHidden() | |
| .controlSize(.small) | |
| } | |
| .disabled(hideAll.current) | |
| SettingsCardDivider() |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift`
around lines 474 - 484, The SettingsCardRow containing the showAgentActivity
toggle for "Show Active Agents in Sidebar" is missing a guard condition based on
hideAll.current, unlike the neighboring rows in the same detail block. Add a
disabled state modifier to the SettingsCardRow that disables it when
hideAll.current is true, ensuring the toggle cannot be interacted with when the
"Hide All Sidebar Details" setting is enabled, matching the pattern applied to
other related controls in this section.
| "sidebar.showCustomMetadata", | ||
| "sidebar.showLog", | ||
| "sidebar.showNotificationMessage", | ||
| "sidebar.showAgentActivity", |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Insert sidebar.showAgentActivity in alphabetical order.
The new setting path is appended after "sidebar.showNotificationMessage" (line 98), but the comment at line 31–37 states this is a "deliberately hand-maintained contract list." The sidebar.show* entries are alphabetically ordered; showAgentActivity (A) should be inserted before showBranchDirectory (B) at line 95, not after showNotificationMessage (N). This keeps the list maintainable and consistent with the documented discipline.
📋 Proposed reordering
"sidebar.rightMaxWidth",
"sidebar.showBranchDirectory",
+ "sidebar.showAgentActivity",
"sidebar.showCustomMetadata",
"sidebar.showLog",
"sidebar.showNotificationMessage",
- "sidebar.showAgentActivity",
"sidebar.showPorts",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "sidebar.showAgentActivity", | |
| "sidebar.rightMaxWidth", | |
| "sidebar.showAgentActivity", | |
| "sidebar.showBranchDirectory", | |
| "sidebar.showCustomMetadata", | |
| "sidebar.showLog", | |
| "sidebar.showNotificationMessage", | |
| "sidebar.showPorts", | |
| "sidebar.showProgress", | |
| "sidebar.showPullRequests", | |
| "sidebar.showSSH", | |
| "sidebar.showWorkspaceDescription", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift`
at line 99, The newly added setting path "sidebar.showAgentActivity" is placed
in the wrong position within the hand-maintained contract list in
SettingsRowAnchorResolutionTests.swift. This list maintains alphabetical
ordering for the sidebar.show* entries as documented in the comments. Move
"sidebar.showAgentActivity" from its current position after
"sidebar.showNotificationMessage" to its correct alphabetical location before
"sidebar.showBranchDirectory" to maintain the documented alphabetical ordering
discipline for this deliberately maintained contract list.
…ndicator # Conflicts: # Resources/Localizable.xcstrings # Sources/ContentView.swift # Sources/WorkspaceSidebarObservation.swift
Restyle the sidebar agent-activity spinner to the native macOS indeterminate 'spokes' look while keeping it GPU-only: the only animated property is the layer transform.rotation.z, interpolated by the render server off the main thread, so there is zero per-frame CPU. Pause the animation when the view leaves the window, the window is occluded/minimized, or Reduce Motion is on. Add a DEBUG-only Spinner Gallery window (Debug > Debug Windows > Spinner Gallery, and the debug.spinner_gallery.show socket verb) comparing the GPU spokes/arc spinners against NSProgressIndicator (default/threaded) and SwiftUI ProgressView, each labeled with its energy characteristics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The main merge silently kept the branch-base bonsplit SHA (5728c21) instead of main's c4aa88a, which carries the Tab.isAudioPlaying API the merged Workspace.swift call sites depend on. Realign to main's pointer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tings strings Autoreview found the documented sidebar.showAgentActivity cmux.json key was never parsed: add it to SidebarSettingsFileMapping.booleanSettings, the supported JSON path catalog, the default config template, and the web schema (mirroring showProgress) so the documented disable example actually works. Add the missing en/ja localization entries for settings.app.showAgentActivity, its subtitle, and the search-alias key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gate the sidebar agent-activity badge on !settings.hidesAllDetails (matching the adjacent remote/SSH and conversation-subtitle rows) and disable the Settings toggle under hide-all like the other detail toggles, so the master hide-all setting wins over this default-on indicator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new DEBUG protocol requirement on ControlDebugContext needs a benign default in the package test stub extension so fake ControlCommandContext conformers keep compiling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sidebar observation state stored the full [panelId:[agentKey:lifecycle]] map, so it fed removeDuplicates(); idle/needsInput/unknown lifecycle churn that left the visible running count unchanged still invalidated every sidebar row and the custom-sidebar merged publisher. Reduce to activeCodingAgentCount before the dedup so only count changes refresh rows (sidebar hot-path rule, issue #2586 class). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ity key GPUSpinner (style + representable + backing view) is now shared by the sidebar badge and the debug gallery, so move it out of SidebarAgentActivityIndicator.swift into Sidebar/GPUSpinner.swift (one logical component per file). Add Swift-DocC to the new public showAgentActivity DefaultsKey. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Use a muted grey (secondary label, or dimmed selection foreground on selected rows) instead of the bright foreground, and thin the spokes (0.16 -> 0.10 of the diameter, slightly longer). Render the debug gallery in grey too so it matches the shipping appearance.
…er gap, softer fade)
…icator General-purpose CLI control of the sidebar spinner + count, decoupled from agents: it sets a reserved 'manual' (or 'manual:<id>') lifecycle key so several named loaders can stack into the count, and 'off' clears one. The manual namespace is excluded from structuredAgentHookStatusKeys, so it only drives the spinner and never affects agent hibernation/PID/status handling. Same path the agent hooks use (set_agent_lifecycle), now exposed as a first-class command with --help. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…right, bigger - Rehome the CLI command to the canonical workspace noun: cmux workspace loading <on|off> [--id <name>] (no new deprecated top-level verb). - Sidebar indicator: drop the count badge, show just the spinner, move it to the row's trailing edge, and size it larger (~14pt) so it reads as a loading indicator rather than a notification count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The spinner now occupies the trailing close-button corner (flush right) and swaps with the close x on hover, instead of sitting inset to the left of the reserved close slot.
The spinner is a live status signal, not a verbose detail, so it should survive the master hide-all toggle. Drop the !hidesAllDetails gate on the sidebar spinner and stop disabling the Settings toggle under hide-all; update the subtitle (spinner-only, no count; stays visible with details hidden).
Anchor each repeating animation's beginTime to a multiple of its duration in the layer time space, so every spinner of the same duration shows the same rotation phase regardless of when it was created/shown/resumed. The sidebar spinners now spin in unison.
…e/push animations - New sidebar.loadingSpinnerPosition setting (leading/trailing, default trailing) wired through catalog, Settings picker (Left/Right), cmux.json, web schema, search/navigation, localization. Renamed the show/hide toggle to 'Show Loading Spinner'. - Row: the leading status slot now holds the unread badge AND the left spinner, cross-fading into each other in one badge-sized slot; the slot fades in and pushes the title right (value-scoped .animation). Right position keeps the flush trailing-corner spinner. Spinner fades in/out in both positions. - SidebarAgentActivityIndicator takes an explicit size.
… to left Per the documented #5764/#5845 rule, implicit .animation/.transition on a sidebar row interpolates the LazyVStack's measured height every frame and pegs sidebar-wide layout when many rows are active. The fade/cross-fade/push animations did exactly that and made reordering animate, so remove them; the spinner now appears in one discrete layout pass. Also flip the default loadingSpinnerPosition to leading (left).
Animate only the title's horizontal shift when the leading slot (unread badge or left spinner) appears/disappears. Scoped to leadingSlotActive and height-neutral (slot <= close-button height), so it doesn't interpolate the LazyVStack height (#5764) and doesn't animate reordering.
…ndicator # Conflicts: # .github/swift-file-length-budget.tsv # cmux.xcodeproj/project.pbxproj
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d98b582. Configure here.

Summary
Verification
Dogfood
Do not merge until the tagged build is dogfooded. Expected behavior: workspaces with running coding agents show a spinner plus count in the sidebar; turning off Show Active Agents in Sidebar hides it.
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Touches control-socket sidebar mutations, agent lifecycle semantics (manual vs agent keys), and high-traffic sidebar row rendering; behavior is bounded by caps and tests but worth careful UI/socket regression checks.
Overview
Adds a loading spinner on sidebar workspace rows when coding agents are running or a manual loader is active, driven by a new GPU macOS-style spokes spinner (phase-locked, pauses under Reduce Motion / occlusion).
Automation:
cmux workspace loading <on|off> [--id <name>]and the v1workspace_loadingsocket command toggle workspace-scopedmanual/manual:<id>loaders (capped, validated ids);set_agent_lifecyclerejects the reservedmanual*namespace so those loaders only go through this path and do not affect hibernation.Settings:
sidebar.showAgentActivityplus leading/trailing placement for the spinner and unread badge, wired through Settings UI,cmux.json, search, and localization.Sidebar layout: Leading/trailing status slots cross-fade spinner vs badge and coordinate with the close button; snapshot refresh includes active-task count for immediate updates (e.g. with context menu open).
Refactor: Agent lifecycle helpers move to
Workspace+AgentLifecycle.swiftwith manual-key handling (persist across panel close, excluded from hibernation rollup). DEBUG-only Spinner Gallery for comparing GPU vs native spinners.Reviewed by Cursor Bugbot for commit 3036399. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds a native macOS‑style GPU spinner to sidebar workspace rows when coding agents are running or a manual loader is active. Spinner is phase‑locked, Reduce Motion aware, and stays visible even with Hide All Sidebar Details; placement is configurable and it shares space with the unread badge/close corner.
New Features
sidebar.showAgentActivity(default: true),sidebar.loadingSpinnerPosition,sidebar.notificationBadgePosition(leading/trailing, withleft/right/start/endaliases). Wired to Settings UI, search,cmux.json, and schema.cmux workspace loading <on|off> [--id <name>] [--workspace <id>] [--window <id>] [--json]viaworkspace_loading; workspace‑scopedmanual/manual:<id>loaders;onclears duplicates first; resolves workspace/window before the v1 bridge; returnsbefore/after; ids[A-Za-z0-9._-], ≤64 chars; ≤32 loaders/workspace.debug.spinner_gallery.show.Bug Fixes
offclears across panels; themanualnamespace is reserved and excluded from hibernation/PID handling andset_agent_lifecycle.set_agent_lifecyclerejectsmanual*keys;workspace_loadingenforces id charset/length and cap, rejects malformed flags and blank--tab, reports correctbefore/after.Written for commit 3036399. Summary will update on new commits.
Summary by CodeRabbit
cmux workspace loading <on|off> [--id <name>]to toggle per-workspace loading (including optional JSON output).