Skip to content

Fix stale sidebar agent status refresh - #6804

Merged
austinywang merged 13 commits into
mainfrom
issue-6799-sidebar-agent-status-stale
Jun 29, 2026
Merged

austinywang merged 13 commits into
mainfrom
issue-6799-sidebar-agent-status-stale

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a regression for a structured agent status becoming visible after a PID ownership update
  • move agent PID ownership and lifecycle maps behind a focused @Observable runtime model with an AsyncStream change signal
  • have sidebar rows refresh their existing value snapshot from that runtime stream, so status visibility changes update independently per workspace without broadcasting Workspace.objectWillChange

Fixes #6799

Testing

  • python3 scripts/normalize-pbxproj.py cmux.xcodeproj/project.pbxproj && scripts/check-pbxproj.sh && git diff --check
  • Not run locally per issue instructions: the cmux app test target launches the app; relying on CI for app-host tests.

Demo Video

N/A. This is an intermittent state-observation race; the deterministic regression test is the useful proof, and the issue instructions say not to build or launch the app before CI is green.

Review Trigger

Please focus on the sidebar observation path for agent runtime visibility: hook/status metadata can already exist while PID ownership/lifecycle state changes later, and each workspace must refresh independently without broad workspace invalidation.

Checklist

  • Regression test committed before the fix commit.
  • PR body includes Fixes #6799.
  • No local app build or app-host test run.

Summary by CodeRabbit

  • New Features

    • Sidebar workspace status now updates more reliably when agent runtime activity changes.
    • Hidden sidebar entries can become visible automatically when related agent activity is recorded.
  • Bug Fixes

    • Improved refresh behavior so workspace rows stay in sync with agent process and lifecycle changes.
    • Reduced unnecessary UI change notifications while keeping displayed state current.

@vercel

vercel Bot commented Jun 26, 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 Jun 27, 2026 6:11am
cmux-staging Building Building Preview, Comment Jun 27, 2026 6:11am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@austinywang, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 minutes and 53 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e730d697-d99d-485b-a339-73a703767908

📥 Commits

Reviewing files that changed from the base of the PR and between 0efdb77 and 75f392c.

📒 Files selected for processing (7)
  • Sources/ContentView.swift
  • Sources/Workspace+PanelLifecycle.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceSidebarAgentRuntimeObservationModel.swift
  • Sources/WorkspaceSidebarObservation.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceSidebarObservationTests.swift
📝 Walkthrough

Walkthrough

Workspace agent runtime tracking now flows through a shared observation model, sidebar observation state includes the runtime fields, the tab row refreshes from runtime changes, and tests were migrated to Testing with a new agent-PID visibility case.

Changes

Sidebar agent runtime observation

Layer / File(s) Summary
Runtime model and wiring
Sources/WorkspaceSidebarAgentRuntimeObservationModel.swift, cmux.xcodeproj/project.pbxproj
A new main-actor observation model stores agent runtime maps, exposes change streams, and is added to the Xcode project.
Workspace forwards runtime maps
Sources/Workspace.swift
Workspace replaces stored agent runtime dictionaries with computed accessors that delegate reads and writes to the sidebar agent runtime observation model.
Sidebar observation and refresh
Sources/WorkspaceSidebarObservation.swift, Sources/ContentView.swift
Sidebar observation state gains agent runtime fields, the observation publisher combines those fields with listening ports, and the tab row refresh task reacts to runtime observation updates.
Sidebar observation tests
cmuxTests/WorkspaceSidebarObservationTests.swift
The sidebar observation tests move to the Testing framework, update assertions, and add a case that records an agent PID and checks visibility, change generation, and object-will-change behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#5662: Both PRs change Sources/WorkspaceSidebarObservation.swift’s sidebar observation publisher/state construction and update tests around publisher emissions.
  • manaflow-ai/cmux#5855: Both PRs touch the same sidebar agent-runtime coupling state and update WorkspaceSidebarObservationTests around agent PID ownership affecting sidebar visibility.
  • manaflow-ai/cmux#6460: Both PRs revolve around the agentPIDs and related runtime state used to drive sidebar updates.

Poem

I’m a rabbit with a hoppy grin,
I watched the agent lights turn in.
PIDs twinkle, panels wake,
Sidebar dots no longer fake.
🐇✨

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: fixing stale sidebar agent status refresh behavior.
Description check ✅ Passed The PR description follows the template well with Summary, Testing, Demo Video, Review Trigger, and Checklist sections.
Linked Issues check ✅ Passed The changes address #6799 by making sidebar rows observe agent runtime updates and adding a regression test for the stale status case.
Out of Scope Changes check ✅ Passed The added model, sidebar refresh wiring, test updates, and project file changes all support the stated stale status fix.
Cmux Swift Actor Isolation ✅ Passed PASS: the new runtime model and Workspace are @MainActor, and all added accesses occur on the main actor or via the main-actor mutation bus.
Cmux Swift Blocking Runtime ✅ Passed PASS: The new runtime model is @MainActor+AsyncStream; the changed files add no semaphores, sleeps, asyncAfter, sync, or locks, and the only Task hop is onTermination cleanup.
Cmux Browser Automation Off-Main ✅ Passed PR only updates submodule pointers; ghostty hunks add screen-row selection/clipboard APIs, not browser.* routing, waits, or mainActor/processV2Command changes.
Cmux Expensive Synchronous Load ✅ Passed No new synchronous agent-history load was added; the new task only listens to an AsyncStream and refreshes a cached snapshot, while load() stays in the existing cache path.
Cmux Cache Substitution Correctness ✅ Passed PASS: the new runtime model is the authoritative store for transient sidebar UI state, and its AsyncStream yields immediately plus on updates, so no stale persistence/snapshot cache substitution ap...
Cmux No Hacky Sleeps ✅ Passed Changed files are Swift/pbxproj only; the new row refresh uses AsyncStream, with no introduced sleeps, timers, polling, or asyncAfter in the diff.
Cmux Algorithmic Complexity ✅ Passed No new nested scans or hot-path re-sorts; the new runtime model and per-workspace AsyncStream are linear and bounded per workspace.
Cmux Swift Concurrency ✅ Passed Uses @Observable + AsyncStream; added Tasks are SwiftUI/onTermination boundaries, and no new DispatchQueue/Combine app-state or completion-handler APIs appear.
Cmux Swift @Concurrent ✅ Passed The new async row observer is explicitly @MainActor UI-bound, and the added model is actor-isolated; no new nonisolated async/@concurrent mismatch appears.
Cmux Swift File And Package Boundaries ✅ Passed The PR makes a focused sidebar-runtime extraction: a 60-line model plus tiny glue/tests; large existing app files are only incidentally touched, not mixed with new unrelated responsibilities.
Cmux Swiftpm Lockfiles ✅ Passed Diff only bumps vendored submodules (ghostty, vendor/bonsplit); no cmux-owned .gitignore, Package.resolved, or cmux.xcodeproj package-reference changes appear.
Cmux Swift Logging ✅ Passed The changed sidebar/runtime code adds observation plumbing and tests only; no new print/debugPrint/dump/NSLog/Logger calls appear in the diff.
Cmux User-Facing Error Privacy ✅ Passed No production-facing error/alert/copy strings were added; the diff only changes sidebar observation/state plumbing and tests.
Cmux Full Internationalization ✅ Passed PASS: The diff only adds runtime-observation plumbing and a DEBUG-only log; no production Swift UI text, string catalogs, or web locale files were changed.
Cmux Swiftui State Layout ✅ Passed The PR adds a new @MainActor @Observable model; no new ObservableObject/@published state or layout-changing GeometryReader was introduced in the touched SwiftUI paths.
Cmux Architecture Rethink ✅ Passed The agent-runtime maps now have a clear focused owner, and sidebar rows refresh from a direct per-workspace stream without sleeps, polling, or duplicate lifecycle owners.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No standalone NSWindow/NSPanel/WindowGroup changes or cmuxAuxiliaryWindowIdentifiers edits; the diff only adds sidebar observation/task plumbing.
Cmux Source Artifacts ✅ Passed Changed paths are tracked submodules (ghostty, vendor/bonsplit); diff only bumps gitlinks to source commits, with no logs/build output/temp artifacts added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test-only/debug seam was added in production Sources; the new runtime model has a real production caller and the added DEBUG code is logging only.
Cmux No Ambient Global State ✅ Passed No new file-scope funcs/vars or singleton-style namespaces were added; the new agent runtime state lives on a Workspace-owned WorkspaceSidebarAgentRuntimeObservationModel instance.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6799-sidebar-agent-status-stale

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 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a stale sidebar agent status visibility bug by moving agent runtime maps (agentPIDs, agentPIDPanelIdsByKey, agentPIDKeysByPanelId, agentLifecycleStatesByPanelId) out of Workspace and into a new @MainActor @Observable model whose properties are all @ObservationIgnored, then fanning out change signals via per-subscriber AsyncStreams so each sidebar row can refresh independently without broadcasting Workspace.objectWillChange.

  • Introduces WorkspaceSidebarAgentRuntimeObservationModel with explicit setters and an AsyncStream-based notification channel (changes()) that uses bufferingNewest(1) to coalesce rapid-fire mutations before subscribers can drain them.
  • Adds a .sidebarAgentRuntimeObservation(id:model:onChange:) SwiftUI view modifier in TabItemView that subscribes each sidebar row to the new stream independently.
  • Migrates the existing XCTest-based sidebar tests to Swift Testing (@Test, #expect) and adds two new regression tests: one verifying that a recordAgentPID call raises changeGeneration and makes a hidden structured status visible, and one verifying terminalAgentContext does not observe the agent runtime maps.

Confidence Score: 5/5

Safe to merge. The change is well-scoped: it encapsulates agent runtime maps in a dedicated @mainactor model, uses @ObservationIgnored to prevent broad view invalidation, and fans change signals to per-workspace AsyncStreams. The @mainactor isolation guarantees consumers always see fully-settled map state even when a single recordAgentPID call triggers multiple notifyChanged() yields.

The architecture directly addresses the root cause rather than patching symptoms. Computed property delegation in Workspace+PanelLifecycle means all existing call sites — recordAgentPID, clearAgentPID, removeAgentPIDOwnership, recordAgentPIDOwnership — route through the new equality-guarded setters without any call-site changes. The regression test is deterministic and covers both the visibility fix and the isolation from Workspace.objectWillChange. No new global state, no timing-based synchronization, and no broad invalidation path is introduced.

No files require special attention.

Important Files Changed

Filename Overview
Sources/WorkspaceSidebarAgentRuntimeObservationModel.swift New model class that encapsulates all four agent runtime maps, exposes them with equality-guarded setters, and fans change signals out to per-subscriber AsyncStreams. All properties are @ObservationIgnored so reads don't register in any @observable tracking context; @mainactor isolation ensures continuation yields and consumer task scheduling never interleave with a partially-updated map state.
Sources/Workspace+PanelLifecycle.swift Adds four computed properties that delegate reads and writes to the new model, replacing the former plain vars on Workspace. All existing call sites in recordAgentPID, clearAgentPID, removeAgentPIDOwnership, and recordAgentPIDOwnership now route through the equality-guarded setters transparently.
Sources/WorkspaceSidebarObservation.swift Adds the .sidebarAgentRuntimeObservation view extension that wires a .task(id:) loop over model.changes() to refreshWorkspaceSnapshot(); uses bufferingNewest(1) coalescing inherited from the model. Adds SwiftUI import; no changes to the Combine-based makeSidebarObservationPublisher path.
Sources/ContentView.swift Threads the new .sidebarAgentRuntimeObservation modifier into TabItemView's modifier chain, keyed on tab.id so the AsyncStream subscription is restarted only when the workspace tab identity changes.
Sources/Workspace.swift Replaces four plain var agent runtime maps with a single let sidebarAgentRuntimeObservation model constant. Because it's let, Workspace.objectWillChange is never triggered by agent runtime mutations — which is exactly the isolation the fix requires.
cmuxTests/WorkspaceSidebarObservationTests.swift Migrated from XCTestCase to Swift Testing; adds agentRuntimeObservationChangesWhenAgentPIDMakesExistingStatusVisible (regression for the bug) and terminalAgentContextDoesNotObserveAgentRuntimeMaps (proves @ObservationIgnored isolation). ObservationChangeFlag uses @unchecked Sendable to satisfy the Sendable closure requirement for withObservationTracking; safe because all accesses are @mainactor.
cmux.xcodeproj/project.pbxproj Adds WorkspaceSidebarAgentRuntimeObservationModel.swift to both the file references and the app target build phase. Entry IDs use the issue number as a mnemonic prefix (6799A001…), consistent with the existing project convention.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Hook as Agent Hook
    participant WS as Workspace
    participant Model as WorkspaceSidebarAgentRuntimeObservationModel
    participant Stream as AsyncStream<Void>
    participant Row as TabItemView (Sidebar Row)

    Hook->>WS: recordAgentPID(key:pid:panelId:)
    WS->>Model: setAgentPIDs(newValue)
    Model->>Model: "guard != newValue"
    Model->>Model: "agentPIDs = newValue, changeGeneration &+= 1"
    Model->>Stream: continuation.yield(()) [bufferingNewest(1)]
    Note over WS,Model: Additional map mutations (ownership, lifecycle) also call notifyChanged() — all synchronous on @MainActor
    Note over Stream: Task won't resume until current @MainActor turn completes — consumer always sees fully-settled map state
    Stream-->>Row: for await _ in model.changes()
    Row->>Row: refreshWorkspaceSnapshot()
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 Hook as Agent Hook
    participant WS as Workspace
    participant Model as WorkspaceSidebarAgentRuntimeObservationModel
    participant Stream as AsyncStream<Void>
    participant Row as TabItemView (Sidebar Row)

    Hook->>WS: recordAgentPID(key:pid:panelId:)
    WS->>Model: setAgentPIDs(newValue)
    Model->>Model: "guard != newValue"
    Model->>Model: "agentPIDs = newValue, changeGeneration &+= 1"
    Model->>Stream: continuation.yield(()) [bufferingNewest(1)]
    Note over WS,Model: Additional map mutations (ownership, lifecycle) also call notifyChanged() — all synchronous on @MainActor
    Note over Stream: Task won't resume until current @MainActor turn completes — consumer always sees fully-settled map state
    Stream-->>Row: for await _ in model.changes()
    Row->>Row: refreshWorkspaceSnapshot()
Loading

Reviews (10): Last reviewed commit: "Avoid observing sidebar runtime maps fro..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift Outdated
@blacksmith-sh

This comment has been minimized.

@blacksmith-sh

This comment has been minimized.

@austinywang
austinywang merged commit b196623 into main Jun 29, 2026
30 checks passed
austinywang added a commit that referenced this pull request Jun 30, 2026
…ation

Agent lifecycle transitions drove sidebar state coloring by calling
objectWillChange.send() on the whole Workspace from recordAgentLifecycleChange.
Workspace is a large ObservableObject on a hot agent-monitoring path, so that
broad fan-out invalidates every unrelated observer per transition — the sidebar
CPU-spin class called out in CLAUDE.md, and the exact invariant the runtime
observation model (PR #6804) enforces for agent PID churn.

Route the lifecycle color refresh through a dedicated PassthroughSubject that
only the sidebar observation pipeline subscribes to. The lifecycle state itself
already lives on WorkspaceSidebarAgentRuntimeObservationModel (its setter bumps
changeGeneration / .changes()), so per-row coloring still refreshes via the
existing narrow stream, and group-header coloring still refreshes via the
sidebar observation publisher.

Adds a regression test asserting an agent lifecycle change notifies the narrow
stream and refreshes the sidebar publisher without firing Workspace.objectWillChange.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 75f392c6 Deployed Jun 27, 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.

Sidebar agent status: starting a new agent while another is idle sometimes leaves a workspace stuck on 'Idle' (status doesn't flip to Running)

1 participant