Repository navigation
Fix hidden Settings CPU during Codex output #4661
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
21c4333
8bd1ba7
9d6f660
6a337a7
73f1892
18d8f96
9ec3e57
f184ec6
24741af
4e8cdc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import AppKit | ||
| import SwiftUI | ||
| import Observation | ||
| import Darwin | ||
| import Bonsplit | ||
| import UniformTypeIdentifiers | ||
|
|
@@ -634,11 +635,8 @@ struct cmuxApp: App { | |
| } | ||
|
|
||
| Window(String(localized: "settings.title", defaultValue: "Settings"), id: SettingsWindowPresenter.windowID) { | ||
| SettingsRootView() | ||
| SettingsWindowRootView() | ||
| .cmuxAppearanceColorScheme(appearanceMode) | ||
| .background(WindowAccessor { window in | ||
| SettingsWindowPresenter.configure(window: window) | ||
| }) | ||
| } | ||
| .defaultSize(width: 980, height: 680) | ||
| .windowResizability(.contentMinSize) | ||
|
|
@@ -5322,8 +5320,7 @@ struct SettingsView: View { | |
| @State private var didRequestBrowserImportDetection = false | ||
| @State private var isDetectingImportBrowsers = false | ||
| @State private var browserImportDetectionGeneration = 0 | ||
| @State private var browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText | ||
| @State private var socketPasswordDraft = "" | ||
| @Bindable var draftState: SettingsDraftState | ||
| @State private var socketPasswordStatusMessage: String? | ||
| @State private var socketPasswordStatusIsError = false | ||
| @State private var notificationCustomSoundStatusMessage: String? | ||
|
|
@@ -5806,7 +5803,7 @@ struct SettingsView: View { | |
| } | ||
|
|
||
| private var browserInsecureHTTPAllowlistHasUnsavedChanges: Bool { | ||
| browserInsecureHTTPAllowlistDraft != browserInsecureHTTPAllowlist | ||
| draftState.browserInsecureHTTPAllowlistDraft != browserInsecureHTTPAllowlist | ||
| } | ||
|
|
||
| private var hasCustomNotificationSoundFilePath: Bool { | ||
|
|
@@ -6087,7 +6084,7 @@ struct SettingsView: View { | |
| } | ||
|
|
||
| private func saveSocketPassword() { | ||
| let trimmed = socketPasswordDraft.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let trimmed = draftState.socketPasswordDraft.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !trimmed.isEmpty else { | ||
| socketPasswordStatusMessage = String(localized: "settings.automation.socketPassword.enterFirst", defaultValue: "Enter a password first.") | ||
| socketPasswordStatusIsError = true | ||
|
|
@@ -6096,7 +6093,7 @@ struct SettingsView: View { | |
|
|
||
| do { | ||
| try SocketControlPasswordStore.savePassword(trimmed) | ||
| socketPasswordDraft = "" | ||
| draftState.socketPasswordDraft = "" | ||
| socketPasswordStatusMessage = String(localized: "settings.automation.socketPassword.saved", defaultValue: "Password saved.") | ||
| socketPasswordStatusIsError = false | ||
| } catch { | ||
|
|
@@ -6108,7 +6105,7 @@ struct SettingsView: View { | |
| private func clearSocketPassword() { | ||
| do { | ||
| try SocketControlPasswordStore.clearPassword() | ||
| socketPasswordDraft = "" | ||
| draftState.socketPasswordDraft = "" | ||
| socketPasswordStatusMessage = String(localized: "settings.automation.socketPassword.cleared", defaultValue: "Password cleared.") | ||
| socketPasswordStatusIsError = false | ||
| } catch { | ||
|
|
@@ -7012,15 +7009,15 @@ struct SettingsView: View { | |
| : String(localized: "settings.automation.socketPassword.subtitleUnset", defaultValue: "No password set. External clients will be blocked until one is configured.") | ||
| ) { | ||
| HStack(spacing: 8) { | ||
| SecureField(String(localized: "settings.automation.socketPassword.placeholder", defaultValue: "Password"), text: $socketPasswordDraft) | ||
| SecureField(String(localized: "settings.automation.socketPassword.placeholder", defaultValue: "Password"), text: $draftState.socketPasswordDraft) | ||
| .textFieldStyle(.roundedBorder) | ||
| .frame(width: 170) | ||
| Button(hasSocketPasswordConfigured ? String(localized: "settings.automation.socketPassword.change", defaultValue: "Change") : String(localized: "settings.automation.socketPassword.set", defaultValue: "Set")) { | ||
| saveSocketPassword() | ||
| } | ||
| .buttonStyle(.bordered) | ||
| .controlSize(.small) | ||
| .disabled(socketPasswordDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) | ||
| .disabled(draftState.socketPasswordDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) | ||
| if hasSocketPasswordConfigured { | ||
| Button(String(localized: "settings.automation.socketPassword.clear", defaultValue: "Clear")) { | ||
| clearSocketPassword() | ||
|
|
@@ -7349,7 +7346,7 @@ struct SettingsView: View { | |
| .font(.caption) | ||
| .foregroundStyle(.secondary) | ||
|
|
||
| TextEditor(text: $browserInsecureHTTPAllowlistDraft) | ||
| TextEditor(text: $draftState.browserInsecureHTTPAllowlistDraft) | ||
| .font(.system(size: 12, weight: .regular, design: .monospaced)) | ||
| .frame(minHeight: 86) | ||
| .padding(6) | ||
|
|
@@ -7811,7 +7808,7 @@ struct SettingsView: View { | |
| browserImportHintVariantRaw = BrowserImportHintSettings.variant(for: browserImportHintVariantRaw).rawValue | ||
| didLoadBrowserHistoryForSettings = BrowserHistoryStore.shared.isLoaded | ||
| browserHistoryEntryCount = didLoadBrowserHistoryForSettings ? BrowserHistoryStore.shared.entries.count : 0 | ||
| browserInsecureHTTPAllowlistDraft = browserInsecureHTTPAllowlist | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| draftState.syncBrowserInsecureHTTPAllowlistFromSavedValue(browserInsecureHTTPAllowlist) | ||
| reloadWorkspaceTabColorSettings() | ||
| refreshNotificationCustomSoundStatus() | ||
| let target = SettingsWindowPresenter.consumePendingContentNavigationTarget() | ||
|
|
@@ -7832,11 +7829,9 @@ struct SettingsView: View { | |
| .onChange(of: notificationSoundCustomFilePath) { _, _ in | ||
| refreshNotificationCustomSoundStatus() | ||
| } | ||
| .onChange(of: browserInsecureHTTPAllowlist) { oldValue, newValue in | ||
| .onChange(of: browserInsecureHTTPAllowlist) { _, newValue in | ||
| // Keep draft in sync with external changes unless the user has local unsaved edits. | ||
| if browserInsecureHTTPAllowlistDraft == oldValue { | ||
| browserInsecureHTTPAllowlistDraft = newValue | ||
| } | ||
| draftState.syncBrowserInsecureHTTPAllowlistFromSavedValue(newValue) | ||
| } | ||
| .onReceive(BrowserHistoryStore.shared.$entries) { entries in | ||
| guard BrowserHistoryStore.shared.isLoaded else { return } | ||
|
|
@@ -7962,7 +7957,8 @@ struct SettingsView: View { | |
| browserHostWhitelist = BrowserLinkOpenSettings.defaultBrowserHostWhitelist | ||
| browserExternalOpenPatterns = BrowserLinkOpenSettings.defaultBrowserExternalOpenPatterns | ||
| browserInsecureHTTPAllowlist = BrowserInsecureHTTPSettings.defaultAllowlistText | ||
| browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText | ||
| draftState.browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText | ||
| draftState.browserInsecureHTTPAllowlistSyncedValue = BrowserInsecureHTTPSettings.defaultAllowlistText | ||
| notificationSound = NotificationSoundSettings.defaultValue | ||
| notificationSoundCustomFilePath = NotificationSoundSettings.defaultCustomFilePath | ||
| notificationCustomSoundStatusMessage = nil | ||
|
|
@@ -8040,7 +8036,7 @@ struct SettingsView: View { | |
| sidebarMatchTerminalBackground = false | ||
| showOpenAccessConfirmation = false | ||
| pendingOpenAccessMode = nil | ||
| socketPasswordDraft = "" | ||
| draftState.socketPasswordDraft = "" | ||
| socketPasswordStatusMessage = nil | ||
| socketPasswordStatusIsError = false | ||
| refreshDetectedImportBrowsers() | ||
|
|
@@ -8095,7 +8091,8 @@ struct SettingsView: View { | |
| } | ||
|
|
||
| private func saveBrowserInsecureHTTPAllowlist() { | ||
| browserInsecureHTTPAllowlist = browserInsecureHTTPAllowlistDraft | ||
| browserInsecureHTTPAllowlist = draftState.browserInsecureHTTPAllowlistDraft | ||
| draftState.browserInsecureHTTPAllowlistSyncedValue = draftState.browserInsecureHTTPAllowlistDraft | ||
| } | ||
|
|
||
| private func refreshDetectedImportBrowsers() { | ||
|
|
@@ -8817,22 +8814,104 @@ private struct GlobalHotkeySection: View { | |
| } | ||
| } | ||
|
|
||
| private struct SettingsWindowRootView: View { | ||
| @State private var draftState = SettingsDraftState() | ||
| @State private var windowReference = WeakSettingsWindowReference() | ||
| @State private var shouldRenderSettingsContent = true | ||
|
|
||
| var body: some View { | ||
| Group { | ||
| if shouldRenderSettingsContent { | ||
| SettingsRootView(draftState: draftState) | ||
| } else { | ||
| Color.clear | ||
| .frame( | ||
| minWidth: SettingsWindowPresenter.minimumSize.width, | ||
| minHeight: SettingsWindowPresenter.minimumSize.height | ||
| ) | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
| .background(WindowAccessor { window in | ||
| windowReference.window = window | ||
| SettingsWindowPresenter.configure(window: window) | ||
| setContentVisibility(!window.isMiniaturized) | ||
| }) | ||
| .onReceive(NotificationCenter.default.publisher(for: NSWindow.didMiniaturizeNotification)) { notification in | ||
| guard isObservedWindow(notification.object) else { return } | ||
| setContentVisibility(false) | ||
| } | ||
| .onReceive(NotificationCenter.default.publisher(for: NSWindow.didDeminiaturizeNotification)) { notification in | ||
| guard isObservedWindow(notification.object) else { return } | ||
| setContentVisibility(true) | ||
| } | ||
| .onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { notification in | ||
| guard isObservedWindow(notification.object) else { return } | ||
| setContentVisibility(true) | ||
| } | ||
| .onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeMainNotification)) { notification in | ||
| guard isObservedWindow(notification.object) else { return } | ||
| setContentVisibility(true) | ||
| } | ||
| .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { notification in | ||
| guard isObservedWindow(notification.object) else { return } | ||
| setContentVisibility(false) | ||
| windowReference.window = nil | ||
| } | ||
| } | ||
|
|
||
| private func isObservedWindow(_ object: Any?) -> Bool { | ||
| guard | ||
| let notificationWindow = object as? NSWindow, | ||
| let window = windowReference.window | ||
| else { | ||
| return false | ||
| } | ||
| return notificationWindow === window | ||
| } | ||
|
|
||
| private func setContentVisibility(_ isVisible: Bool) { | ||
| guard shouldRenderSettingsContent != isVisible else { return } | ||
| shouldRenderSettingsContent = isVisible | ||
| } | ||
| } | ||
|
Comment on lines
+8820
to
+8876
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because Rule Used: Flag SwiftUI changes that can cause stale state, b... (source) |
||
|
|
||
| @MainActor | ||
| private final class WeakSettingsWindowReference { | ||
| weak var window: NSWindow? | ||
| } | ||
|
|
||
| @MainActor | ||
| @Observable | ||
| final class SettingsDraftState { | ||
| var browserInsecureHTTPAllowlistDraft = BrowserInsecureHTTPSettings.defaultAllowlistText | ||
| var browserInsecureHTTPAllowlistSyncedValue = BrowserInsecureHTTPSettings.defaultAllowlistText | ||
| var socketPasswordDraft = "" | ||
| var settingsColumnVisibility: NavigationSplitViewVisibility = .all | ||
| var settingsSearchText = "" | ||
|
|
||
| func syncBrowserInsecureHTTPAllowlistFromSavedValue(_ savedValue: String) { | ||
| if browserInsecureHTTPAllowlistDraft == browserInsecureHTTPAllowlistSyncedValue { | ||
| browserInsecureHTTPAllowlistDraft = savedValue | ||
| } | ||
| browserInsecureHTTPAllowlistSyncedValue = savedValue | ||
| } | ||
| } | ||
|
|
||
| private struct SettingsRootView: View { | ||
| @Bindable var draftState: SettingsDraftState | ||
| @SceneStorage("selectedSettingsSection") private var selectedSectionRaw = SettingsNavigationTarget.account.rawValue | ||
| @SceneStorage("selectedSettingsSidebarEntry") private var selectedSidebarEntryID = SettingsSearchIndex.defaultSelectionID | ||
| @State private var columnVisibility: NavigationSplitViewVisibility = .all | ||
| @State private var searchText = "" | ||
|
|
||
| private var selectedSection: SettingsNavigationTarget { | ||
| SettingsNavigationTarget(rawValue: selectedSectionRaw) ?? .account | ||
| } | ||
|
|
||
| private var sidebarEntries: [SettingsSearchEntry] { | ||
| SettingsSearchIndex.entries(matching: searchText) | ||
| SettingsSearchIndex.entries(matching: draftState.settingsSearchText) | ||
| } | ||
|
|
||
| private var isSearching: Bool { | ||
| !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty | ||
| !draftState.settingsSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty | ||
| } | ||
|
|
||
| private var sidebarSelection: Binding<String> { | ||
|
|
@@ -8843,7 +8922,7 @@ private struct SettingsRootView: View { | |
| } | ||
|
|
||
| var body: some View { | ||
| NavigationSplitView(columnVisibility: $columnVisibility) { | ||
| NavigationSplitView(columnVisibility: $draftState.settingsColumnVisibility) { | ||
| List(selection: sidebarSelection) { | ||
| if sidebarEntries.isEmpty { | ||
| Text(String(localized: "settings.search.noResults", defaultValue: "No Results")) | ||
|
|
@@ -8858,22 +8937,21 @@ private struct SettingsRootView: View { | |
| .listStyle(.sidebar) | ||
| .navigationTitle(String(localized: "settings.title", defaultValue: "Settings")) | ||
| .searchable( | ||
| text: $searchText, | ||
| text: $draftState.settingsSearchText, | ||
| placement: .sidebar, | ||
| prompt: Text(String(localized: "settings.search.prompt", defaultValue: "Search")) | ||
| ) | ||
| .navigationSplitViewColumnWidth(210) | ||
| } detail: { | ||
| SettingsView() | ||
| SettingsView(draftState: draftState) | ||
| } | ||
| .navigationSplitViewStyle(.balanced) | ||
| .frame(minWidth: SettingsWindowPresenter.minimumSize.width, minHeight: SettingsWindowPresenter.minimumSize.height) | ||
| .onChange(of: searchText) { _, newValue in | ||
| .onChange(of: draftState.settingsSearchText) { _, newValue in | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Root navigates on every remountMedium Severity Unmounting settings while minimized means Reviewed by Cursor Bugbot for commit f184ec6. Configure here. |
||
| guard newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } | ||
| selectedSidebarEntryID = SettingsSearchIndex.sectionID(for: selectedSection) | ||
| } | ||
| .onAppear { | ||
| searchText = "" | ||
| if let target = SettingsWindowPresenter.consumePendingNavigationTarget() { | ||
| navigate(to: target, postRequest: true) | ||
| } else { | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.