Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
853003f
Add collapsible user-defined sidebar sections
rodchristiansen Apr 5, 2026
8d3c578
Add SidebarSection.swift to Xcode project
rodchristiansen Apr 5, 2026
14a0980
Fix sidebar section reactivity, rename focus, and live updates
rodchristiansen Apr 5, 2026
10f2d7a
Adopt macOS 26 Liquid Glass design
rodchristiansen Apr 6, 2026
352ad20
Fix split divider position and persist sidebar sections
rodchristiansen Apr 5, 2026
0519a81
Persist sidebar sections across restart and auto-rename on create
rodchristiansen Apr 5, 2026
7662dde
Add workspace target:current and fix section rename focus race
rodchristiansen Apr 5, 2026
6a15254
Fix target:current decoding, section reorder, and rename focus
rodchristiansen Apr 5, 2026
93a9341
Fix target:current re-splitting and swap Files/Lazygit order
rodchristiansen Apr 5, 2026
ad605e1
Add autoApply for workspace commands on tab switch
rodchristiansen Apr 5, 2026
2e9b441
Fix autoApply: add direct workspace selection observer
rodchristiansen Apr 5, 2026
c4378cf
Fix autoApply: check pane count instead of panel count
rodchristiansen Apr 5, 2026
27ddeee
AutoApply: track per-session, fire once per workspace regardless of l…
rodchristiansen Apr 5, 2026
2910b82
Merge branch 'liquid-glass-design'
rodchristiansen Apr 6, 2026
1951e3a
Address Liquid Glass PR review feedback
rodchristiansen Apr 6, 2026
2f90041
Address sidebar sections PR review feedback
rodchristiansen Apr 6, 2026
93d63ad
Merge branch 'feature/sidebar-sections'
rodchristiansen Apr 6, 2026
598ca17
Merge branch 'liquid-glass-design'
rodchristiansen Apr 6, 2026
2c3e61d
Fix notifications popover, toolbar buttons, and sidebar controls for …
rodchristiansen Apr 7, 2026
65db12c
Address remaining Liquid Glass PR review feedback
rodchristiansen Apr 7, 2026
69e6439
Address remaining sidebar sections PR review feedback
rodchristiansen Apr 7, 2026
08cad14
Merge branch 'liquid-glass-design'
rodchristiansen Apr 7, 2026
d4a9ae3
Merge branch 'feature/sidebar-sections'
rodchristiansen Apr 7, 2026
df47ffe
Fix sidebar rows not filling width and remove legacy titlebar control…
rodchristiansen Apr 7, 2026
21e9e70
Fix terminal surfaces losing theme after config reload
rodchristiansen Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
A5001001 /* cmuxApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001011 /* cmuxApp.swift */; };
A5001002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001012 /* ContentView.swift */; };
E62155868BB29FEB5DAAAF25 /* SidebarSelectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD52285508B1D6A9875E7B3 /* SidebarSelectionState.swift */; };
A1B2C3D4E5F60001DEADBEEF /* SidebarSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60002DEADBEEF /* SidebarSection.swift */; };
B9000018A1B2C3D4E5F60719 /* WindowDragHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */; };
A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; };
A5001004 /* GhosttyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001014 /* GhosttyConfig.swift */; };
Expand Down Expand Up @@ -211,6 +212,7 @@
A5001011 /* cmuxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cmuxApp.swift; sourceTree = "<group>"; };
A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
9AD52285508B1D6A9875E7B3 /* SidebarSelectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSelectionState.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60002DEADBEEF /* SidebarSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSection.swift; sourceTree = "<group>"; };
B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDragHandleView.swift; sourceTree = "<group>"; };
A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = "<group>"; };
A5001014 /* GhosttyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfig.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -458,6 +460,7 @@
children = (
A5001011 /* cmuxApp.swift */,
A5001012 /* ContentView.swift */,
A1B2C3D4E5F60002DEADBEEF /* SidebarSection.swift */,
9AD52285508B1D6A9875E7B3 /* SidebarSelectionState.swift */,
B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */,
A50012F0 /* Backport.swift */,
Expand Down Expand Up @@ -785,6 +788,7 @@
A5001001 /* cmuxApp.swift in Sources */,
A5001002 /* ContentView.swift in Sources */,
E62155868BB29FEB5DAAAF25 /* SidebarSelectionState.swift in Sources */,
A1B2C3D4E5F60001DEADBEEF /* SidebarSection.swift in Sources */,
B9000018A1B2C3D4E5F60719 /* WindowDragHandleView.swift in Sources */,
A50012F1 /* Backport.swift in Sources */,
A50012F3 /* KeyboardShortcutSettings.swift in Sources */,
Expand Down
74 changes: 61 additions & 13 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,37 @@ import Darwin

final class MainWindowHostingView<Content: View>: NSHostingView<Content> {
private let zeroSafeAreaLayoutGuide = NSLayoutGuide()
private let usesSystemSafeArea: Bool

override var safeAreaInsets: NSEdgeInsets { NSEdgeInsetsZero }
override var safeAreaRect: NSRect { bounds }
override var safeAreaLayoutGuide: NSLayoutGuide { zeroSafeAreaLayoutGuide }
override var safeAreaInsets: NSEdgeInsets {
usesSystemSafeArea ? super.safeAreaInsets : NSEdgeInsetsZero
}
override var safeAreaRect: NSRect {
usesSystemSafeArea ? super.safeAreaRect : bounds
}
override var safeAreaLayoutGuide: NSLayoutGuide {
usesSystemSafeArea ? super.safeAreaLayoutGuide : zeroSafeAreaLayoutGuide
}

required init(rootView: Content) {
if #available(macOS 26.0, *) {
// On macOS 26, use system safe area so:
// - Sidebar (.ignoresSafeArea) extends under the glass titlebar
// - Terminal content respects the titlebar and stays below it
self.usesSystemSafeArea = true
} else {
self.usesSystemSafeArea = false
}
super.init(rootView: rootView)
addLayoutGuide(zeroSafeAreaLayoutGuide)
NSLayoutConstraint.activate([
zeroSafeAreaLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor),
zeroSafeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor),
zeroSafeAreaLayoutGuide.topAnchor.constraint(equalTo: topAnchor),
zeroSafeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor),
])
if !usesSystemSafeArea {
addLayoutGuide(zeroSafeAreaLayoutGuide)
NSLayoutConstraint.activate([
zeroSafeAreaLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor),
zeroSafeAreaLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor),
zeroSafeAreaLayoutGuide.topAnchor.constraint(equalTo: topAnchor),
zeroSafeAreaLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}

@available(*, unavailable)
Expand Down Expand Up @@ -2581,6 +2598,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
syncMenuBarExtraVisibility()
updateController.startUpdaterIfNeeded()
}
// Start the titlebar accessory controller on all versions so the
// notifications popover infrastructure is available. On macOS 26
// the visual titlebar items come from SwiftUI .toolbar, but the
// popover is still managed by the accessory controller.
titlebarAccessoryController.start()
windowDecorationsController.start()
installMainWindowKeyObserver()
Expand Down Expand Up @@ -5810,6 +5831,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
terminalPanel.hostedView.reconcileGeometryNow()
terminalPanel.hostedView.refreshHostBackgroundAfterGhosttyConfigReload()
terminalPanel.surface.forceRefresh(reason: "appDelegate.refreshAfterGhosttyConfigReload")
// Force each surface to re-derive its config with the correct light/dark
// conditional state and re-apply the color scheme from the current appearance.
terminalPanel.surface.reapplyColorSchemeAndConfig()
refreshedCount += 1
}
#if DEBUG
Expand Down Expand Up @@ -7084,10 +7108,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
window.collectionBehavior.insert(.fullScreenDisallowsTiling)
}
window.title = ""
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
if #available(macOS 26.0, *) {
// On macOS 26+, let the system render the native glass titlebar
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = false
} else {
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
}
window.isMovableByWindowBackground = false
window.isMovable = false
if #available(macOS 26.0, *) {
window.isMovable = true
} else {
window.isMovable = false
}
let restoredFrame = resolvedWindowFrame(from: sessionWindowSnapshot)
if let restoredFrame {
window.setFrame(restoredFrame, display: false)
Expand Down Expand Up @@ -10119,6 +10153,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
#endif

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

@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 on lines 10155 to +10162

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.

titlebarAccessoryController.start()
titlebarAccessoryController.attach(to: window)
}
Expand All @@ -10127,7 +10168,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
windowDecorationsController.apply(to: window)
}

/// Notification posted on macOS 26 to toggle the SwiftUI notifications popover.
static let toggleNotificationsPopoverNotification = Notification.Name("cmux.toggleNotificationsPopover")

func toggleNotificationsPopover(animated: Bool = true, anchorView: NSView? = nil) {
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.

titlebarAccessoryController.toggleNotificationsPopover(animated: animated, anchorView: anchorView)
}

Expand Down
77 changes: 76 additions & 1 deletion Sources/CmuxConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct CmuxCommandDefinition: Codable, Sendable, Identifiable {
var description: String?
var keywords: [String]?
var restart: CmuxRestartBehavior?
var autoApply: Bool?
var workspace: CmuxWorkspaceDefinition?
var command: String?
var confirm: Bool?
Expand All @@ -24,6 +25,7 @@ struct CmuxCommandDefinition: Codable, Sendable, Identifiable {
description: String? = nil,
keywords: [String]? = nil,
restart: CmuxRestartBehavior? = nil,
autoApply: Bool? = nil,
workspace: CmuxWorkspaceDefinition? = nil,
command: String? = nil,
confirm: Bool? = nil
Expand All @@ -32,6 +34,7 @@ struct CmuxCommandDefinition: Codable, Sendable, Identifiable {
self.description = description
self.keywords = keywords
self.restart = restart
self.autoApply = autoApply
self.workspace = workspace
self.command = command
self.confirm = confirm
Expand All @@ -43,6 +46,7 @@ struct CmuxCommandDefinition: Codable, Sendable, Identifiable {
description = try container.decodeIfPresent(String.self, forKey: .description)
keywords = try container.decodeIfPresent([String].self, forKey: .keywords)
restart = try container.decodeIfPresent(CmuxRestartBehavior.self, forKey: .restart)
autoApply = try container.decodeIfPresent(Bool.self, forKey: .autoApply)
workspace = try container.decodeIfPresent(CmuxWorkspaceDefinition.self, forKey: .workspace)
command = try container.decodeIfPresent(String.self, forKey: .command)
confirm = try container.decodeIfPresent(Bool.self, forKey: .confirm)
Expand Down Expand Up @@ -90,23 +94,33 @@ enum CmuxRestartBehavior: String, Codable, Sendable {
case confirm
}

enum CmuxWorkspaceTarget: String, Codable, Sendable {
/// Apply the layout to the currently selected workspace.
case current
/// Create a new workspace (default).
case new
}

struct CmuxWorkspaceDefinition: Codable, Sendable {
var name: String?
var cwd: String?
var color: String?
var target: CmuxWorkspaceTarget?
var layout: CmuxLayoutNode?

init(name: String? = nil, cwd: String? = nil, color: String? = nil, layout: CmuxLayoutNode? = nil) {
init(name: String? = nil, cwd: String? = nil, color: String? = nil, target: CmuxWorkspaceTarget? = nil, layout: CmuxLayoutNode? = nil) {
self.name = name
self.cwd = cwd
self.color = color
self.target = target
self.layout = layout
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: .name)
cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
target = try container.decodeIfPresent(CmuxWorkspaceTarget.self, forKey: .target)
layout = try container.decodeIfPresent(CmuxLayoutNode.self, forKey: .layout)

if let rawColor = try container.decodeIfPresent(String.self, forKey: .color) {
Expand Down Expand Up @@ -270,6 +284,8 @@ final class CmuxConfigStore: ObservableObject {
return (home as NSString).appendingPathComponent(".config/cmux/cmux.json")
}()

private weak var trackedTabManager: TabManager?
private var autoAppliedWorkspaceIds = Set<UUID>()
private var cancellables = Set<AnyCancellable>()
private var localFileWatchSource: DispatchSourceFileSystemObject?
private var localFileDescriptor: Int32 = -1
Expand All @@ -292,6 +308,7 @@ final class CmuxConfigStore: ObservableObject {
// MARK: - Public API

func wireDirectoryTracking(tabManager: TabManager) {
trackedTabManager = tabManager
cancellables.removeAll()

tabManager.$selectedTabId
Expand All @@ -311,6 +328,23 @@ final class CmuxConfigStore: ObservableObject {
}
.store(in: &cancellables)

// Separate observer for autoApply: fires on every workspace switch
// (after a short delay so the workspace is fully visible).
// Captures the tab ID at emission time so a rapid B→C switch
// doesn't accidentally apply B's config to C.
tabManager.$selectedTabId
.dropFirst() // skip the initial value on subscribe
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] tabId in
guard let tabId else { return }
// Small delay so the config for the new directory loads first.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
self?.checkAutoApply(forTabId: tabId)
}
}
.store(in: &cancellables)

if let directory = tabManager.selectedWorkspace?.currentDirectory {
updateLocalConfigPath(directory)
}
Expand Down Expand Up @@ -381,6 +415,47 @@ final class CmuxConfigStore: ObservableObject {
loadedCommands = commands
commandSourcePaths = sourcePaths
configRevision &+= 1
checkAutoApply()
}

/// If the selected workspace hasn't been auto-applied this session and a
/// loaded command has `autoApply: true` with `target: "current"`, execute
/// it automatically. Tracks applied workspaces so it only fires once per
/// workspace per app session.
/// - Parameter forTabId: When provided, only applies if this tab is still
/// selected, preventing stale delayed applications after rapid switching.
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 }
Comment on lines +427 to +436

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.


// Prefer local config directory; fall back to global config directory
// so that autoApply commands defined only in the global config still work.
let baseCwd: String
if let localPath = localConfigPath {
baseCwd = (localPath as NSString).deletingLastPathComponent
} else {
baseCwd = (globalConfigPath as NSString).deletingLastPathComponent
}

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

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.


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

command: command,
tabManager: tabManager,
baseCwd: baseCwd,
configSourcePath: commandSourcePaths[command.id],
globalConfigPath: globalConfigPath
)
}

// MARK: - Parsing
Expand Down
25 changes: 24 additions & 1 deletion Sources/CmuxConfigExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ struct CmuxConfigExecutor {
baseCwd: String
) {
let workspaceName = wsDef.name ?? command.name
let resolvedCwd = CmuxConfigStore.resolveCwd(wsDef.cwd, relativeTo: baseCwd)

// "target": "current" — apply the layout to the selected workspace in-place.
// If no workspace is selected, skip silently rather than falling through
// to the name-based create/recreate path.
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
Comment on lines +96 to +112

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.

}

let restart = command.restart ?? .ignore

if let existing = tabManager.tabs.first(where: { $0.customTitle == workspaceName }) {
Expand Down Expand Up @@ -118,7 +142,6 @@ struct CmuxConfigExecutor {
}
}

let resolvedCwd = CmuxConfigStore.resolveCwd(wsDef.cwd, relativeTo: baseCwd)
let newWorkspace = tabManager.addWorkspace(workingDirectory: resolvedCwd)
newWorkspace.setCustomTitle(workspaceName)
if let color = wsDef.color {
Expand Down
Loading
Loading