Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ extension ControlCommandCoordinator {
func settingsOpen(_ params: [String: JSONValue]) -> ControlCallResult {
let targetRaw = string(params, "target")
let requestedActivate = bool(params, "activate") ?? true
let resolution = systemContext?.controlSettingsOpen(
guard let systemContext else {
// Fail closed: without the app context no window can have been
// presented, and `opened` must mean a window actually exists
// (https://github.com/manaflow-ai/cmux/issues/7775).
return .err(code: "unavailable", message: "Settings context not attached", data: nil)
}
let resolution = systemContext.controlSettingsOpen(
targetRaw: targetRaw,
requestedActivate: requestedActivate
) ?? .opened(target: targetRaw ?? "general")
)
switch resolution {
case .invalidTarget:
return .err(
Expand All @@ -46,6 +52,12 @@ extension ControlCommandCoordinator {
"opened": .bool(true),
"target": .string(target),
]))
case .failed(let message):
return .err(
code: "unavailable",
message: message,
data: .object(["target": orNull(targetRaw)])
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/// The outcome of `settings.open` (the legacy `v2SettingsOpen` body): the app
/// validates the target against its `SettingsNavigationTarget` enum and
/// schedules the window presentation.
/// presents the window before replying, so `opened` means a window actually
/// materialized (https://github.com/manaflow-ai/cmux/issues/7775).
public enum ControlSettingsOpenResolution: Sendable, Equatable {
/// The `target` param did not name a known settings pane.
case invalidTarget
/// The settings window presentation was scheduled. Carries the resolved
/// target raw value (`"general"` when no target was given).
/// The settings window was presented (or ordered front under a hidden
/// app). Carries the resolved target raw value (`"general"` when no
/// target was given).
case opened(target: String)
/// The app could not make a settings window visible. `message` carries
/// the diagnostic reason from the window presenter.
case failed(message: String)
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,15 @@
import CmuxSettings
import SwiftUI

/// The settings window SwiftUI `Scene`.
/// Root view of the settings window, hosted in an AppKit-owned
/// `NSWindow` by the app's `SettingsWindowFactory` (cmux issue
/// #7777; a SwiftUI `Window` scene's `openWindow(id:)` could
/// silently no-op and strand the open path).
///
/// Composes a single tall `ScrollView` of stacked sections — the
/// legacy in-app layout — with a left sidebar that scrolls to a
/// section's anchor on click. This mirrors what cmux's settings
/// window has historically looked like; using a `NavigationSplitView`
/// with one-pane-at-a-time selection was a previous (now reverted)
/// architecture choice.
@MainActor
public struct SettingsWindowScene: Scene {
private let runtime: SettingsRuntime

public init(runtime: SettingsRuntime) {
self.runtime = runtime
}

public var body: some Scene {
WindowGroup(String(localized: "settings.title", defaultValue: "Settings"), id: "cmux.settings") {
SettingsWindowRoot(runtime: runtime)
.settingsRuntime(runtime)
}
.defaultSize(width: 980, height: 680)
.windowResizability(.contentMinSize)
.commands { SidebarCommands() }
}
}

/// Root view of the settings window. Owns the search query, the
/// scroll proxy, and the section anchors. Renders sidebar + tall
/// scrolling content side-by-side.
/// section's anchor on click. Owns the search query, the scroll
/// proxy, and the section anchors.
@MainActor
public struct SettingsWindowRoot: View {
private let runtime: SettingsRuntime
Expand All @@ -50,8 +29,10 @@ public struct SettingsWindowRoot: View {
// because under search the user can click an individual setting
// hit and we still want the section pane to follow, but two
// sibling hits inside one section must each be selectable.
@SceneStorage("selectedSettingsSection") private var selectedSectionRaw: String = SettingsSectionID.account.rawValue
@SceneStorage("selectedSettingsSidebarEntry") private var selectedSidebarEntryID: String = "section:\(SettingsSectionID.account.rawValue)"
// @AppStorage (not @SceneStorage): the window is AppKit-hosted, so
// there is no SwiftUI scene to store into (cmux issue #7777).
@AppStorage("selectedSettingsSection") private var selectedSectionRaw: String = SettingsSectionID.account.rawValue
@AppStorage("selectedSettingsSidebarEntry") private var selectedSidebarEntryID: String = "section:\(SettingsSectionID.account.rawValue)"
// Legacy `SettingsRootView` binds `NavigationSplitView`'s
// `columnVisibility` so the user can collapse the sidebar via the
// toolbar button (or the SidebarCommands menu) and have that state
Expand Down Expand Up @@ -135,6 +116,12 @@ public struct SettingsWindowRoot: View {
.onReceive(NotificationCenter.default.publisher(for: Self.navigationRequestName)) { notification in
applyNavigationRequest(notification)
}
.onReceive(NotificationCenter.default.publisher(for: Self.sidebarToggleRequestName)) { _ in
// AppKit hosts this window, so SwiftUI's SidebarCommands cannot
// reach the split view; the host app routes its sidebar-toggle
// menu command here when the Settings window is key.
columnVisibility = columnVisibility == .detailOnly ? .all : .detailOnly
}
.onChange(of: searchText) { _, newValue in
// Legacy SettingsRootView resyncs the sidebar entry to the
// section row whenever the search text is cleared, so
Expand All @@ -146,6 +133,7 @@ public struct SettingsWindowRoot: View {
}

public static let navigationRequestName = Notification.Name("cmux.settings.navigate")
public static let sidebarToggleRequestName = Notification.Name("cmux.settings.toggleSidebar")

/// Legacy `SettingsRootView.onReceive` only updates the selection
/// state (sidebar entry + section pane) in response to an external
Expand Down
125 changes: 125 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -183223,6 +183223,131 @@
}
}
},
"settings.window.runtimeUnavailable": {
"extractionState": "manual",
"localizations": {
"ar": {
"stringUnit": {
"state": "translated",
"value": "تعذّر تحميل الإعدادات. يُرجى إعادة تشغيل cmux والإبلاغ عن هذه المشكلة."
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Postavke se nisu mogle učitati. Ponovo pokrenite cmux i prijavite ovaj problem."
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Indstillingerne kunne ikke indlæses. Genstart cmux, og rapportér dette problem."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Die Einstellungen konnten nicht geladen werden. Bitte starte cmux neu und melde dieses Problem."
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Settings could not load. Please restart cmux and report this issue."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "No se pudieron cargar los ajustes. Reinicia cmux e informa de este problema."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Impossible de charger les réglages. Veuillez redémarrer cmux et signaler ce problème."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Impossibile caricare le impostazioni. Riavvia cmux e segnala questo problema."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "設定を読み込めませんでした。cmux を再起動して、この問題を報告してください。"
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "មិនអាចផ្ទុកការកំណត់បានទេ។ សូមចាប់ផ្ដើម cmux ឡើងវិញ ហើយរាយការណ៍អំពីបញ្ហានេះ។"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "설정을 불러올 수 없습니다. cmux를 다시 시작하고 이 문제를 신고해 주세요."
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Innstillingene kunne ikke lastes. Start cmux på nytt og rapporter dette problemet."
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Nie udało się wczytać ustawień. Uruchom cmux ponownie i zgłoś ten problem."
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Não foi possível carregar os ajustes. Reinicie o cmux e informe este problema."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Не удалось загрузить настройки. Перезапустите cmux и сообщите об этой проблеме."
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "ไม่สามารถโหลดการตั้งค่าได้ โปรดรีสตาร์ท cmux และรายงานปัญหานี้"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Ayarlar yüklenemedi. Lütfen cmux'u yeniden başlatın ve bu sorunu bildirin."
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Не вдалося завантажити налаштування. Перезапустіть cmux і повідомте про цю проблему."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "无法加载设置。请重启 cmux 并报告此问题。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "無法載入設定。請重新啟動 cmux 並回報此問題。"
}
}
}
},
"settings.workspaceColors.base": {
"extractionState": "manual",
"localizations": {
Expand Down
47 changes: 47 additions & 0 deletions Sources/App/AppDelegateSettingsPresentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import AppKit

/// The Settings-open entrypoints shared by the app menu, ⌘,, the command
/// palette, help commands, and the menu-bar extra. Split out of
/// `AppDelegate.swift` (file-length budget) alongside the AppKit-owned
/// Settings window lifecycle (https://github.com/manaflow-ai/cmux/issues/7777).
extension AppDelegate {
@MainActor
static func presentPreferencesWindow(
navigationTarget: SettingsNavigationTarget? = nil,
showFallbackSettingsWindow: (@MainActor (SettingsNavigationTarget?) -> Void)? = nil,
// The legacy body also passed .activateIgnoringOtherApps; the option
// is deprecated and documented as a no-op on macOS 14+ (this target's
// minimum), so dropping it is behavior-neutral.
activateApplication: @MainActor () -> Void = {
NSRunningApplication.current.activate(options: [.activateAllWindows])
}
) {
#if DEBUG
cmuxDebugLog("settings.open.present path=appkitWindow")
#endif
if let showFallbackSettingsWindow {
showFallbackSettingsWindow(navigationTarget)
} else if case .failed = SettingsWindowPresenter.show(navigationTarget: navigationTarget) {
// The presenter already logged the loud failure diagnostics;
// surface the failed menu/⌘, action instead of silently activating.
NSSound.beep()
return
}
activateApplication()
#if DEBUG
cmuxDebugLog("settings.open.present activate=1")
#endif
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@MainActor
func openPreferencesWindow(debugSource: String, navigationTarget: SettingsNavigationTarget? = nil) {
#if DEBUG
cmuxDebugLog("settings.open.request source=\(debugSource)")
#endif
Self.presentPreferencesWindow(navigationTarget: navigationTarget)
}

@objc func openPreferencesWindow() {
openPreferencesWindow(debugSource: "appDelegate")
}
}
Loading
Loading