Skip to content

Add a right-sidebar toggle button to the trailing edge of the title bar - #5988

Open
lidge-jun wants to merge 6 commits into
manaflow-ai:mainfrom
lidge-jun:right-sidebar-titlebar-toggle
Open

lidge-jun wants to merge 6 commits into
manaflow-ai:mainfrom
lidge-jun:right-sidebar-titlebar-toggle

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The left titlebar cluster has a sidebar toggle button, but the right sidebar (Files / Find / Sessions / Feed / Dock) has no chrome affordance once it's closed — its header close button disappears with it, so the only way to reopen it is the keyboard shortcut. Mouse-first users lose discoverability of the whole Files panel.

Change

A single trailing-edge titlebar accessory with a sidebar.right toggle button:

  • RightSidebarToggleAccessoryViewController attached at layoutAttribute = .right, living in UpdateTitlebarAccessory.swift next to the existing controls cluster (reuses TitlebarControlButton, TitlebarControlIconStyle, NonDraggableHostingView — all file-internal).
  • Follows the titlebarControlsStyle setting (classic/compact/roomy/pill/soft) via @AppStorage and resizes when the style changes.
  • Shared action path: toggles through AppDelegate.toggleRightSidebarInActiveMainWindow(preferredWindow:), the same path as the toggleRightSidebar keyboard shortcut (Cmd+Opt+B by default); the tooltip renders the currently bound shortcut via KeyboardShortcutSettings.Action.toggleRightSidebar.tooltip.
  • Hidden in minimal mode and fullscreen together with the existing controls cluster, and removed through the same accessory lifecycle (applyAccessoryVisibility / removeAccessoryIfPresent).

Localization audit

Two new user-facing strings (titlebar.rightSidebar.accessibilityLabel, titlebar.rightSidebar.tooltip) added to Resources/Localizable.xcstrings with translations for all 19 supported locales, phrased consistently with the existing titlebar.sidebar.* entries. The xcstrings edit is insertion-only (238 added lines, no reformat).

Verification

  • accessibilityIdentifier: titlebarControl.toggleRightSidebar (matches the titlebarControl.* convention for UI tests).
  • Manual steps: launch → button appears at the right end of the title bar → click toggles the right sidebar both ways → switch titlebar controls style in the debug styles menu and confirm the button matches → minimal mode / fullscreen hides it.

⚠️ Authored without a local Xcode toolchain (syntax-checked only) — relying on CI for build verification.

🤖 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

Adds a right-sidebar toggle button on the title bar’s trailing edge to restore mouse discoverability for Files/Find/Sessions/Feed/Dock. It mirrors toggleRightSidebar (Cmd+Opt+B), aligns with the controls row, and hides in minimal, fullscreen, and while the right sidebar is open.

Shares the toggleRightSidebar action path; tooltip shows the bound shortcut. Follows titlebarControlsStyle and resizes on change via KVO. Centers on the traffic lights row. Accessory is file‑private. Container passes clicks through outside the button. Binds to sidebar visibility with retries during session restore, and rebinds when the window context replaces FileExplorerState, so the toggle yields to the sidebar’s X without stacking. Localized accessibility label + tooltip for 19 locales. UI test id: titlebarControl.toggleRightSidebar.

Written for commit 873d992. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a right‑sidebar toggle button in the titlebar for quick show/hide of the right sidebar; it auto-hides in fullscreen/minimal modes and respects titlebar style preferences and current sidebar visibility.
  • Localization

    • Added accessibility label and tooltip translations for the right‑sidebar titlebar control across supported languages.

The left titlebar cluster has a sidebar toggle, but once the right
sidebar is closed nothing in the window chrome reopens it — the only
way back is the keyboard shortcut.

- New RightSidebarToggleAccessoryViewController attached at
  layoutAttribute .right, reusing TitlebarControlButton and the
  titlebar controls style (the button follows the
  titlebarControlsStyle setting and resizes on change).
- Toggles via the same AppDelegate.toggleRightSidebarInActiveMainWindow
  path as the Cmd+Opt+B shortcut; tooltip shows the bound shortcut.
- Hidden in minimal mode and fullscreen alongside the existing controls
  cluster, and removed through the same accessory lifecycle.
- Localized accessibility label + tooltip added for all 19 supported
  locales in Localizable.xcstrings.

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

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

@lidge-jun is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented 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

Adds a localized right-sidebar toggle button to the titlebar: a SwiftUI-hosted accessory that toggles the right sidebar, plus wiring to attach, hide/show, and remove the accessory in the titlebar accessory lifecycle.

Changes

Right Sidebar Toggle Accessory

Layer / File(s) Summary
Localization for right sidebar toggle accessory
Resources/Localizable.xcstrings
Adds titlebar.rightSidebar.accessibilityLabel and titlebar.rightSidebar.tooltip localization keys with translations across multiple locales and extractionState: "manual".
Integration into titlebar lifecycle
Sources/Update/UpdateTitlebarAccessory.swift
Adds rightSidebarToggleAccessoryIdentifier, attaches RightSidebarToggleAccessoryViewController to windows that lack it, includes it in the minimal/fullscreen hide-show predicate, and includes its identifier when filtering/removing accessories.
Right-sidebar accessory implementation
Sources/Update/UpdateTitlebarAccessory.swift
Implements a trailing TitlebarControlButton hosted in a NonDraggableHostingView; button toggles the right sidebar via AppDelegate.shared?.toggleRightSidebarInActiveMainWindow(...), sizes from hostingView.fittingSize, reacts to titlebarControlsStyle KVO, and hides when FileExplorerState.$isVisible is true.

Sequence Diagram

sequenceDiagram
  participant TitlebarAccessoryController
  participant RightSidebarToggleAccessoryViewController
  participant AppDelegate
  participant Window

  TitlebarAccessoryController->>RightSidebarToggleAccessoryViewController: attach accessory to Window
  RightSidebarToggleAccessoryViewController->>Window: present hosted SwiftUI button
  RightSidebarToggleAccessoryViewController->>AppDelegate: toggleRightSidebarInActiveMainWindow(preferredWindow:)
  TitlebarAccessoryController->>RightSidebarToggleAccessoryViewController: hide/show based on minimal/fullscreen state
  TitlebarAccessoryController->>RightSidebarToggleAccessoryViewController: remove accessory when filtering identifiers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#5059: Adjusts titlebar accessory/button layout and positioning logic used by the new right-sidebar toggle.
  • manaflow-ai/cmux#5062: Modifies titlebar control layout and minimal-mode alignment adjacent to the new toggle’s hide/show behavior.
  • manaflow-ai/cmux#5102: Fixes titlebar hit-testing and click behavior that intersects with the right-sidebar toggle’s click pass-through handling.

Poem

🐰 A tiny toggle springs to the light,
Centered by lights and tucked in tight,
A tap sends a whisper to open the side,
Localized flutters for every guide,
The rabbit hops—right panels glide!


Important

Pre-merge checks failed

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

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error UpdateTitlebarAccessory.swift contains timing/polling sync via DispatchQueue.main.asyncAfter retries for sidebar visibility binding and attachment (including pendingAttachRetries up to 40). Replace the asyncAfter-based retry/polling with an event-driven readiness signal (publisher/callback/notification) for when window/fileExplorerState context becomes available, avoiding delayed-main-queue coordination in production.
Cmux Algorithmic Complexity ❌ Error UpdateTitlebarAccessory.swift: attachToExistingWindows() loops NSApp.windows and attachIfNeeded() calls NSApp.windows.contains(where:) per window (nested full scan). Remove the per-window NSApp.windows.contains(where:) guard (outer loop already iterates the snapshot) or replace with O(1) membership via Set/Set-like lookup.
Cmux Swift Concurrency ❌ Error UpdateTitlebarAccessory.swift adds a new Combine subscription to FileExplorerState.$isVisible via AnyCancellable/sink to hide the right-sidebar toggle. Replace the new FileExplorerState.$isVisible Combine sink (AnyCancellable/.sink) with an Observation/async-await-based watcher tied to the accessory controller lifecycle.
Cmux Architecture Rethink ❌ Error PR adds delayed retry polling for right-sidebar visibility binding: scheduleBindRetryIfNeeded uses DispatchQueue.main.asyncAfter(.now()+0.25) with bindRetriesRemaining=40 when window/context isn’t... Replace the asyncAfter/retry-counter binding with a deterministic readiness signal for the window’s FileExplorerState (single source of truth), so the toggle attaches/binds immediately without scheduled backoff.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.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 accurately and concisely describes the main change: adding a right-sidebar toggle button to the titlebar's trailing edge.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PR introduces new @MainActor-annotated RightSidebarToggleAccessoryViewController and SwiftUI RightSidebarToggleTitlebarView; no implicit MainActor models, mutable Sendable types without isolation,...
Cmux Expensive Synchronous Load ✅ Passed UpdateTitlebarAccessory.swift adds a right-sidebar titlebar toggle, but it does not introduce any RestorableAgentSessionIndex.load or other expensive sync loader on main/interactive paths (only tog...
Cmux Cache Substitution Correctness ✅ Passed UpdateTitlebarAccessory binds right-sidebar visibility via event-driven $isVisible with a bounded cold-context retry (scheduleBindRetryIfNeeded), and the PR doesn’t introduce cache substitution in...
Cmux No Hacky Sleeps ✅ Passed Non-Swift diff only deletes web/app/api/stripe/founders-welcome/route.ts and removes comments in web/app/env.ts; no sleeps/setTimeout/setInterval/timers/polling introduced.
Cmux Swift @Concurrent ✅ Passed No Swift concurrency-annotation issues found: UpdateTitlebarAccessory.swift contains 0 @concurrent, 0 nonisolated async, and 0 await; Tasks are @MainActor UI updates.
Cmux Swift File And Package Boundaries ✅ Passed PASS: PR only extends existing oversized Sources/Update/UpdateTitlebarAccessory.swift (~3435 lines); added right-sidebar toggle section is UI/AppKit glue (no networking/parsing) and grows from 3202...
Cmux Swift Logging ✅ Passed In Sources/Update/UpdateTitlebarAccessory.swift, there’s no print/debugPrint/dump/NSLog or Logger usage; the only new logging is cmuxDebugLog wrapped in #if DEBUG.
Cmux User-Facing Error Privacy ✅ Passed Changes add only right-sidebar toggle accessibilityLabel/tooltip and titlebar toggle UI code; no production user-facing errors/alerts/command/API/recovery text or sensitive vendor/provider details...
Cmux Full Internationalization ✅ Passed New titlebar right-sidebar strings are localized via String(localized:defaultValue:), and Resources/Localizable.xcstrings contains both keys with complete 19-locale translations (extractionState=ma...
Cmux Swiftui State Layout ✅ Passed Added right-sidebar toggle SwiftUI view uses @AppStorage only and introduces no new ObservableObject/@published, GeometryReader measurement, lazy/list store-ref rows, or render-time state mutation;...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed UpdateTitlebarAccessory.swift adds only a titlebar trailing toggle (no NSWindow/NSPanel/WindowGroup creation). Repo-wide check finds 0 cmux window identifier assignments missing from cmuxAuxiliaryW...
Cmux Source Artifacts ✅ Passed PR #5988 changes only Resources/Localizable.xcstrings and Sources/Update/UpdateTitlebarAccessory.swift; the repo’s source-control-artifacts rules allow localization catalogs and these are normal so...
Description check ✅ Passed The pull request description comprehensively addresses the template requirements with detailed problem statement, change explanation, testing approach, and verification steps.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a trailing-edge titlebar button that toggles the right sidebar via the same toggleRightSidebarInActiveMainWindow path as the keyboard shortcut, hiding in minimal/fullscreen modes and when the sidebar is open. Both new user-facing strings are localized across all 19 supported locales.

  • RightSidebarToggleAccessoryViewController is correctly private, uses KVO on the single titlebarControlsStyle key (not the broad UserDefaults.didChangeNotification), and the container passes clicks through to underlying chrome.
  • Sidebar visibility binding uses a DispatchQueue.main.asyncAfter polling loop (40 retries × 0.25 s) to handle session restore races, which leaves the button incorrectly visible for up to 10 s when the sidebar is already open at launch.

Confidence Score: 4/5

The new button and its visibility/hide logic are sound; one issue in the session-restore path leaves the toggle button incorrectly visible until polling resolves.

The session-restore binding uses a 40-iteration asyncAfter(0.25 s) polling loop as its only synchronization mechanism. When the right sidebar is already open at launch, setToggleHidden(true) is delayed for as long as it takes the fileExplorerState context to register — visibly incorrect titlebar state for up to 10 seconds on a slow restore path. All other mechanics (KVO-only resize, click-passthrough container, fullscreen/minimal hiding, localization) look correct.

Sources/Update/UpdateTitlebarAccessory.swift — specifically scheduleBindRetryIfNeeded and the bindSidebarVisibilityIfNeeded retry path.

Important Files Changed

Filename Overview
Sources/Update/UpdateTitlebarAccessory.swift Adds RightSidebarToggleAccessoryViewController with correct file-private scope, KVO-only style resize, and click-passthrough container; the scheduleBindRetryIfNeeded helper uses a 40×0.25 s asyncAfter polling loop for session-restore synchronization instead of a real signal.
Resources/Localizable.xcstrings Insertion-only: adds titlebar.rightSidebar.accessibilityLabel and titlebar.rightSidebar.tooltip with all 19 supported locale translations, consistent with existing titlebar.sidebar.* entries.

Sequence Diagram

sequenceDiagram
    participant W as NSWindow
    participant C as UpdateTitlebarAccessoryController
    participant VC as RightSidebarToggleAccessoryViewController
    participant AD as AppDelegate
    participant FE as FileExplorerState

    W->>C: attach(to:) / viewDidAppear
    C->>VC: "addTitlebarAccessoryViewController (layoutAttribute = .right)"
    VC->>VC: applyLayout()
    VC->>VC: bindSidebarVisibilityIfNeeded()
    alt context ready
        VC->>AD: contextForMainTerminalWindow(window)
        AD-->>VC: fileExplorerState
        VC->>FE: subscribe $isVisible
        FE-->>VC: "sidebarVisible -> setToggleHidden(sidebarVisible)"
    else context not ready (session restore)
        VC->>VC: scheduleBindRetryIfNeeded() asyncAfter 0.25s x 40
        note over VC: button may be incorrectly visible for up to 10 s
        VC-->>VC: retry bindSidebarVisibilityIfNeeded()
    end
    W->>C: applyAccessoryVisibility (minimal/fullscreen)
    C->>VC: "isHidden = true"
    W->>AD: user clicks button
    AD->>AD: toggleRightSidebarInActiveMainWindow(preferredWindow:)
Loading

Reviews (6): Last reviewed commit: "Rebind sidebar-visibility when the windo..." | Re-trigger Greptile

Comment on lines +3258 to +3259
@MainActor
final class RightSidebarToggleAccessoryViewController: NSTitlebarAccessoryViewController {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 RightSidebarToggleAccessoryViewController is declared internal (no access modifier), making it visible to the whole module despite being a file-private implementation detail. RightSidebarToggleTitlebarView correctly uses private; the view controller should too.

Suggested change
@MainActor
final class RightSidebarToggleAccessoryViewController: NSTitlebarAccessoryViewController {
@MainActor
private final class RightSidebarToggleAccessoryViewController: NSTitlebarAccessoryViewController {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3c3f0b8 — the view controller is now private, matching the view.

Comment on lines +3290 to +3298
userDefaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.applyFittingSize()
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Overly-broad UserDefaults.didChangeNotification observer

Every UserDefaults write across the entire app — workspace settings, history entries, search state, etc. — fires this observer and enqueues a new Task { @MainActor } for applyFittingSize(). The guard in applyFittingSize() prevents actual frame changes when the size hasn't changed, but the task allocation and main-actor dispatch still happen for every write. The SwiftUI view already has @AppStorage("titlebarControlsStyle") which triggers a body re-render (and updates fittingSize) on exactly the right key. An onChange modifier on that view feeding back through the onToggle-style closure pattern, or KVO on the specific defaults key, would limit the resize work to the one key that actually changes the button's geometry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3c3f0b8 — replaced the broad UserDefaults.didChangeNotification observer with KVO on the titlebarControlsStyle key, so it only fires when the style actually changes.

…only its style key

Review follow-up: the accessory class needs no module-wide visibility,
and the broad UserDefaults.didChangeNotification observer ran
fittingSize on every defaults write app-wide. KVO on the
titlebarControlsStyle key fires only when the style actually changes.

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)
Sources/Update/UpdateTitlebarAccessory.swift (1)

3259-3332: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider modernizing the KVO implementation.

The old-style KVO pattern (lines 3292–3298, 3311–3324) requires manual observer cleanup and boilerplate. The modern block-based KVO API is safer, more concise, and eliminates the need for isObservingStyleDefault and manual removeObserver calls.

♻️ Proposed refactor using block-based KVO

Replace the manual KVO setup/teardown with a single observation that auto-cleans up:

 `@MainActor`
 private final class RightSidebarToggleAccessoryViewController: NSTitlebarAccessoryViewController {
     private static let styleDefaultsKey = "titlebarControlsStyle"
     private let hostingView: NonDraggableHostingView<RightSidebarToggleTitlebarView>
     private let containerView: NSView
-    private var isObservingStyleDefault = false
+    private var styleObservation: NSKeyValueObservation?
 
     init() {
         let containerView = NSView()
         self.containerView = containerView
         let toggle = { [weak containerView] in
             `#if` DEBUG
             cmuxDebugLog("titlebar.toggleRightSidebar")
             `#endif`
             _ = AppDelegate.shared?.toggleRightSidebarInActiveMainWindow(
                 preferredWindow: containerView?.window
             )
         }
         hostingView = NonDraggableHostingView(
             rootView: RightSidebarToggleTitlebarView(onToggle: toggle)
         )
 
         super.init(nibName: nil, bundle: nil)
 
         view = containerView
         containerView.translatesAutoresizingMaskIntoConstraints = true
         hostingView.translatesAutoresizingMaskIntoConstraints = true
         hostingView.autoresizingMask = []
         containerView.addSubview(hostingView)
         applyFittingSize()
 
-        // The button size follows the titlebar controls style; resize when
-        // that specific default changes (KVO on the key, not the broad
-        // UserDefaults.didChangeNotification).
-        UserDefaults.standard.addObserver(
-            self,
-            forKeyPath: Self.styleDefaultsKey,
-            options: [],
-            context: nil
-        )
-        isObservingStyleDefault = true
+        // The button size follows the titlebar controls style; resize when
+        // that specific default changes (KVO on the key, not the broad
+        // UserDefaults.didChangeNotification).
+        styleObservation = UserDefaults.standard.observe(
+            \.titlebarControlsStyle,
+            options: []
+        ) { [weak self] _, _ in
+            Task { `@MainActor` [weak self] in
+                self?.applyFittingSize()
+            }
+        }
     }
 
     required init?(coder: NSCoder) {
         fatalError("init(coder:) has not been implemented")
     }
-
-    deinit {
-        if isObservingStyleDefault {
-            UserDefaults.standard.removeObserver(self, forKeyPath: Self.styleDefaultsKey)
-        }
-    }
-
-    override func observeValue(
-        forKeyPath keyPath: String?,
-        of object: Any?,
-        change: [NSKeyValueChangeKey: Any]?,
-        context: UnsafeMutableRawPointer?
-    ) {
-        guard keyPath == Self.styleDefaultsKey else {
-            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
-            return
-        }
-        Task { `@MainActor` [weak self] in
-            self?.applyFittingSize()
-        }
-    }
+    // deinit not needed - NSKeyValueObservation auto-cleans up
 
     private func applyFittingSize() {
         let size = hostingView.fittingSize

Note: You'll need to extend UserDefaults with a keyPath for titlebarControlsStyle:

extension UserDefaults {
    `@objc` dynamic var titlebarControlsStyle: Int {
        return integer(forKey: "titlebarControlsStyle")
    }
}
🤖 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 3259 - 3332, The
class RightSidebarToggleAccessoryViewController uses legacy KVO via
UserDefaults.standard.addObserver(forKeyPath:options:context:) plus
isObservingStyleDefault, observeValue(_:of:change:context:) and manual
removeObserver in deinit; replace this with the modern block-based KVO API
(NSKeyValueObservation) to auto-manage the token: add a stored
NSKeyValueObservation? property (e.g. styleObservation), use
UserDefaults.standard.observe(\.titlebarControlsStyle, options: []) to assign
the observation and run applyFittingSize on change (dispatch to MainActor if
needed), remove the isObservingStyleDefault flag and the override observeValue
implementation, and drop the manual removeObserver call in deinit so cleanup is
automatic.
🤖 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 `@Sources/Update/UpdateTitlebarAccessory.swift`:
- Around line 3259-3332: The class RightSidebarToggleAccessoryViewController
uses legacy KVO via
UserDefaults.standard.addObserver(forKeyPath:options:context:) plus
isObservingStyleDefault, observeValue(_:of:change:context:) and manual
removeObserver in deinit; replace this with the modern block-based KVO API
(NSKeyValueObservation) to auto-manage the token: add a stored
NSKeyValueObservation? property (e.g. styleObservation), use
UserDefaults.standard.observe(\.titlebarControlsStyle, options: []) to assign
the observation and run applyFittingSize on change (dispatch to MainActor if
needed), remove the isObservingStyleDefault flag and the override observeValue
implementation, and drop the manual removeObserver call in deinit so cleanup is
automatic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e830cebf-2367-4e0d-bc4b-c8098db5c143

📥 Commits

Reviewing files that changed from the base of the PR and between 12985cc and 3c3f0b8.

📒 Files selected for processing (1)
  • Sources/Update/UpdateTitlebarAccessory.swift

… open sidebar

The accessory previously sized its container to the bare button, so the
system placed it below the titlebar controls row (visibly lower than
the leading cluster). It now mirrors the cluster's layout strategy:
container spans the titlebar height and the button centers on the
traffic lights' midY.

The toggle also hides while the right sidebar is open. manaflow-ai#3757 moved the
right-sidebar control into the sidebar chrome as an X button at the
same trailing position; showing both stacked them. With the titlebar
toggle visible only when the sidebar is closed, the control reads as a
single button that moves into the sidebar chrome when it opens —
matching how the left sidebar toggle reads as part of the open left
sidebar — and the testRightSidebarCloseButtonLivesInsideSidebarChrome
expectation (no global titlebar toggle while the sidebar is open)
still holds.

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

Copy link
Copy Markdown
Contributor Author

Design update in 343d0f4, after digging into #3757 (which deliberately moved the right-sidebar control out of the global titlebar and into the sidebar chrome as the X button):

  • The titlebar toggle now shows only while the right sidebar is closed. When it opens, the accessory hides and the chrome X button (same trailing position) takes over — visually the control reads as one button that moves into the sidebar, the way the left sidebar toggle reads as part of the open left sidebar. The testRightSidebarCloseButtonLivesInsideSidebarChrome expectation (no global titlebar toggle while the sidebar is open) still holds.
  • Fixed vertical alignment: the accessory now mirrors the leading cluster's layout strategy (container spans the titlebar height, button centers on the traffic lights' midY) instead of letting the system place the bare-button-sized container below the controls row — the same accessory-sizing pitfall Move right sidebar close button into sidebar and fix titlebar sizing #3757 originally fixed.

@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 3395-3399: setToggleHidden(_:) currently uses the incoming hidden
value directly which can re-show the accessory even when fullscreen/minimal
suppression should keep it hidden; change the method to compute a finalHidden by
OR-ing the passed hidden with the current suppression state (e.g. let
finalHidden = hidden || isTitlebarAccessorySuppressed() or check
window/fullscreen/minimal flags), then use guard isHidden != finalHidden else {
return } and set isHidden, view.isHidden and view.alphaValue based on
finalHidden; add or call a helper like isTitlebarAccessorySuppressed() to
encapsulate the fullscreen/minimal check.
🪄 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: b62afb8a-da93-4e11-98a0-d84b5e5f0a1c

📥 Commits

Reviewing files that changed from the base of the PR and between 3c3f0b8 and 343d0f4.

📒 Files selected for processing (1)
  • Sources/Update/UpdateTitlebarAccessory.swift

Comment on lines +3395 to +3399
private func setToggleHidden(_ hidden: Bool) {
guard isHidden != hidden else { return }
isHidden = hidden
view.isHidden = hidden
view.alphaValue = hidden ? 0 : 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve fullscreen/minimal suppression when sidebar state changes.

setToggleHidden(_:) currently maps sidebar visibility directly to controller visibility. That can re-show the accessory after a sidebar close even while fullscreen/minimal mode should still suppress all titlebar accessories.

Suggested fix
+    private func shouldSuppressForWindowMode() -> Bool {
+        WorkspacePresentationModeSettings.mode() == .minimal
+            || (view.window?.styleMask.contains(.fullScreen) ?? false)
+    }
+
-    private func setToggleHidden(_ hidden: Bool) {
-        guard isHidden != hidden else { return }
-        isHidden = hidden
-        view.isHidden = hidden
-        view.alphaValue = hidden ? 0 : 1
+    private func setToggleHidden(_ hiddenBySidebar: Bool) {
+        let hidden = hiddenBySidebar || shouldSuppressForWindowMode()
+        guard isHidden != hidden || view.isHidden != hidden else { return }
+        isHidden = hidden
+        view.isHidden = hidden
+        view.alphaValue = hidden ? 0 : 1
     }
🤖 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 3395 - 3399,
setToggleHidden(_:) currently uses the incoming hidden value directly which can
re-show the accessory even when fullscreen/minimal suppression should keep it
hidden; change the method to compute a finalHidden by OR-ing the passed hidden
with the current suppression state (e.g. let finalHidden = hidden ||
isTitlebarAccessorySuppressed() or check window/fullscreen/minimal flags), then
use guard isHidden != finalHidden else { return } and set isHidden,
view.isHidden and view.alphaValue based on finalHidden; add or call a helper
like isTitlebarAccessorySuppressed() to encapsulate the fullscreen/minimal
check.

lidge-jun and others added 2 commits June 13, 2026 08:24
During session restore with the right sidebar already open, the
accessory's first layout passes run before the main-window context is
registered, so the visibility binding never attached and the toggle
stacked on the sidebar's X button until the first interaction. Retry
the lookup briefly (matching the accessory attach retry pattern) and
let the @published subscription apply the restored state immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The accessory container spans the full titlebar height for layout, so
an opaque container swallowed clicks that landed inside the accessory
frame but outside the button — notably on the sidebar chrome's X
button while the toggle briefly overlapped it before the visibility
binding attached. Hit-test now claims only actual content.

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.

♻️ Duplicate comments (1)
Sources/Update/UpdateTitlebarAccessory.swift (1)

3418-3422: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve fullscreen/minimal suppression when sidebar visibility updates.

Line 3418 rewrites the controller’s hidden state from FileExplorerState.isVisible alone. If applyAccessoryVisibility(for:) has already hidden this accessory for minimal mode or fullscreen, the next sidebarVisible == false emission flips isHidden back to false and resurrects the button in a mode where this PR says it should stay suppressed.

Suggested fix
+    private func shouldSuppressForWindowMode() -> Bool {
+        WorkspacePresentationModeSettings.mode() == .minimal
+            || (view.window?.styleMask.contains(.fullScreen) ?? false)
+    }
+
-    private func setToggleHidden(_ hidden: Bool) {
-        guard isHidden != hidden else { return }
-        isHidden = hidden
-        view.isHidden = hidden
-        view.alphaValue = hidden ? 0 : 1
+    private func setToggleHidden(_ hiddenBySidebar: Bool) {
+        let hidden = hiddenBySidebar || shouldSuppressForWindowMode()
+        guard isHidden != hidden || view.isHidden != hidden else { return }
+        isHidden = hidden
+        view.isHidden = hidden
+        view.alphaValue = hidden ? 0 : 1
     }

Based on PR objectives, this accessory is supposed to stay hidden in minimal mode and fullscreen.

🤖 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 3418 - 3422, The
current setToggleHidden(_:) unconditionally overwrites isHidden from sidebar
visibility and can re-show the accessory despite fullscreen/minimal suppression;
add a suppression tracking flag or enum (e.g., isSuppressedByMode or
suppressionReasons set) that applyAccessoryVisibility(for:) sets when hiding for
fullscreen/minimal, and change setToggleHidden(_:) to early-return when hidden
== false but suppression flag indicates it should remain suppressed;
alternatively accept a reason parameter so applyAccessoryVisibility(for:) sets
suppression and only the matching reason can clear it (keeps
FileExplorerState-driven visibility updates but preserves mode-based
suppression).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Sources/Update/UpdateTitlebarAccessory.swift`:
- Around line 3418-3422: The current setToggleHidden(_:) unconditionally
overwrites isHidden from sidebar visibility and can re-show the accessory
despite fullscreen/minimal suppression; add a suppression tracking flag or enum
(e.g., isSuppressedByMode or suppressionReasons set) that
applyAccessoryVisibility(for:) sets when hiding for fullscreen/minimal, and
change setToggleHidden(_:) to early-return when hidden == false but suppression
flag indicates it should remain suppressed; alternatively accept a reason
parameter so applyAccessoryVisibility(for:) sets suppression and only the
matching reason can clear it (keeps FileExplorerState-driven visibility updates
but preserves mode-based suppression).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 32c41472-3f1c-4979-863c-1523ce688cb3

📥 Commits

Reviewing files that changed from the base of the PR and between 343d0f4 and 43367fb.

📒 Files selected for processing (1)
  • Sources/Update/UpdateTitlebarAccessory.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
Sources/Update/UpdateTitlebarAccessory.swift (1)

3394-3434: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unify the accessory's final hidden state.

Line 3403 only schedules a retry when the window context is missing, so a restored window can show this toggle before the first FileExplorerState value arrives. Then Lines 3429-3433 overwrite the controller-level minimal/fullscreen hide state with the raw sidebar bit, so closing the sidebar can re-show the button even though the titlebar accessory should still stay suppressed.

Suggested fix
 private final class RightSidebarToggleAccessoryViewController: NSTitlebarAccessoryViewController {
+    private var sidebarVisibilityResolved = false
+    private var sidebarVisible = true
+
+    private func shouldSuppressForWindowMode() -> Bool {
+        WorkspacePresentationModeSettings.mode() == .minimal
+            || (view.window?.styleMask.contains(.fullScreen) ?? false)
+    }
+
+    private func applyFinalVisibility() {
+        let hidden = !sidebarVisibilityResolved || sidebarVisible || shouldSuppressForWindowMode()
+        guard isHidden != hidden || view.isHidden != hidden else { return }
+        isHidden = hidden
+        view.isHidden = hidden
+        view.alphaValue = hidden ? 0 : 1
+    }
+
     init() {
         let containerView = ClickPassThroughAccessoryContainerView()
         self.containerView = containerView
         ...
         containerView.addSubview(hostingView)
         applyLayout()
+        applyFinalVisibility()
         ...
     }

     private func bindSidebarVisibilityIfNeeded() {
         guard observedFileExplorerState == nil else { return }
         guard let window = view.window,
               let state = AppDelegate.shared?.contextForMainTerminalWindow(window)?.fileExplorerState
         else {
+            applyFinalVisibility()
             scheduleBindRetryIfNeeded()
             return
         }
-        bindRetriesRemaining = 0
+        bindRetriesRemaining = 0
+        sidebarVisibilityResolved = true
         observedFileExplorerState = state
         sidebarVisibilityCancellable = state.$isVisible
             .removeDuplicates()
             .receive(on: RunLoop.main)
             .sink { [weak self] sidebarVisible in
-                self?.setToggleHidden(sidebarVisible)
+                self?.sidebarVisible = sidebarVisible
+                self?.applyFinalVisibility()
             }
     }

-    private func setToggleHidden(_ hidden: Bool) {
-        guard isHidden != hidden else { return }
-        isHidden = hidden
-        view.isHidden = hidden
-        view.alphaValue = hidden ? 0 : 1
-    }
+    private func setToggleHidden(_ hiddenBySidebar: Bool) {
+        sidebarVisible = hiddenBySidebar
+        applyFinalVisibility()
+    }
 }
🤖 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 3394 - 3434, The
accessory currently applies the sidebar's visibility directly in
bindSidebarVisibilityIfNeeded -> sink and setToggleHidden, which can re-show the
toggle even when the controller intends it suppressed (e.g., minimal/fullscreen
state); update the logic so the final hidden state is the OR of the
controller-level suppression flag and the sidebar visibility. Concretely, add or
use a single source-of-truth (e.g., a computed property or method like
desiredAccessoryHidden()) that combines the controller suppression state and the
sidebarVisible boolean, call that from the sink installed in
bindSidebarVisibilityIfNeeded, and have setToggleHidden accept/apply that
combined value (ensure scheduleBindRetryIfNeeded remains unchanged but does not
short-circuit the unified hide logic).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Sources/Update/UpdateTitlebarAccessory.swift`:
- Around line 3394-3434: The accessory currently applies the sidebar's
visibility directly in bindSidebarVisibilityIfNeeded -> sink and
setToggleHidden, which can re-show the toggle even when the controller intends
it suppressed (e.g., minimal/fullscreen state); update the logic so the final
hidden state is the OR of the controller-level suppression flag and the sidebar
visibility. Concretely, add or use a single source-of-truth (e.g., a computed
property or method like desiredAccessoryHidden()) that combines the controller
suppression state and the sidebarVisible boolean, call that from the sink
installed in bindSidebarVisibilityIfNeeded, and have setToggleHidden
accept/apply that combined value (ensure scheduleBindRetryIfNeeded remains
unchanged but does not short-circuit the unified hide logic).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3023f03f-a913-4f4f-abae-d099bb103abf

📥 Commits

Reviewing files that changed from the base of the PR and between 43367fb and a87706a.

📒 Files selected for processing (1)
  • Sources/Update/UpdateTitlebarAccessory.swift

Session restore can swap the context's FileExplorerState after an early
bind; the bind-once guard left the subscription watching the dead
object, so a sidebar restored open still showed the titlebar toggle
stacked on the chrome X button. Rebind whenever the resolved state
instance changes (layout passes drive the re-check).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@teamleaderleo teamleaderleo added area: layout Splits, panes, tabs, windows, resizing, full screen review: needs-attention Actionable automated review finding needs an author reply labels Sep 30, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

The titlebar toggle remains a distinct UI change; leaving it open with the fullscreen suppression and observer findings for follow-up.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: layout Splits, panes, tabs, windows, resizing, full screen review: needs-attention Actionable automated review finding needs an author reply

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants