Skip to content

Minimal mode: stop the toggle from re-evaluating the window root, sidebar, and Bonsplit trees - #5932

Closed
austinywang wants to merge 13 commits into
mainfrom
issue-5732-minimal-mode-toggle-hang
Closed

austinywang wants to merge 13 commits into
mainfrom
issue-5732-minimal-mode-toggle-hang

Conversation

@austinywang

@austinywang austinywang commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5732

Root cause

Toggling minimal mode ran one giant synchronous AttributeGraph transaction on the main thread. Three compounding causes, all empirically confirmed with a DEBUG probe and Self._logChanges() (external samplers cannot attach on this macOS 26 generation):

  1. The mode was observed by the biggest bodies in the app. workspacePresentationMode was an @AppStorage on ContentView (window root), VerticalTabsSidebar, and every mounted WorkspaceContentView. A toggle invalidated all of them, and because the re-evaluated parents rebuild closure-carrying children (which defeat SwiftUI's implicit diffing), the entire Bonsplit subtree (TabBarView.splitButtons / AGGraphSetOutputValue Sentry frames) and the O(N) sidebar render context (VerticalTabsSidebar.workspaceScrollContent frames) re-evaluated — per window — for a swap that only changes a titlebar band, a top inset, and a small sidebar controls strip.
  2. Dotted-key @AppStorage amplifiers. Keys containing dots (titlebarDebug.… on ContentView, cmuxExtensionSidebar.providerId on the sidebar) break per-key KVO, so SwiftUI falls back to invalidating the holder on every UserDefaults write of any key. _logChanges showed ContentView re-running with the four titlebarDebug properties "changed" when only the mode key was written. This is why the window root and sidebar re-rendered on every defaults write app-wide, not just on toggles.
  3. Workspace.tmuxLayoutSnapshot was @Published and assigned on every bonsplit geometry change. The chrome swap resizes the content, bonsplit reports geometry, the publish re-evaluated the whole mounted workspace's Bonsplit body mid-layout (the "NSHostingView is being laid out reentrantly" warning), 1–2 extra times per toggle — and the same storm fired on every divider drag and window resize. This silently defeated the careful paneLayoutVersion gating right below it whose comment says geometry events must not fire objectWillChange.

Fix — observe the mode (and the dotted keys) on leaf chrome only

  • Sources/MinimalModeChromeSwap.swift (new): small leaf views own the subscriptions and store the heavy subtrees as already-built view values, so a toggle re-runs only the leaves and re-layouts the stored content: MinimalModeTitlebarBandHost (band mount/unmount + the AppKit side effects that used to live in ContentView.onChange), MinimalModeContentTopPaddingBridge (mode-dependent top inset), MinimalModeTitlebarEventSurfaceHost, MinimalModeSafeAreaBridge (per-workspace top safe-area cancellation), and SidebarMinimalModeTitlebarControlsOverlay (one shared construction path for both sidebar variants, replacing two duplicated inline blocks).
  • WorkspaceContentView is Equatable and mounted with .equatable(): window-root re-renders (titlebar text churn, selection changes, appearance) no longer re-evaluate the Bonsplit subtree. == compares workspace identity plus the five render-relevant value inputs; workspace/notificationStore keep invalidating through their own subscriptions.
  • Dotted-key extraction: the four titlebarDebug.… keys move off ContentView into TitlebarDebugChromeSentinel (a zero-size background leaf that reapplies window decorations when the debug insets actually change); the sidebar's provider selection moves behind ExtensionSidebarProviderSelectionModel, which watches UserDefaults.didChangeNotification and publishes only on real provider changes.
  • tmuxLayoutSnapshot is no longer @Published: writes route through storeTmuxLayoutSnapshot, which fires objectWillChange only while a tmux pane-overlay experiment target is active (the only reactive consumer; default off). Divider drags / resizes / chrome swaps no longer re-evaluate mounted Bonsplit bodies.

ContentView no longer observes the mode at all; imperative readers (syncTrafficLightInset, palette context, titlebar-controls hiding) read WorkspacePresentationModeSettings/MinimalModeTitlebarDebugSettings from defaults at call time, and the palette enable/disable commands write UserDefaults directly (same single mutation path as the Settings toggle).

Numbers (same workload, same build pipeline, before/after)

Tagged Debug dev build (cloud builder), M-series MacBook Pro (macOS 26.4.1), single window, 35 workspaces, selected workspace split into 3 panes / 6 surfaces, sidebar visible. Toggles driven through the shared UserDefaults path by the DEBUG set_presentation_mode verb, which logs the main-thread block until the run loop turns again. Raw before-series and methodology: #5732 (comment)

12-toggle series mainBlockedMs bodies re-evaluated per toggle
before mean 117.7 · median 124.4 · max 152.0 ContentView, VerticalTabsSidebar (O(N) render context), WorkspaceContentView ×2 + full Bonsplit subtree re-eval
after mean 96.9 · median 101.7 · max 131.8 contentView=0 sidebar=0 workspaceContent=0 — leaf hosts only (bridge=1 chromeHost=3 tabItem=1)

The wall-clock residual is the genuine relayout: it halves when the selected workspace has 1 pane instead of 3 (~75ms vs ~145ms on a local build), i.e. it is the per-pane Ghostty resize + AppKit constraint pass — the same cost class as any window resize, and it does not scale with workspace/window count anymore. The part the fix removes is exactly the part that scaled with app state (whole-window AttributeGraph recompute per window, O(N) sidebar, full Bonsplit re-eval, and the every-defaults-write amplifiers), which is what turns into the multi-second AGGraphSetOutputValue hang family at production workloads (37 workspaces / 65 surfaces / 30 live agents / 3 windows). The dotted-key and tmuxLayoutSnapshot fixes also remove body-churn storms far beyond the toggle path (every defaults write; every divider drag/resize).

Behavior verified on the tagged build via socket-driven screenshots: standard band/traffic lights, minimal full-bleed content with pane tab bar on top, minimal+hidden-sidebar traffic-light inset on the pane tab bar, sidebar controls strip, splits/workspace switching/surface create+close.

Measurement probe (kept, DEBUG-only)

set_presentation_mode <minimal|standard|toggle> logs mainBlockedMs and setMs (synchronous defaults-observer cost) to the debug event log, so this hang class stays measurable — external samplers (sample, xctrace) cannot read thread state on this macOS generation even with get-task-allow. The per-toggle body counts quoted above came from temporary render-time counters used during development; they were removed before merge per the swiftui-state-layout rule (no state writes from body), so the shipped probe is timing-only.

Coverage

cmuxTests/WorkspaceContentViewEquatableTests.swift pins the contracts the fix depends on: closure-identity churn must compare equal (otherwise .equatable() cannot skip the Bonsplit subtree and the hang returns), every render-relevant input change must compare unequal (otherwise workspaces render stale), and ExtensionSidebarProviderSelectionModel must publish only on real provider changes (otherwise the sidebar re-renders on every defaults write again). This mirrors TaskManagerViewSnapshotBoundaryTests. A two-commit red/green structure is not applicable: the "red" state is a ~120 ms main-thread stall in a live app under load, which unit-test CI cannot observe, and the contract tests cannot compile before the fix because the conformances they pin are part of the fix.

Localization audit

No user-facing strings added or changed: the new views render the existing localized HiddenTitlebarSidebarControlsView/titlebar band unchanged; the only new text is the debug socket help line in the existing unlocalized DEBUG help block. No Resources/Localizable.xcstrings or web/messages/* changes required.

Summary by CodeRabbit

  • Performance Improvements

    • Faster, smoother switching between minimal and standard workspace layouts by localizing mode changes and reducing unnecessary UI invalidations and relayouts.
    • Mode-dependent titlebar/top-padding updates no longer trigger full-window re-layouts.
  • Stability / UX

    • More stable sidebar behavior: provider selection no longer forces unnecessary redraws.
    • Hidden titlebar controls unified for consistent rendering in minimal mode.
  • Tests

    • Added tests validating workspace view equality rules and sidebar provider-change semantics.

…5732)

Test-only socket verb that flips workspacePresentationMode through the same
UserDefaults path as the Settings toggle and the palette commands, then logs
how long the main thread stays blocked before the run loop turns again
(mainBlockedMs), the synchronous defaults-observer cost (setMs), and which
view bodies re-evaluated during the swap (PresentationModeToggleDiagnostics).

External samplers cannot attach on this macOS 26 generation (sample/xctrace
fail to read thread state even with get-task-allow), so this probe is the
measurement instrument for
#5732.

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

vercel Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jun 18, 2026 8:31pm
cmux-staging Building Building Preview, Comment Jun 18, 2026 8:31pm

@austinywang

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

@coderabbitai

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

Localizes minimal-mode and dotted-key debug subscriptions into bridge/host views, makes WorkspaceContentView equatable to gate Bonsplit re-evaluation, centralizes presentation-mode side effects, controls tmux snapshot publishing, and adds tests plus a debug terminal probe.

Changes

Minimal Mode Toggle Re-evaluation Localization

Layer / File(s) Summary
Bridge/host wrapper infrastructure
Sources/MinimalModeChromeSwap.swift, Sources/ContentView.swift
Adds wrappers and a provider-selection model that own minimal-mode and dotted-key debug subscriptions: MinimalModeSafeAreaBridge, MinimalModeTitlebarBandHost, MinimalModeContentTopPaddingBridge, MinimalModeTitlebarEventSurfaceHost, TitlebarDebugChromeSentinel, ExtensionSidebarProviderSelectionModel, and SidebarMinimalModeTitlebarControlsOverlay; consolidates overlay call sites.
WorkspaceContentView Equatable & content padding
Sources/MinimalModeChromeSwap.swift, Sources/WorkspaceContentView.swift, Sources/ContentView.swift
Provides Equatable conformance for WorkspaceContentView, removes per-view @AppStorage presentation-mode state, routes terminal top-padding through MinimalModeContentTopPaddingBridge, and marks the terminal/Bonsplit subtree .equatable() to avoid chrome-only re-evaluation.
Titlebar band and debug chrome routing
Sources/ContentView.swift, Sources/MinimalModeChromeSwap.swift
Wraps titlebar band in MinimalModeTitlebarBandHost(onModeChange:), centralizes presentation-mode side effects in applyPresentationModeChange(), and routes titlebar debug-inset updates via TitlebarDebugChromeSentinel callback instead of window-root onChange.
Sidebar titlebar controls overlay unification
Sources/ContentView.swift, Sources/MinimalModeChromeSwap.swift
Introduces minimalModeTitlebarControlsOverlay and SidebarMinimalModeTitlebarControlsOverlay to consolidate hidden-titlebar-controls rendering and own presentation-mode/debug-inset subscriptions; replaces conditional branches at two overlay sites.
Presentation mode settings and command palette
Sources/ContentView.swift
Command-palette minimal-mode actions now persist via WorkspacePresentationModeSettings.modeKey in UserDefaults, and command-palette contexts derive workspaceMinimalModeEnabled from WorkspacePresentationModeSettings.isMinimal(); setTitlebarControlsHidden uses WorkspacePresentationModeSettings.isMinimal().
Extension provider selection model integration
Sources/MinimalModeChromeSwap.swift, Sources/ContentView.swift
Replaces root @AppStorage reads for extension sidebar provider selection with ExtensionSidebarProviderSelectionModel.shared.providerId observation and computes the effective provider from that value.
Workspace tmux layout snapshot change control
Sources/Workspace.swift
Removes @Published from tmuxLayoutSnapshot and adds storeTmuxLayoutSnapshot(_:) which conditionally emits objectWillChange.send() only when tmux-pane-overlay experiment targets are active; existing write sites updated.
Debug diagnostics and testing infrastructure
Sources/TerminalController+PresentationModeProbe.swift, Sources/TerminalController.swift
Adds a DEBUG-only `set_presentation_mode <minimal
Equatable and provider model test coverage
cmuxTests/WorkspaceContentViewEquatableTests.swift
Adds tests validating WorkspaceContentView Equatable semantics (ignores closure identity, detects render-relevant inputs) and that ExtensionSidebarProviderSelectionModel only publishes on real provider-id changes.
Project build configuration updates
cmux.xcodeproj/project.pbxproj
Registers MinimalModeChromeSwap.swift and TerminalController+PresentationModeProbe.swift in the main target and WorkspaceContentViewEquatableTests.swift in the test target; updates groups and sources build phases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5062: Touches minimal-mode titlebar controls/layout and may overlap padding/position changes introduced here.
  • manaflow-ai/cmux#4481: Related changes to titlebar accessory/controls overlay positioning and metrics used for alignment.
  • manaflow-ai/cmux#3150: Also modifies hidden titlebar/sidebar controls wiring and minimal-mode visibility logic.

Poem

🐰 A little bridge, a sentinel bright,

chrome swaps softened, main thread light.
Equatable roots hush Bonsplit's shout,
snapshots whisper what changed about.
Hop, tests pass—debug carrots out.


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error PR adds new state owner via singleton ExtensionSidebarProviderSelectionModel.shared and listens with NotificationCenter.default.addObserver for UserDefaults.didChangeNotification, which the r... Refactor to avoid a new singleton/NotificationCenter observer for providerId: inject an existing coordinator/model, or make a non-singleton view-owned model with explicit ownership/invariants, ideally driven by per-key safe observation r...
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main objective: preventing minimal-mode toggles from re-evaluating major SwiftUI components (window root, sidebar, Bonsplit trees).
Description check ✅ Passed The description comprehensively covers root cause analysis, the fix approach with architectural changes, performance metrics (before/after timing), behavior verification, debug probe implementation, test coverage, and localization audit.
Linked Issues check ✅ Passed All requirements from issue #5732 are addressed: preventing full-window AttributeGraph recomputes (via leaf-host architecture), avoiding heavy subtree re-evaluations (via Equatable/.equatable()), eliminating dotted-key amplification (via dedicated models), stopping geometry-driven publishes (via conditional objectWillChange), and confining work to lightweight leaves.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the minimal-mode toggle hang: chrome architecture refactoring, Equatable conformance for content views, dotted-key extraction, snapshot publishing control, and related tests/measurement probes. No unrelated modifications present.
Cmux Swift Actor Isolation ✅ Passed New production code mainly adds SwiftUI view wrappers plus @MainActor @Observable ExtensionSidebarProviderSelectionModel; observed updates are main-actor confined (queue .main, MainActor.assumeIsol...
Cmux Swift Blocking Runtime ✅ Passed Verified new files: TerminalController+PresentationModeProbe.swift is #if DEBUG and uses CACurrentMediaTime + Task { @MainActor } logging (no sleep/lock/wait); MinimalModeChromeSwap.swift contains...
Cmux Expensive Synchronous Load ✅ Passed No RestorableAgentSessionIndex.load/sysctl-style sync loaders added in PR’s new/modified minimal-mode/probe views; existing heavy loading remains in SharedLiveAgentIndex Task.detached + nil-guarded...
Cmux Cache Substitution Correctness ✅ Passed tmuxLayoutSnapshot is updated from fresh LayoutSnapshot via storeTmuxLayoutSnapshot (fresh, with overlay-only objectWillChange); providerId is cold-seeded from UserDefaults and refreshed only on re...
Cmux No Hacky Sleeps ✅ Passed PR #5932 only changes Swift, pbxproj, and a .tsv in .github; no TS/JS/shell/build/runtime scripts were touched, so no hacky-sleep rule violation to report.
Cmux Algorithmic Complexity ✅ Passed New code avoids scalable scans: MinimalModeChromeSwap equatable compares fixed fields; ExtensionSidebarProviderSelectionModel refreshFromDefaults reads a single UserDefaults key; Workspace storeTmu...
Cmux Swift Concurrency ✅ Passed PR adds only DEBUG-only Task { @mainactor in ... } for deferred logging; no new DispatchQueue/Combine/app-state publishers or completion-handler async APIs in changed cmux code.
Cmux Swift @Concurrent ✅ Passed Repo search found zero @concurrent and zero nonisolated async in the PR’s Swift files (and none in Sources/). New code uses UI-bound Task { @mainactor ... } only for logging.
Cmux Swift File And Package Boundaries ✅ Passed Sources/MinimalModeChromeSwap.swift is 321 lines (<400) and contains only cmux UI/AppKit + @AppStorage/UserDefaults glue (no networking/parsing/protocol). DEBUG probe file is 47 lines; test is in c...
Cmux Swift Logging ✅ Passed Scanned PR-related Swift files for print/debugPrint/dump/NSLog/Logger; found only #if DEBUG-guarded NSLog and a #if DEBUG cmuxDebugLog probe. No non-debug forbidden logging or file-scoped Logger co...
Cmux User-Facing Error Privacy ✅ Passed user-facing error/privacy rule disallows leaking internal/provider details. The only new user-facing copy (set_presentation_mode help + “ERROR: Usage…”) is wrapped in #if DEBUG and is generic (mini...
Cmux Full Internationalization ✅ Passed PR diff shows no new SwiftUI/UI Text or localized-string violations; added user-facing strings appear only in #if DEBUG terminal help or debug/test assertions, and no web/messages or xcstrings/Info...
Cmux Swiftui State Layout ✅ Passed New SwiftUI chrome code adds only @Observable (ExtensionSidebarProviderSelectionModel); MinimalModeChromeSwap.swift has 0 @Published/ObservableObject/GeometryReader and no render-time @State/@appst...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Inspected PR-touched Swift files: no new Window/WindowGroup/NSWindowController/NSPanel created or modified for close-shortcut routing; MinimalModeChromeSwap only uses NSWindow as an optional type....
Cmux Source Artifacts ✅ Passed PR #5932 changed only code/config files (.swift, .pbxproj, test .swift, and .github/swift-file-length-budget.tsv); no logs, screenshots, DerivedData, or broad artifact dirs appear.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5732-minimal-mode-toggle-hang

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR surgically fixes the minimal-mode toggle hang by moving every presentation-mode @AppStorage subscription off the large window-root and sidebar bodies and onto small leaf views, so a toggle re-runs only those leaves. It also eliminates two compounding amplifiers: dotted-key @AppStorage entries (which caused SwiftUI to invalidate their holder on every UserDefaults write) are moved to zero-size sentinel leaves, and tmuxLayoutSnapshot is de-published so divider drags and chrome relayouts no longer re-evaluate the entire Bonsplit subtree mid-layout.

  • Leaf-based subscription split: five new small SwiftUI views (MinimalModeTitlebarBandHost, MinimalModeContentTopPaddingBridge, MinimalModeSafeAreaBridge, MinimalModeTitlebarEventSurfaceHost, SidebarMinimalModeTitlebarControlsOverlay) each own the mode @AppStorage; ContentView and VerticalTabsSidebar no longer observe the mode at all.
  • WorkspaceContentView is Equatable + .equatable(): window-root re-renders (titlebar churn, selection) skip the Bonsplit subtree when the five render-relevant inputs are unchanged; closures and observed stores continue to invalidate through their own subscriptions.
  • tmuxLayoutSnapshot de-published + storeTmuxLayoutSnapshot: objectWillChange fires only while the tmux pane-overlay experiment is active, eliminating the O(workspaces) Bonsplit re-evaluation storm on every divider drag, window resize, and chrome swap.

Confidence Score: 5/5

The change is safe to merge; all new code paths are well-scoped leaf views or DEBUG-only probes, behavior is verified by the new equatable contract tests, and the architectural invariants are clearly documented.

The mode-subscription refactor is narrow and mechanical — each new leaf view owns exactly one @AppStorage key, the Equatable conformance has a safe fallback for the off-main case, and the tmuxLayoutSnapshot change is gated behind a feature flag check. The only non-trivial concern is that the nonisolated == operator relies on a runtime thread check rather than compile-time isolation guarantees, but it cannot cause stale renders (the fallback is an extra render, not a skipped one), and the contract tests cover every render-relevant input.

Sources/WorkspaceContentView+Equatable.swift — the nonisolated == pattern is the one place where actor isolation is enforced at runtime rather than compile time; worth revisiting if the stored properties can be declared nonisolated.

Important Files Changed

Filename Overview
Sources/WorkspaceContentView+Equatable.swift New Equatable conformance for .equatable() skip optimization; nonisolated == uses MainActor.assumeIsolated guarded by Thread.isMainThread — safe fallback present but relies on undocumented SwiftUI-on-main guarantee
Sources/ExtensionSidebarProviderSelectionModel.swift New @observable model correctly uses @mainactor + @observable; filters UserDefaults.didChangeNotification to mutate only on real provider changes; handles main/non-main delivery paths
Sources/MinimalModeTitlebarBandHost.swift New leaf host that owns the presentation-mode @AppStorage subscription and fires the onModeChange callback for AppKit side effects; cleanly isolates the mode toggle from ContentView
Sources/Workspace.swift tmuxLayoutSnapshot de-published; storeTmuxLayoutSnapshot conditionally fires objectWillChange only when the overlay experiment is active, eliminating mid-layout Bonsplit re-evaluation on divider drags and chrome swaps
Sources/ContentView.swift Removes all direct presentation-mode and dotted-key @AppStorage subscriptions from the window root; mode-dependent logic delegated to leaf hosts; imperative readers use WorkspacePresentationModeSettings.isMinimal()
Sources/TitlebarDebugChromeSentinel.swift New zero-size background leaf that holds the four dotted-key titlebarDebug @AppStorage subscriptions; correctly isolated to prevent ContentView invalidation on every UserDefaults write
cmuxTests/WorkspaceContentViewEquatableTests.swift New test suite pinning the three contracts the optimization depends on: closure identity is ignored, every render-relevant input is detected, provider-selection model mutates only on real provider changes

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph After["After this PR"]
        X[UserDefaults modeKey write] --> L1[MinimalModeTitlebarBandHost]
        X --> L2[MinimalModeContentTopPaddingBridge]
        X --> L3[MinimalModeSafeAreaBridge]
        X --> L4[MinimalModeTitlebarEventSurfaceHost]
        X --> L5[SidebarMinimalModeTitlebarControlsOverlay]
        L1 -->|onModeChange| AppKit[AppKit side effects]
        L2 -->|re-layout only| Skip1[Content subtree: no body re-eval]
        L3 -->|re-layout only| Skip2[Bonsplit subtree: no body re-eval]
        DotKey[dotted-key defaults write] --> Sentinel[TitlebarDebugChromeSentinel leaf]
        DotKey --> Model[ExtensionSidebarProviderSelectionModel]
        Model -->|only on real change| Sidebar[VerticalTabsSidebar re-renders]
    end
    subgraph Before["Before #5732"]
        A[UserDefaults modeKey write] --> B[ContentView full re-eval]
        A --> C[VerticalTabsSidebar full re-eval]
        A --> D[WorkspaceContentView re-eval x N]
        B --> E[Bonsplit subtree re-eval per window]
        C --> F[O-N sidebar render context]
        D --> G[Full Bonsplit re-eval per workspace]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    subgraph After["After this PR"]
        X[UserDefaults modeKey write] --> L1[MinimalModeTitlebarBandHost]
        X --> L2[MinimalModeContentTopPaddingBridge]
        X --> L3[MinimalModeSafeAreaBridge]
        X --> L4[MinimalModeTitlebarEventSurfaceHost]
        X --> L5[SidebarMinimalModeTitlebarControlsOverlay]
        L1 -->|onModeChange| AppKit[AppKit side effects]
        L2 -->|re-layout only| Skip1[Content subtree: no body re-eval]
        L3 -->|re-layout only| Skip2[Bonsplit subtree: no body re-eval]
        DotKey[dotted-key defaults write] --> Sentinel[TitlebarDebugChromeSentinel leaf]
        DotKey --> Model[ExtensionSidebarProviderSelectionModel]
        Model -->|only on real change| Sidebar[VerticalTabsSidebar re-renders]
    end
    subgraph Before["Before #5732"]
        A[UserDefaults modeKey write] --> B[ContentView full re-eval]
        A --> C[VerticalTabsSidebar full re-eval]
        A --> D[WorkspaceContentView re-eval x N]
        B --> E[Bonsplit subtree re-eval per window]
        C --> F[O-N sidebar render context]
        D --> G[Full Bonsplit re-eval per workspace]
    end
Loading

Reviews (12): Last reviewed commit: "fix: use presentation mode snapshot in f..." | Re-trigger Greptile

@austinywang
austinywang force-pushed the issue-5732-minimal-mode-toggle-hang branch from ccaca32 to 1ac08d0 Compare June 12, 2026 01:59

@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/Workspace.swift`:
- Around line 19962-19971: Extract the tmux snapshot publish logic from
Sources/Workspace.swift into a new extension file named
Workspace+TmuxLayoutSnapshot.swift: move the private func
storeTmuxLayoutSnapshot(_ snapshot: LayoutSnapshot?) implementation (including
the comment) into an extension Workspace { ... } in that file, retaining the
same access level and references to tmuxLayoutSnapshot,
TmuxOverlayExperimentSettings, and objectWillChange so compilation and behavior
are unchanged; remove the original method from Sources/Workspace.swift and add
the new file to the target so the project builds.
🪄 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: 052199ba-51d4-4a87-bd5c-38e296155e2a

📥 Commits

Reviewing files that changed from the base of the PR and between 46a4143 and ccaca32.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (6)
  • Sources/ContentView.swift
  • Sources/MinimalModeChromeSwap.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceContentView.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceContentViewEquatableTests.swift

Comment thread Sources/Workspace.swift
Toggling minimal mode hung the main thread in one giant synchronous
AttributeGraph transaction: the mode was observed via @AppStorage on three
large bodies (ContentView, VerticalTabsSidebar, every mounted
WorkspaceContentView), and the re-evaluated parents rebuilt closure-carrying
children, so the whole Bonsplit tree and the O(N) sidebar re-rendered for a
chrome swap that only changes a titlebar band, a top inset, and a small
sidebar controls strip.

Verified with the set_presentation_mode probe (35 workspaces, 3-pane
selected workspace): per-toggle body counts go from
{contentView=1 sidebar=1 workspaceContent=2 + full Bonsplit re-eval} to
{contentView=0 sidebar=0 workspaceContent=0} with only leaf hosts re-running;
the remaining main-thread block is the genuine per-pane terminal relayout.

- MinimalModeChromeSwap.swift: leaf views that own the mode subscription —
  MinimalModeTitlebarBandHost (band mount/unmount + AppKit side effects),
  MinimalModeContentTopPaddingBridge (content top inset),
  MinimalModeTitlebarEventSurfaceHost, MinimalModeSafeAreaBridge (per-
  workspace safe-area cancellation), SidebarMinimalModeTitlebarControlsOverlay
  (one shared construction path for both sidebar variants).
- WorkspaceContentView is Equatable and mounted with .equatable(): window-
  root re-renders no longer re-evaluate the Bonsplit subtree; workspace and
  notification stores keep their own subscriptions.
- Dotted-key @AppStorage amplifiers removed (dots break per-key KVO, so
  SwiftUI invalidated the holder on EVERY UserDefaults write, empirically
  confirmed via Self._logChanges): the four titlebarDebug.* keys move off
  ContentView into TitlebarDebugChromeSentinel, and the sidebar's
  cmuxExtensionSidebar.providerId moves behind
  ExtensionSidebarProviderSelectionModel, which publishes only on real
  provider changes.
- Workspace.tmuxLayoutSnapshot is no longer @published: bonsplit reports
  geometry on every divider drag/resize/chrome relayout and each publish
  re-evaluated the whole mounted Bonsplit body mid-layout. Writes route
  through storeTmuxLayoutSnapshot, which still fires objectWillChange while
  a tmux pane-overlay experiment target is active (the only reactive
  consumer).

cmuxTests/WorkspaceContentViewEquatableTests.swift pins the diffing
contracts: closure-identity churn compares equal (so .equatable() can skip
the Bonsplit subtree), every render-relevant input change compares unequal,
and the provider-selection model publishes only on real changes.

Fixes #5732

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@austinywang
austinywang force-pushed the issue-5732-minimal-mode-toggle-hang branch from 1ac08d0 to 5d3ad50 Compare June 12, 2026 02:03
austinywang and others added 2 commits June 11, 2026 19:13
Take main's entries and re-sync the four files this branch touches
(TerminalController, Workspace, ContentView, WorkspaceContentView) to the
merged tree's actual line counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SwiftUI re-exports Observation on macOS 14+, but the explicit import keeps
the @observable model and withObservationTracking call sites self-contained
(autoreview P1).

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

@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)
cmuxTests/WorkspaceContentViewEquatableTests.swift (1)

1-131: ⚠️ Potential issue | 🔴 Critical

Wire WorkspaceContentViewEquatableTests.swift into the cmuxTests target sources

cmux.xcodeproj/project.pbxproj contains a PBXFileReference for WorkspaceContentViewEquatableTests.swift (A5732006), but no corresponding PBXBuildFile entry was found—so it isn’t added to the cmuxTests target’s PBXSourcesBuildPhase. This will cause the test to be silently ignored by Xcode/CI; add the missing PBXBuildFile and include it in the cmuxTests sources phase.

🤖 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/WorkspaceContentViewEquatableTests.swift` around lines 1 - 131, The
test file WorkspaceContentViewEquatableTests.swift (PBXFileReference A5732006)
is present in the project but not wired into the cmuxTests target sources; add a
PBXBuildFile entry for A5732006 in the project.pbxproj and include that
PBXBuildFile in the cmuxTests PBXSourcesBuildPhase so the file is compiled into
the cmuxTests target (also verify target membership in Xcode UI if needed).
Ensure the new PBXBuildFile references the existing PBXFileReference A5732006
and that the cmuxTests target’s sources phase contains that build file entry.

Source: Coding guidelines

🤖 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/WorkspaceContentViewEquatableTests.swift`:
- Around line 1-131: The test file WorkspaceContentViewEquatableTests.swift
(PBXFileReference A5732006) is present in the project but not wired into the
cmuxTests target sources; add a PBXBuildFile entry for A5732006 in the
project.pbxproj and include that PBXBuildFile in the cmuxTests
PBXSourcesBuildPhase so the file is compiled into the cmuxTests target (also
verify target membership in Xcode UI if needed). Ensure the new PBXBuildFile
references the existing PBXFileReference A5732006 and that the cmuxTests
target’s sources phase contains that build file entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 194b4cb7-7834-46fc-a86a-da11825a8fa8

📥 Commits

Reviewing files that changed from the base of the PR and between aa95f6c and 4e37614.

📒 Files selected for processing (2)
  • Sources/MinimalModeChromeSwap.swift
  • cmuxTests/WorkspaceContentViewEquatableTests.swift

…elivery synchronous on main; Swift Testing

Addresses autoreview findings:
- MinimalModeChromeSwap.swift bundled several major types; split into
  single-type files (safe-area bridge, band host, padding bridge, event
  surface host, debug sentinel, provider-selection model, sidebar overlay,
  WorkspaceContentView+Equatable) per the one-major-type-per-file rule.
- ExtensionSidebarProviderSelectionModel now observes defaults with
  queue: nil, so main-thread writes (every in-app mutation path) refresh
  synchronously in the same turn — and the model test is deterministic
  instead of racing an operation-queue delivery.
- New non-UI coverage converted from XCTest to Swift Testing per the cmux
  testing policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ontroller split landed)

Take main's entries and re-sync this branch's touched files to the merged
tree's actual line counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 0b589a87 Deployed Jun 18, 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.

Minimal mode: toggling minimal mode hangs the main thread (large synchronous SwiftUI relayout)

3 participants