Skip to content

Fix terminal surfaces losing theme after config reload - #2707

Closed
rodchristiansen wants to merge 25 commits into
manaflow-ai:mainfrom
rodchristiansen:fix/theme-sync-after-config-reload
Closed

rodchristiansen wants to merge 25 commits into
manaflow-ai:mainfrom
rodchristiansen:fix/theme-sync-after-config-reload

Conversation

@rodchristiansen

@rodchristiansen rodchristiansen commented Apr 8, 2026 •

Copy link
Copy Markdown

Summary

  • After ghostty_app_update_config, surfaces can end up with stale theme colors (e.g. light foreground on dark background) because ghostty_surface_set_color_scheme's internal dedup skips re-derivation when the scheme hasn't changed
  • Fix by calling ghostty_surface_update_config on each surface after config reload, which forces a full config re-derive with the surface's current light/dark conditional state, bypassing the dedup
  • Also re-applies the color scheme from the current macOS appearance to keep Swift-side tracking in sync

Test plan

  • Open cmux with theme = light:X,dark:Y conditional themes configured
  • Toggle macOS appearance between light and dark mode — all panes should update consistently
  • Use Reload Configuration from the menu — all panes should retain correct theme colors
  • Verify no regressions with single-theme configs (non-conditional theme = X)

Note

Medium Risk
Medium risk due to broad UI/windowing changes (macOS 26 NavigationSplitView/toolbar/glass behavior) plus new session-persisted sidebar section/grouping state that affects workspace ordering and restore logic.

Overview
Fixes terminal surfaces showing stale theme colors after Ghostty config reload by forcing each surface to ghostty_surface_update_config and reapplying the Swift-side color scheme (reapplyColorSchemeAndConfig).

Adds workspace grouping in the sidebar via new SidebarSection model and TabManager.sidebarLayout, including section CRUD (rename/reorder/collapse), drag-and-drop support, context-menu “Move to Section”, and persistence/restore through SessionPersistence (workspace UUIDs are now stored and restored).

Introduces a macOS 26-specific window/UI path: uses NavigationSplitView for native glass sidebar + SwiftUI toolbars (notifications popover handled via a notification bridge), adjusts safe-area/titlebar behavior, disables manual window glass insertion, tweaks terminal rounding/insets, and updates toolbar/accessory attachment logic for 26+.

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


Summary by cubic

Fixes terminal theme desync after config reload by forcing each surface to re-derive its config and reapply the current appearance. Also brings native macOS 26 UI updates and optional sidebar sections with session persistence.

  • Bug Fixes

    • After config reload, call ghostty_surface_update_config and then reapply the color scheme on every surface to prevent stale light/dark theme colors.
  • New Features

    • macOS 26 UI: native glass sidebar with NavigationSplitView, SwiftUI toolbar (terminal/browser/split/notifications/new tab), and rounded leading corners with safe insets; legacy titlebar accessory is skipped.
    • Sidebar sections: create/rename/reorder/collapse named groups, drag tabs onto section headers, “Move to Section” context menu, and pinned tabs always on top; state persists across sessions.
    • Session persistence: save workspace UUIDs and sidebar sections; changes trigger autosave; backward-compatible with older snapshots.
    • Config commands: support workspace.target: "current" and autoApply; auto-apply runs once per workspace on switch (single-pane only), using local or global config.
    • Updated bonsplit to improve split divider behavior; hide its tab bar for single-tab panes on macOS 26.

Written for commit 21e9e70. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Organize workspaces into custom sidebar sections with collapse, rename, and reorder capabilities.
    • Auto-apply workspace commands on selection when configured.
    • Enhanced macOS 26 support with improved window behavior, native safe-area handling, and modernized UI.
    • Sidebar sections now persist across sessions.
  • Bug Fixes & Improvements

    • Improved terminal appearance with corner radius adjustments and glass effect styling.
    • Better config reload handling with visual refresh.
    • Redesigned notifications interface with modern popover on macOS 26+.

Users can now organize workspace tabs into named, collapsible sections
in the sidebar. Sections are opt-in — the sidebar behaves identically
when no sections exist.

- New SidebarSection model with name, collapse state, and ordered
  workspace membership
- Section CRUD in TabManager (create, rename, delete, reorder)
- Context menu: "Move to Section" submenu on workspace tabs with
  "New Section..." option
- SidebarSectionHeaderView with disclosure chevron, inline rename,
  context menu, accessibility labels, and drop target
- Drag workspaces onto section headers to assign them
- Section state persists across sessions via SessionPersistence
- Pinned workspaces always render at top regardless of section
- Backward-compatible: old session snapshots restore cleanly
The new file needs to be registered in the .pbxproj so Xcode
can find the SidebarSection and SidebarLayout types.
- Forward section objectWillChange to TabManager via sectionRevision
  counter so sidebar re-renders immediately on any section mutation
- Use @focusstate on rename TextField with delayed auto-focus so
  cursor lands in the text field instead of the terminal
- Guard onTapGesture during rename editing to prevent conflicts
- Add toggleCollapsed()/setCollapsed() helpers that bump revision
- Pipe Combine observers from each section to TabManager on didSet
NavigationSplitView with native glass sidebar, system titlebar, and
SwiftUI toolbar on macOS 26. Terminal detail content gets 8pt external
padding, 6pt internal content inset, and leading corner rounding (16pt)
when the sidebar is visible.

Key changes:
- NavigationSplitView wraps sidebar + detail for native glass treatment
- System titlebar (titlebarAppearsTransparent = false) with SwiftUI
  .toolbar for bell and new-tab buttons
- SplitViewDividerHider clears NSSplitView divider color
- Terminal content inset (6pt) keeps text within safe zone
- Leading corners rounded at AppKit portal level when sidebar present
- Bonsplit tab bar hidden for single-tab panes on macOS 26
- App icon auto-set skipped on macOS 26 (system handles dark variants)
- Old titlebar accessory controllers skipped on macOS 26
Update bonsplit submodule: the animated split entry path
hardcoded 0.5 instead of using the configured divider ratio.

Include sidebar sections (id, name, collapsed state, workspace
membership) in the autosave fingerprint so that section changes
trigger session persistence and survive app restart.
Preserve workspace UUIDs in session snapshots so section membership
survives app restart. Include sections in the autosave fingerprint
so create/rename/reorder/collapse changes trigger persistence.

Auto-enter rename mode when a new section is created so the user
can immediately type the name.
- Add "target": "current" to cmux.json workspace commands so layouts
  apply to the selected workspace in-place instead of spawning a new one
- Fix section rename auto-focus race: delay pendingRenameSectionId to
  next run loop so SwiftUI mounts the header view before the publisher
  emits
- Decode 'target' field in CmuxWorkspaceDefinition custom decoder so
  target:current actually takes effect from cmux.json
- Fix within-section drag reorder: reorder section.workspaceIds instead
  of the flat tabs array so visual order updates correctly
- Fix section rename auto-focus timing race (from prior commit)
- Fix bell button no-op: start titlebarAccessoryController on macOS 26
  so notifications popover infrastructure is available
- Add tab bar design comment: bonsplit shows tabs when pane.tabs > 1
- Replace dividerColor KVC with safe responds(to:)/perform selector
- Add accessibility labels to toolbar buttons (bell, new tab)
- Localize all WindowToolbarController toolbar item strings
- Add comment about NavigationSplitView width persistence limitation
- Move corner radius update inside CATransaction to prevent flicker
- Replace magic number 20 with named sidebarDetectionThreshold constant
- Remove unused TerminalBackgroundFill struct
- Fix SplitViewDividerHider doc comment and remove unused property
- Add comment explaining uniform 6pt content inset design
- Remove || true debug code left in section menu guard
- Fix intra-section drag off-by-one: remove before insert so indices
  are stable when dragging forward
- Fix tabIndexById to use flat tabs order for shift-selection and
  shortcut badges (not section order)
- Fix dictionary mutation during iteration in panel close loop
- Make target:current fail closed when no workspace is selected
- Only auto-apply to single-pane workspaces to avoid tearing down
  restored or user-customized split configurations
- Validate destination section before removing workspace from all
  sections in moveWorkspaceToSection
- Add backwards-compat comment for optional SessionWorkspaceSnapshot.id
- Replace SwiftUI import with Foundation in SidebarSection (only needs
  Combine + Foundation)
- Remove redundant revision property and bumpRevision() calls from
  SidebarSection (mutations already trigger objectWillChange)
- Remove redundant tabManager.objectWillChange.send() in section header
…macOS 26

- Fix bell button no-op: use SwiftUI .popover on macOS 26 instead of the
  broken AppKit titlebar accessory path; bridge keyboard shortcut via
  NotificationCenter
- Move pane action buttons (terminal, browser, split right/down) from
  bonsplit tab bar to native toolbar as a ControlGroup on macOS 26
- Hide bonsplit split buttons when tab bar is hidden (macOS 26)
- Hide old HiddenTitlebarSidebarControlsView on macOS 26
- Remove system sidebar toggle chevron (.toolbar(removing: .sidebarToggle))
- Fix asymmetric inset: only inset leading/top edges where corners are
  rounded, preserving terminal real estate on right/bottom
- Fix misleading comment about fullSizeContentView on macOS 26
- Update SplitViewDividerHider doc to match actual implementation
- Clarify hideTabBar comment: PaneContainerView conditionally shows tab
  bar for multi-tab panes
- Improve sidebar detection heuristic comment explaining why frame-based
  approach is used (AppKit view cannot access SwiftUI SidebarState)
- Add GeometryReader to persist sidebar width on macOS 26 when user
  drags the native NavigationSplitView divider
- Remove redundant sectionRevision read in sidebar body (sidebarLayout
  already reads it internally, establishing the SwiftUI dependency)
- Fix autoApply silently skipping global-config commands: fall back to
  global config directory when no local config is present
- Fix auto-apply race condition on rapid tab switching: capture the
  emitted tab ID and verify it's still selected before applying, so a
  quick B→C switch doesn't apply B's config to C
…s on macOS 26

- Use flexible frame for sidebar in NavigationSplitView so rows extend full width
  instead of floating centered with gaps
- Skip attaching legacy TitlebarControlsAccessoryViewController on macOS 26 since
  NavigationSplitView toolbar provides its own controls
- Add unread notification badge to the new toolbar bell button
After ghostty_app_update_config, each surface re-derives its config
using its conditional state (light/dark). However,
ghostty_surface_set_color_scheme has an internal dedup that skips
when the scheme hasn't changed, so surfaces could end up with stale
theme colors (e.g. light foreground on dark background) after a
config reload or appearance toggle.

Fix by calling ghostty_surface_update_config on each surface in
refreshTerminalSurfacesAfterGhosttyConfigReload, which forces a
full config re-derive with the surface's current conditional state,
bypassing the color scheme dedup. Also re-apply the color scheme
from the current macOS appearance to keep Swift-side tracking in sync.
Copilot AI review requested due to automatic review settings April 8, 2026 03:56
@vercel

vercel Bot commented Apr 8, 2026

Copy link
Copy Markdown

@rodchristiansen 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 Apr 8, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR introduces a comprehensive macOS 26+ UI overhaul featuring a new sidebar sections system for workspace organization, a NavigationSplitView layout, revised titlebar behavior, workspace auto-apply functionality, and new toolbar controls. It adds session persistence for sidebar sections, updates terminal surface refresh logic, and refactors config execution to support in-place workspace updates with conditional macOS version-specific code paths throughout.

Changes

Cohort / File(s) Summary
Sidebar System Foundation
Sources/SidebarSection.swift, Sources/SessionPersistence.swift
Introduced new SidebarSection class with collapsible state and SidebarLayout struct for modeling pinned/ungrouped/section-grouped workspace organization. Extended session snapshots to persist sidebar section metadata (id, name, isCollapsed, workspaceIds).
Tab Manager & Section Management
Sources/TabManager.swift
Added section state tracking (sections, sectionRevision), section management APIs (createSection, renameSection, deleteSection, etc.), and sidebarLayout computed property. Integrated section cleanup into workspace removal paths and updated session persistence/restoration to serialize and restore sections.
macOS 26+ UI Overhaul
Sources/ContentView.swift
Refactored sidebar rendering with VerticalTabsSidebar extraction, replaced layout with NavigationSplitView on macOS 26+, removed SidebarTopScrim, added glass effect modifiers (FooterButtonGlassModifier, TabItemGlassModifier), suppressed titlebar overlay, and updated titlebar padding/spacing logic. Enhanced sidebar interactions with section header controls and section-aware drag-drop reordering.
Titlebar & Accessory Management
Sources/AppDelegate.swift, Sources/Update/UpdateTitlebarAccessory.swift
Conditionally use system safe-area metrics on macOS 26+, routed notifications popover to SwiftUI on macOS 26+, adjusted titlebar transparency and window movability, and prevented legacy titlebar accessory attachment on macOS 26+. Changed NotificationsPopoverView visibility to internal.
Terminal Surface & Portal Updates
Sources/GhosttyTerminalView.swift, Sources/TerminalWindowPortal.swift
Added TerminalSurface.reapplyColorSchemeAndConfig() to refresh config after reloads, relaxed applySurfaceColorScheme to fileprivate, and inset scroll view frame on macOS 26+. Applied corner radius/masking to hosted terminal view when sidebar is inferred on macOS 26+.
Workspace & Config Auto-Apply
Sources/CmuxConfig.swift, Sources/CmuxConfigExecutor.swift, Sources/Workspace.swift
Added autoApply and target (.current / .new) fields to command/workspace definitions. Implemented checkAutoApply(forTabId:) for conditional single-execution of auto-apply commands. Updated executor to handle in-place workspace updates (title/color/layout) when target == .current. Extended Workspace initializer to accept optional restoredId and updated bonsplitAppearance OS-dependent tab chrome behavior.
Toolbar & Window Controls
Sources/WindowToolbarController.swift
Added three new toolbar item identifiers (cmux.sidebarToggle, cmux.notifications, cmux.newTab) on macOS 26+, switched toolbar style to .unified on macOS 26+, and implemented action methods wiring to AppDelegate behaviors.
App Entry Point & Refactoring
Sources/cmuxApp.swift
Extracted mainWindowContent into a dedicated @ViewBuilder and bypassed appearance observation on macOS 26+ in AppIconSettings.applyIcon.
Build & Vendor Updates
GhosttyTabs.xcodeproj/project.pbxproj, vendor/bonsplit
Added SidebarSection.swift to Xcode target and updated bonsplit submodule commit.

Sequence Diagrams

sequenceDiagram
    participant User
    participant AppDelegate
    participant CmuxConfigStore
    participant TabManager
    participant Workspace
    participant Terminal

    User->>AppDelegate: Launch app / Change workspace
    AppDelegate->>CmuxConfigStore: loadAll()
    CmuxConfigStore->>CmuxConfigStore: checkAutoApply(forTabId:)
    CmuxConfigStore->>CmuxConfigStore: Find command where autoApply==true<br/>and target==.current
    alt Command found and conditions met
        CmuxConfigStore->>Workspace: Execute command in current workspace
        Workspace->>Workspace: Set title, color, layout
        Workspace->>Terminal: Apply layout (close non-focused panels)
    else Command not applicable
        CmuxConfigStore->>CmuxConfigStore: Skip (already applied or conditions unmet)
    end
    Terminal->>AppDelegate: Render updated workspace
Loading
sequenceDiagram
    participant User
    participant ContentView as ContentView<br/>(Sidebar UI)
    participant TabManager
    participant SidebarSection
    participant SessionPersistence

    User->>ContentView: Drag workspace to section header
    ContentView->>TabManager: moveWorkspaceToSection(tabId, sectionId)
    TabManager->>SidebarSection: addWorkspace(workspaceId, at index)
    SidebarSection->>TabManager: notifySectionChange()
    TabManager->>TabManager: Increment sectionRevision
    TabManager->>ContentView: Update sidebarLayout (SwiftUI dependency)
    ContentView->>ContentView: Re-render section groups
    TabManager->>SessionPersistence: Session autosave includes sections
    SessionPersistence->>SessionPersistence: Serialize sidebar section state
Loading
sequenceDiagram
    participant AppDelegate
    participant ContentView as macOS 26+ UI
    participant WindowToolbarController
    participant TabManager

    AppDelegate->>ContentView: Initialize window (macOS 26+)
    ContentView->>ContentView: Render NavigationSplitView layout
    ContentView->>WindowToolbarController: Configure toolbar
    WindowToolbarController->>WindowToolbarController: Add sidebar toggle,<br/>notifications, new tab items
    User->>WindowToolbarController: Click sidebar toggle
    WindowToolbarController->>AppDelegate: toggleSidebarAction()
    AppDelegate->>ContentView: Update sidebarState.isVisible
    ContentView->>ContentView: Animate NavigationSplitView column
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hops of joy through sections new,
Sidebar sorted, organized true,
macOS twenty-six takes flight,
Glass effects gleaming bright,
Auto-apply makes workflows sweet—
Tab management's now complete! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix terminal surfaces losing theme after config reload' accurately and concisely describes the main bug fix in the changeset.
Description check ✅ Passed The description covers the key aspects: summary of the problem and solution, test plan with checkboxes, and identifies risks. However, it lacks explicit sections matching the template structure and is missing a demo video.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/theme-sync-after-config-reload

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.

Copilot AI 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.

Pull request overview

This PR addresses terminal surfaces ending up with stale theme colors after a Ghostty config reload by forcing each surface to re-derive its config and re-assert the current light/dark color scheme. In addition, it introduces a set of macOS 26-specific UI changes (NavigationSplitView / Liquid Glass / toolbar behavior) and adds sidebar “sections” plus related session persistence and config behavior.

Changes:

  • Force Ghostty surfaces to re-derive config + re-apply the correct color scheme after config reload.
  • Add macOS 26 UI adaptations (NavigationSplitView layout, glass/titlebar/toolbar adjustments, rounded terminal corners).
  • Introduce sidebar sections (grouping/collapse/reorder/move-to-section) with session persistence support, and add cmux config auto-apply / “target: current”.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Sources/Workspace.swift Adds workspace UUID persistence in snapshots and supports restoring a workspace’s ID; macOS 26-specific bonsplit appearance tweaks.
Sources/WindowToolbarController.swift Adds macOS 26 toolbar identifiers/items and switches toolbar style on macOS 26.
Sources/Update/UpdateTitlebarAccessory.swift Exposes NotificationsPopoverView for reuse and skips legacy titlebar accessory attachment on macOS 26.
Sources/TerminalWindowPortal.swift Rounds terminal corners on macOS 26 when a sidebar is heuristically detected.
Sources/TabManager.swift Adds sidebar sections model + layout computation + autosave hashing + session snapshot/restore integration.
Sources/SidebarSection.swift Introduces SidebarSection + SidebarLayout types to represent sidebar grouping.
Sources/SessionPersistence.swift Extends session snapshots with optional workspace IDs and adds sidebar section snapshot types.
Sources/GhosttyTerminalView.swift Adds surface reapplyColorSchemeAndConfig() and macOS 26 scroll view insets; exposes applySurfaceColorScheme to same-file callers.
Sources/ContentView.swift Adds macOS 26 NavigationSplitView + SwiftUI toolbar controls, sections UI, glass modifiers, and sidebar background behavior changes.
Sources/CmuxConfigExecutor.swift Adds “target: current” support to apply a workspace layout to the currently selected workspace.
Sources/CmuxConfig.swift Adds autoApply, workspace target, and auto-apply wiring logic in CmuxConfigStore.
Sources/cmuxApp.swift Refactors main window content construction and adjusts macOS 26 app icon behavior.
Sources/AppDelegate.swift Forces surface config+scheme re-derivation after Ghostty config reload; macOS 26 titlebar/window behavior changes; adds notification toggle indirection for SwiftUI popover.
GhosttyTabs.xcodeproj/project.pbxproj Adds SidebarSection.swift to the Xcode project build.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Sources/ContentView.swift
Comment on lines +15795 to +15799
// Clear the divider color via ObjC messaging (private API).
let selector = NSSelectorFromString("setDividerColor:")
if splitView.responds(to: selector) {
splitView.perform(selector, with: NSColor.clear)
}

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

SplitViewDividerHiderView is invoking a private AppKit API via perform(NSSelectorFromString("setDividerColor:"), ...). This is brittle across OS updates and can cause App Store rejection. Prefer a public approach (e.g., accept the default divider, adjust layout/background to visually mask it, or use a supported dividerStyle/appearance API) and remove the private selector call.

Suggested change
// Clear the divider color via ObjC messaging (private API).
let selector = NSSelectorFromString("setDividerColor:")
if splitView.responds(to: selector) {
splitView.perform(selector, with: NSColor.clear)
}

Copilot uses AI. Check for mistakes.
Comment thread Sources/ContentView.swift
@State private var isEditing = false
@State private var editedName = ""
@FocusState private var isTextFieldFocused: Bool
@Environment(\.colorScheme) private var colorScheme

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

@Environment(\.colorScheme) private var colorScheme is declared but never used in SidebarSectionHeaderView, which will trigger an unused-variable warning. Please remove it (or use it if it was intended to drive styling).

Suggested change
@Environment(\.colorScheme) private var colorScheme

Copilot uses AI. Check for mistakes.
Comment thread Sources/ContentView.swift
Comment on lines 2884 to +2890
private var contentAndSidebarLayout: AnyView {
// On macOS 26, use NavigationSplitView so the system recognizes
// the sidebar column and applies native Liquid Glass treatment.
if #available(macOS 26.0, *) {
return AnyView(
NavigationSplitView(columnVisibility: Binding(
get: { sidebarState.isVisible ? .all : .detailOnly },

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

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

The PR title/description focuses on fixing Ghostty surface theme state after config reload, but this diff also introduces substantial unrelated changes (macOS 26 NavigationSplitView/Liquid Glass work, toolbar controls, sidebar sections, config auto-apply, etc.). Please either update the PR description to cover these additions (including rationale/test plan) or split the changes into separate PRs to keep review and risk manageable.

Copilot uses AI. Check for mistakes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 21e9e70. Configure here.

Comment thread Sources/AppDelegate.swift
if #available(macOS 26.0, *) {
NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: nil)
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Notifications popover dismiss/query broken on macOS 26

Medium Severity

On macOS 26, toggleNotificationsPopover was updated to use a NotificationCenter notification to drive the SwiftUI-managed isNotificationsPopoverPresented state, but dismissNotificationsPopoverIfShown and isNotificationsPopoverShown still delegate exclusively to titlebarAccessoryController, which won't own the popover on macOS 26. This means Escape-to-dismiss (line 10882) won't work and the keyboard-event consumption guard (line 10889) will always evaluate to false, letting keystrokes leak into the terminal while the popover is open.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 21e9e70. Configure here.

Comment thread Sources/ContentView.swift
// sidebarLayout reads sectionRevision internally, establishing the
// SwiftUI dependency — no separate read needed here.
let layout = tabManager.sidebarLayout
let allOrdered = layout.allWorkspacesInOrder

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused allOrdered variable is dead code

Low Severity

allOrdered is computed from layout.allWorkspacesInOrder but never referenced anywhere. The sidebar body iterates layout.pinnedWorkspaces, layout.ungroupedWorkspaces, and layout.sectionGroups directly, making this a dead store.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 21e9e70. Configure here.

Comment thread Sources/cmuxApp.swift
}

var body: some Scene {
WindowGroup { mainWindowContent }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

.windowStyle(.hiddenTitleBar) removed for all macOS versions

Medium Severity

The SwiftUI .windowStyle(.hiddenTitleBar) modifier was dropped during the mainWindowContent extraction and not conditionally re-applied for pre-macOS 26. While NSWindow properties (titlebarAppearsTransparent, titleVisibility) and fullSizeContentView partially compensate, .windowStyle(.hiddenTitleBar) also affects SwiftUI's own layout calculations and window creation, potentially causing subtle layout differences on macOS 13–15.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 21e9e70. Configure here.

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

🧹 Nitpick comments (5)
Sources/SessionPersistence.swift (1)

359-359: Consider defaulting sections to [] after decode.

Optional works for backward compatibility, but a non-optional in-memory shape ([] when missing) reduces nil checks and downstream branching.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SessionPersistence.swift` at line 359, The decoded model leaves var
sections: [SessionSidebarSectionSnapshot]? nil; after decoding in
SessionPersistence's Decodable init (or wherever the JSON decoding occurs), set
sections to an empty array when missing (e.g., assign sections = sections ?? [])
so the in-memory shape is non-nil and callers don't need to handle optional
sections; reference the sections property and the SessionSidebarSectionSnapshot
type when making the change.
Sources/Workspace.swift (1)

6778-6803: hideTabBar now describes only half of this branch.

It also controls split-button visibility, so a broader name would make the 26+ appearance policy easier to read the next time this logic changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 6778 - 6803, The boolean hideTabBar in
bonsplitAppearance is misleading because it also controls showSplitButtons;
rename it to something reflecting both effects (e.g., useCompactChrome or
hideChromeControls) and update all uses in that function: set the new variable
based on the macOS availability check, use it for tabBarHeight (compact -> 0
else 33) and for showSplitButtons (show when not compact), keeping
splitButtonTooltips, enableAnimations, and chromeColors unchanged; ensure the
variable name is updated in the BonsplitConfiguration.Appearance initializer so
the intent is clear.
Sources/CmuxConfig.swift (1)

447-458: Only the first matching auto-apply command is executed.

The use of .first(where:) means if multiple commands have autoApply: true and target: .current, only the first one in load order (local config first, then global) will execute. This is likely intentional but worth documenting in user-facing config documentation so users understand the precedence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/CmuxConfig.swift` around lines 447 - 458, The current code uses
loadedCommands.first(where: { $0.autoApply == true && $0.workspace?.target ==
.current }) so only a single matching autoApply command runs; change this to
find all matches (e.g., use loadedCommands.filter(...)) and iterate over each
matching command, calling CmuxConfigExecutor.execute(...) for each and
referencing commandSourcePaths[command.id] for the configSourcePath; keep
inserting workspace.id into autoAppliedWorkspaceIds once per workspace and
preserve loadedCommands order so local configs still take precedence.
Sources/TabManager.swift (1)

5962-5970: Deduplicate restored workspace membership across sections.

Restore currently filters unknown IDs but does not enforce one-section-per-workspace. A malformed or legacy snapshot with duplicates can restore the same workspace into multiple sections.

Suggested hardening
 // Restore sidebar sections, filtering out workspace IDs that weren't restored.
 let restoredTabIds = Set(newTabs.map(\.id))
+var assignedWorkspaceIds: Set<UUID> = []
 let restoredSections: [SidebarSection] = (snapshot.sections ?? []).map { sectionSnapshot in
     SidebarSection(
         id: sectionSnapshot.id,
         name: sectionSnapshot.name,
         isCollapsed: sectionSnapshot.isCollapsed,
-        workspaceIds: sectionSnapshot.workspaceIds.filter { restoredTabIds.contains($0) }
+        workspaceIds: sectionSnapshot.workspaceIds.filter {
+            restoredTabIds.contains($0) && assignedWorkspaceIds.insert($0).inserted
+        }
     )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager.swift` around lines 5962 - 5970, The restore logic can
place the same workspace ID into multiple SidebarSection instances; update the
restoredSections construction to deduplicate and enforce
one-section-per-workspace by tracking assigned IDs as you map snapshot.sections:
create a mutable Set (e.g., assignedIds) outside the map, then for each
sectionSnapshot compute workspaceIdsFiltered =
sectionSnapshot.workspaceIds.filter { restoredTabIds.contains($0) &&
!assignedIds.contains($0) }, add those filtered IDs into assignedIds, and use
workspaceIdsFiltered when creating the SidebarSection (referencing
restoredTabIds, SidebarSection, restoredSections, and snapshot.sections).
Sources/ContentView.swift (1)

15768-15803: Avoid full split-view tree traversal on every updateNSView.

updateNSView calls scheduleHide() each refresh; this can repeatedly walk the full window hierarchy. Consider patch-once-per-window or throttling.

Lightweight guard to reduce repeated work
 `@available`(macOS 26.0, *)
 final class SplitViewDividerHiderView: NSView {
+    private weak var lastPatchedWindow: NSWindow?
+    private var didPatchInCurrentWindow = false
+
     override func viewDidMoveToWindow() {
         super.viewDidMoveToWindow()
+        if window !== lastPatchedWindow {
+            lastPatchedWindow = window
+            didPatchInCurrentWindow = false
+        }
         scheduleHide()
     }

     func scheduleHide() {
         DispatchQueue.main.async { [weak self] in
-            self?.hideDividers()
+            guard let self else { return }
+            guard !self.didPatchInCurrentWindow else { return }
+            self.hideDividers()
+            self.didPatchInCurrentWindow = true
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 15768 - 15803, The current
updateNSView(_:context:) → scheduleHide() flow causes patchSplitViews(in:) to
traverse the entire window every refresh; change it to patch each window only
once (or throttle repeated runs) by tracking which NSWindow instances have
already been patched: add a static weak set/NSHashTable or an associated-object
flag keyed on the NSWindow and check it in hideDividers()/patchSplitViews(in:)
(or before scheduling work in scheduleHide()); also clear the flag if the window
changes/closes (hook viewDidMoveToWindow / windowWillClose) so new windows get
patched. This keeps the existing symbols (SplitViewDividerHiderView,
updateNSView, scheduleHide, hideDividers, patchSplitViews, viewDidMoveToWindow)
but prevents repeated full-tree traversals per refresh.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 10155-10162: The macOS 26 change removed attaching titlebar
accessory views in attachUpdateAccessory(to window: NSWindow) but left
notifications toggling via a global NotificationCenter broadcast (object: nil),
causing all ContentView subscribers to toggle and desync with controller-backed
APIs; fix this by introducing a window-scoped source-of-truth for the macOS-26
path and wiring everything to it: update ContentView to filter notification
toggle events by the target NSWindow (or a unique window identifier) instead of
listening to object: nil, and modify showNotificationsPopoverFromMenuBar(),
dismissNotificationsPopoverIfShown(), and isNotificationsPopoverShown() to
consult/update the new macOS-26 window-scoped state (falling back to
titlebarAccessoryController behavior on older macOS) so controller-backed
methods remain consistent with visible popovers.

In `@Sources/CmuxConfigExecutor.swift`:
- Around line 96-112: The .current branch in CmuxConfigExecutor bypasses the
restart handling and always closes panels and applies layout; update the branch
to consult wsDef.restart before mutating the workspace: retrieve the restart
policy from wsDef (e.g., .ignore, .confirm, .always) at the start of the
wsDef.target == .current block and short-circuit appropriately (for .ignore
return without changing panels/layout; for .confirm invoke the existing confirm
prompt/path used by the restart switch to get user consent) and only perform
setCustomTitle/setCustomColor/closing panels/applyCustomLayout when the restart
policy permits; use the same logic/code path as the later restart switch so
behavior is consistent with other targets.

In `@Sources/ContentView.swift`:
- Around line 15795-15799: Remove the use of the private AppKit selector
("setDividerColor:") invoked via splitView.perform(...) in the ContentView.swift
block (the selector variable and the responds/to/perform code) and instead rely
only on public APIs: set splitView.dividerStyle = .thin and, if your deployment
target supports it, set splitView.dividerColor via the public NSSplitView API;
if dividerColor is unavailable for your target OS, remove the private workaround
and add a short TODO comment referencing splitView.dividerStyle and the need to
implement a custom NSView-based separator or document the migration plan so we
avoid private API usage.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 9136-9148: The macOS 26 branch unconditionally subtracts a 6pt
inset when computing scrollFrame which can yield negative sizes or over-shifted
origins during tiny/intermediate layouts; inside the if `#available`(macOS 26.0,
*) block, clamp the inset before building scrollFrame (e.g., cap inset to no
more than half the bounds.width and bounds.height or ensure bounds.width - inset
>= 0 and bounds.height - inset >= 0) and then use that clamped inset when
computing x, width and height so scrollFrame never becomes negative or moves the
origin incorrectly.
- Around line 4448-4456: reapplyColorSchemeAndConfig currently unwraps `surface`
directly and calls `ghostty_surface_update_config`, which can dereference a
freed native pointer; instead obtain the runtime-safe surface via the wrapper
`liveSurfaceForGhosttyAccess(reason:)` before any native calls—use
`attachedView` to ensure view exists, call `liveSurfaceForGhosttyAccess(reason:
"reapplyColorSchemeAndConfig")` to get a guarded surface, and only then call
`ghostty_surface_update_config(surface, config)` (and bail out if the
live-surface wrapper returns nil) so you never call into Ghostty with a
freed/native-invalid surface.

In `@Sources/SessionPersistence.swift`:
- Around line 331-334: SessionWorkspaceSnapshot.id being optional is okay for
legacy decoding but must be enforced non-nil when creating/writing new
snapshots: add a guard that checks SessionWorkspaceSnapshot.id is non-nil in the
code path that persists/encodes new snapshots (e.g., the function that writes or
encodes snapshots such as the snapshot persistence/serialize method), and if nil
throw/return an error (or assert) so callers must supply the live workspace
UUID; keep the optional only for decoding but ensure any constructor or save
method validates id and refuses to write snapshots without an id.

In `@Sources/TabManager.swift`:
- Around line 2686-2792: reorderWorkspace(...) must ignore attempts to perform
flat-array reorders for workspaces that are inside a section; add an early
return at the start of the reorderWorkspace(tabId:toIndex:) implementation that
checks sectionForWorkspace(tabId) and returns immediately if it yields a
SidebarSection (i.e., the workspace is grouped), so the function no longer
mutates tabs ordering for sectioned items.

In `@Sources/TerminalWindowPortal.swift`:
- Around line 1499-1515: The current corner-radius heuristic in
TerminalWindowPortal using targetFrame.origin.x > 20 is too broad and rounds
inner corners for right-side panes; replace this x-offset check with a reliable
detection of leading-edge panes (reuse the existing sidebar-edge detection
helper or API already present in this class/module) so you only set
desiredRadius and maskedCorners when the pane is actually on the outer leading
edge; update the conditional around hostedView.layer?.cornerRadius and
maskedCorners to call that helper (or a method like isPaneOnLeadingEdge) instead
of comparing origin.x, keeping the macOS 26 availability guard and the
CATransaction-safe update logic.

In `@Sources/WindowToolbarController.swift`:
- Around line 164-177: The WindowToolbarController implementation is dead
(defines toolbarDefaultItemIdentifiers, toolbarAllowedItemIdentifiers and
toolbar item identifiers such as sidebarToggleIdentifier,
notificationsIdentifier, newTabIdentifier and has a start(tabManager:) method
that is never called); either remove the entire WindowToolbarController class
and its identifier constants to eliminate unused NSToolbar code, or wire it up
by instantiating WindowToolbarController and calling start(tabManager:) during
window/app setup (e.g., where NSWindow is created), attach its NSToolbar to the
window, and ensure toolbarDefaultItemIdentifiers/toolbarAllowedItemIdentifiers
are used by setting the toolbar.delegate to the controller so the defined items
(sidebarToggleIdentifier, notificationsIdentifier, newTabIdentifier,
commandItemIdentifier) become active.
- Around line 230-243: toggleSidebarAction toggles the global sidebarState on
AppDelegate which affects the key/active window rather than the window whose
toolbar was clicked; update toggleSidebarAction to call
AppDelegate.shared?.toggleSidebarInActiveMainWindow() so it uses the established
cross-window routing pattern (see newTabAction's use of
addWorkspaceInPreferredMainWindow as the model). For toggleNotificationsAction,
decide if notifications should be routed per-window—if so, replace the direct
call to AppDelegate.shared?.toggleNotificationsPopover(animated:) with the
equivalent per-window routing helper (mirror the active-main-window routing used
by toggleSidebarInActiveMainWindow); otherwise leave it as-is. Ensure you only
change toggleSidebarAction (and toggleNotificationsAction if applying per-window
routing) and keep newTabAction unchanged as the correct pattern example.

In `@vendor/bonsplit`:
- Line 1: The parent repo points to a submodule commit
(cfff8a9318f8131604681e4cb86c11925e01bf1e) that hasn't been pushed to the
bonsplit remote; push that commit to the bonsplit repository's main branch (push
the local bonsplit commit to manaflow-ai/bonsplit main), then update the parent
repo pointer. After pushing, verify the commit is reachable from origin/main
using git merge-base --is-ancestor cfff8a9318f8131604681e4cb86c11925e01bf1e
origin/main (run this inside the bonsplit checkout), and only then update/commit
the submodule pointer in the parent repo.

---

Nitpick comments:
In `@Sources/CmuxConfig.swift`:
- Around line 447-458: The current code uses loadedCommands.first(where: {
$0.autoApply == true && $0.workspace?.target == .current }) so only a single
matching autoApply command runs; change this to find all matches (e.g., use
loadedCommands.filter(...)) and iterate over each matching command, calling
CmuxConfigExecutor.execute(...) for each and referencing
commandSourcePaths[command.id] for the configSourcePath; keep inserting
workspace.id into autoAppliedWorkspaceIds once per workspace and preserve
loadedCommands order so local configs still take precedence.

In `@Sources/ContentView.swift`:
- Around line 15768-15803: The current updateNSView(_:context:) → scheduleHide()
flow causes patchSplitViews(in:) to traverse the entire window every refresh;
change it to patch each window only once (or throttle repeated runs) by tracking
which NSWindow instances have already been patched: add a static weak
set/NSHashTable or an associated-object flag keyed on the NSWindow and check it
in hideDividers()/patchSplitViews(in:) (or before scheduling work in
scheduleHide()); also clear the flag if the window changes/closes (hook
viewDidMoveToWindow / windowWillClose) so new windows get patched. This keeps
the existing symbols (SplitViewDividerHiderView, updateNSView, scheduleHide,
hideDividers, patchSplitViews, viewDidMoveToWindow) but prevents repeated
full-tree traversals per refresh.

In `@Sources/SessionPersistence.swift`:
- Line 359: The decoded model leaves var sections:
[SessionSidebarSectionSnapshot]? nil; after decoding in SessionPersistence's
Decodable init (or wherever the JSON decoding occurs), set sections to an empty
array when missing (e.g., assign sections = sections ?? []) so the in-memory
shape is non-nil and callers don't need to handle optional sections; reference
the sections property and the SessionSidebarSectionSnapshot type when making the
change.

In `@Sources/TabManager.swift`:
- Around line 5962-5970: The restore logic can place the same workspace ID into
multiple SidebarSection instances; update the restoredSections construction to
deduplicate and enforce one-section-per-workspace by tracking assigned IDs as
you map snapshot.sections: create a mutable Set (e.g., assignedIds) outside the
map, then for each sectionSnapshot compute workspaceIdsFiltered =
sectionSnapshot.workspaceIds.filter { restoredTabIds.contains($0) &&
!assignedIds.contains($0) }, add those filtered IDs into assignedIds, and use
workspaceIdsFiltered when creating the SidebarSection (referencing
restoredTabIds, SidebarSection, restoredSections, and snapshot.sections).

In `@Sources/Workspace.swift`:
- Around line 6778-6803: The boolean hideTabBar in bonsplitAppearance is
misleading because it also controls showSplitButtons; rename it to something
reflecting both effects (e.g., useCompactChrome or hideChromeControls) and
update all uses in that function: set the new variable based on the macOS
availability check, use it for tabBarHeight (compact -> 0 else 33) and for
showSplitButtons (show when not compact), keeping splitButtonTooltips,
enableAnimations, and chromeColors unchanged; ensure the variable name is
updated in the BonsplitConfiguration.Appearance initializer so the intent is
clear.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a88a287-b84d-43f2-9287-334fe1c70915

📥 Commits

Reviewing files that changed from the base of the PR and between 2669b6d and 21e9e70.

📒 Files selected for processing (15)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/AppDelegate.swift
  • Sources/CmuxConfig.swift
  • Sources/CmuxConfigExecutor.swift
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/SessionPersistence.swift
  • Sources/SidebarSection.swift
  • Sources/TabManager.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/Update/UpdateTitlebarAccessory.swift
  • Sources/WindowToolbarController.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • vendor/bonsplit

Comment thread Sources/AppDelegate.swift
Comment on lines 10155 to +10162
func attachUpdateAccessory(to window: NSWindow) {
if #available(macOS 26.0, *) {
// On macOS 26, toolbar buttons are native SwiftUI .toolbar items
// in the NavigationSplitView. Skip attaching the old titlebar
// accessory views, but the controller is already started (for
// notifications popover support).
return
}

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

Unify the macOS 26 notifications popover behind one window-scoped source of truth.

These changes move 26+ away from titlebarAccessoryController by skipping attachment and toggling through a global notification with object: nil. That creates two regressions: every ContentView subscriber can toggle at once, and the rest of AppDelegate still uses controller-backed show/dismiss/query methods that no longer reflect the visible popover. In practice this can fan the popover out across all windows and break menu-bar/Escape handling on macOS 26+.

🧭 Possible starting point
 func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
     if `#available`(macOS 26.0, *) {
-        NotificationCenter.default.post(name: Self.toggleNotificationsPopoverNotification, object: nil)
+        let targetWindow = anchorView?.window ?? NSApp.keyWindow ?? NSApp.mainWindow
+        NotificationCenter.default.post(
+            name: Self.toggleNotificationsPopoverNotification,
+            object: targetWindow
+        )
         return
     }
     titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
 }

Companion changes are still needed so Sources/ContentView.swift filters by the addressed window, and so showNotificationsPopoverFromMenuBar(), dismissNotificationsPopoverIfShown(), and isNotificationsPopoverShown() use that same macOS-26-backed state.

Also applies to: 10171-10179

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 10155 - 10162, The macOS 26 change
removed attaching titlebar accessory views in attachUpdateAccessory(to window:
NSWindow) but left notifications toggling via a global NotificationCenter
broadcast (object: nil), causing all ContentView subscribers to toggle and
desync with controller-backed APIs; fix this by introducing a window-scoped
source-of-truth for the macOS-26 path and wiring everything to it: update
ContentView to filter notification toggle events by the target NSWindow (or a
unique window identifier) instead of listening to object: nil, and modify
showNotificationsPopoverFromMenuBar(), dismissNotificationsPopoverIfShown(), and
isNotificationsPopoverShown() to consult/update the new macOS-26 window-scoped
state (falling back to titlebarAccessoryController behavior on older macOS) so
controller-backed methods remain consistent with visible popovers.

Comment on lines +96 to +112
if wsDef.target == .current {
guard let current = tabManager.selectedWorkspace else { return }
current.setCustomTitle(workspaceName)
if let color = wsDef.color {
current.setCustomColor(color)
}
if let layout = wsDef.layout {
// Close all panels except the focused one so applyCustomLayout
// starts from a single pane and doesn't stack on existing splits.
let keep = current.focusedPanelId
let panelIdsToClose = current.panels.keys.filter { $0 != keep }
for panelId in panelIdsToClose {
current.closePanel(panelId, force: true)
}
current.applyCustomLayout(layout, baseCwd: resolvedCwd)
}
return

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

target: .current bypasses the configured restart policy.

This branch returns before the restart switch below, so restart: .ignore and restart: .confirm still force-close non-focused panels and apply the new layout. That makes reload/auto-apply destructive in cases that should currently select-or-prompt instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/CmuxConfigExecutor.swift` around lines 96 - 112, The .current branch
in CmuxConfigExecutor bypasses the restart handling and always closes panels and
applies layout; update the branch to consult wsDef.restart before mutating the
workspace: retrieve the restart policy from wsDef (e.g., .ignore, .confirm,
.always) at the start of the wsDef.target == .current block and short-circuit
appropriately (for .ignore return without changing panels/layout; for .confirm
invoke the existing confirm prompt/path used by the restart switch to get user
consent) and only perform setCustomTitle/setCustomColor/closing
panels/applyCustomLayout when the restart policy permits; use the same
logic/code path as the later restart switch so behavior is consistent with other
targets.

Comment thread Sources/ContentView.swift
Comment on lines +15795 to +15799
// Clear the divider color via ObjC messaging (private API).
let selector = NSSelectorFromString("setDividerColor:")
if splitView.responds(to: selector) {
splitView.perform(selector, with: NSColor.clear)
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify there are no private selector invocations for split-view divider color.
# Expected result after fix: no matches.
rg -n --type=swift 'NSSelectorFromString\("setDividerColor:"\)|perform\(\s*selector,\s*with:'

Repository: manaflow-ai/cmux

Length of output: 524


🏁 Script executed:

sed -n '15790,15810p' Sources/ContentView.swift

Repository: manaflow-ai/cmux

Length of output: 913


🏁 Script executed:

# Search for additional context about NSSplitView divider manipulation in the codebase
rg -n -B 3 -A 3 'setDividerColor' Sources/

Repository: manaflow-ai/cmux

Length of output: 646


🏁 Script executed:

# Check if there are comments or documentation explaining why setDividerColor is needed
rg -n -B 5 'NSSelectorFromString\("setDividerColor' Sources/ContentView.swift

Repository: manaflow-ai/cmux

Length of output: 421


🏁 Script executed:

# Search for other divider-related properties or methods on NSSplitView in the codebase
rg -n 'divider' Sources/ContentView.swift | head -20

Repository: manaflow-ai/cmux

Length of output: 967


🏁 Script executed:

# Check if there are any appearance or theme-related calls that might control divider color publicly
rg -n 'NSAppearance|dividerColor|separatorColor' Sources/

Repository: manaflow-ai/cmux

Length of output: 3747


🏁 Script executed:

# Look for context about why the divider needs to be cleared/made invisible
rg -n -B 10 'patchSplitViews' Sources/ContentView.swift | head -30

Repository: manaflow-ai/cmux

Length of output: 1017


Remove private AppKit API usage for divider color.

The private setDividerColor: selector via perform() can trigger App Store rejections and binary compatibility issues. This approach is not maintainable across OS updates.

However, note that simply removing this code while keeping only dividerStyle = .thin may not fully hide the dividers as intended—dividerStyle controls thickness/appearance but not visibility/color. Consider:

  1. Using NSSplitView.dividerColor if it's settable publicly in your target OS versions
  2. Exploring alternative layout approaches (e.g., custom NSView with manual layout) if public APIs don't support transparent dividers
  3. If a workaround is unavoidable, document the rationale and test against the latest SDK to catch deprecation warnings early

Remove the private selector code and use only public APIs, or document why a private workaround is necessary with a clear plan to migrate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 15795 - 15799, Remove the use of the
private AppKit selector ("setDividerColor:") invoked via splitView.perform(...)
in the ContentView.swift block (the selector variable and the
responds/to/perform code) and instead rely only on public APIs: set
splitView.dividerStyle = .thin and, if your deployment target supports it, set
splitView.dividerColor via the public NSSplitView API; if dividerColor is
unavailable for your target OS, remove the private workaround and add a short
TODO comment referencing splitView.dividerStyle and the need to implement a
custom NSView-based separator or document the migration plan so we avoid private
API usage.

Comment on lines +4448 to +4456
func reapplyColorSchemeAndConfig() {
guard let surface, let view = attachedView else { return }
// Force the surface to re-derive its config with its current conditional state.
// ghostty_surface_set_color_scheme has an internal dedup that skips when the
// scheme hasn't changed, but after a config reload the underlying theme data
// may have changed. ghostty_surface_update_config bypasses that dedup.
if let config = GhosttyApp.shared.config {
ghostty_surface_update_config(surface, config)
}

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

Use the live-surface guard before calling into Ghostty here.

Line 4449 unwraps surface directly, but this path dereferences the native pointer during a reload pass. That bypasses liveSurfaceForGhosttyAccess(reason:), so a wrapper whose runtime surface was freed out-of-band can still reach ghostty_surface_update_config.

🛡️ Proposed fix
-    func reapplyColorSchemeAndConfig() {
-        guard let surface, let view = attachedView else { return }
+    `@MainActor`
+    func reapplyColorSchemeAndConfig() {
+        guard let view = attachedView,
+              let surface = liveSurfaceForGhosttyAccess(reason: "reapplyColorSchemeAndConfig") else { return }
         // Force the surface to re-derive its config with its current conditional state.
         // ghostty_surface_set_color_scheme has an internal dedup that skips when the
         // scheme hasn't changed, but after a config reload the underlying theme data
         // may have changed. ghostty_surface_update_config bypasses that dedup.
         if let config = GhosttyApp.shared.config {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 4448 - 4456,
reapplyColorSchemeAndConfig currently unwraps `surface` directly and calls
`ghostty_surface_update_config`, which can dereference a freed native pointer;
instead obtain the runtime-safe surface via the wrapper
`liveSurfaceForGhosttyAccess(reason:)` before any native calls—use
`attachedView` to ensure view exists, call `liveSurfaceForGhosttyAccess(reason:
"reapplyColorSchemeAndConfig")` to get a guarded surface, and only then call
`ghostty_surface_update_config(surface, config)` (and bail out if the
live-surface wrapper returns nil) so you never call into Ghostty with a
freed/native-invalid surface.

Comment on lines +9136 to +9148
// On macOS 26, inset the scroll view on the leading and top edges
// to keep terminal text within the rounded corner safe zone.
// Only leading corners are rounded (when sidebar is visible), so
// right/bottom edges use no inset to maximize terminal real estate.
let scrollFrame: CGRect
if #available(macOS 26.0, *) {
let inset: CGFloat = 6
scrollFrame = CGRect(
x: bounds.origin.x + inset,
y: bounds.origin.y,
width: bounds.width - inset,
height: bounds.height - inset
)

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 | 🟡 Minor

Clamp the macOS 26 inset before building the scroll frame.

Lines 9142-9147 subtract a fixed 6pt inset unconditionally. During split churn or tiny intermediate layouts, that can produce negative frame sizes and an over-shifted leading origin, which then bleeds into the scroll/content-size reconciliation path.

📐 Proposed fix
         if `#available`(macOS 26.0, *) {
             let inset: CGFloat = 6
+            let leadingInset = min(inset, bounds.width)
+            let topInset = min(inset, bounds.height)
             scrollFrame = CGRect(
-                x: bounds.origin.x + inset,
+                x: bounds.origin.x + leadingInset,
                 y: bounds.origin.y,
-                width: bounds.width - inset,
-                height: bounds.height - inset
+                width: max(0, bounds.width - leadingInset),
+                height: max(0, bounds.height - topInset)
             )
         } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 9136 - 9148, The macOS 26
branch unconditionally subtracts a 6pt inset when computing scrollFrame which
can yield negative sizes or over-shifted origins during tiny/intermediate
layouts; inside the if `#available`(macOS 26.0, *) block, clamp the inset before
building scrollFrame (e.g., cap inset to no more than half the bounds.width and
bounds.height or ensure bounds.width - inset >= 0 and bounds.height - inset >=
0) and then use that clamped inset when computing x, width and height so
scrollFrame never becomes negative or moves the origin incorrectly.

Comment thread Sources/TabManager.swift
Comment on lines +2686 to +2792
// MARK: - Sidebar Sections

/// Subscribe to every section's `objectWillChange` so that any property
/// mutation (collapse, membership, name) bumps `sectionRevision` and
/// triggers a SwiftUI re-render of the sidebar layout.
private func rebindSectionObservers() {
sectionObserverCancellables.removeAll()
for section in sections {
section.objectWillChange
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.sectionRevision &+= 1
}
.store(in: &sectionObserverCancellables)
}
}

private func notifySectionChange() {
sectionRevision &+= 1
}

@discardableResult
func createSection(name: String) -> SidebarSection {
let section = SidebarSection(name: name)
sections.append(section)
// Delay so SwiftUI renders the new SidebarSectionHeaderView
// (and subscribes to $pendingRenameSectionId) before we emit.
DispatchQueue.main.async { [weak self] in
self?.pendingRenameSectionId = section.id
}
return section
}

func renameSection(sectionId: UUID, name: String) {
guard let section = sections.first(where: { $0.id == sectionId }) else { return }
section.name = name
notifySectionChange()
}

func deleteSection(sectionId: UUID) {
sections.removeAll { $0.id == sectionId }
}

func reorderSection(sectionId: UUID, toIndex targetIndex: Int) {
guard let currentIndex = sections.firstIndex(where: { $0.id == sectionId }) else { return }
let clamped = max(0, min(targetIndex, sections.count - 1))
guard currentIndex != clamped else { return }
let section = sections.remove(at: currentIndex)
sections.insert(section, at: clamped)
}

func moveWorkspaceToSection(tabId: UUID, sectionId: UUID, atIndex: Int? = nil) {
// Validate destination exists before modifying any state.
guard let section = sections.first(where: { $0.id == sectionId }) else { return }
// Remove from any existing section first
for s in sections {
s.removeWorkspace(tabId)
}
section.addWorkspace(tabId, at: atIndex)
notifySectionChange()
}

func removeWorkspaceFromSection(tabId: UUID) {
for section in sections {
section.removeWorkspace(tabId)
}
notifySectionChange()
}

func sectionForWorkspace(_ tabId: UUID) -> SidebarSection? {
sections.first { $0.contains(tabId) }
}

var sidebarLayout: SidebarLayout {
// Read sectionRevision to establish a SwiftUI dependency so the
// layout is recomputed whenever any section property changes.
let _ = sectionRevision
let tabById = Dictionary(uniqueKeysWithValues: tabs.map { ($0.id, $0) })
let pinnedWorkspaces = tabs.filter { $0.isPinned }

// Workspace IDs that are in some section (and not pinned)
var sectionedIds = Set<UUID>()
let sectionGroups: [SidebarLayout.SectionGroup] = sections.map { section in
let workspaces = section.workspaceIds.compactMap { id -> Workspace? in
guard let ws = tabById[id], !ws.isPinned else { return nil }
return ws
}
for ws in workspaces {
sectionedIds.insert(ws.id)
}
return SidebarLayout.SectionGroup(section: section, workspaces: workspaces)
}

let ungroupedWorkspaces = tabs.filter { !$0.isPinned && !sectionedIds.contains($0.id) }
return SidebarLayout(
pinnedWorkspaces: pinnedWorkspaces,
ungroupedWorkspaces: ungroupedWorkspaces,
sectionGroups: sectionGroups
)
}

private func cleanupSectionsForRemovedWorkspace(_ workspaceId: UUID) {
for section in sections {
section.removeWorkspace(workspaceId)
}
notifySectionChange()
}

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

Block flat reorders for grouped workspaces.

Now that section membership is first-class, reorderWorkspace(...) should reject grouped tabs. Otherwise flat-array reorder calls can mutate tabs order for sectioned items and conflict with section-aware ordering behavior.

Suggested fix
 `@discardableResult`
 func reorderWorkspace(tabId: UUID, toIndex targetIndex: Int) -> Bool {
+    guard sectionForWorkspace(tabId) == nil else { return false }
     guard let currentIndex = tabs.firstIndex(where: { $0.id == tabId }) else { return false }
     if tabs.count <= 1 { return true }

     let workspace = tabs[currentIndex]
     let clamped = clampedReorderIndex(for: workspace, targetIndex: targetIndex)
     if currentIndex == clamped { return true }

     tabs.remove(at: currentIndex)
     tabs.insert(workspace, at: clamped)
     return true
 }

 `@discardableResult`
 func reorderWorkspace(tabId: UUID, before beforeId: UUID? = nil, after afterId: UUID? = nil) -> Bool {
+    guard sectionForWorkspace(tabId) == nil else { return false }
+    if let beforeId, sectionForWorkspace(beforeId) != nil { return false }
+    if let afterId, sectionForWorkspace(afterId) != nil { return false }
     guard tabs.contains(where: { $0.id == tabId }) else { return false }
     if let beforeId {
         guard let idx = tabs.firstIndex(where: { $0.id == beforeId }) else { return false }
         return reorderWorkspace(tabId: tabId, toIndex: idx)
     }

Based on learnings: reorderWorkspace(...) must reject grouped tabs (early return); flat-array reorders are not allowed for workspaces inside a group.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TabManager.swift` around lines 2686 - 2792, reorderWorkspace(...)
must ignore attempts to perform flat-array reorders for workspaces that are
inside a section; add an early return at the start of the
reorderWorkspace(tabId:toIndex:) implementation that checks
sectionForWorkspace(tabId) and returns immediately if it yields a SidebarSection
(i.e., the workspace is grouped), so the function no longer mutates tabs
ordering for sectioned items.

Comment on lines +1499 to +1515
// On macOS 26, round the terminal's leading corners when a sidebar
// is visible to its left, matching the NavigationSplitView glass shape.
// Applied inside the CATransaction to prevent animation flicker.
if #available(macOS 26.0, *) {
// Detect sidebar presence via x-offset. The sidebar has a minimum
// width of 120pt, so any x > 20 reliably indicates a sidebar is
// to our left. This AppKit view cannot access SwiftUI SidebarState
// directly; the frame-based heuristic is the simplest reliable path.
let hasSidebarToLeft = targetFrame.origin.x > 20
let desiredRadius: CGFloat = hasSidebarToLeft ? 16 : 0
if hostedView.layer?.cornerRadius != desiredRadius {
hostedView.layer?.cornerRadius = desiredRadius
hostedView.layer?.maskedCorners = hasSidebarToLeft
? [.layerMinXMinYCorner, .layerMinXMaxYCorner]
: []
}
}

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 | 🟡 Minor

Don't use pane X-offset as the sidebar signal.

Line 1507 will also be true for any non-leading split pane, so right-hand panes will get rounded inside corners even when the sidebar is hidden. Please gate this to panes that are actually on the outer leading edge, or reuse the existing sidebar-edge detection instead of a fixed > 20 heuristic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalWindowPortal.swift` around lines 1499 - 1515, The current
corner-radius heuristic in TerminalWindowPortal using targetFrame.origin.x > 20
is too broad and rounds inner corners for right-side panes; replace this
x-offset check with a reliable detection of leading-edge panes (reuse the
existing sidebar-edge detection helper or API already present in this
class/module) so you only set desiredRadius and maskedCorners when the pane is
actually on the outer leading edge; update the conditional around
hostedView.layer?.cornerRadius and maskedCorners to call that helper (or a
method like isPaneOnLeadingEdge) instead of comparing origin.x, keeping the
macOS 26 availability guard and the CATransaction-safe update logic.

Comment on lines +164 to 177
if #available(macOS 26.0, *) {
return [sidebarToggleIdentifier, notificationsIdentifier, newTabIdentifier,
.flexibleSpace, commandItemIdentifier]
}
return [commandItemIdentifier, .flexibleSpace]
}

func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[commandItemIdentifier, .flexibleSpace]
if #available(macOS 26.0, *) {
return [sidebarToggleIdentifier, notificationsIdentifier, newTabIdentifier,
.flexibleSpace, commandItemIdentifier]
}
return [commandItemIdentifier, .flexibleSpace]
}

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect macOS 26 toolbar composition across AppKit and SwiftUI layers.
# Expected: a single source of truth for sidebar/notifications/new-workspace controls.

rg -n -C6 'NavigationSplitView|toolbar\(removing:\s*\.sidebarToggle\)|ToolbarItemGroup|Image\(systemName:\s*"sidebar|Image\(systemName:\s*"bell|Image\(systemName:\s*"plus|newSurface|newWorkspace' Sources/ContentView.swift

rg -n -C6 'sidebarToggleIdentifier|notificationsIdentifier|newTabIdentifier|toolbarDefaultItemIdentifiers|toolbarAllowedItemIdentifiers' Sources/WindowToolbarController.swift

Repository: manaflow-ai/cmux

Length of output: 13141


🏁 Script executed:

# Find where WindowToolbarController is attached to the window
rg -n 'WindowToolbarController|NSWindow.*toolbar|window.toolbar' Sources/ --type swift -B2 -A2 | head -80

Repository: manaflow-ai/cmux

Length of output: 3416


🏁 Script executed:

# Check AppDelegate window creation to see toolbar setup
rg -n 'createMainWindow|setWindowToolbar|NSToolbar' Sources/AppDelegate.swift -B3 -A3 | head -100

Repository: manaflow-ai/cmux

Length of output: 2531


🏁 Script executed:

# Check if there's any code that conditionally disables SwiftUI toolbar on macOS 26
rg -n 'toolbar.*macOS.*26|#available.*macOS.*26.*toolbar' Sources/ContentView.swift -B2 -A2

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Find where WindowToolbarController is created/instantiated
rg -n 'WindowToolbarController\(\)|WindowToolbarController\.shared|WindowToolbarController\.init' Sources/ --type swift -B2 -A2

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check _isMinimal or visibility state in WindowToolbarController
rg -n '_isMinimal|_visible|setupMinimalToggle' Sources/WindowToolbarController.swift -B3 -A3

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look for where ContentView's root window is created to see toolbar initialization order
rg -n 'ContentView\(\)|WindowScene|\.body.*ContentView' Sources/ContentView.swift | head -20

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check main window creation in AppDelegate to understand toolbar setup flow
sed -n '7038,7100p' Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 3074


🏁 Script executed:

# Search for WindowToolbarController creation/init in AppDelegate
rg -n 'WindowToolbarController' Sources/AppDelegate.swift -B2 -A2

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check cmuxApp.swift for WindowToolbarController setup
rg -n 'WindowToolbarController' Sources/cmuxApp.swift -B3 -A3

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Look for where toolbar items are actually populated - check the full toolbar delegate method
sed -n '179,230p' Sources/WindowToolbarController.swift

Repository: manaflow-ai/cmux

Length of output: 3023


🏁 Script executed:

# Search more broadly for WindowToolbarController usage across entire repo
git ls-files -z | xargs -0 grep -l 'WindowToolbarController' 2>/dev/null

Repository: manaflow-ai/cmux

Length of output: 137


🏁 Script executed:

# Check if WindowToolbarController is even instantiated anywhere
rg 'WindowToolbarController()' --type swift -g '!Sources/WindowToolbarController.swift'

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Search for where attach method is called
rg 'attach\(to:' Sources/ --type swift -B2 -A2

Repository: manaflow-ai/cmux

Length of output: 1310


🏁 Script executed:

# Check if there's a singleton or static property for WindowToolbarController
rg 'static.*WindowToolbarController|shared.*WindowToolbarController|WindowToolbarController\.shared' Sources/ --type swift

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check the complete WindowToolbarController class to see if it's a singleton or how it's instantiated
head -100 Sources/WindowToolbarController.swift

Repository: manaflow-ai/cmux

Length of output: 3633


🏁 Script executed:

# Look at the top of AppDelegate to see if WindowToolbarController is created there
head -200 Sources/AppDelegate.swift | grep -n 'class AppDelegate\|init\|WindowToolbarController'

Repository: manaflow-ai/cmux

Length of output: 246


🏁 Script executed:

# Check if there's any `@NSApplicationDelegateAdaptor` or initialization in cmuxApp
rg -n 'WindowToolbarController|NSApplicationDelegateAdaptor' Sources/cmuxApp.swift -B3 -A3

Repository: manaflow-ai/cmux

Length of output: 711


🏁 Script executed:

# Find where WindowToolbarController.start is called
rg -n '\.start\(' Sources/ --type swift | grep -i 'windowtoolbar\|tabManager'

Repository: manaflow-ai/cmux

Length of output: 339


🏁 Script executed:

# Look at AppDelegate initialization/window creation to see if WindowToolbarController is started
rg -n 'func application|didFinishLaunching|applicationDidFinishLaunching' Sources/AppDelegate.swift -A20 | head -50

Repository: manaflow-ai/cmux

Length of output: 2325


🏁 Script executed:

# Check if there's a specific place where WindowToolbarController is instantiated and started
rg -n 'let.*toolbarController|var.*toolbarController|WindowToolbarController()' Sources/ --type swift -B2 -A2

Repository: manaflow-ai/cmux

Length of output: 515


🏁 Script executed:

# Search for WindowToolbarController as a property in AppDelegate or elsewhere
rg -n 'var.*WindowToolbarController|let.*WindowToolbarController' Sources/ --type swift -B2 -A2

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Check the createMainWindow function more thoroughly to see if toolbar is set up there
sed -n '7038,7150p' Sources/AppDelegate.swift | grep -n -i 'toolbar\|window\|contentview'

Repository: manaflow-ai/cmux

Length of output: 3437


🏁 Script executed:

# Look for where window's contentView is set in createMainWindow
sed -n '7100,7200p' Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 4095


🏁 Script executed:

# Do a comprehensive search for any instantiation or usage of WindowToolbarController
rg 'WindowToolbarController' Sources/ --type swift

Repository: manaflow-ai/cmux

Length of output: 164


🏁 Script executed:

# Check if WindowToolbarController is referenced in the entire project
git grep -n 'WindowToolbarController'

Repository: manaflow-ai/cmux

Length of output: 761


Remove or complete the inactive WindowToolbarController implementation.

WindowToolbarController defines macOS 26 toolbar items (sidebar, notifications, new workspace) but is never instantiated or started in the codebase. Its start(tabManager:) method is never called, so the NSToolbar layer remains unused. Only ContentView's SwiftUI .toolbar() with ToolbarItemGroup(.primaryAction) is active. Either remove this dead code or complete the integration if it was intended for future use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/WindowToolbarController.swift` around lines 164 - 177, The
WindowToolbarController implementation is dead (defines
toolbarDefaultItemIdentifiers, toolbarAllowedItemIdentifiers and toolbar item
identifiers such as sidebarToggleIdentifier, notificationsIdentifier,
newTabIdentifier and has a start(tabManager:) method that is never called);
either remove the entire WindowToolbarController class and its identifier
constants to eliminate unused NSToolbar code, or wire it up by instantiating
WindowToolbarController and calling start(tabManager:) during window/app setup
(e.g., where NSWindow is created), attach its NSToolbar to the window, and
ensure toolbarDefaultItemIdentifiers/toolbarAllowedItemIdentifiers are used by
setting the toolbar.delegate to the controller so the defined items
(sidebarToggleIdentifier, notificationsIdentifier, newTabIdentifier,
commandItemIdentifier) become active.

Comment on lines +230 to +243
@objc private func toggleSidebarAction() {
_ = AppDelegate.shared?.sidebarState?.toggle()
}

@objc private func toggleNotificationsAction() {
_ = AppDelegate.shared?.toggleNotificationsPopover(animated: true)
}

@objc private func newTabAction() {
if let appDelegate = AppDelegate.shared {
if appDelegate.addWorkspaceInPreferredMainWindow(debugSource: "toolbar.newTab") == nil {
appDelegate.openNewMainWindow(nil)
}
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether toolbar actions are window-scoped or app-global.
# Expected: evidence that routing uses the clicked window context (or confirm it does not).

rg -n -C4 '\bsidebarState\b|func\s+addWorkspaceInPreferredMainWindow|func\s+openNewMainWindow|func\s+registerMainWindow' Sources/AppDelegate.swift

rg -n -C4 'toggleSidebarAction|toggleNotificationsAction|newTabAction|toolbar\(_:itemForItemIdentifier' Sources/WindowToolbarController.swift

rg -n -C4 'class\s+MainWindowRegistration|struct\s+MainWindowRegistration|mainWindow.*tabManager|active.*tabManager' Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 21055


🏁 Script executed:

# Verify WindowToolbarController ownership and window association
rg -n -B5 -A15 'class\s+WindowToolbarController|struct\s+WindowToolbarController' Sources/WindowToolbarController.swift

# Check if WindowToolbarController has a window property
rg -n 'var\s+window|weak.*window|let.*window' Sources/WindowToolbarController.swift

# Check how WindowToolbarController is instantiated and associated with windows
rg -n 'WindowToolbarController' Sources/AppDelegate.swift

# Check other toolbar-related usage in ContentView or window setup
rg -n 'WindowToolbarController' Sources/ContentView.swift

Repository: manaflow-ai/cmux

Length of output: 1096


🏁 Script executed:

wc -l Sources/WindowToolbarController.swift

Repository: manaflow-ai/cmux

Length of output: 101


🏁 Script executed:

cat -n Sources/WindowToolbarController.swift

Repository: manaflow-ai/cmux

Length of output: 11545


🏁 Script executed:

# Verify whether AppDelegate.sidebarState represents active window and check the contextForMainWindow routing pattern
rg -n -B3 -A3 'func contextForMainWindow|func preferredMainWindowContextForWorkspaceCreation|private func setActiveMainWindow' Sources/AppDelegate.swift | head -60

Repository: manaflow-ai/cmux

Length of output: 896


🏁 Script executed:

# See the complete setActiveMainWindow implementation and its effects
rg -n -A30 'private func setActiveMainWindow' Sources/AppDelegate.swift | head -50

Repository: manaflow-ai/cmux

Length of output: 1936


🏁 Script executed:

# Check addWorkspaceInPreferredMainWindow implementation for proper window routing
rg -n -A50 'func addWorkspaceInPreferredMainWindow' Sources/AppDelegate.swift | head -80

Repository: manaflow-ai/cmux

Length of output: 2088


🏁 Script executed:

# Check if toggleSidebarInActiveMainWindow is actually being used and how it handles window routing correctly
rg -n -B5 -A25 'func toggleSidebarInActiveMainWindow' Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 1524


Use the existing cross-window routing method for sidebar and notification actions.

toggleSidebarAction() and toggleNotificationsAction() currently manipulate global AppDelegate properties without window context. In multi-window sessions, this toggles the active/key window's sidebar instead of the window whose toolbar button was clicked.

Replace AppDelegate.shared?.sidebarState?.toggle() with AppDelegate.shared?.toggleSidebarInActiveMainWindow(), which already implements the correct routing pattern (active manager → key window → main window → fallback). Similarly, check whether toggleNotificationsPopover() should route per-window or if it intentionally targets the active window; if per-window routing is needed, apply the same pattern.

The newTabAction() correctly uses addWorkspaceInPreferredMainWindow() and should be the model for other toolbar actions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/WindowToolbarController.swift` around lines 230 - 243,
toggleSidebarAction toggles the global sidebarState on AppDelegate which affects
the key/active window rather than the window whose toolbar was clicked; update
toggleSidebarAction to call
AppDelegate.shared?.toggleSidebarInActiveMainWindow() so it uses the established
cross-window routing pattern (see newTabAction's use of
addWorkspaceInPreferredMainWindow as the model). For toggleNotificationsAction,
decide if notifications should be routed per-window—if so, replace the direct
call to AppDelegate.shared?.toggleNotificationsPopover(animated:) with the
equivalent per-window routing helper (mirror the active-main-window routing used
by toggleSidebarInActiveMainWindow); otherwise leave it as-is. Ensure you only
change toggleSidebarAction (and toggleNotificationsAction if applying per-window
routing) and keep newTabAction unchanged as the correct pattern example.

Comment thread vendor/bonsplit
@@ -1 +1 @@
Subproject commit b2788b1e77d43f0c114dcf189aa59cae8abb47de
Subproject commit cfff8a9318f8131604681e4cb86c11925e01bf1e

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

SUBMODULE_PATH="vendor/bonsplit"
EXPECTED_COMMIT="cfff8a9318f8131604681e4cb86c11925e01bf1e"

echo "== Resolve gitlink pointer from parent repo =="
POINTER="$(git ls-tree HEAD "$SUBMODULE_PATH" | awk '{print $3}')"
echo "Pointer in parent: $POINTER"
test "$POINTER" = "$EXPECTED_COMMIT"

echo "== Ensure submodule is present =="
if [ ! -e "$SUBMODULE_PATH/.git" ]; then
  git submodule update --init "$SUBMODULE_PATH"
fi

echo "== Fetch bonsplit main and verify ancestry =="
git -C "$SUBMODULE_PATH" fetch origin main --quiet
if git -C "$SUBMODULE_PATH" merge-base --is-ancestor "$EXPECTED_COMMIT" origin/main; then
  echo "OK: commit is reachable from origin/main"
else
  echo "FAIL: commit is NOT on origin/main"
  exit 1
fi

echo "== Verify key Bonsplit API symbols still exist =="
rg -n --type=swift -C2 '\bBonsplitConfiguration\.Appearance\b|\btabBarHeight\b|\bshowSplitButtons\b|\bchromeColors\b' "$SUBMODULE_PATH"
rg -n --type=swift -C2 '\bclass\s+BonsplitController\b|\bonExternalTabDrop\b|\bonTabCloseRequest\b|\btabs\s*\(inPane:\s*|\bselectedTab\s*\(inPane:\s*|\bfocusedPaneId\b' "$SUBMODULE_PATH"

echo "== Show commit metadata =="
git -C "$SUBMODULE_PATH" show --no-patch --oneline "$EXPECTED_COMMIT"

Repository: manaflow-ai/cmux

Length of output: 689


Push the bonsplit commit to origin/main before updating the parent repo pointer.

The submodule pointer cfff8a9318f8131604681e4cb86c11925e01bf1e does not exist in the manaflow-ai/bonsplit remote. Git failed to fetch it with:

fatal: remote error: upload-pack: not our ref cfff8a9318f8131604681e4cb86c11925e01bf1e

Per submodule workflow, you must push the bonsplit commit to the manaflow-ai/bonsplit main branch first, then update the parent pointer. Verify the commit is reachable from origin/main with:

cd vendor/bonsplit && git merge-base --is-ancestor cfff8a9318f8131604681e4cb86c11925e01bf1e origin/main
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/bonsplit` at line 1, The parent repo points to a submodule commit
(cfff8a9318f8131604681e4cb86c11925e01bf1e) that hasn't been pushed to the
bonsplit remote; push that commit to the bonsplit repository's main branch (push
the local bonsplit commit to manaflow-ai/bonsplit main), then update the parent
repo pointer. After pushing, verify the commit is reachable from origin/main
using git merge-base --is-ancestor cfff8a9318f8131604681e4cb86c11925e01bf1e
origin/main (run this inside the bonsplit checkout), and only then update/commit
the submodule pointer in the parent repo.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

5 issues found across 15 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/TerminalWindowPortal.swift">

<violation number="1" location="Sources/TerminalWindowPortal.swift:1507">
P3: Sidebar presence is inferred solely from `targetFrame.origin.x > 20`, so any non-leftmost split pane will be treated as sidebar-adjacent and get rounded leading corners even when no sidebar is present or only the left pane touches it.</violation>
</file>

<file name="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:2982">
P2: Notifications popover toggling listens to a global notification and blindly toggles local state, so a single toggle event can flip popover state in every window and desynchronize visibility across windows.</violation>
</file>

<file name="Sources/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:10161">
P2: macOS 26 skips accessory attachment, but menu-bar notifications still require attached accessory controllers, making the popover path a no-op.</violation>
</file>

<file name="Sources/CmuxConfig.swift">

<violation number="1" location="Sources/CmuxConfig.swift:452">
P1: Auto-apply executes local workspace config implicitly on tab/workspace activation without trust/confirmation gating, enabling untrusted cmux.json to trigger actions automatically.</violation>
</file>

<file name="Sources/cmuxApp.swift">

<violation number="1" location="Sources/cmuxApp.swift:354">
P2: `.windowStyle(.hiddenTitleBar)` was removed from the `WindowGroup` during the `mainWindowContent` extraction and not conditionally re-applied for pre-macOS 26. While NSWindow properties like `titlebarAppearsTransparent` and `titleVisibility` partially compensate, `.windowStyle(.hiddenTitleBar)` also affects SwiftUI's own layout calculations and window creation, potentially causing subtle layout differences on macOS 13–15.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/CmuxConfig.swift
}) else { return }

autoAppliedWorkspaceIds.insert(workspace.id)
CmuxConfigExecutor.execute(

@cubic-dev-ai cubic-dev-ai Bot Apr 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Auto-apply executes local workspace config implicitly on tab/workspace activation without trust/confirmation gating, enabling untrusted cmux.json to trigger actions automatically.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/CmuxConfig.swift, line 452:

<comment>Auto-apply executes local workspace config implicitly on tab/workspace activation without trust/confirmation gating, enabling untrusted cmux.json to trigger actions automatically.</comment>

<file context>
@@ -381,6 +415,47 @@ final class CmuxConfigStore: ObservableObject {
+        }) else { return }
+
+        autoAppliedWorkspaceIds.insert(workspace.id)
+        CmuxConfigExecutor.execute(
+            command: command,
+            tabManager: tabManager,
</file context>
Fix with Cubic

Comment thread Sources/ContentView.swift
onDismiss: { isNotificationsPopoverPresented = false }
)
}
.onReceive(NotificationCenter.default.publisher(for: AppDelegate.toggleNotificationsPopoverNotification)) { _ in

@cubic-dev-ai cubic-dev-ai Bot Apr 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Notifications popover toggling listens to a global notification and blindly toggles local state, so a single toggle event can flip popover state in every window and desynchronize visibility across windows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 2982:

<comment>Notifications popover toggling listens to a global notification and blindly toggles local state, so a single toggle event can flip popover state in every window and desynchronize visibility across windows.</comment>

<file context>
@@ -2869,6 +2882,124 @@ struct ContentView: View {
+                                onDismiss: { isNotificationsPopoverPresented = false }
+                            )
+                        }
+                        .onReceive(NotificationCenter.default.publisher(for: AppDelegate.toggleNotificationsPopoverNotification)) { _ in
+                            isNotificationsPopoverPresented.toggle()
+                        }
</file context>
Fix with Cubic

Comment thread Sources/AppDelegate.swift
// in the NavigationSplitView. Skip attaching the old titlebar
// accessory views, but the controller is already started (for
// notifications popover support).
return

@cubic-dev-ai cubic-dev-ai Bot Apr 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: macOS 26 skips accessory attachment, but menu-bar notifications still require attached accessory controllers, making the popover path a no-op.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/AppDelegate.swift, line 10161:

<comment>macOS 26 skips accessory attachment, but menu-bar notifications still require attached accessory controllers, making the popover path a no-op.</comment>

<file context>
@@ -10119,6 +10153,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
+            // in the NavigationSplitView. Skip attaching the old titlebar
+            // accessory views, but the controller is already started (for
+            // notifications popover support).
+            return
+        }
         titlebarAccessoryController.start()
</file context>
Fix with Cubic

Comment thread Sources/cmuxApp.swift
}

var body: some Scene {
WindowGroup { mainWindowContent }

@cubic-dev-ai cubic-dev-ai Bot Apr 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: .windowStyle(.hiddenTitleBar) was removed from the WindowGroup during the mainWindowContent extraction and not conditionally re-applied for pre-macOS 26. While NSWindow properties like titlebarAppearsTransparent and titleVisibility partially compensate, .windowStyle(.hiddenTitleBar) also affects SwiftUI's own layout calculations and window creation, potentially causing subtle layout differences on macOS 13–15.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/cmuxApp.swift, line 354:

<comment>`.windowStyle(.hiddenTitleBar)` was removed from the `WindowGroup` during the `mainWindowContent` extraction and not conditionally re-applied for pre-macOS 26. While NSWindow properties like `titlebarAppearsTransparent` and `titleVisibility` partially compensate, `.windowStyle(.hiddenTitleBar)` also affects SwiftUI's own layout calculations and window creation, potentially causing subtle layout differences on macOS 13–15.</comment>

<file context>
@@ -316,40 +316,42 @@ struct cmuxApp: App {
+    }
+
+    var body: some Scene {
+        WindowGroup { mainWindowContent }
         .commands {
             CommandGroup(replacing: .appSettings) {
</file context>
Fix with Cubic

// width of 120pt, so any x > 20 reliably indicates a sidebar is
// to our left. This AppKit view cannot access SwiftUI SidebarState
// directly; the frame-based heuristic is the simplest reliable path.
let hasSidebarToLeft = targetFrame.origin.x > 20

@cubic-dev-ai cubic-dev-ai Bot Apr 8, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Sidebar presence is inferred solely from targetFrame.origin.x > 20, so any non-leftmost split pane will be treated as sidebar-adjacent and get rounded leading corners even when no sidebar is present or only the left pane touches it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TerminalWindowPortal.swift, line 1507:

<comment>Sidebar presence is inferred solely from `targetFrame.origin.x > 20`, so any non-leftmost split pane will be treated as sidebar-adjacent and get rounded leading corners even when no sidebar is present or only the left pane touches it.</comment>

<file context>
@@ -1496,6 +1496,23 @@ final class WindowTerminalPortal: NSObject {
+                // width of 120pt, so any x > 20 reliably indicates a sidebar is
+                // to our left. This AppKit view cannot access SwiftUI SidebarState
+                // directly; the frame-based heuristic is the simplest reliable path.
+                let hasSidebarToLeft = targetFrame.origin.x > 20
+                let desiredRadius: CGFloat = hasSidebarToLeft ? 16 : 0
+                if hostedView.layer?.cornerRadius != desiredRadius {
</file context>
Fix with Cubic

@greptile-apps

greptile-apps Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes terminal surfaces displaying stale theme colors (e.g. light foreground on dark background) after a Ghostty config reload by adding reapplyColorSchemeAndConfig() to each surface, which calls ghostty_surface_update_config to bypass the internal dedup in ghostty_surface_set_color_scheme, then re-asserts the macOS appearance color scheme. Alongside the bug fix, the PR also ships sidebar sections, macOS 26 UI updates (NavigationSplitView, glass sidebar), session persistence for workspace UUIDs and sections, and a new autoApply/target: "current" config-command feature.

Confidence Score: 5/5

Safe to merge — the core bug fix is correct and all remaining findings are P2 style/documentation suggestions.

The primary fix (reapplyColorSchemeAndConfig + ghostty_surface_update_config) is correctly sequenced after the config swap and properly uses the updated GhosttyApp.shared.config. New features (sidebar sections, session persistence, autoApply) use backwards-compatible optional fields and well-guarded logic. All open findings are P2: the nil-config silent skip is an edge case unlikely in steady state, the autoApply restriction is a documentation gap rather than a defect, and the session-restore overwrite is arguably intentional behavior.

Sources/CmuxConfig.swift — autoApply behavior for non-target:current commands and session-restore interaction worth a second look before shipping docs.

Vulnerabilities

No security concerns identified. The autoApply feature correctly restricts auto-execution to workspace layout commands (target: current) only, explicitly excluding arbitrary shell command entries — preventing untrusted config files from running code silently on workspace switch.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds reapplyColorSchemeAndConfig() which calls ghostty_surface_update_config + applySurfaceColorScheme(force:true) to bypass Ghostty's dedup after config reload; nil-config path silently skips the update_config step (P2).
Sources/AppDelegate.swift Calls reapplyColorSchemeAndConfig() on each terminal panel inside refreshTerminalSurfacesAfterGhosttyConfigReload; logic is straightforward and well-sequenced after the existing forceRefresh call.
Sources/CmuxConfig.swift Introduces autoApply/target fields and checkAutoApply gated on target: current workspace commands only; shell commands and target: new commands silently ignore autoApply: true (P2), and session-restored single-pane workspaces are eligible for layout overwrite (P2).
Sources/CmuxConfigExecutor.swift Adds target: current branch that closes all non-focused panels and applies a custom layout in-place; logic is clean and the early-return on missing selected workspace prevents silent no-op.
Sources/SidebarSection.swift New model for collapsible sidebar sections with ordered workspace IDs; addWorkspace correctly deduplicates before insertion and SidebarLayout.allWorkspacesInOrder provides a clear flat ordering for navigation.
Sources/SessionPersistence.swift Adds optional id: UUID? to SessionWorkspaceSnapshot and optional sections to SessionTabManagerSnapshot for backwards-compatible persistence; schema version correctly remains at 1 since all new fields are optional.
Sources/TabManager.swift Adds sections: [SidebarSection] with observer rebinding on didSet, sectionRevision bump for SwiftUI dependency tracking, and sidebarLayout computed property; section persistence and restore handle filtered workspace IDs correctly.

Sequence Diagram

sequenceDiagram
    participant User
    participant GhosttyApp
    participant AppDelegate
    participant TerminalSurface

    User->>GhosttyApp: Reload Configuration
    GhosttyApp->>GhosttyApp: ghostty_app_update_config(app, newConfig)
    GhosttyApp->>GhosttyApp: config = newConfig
    GhosttyApp->>GhosttyApp: scheduleSurfaceRefreshAfterConfigurationReload()
    Note over GhosttyApp: DispatchQueue.main.async

    GhosttyApp->>AppDelegate: refreshTerminalSurfacesAfterGhosttyConfigReload()
    loop Each TerminalPanel
        AppDelegate->>TerminalSurface: reconcileGeometryNow()
        AppDelegate->>TerminalSurface: refreshHostBackgroundAfterGhosttyConfigReload()
        AppDelegate->>TerminalSurface: forceRefresh()
        AppDelegate->>TerminalSurface: reapplyColorSchemeAndConfig()
        Note over TerminalSurface: ghostty_surface_update_config(surface, config) bypasses set_color_scheme dedup
        TerminalSurface->>TerminalSurface: applySurfaceColorScheme(force: true)
    end
Loading

Reviews (1): Last reviewed commit: "Fix terminal surfaces losing theme after..." | Re-trigger Greptile

Comment thread Sources/CmuxConfig.swift
Comment on lines +447 to +449
guard let command = loadedCommands.first(where: {
$0.autoApply == true && $0.workspace?.target == .current
}) else { return }

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 autoApply silently ignored for non-target: current commands

The filter $0.autoApply == true && $0.workspace?.target == .current means autoApply: true is a no-op for both shell command entries and workspace commands with target: "new" (the default). A user who sets autoApply: true on a shell command or a target: new workspace command will get no feedback about why it never fires.

Consider either logging a warning when a non-applicable command has autoApply: true, or documenting the restriction in the CmuxCommandDefinition field comment so the constraint is visible at the schema level.

Comment on lines +4448 to +4460
func reapplyColorSchemeAndConfig() {
guard let surface, let view = attachedView else { return }
// Force the surface to re-derive its config with its current conditional state.
// ghostty_surface_set_color_scheme has an internal dedup that skips when the
// scheme hasn't changed, but after a config reload the underlying theme data
// may have changed. ghostty_surface_update_config bypasses that dedup.
if let config = GhosttyApp.shared.config {
ghostty_surface_update_config(surface, config)
}
// Re-apply color scheme to ensure the surface's conditional state matches
// the current macOS appearance, in case it drifted.
view.applySurfaceColorScheme(force: true)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Silent skip when GhosttyApp.shared.config is nil

If GhosttyApp.shared.config is nil (e.g. during a failed config allocation), ghostty_surface_update_config is skipped while applySurfaceColorScheme(force: true) still runs. This means the surface gets the scheme re-applied but not the re-derived config, leaving theme colors potentially stale — the exact bug this PR fixes. While this path is unlikely in steady state, a log line or assert would make the nil case observable.

if let config = GhosttyApp.shared.config {
    ghostty_surface_update_config(surface, config)
} else {
    // config unavailable — scheme will be re-applied but config re-derivation is skipped
    #if DEBUG
    dlog("reapplyColorSchemeAndConfig: skipped surface_update_config — no app config")
    #endif
}

Comment thread Sources/CmuxConfig.swift
Comment on lines +427 to +436
private func checkAutoApply(forTabId: UUID? = nil) {
guard let tabManager = trackedTabManager,
let workspace = tabManager.selectedWorkspace,
// If a specific tab ID was requested, verify it's still selected.
forTabId == nil || workspace.id == forTabId,
!autoAppliedWorkspaceIds.contains(workspace.id),
// Only auto-apply to workspaces with a single pane — don't tear
// down user-customized layouts or restored split configurations.
workspace.panels.count <= 1
else { return }

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 Session-restored single-pane workspaces eligible for autoApply overwrite

autoAppliedWorkspaceIds starts empty each session, so a restored single-pane workspace (panels.count <= 1) that hasn't been auto-applied yet will have its layout overwritten by target: current on first selection. Because closePanel(..., force: true) is called for all but the focused panel before applyCustomLayout, any scrollback or state in that pane is lost.

If the intent is to skip auto-apply on restored workspaces, you could seed autoAppliedWorkspaceIds from the restored workspace IDs during restoreFromSnapshot, or add a dedicated wasRestoredFromSession flag on Workspace.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants