Skip to content

perf: incremental notification badge sync (#4636) - #4643

Closed
austinywang wants to merge 4 commits into
mainfrom
issue-4636-sync-badges-perf
Closed

austinywang wants to merge 4 commits into
mainfrom
issue-4636-sync-badges-perf

Conversation

@austinywang

@austinywang austinywang commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #4636.

This takes the combination approach from the issue:

  • narrows notification badge observation to a non-@Published, workspace-scoped TerminalNotificationStore.badgeInvalidations(forTabId:) publisher, so unrelated workspace notifications no longer wake every mounted WorkspaceContentView badge sync path
  • moves Bonsplit tab presentation refresh into Workspace and updates by dirty panel IDs with direct panelId -> tabId -> updateTab lookup instead of walking allPaneIds × tabs(inPane:)
  • keeps full-panel sync only for initial/on-appear repair, while notification, focused-read, manual unread, restored unread, and workspace manual unread changes use panel-scoped invalidations

Verification

Local tests/builds were not run per repo/task instructions; CI is the verification path for this PR.

I did not capture before/after sample numbers because I did not synthesize the high-fanout workload locally. The algorithmic change is the verification argument:

  • before: every notificationStore.notifications change was observed by every mounted workspace and ran O(workspaces × panes × tabs) tab evaluation, e.g. 20 workspaces × 5 panes × 4 tabs = about 400 tab evaluations per notification before any updateTab diff guard
  • after: a notification emits only for its tabId, and only the changed panel IDs are synced, so the hot path is O(changed-panels) for the affected workspace; unrelated mounted workspaces receive no badge invalidation
  • workspace manual unread representative changes update only the old/new representative panels; local manual/restored panel unread changes update the symmetric-difference panel set

Behaviors Covered

  • notification badge invalidations emit only for the changed workspace/panel
  • a targeted Bonsplit tab presentation sync updates the requested panel without repairing unrelated tabs
  • full workspace presentation sync still repairs all panels when explicitly requested

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


Note

Medium Risk
Touches unread/badge presentation across notifications, workspace manual unread, and Bonsplit tab state; behavior should match prior logic but regressions could show wrong badges on edge cases (representative panel, focused-read indicator).

Overview
Replaces global notificationStore.notifications observation in mounted workspaces with a workspace-scoped badgeInvalidations(forTabId:) Combine publisher that emits panel- or workspace-manual-unread scoped invalidations when notifications, focused-read indicators, or workspace manual unread change.

Workspace now owns targeted Bonsplit tab presentation sync (showsNotificationBadge, kind, pin) via syncBonsplitTabPresentationState(forPanelIds:) using direct panelId → tabId updates instead of scanning all panes and tabs; full sync remains for on-appear repair.

WorkspaceContentView subscribes to invalidations and symmetric-difference panel sets for manual/restored unread, with explicit handling when the workspace manual-unread representative panel moves. Tests cover publisher scoping and that targeted sync does not rewrite unrelated tabs.

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


Summary by cubic

Makes notification badge sync incremental and tab-scoped so only affected panels update, fixing #4636. Bonsplit tab presentation state (badge/kind/pin) now updates per panel to cut work and improve responsiveness.

  • Refactors
    • Added a Combine publisher badgeInvalidations(forTabId:) that emits TerminalNotificationBadgeInvalidation scoped to .panelIds or .workspaceManualUnread, including focused‑read changes.
    • Workspace now provides syncBonsplitTabPresentationState(forPanelIds:) and syncBonsplitTabPresentationStateForAllPanels() to update exact panels; full sync remains for on‑appear/repair.
    • WorkspaceContentView listens via .onReceive and applies panel‑scoped invalidations; it uses symmetric‑diffs for manual/restored unread and updates only representative panels on workspace manual‑unread flips.
    • Added tests for publisher scoping and that targeted tab presentation sync updates only the requested panels.

Written for commit 084c049. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Improvements

    • More accurate unread notification badges across tabs and panels.
    • Better tracking and synchronization of manual-unread state with reduced unnecessary badge refreshes.
    • More targeted, incremental badge updates for faster, more reliable notification indicator behavior.
  • Tests

    • New tests validating scoped badge invalidations and targeted sync behavior.

Review Change Stack

@vercel

vercel Bot commented May 23, 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 26, 2026 7:32pm
cmux-staging Building Building Preview, Comment May 26, 2026 7:32pm

@coderabbitai

coderabbitai Bot commented May 23, 2026 •

Copy link
Copy Markdown

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 30 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

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

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

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: 70b1103a-3e48-4853-b735-7df74782eacf

📥 Commits

Reviewing files that changed from the base of the PR and between c8a6276 and 084c049.

📒 Files selected for processing (4)
  • Sources/TerminalNotificationStore.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceContentView.swift
  • cmuxTests/WorkspaceManualUnreadTests.swift
📝 Walkthrough

Walkthrough

This PR adds targeted badge invalidations in TerminalNotificationStore, a per-panel Bonsplit presentation reconciler in Workspace, and incremental badge-sync wiring in WorkspaceContentView that subscribes to per-workspace invalidations and updates only affected panels.

Changes

Notification badge invalidation and incremental sync

Layer / File(s) Summary
Badge invalidation model and publisher
Sources/TerminalNotificationStore.swift
TerminalNotificationBadgeInvalidation type, badgeInvalidationSubject, and badgeInvalidations(forTabId:) publisher are added; file imports Combine.
Badge invalidation emission from notification changes
Sources/TerminalNotificationStore.swift
@Published observers on notifications, manualUnreadWorkspaceIds, and focusedReadIndicatorByTabId compute per-tab unread panel-id sets, diff old vs new via symmetric difference, and publish .panelIds / .workspaceManualUnread invalidations using helper functions.
Workspace tab presentation state reconciliation
Sources/Workspace.swift
Adds syncBonsplitTabPresentationState(forPanelIds:) and syncBonsplitTabPresentationStateForAllPanels() which compute expected badge visibility (including workspace manual-unread representative) and reconcile Bonsplit tab presentation (showsNotificationBadge/isPinned/kind). Existing badge-only helpers delegate to these.
View-level incremental badge update and state tracking
Sources/WorkspaceContentView.swift
Replaces @EnvironmentObject injection with a computed notificationStore, adds local @State tracking and unreadBadgeRevision, subscribes to notificationStore.badgeInvalidations(forTabId:), and uses targeted sync helpers to update only changed panels; replaces prior full-scan sync implementation.
Badge invalidation and targeted sync test coverage
cmuxTests/WorkspaceManualUnreadTests.swift
Adds Combine import and tests asserting that per-tab badge invalidations are emitted only for the affected workspace and that targeted syncBonsplitTabPresentationState(forPanelIds:) updates only requested panels (full sync clears remaining state).

Sequence Diagram(s)

sequenceDiagram
  participant WorkspaceContentView
  participant TerminalNotificationStore
  participant Workspace
  participant BonsplitController
  TerminalNotificationStore->>TerminalNotificationStore: detect notifications/manualUnread/focus diffs
  TerminalNotificationStore->>WorkspaceContentView: emit badgeInvalidations(forTabId:) (.panelIds/.workspaceManualUnread)
  WorkspaceContentView->>Workspace: syncBonsplitTabPresentationState(forPanelIds:)
  Workspace->>BonsplitController: updateTab(tabId, showsNotificationBadge/isPinned/kind)
  WorkspaceContentView->>WorkspaceContentView: bump unreadBadgeRevision / adjust local manual-unread tracking
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4550: Changes to Sources/Workspace.swift's unread badge logic and workspace unread state derivation; overlaps with this PR's presentation-state refactor.

Poem

🐰 I used to hop through every tab and pane,
now I whisper just the panels that changed my brain.
Small nudges, tidy hops, no thunderous rescan—
badges bloom only where the new unread began. 🌱

🚥 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 'perf: incremental notification badge sync (#4636)' accurately summarizes the main change—moving from full to incremental badge synchronization—and is directly related to the primary objective of the PR.
Description check ✅ Passed The description covers Summary (what changed and why), Verification (explaining the algorithmic improvement and noting that CI is the verification path), and Behaviors Covered, but is missing explicit Testing section with manual verification steps and no demo video provided.
Linked Issues check ✅ Passed The PR addresses the primary coding objectives from #4636: workspace-scoped badge invalidation publishers, incremental panel-targeted updates replacing O(panes×tabs) scans, and tests verifying workspace/panel scoping and targeted sync behavior.
Out of Scope Changes check ✅ Passed All changes are within scope: new TerminalNotificationBadgeInvalidation type with publisher, Workspace presentation sync methods, WorkspaceContentView invalidation wiring, and targeted test coverage for badge sync incremental behavior.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation violations. Store stays @MainActor; invalidation struct is value-type without implicit MainActor; all accesses properly bounded in MainActor contexts.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no blocking/timing primitives in badge sync code. Uses Combine PassthroughSubject for async invalidations, event-driven bindings, no Task.sleep, semaphores, main.sync, or locks.
Cmux No Hacky Sleeps ✅ Passed PR only modifies Swift files (.swift). Custom check scope explicitly covers "TypeScript, JavaScript, shell, and non-Swift build/runtime scripts" and excludes Swift, which is covered separately.
Cmux Swift Concurrency ✅ Passed PR adds PassthroughSubject publisher to existing ObservableObject store for incremental updates. Not new Combine app state. No @Published props, ObservableObjects, DispatchQueues, or Tasks added.
Cmux Swift @Concurrent ✅ Passed All new Swift code maintains proper actor isolation; synchronous operations on @MainActor, no async functions without @concurrent, no invalid @concurrent usage.
Cmux Swift File And Package Boundaries ✅ Passed All modified production files add under 250 lines to already-oversized files. No inappropriate responsibility mixing introduced. Changes preserve clear extraction paths.
Cmux Swift Logging ✅ Passed PR adds zero logging statements. All 376 added lines contain no print, debugPrint, dump, NSLog, or problematic file logging. Test code validating new Combine publishers is allowed.
Cmux User-Facing Error Privacy ✅ Passed PR contains no new user-facing error messages, alerts, or strings that violate privacy rules. Changes are internal refactoring of badge sync logic with new Combine publishers and data structures.
Cmux Full Internationalization ✅ Passed No new user-facing strings added. Changes are internal performance refactoring; test fixtures use plain strings. No String(localized:) calls added; web message removals align with code changes.
Cmux Swiftui State Layout ✅ Passed No violations: TerminalNotificationStore is existing legacy ObservableObject; WorkspaceContentView uses safe let bindings in body, @State mutations only in event handlers.
Cmux Architecture Rethink ✅ Passed PR increments badge sync via Combine publisher scoped to workspace, eliminating O(workspaces×panes) per event. No timing repairs, split lifecycle, or duplicate state ownership. Legitimate @State use.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds no new window creation code. It optimizes notification badge sync via Combine publishers and refactored helpers. Window references found are pre-existing.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-4636-sync-badges-perf

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.

@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 c8a6276. Configure here.

Comment thread Sources/WorkspaceContentView.swift
@greptile-apps

greptile-apps Bot commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Narrows notification badge sync from an O(workspaces × panes × tabs) sweep per notification to an O(changed-panels) targeted update for the affected workspace. TerminalNotificationStore now emits a scoped badgeInvalidations(forTabId:) Combine publisher instead of relying on broad @Published observation, and WorkspaceContentView applies incremental invalidations rather than walking all panels.

  • TerminalNotificationStore adds a PassthroughSubject<TerminalNotificationBadgeInvalidation, Never> that emits on notification content changes, workspace-level manual unread flips, and focused-read indicator transitions, filtered per tab by badgeInvalidations(forTabId:).
  • Workspace exposes syncBonsplitTabPresentationState(forPanelIds:) for targeted badge/kind/pin updates and keeps syncBonsplitTabPresentationStateForAllPanels() for on-appear repair.
  • WorkspaceContentView replaces @EnvironmentObject observation with onReceive of the scoped publisher plus symmetric-difference onChange handlers; unreadBadgeRevision is read in body to trigger ring-indicator re-renders after each targeted sync.

Confidence Score: 5/5

Safe to merge — the algorithmic change is well-scoped, the full-repair path on onAppear is preserved, and the new tests cover both publisher scoping and targeted-sync isolation.

The three emission sites in TerminalNotificationStore each correctly compute symmetric differences before sending, the WorkspaceContentView caches and updates workspace-manual-unread state consistently across all change paths, and unreadBadgeRevision is read in body to ensure ring indicators refresh after every targeted sync. No missed invalidation path or stale-state condition was found.

No files require special attention.

Important Files Changed

Filename Overview
Sources/TerminalNotificationStore.swift Adds PassthroughSubject-backed badgeInvalidations(forTabId:) publisher with three emission sites; logic is correct and scoped properly per tabId.
Sources/Workspace.swift New public syncBonsplitTabPresentationState(forPanelIds:) consolidates badge/kind/pin update logic; thin wrappers preserve existing call sites.
Sources/WorkspaceContentView.swift Replaces broad @EnvironmentObject observation with onReceive of the scoped publisher; unreadBadgeRevision is read in body to keep ring indicators fresh after targeted syncs.
cmuxTests/WorkspaceManualUnreadTests.swift Adds two new tests covering publisher scoping and targeted-sync non-interference.

Reviews (3): Last reviewed commit: "merge: resolve conflicts with main" | Re-trigger Greptile

Comment thread Sources/WorkspaceContentView.swift
Comment thread Sources/TerminalNotificationStore.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.

Actionable comments posted: 3

🤖 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 `@Sources/TerminalNotificationStore.swift`:
- Around line 2209-2221: Loop currently emits a .panelIds invalidation for every
tabId even when the focused panel didn’t change; modify the loop in
TerminalNotificationStore so you skip a tab when oldValue[tabId] ==
newValue[tabId] (or both nil) before building changedPanelIds and sending
badgeInvalidationSubject. Specifically, inside the for tabId in tabIds loop,
check equality of oldValue[tabId] and newValue[tabId] and continue if unchanged,
then proceed to collect changedPanelIds and send
TerminalNotificationBadgeInvalidation(scope: .panelIds(changedPanelIds)).

In `@Sources/Workspace.swift`:
- Around line 10176-10181: The reconciliation currently treats a nil
expectedKind as "no-op" because kindUpdate is created with expectedKind.map {
.some($0) } and the guard only checks kind mismatch when expectedKind != nil, so
stale tab.kind never gets cleared; change kindUpdate to explicitly wrap the
optional (e.g., let kindUpdate: String?? = .some(expectedKind)) so a nil
expectedKind becomes an explicit clear, and update the guard condition(s) to
compare tab.kind != expectedKind (remove the expectedKind != nil gating) so
differences including nil vs non-nil are detected and applied; apply the same
change in the other similar block around the code referenced at 10185-10189.

In `@Sources/WorkspaceContentView.swift`:
- Line 176: The State property unreadBadgeRevision is dead code because it's
only written to by bumpUnreadBadgeRevision() and never read in body; either
reference unreadBadgeRevision inside WorkspaceContentView.body (for example by
adding let _ = unreadBadgeRevision or using .id(unreadBadgeRevision) on the
view) to force SwiftUI refresh when bumpUnreadBadgeRevision() is called, or
remove the unreadBadgeRevision declaration and all uses of
bumpUnreadBadgeRevision() entirely; update symbols: unreadBadgeRevision and
bumpUnreadBadgeRevision() accordingly.
🪄 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: 375eec17-5ad6-45eb-a600-9b2c4fc01544

📥 Commits

Reviewing files that changed from the base of the PR and between 200919a and c8a6276.

📒 Files selected for processing (4)
  • Sources/TerminalNotificationStore.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceContentView.swift
  • cmuxTests/WorkspaceManualUnreadTests.swift

Comment thread Sources/TerminalNotificationStore.swift
Comment thread Sources/Workspace.swift Outdated
Comment thread Sources/WorkspaceContentView.swift
Comment thread Sources/Workspace.swift
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: syncBonsplitNotificationBadges runs O(panes × tabs) on every notification, manualUnread, and restoredUnread change in every mounted workspace

3 participants