Skip to content

Migrate the macOS app off ObservableObject to @Observable - #8371

Closed
azooz2003-bit wants to merge 14 commits into
mainfrom
feat-obsmg
Closed

azooz2003-bit wants to merge 14 commits into
mainfrom
feat-obsmg

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator

Replaces every remaining ObservableObject conformance in cmux-owned code with the @Observable macro: 42 types across the app target and the CmuxAppKitSupportUI/CmuxTerminal packages, including the Panel protocol (now requires Observation.Observable) and the Workspace/TabManager god objects. SwiftUI now invalidates views per accessed property instead of on every objectWillChange, which removes the whole-store redraw traffic that @Published fan-out caused.

Mechanics, wave by wave (one commit each):

  1. 20 leaf stores (sidebar, file explorer state, shortcuts observer, todo state, hover/visibility singletons).
  2. TerminalSurface + SearchState (keeps its non-MainActor isolation and nonisolated deinit).
  3. Panel protocol + all 14 conformers and the browser stores.
  4. TerminalNotificationStore/SidebarUnreadModel, CmuxConfigStore, FileExplorerStore, SessionIndexStore.
  5. Workspace (39 tracked properties, 22 bridge publishers, 146 @ObservationIgnored internals).
  6. TabManager + app environment plumbing, final sweep.

Behavioral-parity rules used throughout: only formerly-@Published properties are tracked and every other stored member is @ObservationIgnored, so redraw semantics and hot-path costs match the old world; remaining Combine subscribers consume explicit CurrentValueSubject bridges fed from willSet (the established tabsPublisher seam pattern), so emission timing and replay-on-subscribe are identical; manual objectWillChange.send() invalidation was replaced by tracking the announced state, and FileExplorerStore's AppKit outline coordinator keeps its 50ms-debounced reload via an explicit changePublisher that fires on every former invalidation path. View wrappers moved @StateObject→@State, @ObservedObject→let/@Bindable, @EnvironmentObject→@Environment(T.self); .onReceive($prop) sites became .onChange(initial: true) to preserve the projected publisher's replay delivery. The TabItemView Equatable typing-latency gate and the sidebar snapshot boundaries are untouched.

Zero ObservableObject/@Published/@StateObject/@ObservedObject/@EnvironmentObject/objectWillChange uses remain in cmux-owned macOS and iOS sources (the iOS app was already fully on @Observable).

Supersedes #7606, which predates the sub-model refactor waves this migration builds on.

🤖 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

Migrates the macOS app fully to @Observable, including Workspace, TabManager, all panels, and core stores. Also fixes the titlebar unread badge by reading the tracked notificationMenuSnapshot.unreadCount, and updates tests to keep @Observable macro scoping happy.

  • Refactors

    • Replaced all remaining ObservableObject with @Observable across app and packages; Panel now requires Observation.Observable. Migrated all panel types and TerminalSurface while keeping its isolation and nonisolated deinit.
    • Moved views to value + @Bindable and @Environment(Type.self); updated app root and tests. Injected BrowserDesignModeCardDragBridge from the host.
    • Added willSet‑fed bridge publishers for remaining Combine consumers (e.g., notificationMenuSnapshotPublisher, pendingBackgroundWorkspaceLoadIdsPublisher, BrowserPanel pageTitlePublisher/isLoadingPublisher/faviconPNGDataPublisher/isMutedPublisher/webViewLifecycleStatePublisher, minimal‑mode hoveredWindowNumberPublisher, notifications shownWindowNumbersPublisher). Kept the file explorer’s debounced outline reload via FileExplorerStore.changePublisher.
    • Migrated core stores/utilities to @Observable (notification, config, file explorer, session index, keyboard shortcuts, closed‑item history, sidebar drag auto‑scroll, workspace todo). Documented that the agent‑index notification availability task is pull‑only at NSMenu construction, so no global invalidation is required.
    • Widened @Observable test mocks to fileprivate to satisfy the macro’s file‑scope conformance emission and fixed tests to observe tracked properties instead of objectWillChange.
  • Migration

    • Replace .environmentObject(X) with .environment(X) and read via @Environment(X.self).
    • Replace @ObservedObject with let/@Bindable and @StateObject with @State.
    • Swap $prop subscribers for the provided bridge ...Publishers where Combine remains.

Written for commit 5c57c3e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Improvements
    • Improved UI reactivity and consistency across browser, terminal, sidebar, notifications, and settings by modernizing state updates and reducing redundant refreshes.
  • Documentation
    • Updated documentation to better explain UI update timing and legacy-compatibility behavior for state change propagation.
  • Tests
    • Expanded and adjusted tests to validate observation-driven updates and confirm that equal-value and unmount/remount scenarios don’t trigger unnecessary refreshes.

cmux reload-cloud and others added 7 commits July 17, 2026 12:29
Wave 1 of the ObservableObject -> @observable migration: 20 leaf state
stores (SidebarState, SidebarSelectionState, FileExplorerState,
KeyboardShortcutSettingsObserver, ClosedItemHistoryStore,
WorkspaceTodoState, hover/visibility singletons, and 13 more) plus all
their @StateObject/@ObservedObject/@EnvironmentObject call sites.
Remaining Combine subscribers of formerly-@published properties are
served by explicit CurrentValueSubject bridges fed from willSet,
matching the established tabsPublisher seam semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wave 2: TerminalSurface keeps its non-MainActor isolation and
nonisolated deinit; only the formerly-@published searchState and
keyboardCopyModeActive are tracked, all other storage is
@ObservationIgnored so keystroke-hot paths gain no registrar overhead.
searchStatePublisher/needlePublisher bridges preserve the exact
Published willSet emission timing for the remaining Combine
subscribers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wave 3: Panel now requires Observation.Observable instead of
ObservableObject; all 14 conformers (TerminalPanel, BrowserPanel,
CustomSidebarPanel, FilePreviewPanel, MarkdownPanel, ProjectPanel,
WorkspaceTodoPanel, RightSidebarToolPanel,
CMUXSidebarExtensionBrowserPanel, CloudVMLoadingPanel,
AgentSessionPanel) plus BrowserProfileStore, BrowserHistoryStore, and
BrowserSearchState migrate with behavioral-parity tracking. Remaining
Combine subscribers (Workspace tab-title pipelines, DockSplitStore,
tests) consume explicit willSet-fed bridge publishers. BrowserPanel's
manual objectWillChange sends are replaced by making the announced
state tracked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bservable

Wave 4: TerminalNotificationStore, SidebarUnreadModel, CmuxConfigStore,
FileExplorerStore, and SessionIndexStore. The SidebarUnreadModel
coalescing boundary and SessionIndexStore snapshot boundaries are
preserved. FileExplorerStore's manual objectWillChange invalidation is
replaced by an explicit coalesced changePublisher that fires on every
former invalidation path (tracked-property didSets plus the in-place
node mutation sites), keeping the AppKit outline coordinator's 50ms
debounced reload contract intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wave 5: the Workspace god object drops ObservableObject; its 39
formerly-@published properties become tracked and 146 internal stored
members are @ObservationIgnored for behavioral parity. 22
CurrentValueSubject bridges preserve Published willSet semantics for
the remaining Combine consumers (sidebar observation, mobile list
observer, right-sidebar tools, config, tests). The PaneTreeHosting
hooks stop re-emitting objectWillChange since PaneTreeModel's tracked
properties are the observation surface; setRemoteTmuxWindowMirror's
manual send is replaced by tracking remoteTmuxWindowMirrors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wave 6 (final): TabManager drops ObservableObject; the existing
tabsPublisher/selectedTabIdPublisher/workspaceGroupsPublisher bridges
stay, their hooks stop re-emitting objectWillChange since the tracked
WorkspacesModel properties are the observation surface. cmuxApp's
remaining @StateObject cluster becomes @State and the AppDelegate
environment chain moves to .environment/@Environment. Also splits a
two-variable @ObservationIgnored declaration in Workspace.swift that
the macro rejects (wave-5 build fix).

The app now has zero ObservableObject/@Published/@StateObject/
@ObservedObject/@EnvironmentObject usage in cmux-owned code; the only
Combine remnants are documented willSet-fed bridge publishers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

# Conflicts:
#	Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
#	Sources/ContentView.swift
#	Sources/Panels/BrowserPanel.swift
@greptile-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Too many files changed for review. (108 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@vercel

vercel Bot commented Jul 17, 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 Jul 17, 2026 9:03pm
cmux-staging Ready Ready Preview, Comment Jul 17, 2026 9:03pm

@coderabbitai

coderabbitai Bot commented Jul 17, 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
📝 Walkthrough

Walkthrough

The PR migrates application models and SwiftUI observation boundaries from ObservableObject/@Published to Swift Observation, adds explicit Combine bridge publishers, updates environment and binding wiring, and revises tests and documentation for the new observation semantics.

Changes

Observation migration

Layer / File(s) Summary
Core model migration
Sources/Panels/*, Sources/Workspace.swift, Sources/TabManager.swift, Sources/TerminalNotificationStore.swift
Models and panels adopt @Observable; internal identity, cache, task, and runtime state is marked @ObservationIgnored.
Publisher bridges and SwiftUI wiring
Sources/ContentView.swift, Sources/WorkspaceSidebarObservation.swift, Sources/*View.swift, Packages/macOS/*
Explicit publishers replace projected @Published streams, while views use typed environments, @Bindable, @State, and plain references.
Validation and documentation
cmuxTests/*, Packages/*
Tests use Observation tracking or named bridge publishers, and comments describe updated timing, replay, and parity contracts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#4318 — Overlaps SidebarLazyLayoutScaleTests.
  • manaflow-ai/cmux-dev-artifacts#4476 — Overlaps WorkspaceContentViewVisibilityTests.
  • manaflow-ai/cmux-dev-artifacts#4194 — Overlaps WorkspaceSidebarObservationTests.

Possibly related PRs

Suggested reviewers: austinywang


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error Touched app-target logic includes reusable, testable models/stores (WorkspaceTodoState, FileExplorerStore, SessionIndexStore, CmuxConfigStore) that the rule says should live behind packages. Extract those seams into small package targets (e.g. CmuxWorkspaces for workspace/todo/session/file-explorer state and a config package); leave app target for UI/AppKit composition only.
Cmux Architecture Rethink ❌ Error SidebarTabItemSettingsStore moved from @StateObject to @State even though init registers observers and starts loading, so view re-evals can recreate side effects. Keep that model under one stable owner (@StateObject/parent-owned) or move observer/task startup out of init into idempotent .task/onAppear and store only pure state in @State.
Docstring Coverage ⚠️ Warning Docstring coverage is 41.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers the migration well but omits required template sections like Testing, Demo Video, Review Trigger, and Checklist. Add the missing template sections: testing details, a demo video link if applicable, the review trigger block, and a completed checklist.
✅ Passed checks (21 passed)
Check name Status Explanation
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 PASS: UI-bound observables are annotated @MainActor; the few non-MainActor models are documented main-thread-only/runtime types and hop back to MainActor where needed, so no new isolation bug.
Cmux Swift Blocking Runtime ✅ Passed PASS: Added-line scan found no new waits/sleeps/asyncAfter/main.sync; the only lock hits are existing TerminalSurface NSLock fields re-annotated with @ObservationIgnored.
Cmux Browser Automation Off-Main ✅ Passed PR only changes six test files for @Observable migration; it doesn’t touch the browser socket automation routing files or introduce any WebKit/socket wait paths.
Cmux Expensive Synchronous Load ✅ Passed PASS: The diff only changes observation plumbing/comments; no added or moved synchronous agent-history load or JSONL/transcript parsing appears in actual diff hunks on main/interactive paths.
Cmux Cache Substitution Correctness ✅ Passed Only cache substitutions have cold/stale fallbacks and rationale; no persistence/history/undo/snapshot path swaps a fresh read for an unguarded cache.
Cmux No Hacky Sleeps ✅ Passed Branch diff only touches Swift sources/tests; no covered non-Swift runtime files or delay/sleep patterns were introduced.
Cmux Algorithmic Complexity ✅ Passed Production diff is observation-plumbing only; added lines are wrappers/bridges, and a scan found no new nested scans, sorts, or filters in hot paths.
Cmux Swift Concurrency ✅ Passed Cumulative diff adds Observation/Combine bridge publishers and test sinks, but no new background queues, detached Tasks, or completion-handler APIs in runtime code.
Cmux Swift @Concurrent ✅ Passed PR diff has no @concurrent or nonisolated async code changes; async-looking hunks are comment-only or an @ObservationIgnored async closure property, so no rule violation.
Cmux Swiftpm Lockfiles ✅ Passed PR range has no Package.swift/Package.resolved/.gitignore/project/workflow changes, and no cmux-owned .gitignore ignores Package.resolved.
Cmux Swift Logging ✅ Passed No new or materially changed logging was introduced; diff scans found no added print/debugPrint/dump/NSLog/Logger in production code.
Cmux User-Facing Error Privacy ✅ Passed PASS: The actual diff only changes test files, which the rule explicitly allows, and no production-facing error/alert/recovery copy was added.
Cmux Full Internationalization ✅ Passed PR only changes observation plumbing/tests/comments; no production localized text, string catalogs, or web locale files were added or changed.
Cmux Swiftui State Layout ✅ Passed Head commit only widens test mocks; touched SwiftUI files use @Observable/let/@bindable and show no new GeometryReader or render-time state mutation.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Touched window files only changed view/store observation code; the actual controllers/identifiers were unchanged, and the debug window stays registered as cmux.pdfPreviewChromeDebug.
Cmux Source Artifacts ✅ Passed All changed paths are source/test files; none are logs, build output, caches, temp dirs, or other source-control artifacts.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test/debug seam was added in production Sources; all debug/ForTesting symbols were pre-existing and only observation annotations changed.
Cmux No Ambient Global State ✅ Passed No new ambient globals/singletons were introduced; existing shared observers were only converted to @Observable and retained.
Title check ✅ Passed The title clearly summarizes the main migration from ObservableObject to @Observable, even though it omits some package and iOS scope.
✨ 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 feat-obsmg

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmuxTests/CmuxConfigTests.swift (1)

698-716: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing observation tracking makes the inverted expectation vacuous.

The migration removed the Combine subscription to $loadedActions here but did not add the corresponding Observation tracking mechanism (unlike the tests below it). Because didAutoReload.fulfill() is never called, this inverted expectation will always pass after 0.25 seconds, even if a hot reload incorrectly occurs.

Add withObservationTracking to properly assert that the configuration does not auto-reload.

💚 Proposed fix
         let didAutoReload = expectation(description: "cmux.json should not hot reload")
         didAutoReload.isInverted = true
+        withObservationTracking {
+            _ = store.loadedActions
+        } onChange: {
+            didAutoReload.fulfill()
+        }
         try """
         {
           "actions": {
🤖 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 `@cmuxTests/CmuxConfigTests.swift` around lines 698 - 716, Update the
hot-reload test around the inverted expectation didAutoReload to observe
store.loadedActions using withObservationTracking, and fulfill the expectation
when the observed value changes. Preserve the existing timeout and assertions so
the test fails if configuration observation triggers an automatic reload.
Sources/Panels/FilePreviewTextEditor.swift (1)

17-76: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Pass textContent through SwiftUI
Sources/Panels/FilePreviewTextEditor.swift:17-76 only refreshes the AppKit editor when SwiftUI re-runs updateNSView, but FilePreviewPanelView.body never reads panel.textContent. That leaves clean reload/revert paths able to change textContent without updating textView.string, so the editor can stay stale when isDirty is already false. Pass the content in as an explicit input or update the text view directly when new text is loaded.

🤖 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/Panels/FilePreviewTextEditor.swift` around lines 17 - 76, Ensure
SwiftUI observes panel.textContent so FilePreviewTextEditor.updateNSView runs
whenever clean reload or revert changes it. Update FilePreviewPanelView.body to
pass panel.textContent as an explicit input to FilePreviewTextEditor, preserving
the existing textView.string synchronization in updateNSView.
🤖 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.

Outside diff comments:
In `@cmuxTests/CmuxConfigTests.swift`:
- Around line 698-716: Update the hot-reload test around the inverted
expectation didAutoReload to observe store.loadedActions using
withObservationTracking, and fulfill the expectation when the observed value
changes. Preserve the existing timeout and assertions so the test fails if
configuration observation triggers an automatic reload.

In `@Sources/Panels/FilePreviewTextEditor.swift`:
- Around line 17-76: Ensure SwiftUI observes panel.textContent so
FilePreviewTextEditor.updateNSView runs whenever clean reload or revert changes
it. Update FilePreviewPanelView.body to pass panel.textContent as an explicit
input to FilePreviewTextEditor, preserving the existing textView.string
synchronization in updateNSView.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2a4e4e8a-056e-40f5-98c5-b25e1aca852a

📥 Commits

Reviewing files that changed from the base of the PR and between c9f2d8c and 9d8f96e.

📒 Files selected for processing (105)
  • Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
  • Packages/iOS/CmuxMobileBrowser/Sources/CmuxMobileBrowser/BrowserSurfaceState.swift
  • Packages/macOS/CmuxAppKitSupportUI/Sources/CmuxAppKitSupportUI/Scroll/SidebarDragAutoScrollController.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Profiles/Repository/BrowserProfileRepository.swift
  • Packages/macOS/CmuxPanes/Sources/CmuxPanes/Model/PaneTreeHosting.swift
  • Packages/macOS/CmuxPanes/Sources/CmuxPanes/Model/PaneTreeModel.swift
  • Packages/macOS/CmuxPanes/Sources/CmuxPanes/Model/SplitLayoutModel.swift
  • Packages/macOS/CmuxPanes/Tests/CmuxPanesTests/PaneTreeModelTests.swift
  • Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/WorkspaceModel/WorkspaceSidebarMetadataModel.swift
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
  • Packages/macOS/CmuxUpdater/Sources/CmuxUpdater/UpdateController.swift
  • Packages/macOS/CmuxUpdater/Sources/CmuxUpdater/UpdateStateModel.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Core/Model/SurfaceRegistryModel.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesHosting.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspacesModelTests.swift
  • Sources/AllShortcutsPopover.swift
  • Sources/App/MenuBarExtraController.swift
  • Sources/AppDelegate.swift
  • Sources/BackgroundWorkspacePrimeCoordinator.swift
  • Sources/BrowserWindowPortal.swift
  • Sources/CMUXSidebarExtensionBrowserPanel.swift
  • Sources/Canvas/WorkspaceCanvasHostView.swift
  • Sources/ClosedItemHistory.swift
  • Sources/CmuxConfig.swift
  • Sources/CoalesceLatestPublisher.swift
  • Sources/ContentView.swift
  • Sources/DiffCommentSubmissionPool.swift
  • Sources/DockSplitStore.swift
  • Sources/Feed/FeedPanelView.swift
  • Sources/Feed/FeedPanelViewModel.swift
  • Sources/FileExplorerState.swift
  • Sources/FileExplorerStore.swift
  • Sources/FileExplorerView.swift
  • Sources/Find/BrowserSearchOverlay.swift
  • Sources/Find/SurfaceSearchOverlay.swift
  • Sources/FocusHistoryMenuInvalidator.swift
  • Sources/FocusSurfaceBroadcaster.swift
  • Sources/KeyboardShortcutSettingsFileStore.swift
  • Sources/Mobile/MobileWorkspaceListObserver.swift
  • Sources/NotificationsPage.swift
  • Sources/Panels/AgentSessionPanel.swift
  • Sources/Panels/BrowserDesignModePopoverHost.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/Panels/CustomSidebarPanel.swift
  • Sources/Panels/CustomSidebarPanelView.swift
  • Sources/Panels/FilePreviewPanel.swift
  • Sources/Panels/FilePreviewTextEditor.swift
  • Sources/Panels/MarkdownPanel.swift
  • Sources/Panels/MarkdownPanelView.swift
  • Sources/Panels/MarkdownTypographyControl.swift
  • Sources/Panels/PDFPreviewChromeDebugWindowController.swift
  • Sources/Panels/Panel.swift
  • Sources/Panels/PanelContentView.swift
  • Sources/Panels/ProjectBuildSettingsTabView.swift
  • Sources/Panels/ProjectFilesTabView.swift
  • Sources/Panels/ProjectPanel.swift
  • Sources/Panels/ProjectPanelView.swift
  • Sources/Panels/ProjectSchemesTabView.swift
  • Sources/Panels/ProjectTargetsTabView.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/Panels/TerminalPanelView.swift
  • Sources/Panels/WorkspaceTodoPanel.swift
  • Sources/Panels/WorkspaceTodoPanelView.swift
  • Sources/PricingPlansScreen.swift
  • Sources/RightSidebarPanelView.swift
  • Sources/RightSidebarToolPanel.swift
  • Sources/SessionIndexStore.swift
  • Sources/SessionIndexView.swift
  • Sources/Sidebar/SidebarState.swift
  • Sources/SidebarSelectionState.swift
  • Sources/TabManager.swift
  • Sources/TerminalNotificationStore.swift
  • Sources/TextBoxInput.swift
  • Sources/Update/MinimalModeSidebarControls.swift
  • Sources/Update/UpdateTitlebarAccessory.swift
  • Sources/VerticalTabsSidebar+EmptyAreasAndFooter.swift
  • Sources/WindowDragHandleView.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceContentView.swift
  • Sources/WorkspaceSidebarObservation.swift
  • Sources/WorkspaceTodoState.swift
  • Sources/cmuxApp.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/BrowserConfigTests.swift
  • cmuxTests/BrowserDesignModeComposerHostingViewTests.swift
  • cmuxTests/ClosedMainWindowRoutingTests.swift
  • cmuxTests/CmuxConfigTests.swift
  • cmuxTests/CmuxDurableDeepLinkRestoreTests.swift
  • cmuxTests/DockShortcutRoutingTests.swift
  • cmuxTests/DockTerminalReattachTests.swift
  • cmuxTests/FileDropOverlayViewTests.swift
  • cmuxTests/FileExplorerStoreTests.swift
  • cmuxTests/FocusSurfaceBroadcasterTests.swift
  • cmuxTests/GhosttyConfigTests.swift
  • cmuxTests/MarkdownPanelTests.swift
  • cmuxTests/MobileWorkspaceListFidelityTests.swift
  • cmuxTests/SessionIndexViewTests.swift
  • cmuxTests/SidebarLazyLayoutScaleTests.swift
  • cmuxTests/WindowDockLifecycleTests.swift
  • cmuxTests/WorkspaceContentViewVisibilityTests.swift
  • cmuxTests/WorkspaceRemoteConnectionTests.swift
  • cmuxTests/WorkspaceSidebarObservationTests.swift
  • cmuxTests/WorkspaceUnitTests.swift

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Verification status for this first pass:

  • Tagged Debug build (obsmg) is green on the PR head via the Blacksmith reload lane (4 successive full builds across the waves, final run https://github.com/manaflow-ai/cmux/actions/runs/29612665037).
  • Runtime smoke on the tagged app over the debug socket: boot, workspace create/list, terminal input, sidebar render, window placement; steady-state CPU 2-7% (no observation invalidation loop).
  • Swift unit-test CI jobs (tests, tests-build-and-lag, ui-regressions, release-build) are administratively disabled this week (PR-CI advisory experiment through 2026-07-21), so they report missing on the merge gate.
  • web-typecheck fails identically on other branches (RedirectType export missing from next/navigation.js); this diff touches zero web files.
  • test-e2e.yml (AutomationSocketUITests, run https://github.com/manaflow-ai/cmux/actions/runs/29613667606) failed with "Failed to activate application (Running Background)" on a runner that reported no logged-in GUI user; the same lane failed 9 times on main in the hour before this dispatch, so this is runner infra, not the migration. The same activation path works on the local tagged build.

🤖 Generated with Claude Code

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Investigated a dogfood report that sidebar scrolling feels slower on the migrated build. Controlled A/B (Debug vs Debug, this PR's head vs its exact merge-base main commit, 78 seeded workspaces, identical synthetic scroll bursts, one app at a time): median main-thread CPU 11.7% (migrated) vs 9.4% (baseline), ranges overlapping heavily under background load. A Self._printChanges() probe showed zero VerticalTabsSidebar body re-evaluations during scroll, i.e. the migration introduces no observation-invalidation storm in the scroll path; the AttributeGraph work visible in sample during scrolling is normal LazyVStack row realization. No spin loop at idle (2-7% CPU). Conclusion: no reproducible migration-attributable scroll regression; the perceived slowness is most likely Debug-build overhead vs a Release daily driver, plus machine load.

One real follow-up found while auditing: the mechanical @StateObject → @State conversions mean initializer expressions of reference-type view state now run on every parent body evaluation (throwaway instances). For stores with side-effectful inits this is new churn: SidebarTabItemSettingsStore (registers 2 NotificationCenter observers + builds a defaults snapshot; its GhosttyConfig.load() is cached so that part is cheap) and FeedPanelViewModel (registers an observer + arms observation tracking in init). Not in any hot path measured, but worth a follow-up that makes those inits side-effect-free with an idempotent start() from .task.

🤖 Generated with Claude Code

# Conflicts:
#	Sources/TabManager.swift
#	Sources/Workspace.swift
#	Sources/cmuxApp.swift
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Merged latest main (be19996), which includes the AppKit NSTableView sidebar ("Lawrence Sidebar", default ON since #8433). Migration additions in the merge: SidebarLayoutModel (the one ObservableObject main added since the last merge-base) is now @observable with its width-applier wrappers on plain references, preserving the unobserved-holder isolation (ContentView's body still never reads width; the settle pipeline keeps its queue-hopped onReceive via a widthPublisher bridge, since onChange would register the god-body dependency that model exists to avoid). Three merge conflicts resolved by taking main's new members (nativeSSHConnectionBroker on TabManager/Workspace) with parity @ObservationIgnored annotations.

Verified on the tagged build: AppKit sidebar is the active path (sidebar.table.rowsBuild reconciles logged), workspace create bumps the table reconcile (37→41), selection moves via socket, session restore intact, full-tree sweep still shows zero legacy observation constructs.

🤖 Generated with 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift (1)

84-84: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Exclude continuation waiters from the Observation surface.

sessionRevalidationWaiters is internal coordination state, not UI state. Without @ObservationIgnored, every append/remove can invalidate AuthCoordinator observers and trigger unnecessary recomputation. This should match signOutCredentialCaptureWaiters at Line 135.

-    var sessionRevalidationWaiters: [CheckedContinuation<Void, Never>] = []
+    `@ObservationIgnored` var sessionRevalidationWaiters: [CheckedContinuation<Void, Never>] = []
🤖 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
`@Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift`
at line 84, Add `@ObservationIgnored` to the sessionRevalidationWaiters property
in AuthCoordinator, matching the existing annotation on
signOutCredentialCaptureWaiters, so continuation waiter mutations are excluded
from observation.
🤖 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.

Outside diff comments:
In
`@Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift`:
- Line 84: Add `@ObservationIgnored` to the sessionRevalidationWaiters property in
AuthCoordinator, matching the existing annotation on
signOutCredentialCaptureWaiters, so continuation waiter mutations are excluded
from observation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 39e18fb1-7b7d-4d58-8503-2ff013b1f38c

📥 Commits

Reviewing files that changed from the base of the PR and between 9d8f96e and be19996.

📒 Files selected for processing (1)
  • Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Merge-gate status (2026-07-20, spec merge onto main): web-typecheck now passes (main fixed the next/navigation issue), remote-daemon-tests/react-apps-check/web-db-migrations pass, zero review-bot findings. workflow-guard-tests fails from main's own iroh test commit (file identical to origin/main) — filed #8529. The Swift test lanes (tests, tests-build-and-lag, release-build, ui-regressions) still report missing (CI restore incomplete, due 2026-07-21), so the migration-modified unit tests have not yet executed anywhere — treating that as a merge precondition. Branch is current with main (20386bb) and the tagged build is rebuilt on that head.

🤖 Generated with Claude Code

cmux reload-cloud and others added 2 commits July 20, 2026 21:10
An ordinary notification must invalidate observers of the tracked
notificationMenuSnapshot.unreadCount projection the titlebar bell badge
renders from. Red against the current badge read, which uses the
untracked indexes-backed unreadCount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bell badge read TerminalNotificationStore.unreadCount, which is
derived from the @ObservationIgnored indexes cache; after dropping
ObservableObject the view no longer received whole-store invalidations,
so ordinary notification changes never refreshed the badge. Read the
tracked, equality-coalesced notificationMenuSnapshot.unreadCount
instead (same value, refreshed by refreshUnreadPresentation), matching
the popover which already renders from the snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 21, 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.

# Conflicts:
#	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.

Actionable comments posted: 1

🤖 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/Update/UpdateTitlebarAccessory.swift`:
- Around line 239-254: Update NotificationsPopoverVisibilityState so the
sourceLessShown state read by isShown(in:) participates in `@Observable` tracking;
remove `@ObservationIgnored` from sourceLessShown or derive isShown(in:) solely
from already-observed state, while preserving source-less show/hide behavior for
non-nil window numbers.
🪄 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: 6fed9f77-ae35-4113-ac12-39aff6536cc9

📥 Commits

Reviewing files that changed from the base of the PR and between be19996 and 1b548a1.

📒 Files selected for processing (7)
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/TabManager.swift
  • Sources/TerminalNotificationStore.swift
  • Sources/Update/UpdateTitlebarAccessory.swift
  • Sources/Workspace.swift

Comment on lines +239 to +254
@Observable
final class NotificationsPopoverVisibilityState {
static let shared = NotificationsPopoverVisibilityState()

@Published private(set) var isShown = false
@Published private(set) var shownWindowNumbers: Set<Int> = []
private var shownPopoverIDs: Set<ObjectIdentifier> = []
private var shownPopoverWindowNumbers: [ObjectIdentifier: Int] = [:]
private var sourceLessShown = false
private(set) var isShown = false
/// Legacy Combine bridge for the remaining `.$shownWindowNumbers` subscribers. Emits the
/// new value during willSet and replays the current value on subscribe — the
/// exact `Published.Publisher` semantics those call sites were written
/// against. Delete when the subscribers move to @Observable observation.
@ObservationIgnored let shownWindowNumbersPublisher = CurrentValueSubject<Set<Int>, Never>([])
private(set) var shownWindowNumbers: Set<Int> = [] {
willSet { shownWindowNumbersPublisher.send(newValue) }
}
@ObservationIgnored private var shownPopoverIDs: Set<ObjectIdentifier> = []
@ObservationIgnored private var shownPopoverWindowNumbers: [ObjectIdentifier: Int] = [:]
@ObservationIgnored private var sourceLessShown = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect isShown(in:) to confirm it reads tracked state (isShown / shownWindowNumbers).
rg -nP -A15 'func isShown\s*\(in' Sources/Update/UpdateTitlebarAccessory.swift

Repository: manaflow-ai/cmux

Length of output: 888


🏁 Script executed:

#!/bin/bash
# Inspect the visibility state implementation and the callers that use isShown(in:).
sed -n '239,340p' Sources/Update/UpdateTitlebarAccessory.swift
printf '\n--- callers ---\n'
rg -n -A4 -B4 'isShown\(in:' Sources/Update/UpdateTitlebarAccessory.swift

Repository: manaflow-ai/cmux

Length of output: 5439


Keep the sourceLessShown branch observable at Sources/Update/UpdateTitlebarAccessory.swift:272-274
isShown(in:) still reads sourceLessShown, but that flag is @ObservationIgnored. For non-nil window numbers, a source-less show/hide can change the result without invalidating the view, so the titlebar controls can stay stale. Track that state, or derive the result from tracked data only.

🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 240-240: Classes should have an explicit deinit method

(required_deinit)

🤖 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/Update/UpdateTitlebarAccessory.swift` around lines 239 - 254, Update
NotificationsPopoverVisibilityState so the sourceLessShown state read by
isShown(in:) participates in `@Observable` tracking; remove `@ObservationIgnored`
from sourceLessShown or derive isShown(in:) solely from already-observed state,
while preserving source-less show/hide behavior for non-nil window numbers.

…idation

Autoreview flagged the removed objectWillChange as a stale fork-menu
risk; rejected with evidence: both availability consumers pull at
NSMenu construction time, so no invalidation is required. Record the
invariant at the site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Autoreview (canonical helper, codex engine, branch mode vs origin/main) ran twice:

Round 1 found one real migration regression, now fixed with a red/green pair: the titlebar bell badge read TerminalNotificationStore.unreadCount, which derives from the @ObservationIgnored indexes cache, so after dropping ObservableObject the view lost its whole-store invalidation and ordinary notification changes never refreshed the badge. Fix: the badge renders from the tracked, equality-coalesced notificationMenuSnapshot.unreadCount (same value, same refresh hub, matching the popover). Test: testMenuSnapshotUnreadCountInvalidatesWhenOrdinaryNotificationArrives (red on the old read, green on the fix; executes when the CI test lanes restore).

Round 2 on the fixed head found one finding, rejected with evidence: it claimed the removed objectWillChange in the shared-agent-index notification task leaves fork-conversation availability stale at the bonsplit boundary. Both consumers (TabContextMenuBuilder.makeMenu in vendor/bonsplit, GhosttyNSView+ForkConversationContextMenu) evaluate availability at NSMenu construction, i.e. pull-at-open; the old send never updated an already-open AppKit menu and freshly opened menus pull current state, so no behavior changed. Invariant now documented at the site.

CodeRabbit's five failed pre-merge checks reference files absent from this PR's three-dot diff (iroh supervisor/release-gate, sidebar probe kit, device-ID helper, localization keys) — they belong to main's own commits transiting during base merges. The one applicable check ("new Combine bridges") is the sanctioned transitional seam per the dual-observation policy: bridges keep exact Published timing while consumer migration to withObservationTracking is deferred to follow-ups. Merge-conflict gate: clean both rounds. Head: e850334.

🤖 Generated with Claude Code

The @observable macro emits its Observable conformance extension at
file scope; a private class nested inside a test class is inaccessible
there, which broke the cmuxTests build on the first restored CI run
(DetachedWorkspaceTestPanel and five sibling mocks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 21, 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.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Full merge gate is green on the speculative merge (run https://github.com/manaflow-ai/cmux/actions/runs/29866305381): all four app-host unit-test shards, swift-package-tests, tests-build-and-lag, release-build, workflow-guard-tests, and the web/daemon lanes pass. This is the first complete execution of the migration-modified unit tests, including the titlebar-badge regression test, after fixing the @Observable-on-nested-private-mock compile error (5c57c3e). ui-regressions/release-ghostty-cli-helper were path-gated out of the run. GitHub reports the PR MERGEABLE/CLEAN.

E2E AutomationSocketUITests is excluded as evidence in either direction: a controlled A/B showed main fails identically on both runner lanes (no logged-in GUI user) — filed as a lane issue. Depot's 20-minute budget can't fit this branch's cold rebuild; documented, not a blocker.

Merge remains gated only on the owner's dogfood verdict.

🤖 Generated with Claude Code

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
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.

3 participants