Skip to content

Fix sidebar status pills for newly created workspaces - #5662

Merged
austinywang merged 6 commits into
mainfrom
issue-5659-status-pill-new-workspace
Jun 10, 2026
Merged

austinywang merged 6 commits into
mainfrom
issue-5659-status-pill-new-workspace

Conversation

@austinywang

@austinywang austinywang commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Fix late sidebar row subscriptions by making Workspace.sidebarObservationPublisher emit one initial sync event per subscriber before normal debounced updates.
  • Add a regression test for a workspace whose status metadata exists before the sidebar observer subscribes.
  • Preserve the heartbeat-only no-invalidation behavior after the initial sync baseline.

Root cause

New workspace rows could cache an empty workspaceSnapshotStorage before their tab.sidebarObservationPublisher subscription became active. Because the workspace publisher dropped the first @Published value, a publisher materialized after the first status mutation treated the already-mutated status dictionary as its initial value and dropped it, leaving the row stuck on the empty cached snapshot. This rules out ForEach identity and status filtering: the model data was present and the snapshot builder renders it when refreshed.

Testing

  • Not run locally per repo policy.

Closes #5659


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Ensure sidebar status pills render for newly created workspaces by emitting the current workspace state to new subscribers. Addresses #5659 and prevents late-subscribing rows from caching empty snapshots.

  • Bug Fixes
    • Rebuilt sidebarImmediateObservationPublisher and sidebarObservationPublisher to emit current @Published values with removeDuplicates, so each subscriber gets an initial event; made WorkspaceRemoteConnectionState Equatable and moved helpers to WorkspaceSidebarObservation.swift.
    • Added regression tests for late subscribers on both publishers, kept heartbeat-only changes ignored, fixed test imports for cmux/cmux_DEV, and marked tests @MainActor for stable execution.

Written for commit 3c6fe02. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Sidebar reliably delivers current workspace state to subscribers that attach late and emits immediate updates (e.g., title/status).
    • Sidebar no longer invalidates or refreshes when only remote heartbeat details change.
  • Tests

    • Added deterministic unit tests covering immediate-emission behavior, late-subscriber scenarios, and heartbeat-only ignore cases.

@vercel

vercel Bot commented Jun 8, 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 9, 2026 8:04pm
cmux-staging Building Building Preview, Comment Jun 9, 2026 8:04pm

@coderabbitai

coderabbitai Bot commented Jun 8, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ebd8bead-163e-411a-88e2-95b3dc3f7206

📥 Commits

Reviewing files that changed from the base of the PR and between 112e59c and 3c6fe02.

📒 Files selected for processing (1)
  • cmuxTests/WorkspaceSidebarObservationTests.swift

📝 Walkthrough

Walkthrough

Adds Equatable sidebar snapshot types, moves sidebar publishers to snapshot-driven CombineLatest pipelines with removeDuplicates(), wires new source and test files into the Xcode project, and updates tests to reset counters and assert late-subscriber and heartbeat-only behaviors.

Changes

Sidebar observation and tests

Layer / File(s) Summary
Snapshot types and Equatable conformance
Sources/Workspace.swift, Sources/WorkspaceSidebarObservation.swift
Adds Equatable snapshot structs for immediate and full sidebar state and updates WorkspaceRemoteConnectionState to conform to Equatable.
Publisher pipeline rewrite
Sources/Workspace.swift, Sources/WorkspaceSidebarObservation.swift
Replaces per-field dropFirst()+MergeMany approach with makeSidebarImmediateObservationPublisher() and makeSidebarObservationPublisher() that use CombineLatest to build snapshot structs, apply removeDuplicates(), then map to Void.
Xcode project wiring
cmux.xcodeproj/project.pbxproj
Registers WorkspaceSidebarObservation.swift and WorkspaceSidebarObservationTests.swift in PBX build and file reference sections so they compile in app and test targets.
Unit test resets and late-subscriber coverage
cmuxTests/WorkspaceSidebarObservationTests.swift, cmuxTests/WorkspaceUnitTests.swift
Resets observation counters immediately after subscribing for deterministic baselines; adds tests that verify late subscribers receive at least one emission and that remote-heartbeat-only changes do not trigger sidebar invalidation.
sequenceDiagram
  participant Workspace
  participant SidebarPublisher as "makeSidebarObservationPublisher"
  participant ImmediatePublisher as "makeSidebarImmediateObservationPublisher"
  participant TestSubscriber as "Unit tests / UI"
  Workspace->>SidebarPublisher: combineLatest workspace fields, status, git, remote, listeningPorts
  Workspace->>ImmediatePublisher: combineLatest title/pinning/color + latest conversation fields
  SidebarPublisher->>TestSubscriber: emit Void when SidebarObservationState changes
  ImmediatePublisher->>TestSubscriber: emit Void when SidebarImmediateObservationState changes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4309: Intersects at Workspace conversation timestamp plumbing and related sidebar observation/validation changes.

Poem

I nibble on code at night,
I stitch snapshots soft and light,
A late subscriber hops to see,
One little emit — the view agrees. 🐇


Important

Pre-merge checks failed

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

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Cache Substitution Correctness ❓ Inconclusive Custom check references .github/review-bot-rules/cache-substitution-correctness.md which does not exist in the repository. Cannot verify against the referenced rule. Create the referenced rule file or clarify which existing rule applies to this check.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: addressing sidebar status pills not rendering for newly created workspaces, which matches the core issue resolved by the changeset.
Description check ✅ Passed The description includes all required sections: a detailed summary explaining what changed and why, testing notes, and a checklist with most items addressed.
Linked Issues check ✅ Passed All coding requirements from issue #5659 are met: publishers emit current values to late subscribers via Equatable state tracking and removeDuplicates, heartbeat-only no-invalidation is preserved, and regression tests cover late-subscriber scenarios.
Out of Scope Changes check ✅ Passed All changes directly support the fix: adding Equatable conformance to WorkspaceRemoteConnectionState, refactoring publishers to use new helper methods, adding comprehensive tests, and updating project configuration are all in scope.
Cmux Swift Actor Isolation ✅ Passed New private value structs are Sendable-composing; extension methods inherit @MainActor from Workspace class; no implicit MainActor models, shared mutable Sendable, or background access violations.
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing-based synchronization introduced. WorkspaceSidebarObservation.swift uses Combine publishers with removeDuplicates(). Tests use standard XCTest patterns only.
Cmux Expensive Synchronous Load ✅ Passed PR adds sidebar publishers combining @Published properties via Combine operators only; no expensive synchronous loads or I/O added to main actor or interactive paths.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift and Xcode project files. The "runtime-no-hacky-sleeps" rule explicitly covers TypeScript, JavaScript, shell, and non-Swift runtimes only; Swift is covered by a separate check.
Cmux Algorithmic Complexity ✅ Passed No violations. Panel sorting on tiny fixed-size collection (max 2-4 items) is necessary for Equatable semantics and was pre-existing. No unbounded scans or hot-path violations.
Cmux Swift Concurrency ✅ Passed PR refactors sidebar publishers from existing @Published fields and removes dispatch patterns. No new Combine app state, dispatch queues, or Tasks introduced.
Cmux Swift @Concurrent ✅ Passed New code follows @concurrent annotation rules: synchronous methods in @MainActor Workspace extension correctly lack @concurrent, no async without @concurrent, no heavy operations without hops.
Cmux Swift File And Package Boundaries ✅ Passed WorkspaceSidebarObservation.swift (136 lines) has single responsibility, no mixed concerns, respects 400-line threshold for new files, and is appropriately in app target.
Cmux Swift Logging ✅ Passed New production code (WorkspaceSidebarObservation.swift) contains no logging violations; test additions are allowed; pre-existing NSLog in Workspace.swift not part of this PR.
Cmux User-Facing Error Privacy ✅ Passed The PR adds no user-facing error messages, alerts, or command output that violates privacy policy. All changes are internal implementation (Combine publishers, Equatable conformance) or test code.
Cmux Full Internationalization ✅ Passed No user-facing text or localization requirements violated. Changes are internal API refactoring (Combine publishers) and tests with no new localized strings.
Cmux Swiftui State Layout ✅ Passed Refactors existing Workspace @Published properties into combinators; no new ObservableObject/@published, GeometryReader, store refs in lazy subtrees, or render-time mutation.
Cmux Architecture Rethink ✅ Passed Architectural fix avoids timing repairs/locks; uses CombineLatest+removeDuplicates from @Published values, maintains single state ownership in Workspace, names invariant, includes regression tests.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds Combine publishers and test cases for sidebar observation; no new NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code introduced.
Cmux Source Artifacts ✅ Passed All changed files are hand-written source code, tests, or required Xcode project configuration. No local tool output, generated logs, caches, DerivedData, temp folders, or other artifacts detected.
✨ 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 issue-5659-status-pill-new-workspace

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the MergeMany + dropFirst publisher pattern with CombineLatest over all sidebar-visible @Published properties, ensuring late subscribers receive an immediate initial emission reflecting current workspace state. All Equatable conformances (WorkspaceRemoteConnectionState, and the new private snapshot structs) are in place and all referenced types (SidebarStatusEntry, WorkspaceRemoteConfiguration, etc.) were already Equatable.

  • Publisher architecture: makeSidebarObservationPublisher() and makeSidebarImmediateObservationPublisher() now build SidebarObservationState / SidebarImmediateObservationState structs from current @Published values and apply removeDuplicates() before mapping to Void, replacing the previous dropFirst() approach that dropped the initial state.
  • Test corrections: Existing mutation tests reset their counters (publishCount = 0 / observedSidebarInvalidation = false) after subscribing to account for the new initial emission; new tests cover late-subscriber and heartbeat-ignore scenarios explicitly.

Confidence Score: 5/5

Safe to merge — the fix is structurally correct and replaces a timing-repair side-channel with canonical current-value observation.

The switch from MergeMany + dropFirst to CombineLatest + removeDuplicates directly addresses the root cause: CombineLatest emits the combined current state immediately on subscription, so a sidebar row that subscribes after workspace status has already been mutated still receives the correct initial snapshot. All Equatable conformances required by the new state structs were verified in place. Existing tests are correctly updated to reset their counters after subscription, and three new deterministic tests cover the late-subscriber, immediate-field, and heartbeat-ignore cases. No bad state is left representable and no timing repair is needed.

No files require special attention.

Important Files Changed

Filename Overview
Sources/WorkspaceSidebarObservation.swift New file housing the two publisher factory methods; uses nested CombineLatest4 chains to compose all sidebar-visible @published fields into Equatable state structs, ensuring each subscriber receives one immediate emission from current workspace state.
Sources/Workspace.swift Removes the old dropFirst-based sidebarObservationSignal helper and inline lazy publisher bodies; delegates to makeSidebar*ObservationPublisher(); adds Equatable conformance to WorkspaceRemoteConnectionState to satisfy SidebarObservationState synthesis.
cmuxTests/WorkspaceSidebarObservationTests.swift New test file with three deterministic @mainactor tests: late-subscriber emission for sidebarObservationPublisher and sidebarImmediateObservationPublisher, and heartbeat-only no-invalidation regression; all tests correctly reset counters after subscription to isolate post-subscribe mutations.
cmuxTests/WorkspaceUnitTests.swift Adds observedSidebarInvalidation = false and publishCount = 0 resets after subscription in existing mutation tests to account for the new immediate initial emission; removes duplicate heartbeat test now housed in WorkspaceSidebarObservationTests.
cmux.xcodeproj/project.pbxproj Registers WorkspaceSidebarObservation.swift (app target) and WorkspaceSidebarObservationTests.swift (test target) in both PBXBuildFile and PBXFileReference sections; no other structural changes.

Sequence Diagram

sequenceDiagram
    participant SidebarRow
    participant LazyPublisher as sidebarObservationPublisher (lazy var)
    participant CombineLatest as CombineLatest chain
    participant Published as @Published properties

    Note over SidebarRow,Published: Late-subscriber scenario (post-fix)

    Published->>Published: statusEntries mutated before subscribe
    SidebarRow->>LazyPublisher: ".sink { refreshWorkspaceSnapshot() }"
    LazyPublisher->>CombineLatest: create subscription
    CombineLatest->>Published: "subscribe to all @Published fields"
    Published-->>CombineLatest: emit current values immediately (CombineLatest semantics)
    CombineLatest->>CombineLatest: build SidebarObservationState(current values)
    CombineLatest->>CombineLatest: removeDuplicates() — state is new, passes through
    CombineLatest-->>SidebarRow: emit () — initial sync event
    SidebarRow->>SidebarRow: refreshWorkspaceSnapshot()

    Note over SidebarRow,Published: Subsequent mutation
    Published->>Published: statusEntries mutated again
    Published-->>CombineLatest: emit new value
    CombineLatest->>CombineLatest: build updated SidebarObservationState
    CombineLatest->>CombineLatest: removeDuplicates() — state changed, passes
    CombineLatest-->>SidebarRow: emit ()
    SidebarRow->>SidebarRow: refreshWorkspaceSnapshot()

    Note over SidebarRow,Published: Heartbeat-only change (no invalidation)
    Published->>Published: remoteHeartbeatCount / remoteLastHeartbeatAt mutated
    Note over CombineLatest: These fields are NOT in SidebarObservationState
    CombineLatest--xSidebarRow: no emission
Loading

Reviews (5): Last reviewed commit: "Mark sidebar observation tests main acto..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift Outdated
Comment on lines +10705 to +10710
return Publishers.MergeMany(publishers)
// Rows cache an immutable sidebar snapshot. Emit once per subscription
// so rows mounted after metadata already changed still rebuild from
// the workspace's current state instead of waiting for another change.
.prepend(())
.eraseToAnyPublisher()

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 Timing-repair side-channel leaves bad state representable

.prepend(()) ensures every subscriber gets one event but does not structurally prevent a stale snapshot: the row still relies on receiving an event at all in order to call refreshWorkspaceSnapshot(), so any future code path that skips subscription or lets the cancellable go out of scope early would silently render stale state. The structural fix would be to have the snapshot cache eagerly populate from workspace state when the lazy publisher is first accessed (or when the subscription begins), making the initial value the canonical representation rather than a side-channel emit. As written, the invariant "snapshot is current before display" is still enforced only by the existence of this prepend.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I replaced the .prepend(()) timing repair with current-state observation publishers for both sidebarObservationPublisher and sidebarImmediateObservationPublisher.

The publishers now compose the relevant @Published current values into Equatable sidebar-visible state, removeDuplicates() that state, and only then map to the existing Void invalidation signal. That makes the first subscriber emission canonical state from the workspace itself instead of a synthetic side-channel event, while preserving the row snapshot boundary and the existing refreshWorkspaceSnapshot() mutation path.

I also kept the late status-subscriber regression and added coverage for a late immediate-field subscriber, with the older mutation tests reset after subscription so they still prove subsequent mutations invalidate the sidebar.

This branch was successfully deployed

1 active deployment
Preview – cmux — 3c6fe022 Deployed Jun 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.

Status pills don't render on workspaces created after app launch

1 participant