Skip to content

Fix restored panel unread sidebar badges - #4550

Merged
lawrencecchen merged 4 commits into
mainfrom
issue-session-restore-sidebar-notifications
May 23, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
issue-session-restore-sidebar-notifications

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

  • Persist whether a restored panel unread indicator should contribute to the workspace sidebar badge.
  • Sync that restored panel unread contribution through the existing panel-derived workspace unread store.
  • Keep visual-only restored notification indicators out of workspace unread counts.

Tests:

  • Red before fix: xcodebuildmcp macos test --scheme cmux-unit --project-path cmux.xcodeproj --derived-data-path /tmp/cmux-sessionnotif-red --json '{"extraArgs":["-only-testing:cmuxTests/WorkspaceManualUnreadTests/testRestoredPanelUnreadIndicatorMarksWorkspaceUnreadForSidebar"]}' failed with restored unread count 0 instead of 1.
  • Green after fix: same focused test passed.
  • xcodebuildmcp macos test --scheme cmux-unit --project-path cmux.xcodeproj --derived-data-path /tmp/cmux-sessionnotif-red --json '{"extraArgs":["-only-testing:cmuxTests/SessionPersistenceTests/testSessionSnapshotRestoresUnreadNotifications"]}' passed.

Dogfood:

  • ./scripts/reload.sh --tag srnotif succeeded.

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


Note

Medium Risk
Adjusts session snapshot/restore and workspace unread projection logic, which can affect unread badge counts across restores and autosave fingerprinting. Risk is moderate due to state-migration/legacy handling and multiple code paths (restore, detach/attach, notification store sync).

Overview
Fixes incorrect workspace sidebar unread badges after session restore by persisting whether a restored panel unread indicator should contribute to workspace unread.

SessionPanelSnapshot now carries restoredUnreadContributesToWorkspace, and Workspace replaces the boolean restored-unread set with a typed RestoredPanelUnreadIndicator map that distinguishes visual-only vs workspace-unread indicators; this is propagated through snapshot restore, detached surface transfer, pruning, and unread syncing.

TabManager’s autosave fingerprint now hashes this per-panel contribution, and new unit tests cover restored/legacy snapshots and visual-only restored indicators not affecting workspace unread counts.

Reviewed by Cursor Bugbot for commit 75d49c9. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes sidebar unread badges after session restore by persisting whether a restored panel’s indicator contributes to workspace unread, and treating read restored badges as visual‑only. Meets the session restore sidebar notifications requirement and keeps visual‑only indicators out.

  • Bug Fixes
    • Persist restoredUnreadContributesToWorkspace in SessionPanelSnapshot; nil defaults to contributing for legacy snapshots.
    • Replace restoredUnread with typed RestoredPanelUnreadIndicator; use it across restore/detach/attach/prune, panel‑derived workspace unread, and TabManager autosave fingerprints.
    • Keep read restored badges visual‑only on restore: if a panel shows a badge but has no unread notifications, snapshot and restore it as non‑contributing.
    • Add tests for contributing vs visual‑only behavior, legacy nil‑flag handling, and fingerprint differences.

Written for commit 75d49c9. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • Session snapshots now include a per-panel flag indicating whether a restored unread should contribute to workspace unread counts.
  • Bug Fixes

    • Restore/attach/detach flows preserve restored-unread contribution state so workspace unread indicators remain correct after restore.
    • Session autosave fingerprint now reflects restored-unread contribution semantics.
  • Tests

    • Added tests covering restored panel unread behavior and legacy snapshot handling.

Review Change Stack

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented May 22, 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 May 23, 2026 12:22am
cmux-staging Building Building Preview, Comment May 23, 2026 12:22am

@coderabbitai

coderabbitai Bot commented May 22, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a per-panel restored-unread indicator (visualOnly vs workspaceUnread) persisted in session snapshots and carried on detached transfers; Workspace replaces the restored-unread ID set with per-panel indicators, updates restore/clear/mark flows and pruning, updates autosave fingerprint hashing, and adds tests for restore/fingerprint behavior.

Changes

Restored Unread Contribution State

Layer / File(s) Summary
Snapshot and transfer contracts
Sources/SessionPersistence.swift, Sources/Workspace+DetachedSurfaceTransfer.swift, cmuxTests/WorkspaceUnitTests.swift
SessionPanelSnapshot gains restoredUnreadContributesToWorkspace: Bool?; DetachedSurfaceTransfer now carries restoredUnreadIndicator: RestoredPanelUnreadIndicator? and helpers preserve it; test helper constructs transfers with nil indicator.
Workspace model and API
Sources/Workspace.swift
Adds Workspace.RestoredPanelUnreadIndicator enum and replaces restoredUnreadPanelIds: Set<UUID> with restoredUnreadPanelIndicators: [UUID: RestoredPanelUnreadIndicator] plus a computed id set; adds restoredUnreadIndicatorContributesToWorkspace(panelId:) and extends restorePanelUnreadIndicator(_:contributesToWorkspaceUnread:).
Snapshot payload & restore mapping
Sources/Workspace.swift, Sources/SessionPersistence.swift
Snapshotting derives restoredUnreadContributesToWorkspace per panel (forced false when no unread notification snapshots) and includes it in SessionPanelSnapshot; restoration calls restorePanelUnreadIndicator using the stored contribution flag.
Derived unread and mutation paths
Sources/Workspace.swift
Notification helpers split into hasVisibleNotificationIndicator / hasUnreadNotification; derived workspace-unread uses manual unread OR any restored indicator that contributes; markPanelUnread, clearAllPanelUnreadIndicatorsForWorkspaceRead, and restore/clear flows update the indicators dictionary.
Detach/attach lifecycle
Sources/Workspace.swift, Sources/Workspace+DetachedSurfaceTransfer.swift
Detaching includes the panel's restoredUnreadIndicator in transfers; attach repopulates or removes entries; attach-failure cleans up the indicator entry.
Autosave fingerprint and tests
Sources/TabManager.swift, cmuxTests/WorkspaceManualUnreadTests.swift
sessionAutosaveFingerprint now hashes whether the panel's restored indicator contributes to workspace unread; tests added/updated to validate restore behavior (visual-only vs contributes) and fingerprint sensitivity.

Sequence Diagram(s)

sequenceDiagram
  participant Session
  participant Workspace
  participant Panel
  participant TerminalNotificationStore

  Session->>Workspace: buildSessionSnapshot()
  Workspace->>Session: SessionPanelSnapshot(restoredUnreadContributesToWorkspace)
  Session->>Workspace: restoreSessionSnapshot()
  Workspace->>Panel: restorePanelUnreadIndicator(panelId, contributesToWorkspaceUnread)
  Panel->>Workspace: set restoredUnreadPanelIndicators[panelId] = indicator
  Workspace->>Workspace: recompute derived panel-derived workspace unread
  Workspace->>TerminalNotificationStore: sync derived workspace/tab unread (unreadCount)
  Workspace->>Workspace: detachPanel(panelId) -> DetachedSurfaceTransfer(restoredUnreadIndicator)
  Workspace->>Workspace: attachDetachedPanel(transfer) -> restore or remove indicator
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4130: Related changes to session snapshot and unread-restore plumbing affecting SessionPersistence.swift, Workspace, and transfer behavior.
  • manaflow-ai/cmux#4371: Modifies TabManager.sessionAutosaveFingerprint logic tied to restored-unread state hashing.

Poem

🐰 I nudged a Bool into every saved hop,
So panels awake where unread lights pop.
Detach, transfer, attach—no little feat,
The workspace counts what restored flags meet.
Tests clap their paws, the burrow's tidy and neat.

🚥 Pre-merge checks | ✅ 16 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix restored panel unread sidebar badges' directly addresses the main change: fixing workspace sidebar unread badges by persisting restored panel unread indicator contribution state.
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 PR properly isolates new RestoredPanelUnreadIndicator enum as nonisolated Sendable value type; all MainActor mutations contained; no background context accesses to UI state.
Cmux Swift Blocking Runtime ✅ Passed No blocking primitives introduced; PR refactors restored unread state using simple dictionary operations, property access, and enum changes with no semaphores, sleep calls, locks, or timing patterns.
Cmux No Hacky Sleeps ✅ Passed PR only modifies Swift source code; the check scope is TypeScript, JavaScript, shell, and non-Swift build/runtime scripts (per .github/review-bot-rules/runtime-no-hacky-sleeps.md).
Cmux Swift Concurrency ✅ Passed PR refactors unread indicators with synchronous properties and simple getter/setter methods. Replaces existing @Published storage, adds no new DispatchQueue, completion handlers, or problematic Tasks.
Cmux Swift @Concurrent ✅ Passed All new/modified methods are synchronous and properly isolated on @MainActor; no @concurrent violations or heavy async operations detected.
Cmux Swift File And Package Boundaries ✅ Passed PR adds 75 lines to Workspace.swift, 1 to TabManager—below 250-line threshold. No new files or responsibility mixing. Incidental touch to existing oversized files allowed by rule.
Cmux Swift Logging ✅ Passed All NSLog calls in Workspace.swift are guarded by #if DEBUG. No new logging statements, unguarded print/debugPrint/dump, or Logger issues detected. Complies with swift-logging.md.
Cmux User-Facing Error Privacy ✅ Passed No user-facing error messages or sensitive information exposed. Changes are internal to session persistence; new properties are private with no localized error strings.
Cmux Full Internationalization ✅ Passed PR contains only internal refactoring of workspace unread indicator tracking; no user-facing strings, string catalogs, or web UI/metadata changes requiring internationalization.
Cmux Swiftui State Layout ✅ Passed PR refactors existing @Published property in legacy Workspace ObservableObject. No new SwiftUI View files modified, no GeometryReader/List violations, no render-time mutations detected.
Cmux Architecture Rethink ✅ Passed No anti-patterns: typed enum dict, @Published+didSet reactive pattern, single source of truth, clear ownership, proper lifecycle handling, no timing/locks/observers.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR refactors unread state in data models without introducing or changing NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup code.
Description check ✅ Passed PR description includes summary of changes, testing results, and dogfood validation but lacks manual verification details and demo video.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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-session-restore-sidebar-notifications

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 May 22, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes workspace sidebar unread badges being absent after session restore by persisting a per-panel restoredUnreadContributesToWorkspace flag in SessionPanelSnapshot and threading it through the restore, detach/attach, and workspace-unread-sync code paths.

  • SessionPersistence.swift — adds restoredUnreadContributesToWorkspace: Bool? to SessionPanelSnapshot; nil is the legacy-compat sentinel, with a correct fallback at restore time so old sessions default to contributing.
  • Workspace.swift — replaces the Set<UUID> restoredUnreadPanelIds store with a typed [UUID: RestoredPanelUnreadIndicator] dictionary, updates syncPanelDerivedWorkspaceUnread to gate workspace badge on contributesToWorkspaceUnread.
  • Workspace+DetachedSurfaceTransfer.swift / TabManager.swift — carry the indicator type through detach/attach transfers and include it in the autosave fingerprint hash.

Confidence Score: 5/5

Safe to merge — all restore, detach/attach, and autosave paths are updated consistently, legacy nil snapshots default correctly to contributing, and the fix is covered by new focused tests.

The change is narrowly scoped to session restore unread state. The typed enum replaces a plain Bool throughout all code paths, the legacy-nil fallback expression is correct, and four new unit tests exercise exactly the edge cases that previously caused the badge to be missing.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Workspace.swift Core change: replaces restoredUnreadPanelIds (Set) with restoredUnreadPanelIndicators (typed dictionary), wires contributesToWorkspaceUnread into syncPanelDerivedWorkspaceUnread, and handles legacy nil snapshots correctly in applySessionPanelMetadata.
Sources/SessionPersistence.swift Adds restoredUnreadContributesToWorkspace: Bool? = nil to SessionPanelSnapshot; backward-compatible Codable field with nil defaulting to legacy behavior.
Sources/Workspace+DetachedSurfaceTransfer.swift Upgrades detached surface transfer struct from Bool restoredUnread to RestoredPanelUnreadIndicator? restoredUnreadIndicator, preserving contribution semantics through detach/attach cycles.
Sources/TabManager.swift Adds restoredUnreadIndicatorContributesToWorkspace hash to sessionAutosaveFingerprint so switching visual-only vs contributing indicators triggers a new autosave.
cmuxTests/WorkspaceManualUnreadTests.swift Adds four new tests covering: contributed restored unread, legacy nil snapshot, visual-only read notification indicator, and fingerprint changes between visual-only vs contributing restored indicators.
cmuxTests/WorkspaceUnitTests.swift One-line update to DetachedSurfaceTransfer test fixture to use the new restoredUnreadIndicator: nil field.

Reviews (5): Last reviewed commit: "fix: keep read restored indicators visua..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift
Comment on lines +1078 to +1081
restorePanelUnreadIndicator(
panelId,
contributesToWorkspaceUnread: snapshot.restoredUnreadContributesToWorkspace == true
)

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.

P1 Sessions saved before this PR have restoredUnreadContributesToWorkspace = nil (the field didn't exist). Because nil == true evaluates to false, those restored panels get the visual unread indicator but never contribute to the workspace sidebar badge — the exact bug this PR is fixing. On first boot after upgrading, every user with a pre-existing unread panel in their session will silently skip the badge. The fix only kicks in after they clear the unread and go through another save/restore cycle.

Changing to != false treats nil (legacy, no recorded preference) as "should contribute", which is the correct migration default: every panel that was unread when the session was saved should light up the sidebar badge after restore.

Suggested change
restorePanelUnreadIndicator(
panelId,
contributesToWorkspaceUnread: snapshot.restoredUnreadContributesToWorkspace == true
)
restorePanelUnreadIndicator(
panelId,
contributesToWorkspaceUnread: snapshot.restoredUnreadContributesToWorkspace != false
)

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.

Addressed with explicit restoredUnreadContributesToWorkspace persistence and a more precise legacy fallback: nil plus no notification rows contributes to workspace unread, while nil plus read-only notification rows remains visual-only. Covered by testLegacyRestoredPanelUnreadIndicatorMarksWorkspaceUnreadForSidebar and testSessionRestorePreservesFocusedReadIndicatorWithReadNotificationsAsVisualOnly.

— Claude Code

@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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b609403-a063-43f8-844a-d848b3eb69e3

📥 Commits

Reviewing files that changed from the base of the PR and between bcd630e and 5846f4b.

📒 Files selected for processing (5)
  • Sources/SessionPersistence.swift
  • Sources/Workspace+DetachedSurfaceTransfer.swift
  • Sources/Workspace.swift
  • cmuxTests/WorkspaceManualUnreadTests.swift
  • cmuxTests/WorkspaceUnitTests.swift

Comment thread Sources/Workspace.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 5 files

Re-trigger cubic

@lawrencecchen
lawrencecchen force-pushed the issue-session-restore-sidebar-notifications branch from 5846f4b to 625ae8b Compare May 22, 2026 06:16
@lawrencecchen
lawrencecchen force-pushed the issue-session-restore-sidebar-notifications branch from 625ae8b to 1aec237 Compare May 22, 2026 10:52

@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 and found 1 potential issue.

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 1aec237. Configure here.

Comment thread Sources/Workspace.swift

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

♻️ Duplicate comments (1)
Sources/Workspace.swift (1)

1108-1114: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Default legacy nil restore flags to contributing.

Lines 1110-1111 still infer false from non-empty notifications, so an older snapshot with restoredUnreadContributesToWorkspace == nil can lose its workspace sidebar badge after restore. Only an explicit false should come back as visual-only here.

💡 Proposed fix
-            let contributesToWorkspaceUnread = snapshot.restoredUnreadContributesToWorkspace
-                ?? (snapshot.notifications?.isEmpty ?? true)
+            let contributesToWorkspaceUnread = snapshot.restoredUnreadContributesToWorkspace != false
🤖 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 1108 - 1114, The code currently falls
back from snapshot.restoredUnreadContributesToWorkspace to inferring from
snapshot.notifications, which causes nil (legacy) to be treated as
non-contributing; change the fallback so that only an explicit false makes it
visual-only. In the block where contributesToWorkspaceUnread is computed
(referencing snapshot.restoredUnreadContributesToWorkspace,
snapshot.notifications, and passed into restorePanelUnreadIndicator along with
panelId), replace the fallback expression with a default of true (e.g.
snapshot.restoredUnreadContributesToWorkspace ?? true) so nil => contributes to
workspace unread.
🤖 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.

Duplicate comments:
In `@Sources/Workspace.swift`:
- Around line 1108-1114: The code currently falls back from
snapshot.restoredUnreadContributesToWorkspace to inferring from
snapshot.notifications, which causes nil (legacy) to be treated as
non-contributing; change the fallback so that only an explicit false makes it
visual-only. In the block where contributesToWorkspaceUnread is computed
(referencing snapshot.restoredUnreadContributesToWorkspace,
snapshot.notifications, and passed into restorePanelUnreadIndicator along with
panelId), replace the fallback expression with a default of true (e.g.
snapshot.restoredUnreadContributesToWorkspace ?? true) so nil => contributes to
workspace unread.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 70239ca1-d051-48c7-a162-f2c2bf3bdb71

📥 Commits

Reviewing files that changed from the base of the PR and between 98c9c5b and 75d49c9.

📒 Files selected for processing (2)
  • Sources/Workspace.swift
  • cmuxTests/WorkspaceManualUnreadTests.swift

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Closeout note for the remaining review-bot rollup: I verified the fallback suggestion against the final restore behavior. Applying a blanket nil => true fallback would reintroduce the read-only/focused visual indicator bug that Cursor flagged. The final restore logic intentionally distinguishes legacy nil with no notification rows, which contributes to workspace unread, from legacy nil with read-only notification rows, which remains visual-only. Covered by testLegacyRestoredPanelUnreadIndicatorMarksWorkspaceUnreadForSidebar and testSessionRestorePreservesFocusedReadIndicatorWithReadNotificationsAsVisualOnly.

@lawrencecchen
lawrencecchen merged commit 6f1ecc9 into main May 23, 2026
22 checks passed
@lawrencecchen
lawrencecchen deleted the issue-session-restore-sidebar-notifications branch May 23, 2026 05:04

This branch was successfully deployed

1 active deployment
Preview – cmux — 75d49c9c Deployed May 23, 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