Skip to content

Add sidebar active agent indicator - #6480

Merged
lawrencecchen merged 75 commits into
mainfrom
feat-agent-activity-indicator
Jul 8, 2026
Merged

lawrencecchen merged 75 commits into
mainfrom
feat-agent-activity-indicator

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • show a compact active-agent spinner and count on workspace rows with running coding agents
  • add sidebar.showAgentActivity with Settings > Sidebar UI, search anchors, docs, and localized strings
  • publish agent lifecycle changes into the sidebar snapshot refresh path

Verification

  • swift test --package-path Packages/macOS/CmuxSettingsUI
  • CMUX_ALLOW_LOCAL_XCODEBUILD=1 xcodebuild test -project cmux.xcodeproj -scheme cmux-unit -destination 'platform=macOS' -derivedDataPath /tmp/cmux-agact-test -only-testing:cmuxTests/WorkspaceSidebarObservationTests -only-testing:cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests
  • ./scripts/reload-cloud.sh --tag agact
  • tagged preflight on /tmp/cmux-debug-agact.sock: set_agent_lifecycle codex running, captured screenshot, and verified the sidebar badge crop changed between frames

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.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with 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 v1 workspace_loading socket command toggle workspace-scoped manual / manual:<id> loaders (capped, validated ids); set_agent_lifecycle rejects the reserved manual* namespace so those loaders only go through this path and do not affect hibernation.

Settings: sidebar.showAgentActivity plus 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.swift with 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

    • GPU spinner with macOS spokes; phase‑locked; pauses when off‑window/occluded or Reduce Motion is on.
    • Settings: sidebar.showAgentActivity (default: true), sidebar.loadingSpinnerPosition, sidebar.notificationBadgePosition (leading/trailing, with left/right/start/end aliases). Wired to Settings UI, search, cmux.json, and schema.
    • Row behavior: left/right slots; spinner and unread badge cross‑fade when sharing a side; right spinner shares the close‑button corner; title shift is height‑neutral.
    • CLI/socket: cmux workspace loading <on|off> [--id <name>] [--workspace <id>] [--window <id>] [--json] via workspace_loading; workspace‑scoped manual/manual:<id> loaders; on clears duplicates first; resolves workspace/window before the v1 bridge; returns before/after; ids [A-Za-z0-9._-], ≤64 chars; ≤32 loaders/workspace.
    • Debug: Spinner Gallery window (Debug > Debug Windows > Spinner Gallery…) comparing native vs GPU; registered as an auxiliary window; debug.spinner_gallery.show.
  • Bug Fixes

    • Spinner updates immediately while a row context menu is open; tooltip/accessibility label “Loading (N active tasks)” and localized.
    • Manual loaders persist across panel lifecycle and host close; off clears across panels; the manual namespace is reserved and excluded from hibernation/PID handling and set_agent_lifecycle.
    • Validation and errors: set_agent_lifecycle rejects manual* keys; workspace_loading enforces id charset/length and cap, rejects malformed flags and blank --tab, reports correct before/after.
    • Reduced sidebar invalidations by gating updates when the spinner is hidden and tracking only running‑count changes; tighter spinner/title spacing and inactive‑row alignment; tests include shortened temp home paths.

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

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added sidebar option “Show Active Agents in Sidebar” with an animated active-agent spinner/count on workspace rows.
    • Added cmux workspace loading <on|off> [--id <name>] to toggle per-workspace loading (including optional JSON output).
    • Added loading spinner and notification badge placement controls (leading/trailing/left/right/start/end).
  • Bug Fixes
    • Sidebar spinner/count updates immediately when active-agent/loading state changes.
  • Documentation
    • Updated configuration docs, localization, and settings metadata for the new controls.
  • Tests
    • Expanded UI/sidebar observation and anchor-resolution tests for the new settings.

@vercel

vercel Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 9, 2026 2:23am
cmux-staging Building Building Preview, Comment Jul 9, 2026 2:23am

@coderabbitai

coderabbitai Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a sidebar active-agent indicator with settings, snapshot, and rendering updates. It also adds a cmux workspace loading CLI subcommand and Spinner Gallery debug-window wiring.

Changes

Sidebar agent activity

Layer / File(s) Summary
Workspace state and loading control
Sources/Workspace.swift, Sources/SidebarAgentActivitySummary.swift, Sources/TerminalController+ControlSidebarContext.swift, cmuxTests/WorkspaceSidebarObservationTests.swift
Adds active-agent counting, updates lifecycle derivation for manual loader keys, and implements the sidebar loading toggle control path with observation tests.
Snapshot fields and sidebar row rendering
Sources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift, Sources/ContentView.swift, Sources/Sidebar/SidebarAgentActivityIndicator.swift, cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift
Threads active-agent counts and indicator placement through snapshot state and sidebar row rendering, including spinner/badge layout behavior.
Settings key, navigation, and localized strings
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift, Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift, Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift, Sources/CmuxSettingsJSONPathSupport.swift, Sources/KeyboardShortcutSettingsFileStore+Template.swift, Sources/KeyboardShortcutSettingsFileStore.swift, Sources/SettingsNavigation.swift, Sources/SettingsSearchAliases.swift, Resources/Localizable.xcstrings, Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift, docs/configuration.md, web/data/cmux.schema.json
Adds the sidebar setting, search/navigation entries, JSON path support, template and parser handling, schema entries, localization, tests, and configuration docs.

Spinner Gallery debug window

Layer / File(s) Summary
Spinner Gallery debug wiring
Sources/cmuxApp.swift, Sources/TerminalController+ControlDebugContext.swift, cmux.xcodeproj/project.pbxproj
Adds DEBUG-only menu wiring and project registration for the Spinner Gallery debug window.

CLI workspace loading subcommand

Layer / File(s) Summary
Workspace loading command
CLI/cmux.swift, Resources/Localizable.xcstrings
Adds the loading workspace subcommand, its handler, and the related help and error localization.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • manaflow-ai/cmux#7117: Both changes touch SidebarWorkspaceSnapshotRefreshPolicy and the context-menu immediate snapshot path that now carries activeCodingAgentCount.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error Adds new Combine app-state flow (coalesceLatest + merged sidebar publishers) in WorkspaceSidebarObservation.swift, which the modernization rule forbids. Move the sidebar observation/coalescing to Observation or async-backed state tracking; keep only SwiftUI/AppKit boundary callbacks where needed.
Cmux Full Internationalization ❌ Error Partial i18n: new xcstrings keys are only partly translated (tooltips en/ja; CLI usage en/ja/ko/uk), and web schema copy is English-only. Translate the new xcstrings keys for every existing catalog locale, and move the new web schema/docs copy through next-intl or another locale-specific source for all routing locales.
Cmux No Ambient Global State ❌ Error Sources/SidebarAgentActivitySummary.swift adds a new caseless enum namespace with only a static helper, which the rule forbids for ambient global surface. Replace it with a private/fileprivate free function, or move the count logic onto the owning snapshot/Workspace type.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (21 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New code stays in View/MainActor UI paths and pure value helpers; no new implicit MainActor models, sendable references, or background UI-store access was introduced.
Cmux Swift Blocking Runtime ✅ Passed PASS: The PR adds only animation-driven UI (Core Animation/SwiftUI spring) and state routing; no new semaphores, waits, sleeps, main-sync, polling, or locks in production code.
Cmux Browser Automation Off-Main ✅ Passed Browser wait/callback commands are in socketWorkerMethods and routed to v2BrowserAutomationCommandOnSocketWorker; policy tests verify worker routing, while direct focus/show cases stay main-actor.
Cmux Expensive Synchronous Load ✅ Passed The new sidebar spinner path uses in-memory agentLifecycleStatesByPanelId; no RestorableAgentSessionIndex.load(), transcript/JSONL parsing, or filesystem scans were added to main-actor UI or socket...
Cmux Cache Substitution Correctness ✅ Passed activeCodingAgentCount is UI-only, derived from event-driven workspace state, and the snapshot policy documents immediate-field freshness for the spinner.
Cmux No Hacky Sleeps ✅ Passed PASS: Scanned changed runtime files and found no sleep/timer/poll/yield APIs or delayed dispatch; diff is shaping/build logic only.
Cmux Algorithmic Complexity ✅ Passed New work is linear: activeCodingAgentCount scans each workspace’s panel lifecycle map once, and the sidebar row reads a precomputed snapshot field.
Cmux Swift @Concurrent ✅ Passed PASS: the touched additions are synchronous UI/settings wiring; no new @concurrent or nonisolated async work was introduced.
Cmux Swift File And Package Boundaries ✅ Passed PASS: the PR adds small UI/persistence glue and a tiny pure helper; no oversized file was materially expanded and nothing mixes unrelated responsibilities.
Cmux Swiftpm Lockfiles ✅ Passed PASS: No cmux-owned .gitignore ignores Package.resolved; the only ignore is vendor/bonsplit/.gitignore, and the xcodeproj change is paired with cmux.xcodeproj/project.xcworkspace/xcshareddata/swift...
Cmux Swift Logging ✅ Passed No prohibited runtime logging was added: Workspace NSLog calls are #if DEBUG, CLI prints are intended output, and the new Logger is nonisolated private let.
Cmux User-Facing Error Privacy ✅ Passed New CLI/help text is generic and only exposes public flags/config keys; no vendor names, raw upstream messages, secrets, or internal provider details found.
Cmux Swiftui State Layout ✅ Passed PASS — the new sidebar setting is passed as a Bool snapshot into the lazy row; the spinner is an AppKit bridge, with no new ObservableObject/@Published/GeometryReader/render-time mutation patterns...
Cmux Architecture Rethink ✅ Passed PASS: The new runtime model owns the agent maps, the view only observes it, and the diff adds no timing hacks or duplicate state owners.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed SpinnerGalleryDebugWindowController uses stable cmux.spinnerGallery and cmuxApp registers it in cmuxAuxiliaryWindowIdentifiers; the lint helper script exists.
Cmux Source Artifacts ✅ Passed The only outer diff is a ghostty submodule bump, and its inner diff changes only source/build files—no logs, screenshots, temp dirs, caches, or build outputs.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The new debug API lives in a dedicated #if DEBUG debug file and is wired to real Debug Windows/menu/socket entries, not a test-observation accessor.
Title check ✅ Passed The title clearly matches the main change: adding an active-agent sidebar indicator.
Description check ✅ Passed The description covers the summary and verification steps, and includes the expected dogfood note.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-agent-activity-indicator

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a GPU-driven Core Animation sidebar spinner that appears on workspace rows with running coding agents or active manual loaders (cmux workspace loading on/off), controlled by a new sidebar.showAgentActivity setting with configurable leading/trailing placement for both the spinner and the unread badge.

  • New GPUSpinnerNSView renders macOS-style spokes via a discrete CAKeyframeAnimation on the render server; pauses when occluded or Reduce Motion is on, and phase-locks all spinner instances to the shared Core Animation media clock.
  • New CLI command cmux workspace loading <on|off> [--id <name>] maps to a workspace_loading v1 control-socket command; manual keys are namespaced (manual/manual:<id>), capped at 32 per workspace, and explicitly rejected by set_agent_lifecycle to prevent sidebar-spinner state from leaking into hibernation/PID handling.
  • Settings, docs, schema, and localization updated for sidebar.showAgentActivity, sidebar.loadingSpinnerPosition, and sidebar.notificationBadgePosition.

Confidence Score: 5/5

Safe 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

Filename Overview
Sources/Sidebar/GPUSpinnerNSView.swift New GPU-driven Core Animation spinner; correctly pauses when occluded or Reduce Motion is on, but color.didSet lacks a guard against oldValue triggering unnecessary applyColor() on every SwiftUI re-render.
Sources/Sidebar/SidebarWorkspaceTrailingStatusSlot.swift New trailing-slot view sharing the close-button corner with the spinner and badge; the spinner lacks accessibilityHidden when opacity-0, leaving it visible to VoiceOver alongside the close button.
Sources/SidebarAgentActivitySummary.swift Caseless enum used as a static-function namespace, violating the no-ambient-global-state rule; logic belongs as a method on the owning type.
CLI/WorkspaceLoadingArguments.swift New CLI argument parser for cmux workspace loading; correctly validates charset, enforces --id format, and supports the full flag vocabulary with a clean usage string.
Sources/TerminalController+ControlSidebarContext.swift Correctly gates the manual namespace before the vault-agent fallback in controlSidebarIsAllowedAgentLifecycleKey; controlSidebarSetWorkspaceLoading deduplicates and caps manual loaders at 32 per workspace.
Sources/Workspace+AgentLifecycle.swift Agent lifecycle helpers extracted from Workspace.swift; correctly preserves manual keys across panel teardown by migrating them to the next available panel.
Sources/AgentHibernation/AgentHibernationLifecycleState.swift Adds manualKey and isManualKey helpers; isAllowed correctly excludes the manual namespace so set_agent_lifecycle cannot claim it.
Sources/ContentView.swift Wires spinner/badge placement into SidebarTabItemSettingsSnapshot and TabItemView; correctly computes leading/trailing slot state and delegates rendering to new slot views.
Sources/KeyboardShortcutSettingsFileStore.swift Several previously-private implementation-detail types (ResolvedSettingsSnapshot, ManagedSettingsValue, etc.) and helpers (logInvalid, jsonString) widened to internal access to support the new extension file in Sources/Settings/.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarWorkspaceLoadingV1.swift New workspace_loading v1 command; validates key format, rejects non-manual keys, and reports before=ON;after=OFF wire format.

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
Loading
%%{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
Loading

Reviews (26): Last reviewed commit: "Merge origin/main into agent activity in..." | Re-trigger Greptile

Comment on lines +111 to +115
private static func resolvedCGColor(_ color: NSColor) -> CGColor {
color.usingColorSpace(.deviceRGB)?.cgColor
?? NSColor.secondaryLabelColor.usingColorSpace(.deviceRGB)?.cgColor
?? CGColor(gray: 0.6, alpha: 1)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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!

@coderabbitai coderabbitai 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.

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 win

Avoid 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 win

Exercise non-zero activeCodingAgentCount in refresh-policy tests.

The helper default at Line 124 keeps activeCodingAgentCount at 0 in 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

📥 Commits

Reviewing files that changed from the base of the PR and between 22ab7f1 and 71ea935.

📒 Files selected for processing (17)
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift
  • Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift
  • Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/SettingsNavigation.swift
  • Sources/SettingsSearchAliases.swift
  • Sources/Sidebar/SidebarAgentActivityIndicator.swift
  • Sources/Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift
  • Sources/SidebarAgentActivitySummary.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceSidebarObservation.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift
  • cmuxTests/WorkspaceSidebarObservationTests.swift
  • docs/configuration.md

Comment on lines +474 to +484
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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 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.

Suggested change
"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.

Comment thread Sources/SidebarAgentActivitySummary.swift
lawrencecchen and others added 23 commits June 24, 2026 19:13
…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.
…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.
Comment thread Sources/Sidebar/SidebarWorkspaceStatusSlots.swift Outdated
Comment thread CLI/WorkspaceLoadingArguments.swift
…ndicator

# Conflicts:
#	.github/swift-file-length-budget.tsv
#	cmux.xcodeproj/project.pbxproj
Comment thread Sources/Sidebar/SidebarWorkspaceTrailingStatusSlot.swift

@cursor cursor 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.

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).

Fix All in Cursor

❌ 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.

Comment thread Sources/TerminalController+ControlSidebarContext.swift

This branch was successfully deployed

1 active deployment
Preview – cmux — 30363997 Deployed Jul 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant