Repository navigation
Adopt macOS 26 Liquid Glass design - #2644
rodchristiansen wants to merge 1 commit into
Conversation
|
@rodchristiansen is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis PR adds macOS 26+ compatibility by conditionally routing to modern AppKit APIs (safe-area insets, system transparency) while preserving backward compatibility with older macOS versions. Key changes include safe-area behavior updates, navigation split-view refactoring, titlebar/glass-effect modernization, new toolbar items, terminal geometry adjustments, and sidebar visual polish. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
This review could not be run because your cubic account has exceeded the monthly review limit. If you need help restoring access, please contact contact@cubic.dev. |
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
0a64bf8 to
10f2d7a
Compare
Greptile SummaryThis PR adopts macOS 26 Liquid Glass design system-wide:
Confidence Score: 4/5Not safe to merge as-is due to a hardcoded Apple notarization password that must be revoked before this branch is pushed further. The P0 credential leak in build-local-release.sh blocks merge. The remaining findings (dead WindowToolbarController, broad NSSplitView patching, unconditional tabBarHeight=0) are P2 and do not individually block merge, but the credential issue alone warrants a 4/5. scripts/build-local-release.sh (P0 credential), Sources/WindowToolbarController.swift (dead code), Sources/ContentView.swift (SplitViewDividerHider scope) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[App Launch] --> B{macOS 26+?}
B -- Yes --> C[MainWindowHostingView\nusesSystemSafeArea = true]
B -- No --> D[MainWindowHostingView\nusesSystemSafeArea = false]
C --> E[contentAndSidebarLayout]
E --> F[NavigationSplitView\ncolumnVisibility binding]
F --> G[Sidebar column\nglass material automatic]
F --> H[Detail column\n.padding 8pt]
H --> I[terminalContentWithSidebarDropOverlay]
I --> J[TerminalWindowPortal\nleading corners rounded 16pt\nwhen hasSidebarToLeft]
I --> K[GhosttySurfaceScrollView\n6pt contentInset]
E --> L[.toolbar\nbell + plus buttons\n.accessoryBarAction style]
E --> M[SplitViewDividerHider\npatches NSSplitView dividers → clear]
B -- Yes --> N[AppDelegate\nskip titlebarAccessoryController.start]
B -- Yes --> O[Workspace.bonsplitAppearance\ntabBarHeight = 0]
B -- Yes --> P[AppIconManager\nskip custom icon override]
|
| @@ -123,7 +126,11 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { | |||
| toolbar.autosavesConfiguration = false | |||
| toolbar.showsBaselineSeparator = false | |||
| window.toolbar = toolbar | |||
| window.toolbarStyle = .unifiedCompact | |||
| if #available(macOS 26.0, *) { | |||
| window.toolbarStyle = .unified | |||
| } else { | |||
| window.toolbarStyle = .unifiedCompact | |||
| } | |||
| window.titleVisibility = .hidden | |||
| } | |||
|
|
|||
| @@ -154,11 +161,19 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { | |||
| // MARK: - NSToolbarDelegate | |||
|
|
|||
| func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { | |||
| [commandItemIdentifier, .flexibleSpace] | |||
| 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] | |||
| } | |||
|
|
|||
| func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { | |||
| @@ -175,8 +190,57 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { | |||
| return item | |||
| } | |||
|
|
|||
| if #available(macOS 26.0, *) { | |||
| if itemIdentifier == sidebarToggleIdentifier { | |||
| let item = NSToolbarItem(itemIdentifier: itemIdentifier) | |||
| item.image = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: "Toggle Sidebar") | |||
| item.label = "Sidebar" | |||
| item.toolTip = "Toggle Sidebar" | |||
| item.target = self | |||
| item.action = #selector(toggleSidebarAction) | |||
| return item | |||
| } | |||
|
|
|||
| if itemIdentifier == notificationsIdentifier { | |||
| let item = NSToolbarItem(itemIdentifier: itemIdentifier) | |||
| item.image = NSImage(systemSymbolName: "bell", accessibilityDescription: "Notifications") | |||
| item.label = "Notifications" | |||
| item.toolTip = "Show Notifications" | |||
| item.target = self | |||
| item.action = #selector(toggleNotificationsAction) | |||
| return item | |||
| } | |||
|
|
|||
| if itemIdentifier == newTabIdentifier { | |||
| let item = NSToolbarItem(itemIdentifier: itemIdentifier) | |||
| item.image = NSImage(systemSymbolName: "plus", accessibilityDescription: "New Workspace") | |||
| item.label = "New Workspace" | |||
| item.toolTip = "New Workspace" | |||
| item.target = self | |||
| item.action = #selector(newTabAction) | |||
| return item | |||
| } | |||
| } | |||
|
|
|||
| return nil | |||
| } | |||
|
|
|||
| // MARK: - Toolbar Actions (macOS 26+) | |||
|
|
|||
| @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) | |||
| } | |||
| } | |||
| } | |||
|
|
|||
| } | |||
There was a problem hiding this comment.
Dead code — class is never instantiated
WindowToolbarController is defined but never instantiated anywhere in the codebase (no call site for .start(tabManager:) exists outside this file). The toolbar items it declares for macOS 26 (sidebar toggle, notifications, new tab) are already provided by the SwiftUI .toolbar block inside ContentView.contentAndSidebarLayout. Having a parallel, orphaned AppKit toolbar controller risks confusion about which path is authoritative and inflates binary size. Consider removing this file, or if it is intended as the authoritative path, wire it up and remove the SwiftUI .toolbar block.
| if #available(macOS 26.0, *) { | ||
| hideTabBar = true | ||
| } else { | ||
| hideTabBar = false | ||
| } | ||
| return BonsplitConfiguration.Appearance( | ||
| tabBarHeight: hideTabBar ? 0 : 33, | ||
| splitButtonTooltips: Self.currentSplitButtonTooltips(), |
There was a problem hiding this comment.
tabBarHeight: 0 hides the tab bar for all panes on macOS 26, not just single-tab panes
The PR description says the tab bar is hidden "for single-tab panes on macOS 26", but hideTabBar = true is set unconditionally for all panes on macOS 26, producing tabBarHeight: 0 globally. If Bonsplit uses this height literally (rather than auto-showing the bar when there are multiple tabs regardless of configured height), users with split panes containing multiple tabs would lose their tab-bar navigation entirely on macOS 26. Confirm whether Bonsplit auto-shows the tab bar when tab count > 1 even with tabBarHeight: 0, or add a per-pane tab-count guard if it does not.
There was a problem hiding this comment.
Pull request overview
This PR updates the macOS UI to align with the macOS 26 “Liquid Glass” system look (NavigationSplitView + native titlebar/toolbar behavior), while also adding sidebar “sections” with session persistence and introducing a cmux config auto-apply workflow.
Changes:
- Adopt macOS 26 NavigationSplitView layout, native titlebar/toolbar behavior, and updated terminal padding/corner rounding behavior.
- Add user-defined sidebar sections (collapse/rename/reorder/membership) and persist them across sessions (including restoring workspace IDs).
- Add cmux config enhancements:
autoApplyand workspacetargetsupport, plus safety fixes around Ghostty config reload.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Sources/Workspace.swift | Persist/restore workspace IDs; adjust Bonsplit appearance (tab bar height) for macOS 26. |
| Sources/WindowToolbarController.swift | Add macOS 26 toolbar items/actions (sidebar/bell/new tab) and toolbar style change. |
| Sources/TerminalWindowPortal.swift | Add leading-corner rounding heuristic for macOS 26 when sidebar is present. |
| Sources/TabManager.swift | Implement sidebar sections state, layout computation, autosave hashing, and session snapshot restore/save. |
| Sources/SidebarSection.swift | New SidebarSection model + SidebarLayout struct. |
| Sources/SessionPersistence.swift | Extend session snapshot schema to include workspace IDs + sidebar section snapshots. |
| Sources/GhosttyTerminalView.swift | Guard against config reload reentrancy; sync all surfaces’ color schemes; add macOS 26 terminal internal inset. |
| Sources/ContentView.swift | macOS 26 NavigationSplitView + SwiftUI toolbar, padding changes, glass effects, sidebar sections UI, and split divider hiding. |
| Sources/CmuxConfigExecutor.swift | Add support for applying workspace layouts to the current workspace (target: current). |
| Sources/CmuxConfig.swift | Add autoApply + workspace target config fields; implement auto-apply execution on workspace switch/config load. |
| Sources/cmuxApp.swift | Refactor main window content builder; skip app icon override on macOS 26+. |
| Sources/AppDelegate.swift | Use system safe area on macOS 26; skip legacy titlebar accessory controller on macOS 26; add surface color-scheme sync helper. |
| scripts/build-local-release.sh | New local release script (currently includes hard-coded signing/notarization credentials). |
| GhosttyTabs.xcodeproj/project.pbxproj | Add SidebarSection.swift to the Xcode project. |
| .gitignore | Ignore cmux.json. |
Comments suppressed due to low confidence (5)
Sources/ContentView.swift:13591
- This condition is always true because of the
|| true, so the “Move to Section” submenu will always be included even when there are no sections. Remove the|| true(or replace it with the intended predicate) to avoid shipping debug/placeholder logic.
}
private func hasUnreadNotifications(in targetIds: [UUID]) -> Bool {
Sources/ContentView.swift:12429
- SidebarSectionHeaderView is made clickable via
onTapGestureand then marked as a button for accessibility, but it won’t get standard keyboard activation/focus behavior like a realButton. Consider implementing this header as aButton(or adding explicitaccessibilityAction/ keyboard handlers) so it’s operable via keyboard and VoiceOver.
if showsWorkspaceShortcutHint, let workspaceShortcutLabel {
return SidebarWorkspaceShortcutHintMetrics.slotWidth(
label: workspaceShortcutLabel,
debugXOffset: debugXOffset
)
}
return canCloseWorkspace ? closeButtonWidth : 0
Sources/ContentView.swift:12475
- The accessibility hint strings use “Double-tap…”, which is iOS-centric wording. On macOS VoiceOver, prefer wording like “Press to expand/collapse” to match platform conventions.
lhs.workspaceShortcutDigit == rhs.workspaceShortcutDigit &&
lhs.workspaceShortcutModifierSymbol == rhs.workspaceShortcutModifierSymbol &&
lhs.canCloseWorkspace == rhs.canCloseWorkspace &&
lhs.accessibilityWorkspaceCount == rhs.accessibilityWorkspaceCount &&
Sources/ContentView.swift:15735
- This uses KVC to set
dividerColoron NSSplitView (setValue(_:forKey:)), which relies on an undocumented/private key and may break on OS updates (and can be problematic for distribution). Prefer a supported approach (custom NSSplitView subclass/delegate drawing, or hiding dividers via public APIs) instead of setting private properties by key.
enum SidebarPresetOption: String, CaseIterable, Identifiable {
case nativeSidebar
case glassBehind
case softBlur
case popoverGlass
Sources/ContentView.swift:13612
- TabItemView is
Equatableand deliberately does not subscribe to TabManager publishes; however this new context menu captureslet sections = tabManager.sections/currentSectioninsidebody. Changes to sections (create/rename/reorder) won’t necessarily cause TabItemView to re-evaluate, so the menu can become stale until some unrelated prop changes. Consider threadingtabManager.sectionRevision(or a derived hash) into TabItemView inputs/==, or restructuring so the menu content reads live state when opened.
private func hasUnreadNotifications(in targetIds: [UUID]) -> Bool {
let targetSet = Set(targetIds)
return notificationStore.notifications.contains { targetSet.contains($0.tabId) && !$0.isRead }
}
private func hasReadNotifications(in targetIds: [UUID]) -> Bool {
let targetSet = Set(targetIds)
return notificationStore.notifications.contains { targetSet.contains($0.tabId) && $0.isRead }
}
private func hasLatestNotifications(in targetIds: [UUID]) -> Bool {
targetIds.contains { notificationStore.latestNotification(forTabId: $0) != nil }
}
private func syncSelectionAfterMutation() {
let existingIds = Set(tabManager.tabs.map { $0.id })
selectedTabIds = selectedTabIds.filter { existingIds.contains($0) }
if selectedTabIds.isEmpty, let selectedId = tabManager.selectedTabId {
selectedTabIds = [selectedId]
}
if let selectedId = tabManager.selectedTabId {
lastSidebarSelectionIndex = tabManager.tabs.firstIndex { $0.id == selectedId }
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // On macOS 26 without fullSizeContentView, the system titlebar | ||
| // handles drag natively. | ||
| window.isMovable = true | ||
| window.isMovableByWindowBackground = false | ||
| } else { | ||
| // Keep window immovable; the sidebar's WindowDragHandleView handles | ||
| // drag-to-move via performDrag with temporary movable override. | ||
| // isMovableByWindowBackground=true breaks tab reordering, and | ||
| // isMovable=true blocks clicks on sidebar buttons in minimal mode. | ||
| window.isMovableByWindowBackground = false | ||
| window.isMovable = false | ||
| } | ||
| window.styleMask.insert(.fullSizeContentView) | ||
|
|
There was a problem hiding this comment.
The comment says “On macOS 26 without fullSizeContentView…”, but this code still inserts .fullSizeContentView (and AppDelegate creates main windows with it). Either update the comment to match reality, or conditionally avoid .fullSizeContentView on macOS 26 if that’s actually required for the native titlebar behavior you want.
| final class WindowToolbarController: NSObject, NSToolbarDelegate { | ||
| private let commandItemIdentifier = NSToolbarItem.Identifier("cmux.focusedCommand") | ||
| private let sidebarToggleIdentifier = NSToolbarItem.Identifier("cmux.sidebarToggle") | ||
| private let notificationsIdentifier = NSToolbarItem.Identifier("cmux.notifications") | ||
| private let newTabIdentifier = NSToolbarItem.Identifier("cmux.newTab") | ||
|
|
||
| private weak var tabManager: TabManager? |
There was a problem hiding this comment.
I couldn’t find any call sites that instantiate/start WindowToolbarController (no references outside this file), so the new macOS 26 toolbar items/actions may never be attached to windows at runtime. If this controller is meant to drive the macOS 26 toolbar, wire start(tabManager:) into app startup (or remove these items and rely solely on the SwiftUI .toolbar path).
| if #available(macOS 26.0, *) { | ||
| hideTabBar = true | ||
| } else { | ||
| hideTabBar = false | ||
| } | ||
| return BonsplitConfiguration.Appearance( | ||
| tabBarHeight: hideTabBar ? 0 : 33, | ||
| splitButtonTooltips: Self.currentSplitButtonTooltips(), |
There was a problem hiding this comment.
PR description says the Bonsplit tab bar is hidden for single-tab panes on macOS 26, but this change forces tabBarHeight to 0 on macOS 26 unconditionally. If multi-tab panes still need a tab bar, make this conditional on the pane/tab count (or update the PR description/behavior accordingly).
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Sources/SessionPersistence.swift (1)
330-357:⚠️ Potential issue | 🟠 MajorKeep
SessionWorkspaceSnapshot.idrequired in the persisted model.Line 331 makes workspace identity optional just as sections start referencing workspaces by UUID. That allows snapshots that cannot round-trip section membership and workspace identity reliably. If older session files still need to load, handle that in migration/load logic instead of weakening the saved model.
Based on learnings:
Sources/SessionPersistence.swift — SessionWorkspaceSnapshot.id is required (no default UUID generation). Callers must pass the live workspace id to preserve identity across restore/migration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/SessionPersistence.swift` around lines 330 - 357, Change the persisted model so SessionWorkspaceSnapshot.id is non-optional: update the struct definition to require var id: UUID (remove the optional) and ensure any code constructing SessionWorkspaceSnapshot (e.g., places that create snapshots for workspaces, restore/migrate logic) must supply the live workspace's UUID rather than generating a default; handle older files via explicit migration/load fallback rather than making id optional so section membership (SessionSidebarSectionSnapshot.workspaceIds) can reliably round-trip.Sources/GhosttyTerminalView.swift (1)
2337-2366:⚠️ Potential issue | 🟠 MajorPre-sync surfaces before soft reload too.
The soft reload path calls
ghostty_app_update_config(app, config)without first syncing surfaces to the current color scheme. Any surfaces that missedviewDidChangeEffectiveAppearancestill have staleconfig_conditional_stateand will resolve to the wrong theme colors inghostty_app_update_config, just as described in the comment for the full reload path.Move the pre-sync block outside the
if soft, let configguard so it applies before either code path callsghostty_app_update_config.Suggested fix
+ let currentScheme = GhosttyConfig.currentColorSchemePreference() + let ghosttyScheme: ghostty_color_scheme_e = currentScheme == .dark + ? GHOSTTY_COLOR_SCHEME_DARK : GHOSTTY_COLOR_SCHEME_LIGHT + if Thread.isMainThread { + MainActor.assumeIsolated { + AppDelegate.shared?.syncAllSurfaceColorSchemes(to: ghosttyScheme) + } + } + if soft, let config { ghostty_app_update_config(app, config) lastAppearanceColorScheme = GhosttyConfig.currentColorSchemePreference() NotificationCenter.default.post(name: .ghosttyConfigDidReload, object: nil) scheduleSurfaceRefreshAfterConfigurationReload(source: source) logThemeAction("reload end source=\(source) soft=\(soft) mode=soft") return } guard let newConfig = ghostty_config_new() else { logThemeAction("reload skipped source=\(source) soft=\(soft) reason=config_alloc_failed") return } loadDefaultConfigFilesWithLegacyFallback(newConfig) - // Sync all surfaces to the current color scheme before applying - // the new config. Background surfaces that missed - // viewDidChangeEffectiveAppearance still have a stale - // config_conditional_state, which would resolve to the wrong - // theme colors in ghostty_app_update_config. - let currentScheme = GhosttyConfig.currentColorSchemePreference() - let ghosttyScheme: ghostty_color_scheme_e = currentScheme == .dark - ? GHOSTTY_COLOR_SCHEME_DARK : GHOSTTY_COLOR_SCHEME_LIGHT - if Thread.isMainThread { - MainActor.assumeIsolated { - AppDelegate.shared?.syncAllSurfaceColorSchemes(to: ghosttyScheme) - } - } ghostty_app_update_config(app, newConfig)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/GhosttyTerminalView.swift` around lines 2337 - 2366, The soft-reload branch calls ghostty_app_update_config(app, config) without pre-syncing surfaces, so move the pre-sync block (the code that computes currentScheme/ghosttyScheme and calls AppDelegate.shared?.syncAllSurfaceColorSchemes(to:) inside a MainActor.assumeIsolated when Thread.isMainThread) out of the full-reload path and above the soft/let config guard so it runs for both soft and full reloads before any call to ghostty_app_update_config; keep using GhosttyConfig.currentColorSchemePreference() to determine ghostty_color_scheme_e and preserve the MainActor/Thread.isMainThread check.
🧹 Nitpick comments (3)
.gitignore (1)
50-51: Add a comment explainingcmux.jsonand clarify pattern scope.The new ignore rule lacks context. Consider adding a comment describing what
cmux.jsonis (e.g., user config, build artifact, local state) and whether it should be ignored only at the repository root (/cmux.json) or in all directories (cmux.jsonor**/cmux.json).📝 Example: root-only with comment
# Local scratch (screenshots, etc.) tmp/ tmp-*/ + +# User-specific configuration (not committed) +/cmux.json📝 Example: all directories with comment
# Local scratch (screenshots, etc.) tmp/ tmp-*/ + +# User-specific configuration (not committed) +cmux.json🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.gitignore around lines 50 - 51, The .gitignore currently contains an unexplained rule "cmux.json"; update it to include a brief comment explaining what cmux.json represents (e.g., user config, local state, or build artifact) and decide & express whether you intend to ignore it only at the repository root or in all directories—replace or augment the entry accordingly (use a root-only pattern like /cmux.json or a recursive pattern like **/cmux.json) and add the comment directly above the ignore line so reviewers understand scope and intent.Sources/CmuxConfig.swift (1)
287-289: Consider pruningautoAppliedWorkspaceIdswhen workspaces are closed.The
autoAppliedWorkspaceIdsset accumulates workspace UUIDs indefinitely. In long-running sessions with many workspace creations/closures, this set grows without bounds. While the memory impact is minimal (16 bytes per UUID), consider subscribing to workspace removal notifications to prune stale entries, or periodically intersecting with live workspace IDs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/CmuxConfig.swift` around lines 287 - 289, autoAppliedWorkspaceIds is never pruned and will grow forever; subscribe to workspace-closure/removal events and remove closed workspace UUIDs from autoAppliedWorkspaceIds. Add a binding in the initializer or a new setupBindings method that subscribes (storing the AnyCancellable in cancellables) to the workspace removal publisher (e.g., WorkspaceStore.didRemoveWorkspace / workspaceRemovedPublisher or similar in your app), and in the sink call autoAppliedWorkspaceIds.remove(removedWorkspace.id) (or intersect with current WorkspaceStore.activeWorkspaceIDs periodically). Ensure the cancellable is stored in cancellables and that cleanup also happens on deinit if needed.Sources/ContentView.swift (1)
12477-12489:onDropreturns success before verifying data load.The handler returns
trueimmediately, signaling a successful drop to the system before the asyncloadDataRepresentationcompletes. If the data load fails or the UUID is invalid, the user sees success feedback but no action occurs.Consider deferring the return or using synchronous loading if the payload is small, or accept that this is a known limitation for drag-drop UX.
♻️ Alternative using NSItemProvider's synchronous path (if payload is small)
.onDrop(of: SidebarTabDragPayload.dropContentTypes, isTargeted: nil) { providers in guard let provider = providers.first else { return false } - provider.loadDataRepresentation(forTypeIdentifier: SidebarTabDragPayload.typeIdentifier) { data, _ in - guard let data, let str = String(data: data, encoding: .utf8) else { return } - let prefix = "cmux.sidebar-tab." - guard str.hasPrefix(prefix), - let tabId = UUID(uuidString: String(str.dropFirst(prefix.count))) else { return } - Task { `@MainActor` in - tabManager.moveWorkspaceToSection(tabId: tabId, sectionId: section.id) - } + guard provider.hasItemConformingToTypeIdentifier(SidebarTabDragPayload.typeIdentifier) else { + return false } - return true + provider.loadDataRepresentation(forTypeIdentifier: SidebarTabDragPayload.typeIdentifier) { data, _ in + guard let data, let str = String(data: data, encoding: .utf8) else { return } + let prefix = "cmux.sidebar-tab." + guard str.hasPrefix(prefix), + let tabId = UUID(uuidString: String(str.dropFirst(prefix.count))) else { return } + Task { `@MainActor` in + tabManager.moveWorkspaceToSection(tabId: tabId, sectionId: section.id) + } + } + return true // Accept if provider has conforming type }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/ContentView.swift` around lines 12477 - 12489, The onDrop handler currently returns true immediately before the async provider.loadDataRepresentation completes; change it so the drop return value reflects whether the payload was actually parsed: in the onDrop closure (the block using SidebarTabDragPayload.dropContentTypes and SidebarTabDragPayload.typeIdentifier) attempt a synchronous verification of the provider payload (e.g., use a synchronous API or loadItem/loadDataRepresentation and wait briefly) to confirm you can obtain data and create the UUID for tabId, and only return true when that verification succeeds; if you must keep async loading, return false from onDrop and trigger tabManager.moveWorkspaceToSection(tabId:sectionId:) inside the async completion on success so the system does not report a successful drop unless the payload was validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/build-local-release.sh`:
- Around line 47-50: The check using GHOSTTY_SHA against
GhosttyKit.xcframework/.ghostty_sha never becomes true because
download-prebuilt-ghosttykit.sh extracts the xcframework but does not write that
.ghostty_sha file; update the flow so that after successful extraction (in
scripts/download-prebuilt-ghosttykit.sh) the script writes the current
GHOSTTY_SHA into GhosttyKit.xcframework/.ghostty_sha (or adjust
scripts/build-local-release.sh to compute and write the SHA just after
download), ensuring GHOSTTY_SHA and the .ghostty_sha file are created/updated
consistently so the grep test in build-local-release.sh can succeed (reference
symbols: GHOSTTY_SHA, GhosttyKit.xcframework/.ghostty_sha,
scripts/download-prebuilt-ghosttykit.sh, scripts/build-local-release.sh).
- Around line 24-29: The script currently hardcodes sensitive notary credentials
(SIGN_IDENTITY, SIGN_HASH, TEAM_ID, APPLE_ID, APPLE_PASSWORD, ENTITLEMENTS);
remove the APPLE_PASSWORD from source and update scripts/build-local-release.sh
to read notary credentials from environment variables or the macOS keychain
(e.g. use the same env-backed inputs pattern as scripts/build-sign-upload.sh),
add fallback validation to error if required env vars (APPLE_ID, APPLE_PASSWORD
or KEYCHAIN lookup) are missing, and rotate the exposed app-specific password in
the live account since it has been committed to git history.
- Around line 12-20: The script's argument parsing currently ignores unknown
flags and continues execution; update the for loop/case in
build-local-release.sh to add a default branch (e.g., *) that prints a clear
error like "Unknown option: $arg", shows usage, and exits non‑zero (use exit 2)
so unsupported args do not fall through to the build/sign/notarize/install flow;
modify the existing case block around --sync and -h|--help to include this
default handler and ensure SYNC is only set for the recognized --sync flag.
- Line 113: The script currently uses pkill -f "cmux", which can match any
process whose command line contains "cmux"; replace that invocation (pkill -f
"cmux") with a narrower match such as pkill -x "cmux" for an exact process name
or pkill -f "cmux.app/Contents/MacOS/cmux" to target the GUI app by full path,
ensuring only the intended cmux process is killed.
In `@Sources/AppDelegate.swift`:
- Around line 2601-2605: The macOS 26 availability branch currently skips
initializing titlebarAccessoryController, which leaves existing entry points
(toggleNotificationsPopover(...), showNotificationsPopoverFromMenuBar(),
dismissNotificationsPopoverIfShown(), isNotificationsPopoverShown()) orphaned;
update the macOS 26 branch in the if `#available`(macOS 26.0, *) block to either
start a compatibility adapter that forwards to the new SwiftUI toolbar
presentation state or wire those AppDelegate methods to the new SwiftUI
presenter so they still drive notification UI; specifically ensure
titlebarAccessoryController (or an adapter with the same public API) is
created/started or that toggleNotificationsPopover,
showNotificationsPopoverFromMenuBar, dismissNotificationsPopoverIfShown, and
isNotificationsPopoverShown are reimplemented to interact with the SwiftUI
toolbar presentation state on macOS 26.
In `@Sources/CmuxConfigExecutor.swift`:
- Around line 100-105: The loop mutates current.panels while iterating its live
keys which can crash; before closing panels snapshot the IDs into a separate
array (e.g. let ids = Array(current.panels.keys)) and then iterate that array,
skipping current.focusedPanelId and calling current.closePanel(panelId, force:
true); reference the symbols current, panels, focusedPanelId, closePanel and
ensure applyCustomLayout still runs from a single pane after the closes.
In `@Sources/ContentView.swift`:
- Around line 15729-15739: The code uses KVC setValue(_:forKey:) on NSSplitView
with the private key "dividerColor" inside patchSplitViews(in:) which can break;
remove the private-API call (splitView.setValue(NSColor.clear, forKey:
"dividerColor")) and either rely on the public API (keep splitView.dividerStyle
= .thin) or implement a safe public approach: create/use a NSSplitView subclass
or extension that overrides drawing of the divider (e.g., override
drawDivider(in:) or use layer-backed views and set the divider area’s layer to
clear) and call that instead from patchSplitViews(in:), or guard the KVC behind
a documented, explicit opt-in and robust runtime/availability check if you must
keep it. Ensure references are to patchSplitViews(in:) and NSSplitView rather
than the private "dividerColor" key.
- Line 13589: The if condition using "!tabManager.sections.isEmpty || true" is
always true; either remove the "|| true" to restore the intended check or delete
the entire `if` and render the "Move to Section" menu unconditionally;
specifically, edit the `if !tabManager.sections.isEmpty || true {` block in
ContentView.swift (the logic guarding the "Move to Section" menu) to either `if
!tabManager.sections.isEmpty {` when you only want the menu shown when sections
exist, or remove the `if` wrapper so the menu is always visible.
In `@Sources/GhosttyTerminalView.swift`:
- Around line 9140-9149: The inset logic for macOS 26 can produce negative sizes
for tiny panes; modify the contentInset computation in GhosttyTerminalView so
that when using a 6pt inset (in the `#available`(macOS 26.0, *) branch) you clamp
it to no more than half the current bounds' width and height (e.g., contentInset
= min(6, bounds.width / 2, bounds.height / 2)) so insetBy(...) never yields
negative dimensions before you call setFrameIfNeeded(scrollView, to:
scrollFrame); update the code that sets contentInset and the scrollFrame
calculation accordingly.
In `@Sources/TabManager.swift`:
- Around line 2737-2745: The current moveWorkspaceToSection removes the
workspace from all sections before confirming the destination, which can
silently ungroup a tab if sectionId is stale; change moveWorkspaceToSection to
first resolve the destination by finding the target section (guard let target =
sections.first(where: { $0.id == sectionId }) else { return }), and only after
that iterate sections to call section.removeWorkspace(tabId), then call
target.addWorkspace(tabId, at: atIndex) and notifySectionChange(); do not remove
membership until the destination is validated.
- Around line 2686-2784: reorderWorkspace(tabId:toIndex:) must refuse flat-array
reorders for workspaces that are in a SidebarSection; add an early guard at the
top of that function that uses sectionForWorkspace(tabId) (or checks
sections.first { $0.contains(tabId) }) and returns immediately if a section is
found, so grouped workspaces keep their order managed by section.workspaceIds
and can only be moved via section-aware APIs (e.g. moveWorkspaceToSection or a
section-specific reorder method).
In `@Sources/TerminalWindowPortal.swift`:
- Around line 1505-1509: Replace the heuristic hasSidebarToLeft =
targetFrame.origin.x > 20 with a real check for a visible left sidebar: detect
whether this terminal pane is the left-most inset sidebar or whether the
window's split view has an uncollapsed sidebar (e.g., query the surrounding
NSSplitViewController/NSSplitViewItem or a passed-in isSidebarVisible flag) and
only set desiredRadius = 16 when that sidebar-specific signal is true; update
TerminalWindowPortal's logic that computes desiredRadius/hasSidebarToLeft (and
any call sites) to use that sidebar presence check instead of the x > 20 offset.
In `@Sources/WindowToolbarController.swift`:
- Around line 193-223: Replace all hard-coded user-facing strings in the macOS
26 toolbar-item branch with localized variants: for each NSToolbarItem created
when itemIdentifier == sidebarToggleIdentifier / notificationsIdentifier /
newTabIdentifier, replace label, toolTip and the NSImage
accessibilityDescription with String(localized: "...", defaultValue: "...")
using distinct keys (e.g. "toolbar.sidebar.label", "toolbar.sidebar.tooltip",
"toolbar.sidebar.accessibility") and similarly for notifications and newTab;
ensure you keep the same descriptive defaults ("Sidebar", "Toggle Sidebar",
"Notifications", "Show Notifications", "New Workspace", etc.) and do this inside
the same conditional where you set item.target and item.action
(`#selector`(toggleSidebarAction), `#selector`(toggleNotificationsAction),
`#selector`(newTabAction)).
In `@vendor/bonsplit`:
- Line 1: The vendor/bonsplit submodule pointer references commit
cfff8a9318f8131604681e4cb86c11925e01bf1e which does not exist on the upstream
https://github.com/manaflow-ai/bonsplit.git; either push that missing commit to
the bonsplit remote main branch and then update the parent repo, or revert the
submodule pointer in vendor/bonsplit back to the existing upstream commit (e.g.,
b2788b1e77d43f0c114dcf189aa59cae8abb47de) and commit that change; after fixing
upstream/pointer, run git submodule sync && git submodule update --init
--recursive locally to verify and commit the corrected submodule pointer update.
---
Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2337-2366: The soft-reload branch calls
ghostty_app_update_config(app, config) without pre-syncing surfaces, so move the
pre-sync block (the code that computes currentScheme/ghosttyScheme and calls
AppDelegate.shared?.syncAllSurfaceColorSchemes(to:) inside a
MainActor.assumeIsolated when Thread.isMainThread) out of the full-reload path
and above the soft/let config guard so it runs for both soft and full reloads
before any call to ghostty_app_update_config; keep using
GhosttyConfig.currentColorSchemePreference() to determine ghostty_color_scheme_e
and preserve the MainActor/Thread.isMainThread check.
In `@Sources/SessionPersistence.swift`:
- Around line 330-357: Change the persisted model so SessionWorkspaceSnapshot.id
is non-optional: update the struct definition to require var id: UUID (remove
the optional) and ensure any code constructing SessionWorkspaceSnapshot (e.g.,
places that create snapshots for workspaces, restore/migrate logic) must supply
the live workspace's UUID rather than generating a default; handle older files
via explicit migration/load fallback rather than making id optional so section
membership (SessionSidebarSectionSnapshot.workspaceIds) can reliably round-trip.
---
Nitpick comments:
In @.gitignore:
- Around line 50-51: The .gitignore currently contains an unexplained rule
"cmux.json"; update it to include a brief comment explaining what cmux.json
represents (e.g., user config, local state, or build artifact) and decide &
express whether you intend to ignore it only at the repository root or in all
directories—replace or augment the entry accordingly (use a root-only pattern
like /cmux.json or a recursive pattern like **/cmux.json) and add the comment
directly above the ignore line so reviewers understand scope and intent.
In `@Sources/CmuxConfig.swift`:
- Around line 287-289: autoAppliedWorkspaceIds is never pruned and will grow
forever; subscribe to workspace-closure/removal events and remove closed
workspace UUIDs from autoAppliedWorkspaceIds. Add a binding in the initializer
or a new setupBindings method that subscribes (storing the AnyCancellable in
cancellables) to the workspace removal publisher (e.g.,
WorkspaceStore.didRemoveWorkspace / workspaceRemovedPublisher or similar in your
app), and in the sink call autoAppliedWorkspaceIds.remove(removedWorkspace.id)
(or intersect with current WorkspaceStore.activeWorkspaceIDs periodically).
Ensure the cancellable is stored in cancellables and that cleanup also happens
on deinit if needed.
In `@Sources/ContentView.swift`:
- Around line 12477-12489: The onDrop handler currently returns true immediately
before the async provider.loadDataRepresentation completes; change it so the
drop return value reflects whether the payload was actually parsed: in the
onDrop closure (the block using SidebarTabDragPayload.dropContentTypes and
SidebarTabDragPayload.typeIdentifier) attempt a synchronous verification of the
provider payload (e.g., use a synchronous API or loadItem/loadDataRepresentation
and wait briefly) to confirm you can obtain data and create the UUID for tabId,
and only return true when that verification succeeds; if you must keep async
loading, return false from onDrop and trigger
tabManager.moveWorkspaceToSection(tabId:sectionId:) inside the async completion
on success so the system does not report a successful drop unless the payload
was validated.
🪄 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: 5808bf98-7439-4693-8ba4-028cf7b3f550
📒 Files selected for processing (16)
.gitignoreGhosttyTabs.xcodeproj/project.pbxprojSources/AppDelegate.swiftSources/CmuxConfig.swiftSources/CmuxConfigExecutor.swiftSources/ContentView.swiftSources/GhosttyTerminalView.swiftSources/SessionPersistence.swiftSources/SidebarSection.swiftSources/TabManager.swiftSources/TerminalWindowPortal.swiftSources/WindowToolbarController.swiftSources/Workspace.swiftSources/cmuxApp.swiftscripts/build-local-release.shvendor/bonsplit
| for arg in "$@"; do | ||
| case "$arg" in | ||
| --sync) SYNC=1 ;; | ||
| -h|--help) | ||
| echo "Usage: $0 [--sync]" | ||
| echo " --sync Fetch upstream/main and rebase feature branch before building" | ||
| exit 0 | ||
| ;; | ||
| esac |
There was a problem hiding this comment.
Reject unknown flags instead of silently proceeding.
Any typo here still falls through to the full sign/notarize/install flow. For a local release script, unsupported args should exit non-zero.
💡 Proposed fix
for arg in "$@"; do
case "$arg" in
--sync) SYNC=1 ;;
-h|--help)
echo "Usage: $0 [--sync]"
echo " --sync Fetch upstream/main and rebase feature branch before building"
exit 0
;;
+ *)
+ echo "Unknown argument: $arg" >&2
+ echo "Usage: $0 [--sync]" >&2
+ exit 1
+ ;;
esac
done📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for arg in "$@"; do | |
| case "$arg" in | |
| --sync) SYNC=1 ;; | |
| -h|--help) | |
| echo "Usage: $0 [--sync]" | |
| echo " --sync Fetch upstream/main and rebase feature branch before building" | |
| exit 0 | |
| ;; | |
| esac | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --sync) SYNC=1 ;; | |
| -h|--help) | |
| echo "Usage: $0 [--sync]" | |
| echo " --sync Fetch upstream/main and rebase feature branch before building" | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Unknown argument: $arg" >&2 | |
| echo "Usage: $0 [--sync]" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| done |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/build-local-release.sh` around lines 12 - 20, The script's argument
parsing currently ignores unknown flags and continues execution; update the for
loop/case in build-local-release.sh to add a default branch (e.g., *) that
prints a clear error like "Unknown option: $arg", shows usage, and exits
non‑zero (use exit 2) so unsupported args do not fall through to the
build/sign/notarize/install flow; modify the existing case block around --sync
and -h|--help to include this default handler and ensure SYNC is only set for
the recognized --sync flag.
| SIGN_IDENTITY="Developer ID Application: Emily Carr University of Art and Design (7TF6CSP83S)" | ||
| SIGN_HASH="C0277EBA633F1AA2BC2855E45B3B38A1840053BA" | ||
| TEAM_ID="7TF6CSP83S" | ||
| APPLE_ID="applenotarization@ecuad.ca" | ||
| APPLE_PASSWORD="zdtm-jyob-rhbb-cbfq" | ||
| ENTITLEMENTS="cmux.entitlements" |
There was a problem hiding this comment.
Do not commit the notary password.
Line 28 stores a live app-specific password in source control. Please switch this script to env/keychain-backed credentials and rotate the current password, since it is now in git history. scripts/build-sign-upload.sh already uses environment-backed notary inputs.
🔐 Proposed fix
SIGN_IDENTITY="Developer ID Application: Emily Carr University of Art and Design (7TF6CSP83S)"
SIGN_HASH="C0277EBA633F1AA2BC2855E45B3B38A1840053BA"
-TEAM_ID="7TF6CSP83S"
-APPLE_ID="applenotarization@ecuad.ca"
-APPLE_PASSWORD="zdtm-jyob-rhbb-cbfq"
+TEAM_ID="${APPLE_TEAM_ID:?Set APPLE_TEAM_ID in your environment}"
+APPLE_ID="${APPLE_ID:?Set APPLE_ID in your environment}"
+APPLE_PASSWORD="${APPLE_APP_SPECIFIC_PASSWORD:?Set APPLE_APP_SPECIFIC_PASSWORD in your environment}"
ENTITLEMENTS="cmux.entitlements"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| SIGN_IDENTITY="Developer ID Application: Emily Carr University of Art and Design (7TF6CSP83S)" | |
| SIGN_HASH="C0277EBA633F1AA2BC2855E45B3B38A1840053BA" | |
| TEAM_ID="7TF6CSP83S" | |
| APPLE_ID="applenotarization@ecuad.ca" | |
| APPLE_PASSWORD="zdtm-jyob-rhbb-cbfq" | |
| ENTITLEMENTS="cmux.entitlements" | |
| SIGN_IDENTITY="Developer ID Application: Emily Carr University of Art and Design (7TF6CSP83S)" | |
| SIGN_HASH="C0277EBA633F1AA2BC2855E45B3B38A1840053BA" | |
| TEAM_ID="${APPLE_TEAM_ID:?Set APPLE_TEAM_ID in your environment}" | |
| APPLE_ID="${APPLE_ID:?Set APPLE_ID in your environment}" | |
| APPLE_PASSWORD="${APPLE_APP_SPECIFIC_PASSWORD:?Set APPLE_APP_SPECIFIC_PASSWORD in your environment}" | |
| ENTITLEMENTS="cmux.entitlements" |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 24-24: SIGN_IDENTITY appears unused. Verify use (or export if used externally).
(SC2034)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/build-local-release.sh` around lines 24 - 29, The script currently
hardcodes sensitive notary credentials (SIGN_IDENTITY, SIGN_HASH, TEAM_ID,
APPLE_ID, APPLE_PASSWORD, ENTITLEMENTS); remove the APPLE_PASSWORD from source
and update scripts/build-local-release.sh to read notary credentials from
environment variables or the macOS keychain (e.g. use the same env-backed inputs
pattern as scripts/build-sign-upload.sh), add fallback validation to error if
required env vars (APPLE_ID, APPLE_PASSWORD or KEYCHAIN lookup) are missing, and
rotate the exposed app-specific password in the live account since it has been
committed to git history.
There was a problem hiding this comment.
This was a temp password, mistakenly committed, secret revoked
There was a problem hiding this comment.
@rodchristiansen Thanks for the quick action on revoking the credential — that's the most important immediate step.
The hardcoded values (APPLE_ID, TEAM_ID, and the now-invalid APPLE_PASSWORD) are still present in the script, so the env-var pattern from the suggestion is still worth applying to prevent a recurrence. When you do update the script, the proposed change would look like:
TEAM_ID="${APPLE_TEAM_ID:?Set APPLE_TEAM_ID in your environment}"
APPLE_ID="${APPLE_ID:?Set APPLE_ID in your environment}"
APPLE_PASSWORD="${APPLE_APP_SPECIFIC_PASSWORD:?Set APPLE_APP_SPECIFIC_PASSWORD in your environment}"This mirrors what scripts/build-sign-upload.sh already does and will fail fast with a clear message if any required var is missing, rather than silently using a stale/baked-in value.
🧠 Learnings used
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:4036-4038
Timestamp: 2026-04-01T09:50:41.471Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (buildInteractiveRemoteShellScript), remote shells intentionally export CMUX_SOCKET_PATH and CMUX_SOCKET as "127.0.0.1:<relayPort>" and prepend PATH with "$HOME/.cmux/bin"; CMUX_BUNDLED_CLI_PATH points to that wrapper. WorkspaceRemoteSessionController.remoteCLIWrapperScript() detects host:port, reads "~/.cmux/relay/<port>.daemon_path", and delegates to cmuxd-remote. Do not flag the non-UNIX value of CMUX_SOCKET_PATH/CMUX_SOCKET as a bug; it is the relay contract for remote sessions.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-04T00:55:26.574Z
Learning: When rebuilding cmuxd for release/bundling, always use ReleaseFast: `cd cmuxd && zig build -Doptimize=ReleaseFast`
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2564
File: CLI/cmux.swift:0-0
Timestamp: 2026-04-04T02:33:06.558Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift, baseSSHArguments(_:, localCommand:) now percent-escapes LocalCommand by replacing "%" with "%%" to prevent OpenSSH percent-token expansion. A CLI regression test asserts that the emitted -o LocalCommand retains doubled percent signs.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-04T00:55:26.574Z
Learning: After making code changes, always run the reload script with a tag to build the Debug app using `./scripts/reload.sh --tag <tag>`. Never run bare `xcodebuild` or `open` an untagged `cmux DEV.app`
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2564
File: CLI/cmux.swift:4032-4035
Timestamp: 2026-04-04T02:19:33.834Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (cmux ssh foreground-auth deferral), deferredRemoteReconnectLocalCommand(in:localCLIPath:foregroundAuthToken:) prefers resolvedExecutableURL()?.path for the local cmux binary, then falls back to CMUX_BUNDLED_CLI_PATH and finally PATH; it reads the local socket from ${CMUX_SOCKET_PATH:-${CMUX_SOCKET:-}}. This ensures the deferred LocalCommand runs locally (pre-auth) when auto_connect is false.
Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-04T00:55:26.574Z
Learning: Never run tests locally. All tests (E2E, UI, python socket tests) run via GitHub Actions or on the VM. E2E / UI tests: trigger via `gh workflow run test-e2e.yml`. Unit tests: `xcodebuild -scheme cmux-unit` is safe (no app launch), but prefer CI. Python socket tests: use a tagged build's socket with `CMUX_SOCKET=/tmp/cmux-debug-<tag>.sock`. Never `open` an untagged `cmux DEV.app`
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2398
File: CLI/cmux.swift:0-0
Timestamp: 2026-04-01T09:50:23.728Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift within CMUXCLI.buildInteractiveRemoteShellScript(...), never export CMUX_TAB_ID from the workspace UUID. CMUX_TAB_ID must be surface-scoped: only set it when a surface ID is available (map CMUX_TAB_ID to CMUX_SURFACE_ID). Rationale: tab-action/rename-tab resolve CMUX_TAB_ID before CMUX_SURFACE_ID; workspace-scoped values misroute or fail.
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2225
File: Sources/Workspace.swift:4149-4151
Timestamp: 2026-03-27T00:15:40.894Z
Learning: Repo: manaflow-ai/cmux — WorkspaceRemoteDaemonManifest.releaseURL is guaranteed to be the GitHub asset download base "https://github.com/{owner}/{repo}/releases/download/{tag}" (not the tag page). Therefore, Sources/Workspace.swift should (and does) build the live-manifest URL as releaseURL + "/cmuxd-remote-manifest.json". Do not suggest deriving it from checksumsURL or entry.downloadURL; test fixtures that reference "/releases/tag/..." are from a different context and shouldn’t drive code changes.
Learnt from: gaelic-ghost
Repo: manaflow-ai/cmux PR: 1926
File: scripts/build-sign-upload.sh:8-10
Timestamp: 2026-03-22T00:14:23.473Z
Learning: Repo: manaflow-ai/cmux — `scripts/lib/cmux-paths.sh` `cmux_paths_init()` intentionally preserves any pre-set `CMUX_*` environment variable overrides verbatim (does not canonicalize relative paths to absolute). This is a deliberate Stage 1 design choice; do not flag relative-override canonicalization as a bug unless a concrete reproducer is provided or a later stage explicitly tightens the override-semantics contract.
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2564
File: Sources/Workspace.swift:1072-1075
Timestamp: 2026-04-04T02:21:20.453Z
Learning: Repo: manaflow-ai/cmux — Foreground-auth deferral: In CLI/cmux.swift, PermitLocalCommand and LocalCommand are injected only into the foreground startup SSH command (e.g., via deferredRemoteReconnectLocalCommand(...)) and are not added to the ssh_options payload sent in workspace.remote.configure. Consequently, WorkspaceRemoteSSHBatchCommandBuilder.batchArguments(...) and WorkspaceRemoteSessionController.backgroundSSHOptions(_:) do not need to filter LocalCommand/PermitLocalCommand for batch operations.
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2514
File: CLI/cmux.swift:9691-9711
Timestamp: 2026-04-01T22:58:26.254Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (runClaudeTeams), custom Claude path resolution now trims whitespace and rejects paths that point to the cmux wrapper using isCmuxClaudeWrapper(), before falling back to PATH/bundled. The Resources/bin/claude wrapper also resolves the real path and compares against itself, requiring -f/-x to avoid recursion/self-reference.
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2525
File: Sources/GhosttyTerminalView.swift:481-513
Timestamp: 2026-04-02T10:13:39.235Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, terminal file-link resolution trims trailing unmatched closing delimiters “) ] } >” only when they are dangling (more closers than openers), preserving wrapped tokens like “(file:///tmp/a.png)”. Implemented via terminalFileLinkTrailingClosingDelimiters and count comparison inside trimTrailingTerminalFileLinkPunctuation(_:) and exercised by a regression test (PR `#2525`, commit 3f5c5b6d).
Learnt from: pstanton237
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-06T12:03:16.899Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift, `claudeTeamsLaunchArguments` must use two independent branches: (1) prepend `--teammate-mode auto` only when the caller has NOT already supplied `--teammate-mode`; (2) append `--settings <claudeHooksJSON>` unconditionally (modulo `CMUX_CLAUDE_HOOKS_DISABLED=1` opt-out). These two branches must never be coupled in a single early-return or conditional block — coupling them was the original `#2229` regression (hook JSON silently bypassed when `--teammate-mode auto|manual` was explicit). Mirrors `Resources/bin/claude:208`: `exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@"` where `$@` content never gates hook injection.
Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-05T21:26:10.684Z
Learning: Repo: manaflow-ai/cmux — In Sources/Workspace.swift, applySessionPanelMetadata() must gate listeningPorts restoration on the per-panel snapshot.terminal?.isRemoteBacked flag (not workspace-wide remoteTerminalStartupCommand()), so that local panels are always eligible for port restore regardless of current SSH state. Fixed in commit 4d0fd871 (PR `#2545`).
Learnt from: lucasward
Repo: manaflow-ai/cmux PR: 1903
File: Sources/ContentView.swift:13069-13113
Timestamp: 2026-03-21T06:23:38.764Z
Learning: Repo: manaflow-ai/cmux — In Sources/ContentView.swift, DraggableFolderNSView.updateIcon() intentionally applies the "sidebarMonochromeIcons" setting only on view creation (no live observer). Do not add defaults observers/AppStorage for live refresh in feature-scoped PRs; a live-refresh can be considered in a separate follow-up.
Learnt from: andrekat
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-29T11:05:01.662Z
Learning: In manaflow-ai/cmux PR `#1773`: `setActiveProfileName(_:)` on `TabManager` (Sources/TabManager.swift) must call `updateWindowTitleForSelectedTab()` explicitly — the save and delete paths do not go through `selectedTabId.didSet`, so without the explicit call the AppKit window title is left stale with the old profile prefix. This was confirmed fixed in commit be9eaa26.
| GHOSTTY_SHA="$(git -C ghostty rev-parse HEAD)" | ||
| if [ ! -d "GhosttyKit.xcframework" ] || ! grep -q "$GHOSTTY_SHA" GhosttyKit.xcframework/.ghostty_sha 2>/dev/null; then | ||
| echo "==> Downloading updated GhosttyKit..." | ||
| bash scripts/download-prebuilt-ghosttykit.sh |
There was a problem hiding this comment.
The GhosttyKit SHA check never becomes true.
scripts/download-prebuilt-ghosttykit.sh extracts the xcframework but never writes GhosttyKit.xcframework/.ghostty_sha, so Lines 47-50 force a re-download on every --sync run. The bootstrap path at Lines 56-59 leaves the same broken state.
🧩 Proposed fix
GHOSTTY_SHA="$(git -C ghostty rev-parse HEAD)"
- if [ ! -d "GhosttyKit.xcframework" ] || ! grep -q "$GHOSTTY_SHA" GhosttyKit.xcframework/.ghostty_sha 2>/dev/null; then
+ if [ ! -d "GhosttyKit.xcframework" ] || ! grep -qx "$GHOSTTY_SHA" GhosttyKit.xcframework/.ghostty_sha 2>/dev/null; then
echo "==> Downloading updated GhosttyKit..."
bash scripts/download-prebuilt-ghosttykit.sh
+ printf '%s\n' "$GHOSTTY_SHA" > GhosttyKit.xcframework/.ghostty_sha
fi
echo "==> Sync complete"
fi
# --- Ensure GhosttyKit ---
if [ ! -d "GhosttyKit.xcframework" ]; then
echo "==> Downloading GhosttyKit..."
bash scripts/download-prebuilt-ghosttykit.sh
+ printf '%s\n' "$(git -C ghostty rev-parse HEAD)" > GhosttyKit.xcframework/.ghostty_sha
fiAlso applies to: 56-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/build-local-release.sh` around lines 47 - 50, The check using
GHOSTTY_SHA against GhosttyKit.xcframework/.ghostty_sha never becomes true
because download-prebuilt-ghosttykit.sh extracts the xcframework but does not
write that .ghostty_sha file; update the flow so that after successful
extraction (in scripts/download-prebuilt-ghosttykit.sh) the script writes the
current GHOSTTY_SHA into GhosttyKit.xcframework/.ghostty_sha (or adjust
scripts/build-local-release.sh to compute and write the SHA just after
download), ensuring GHOSTTY_SHA and the .ghostty_sha file are created/updated
consistently so the grep test in build-local-release.sh can succeed (reference
symbols: GHOSTTY_SHA, GhosttyKit.xcframework/.ghostty_sha,
scripts/download-prebuilt-ghosttykit.sh, scripts/build-local-release.sh).
|
|
||
| # --- Install --- | ||
| echo "==> Installing to $INSTALL_PATH..." | ||
| pkill -f "cmux" 2>/dev/null || true |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n scripts/build-local-release.sh | sed -n '105,125p'Repository: manaflow-ai/cmux
Length of output: 835
🏁 Script executed:
cd scripts && rg -n "pkill|killall" --type shRepository: manaflow-ai/cmux
Length of output: 987
🏁 Script executed:
rg -n "pkill.*cmux" scripts/ | head -20Repository: manaflow-ai/cmux
Length of output: 817
🏁 Script executed:
git log --oneline -10 scripts/build-local-release.shRepository: manaflow-ai/cmux
Length of output: 102
🏁 Script executed:
cat -n scripts/rebuild.sh | sed -n '1,20p'Repository: manaflow-ai/cmux
Length of output: 477
🏁 Script executed:
# Check what the actual process name would be for the GUI app
cat -n scripts/build-local-release.sh | grep -A5 -B5 "INSTALL_PATH"Repository: manaflow-ai/cmux
Length of output: 1146
Use a narrower process match to avoid killing unrelated cmux processes.
Line 113 uses pkill -f "cmux", which matches any command line containing that token—including the CLI tool or other background processes. To target only the GUI app during replacement, use pkill -x "cmux" (exact process name match, as in other test scripts) or pkill -f "cmux.app/Contents/MacOS/cmux" (full app path, as in reload scripts).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/build-local-release.sh` at line 113, The script currently uses pkill
-f "cmux", which can match any process whose command line contains "cmux";
replace that invocation (pkill -f "cmux") with a narrower match such as pkill -x
"cmux" for an exact process name or pkill -f "cmux.app/Contents/MacOS/cmux" to
target the GUI app by full path, ensuring only the intended cmux process is
killed.
| if #available(macOS 26.0, *) { | ||
| // On macOS 26, native SwiftUI .toolbar handles titlebar controls. | ||
| } else { | ||
| titlebarAccessoryController.start() | ||
| } |
There was a problem hiding this comment.
Don't orphan the notifications presenter on macOS 26.
These branches fully bypass titlebarAccessoryController, but the rest of AppDelegate still drives notification UI through that controller (toggleNotificationsPopover(...), showNotificationsPopoverFromMenuBar(), dismissNotificationsPopoverIfShown(), isNotificationsPopoverShown()). On macOS 26, the show-notifications shortcut and menu-bar action will stop working unless those entry points are rewired to the new SwiftUI toolbar presentation state.
Also applies to: 10163-10167
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/AppDelegate.swift` around lines 2601 - 2605, The macOS 26
availability branch currently skips initializing titlebarAccessoryController,
which leaves existing entry points (toggleNotificationsPopover(...),
showNotificationsPopoverFromMenuBar(), dismissNotificationsPopoverIfShown(),
isNotificationsPopoverShown()) orphaned; update the macOS 26 branch in the if
`#available`(macOS 26.0, *) block to either start a compatibility adapter that
forwards to the new SwiftUI toolbar presentation state or wire those AppDelegate
methods to the new SwiftUI presenter so they still drive notification UI;
specifically ensure titlebarAccessoryController (or an adapter with the same
public API) is created/started or that toggleNotificationsPopover,
showNotificationsPopoverFromMenuBar, dismissNotificationsPopoverIfShown, and
isNotificationsPopoverShown are reimplemented to interact with the SwiftUI
toolbar presentation state on macOS 26.
| // 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: §ionObserverCancellables) | ||
| } | ||
| } | ||
|
|
||
| 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) { | ||
| // Remove from any existing section first | ||
| for section in sections { | ||
| section.removeWorkspace(tabId) | ||
| } | ||
| guard let section = sections.first(where: { $0.id == sectionId }) else { return } | ||
| 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 | ||
| ) | ||
| } |
There was a problem hiding this comment.
Reject flat-array reorders for grouped workspaces.
Once a workspace is in a SidebarSection, its visible order is owned by section.workspaceIds. Leaving reorderWorkspace(tabId:toIndex:) unrestricted means grouped workspaces can still be moved through the flat tabs array, bypassing the section-aware ordering path.
🔧 Suggested guard
`@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 }Based on learnings, reorderWorkspace(...) must reject grouped tabs (early return). Flat-array reorders are not allowed for workspaces inside a group; use the group-aware reorder APIs instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TabManager.swift` around lines 2686 - 2784,
reorderWorkspace(tabId:toIndex:) must refuse flat-array reorders for workspaces
that are in a SidebarSection; add an early guard at the top of that function
that uses sectionForWorkspace(tabId) (or checks sections.first {
$0.contains(tabId) }) and returns immediately if a section is found, so grouped
workspaces keep their order managed by section.workspaceIds and can only be
moved via section-aware APIs (e.g. moveWorkspaceToSection or a section-specific
reorder method).
| func moveWorkspaceToSection(tabId: UUID, sectionId: UUID, atIndex: Int? = nil) { | ||
| // Remove from any existing section first | ||
| for section in sections { | ||
| section.removeWorkspace(tabId) | ||
| } | ||
| guard let section = sections.first(where: { $0.id == sectionId }) else { return } | ||
| section.addWorkspace(tabId, at: atIndex) | ||
| notifySectionChange() | ||
| } |
There was a problem hiding this comment.
Resolve the destination section before removing current membership.
If sectionId is stale or has just been deleted, this removes tabId from every existing section and then returns, so a failed move silently ungroups the workspace. Guard the target first, then rewrite membership.
🔧 Proposed fix
func moveWorkspaceToSection(tabId: UUID, sectionId: UUID, atIndex: Int? = nil) {
- // Remove from any existing section first
- for section in sections {
- section.removeWorkspace(tabId)
- }
- guard let section = sections.first(where: { $0.id == sectionId }) else { return }
- section.addWorkspace(tabId, at: atIndex)
+ guard let targetSection = sections.first(where: { $0.id == sectionId }) else { return }
+ for section in sections {
+ section.removeWorkspace(tabId)
+ }
+ targetSection.addWorkspace(tabId, at: atIndex)
notifySectionChange()
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TabManager.swift` around lines 2737 - 2745, The current
moveWorkspaceToSection removes the workspace from all sections before confirming
the destination, which can silently ungroup a tab if sectionId is stale; change
moveWorkspaceToSection to first resolve the destination by finding the target
section (guard let target = sections.first(where: { $0.id == sectionId }) else {
return }), and only after that iterate sections to call
section.removeWorkspace(tabId), then call target.addWorkspace(tabId, at:
atIndex) and notifySectionChange(); do not remove membership until the
destination is validated.
| // On macOS 26, round the terminal's leading corners when a sidebar | ||
| // is visible to its left, matching the NavigationSplitView glass shape. | ||
| if #available(macOS 26.0, *) { | ||
| let hasSidebarToLeft = targetFrame.origin.x > 20 | ||
| let desiredRadius: CGFloat = hasSidebarToLeft ? 16 : 0 |
There was a problem hiding this comment.
Gate the rounding off actual sidebar presence, not any minX > 20 offset.
Line 1508 will also be true for right-hand split panes, so regular split layouts can pick up rounded leading corners against the divider just because that pane is offset from the window origin. This should use a sidebar-specific signal, or at least only the leftmost inset hosted pane, rather than any terminal whose frame starts past x = 20.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TerminalWindowPortal.swift` around lines 1505 - 1509, Replace the
heuristic hasSidebarToLeft = targetFrame.origin.x > 20 with a real check for a
visible left sidebar: detect whether this terminal pane is the left-most inset
sidebar or whether the window's split view has an uncollapsed sidebar (e.g.,
query the surrounding NSSplitViewController/NSSplitViewItem or a passed-in
isSidebarVisible flag) and only set desiredRadius = 16 when that
sidebar-specific signal is true; update TerminalWindowPortal's logic that
computes desiredRadius/hasSidebarToLeft (and any call sites) to use that sidebar
presence check instead of the x > 20 offset.
| if #available(macOS 26.0, *) { | ||
| if itemIdentifier == sidebarToggleIdentifier { | ||
| let item = NSToolbarItem(itemIdentifier: itemIdentifier) | ||
| item.image = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: "Toggle Sidebar") | ||
| item.label = "Sidebar" | ||
| item.toolTip = "Toggle Sidebar" | ||
| item.target = self | ||
| item.action = #selector(toggleSidebarAction) | ||
| return item | ||
| } | ||
|
|
||
| if itemIdentifier == notificationsIdentifier { | ||
| let item = NSToolbarItem(itemIdentifier: itemIdentifier) | ||
| item.image = NSImage(systemSymbolName: "bell", accessibilityDescription: "Notifications") | ||
| item.label = "Notifications" | ||
| item.toolTip = "Show Notifications" | ||
| item.target = self | ||
| item.action = #selector(toggleNotificationsAction) | ||
| return item | ||
| } | ||
|
|
||
| if itemIdentifier == newTabIdentifier { | ||
| let item = NSToolbarItem(itemIdentifier: itemIdentifier) | ||
| item.image = NSImage(systemSymbolName: "plus", accessibilityDescription: "New Workspace") | ||
| item.label = "New Workspace" | ||
| item.toolTip = "New Workspace" | ||
| item.target = self | ||
| item.action = #selector(newTabAction) | ||
| return item | ||
| } | ||
| } |
There was a problem hiding this comment.
Localize all user-facing strings.
The label, toolTip, and accessibilityDescription strings are user-facing and must be localized per coding guidelines.
🌐 Proposed fix for localization
if `#available`(macOS 26.0, *) {
if itemIdentifier == sidebarToggleIdentifier {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
- item.image = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: "Toggle Sidebar")
- item.label = "Sidebar"
- item.toolTip = "Toggle Sidebar"
+ item.image = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: String(localized: "toolbar.sidebar.accessibilityDescription", defaultValue: "Toggle Sidebar"))
+ item.label = String(localized: "toolbar.sidebar.label", defaultValue: "Sidebar")
+ item.toolTip = String(localized: "toolbar.sidebar.toolTip", defaultValue: "Toggle Sidebar")
item.target = self
item.action = `#selector`(toggleSidebarAction)
return item
}
if itemIdentifier == notificationsIdentifier {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
- item.image = NSImage(systemSymbolName: "bell", accessibilityDescription: "Notifications")
- item.label = "Notifications"
- item.toolTip = "Show Notifications"
+ item.image = NSImage(systemSymbolName: "bell", accessibilityDescription: String(localized: "toolbar.notifications.accessibilityDescription", defaultValue: "Notifications"))
+ item.label = String(localized: "toolbar.notifications.label", defaultValue: "Notifications")
+ item.toolTip = String(localized: "toolbar.notifications.toolTip", defaultValue: "Show Notifications")
item.target = self
item.action = `#selector`(toggleNotificationsAction)
return item
}
if itemIdentifier == newTabIdentifier {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
- item.image = NSImage(systemSymbolName: "plus", accessibilityDescription: "New Workspace")
- item.label = "New Workspace"
- item.toolTip = "New Workspace"
+ item.image = NSImage(systemSymbolName: "plus", accessibilityDescription: String(localized: "toolbar.newWorkspace.accessibilityDescription", defaultValue: "New Workspace"))
+ item.label = String(localized: "toolbar.newWorkspace.label", defaultValue: "New Workspace")
+ item.toolTip = String(localized: "toolbar.newWorkspace.toolTip", defaultValue: "New Workspace")
item.target = self
item.action = `#selector`(newTabAction)
return item
}
}As per coding guidelines: "All user-facing strings must be localized using String(localized: "key.name", defaultValue: "English text") for every string shown in the UI."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/WindowToolbarController.swift` around lines 193 - 223, Replace all
hard-coded user-facing strings in the macOS 26 toolbar-item branch with
localized variants: for each NSToolbarItem created when itemIdentifier ==
sidebarToggleIdentifier / notificationsIdentifier / newTabIdentifier, replace
label, toolTip and the NSImage accessibilityDescription with String(localized:
"...", defaultValue: "...") using distinct keys (e.g. "toolbar.sidebar.label",
"toolbar.sidebar.tooltip", "toolbar.sidebar.accessibility") and similarly for
notifications and newTab; ensure you keep the same descriptive defaults
("Sidebar", "Toggle Sidebar", "Notifications", "Show Notifications", "New
Workspace", etc.) and do this inside the same conditional where you set
item.target and item.action (`#selector`(toggleSidebarAction),
`#selector`(toggleNotificationsAction), `#selector`(newTabAction)).
| @@ -1 +1 @@ | |||
| Subproject commit b2788b1e77d43f0c114dcf189aa59cae8abb47de | |||
| Subproject commit cfff8a9318f8131604681e4cb86c11925e01bf1e | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Review changes in the bonsplit submodule update
cd vendor/bonsplit || exit 1
# Verify the new commit exists
if ! git cat-file -e cfff8a9318f8131604681e4cb86c11925e01bf1e 2>/dev/null; then
echo "ERROR: Commit cfff8a9318f8131604681e4cb86c11925e01bf1e does not exist in vendor/bonsplit"
exit 1
fi
echo "=== Bonsplit submodule update summary ==="
echo "Old commit: b2788b1e77d43f0c114dcf189aa59cae8abb47de"
echo "New commit: cfff8a9318f8131604681e4cb86c11925e01bf1e"
echo ""
# Show commit log between old and new
echo "=== Commits included in this update ==="
git log --oneline b2788b1e77d43f0c114dcf189aa59cae8abb47de..cfff8a9318f8131604681e4cb86c11925e01bf1e
echo ""
echo "=== Detailed commit messages ==="
git log --format="%h - %s%n%b" b2788b1e77d43f0c114dcf189aa59cae8abb47de..cfff8a9318f8131604681e4cb86c11925e01bf1e
echo ""
echo "=== Files changed in this update ==="
git diff --stat b2788b1e77d43f0c114dcf189aa59cae8abb47de..cfff8a9318f8131604681e4cb86c11925e01bf1e
echo ""
echo "=== API changes (public Swift interfaces) ==="
git diff b2788b1e77d43f0c114dcf189aa59cae8abb47de..cfff8a9318f8131604681e4cb86c11925e01bf1e -- '*.swift' | grep -E '^\+.*public|^\+.*open|^-.*public|^-.*open' | head -50Repository: manaflow-ai/cmux
Length of output: 148
🏁 Script executed:
#!/bin/bash
# Investigate the bonsplit submodule state and verify commit status
echo "=== Checking if submodule is initialized ==="
if [ ! -d vendor/bonsplit/.git ]; then
echo "Submodule not initialized locally"
exit 1
fi
cd vendor/bonsplit || exit 1
echo "=== Current submodule HEAD and branch status ==="
git rev-parse HEAD
git branch -v
echo ""
echo "=== Checking if commit exists anywhere in the submodule ==="
# Try to find the commit in reflog
git reflog | grep cfff8a93 || echo "Not in reflog"
echo ""
echo "=== Checking remote branches ==="
git branch -r
echo ""
echo "=== Checking if commit is on origin/main or ancestors ==="
git merge-base --is-ancestor cfff8a9318f8131604681e4cb86c11925e01bf1e origin/main 2>&1 && echo "Commit IS an ancestor of origin/main" || echo "Commit is NOT an ancestor of origin/main (or doesn't exist)"
echo ""
echo "=== Checking git log from current HEAD ==="
git log --oneline -20
echo ""
echo "=== Checking if there are unpushed commits on current branch ==="
git log origin/main..HEAD --oneline 2>/dev/null | head -20 || echo "Cannot compare with origin/main"Repository: manaflow-ai/cmux
Length of output: 138
🏁 Script executed:
#!/bin/bash
# Check parent repo submodule state and remote bonsplit repository
echo "=== Submodule configuration in parent repo ==="
cat .gitmodules | grep -A 3 "bonsplit" || echo "No bonsplit config found"
echo ""
echo "=== Current submodule pointer in git index ==="
git ls-files --stage | grep bonsplit
echo ""
echo "=== Submodule URL and status ==="
git config --file .gitmodules --get-regexp "bonsplit"
echo ""
echo "=== Check if bonsplit remote is reachable (without cloning) ==="
# Try to get remote URL
REMOTE_URL=$(git config --file .gitmodules --get submodule.vendor/bonsplit.url)
echo "Remote URL: $REMOTE_URL"
if [ -n "$REMOTE_URL" ]; then
echo ""
echo "=== Checking if commit exists on remote (using git ls-remote) ==="
git ls-remote "$REMOTE_URL" cfff8a9318f8131604681e4cb86c11925e01bf1e || echo "Commit not found via ls-remote"
echo ""
echo "=== Listing recent commits on remote main branch ==="
git ls-remote "$REMOTE_URL" main | head -5
fi
echo ""
echo "=== Parent repo git log showing this update ==="
git log --oneline -10 -- vendor/bonsplitRepository: manaflow-ai/cmux
Length of output: 966
Critical: Submodule pointer references a commit that does not exist on the remote bonsplit repository.
The vendor/bonsplit pointer in this PR has been updated to commit cfff8a9318f8131604681e4cb86c11925e01bf1e, but this commit does not exist on the remote https://github.com/manaflow-ai/bonsplit.git. The remote main branch still points to b2788b1e77d43f0c114dcf189aa59cae8abb47de (the old commit).
Before merging this PR, the new commit must be pushed to the bonsplit remote's main branch. Cloning this parent repository will fail when trying to initialize the bonsplit submodule otherwise. Per submodule best practices, always push the submodule commit to its remote main branch before updating the pointer in the parent repository.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@vendor/bonsplit` at line 1, The vendor/bonsplit submodule pointer references
commit cfff8a9318f8131604681e4cb86c11925e01bf1e which does not exist on the
upstream https://github.com/manaflow-ai/bonsplit.git; either push that missing
commit to the bonsplit remote main branch and then update the parent repo, or
revert the submodule pointer in vendor/bonsplit back to the existing upstream
commit (e.g., b2788b1e77d43f0c114dcf189aa59cae8abb47de) and commit that change;
after fixing upstream/pointer, run git submodule sync && git submodule update
--init --recursive locally to verify and commit the corrected submodule pointer
update.
|
Closing and recreating — previous force-push history contained a hardcoded credential (now revoked). |
Summary
.toolbarfor bell and new-tab buttons with.accessoryBarActionstyleKnown limitations
Test plan
🤖 Generated with Claude Code
Summary by cubic
Adopts macOS 26 “Liquid Glass” with a native glass sidebar, system titlebar, and SwiftUI toolbar. Improves terminal layout and visuals, and removes legacy chrome on macOS 26.
New Features
.toolbarwith sidebar toggle, bell, and new tab; hide NSSplitView divider; system handles app icon variants.bonsplittab bar on macOS 26.autoApplyon workspace commands andtarget: "current"to apply layouts in place; auto-apply runs once per workspace on tab switch.scripts/build-local-release.shto build, sign, notarize, and install locally.Bug Fixes
vendor/bonsplitto correct split divider behavior.Written for commit 10f2d7a. Summary will update on new commits.
Summary by CodeRabbit
New Features
Improvements
Chores