Skip to content

Fix sidebar Running/Needs-input pills attaching to wrong workspace on cold load (#7519) - #8062

Closed
andresespinosapc wants to merge 2 commits into
manaflow-ai:mainfrom
andresespinosapc:fix/7519-cold-load-status-pills
Closed

andresespinosapc wants to merge 2 commits into
manaflow-ai:mainfrom
andresespinosapc:fix/7519-cold-load-status-pills

Conversation

@andresespinosapc

@andresespinosapc andresespinosapc commented Jul 14, 2026 •

Copy link
Copy Markdown

Summary

What changed? SidebarWorkspaceSnapshotBuilder.PresentationKey now includes the workspace identity (workspaceId: tab.id). This key guards TabItemView's per-row snapshot memo (workspaceSnapshotStorage / workspaceSnapshotScratch).

Why? Fixes #7519 — on cold app load the sidebar Running / Needs input status pills attach to the wrong workspaces (a neighbour's pill renders on the wrong tile; selecting the tile shows a different state than the pill claims).

Root cause: TabItemView memoizes its per-row sidebar snapshot — which carries the pill data (metadataEntries = statusEntries, and activeCodingAgentCount) — and serves the cached value whenever the current PresentationKey matches the stored one:

if let workspaceSnapshotStorage,
   workspaceSnapshotStorage.presentationKey == presentationKey {
    return workspaceSnapshotStorage
}

But PresentationKey was built from global presentation settings only (showsAgentActivity, visibleAuxiliaryDetails, …) with nothing identifying the workspace, so every row's key compared equal. The sidebar list is a LazyVStack (enforced by scripts/check-sidebar-lazy-layout.py). On cold load, TabManager.restoreSessionSnapshot replaces the entire tabs array with workspaces that all receive fresh UUIDs. When SwiftUI recycles a row's @State across that identity change, the memo returns the previously bound workspace's cached snapshot, and the presentation-key guard cannot detect that it belongs to a different workspace — so the neighbour's pills render on the wrong tile until an observation-driven refresh happens to correct it.

The data layer is not at fault: statusEntries / agentLifecycleStatesByPanelId are cleared on restore and re-seeded by UUID-resolved events (verified via a trace of the restore + hook-resolution paths). The mis-attribution is purely in the render-layer memo.

The fix makes the cache correct-by-construction: a snapshot built for workspace A can never satisfy workspace B's cache guard, so a recycled row falls through to makeWorkspaceSnapshot() and rebuilds from its own tab.

Two commits:

  1. the fix (Sources/ContentView.swift)
  2. the regression test (cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift)

Note on the repo's two-commit red/green convention: because the fix is adding a field to PresentationKey, a test-first commit cannot compile against the buggy API (the test must reference workspaceId), so a literal in-target runtime-red is not achievable for this defect class. The red/green is demonstrated instead by an isolated harness that models the exact cache guard with and without the field: beforeFix → serves A's pills for B (bug reproduced); afterFix → cache miss, rebuilds from B. Happy to restructure if a maintainer prefers a different arrangement.

Testing

  • Unit test (in-target, cmux-unit scheme): added presentationKeyDistinguishesWorkspaces and memoizedSnapshotIsNotServedForADifferentWorkspace to SidebarWorkspaceSnapshotRefreshPolicyTests (already wired into the pbxproj). Both pass compiled into the real cmux module:
    ✔ Test presentationKeyDistinguishesWorkspaces() passed
    ✔ Test memoizedSnapshotIsNotServedForADifferentWorkspace() passed
    ✔ Suite SidebarWorkspaceSnapshotRefreshPolicyTests passed  (10 tests, 0 failures)
    ** TEST SUCCEEDED **
    
    Run: xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -only-testing:cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests CMUX_SKIP_ZIG_BUILD=1 test
  • Isolated harness (red/green): models the getter's cache guard with/without workspaceId — reproduces the bug before the fix and confirms the cache miss after.
  • Build: tagged Debug app builds and signs cleanly with the change.

Manual repro (matches the report): launch the app, shrink the window so the sidebar scrolls (forces LazyVStack row recycling), create ~10–12 workspaces (a couple with nested cwds, e.g. ~/tmp/repro/elastic and ~/tmp/repro/elastic/kiriver), put several claude sessions into distinct states (Running / Needs input / idle), Cmd-Q, and cold-relaunch. Compare each tile's pill against the pane's real state.

Demo Video

  • Video URL or attachment: N/A — the visual manifestation depends on SwiftUI's LazyVStack @State recycling (timing-dependent). Deterministic coverage is provided by the unit test above; can add a screen recording of the manual repro on request.

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed (N/A — internal render fix, no user-facing string/API change)
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Related (not addressed here)

While tracing this I found two adjacent issues worth separate follow-up:

  • SessionIndexStore.swift:418 (filteredEntriesForCurrentScope) uses a cwd prefix match with no longest-prefix / nearest-workspace ownership, so a parent workspace (~/projects/elastic) absorbs a nested child's (~/projects/elastic/kiriver) sessions in the Sessions panel. This is the nested-cwd pattern noted in the issue, but on a different surface than the sidebar pills.
  • RestorableAgentSession.entry(workspaceId:panelId:) falls back to a panel-id-only key on cold load (workspace UUIDs are regenerated); safe while panel ids are unique, but fragile if a panel has moved workspaces.

Fixes #7519

🤖 Generated with Claude Code


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


Summary by cubic

Fixes sidebar "Running"/"Needs input" pills attaching to the wrong workspace on cold launch by scoping snapshot caching per workspace. We now include the workspace ID in the snapshot PresentationKey so memoized snapshots are never reused across workspaces. Fixes #7519.

  • Bug Fixes
    • Add workspaceId (tab.id) to SidebarWorkspaceSnapshotBuilder.PresentationKey and pass it from TabItemView.
    • Ensure recycled LazyVStack rows miss the cache and rebuild from their own tab.
    • Add regression tests in SidebarWorkspaceSnapshotRefreshPolicyTests to verify keys differ per workspace and prevent serving another workspace’s snapshot.

Written for commit 8ab8ea9. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Fixed workspace snapshot caching to avoid showing stale content when switching between workspaces.
    • Improved cache validation so snapshot reuse is correctly scoped to the active workspace, including safe behavior during row identity recycling.
  • Tests

    • Added regression coverage to ensure snapshot presentation keys differ across workspaces and remain stable within the same workspace, preventing cross-workspace cache hits.

…anaflow-ai#7519)

TabItemView memoizes its per-row sidebar snapshot (statusEntries pills and
activeCodingAgentCount) in workspaceSnapshotStorage / workspaceSnapshotScratch,
guarded only by SidebarWorkspaceSnapshotBuilder.PresentationKey. That key was
built from global presentation settings alone, with nothing identifying the
workspace, so every row's key compared equal.

The sidebar list is a LazyVStack (enforced by check-sidebar-lazy-layout.py). On
cold load TabManager.restoreSessionSnapshot replaces the whole tabs array with
workspaces that all get fresh UUIDs. When SwiftUI recycles a row's @State across
that identity change, the memo returns the previously bound workspace's cached
snapshot and the presentation-key guard cannot tell it belongs to a different
workspace, so a neighbour's "Running" / "Needs input" pills render on the wrong
tile.

Include workspaceId (tab.id) in PresentationKey so a snapshot built for one
workspace can never satisfy the cache guard of another; a recycled row then
falls through to makeWorkspaceSnapshot() and rebuilds from its own tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 14, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

@andresespinosapc is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The snapshot presentation key now includes the workspace UUID from the tab. Tests cover workspace-specific key equality, cached snapshot matching, and prevention of cross-workspace reuse after row recycling.

Changes

Workspace snapshot cache identity

Layer / File(s) Summary
Workspace-aware presentation key
Sources/ContentView.swift
PresentationKey stores workspaceId, and workspaceSnapshotPresentationKey populates it from tab.id.
Cache matching regression coverage
cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift
Tests construct workspace-specific keys, require matching presentation keys for cached snapshots, and verify recycled rows do not reuse another workspace’s snapshot.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: austinywang

🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 The PR only adds a workspace UUID to a pure cache-key value and test helpers; no new MainActor, Sendable, protocol, or background-store isolation issues were introduced.
Cmux Swift Blocking Runtime ✅ Passed Diff only adds workspaceId cache-key plumbing and deterministic test helpers; no semaphores, sleeps, sync waits, polling, or locks were introduced in production Swift.
Cmux Browser Automation Off-Main ✅ Passed Diff only adds sidebar snapshot tests; no browser.* routing, worker/main-actor browser automation, or policy coverage changes appear.
Cmux Expensive Synchronous Load ✅ Passed Diff only adds workspaceId to PresentationKey plus tests; no new RestorableAgentSessionIndex.load/Data(contentsOf:)/JSON decode appears in changed hunks.
Cmux Cache Substitution Correctness ✅ Passed PASS: workspaceSnapshot falls back to makeWorkspaceSnapshot() on cold/stale key misses, and PresentationKey now includes tab.id, so the snapshot cache is freshness-checked.
Cmux No Hacky Sleeps ✅ Passed Diff is test-only and adds no fixed waits, timers, polling, or asyncAfter-based synchronization.
Cmux Algorithmic Complexity ✅ Passed Only added a UUID field to PresentationKey and passed tab.id; no new scans, sorts, or batch rescans were introduced, and the tests are exempt.
Cmux Swift Concurrency ✅ Passed PASS: the PR only adds a workspaceId to PresentationKey and pure XCTest helpers; no new DispatchQueue, Combine app-state, completion-handler APIs, or fire-and-forget Tasks were introduced.
Cmux Swift @Concurrent ✅ Passed The PR only adds a workspaceId field and sync test helpers; no @concurrent, nonisolated async, or heavy async UI call sites were changed.
Cmux Swift File And Package Boundaries ✅ Passed PASS: Small cache-key fix in oversized app glue file (+11 lines) with tests; no new mixed responsibility or missing package boundary.
Cmux Swiftpm Lockfiles ✅ Passed Only cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift changed; no .gitignore, Package.swift, or Package.resolved files were modified, so the lockfile rule is satisfied.
Cmux Swift Logging ✅ Passed The diff adds only a workspaceId field and regression tests; no new print/debugPrint/dump/NSLog/Logger changes appear in the modified code paths.
Cmux User-Facing Error Privacy ✅ Passed PASS: Diff only adds workspaceId to an internal cache key and regression tests; no user-facing errors, alerts, command output, or recovery copy were changed.
Cmux Full Internationalization ✅ Passed The PR only changes an internal cache key in Swift plus tests; no user-facing text, xcstrings, Info.plist, or web locale assets were added or modified.
Cmux Swiftui State Layout ✅ Passed Diff only adds workspaceId to a PresentationKey and test helpers; no new ObservableObject/@published, GeometryReader, lazy-row store refs, or render-time state writes.
Cmux Architecture Rethink ✅ Passed Small local correctness fix: PresentationKey now includes workspaceId and tests verify per-workspace cache invalidation; no new timing, observer, or split-owner pattern introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes ContentView.swift and a test file; the diff adds no NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed PASS: The PR only changes a source file and a test file; no logs, screenshots, temp dirs, build outputs, or other artifact paths appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Sources/ContentView.swift only adds workspaceId to PresentationKey; no new test/debug accessor or seam was added in Sources, and the helper lives in cmuxTests via @testable import.
Cmux No Ambient Global State ✅ Passed The production diff only adds workspaceId to nested SidebarWorkspaceSnapshotBuilder.PresentationKey; no new file-scope func/var or singleton was introduced.
Title check ✅ Passed The title clearly summarizes the workspace-scoped sidebar pill cache fix on cold load.
Description check ✅ Passed The description covers summary, testing, demo video, review trigger, and checklist, matching the template well.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents sidebar status pills from appearing on the wrong workspace after row reuse. The main changes are:

  • Adds the workspace UUID to the snapshot presentation key.
  • Builds each row’s key from its current tab identity.
  • Adds tests for same-workspace cache hits and cross-workspace cache misses.
  • Uses a stable default workspace identity in refresh-policy tests.

Confidence Score: 5/5

This looks safe to merge.

  • The test helper now uses one stable identity for same-workspace comparisons.
  • Cross-workspace tests pass distinct identities explicitly.
  • The production key uses the current tab’s UUID.
  • No blocking issues were found in the updated code.

Important Files Changed

Filename Overview
Sources/ContentView.swift Adds the current tab UUID to the snapshot cache key so recycled rows cannot reuse another workspace’s snapshot.
cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift Adds workspace-specific cache tests and preserves same-workspace comparisons with a stable default UUID.

Reviews (4): Last reviewed commit: "Add regression test for #7519 cold-load ..." | Re-trigger Greptile

Comment thread cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 0b939e5f62

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@andresespinosapc

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@andresespinosapc I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,631 of the 240,000 allowed lines of code this month. Reviews resume on 1 August 2026 (in 18 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

@andresespinosapc: Sure, I'll review the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b939e5f62

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift Outdated
Assert PresentationKey distinguishes workspaces, and that a snapshot memoized
for one workspace fails the cache guard of another (the recycled-row state) so
its pills cannot be served on a neighbouring tile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Thank you for this! You had the cache-identity fix first; main now passes workspace-specific snapshots from a parent-owned, workspace-ID-keyed cache and removes the row-local memo in 0bffb5f :)

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 Running / Needs input status pills attached to wrong workspaces on cold app load

2 participants