From 2426c8d729bf07bd82b028eeb4ea0e9f37602563 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 11:17:32 -0700 Subject: [PATCH 01/11] Expose stable window title templates --- .../CmuxSettings/Keys/AppCatalogSection.swift | 8 ++ Sources/App/WindowTitleTemplate.swift | 63 ++++++++++ Sources/AppDelegate.swift | 35 +++++- Sources/CmuxSettingsJSONPathSupport.swift | 1 + ...rdShortcutSettingsFileStore+Template.swift | 1 + .../KeyboardShortcutSettingsFileStore.swift | 5 + Sources/TabManager.swift | 32 ++++++ cmux.xcodeproj/project.pbxproj | 8 ++ cmuxTests/WindowTitleTemplateTests.swift | 108 ++++++++++++++++++ docs/configuration.md | 23 ++++ web/app/[locale]/docs/configuration/page.tsx | 1 + web/data/cmux.schema.json | 6 + web/messages/en.json | 1 + web/messages/ja.json | 1 + 14 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 Sources/App/WindowTitleTemplate.swift create mode 100644 cmuxTests/WindowTitleTemplateTests.swift diff --git a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/AppCatalogSection.swift b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/AppCatalogSection.swift index c72277d4f867..d7efdd3e0866 100644 --- a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/AppCatalogSection.swift +++ b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/AppCatalogSection.swift @@ -20,6 +20,14 @@ public struct AppCatalogSection: SettingCatalogSection { userDefaultsKey: "appIconMode" ) + /// Optional macOS window title template. Empty preserves the default + /// active-workspace title behavior. + public let windowTitleTemplate = DefaultsKey( + id: "app.windowTitleTemplate", + defaultValue: "", + userDefaultsKey: "windowTitleTemplate" + ) + public let menuBarOnly = DefaultsKey( id: "app.menuBarOnly", defaultValue: false, diff --git a/Sources/App/WindowTitleTemplate.swift b/Sources/App/WindowTitleTemplate.swift new file mode 100644 index 000000000000..950686838e0c --- /dev/null +++ b/Sources/App/WindowTitleTemplate.swift @@ -0,0 +1,63 @@ +import Foundation + +struct WindowTitleTemplate: Equatable, Sendable { + struct Context: Equatable, Sendable { + var defaultTitle: String + var activeWorkspace: String + var activeDirectory: String + var windowId: UUID + var appName: String + } + + static let userDefaultsKey = "windowTitleTemplate" + static let defaultRawValue = "" + + var rawValue: String + + static func configured(defaults: UserDefaults = .standard) -> WindowTitleTemplate? { + let rawValue = defaults.string(forKey: userDefaultsKey) ?? defaultRawValue + guard !rawValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return nil + } + return WindowTitleTemplate(rawValue: rawValue) + } + + func resolved(context: Context) -> String { + let replacements = Dictionary(uniqueKeysWithValues: replacements(context: context)) + var resolved = "" + var index = rawValue.startIndex + while index < rawValue.endIndex { + guard rawValue[index] == "{", + let closeIndex = rawValue[index...].firstIndex(of: "}") else { + resolved.append(rawValue[index]) + index = rawValue.index(after: index) + continue + } + + let placeholderStart = rawValue.index(after: index) + let placeholder = String(rawValue[placeholderStart.. [(String, String)] { + [ + ("windowId", context.windowId.uuidString.lowercased()), + ("windowToken", Self.windowToken(for: context.windowId)), + ("activeWorkspace", context.activeWorkspace), + ("activeDirectory", context.activeDirectory), + ("defaultTitle", context.defaultTitle), + ("appName", context.appName), + ] + } + + private static func windowToken(for windowId: UUID) -> String { + String(windowId.uuidString.prefix(8)).lowercased() + } +} diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 7c1a39b56bab..6fc6ce8a5a58 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4421,6 +4421,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent #endif if let existing = mainWindowContexts[key] { tabManager.window = window + tabManager.windowId = existing.windowId existing.window = window let resolvedFileExplorerState = fileExplorerState ?? existing.fileExplorerState if let fileExplorerState { @@ -4445,6 +4446,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) #endif existing.tabManager.window = existingWindow + existing.tabManager.windowId = existing.windowId existing.keyboardFocusCoordinator.update( window: existingWindow, tabManager: existing.tabManager, @@ -4455,6 +4457,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return } tabManager.window = window + tabManager.windowId = windowId existing.window = window let resolvedFileExplorerState = fileExplorerState ?? existing.fileExplorerState if let fileExplorerState { @@ -4471,6 +4474,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent reindexMainWindowContextIfNeeded(existing, for: window) } else { tabManager.window = window + tabManager.windowId = windowId mainWindowContexts[key] = MainWindowContext( windowId: windowId, tabManager: tabManager, @@ -4523,6 +4527,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent cmuxConfigStore: CmuxConfigStore? = nil, fileExplorerState: FileExplorerState? = nil ) -> UUID { + tabManager.windowId = windowId mainWindowContexts[ObjectIdentifier(tabManager)] = MainWindowContext( windowId: windowId, tabManager: tabManager, @@ -7041,10 +7046,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return createMainWindow( initialTerminalInput: suppressWelcome ? "" : nil, + preferredWindowId: startupPrimaryWindowIdForInitialMainWindow(), shouldActivate: shouldActivate ) } + private func startupPrimaryWindowIdForInitialMainWindow() -> UUID? { + guard !didAttemptStartupSessionRestore else { return nil } + guard !didHandleExplicitOpenIntentAtStartup else { return nil } + return startupSessionSnapshot?.windows.first?.windowId + } + + private func availableWindowIdForNewMainWindow(preferredWindowId: UUID?) -> UUID? { + guard let preferredWindowId else { return nil } + guard !mainWindowContexts.values.contains(where: { $0.windowId == preferredWindowId }) else { + return nil + } + return preferredWindowId + } + private func hasVisibleMainTerminalWindow() -> Bool { mainWindowContexts.values.contains { context in guard let window = resolvedWindow(for: context) else { return false } @@ -8132,19 +8152,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent initialWorkingDirectory: String? = nil, initialTerminalInput: String? = nil, sessionWindowSnapshot: SessionWindowSnapshot? = nil, + preferredWindowId: UUID? = nil, shouldActivate: Bool = true, sourceWindow preferredSourceWindow: NSWindow? = nil, remapClosedPanelHistoryFromSessionSnapshot: Bool = true, restoredSessionSnapshotHandler: (([[UUID: UUID]], TabManager) -> Void)? = nil ) -> UUID { reserveInitialSocketPathIfNeeded() - let windowId = UUID() + let requestedWindowId = preferredWindowId ?? sessionWindowSnapshot?.windowId + let windowId = availableWindowIdForNewMainWindow(preferredWindowId: requestedWindowId) ?? UUID() let tabManager = TabManager( initialWorkspaceTitle: initialWorkspaceTitle, initialWorkingDirectory: initialWorkingDirectory, initialTerminalInput: initialTerminalInput, autoWelcomeIfNeeded: initialTerminalInput == nil ) + tabManager.windowId = windowId if let sessionWindowSnapshot { let restoredPanelIdsByWorkspaceIndex = tabManager.restoreSessionSnapshot( sessionWindowSnapshot.tabManager, @@ -12413,11 +12436,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent guard seenStores.insert(identifier).inserted else { continue } store.loadAll() } + refreshWindowTitlesAcrossMainWindows() #if DEBUG cmuxDebugLog("cmuxConfig.reload source=\(source) stores=\(seenStores.count)") #endif } + private func refreshWindowTitlesAcrossMainWindows() { + var seenManagers = Set() + for context in mainWindowContexts.values { + let identifier = ObjectIdentifier(context.tabManager) + guard seenManagers.insert(identifier).inserted else { continue } + context.tabManager.refreshWindowTitle() + } + } + private func refreshGhosttyGotoSplitShortcuts() { guard let config = GhosttyApp.shared.config else { ghosttyGotoSplitLeftShortcut = nil diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index cb009e77154c..9402c880b452 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -68,6 +68,7 @@ extension CmuxSettingsFileStore { "app.language", "app.appearance", "app.appIcon", + "app.windowTitleTemplate", "app.menuBarOnly", "app.newWorkspacePlacement", "app.workspaceInheritWorkingDirectory", diff --git a/Sources/KeyboardShortcutSettingsFileStore+Template.swift b/Sources/KeyboardShortcutSettingsFileStore+Template.swift index fcfef8f619e1..d0e1d66d1471 100644 --- a/Sources/KeyboardShortcutSettingsFileStore+Template.swift +++ b/Sources/KeyboardShortcutSettingsFileStore+Template.swift @@ -60,6 +60,7 @@ extension CmuxSettingsFileStore { "language": LanguageSettings.defaultLanguage.rawValue, "appearance": AppearanceSettings.defaultMode.rawValue, "appIcon": AppIconSettings.defaultMode.rawValue, + "windowTitleTemplate": WindowTitleTemplate.defaultRawValue, "menuBarOnly": MenuBarOnlySettings.defaultMenuBarOnly, "newWorkspacePlacement": WorkspacePlacementSettings.defaultPlacement.rawValue, "forkConversationDefaultDestination": AgentConversationForkDefaultSettings.defaultDestination.rawValue, diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 02b3fb3cf4ab..bd8868822122 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -421,6 +421,11 @@ final class CmuxSettingsFileStore { } snapshot.managedUserDefaults[AppIconSettings.modeKey] = .string(mode.rawValue) } + if let raw = jsonString(section["windowTitleTemplate"]) { + snapshot.managedUserDefaults[WindowTitleTemplate.userDefaultsKey] = .string(raw) + } else if section.keys.contains("windowTitleTemplate") { + logInvalid("app.windowTitleTemplate", sourcePath: sourcePath) + } if let value = jsonBool(section["menuBarOnly"]) { snapshot.managedUserDefaults[MenuBarOnlySettings.menuBarOnlyKey] = .bool(value) } diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index ee2832225bff..490a4f815ace 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -1074,6 +1074,9 @@ class TabManager: ObservableObject { /// The window that owns this TabManager. Set by AppDelegate.registerMainWindow(). /// Used to apply title updates to the correct window instead of NSApp.keyWindow. weak var window: NSWindow? + /// Stable identifier of the owning macOS window. Used only for opt-in title + /// templates that expose a WM-matchable per-window token. + var windowId: UUID? @Published var tabs: [Workspace] = [] /// Named groupings of workspaces shown as collapsible sections in the sidebar. @@ -1378,6 +1381,7 @@ class TabManager: ObservableObject { MainActor.assumeIsolated { [weak self] in self?.sidebarMetadataSettingsDidChange() self?.refreshTabCloseButtonVisibility() + self?.refreshWindowTitle() } }) #if DEBUG @@ -6749,6 +6753,10 @@ class TabManager: ObservableObject { } } + func refreshWindowTitle() { + updateWindowTitleForSelectedTab() + } + private func updateWindowTitleForSelectedTab() { guard let selectedTabId, let tab = tabs.first(where: { $0.id == selectedTabId }) else { @@ -6781,6 +6789,30 @@ class TabManager: ObservableObject { } private func windowTitle(for tab: Workspace?) -> String { + let defaultTitle = defaultWindowTitle(for: tab) + guard let windowId, + let template = WindowTitleTemplate.configured() else { + return defaultTitle + } + + let workspaceTitle = tab.map { + resolvedWorkspaceDisplayTitle(for: $0) + .trimmingCharacters(in: .whitespacesAndNewlines) + } ?? "" + let activeDirectory = tab?.currentDirectory + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let resolvedTitle = template.resolved(context: WindowTitleTemplate.Context( + defaultTitle: defaultTitle, + activeWorkspace: workspaceTitle.isEmpty ? defaultTitle : workspaceTitle, + activeDirectory: activeDirectory, + windowId: windowId, + appName: "cmux" + )) + let trimmedResolvedTitle = resolvedTitle.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedResolvedTitle.isEmpty ? defaultTitle : resolvedTitle + } + + private func defaultWindowTitle(for tab: Workspace?) -> String { guard let tab else { return "cmux" } let trimmedTitle = resolvedWorkspaceDisplayTitle(for: tab) .trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 688d1e06c543..6d56a687d5bb 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -746,6 +746,8 @@ D0B10024A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10025A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift */; }; 313585583035A1E685010A97 /* WindowKeyDownReplayGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */; }; 8770D0F2D2BB45359D6BE5E3 /* WindowKeyDownReplayGuardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */; }; + 604500100000000000000001 /* WindowTitleTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000002 /* WindowTitleTemplate.swift */; }; + 604500100000000000000003 /* WindowTitleTemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000004 /* WindowTitleTemplateTests.swift */; }; A5001209 /* WindowToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001219 /* WindowToolbarController.swift */; }; 9FC97DA1B422776BB1EEC821 /* Workspace+CustomSidebarPullRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C2726A34F27E7A43B77368 /* Workspace+CustomSidebarPullRequests.swift */; }; D7AB00000000000000000015 /* Workspace+DetachedSurfaceTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000016 /* Workspace+DetachedSurfaceTransfer.swift */; }; @@ -1551,6 +1553,8 @@ D0B10025A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowInputRoutingContext.swift; sourceTree = ""; }; 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowKeyDownReplayGuard.swift; sourceTree = ""; }; B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowKeyDownReplayGuardTests.swift; sourceTree = ""; }; + 604500100000000000000002 /* WindowTitleTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplate.swift; sourceTree = ""; }; + 604500100000000000000004 /* WindowTitleTemplateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTitleTemplateTests.swift; sourceTree = ""; }; A5001219 /* WindowToolbarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowToolbarController.swift; sourceTree = ""; }; 42C2726A34F27E7A43B77368 /* Workspace+CustomSidebarPullRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CustomSidebarPullRequests.swift"; sourceTree = ""; }; D7AB00000000000000000016 /* Workspace+DetachedSurfaceTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+DetachedSurfaceTransfer.swift"; sourceTree = ""; }; @@ -1908,6 +1912,7 @@ C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */, B42A82C6AA614E74873D9A5F /* ShortcutRoutingSupport.swift */, 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */, + 604500100000000000000002 /* WindowTitleTemplate.swift */, AB7F2E9143904957AAA70726 /* ShortcutBareStartRouting.swift */, E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */, C7934BB35B66491B1BCA8064 /* MenuBarExtraController.swift */, @@ -2440,6 +2445,7 @@ D7C0DE00000000000000A104 /* CmuxWebViewContextMenuLinkCaptureTests.swift */, D1F0A00300000000000000C2 /* PhonePushPresenceGateTests.swift */, B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */, + 604500100000000000000004 /* WindowTitleTemplateTests.swift */, D1FFC0DE000000000000C001 /* DiffCommentStoreTests.swift */, C9A57002C9A57002C9A57002 /* WorkspaceGroupTests.swift */, FEED49850000000000000002 /* FeedEventClassificationTests.swift */, @@ -3331,6 +3337,7 @@ 807E058A23061EFB70A1B7F8 /* WindowGlassEffect.swift in Sources */, D0B10024A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift in Sources */, 313585583035A1E685010A97 /* WindowKeyDownReplayGuard.swift in Sources */, + 604500100000000000000001 /* WindowTitleTemplate.swift in Sources */, A5001209 /* WindowToolbarController.swift in Sources */, 9FC97DA1B422776BB1EEC821 /* Workspace+CustomSidebarPullRequests.swift in Sources */, D7AB00000000000000000015 /* Workspace+DetachedSurfaceTransfer.swift in Sources */, @@ -3631,6 +3638,7 @@ 063BC42CEE257D6213A2E30C /* WindowAndDragTests.swift in Sources */, F4200000A1B2C3D4E5F60718 /* WindowAppearanceSnapshotTests.swift in Sources */, 8770D0F2D2BB45359D6BE5E3 /* WindowKeyDownReplayGuardTests.swift in Sources */, + 604500100000000000000003 /* WindowTitleTemplateTests.swift in Sources */, 7F0A0E04A8CADC84BB4F1DF7 /* WorkspaceActionDispatcherTests.swift in Sources */, D7AB00000000000000000011 /* WorkspaceAdjacentPaneMoveTests.swift in Sources */, A11EAE000000000000000000 /* WorkspaceAppearanceConfigResolutionTests.swift in Sources */, diff --git a/cmuxTests/WindowTitleTemplateTests.swift b/cmuxTests/WindowTitleTemplateTests.swift new file mode 100644 index 000000000000..53465b8a6c17 --- /dev/null +++ b/cmuxTests/WindowTitleTemplateTests.swift @@ -0,0 +1,108 @@ +import Foundation +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@Suite(.serialized) +struct WindowTitleTemplateTests { + private let backupsDefaultsKey = "cmux.settingsFile.backups.v1" + private let importedManagedDefaultsKey = "cmux.settingsFile.importedManagedDefaults.v1" + + @Test func resolvesWindowPlaceholdersAndPreservesUnknownPlaceholders() throws { + let windowId = try #require(UUID(uuidString: "01234567-89AB-CDEF-0123-456789ABCDEF")) + let template = WindowTitleTemplate( + rawValue: "[cmux:{windowToken}] {activeWorkspace} {activeDirectory} {windowId} {defaultTitle} {appName} {unknown}" + ) + + let resolved = template.resolved(context: WindowTitleTemplate.Context( + defaultTitle: "Fallback", + activeWorkspace: "Build", + activeDirectory: "/tmp/project", + windowId: windowId, + appName: "cmux" + )) + + #expect(resolved == "[cmux:01234567] Build /tmp/project 01234567-89ab-cdef-0123-456789abcdef Fallback cmux {unknown}") + } + + @Test func configuredTemplateTreatsBlankDefaultsValueAsDisabled() throws { + let defaults = try isolatedDefaults() + defaults.set(" \n", forKey: WindowTitleTemplate.userDefaultsKey) + + #expect(WindowTitleTemplate.configured(defaults: defaults) == nil) + } + + @Test func resolverDoesNotExpandPlaceholdersInsideReplacementValues() throws { + let windowId = try #require(UUID(uuidString: "01234567-89AB-CDEF-0123-456789ABCDEF")) + let template = WindowTitleTemplate(rawValue: "{activeWorkspace} {appName}") + + let resolved = template.resolved(context: WindowTitleTemplate.Context( + defaultTitle: "Fallback", + activeWorkspace: "{windowId}", + activeDirectory: "{windowToken}", + windowId: windowId, + appName: "cmux" + )) + + #expect(resolved == "{windowId} cmux") + } + + @Test func settingsFileStoreAppliesAppWindowTitleTemplate() throws { + let defaults = UserDefaults.standard + let keys = [ + WindowTitleTemplate.userDefaultsKey, + backupsDefaultsKey, + importedManagedDefaultsKey, + ] + let previousValues: [String: Any?] = Dictionary( + uniqueKeysWithValues: keys.map { ($0, defaults.object(forKey: $0)) } + ) + defer { + restore(previousValues, defaults: defaults) + } + keys.forEach { defaults.removeObject(forKey: $0) } + + let directoryURL = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-window-title-template-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directoryURL) } + + let settingsFileURL = directoryURL.appendingPathComponent("cmux.json", isDirectory: false) + try """ + { + "app": { + "windowTitleTemplate": "[cmux:{windowToken}] {activeWorkspace}" + } + } + """.write(to: settingsFileURL, atomically: true, encoding: .utf8) + + _ = KeyboardShortcutSettingsFileStore( + primaryPath: settingsFileURL.path, + fallbackPath: nil, + startWatching: false + ) + + #expect(defaults.string(forKey: WindowTitleTemplate.userDefaultsKey) == "[cmux:{windowToken}] {activeWorkspace}") + } + + private func isolatedDefaults() throws -> UserDefaults { + let suiteName = "cmux.WindowTitleTemplateTests.\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defaults.removePersistentDomain(forName: suiteName) + return defaults + } + + private func restore(_ values: [String: Any?], defaults: UserDefaults) { + for (key, value) in values { + if let value { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + } +} diff --git a/docs/configuration.md b/docs/configuration.md index 851474739510..10e2a5a27345 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,6 +2,29 @@ Global app preferences live in `~/.config/cmux/cmux.json`. +## `app.windowTitleTemplate` + +Opt-in template for the macOS `NSWindow.title`. Leave it unset or set it to an empty string to keep the default behavior, where the title follows the active workspace title or current directory. + +```json +{ + "app": { + "windowTitleTemplate": "[cmux:{windowToken}] {activeWorkspace}" + } +} +``` + +Supported placeholders: + +- `{windowId}`: the persisted per-window UUID. +- `{windowToken}`: the first 8 characters of the persisted window UUID. +- `{activeWorkspace}`: the active workspace title, falling back to the default title when the workspace title is blank. +- `{activeDirectory}`: the active workspace's current directory. +- `{defaultTitle}`: the title cmux would have used without a template. +- `{appName}`: `cmux`. + +For tiling window managers such as AeroSpace or yabai, match on the stable token in the title. For example, the template above gives each restored macOS window a title containing `[cmux:abcd1234]`, so a rule can match `\\[cmux:abcd1234\\]`. The token is stable across relaunches for restored windows because it comes from the persisted window UUID. + ## `app.confirmQuit` Controls when cmux asks before quitting: diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index 446f5d55676e..1a125ef68559 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -64,6 +64,7 @@ function buildSettingsFileExample(t: ConfigurationTranslation) { // "appearance": "dark", // "menuBarOnly": false, // "newWorkspacePlacement": "afterCurrent", + // "windowTitleTemplate": "[cmux:{windowToken}] {activeWorkspace}", // "confirmQuit": "always", // "openSupportedFilesInCmux": true, // "workspaceInheritWorkingDirectory": true, diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index b4c017db9910..2a0180247dcb 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -266,6 +266,12 @@ "default": "automatic", "description": "Dock and app switcher icon style." }, + "windowTitleTemplate": { + "type": "string", + "default": "", + "descriptionKey": "schemaDescriptions.app.windowTitleTemplate", + "description": "Optional NSWindow title template. Blank keeps the default active-workspace title. Supported placeholders: {windowId}, {windowToken}, {activeWorkspace}, {activeDirectory}, {defaultTitle}, {appName}." + }, "menuBarOnly": { "type": "boolean", "default": false, diff --git a/web/messages/en.json b/web/messages/en.json index af10b8a069e3..02655cca8ade 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -865,6 +865,7 @@ }, "app": { "confirmQuit": "Control when cmux asks for confirmation before quitting. DEV builds always quit immediately regardless of this setting. Legacy app.warnBeforeQuit is still accepted as a boolean fallback.", + "windowTitleTemplate": "Optional NSWindow title template. Blank keeps the default active-workspace title. Supported placeholders: {windowId}, {windowToken}, {activeWorkspace}, {activeDirectory}, {defaultTitle}, {appName}.", "warnBeforeQuit": "Legacy boolean fallback for quit confirmation. Use app.confirmQuit for new configs.", "warnBeforeClosingTabXButton": "Show a confirmation before closing a tab with the tab close button.", "hideTabCloseButton": "Hide tab close buttons in the pane tab bar.", diff --git a/web/messages/ja.json b/web/messages/ja.json index b8fbc35f3f4c..a65a1edc7ed8 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -824,6 +824,7 @@ }, "app": { "confirmQuit": "cmux の終了前に確認を求めるタイミングを制御します。DEV ビルドではこの設定に関係なく常にすぐ終了します。従来の app.warnBeforeQuit も boolean フォールバックとして引き続き受け付けます。", + "windowTitleTemplate": "任意の NSWindow タイトルテンプレートです。空欄のままなら、アクティブなワークスペース名を使う既定のタイトルを保ちます。対応するプレースホルダー: {windowId}, {windowToken}, {activeWorkspace}, {activeDirectory}, {defaultTitle}, {appName}。", "warnBeforeQuit": "終了確認用の従来の boolean フォールバックです。新しい設定では app.confirmQuit を使ってください。", "warnBeforeClosingTabXButton": "タブの閉じるボタンでタブを閉じる前に確認を表示します。", "hideTabCloseButton": "ペインのタブバーでタブの閉じるボタンを非表示にします。", From f719bca4aa7e95bbffb41e41e518a1d859d621ee Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 11:27:35 -0700 Subject: [PATCH 02/11] Fix window title template CI guards --- Sources/AppDelegate+WindowIdentity.swift | 39 +++++++++++ Sources/AppDelegate.swift | 38 +--------- .../KeyboardShortcutSettingsFileStore.swift | 7 +- Sources/TabManager+WindowTitle.swift | 67 ++++++++++++++++++ Sources/TabManager.swift | 70 ------------------- cmux.xcodeproj/project.pbxproj | 8 +++ web/messages/en.json | 2 +- web/messages/ja.json | 2 +- 8 files changed, 118 insertions(+), 115 deletions(-) create mode 100644 Sources/AppDelegate+WindowIdentity.swift create mode 100644 Sources/TabManager+WindowTitle.swift diff --git a/Sources/AppDelegate+WindowIdentity.swift b/Sources/AppDelegate+WindowIdentity.swift new file mode 100644 index 000000000000..1e4dd90815eb --- /dev/null +++ b/Sources/AppDelegate+WindowIdentity.swift @@ -0,0 +1,39 @@ +import AppKit +import Foundation + +@MainActor +extension AppDelegate { + func mainWindow(for windowId: UUID) -> NSWindow? { + windowForMainWindowId(windowId) + } + + func windowForMainWindowId(_ windowId: UUID) -> NSWindow? { + if let ctx = mainWindowContexts.values.first(where: { $0.windowId == windowId }), + let window = ctx.window { + return window + } + let expectedIdentifier = "cmux.main.\(windowId.uuidString)" + return NSApp.windows.first(where: { $0.identifier?.rawValue == expectedIdentifier }) + } + + func startupPrimaryWindowIdForInitialMainWindow() -> UUID? { + guard !didAttemptStartupSessionRestore else { return nil } + guard !didHandleExplicitOpenIntentAtStartup else { return nil } + return startupSessionSnapshot?.windows.first?.windowId + } + + func availableWindowIdForNewMainWindow(preferredWindowId: UUID?) -> UUID? { + guard let preferredWindowId else { return nil } + guard !mainWindowContexts.values.contains(where: { $0.windowId == preferredWindowId }) else { return nil } + return preferredWindowId + } + + func refreshWindowTitlesAcrossMainWindows() { + var seenManagers = Set() + for context in mainWindowContexts.values { + let identifier = ObjectIdentifier(context.tabManager) + guard seenManagers.insert(identifier).inserted else { continue } + context.tabManager.refreshWindowTitle() + } + } +} diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 6fc6ce8a5a58..9840b0d57257 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1050,7 +1050,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent /// Tracks the cascade point for new windows, matching Ghostty's upstream algorithm. /// Reset to `.zero` so the first window seeds the point from its own position. private var lastCascadePoint = NSPoint.zero - private var startupSessionSnapshot: AppSessionSnapshot? + var startupSessionSnapshot: AppSessionSnapshot? private var didPrepareStartupSessionSnapshot = false var didAttemptStartupSessionRestore = false private var isApplyingSessionRestore = false @@ -4987,10 +4987,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return moved } - func mainWindow(for windowId: UUID) -> NSWindow? { - windowForMainWindowId(windowId) - } - @discardableResult func focusScriptableMainWindow(windowId: UUID, bringToFront shouldBringToFront: Bool) -> Bool { guard let state = scriptableMainWindow(windowId: windowId), @@ -5756,15 +5752,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent DispatchQueue.main.asyncAfter(deadline: .now() + 0.16, execute: reassert) } - func windowForMainWindowId(_ windowId: UUID) -> NSWindow? { - if let ctx = mainWindowContexts.values.first(where: { $0.windowId == windowId }), - let window = ctx.window { - return window - } - let expectedIdentifier = "cmux.main.\(windowId.uuidString)" - return NSApp.windows.first(where: { $0.identifier?.rawValue == expectedIdentifier }) - } - private func resolvedWindow(for context: MainWindowContext) -> NSWindow? { if let window = context.window { return window @@ -7051,20 +7038,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) } - private func startupPrimaryWindowIdForInitialMainWindow() -> UUID? { - guard !didAttemptStartupSessionRestore else { return nil } - guard !didHandleExplicitOpenIntentAtStartup else { return nil } - return startupSessionSnapshot?.windows.first?.windowId - } - - private func availableWindowIdForNewMainWindow(preferredWindowId: UUID?) -> UUID? { - guard let preferredWindowId else { return nil } - guard !mainWindowContexts.values.contains(where: { $0.windowId == preferredWindowId }) else { - return nil - } - return preferredWindowId - } - private func hasVisibleMainTerminalWindow() -> Bool { mainWindowContexts.values.contains { context in guard let window = resolvedWindow(for: context) else { return false } @@ -12442,15 +12415,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent #endif } - private func refreshWindowTitlesAcrossMainWindows() { - var seenManagers = Set() - for context in mainWindowContexts.values { - let identifier = ObjectIdentifier(context.tabManager) - guard seenManagers.insert(identifier).inserted else { continue } - context.tabManager.refreshWindowTitle() - } - } - private func refreshGhosttyGotoSplitShortcuts() { guard let config = GhosttyApp.shared.config else { ghosttyGotoSplitLeftShortcut = nil diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index bd8868822122..026634592ae5 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -392,7 +392,6 @@ final class CmuxSettingsFileStore { return snapshot } - private func parseAppSection( _ section: [String: Any], sourcePath: String, @@ -421,14 +420,10 @@ final class CmuxSettingsFileStore { } snapshot.managedUserDefaults[AppIconSettings.modeKey] = .string(mode.rawValue) } - if let raw = jsonString(section["windowTitleTemplate"]) { - snapshot.managedUserDefaults[WindowTitleTemplate.userDefaultsKey] = .string(raw) - } else if section.keys.contains("windowTitleTemplate") { - logInvalid("app.windowTitleTemplate", sourcePath: sourcePath) - } if let value = jsonBool(section["menuBarOnly"]) { snapshot.managedUserDefaults[MenuBarOnlySettings.menuBarOnlyKey] = .bool(value) } + if let raw = jsonString(section["windowTitleTemplate"]) { snapshot.managedUserDefaults[WindowTitleTemplate.userDefaultsKey] = .string(raw) } else if section.keys.contains("windowTitleTemplate") { logInvalid("app.windowTitleTemplate", sourcePath: sourcePath) } if let raw = jsonString(section["newWorkspacePlacement"]) { guard let placement = NewWorkspacePlacement(rawValue: raw) else { logInvalid("app.newWorkspacePlacement", sourcePath: sourcePath) diff --git a/Sources/TabManager+WindowTitle.swift b/Sources/TabManager+WindowTitle.swift new file mode 100644 index 000000000000..501928b60fcb --- /dev/null +++ b/Sources/TabManager+WindowTitle.swift @@ -0,0 +1,67 @@ +import AppKit +import Foundation + +extension TabManager { + func refreshWindowTitle() { + updateWindowTitleForSelectedTab() + } + + func updateWindowTitleForSelectedTab() { + guard let selectedTabId, + let tab = tabs.first(where: { $0.id == selectedTabId }) else { + updateWindowTitle(for: nil) + return + } + updateWindowTitle(for: tab) + } + + func updateWindowTitle(for tab: Workspace?) { + let title = windowTitle(for: tab) + guard let targetWindow = window else { return } + targetWindow.title = title + } + + /// The name to display for `tab` across window chrome — the custom title + /// bar, `NSWindow.title`, and the toolbar command label. + /// + /// A workspace group's anchor is represented everywhere by the group itself + /// (the sidebar draws only the group header, never a separate anchor row, + /// per `SidebarWorkspaceRenderItem`), so for an anchor the single source of + /// truth for the displayed name is the group's `name`. The anchor's own + /// `title` is merely seeded equal to the group name at creation and would + /// otherwise drift when the group is renamed. + func resolvedWorkspaceDisplayTitle(for tab: Workspace) -> String { + if let group = workspaceGroups.first(where: { $0.anchorWorkspaceId == tab.id }) { + return group.name + } + return tab.title + } + + private func windowTitle(for tab: Workspace?) -> String { + let defaultTitle = defaultWindowTitle(for: tab) + guard let windowId, let template = WindowTitleTemplate.configured() else { return defaultTitle } + + let workspaceTitle = tab.map { + resolvedWorkspaceDisplayTitle(for: $0) + .trimmingCharacters(in: .whitespacesAndNewlines) + } ?? "" + let activeDirectory = tab?.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let resolvedTitle = template.resolved(context: WindowTitleTemplate.Context( + defaultTitle: defaultTitle, + activeWorkspace: workspaceTitle.isEmpty ? defaultTitle : workspaceTitle, + activeDirectory: activeDirectory, + windowId: windowId, + appName: "cmux" + )) + let trimmedResolvedTitle = resolvedTitle.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedResolvedTitle.isEmpty ? defaultTitle : resolvedTitle + } + + private func defaultWindowTitle(for tab: Workspace?) -> String { + guard let tab else { return "cmux" } + let trimmedTitle = resolvedWorkspaceDisplayTitle(for: tab).trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedTitle.isEmpty { return trimmedTitle } + let trimmedDirectory = tab.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedDirectory.isEmpty ? "cmux" : trimmedDirectory + } +} diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 490a4f815ace..af39efd94172 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -6753,76 +6753,6 @@ class TabManager: ObservableObject { } } - func refreshWindowTitle() { - updateWindowTitleForSelectedTab() - } - - private func updateWindowTitleForSelectedTab() { - guard let selectedTabId, - let tab = tabs.first(where: { $0.id == selectedTabId }) else { - updateWindowTitle(for: nil) - return - } - updateWindowTitle(for: tab) - } - - private func updateWindowTitle(for tab: Workspace?) { - let title = windowTitle(for: tab) - guard let targetWindow = window else { return } - targetWindow.title = title - } - - /// The name to display for `tab` across window chrome — the custom title - /// bar, `NSWindow.title`, and the toolbar command label. - /// - /// A workspace group's anchor is represented everywhere by the group itself - /// (the sidebar draws only the group header, never a separate anchor row, - /// per `SidebarWorkspaceRenderItem`), so for an anchor the single source of - /// truth for the displayed name is the group's `name`. The anchor's own - /// `title` is merely seeded equal to the group name at creation and would - /// otherwise drift when the group is renamed. - func resolvedWorkspaceDisplayTitle(for tab: Workspace) -> String { - if let group = workspaceGroups.first(where: { $0.anchorWorkspaceId == tab.id }) { - return group.name - } - return tab.title - } - - private func windowTitle(for tab: Workspace?) -> String { - let defaultTitle = defaultWindowTitle(for: tab) - guard let windowId, - let template = WindowTitleTemplate.configured() else { - return defaultTitle - } - - let workspaceTitle = tab.map { - resolvedWorkspaceDisplayTitle(for: $0) - .trimmingCharacters(in: .whitespacesAndNewlines) - } ?? "" - let activeDirectory = tab?.currentDirectory - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let resolvedTitle = template.resolved(context: WindowTitleTemplate.Context( - defaultTitle: defaultTitle, - activeWorkspace: workspaceTitle.isEmpty ? defaultTitle : workspaceTitle, - activeDirectory: activeDirectory, - windowId: windowId, - appName: "cmux" - )) - let trimmedResolvedTitle = resolvedTitle.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmedResolvedTitle.isEmpty ? defaultTitle : resolvedTitle - } - - private func defaultWindowTitle(for tab: Workspace?) -> String { - guard let tab else { return "cmux" } - let trimmedTitle = resolvedWorkspaceDisplayTitle(for: tab) - .trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmedTitle.isEmpty { - return trimmedTitle - } - let trimmedDirectory = tab.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmedDirectory.isEmpty ? "cmux" : trimmedDirectory - } - func focusTab( _ tabId: UUID, surfaceId: UUID? = nil, diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 6d56a687d5bb..fca27b5c959d 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 3865A0063865A0063865A006 /* AppDelegate+GlobalSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3865B0063865B0063865B006 /* AppDelegate+GlobalSearch.swift */; }; D7AB00000000000000000001 /* AppDelegate+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000002 /* AppDelegate+MoveTabToNewWorkspace.swift */; }; 2907A0012907A0012907A001 /* AppDelegate+RecoverableMainWindowRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2907A0022907A0022907A002 /* AppDelegate+RecoverableMainWindowRoutes.swift */; }; + 604500100000000000000005 /* AppDelegate+WindowIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000006 /* AppDelegate+WindowIdentity.swift */; }; A5001093 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001090 /* AppDelegate.swift */; }; 725746692D9647948561044D /* AppDelegateBareSpaceShortcutRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FCD4CC61D54A2F8F2F463D /* AppDelegateBareSpaceShortcutRoutingTests.swift */; }; E3309A09 /* AppDelegateEqualizeSplitsShortcutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3309A0A /* AppDelegateEqualizeSplitsShortcutTests.swift */; }; @@ -636,6 +637,7 @@ C3677003000000000000001 /* TabManager+CompatibilityTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3677003000000000000002 /* TabManager+CompatibilityTypes.swift */; }; D7AB00000000000000000013 /* TabManager+DetachedWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */; }; E3309A03 /* TabManager+EqualizeSplits.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3309A04 /* TabManager+EqualizeSplits.swift */; }; + 604500100000000000000007 /* TabManager+WindowTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000008 /* TabManager+WindowTitle.swift */; }; A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; }; 2BB56A710BB1FC50367E5BCF /* TabManagerSessionSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */; }; B6BF3DC98DB1495E57900199 /* TabManagerUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */; }; @@ -908,6 +910,7 @@ 3865B0063865B0063865B006 /* AppDelegate+GlobalSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Search/AppDelegate+GlobalSearch.swift"; sourceTree = ""; }; D7AB00000000000000000002 /* AppDelegate+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MoveTabToNewWorkspace.swift"; sourceTree = ""; }; 2907A0022907A0022907A002 /* AppDelegate+RecoverableMainWindowRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+RecoverableMainWindowRoutes.swift"; sourceTree = ""; }; + 604500100000000000000006 /* AppDelegate+WindowIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+WindowIdentity.swift"; sourceTree = ""; }; A5001090 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 17FCD4CC61D54A2F8F2F463D /* AppDelegateBareSpaceShortcutRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateBareSpaceShortcutRoutingTests.swift; sourceTree = ""; }; E3309A0A /* AppDelegateEqualizeSplitsShortcutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateEqualizeSplitsShortcutTests.swift; sourceTree = ""; }; @@ -1443,6 +1446,7 @@ C3677003000000000000002 /* TabManager+CompatibilityTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+CompatibilityTypes.swift"; sourceTree = ""; }; D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+DetachedWorkspace.swift"; sourceTree = ""; }; E3309A04 /* TabManager+EqualizeSplits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+EqualizeSplits.swift"; sourceTree = ""; }; + 604500100000000000000008 /* TabManager+WindowTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+WindowTitle.swift"; sourceTree = ""; }; A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; 10D684CFFB8CDEF89CE2D9E1 /* TabManagerSessionSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerSessionSnapshotTests.swift; sourceTree = ""; }; 42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerUnitTests.swift; sourceTree = ""; }; @@ -1958,6 +1962,7 @@ E3309A0E /* SplitEqualizer.swift */, C5CC15CEAA80B779ADEE78AA /* WorkspaceActionDispatcher.swift */, D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */, + 604500100000000000000008 /* TabManager+WindowTitle.swift */, E30750000000000000000001 /* WorkspaceTabColorResolution.swift */, A5001511 /* UITestRecorder.swift */, A5001520 /* PostHogAnalytics.swift */, @@ -2070,6 +2075,7 @@ 2907A0022907A0022907A002 /* AppDelegate+RecoverableMainWindowRoutes.swift */, C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */, C4160A040000000000000002 /* AppDelegate+FocusHistoryContextMenu.swift */, + 604500100000000000000006 /* AppDelegate+WindowIdentity.swift */, C10D00020000000000000002 /* CloudVMActionLauncher.swift */, B37A00000000000000000006 /* MainWindowFocusTypes.swift */, 2F0C05000000000000000001 /* MainWindowFocusController.swift */, @@ -2942,6 +2948,7 @@ 3865A0063865A0063865A006 /* AppDelegate+GlobalSearch.swift in Sources */, D7AB00000000000000000001 /* AppDelegate+MoveTabToNewWorkspace.swift in Sources */, 2907A0012907A0012907A001 /* AppDelegate+RecoverableMainWindowRoutes.swift in Sources */, + 604500100000000000000005 /* AppDelegate+WindowIdentity.swift in Sources */, A5001093 /* AppDelegate.swift in Sources */, A11EAB000000000000000000 /* AppearanceSettings.swift in Sources */, A5001621 /* AppleScriptSupport.swift in Sources */, @@ -3251,6 +3258,7 @@ C3677003000000000000001 /* TabManager+CompatibilityTypes.swift in Sources */, D7AB00000000000000000013 /* TabManager+DetachedWorkspace.swift in Sources */, E3309A03 /* TabManager+EqualizeSplits.swift in Sources */, + 604500100000000000000007 /* TabManager+WindowTitle.swift in Sources */, A5001003 /* TabManager.swift in Sources */, C7A503000000000000000002 /* TaskManagerSnapshot.swift in Sources */, C7A504000000000000000002 /* TaskManagerTypes.swift in Sources */, diff --git a/web/messages/en.json b/web/messages/en.json index 02655cca8ade..3f19fe593948 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -865,7 +865,7 @@ }, "app": { "confirmQuit": "Control when cmux asks for confirmation before quitting. DEV builds always quit immediately regardless of this setting. Legacy app.warnBeforeQuit is still accepted as a boolean fallback.", - "windowTitleTemplate": "Optional NSWindow title template. Blank keeps the default active-workspace title. Supported placeholders: {windowId}, {windowToken}, {activeWorkspace}, {activeDirectory}, {defaultTitle}, {appName}.", + "windowTitleTemplate": "Optional NSWindow title template. Blank keeps the default active-workspace title. Supported placeholders: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Legacy boolean fallback for quit confirmation. Use app.confirmQuit for new configs.", "warnBeforeClosingTabXButton": "Show a confirmation before closing a tab with the tab close button.", "hideTabCloseButton": "Hide tab close buttons in the pane tab bar.", diff --git a/web/messages/ja.json b/web/messages/ja.json index a65a1edc7ed8..098a1dd0c685 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -824,7 +824,7 @@ }, "app": { "confirmQuit": "cmux の終了前に確認を求めるタイミングを制御します。DEV ビルドではこの設定に関係なく常にすぐ終了します。従来の app.warnBeforeQuit も boolean フォールバックとして引き続き受け付けます。", - "windowTitleTemplate": "任意の NSWindow タイトルテンプレートです。空欄のままなら、アクティブなワークスペース名を使う既定のタイトルを保ちます。対応するプレースホルダー: {windowId}, {windowToken}, {activeWorkspace}, {activeDirectory}, {defaultTitle}, {appName}。", + "windowTitleTemplate": "任意の NSWindow タイトルテンプレートです。空欄のままなら、アクティブなワークスペース名を使う既定のタイトルを保ちます。対応するプレースホルダー: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'。", "warnBeforeQuit": "終了確認用の従来の boolean フォールバックです。新しい設定では app.confirmQuit を使ってください。", "warnBeforeClosingTabXButton": "タブの閉じるボタンでタブを閉じる前に確認を表示します。", "hideTabCloseButton": "ペインのタブバーでタブの閉じるボタンを非表示にします。", From 6d1fc60cbd96287252f59d260a252dc5f9f23777 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 11:29:27 -0700 Subject: [PATCH 03/11] Address window title template review feedback --- Sources/TabManager+WindowTitle.swift | 2 +- web/messages/ar.json | 1 + web/messages/bs.json | 1 + web/messages/da.json | 1 + web/messages/de.json | 1 + web/messages/es.json | 1 + web/messages/fr.json | 1 + web/messages/it.json | 1 + web/messages/km.json | 1 + web/messages/ko.json | 1 + web/messages/no.json | 1 + web/messages/pl.json | 1 + web/messages/pt-BR.json | 1 + web/messages/ru.json | 1 + web/messages/th.json | 1 + web/messages/tr.json | 1 + web/messages/uk.json | 1 + web/messages/zh-CN.json | 1 + web/messages/zh-TW.json | 1 + 19 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Sources/TabManager+WindowTitle.swift b/Sources/TabManager+WindowTitle.swift index 501928b60fcb..e198bb5ede81 100644 --- a/Sources/TabManager+WindowTitle.swift +++ b/Sources/TabManager+WindowTitle.swift @@ -54,7 +54,7 @@ extension TabManager { appName: "cmux" )) let trimmedResolvedTitle = resolvedTitle.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmedResolvedTitle.isEmpty ? defaultTitle : resolvedTitle + return trimmedResolvedTitle.isEmpty ? defaultTitle : trimmedResolvedTitle } private func defaultWindowTitle(for tab: Workspace?) -> String { diff --git a/web/messages/ar.json b/web/messages/ar.json index 1742c9136b2c..e0aec4eb8a26 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "يتحكم في وقت طلب cmux التأكيد قبل الإنهاء. تنتهي إصدارات DEV فوراً دائماً بغض النظر عن هذا الإعداد. يظل app.warnBeforeQuit القديم مقبولاً كخيار احتياطي منطقي.", + "windowTitleTemplate": "قالب اختياري لعنوان NSWindow. اتركه فارغاً للحفاظ على العنوان الافتراضي لمساحة العمل النشطة. العناصر النائبة المدعومة: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "خيار منطقي قديم احتياطي لتأكيد الإنهاء. استخدم app.confirmQuit للإعدادات الجديدة.", "warnBeforeClosingTabXButton": "اعرض تأكيدًا قبل إغلاق تبويب بزر إغلاق التبويب.", "hideTabCloseButton": "أخفِ أزرار إغلاق التبويبات في شريط تبويبات الجزء.", diff --git a/web/messages/bs.json b/web/messages/bs.json index bd5f262ebb89..0cf0b86621aa 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Određuje kada cmux traži potvrdu prije izlaska. DEV verzije se uvijek odmah zatvaraju bez obzira na ovu postavku. Naslijeđeni app.warnBeforeQuit se i dalje prihvata kao boolean rezervna opcija.", + "windowTitleTemplate": "Opcionalni predložak za NSWindow naslov. Prazno zadržava zadani naslov aktivnog radnog prostora. Podržani rezervisani izrazi: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Naslijeđena boolean rezervna opcija za potvrdu izlaska. Za nove konfiguracije koristite app.confirmQuit.", "warnBeforeClosingTabXButton": "Prikaži potvrdu prije zatvaranja taba dugmetom za zatvaranje taba.", "hideTabCloseButton": "Sakrij dugmad za zatvaranje tabova na traci tabova okna.", diff --git a/web/messages/da.json b/web/messages/da.json index d14643957389..a9f81a5147f0 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Styrer hvornår cmux beder om bekræftelse før afslutning. DEV-builds afsluttes altid straks uanset denne indstilling. Den ældre app.warnBeforeQuit accepteres stadig som boolesk fallback.", + "windowTitleTemplate": "Valgfri skabelon for NSWindow-titlen. Tom beholder den standardtitel, der følger det aktive workspace. Understøttede pladsholdere: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Ældre boolesk fallback til afslutningsbekræftelse. Brug app.confirmQuit til nye konfigurationer.", "warnBeforeClosingTabXButton": "Vis en bekræftelse, før en fane lukkes med fanens luk-knap.", "hideTabCloseButton": "Skjul fanernes luk-knapper i rudens fanelinje.", diff --git a/web/messages/de.json b/web/messages/de.json index 14a15f42687d..5e39652153af 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Steuert, wann cmux vor dem Beenden eine Bestätigung anfordert. DEV-Builds beenden sich unabhängig von dieser Einstellung immer sofort. Das ältere app.warnBeforeQuit wird weiterhin als boolescher Fallback akzeptiert.", + "windowTitleTemplate": "Optionale Vorlage für den NSWindow-Titel. Leer behält den Standardtitel des aktiven Arbeitsbereichs bei. Unterstützte Platzhalter: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Älterer boolescher Fallback für die Beenden-Bestätigung. Verwenden Sie app.confirmQuit für neue Konfigurationen.", "warnBeforeClosingTabXButton": "Vor dem Schließen eines Tabs über die Tab-Schließen-Schaltfläche eine Bestätigung anzeigen.", "hideTabCloseButton": "Tab-Schließen-Schaltflächen in der Pane-Tableiste ausblenden.", diff --git a/web/messages/es.json b/web/messages/es.json index cb5e59cb77b3..0fd61a75aa34 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Controla cuándo cmux pide confirmación antes de salir. Las compilaciones DEV siempre se cierran inmediatamente sin importar esta opción. El app.warnBeforeQuit heredado aún se acepta como alternativa booleana.", + "windowTitleTemplate": "Plantilla opcional para el título de NSWindow. En blanco conserva el título predeterminado del espacio de trabajo activo. Marcadores admitidos: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Alternativa booleana heredada para la confirmación de salida. Usa app.confirmQuit para configuraciones nuevas.", "warnBeforeClosingTabXButton": "Mostrar una confirmación antes de cerrar una pestaña con el botón de cerrar pestaña.", "hideTabCloseButton": "Ocultar los botones de cerrar pestaña en la barra de pestañas del panel.", diff --git a/web/messages/fr.json b/web/messages/fr.json index 7821eaf229eb..3b29b3b05ab0 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Contrôle quand cmux demande confirmation avant de quitter. Les builds DEV quittent toujours immédiatement, quel que soit ce réglage. L'ancien app.warnBeforeQuit reste accepté comme fallback booléen.", + "windowTitleTemplate": "Modèle facultatif pour le titre NSWindow. Vide, il conserve le titre par défaut de l'espace de travail actif. Espaces réservés pris en charge : '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Fallback booléen hérité pour la confirmation de fermeture. Utilisez app.confirmQuit pour les nouvelles configurations.", "warnBeforeClosingTabXButton": "Afficher une confirmation avant de fermer un onglet avec le bouton de fermeture d’onglet.", "hideTabCloseButton": "Masquer les boutons de fermeture d’onglet dans la barre d’onglets du volet.", diff --git a/web/messages/it.json b/web/messages/it.json index 9d0e9d300248..a6216f7c8faf 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Controlla quando cmux chiede conferma prima di uscire. Le build DEV escono sempre immediatamente indipendentemente da questa impostazione. Il vecchio app.warnBeforeQuit è ancora accettato come fallback booleano.", + "windowTitleTemplate": "Template opzionale per il titolo NSWindow. Se vuoto, mantiene il titolo predefinito dell'area di lavoro attiva. Segnaposto supportati: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Fallback booleano legacy per la conferma di uscita. Usa app.confirmQuit per le nuove configurazioni.", "warnBeforeClosingTabXButton": "Mostra una conferma prima di chiudere una scheda con il pulsante di chiusura scheda.", "hideTabCloseButton": "Nasconde i pulsanti di chiusura scheda nella barra delle schede del riquadro.", diff --git a/web/messages/km.json b/web/messages/km.json index 8de717d055f1..0f8ed8ba6f9e 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "កំណត់ពេលដែល cmux ស្នើសុំការបញ្ជាក់មុនពេលចាកចេញ។ កំណែ DEV តែងតែចាកចេញភ្លាមៗ ដោយមិនគិតពីការកំណត់នេះទេ។ app.warnBeforeQuit ចាស់នៅតែត្រូវបានទទួលយកជាជម្រើសបម្រុង boolean។", + "windowTitleTemplate": "គំរូចំណងជើង NSWindow ជាជម្រើស។ ទុកទទេ ដើម្បីរក្សាចំណងជើងលំនាំដើមរបស់ workspace សកម្ម។ Placeholder ដែលគាំទ្រ៖ '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'។", "warnBeforeQuit": "ជម្រើសបម្រុង boolean ចាស់សម្រាប់ការបញ្ជាក់មុនចាកចេញ។ ប្រើ app.confirmQuit សម្រាប់ការកំណត់ថ្មី។", "warnBeforeClosingTabXButton": "បង្ហាញការបញ្ជាក់ មុនពេលបិទផ្ទាំងដោយប៊ូតុងបិទផ្ទាំង។", "hideTabCloseButton": "លាក់ប៊ូតុងបិទផ្ទាំងនៅក្នុងរបារផ្ទាំងរបស់ផ្ទាំងបង្អួច។", diff --git a/web/messages/ko.json b/web/messages/ko.json index 3d71e412e30f..d1df18c543ac 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "cmux가 종료 전에 확인을 요청하는 시점을 제어합니다. DEV 빌드는 이 설정과 관계없이 항상 즉시 종료됩니다. 기존 app.warnBeforeQuit도 boolean 대체값으로 계속 허용됩니다.", + "windowTitleTemplate": "선택적 NSWindow 제목 템플릿입니다. 비워 두면 활성 워크스페이스 제목을 사용하는 기본 제목을 유지합니다. 지원되는 자리표시자: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "종료 확인을 위한 기존 boolean 대체값입니다. 새 구성에는 app.confirmQuit을 사용하세요.", "warnBeforeClosingTabXButton": "탭 닫기 버튼으로 탭을 닫기 전에 확인을 표시합니다.", "hideTabCloseButton": "창 영역 탭 막대에서 탭 닫기 버튼을 숨깁니다.", diff --git a/web/messages/no.json b/web/messages/no.json index 4d51b025f4cd..fc72a2e2030c 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Styrer når cmux ber om bekreftelse før avslutning. DEV-bygg avslutter alltid umiddelbart uansett denne innstillingen. Den eldre app.warnBeforeQuit godtas fortsatt som boolsk fallback.", + "windowTitleTemplate": "Valgfri mal for NSWindow-tittelen. Tom verdi beholder standardtittelen for aktivt arbeidsområde. Støttede plassholdere: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Eldre boolsk fallback for avslutningsbekreftelse. Bruk app.confirmQuit for nye konfigurasjoner.", "warnBeforeClosingTabXButton": "Vis en bekreftelse før en fane lukkes med fanens lukkeknapp.", "hideTabCloseButton": "Skjul lukkeknapper for faner i faneraden i ruten.", diff --git a/web/messages/pl.json b/web/messages/pl.json index c117fd02ca0b..f53f3311f288 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Określa, kiedy cmux prosi o potwierdzenie przed zamknięciem. Buildy DEV zawsze zamykają się natychmiast niezależnie od tego ustawienia. Starsze app.warnBeforeQuit jest nadal akceptowane jako zapasowa wartość logiczna.", + "windowTitleTemplate": "Opcjonalny szablon tytułu NSWindow. Pusta wartość zachowuje domyślny tytuł aktywnego obszaru roboczego. Obsługiwane symbole zastępcze: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Starsza zapasowa wartość logiczna dla potwierdzenia zamknięcia. W nowych konfiguracjach używaj app.confirmQuit.", "warnBeforeClosingTabXButton": "Pokaż potwierdzenie przed zamknięciem karty przyciskiem zamykania karty.", "hideTabCloseButton": "Ukryj przyciski zamykania kart na pasku kart panelu.", diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 575d34611d9f..96f815d04d75 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Controla quando o cmux pede confirmação antes de sair. Builds DEV sempre saem imediatamente, independentemente desta configuração. O app.warnBeforeQuit legado ainda é aceito como fallback booleano.", + "windowTitleTemplate": "Modelo opcional para o título NSWindow. Em branco mantém o título padrão do workspace ativo. Espaços reservados compatíveis: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Fallback booleano legado para confirmação de saída. Use app.confirmQuit em novas configurações.", "warnBeforeClosingTabXButton": "Mostrar uma confirmação antes de fechar uma aba com o botão de fechar aba.", "hideTabCloseButton": "Ocultar os botões de fechar aba na barra de abas do painel.", diff --git a/web/messages/ru.json b/web/messages/ru.json index e3530c3fd1a1..3d59fd5becff 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Управляет тем, когда cmux запрашивает подтверждение перед выходом. DEV-сборки всегда завершаются сразу независимо от этой настройки. Устаревший app.warnBeforeQuit по-прежнему принимается как булевый запасной вариант.", + "windowTitleTemplate": "Необязательный шаблон заголовка NSWindow. Пустое значение сохраняет стандартный заголовок активной рабочей области. Поддерживаемые заполнители: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Устаревший булевый запасной вариант для подтверждения выхода. Для новых конфигураций используйте app.confirmQuit.", "warnBeforeClosingTabXButton": "Показывать подтверждение перед закрытием вкладки кнопкой закрытия вкладки.", "hideTabCloseButton": "Скрывать кнопки закрытия вкладок на панели вкладок области.", diff --git a/web/messages/th.json b/web/messages/th.json index ffc0ae1f0e10..8714bd15d40a 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "ควบคุมว่า cmux จะขอการยืนยันก่อนออกเมื่อใด บิลด์ DEV จะออกทันทีเสมอไม่ว่าค่านี้จะเป็นอย่างไร ค่า app.warnBeforeQuit แบบเดิมยังคงใช้เป็นตัวเลือกสำรองแบบบูลีนได้", + "windowTitleTemplate": "เทมเพลตชื่อหน้าต่าง NSWindow แบบไม่บังคับ เว้นว่างไว้เพื่อคงชื่อเริ่มต้นของเวิร์กสเปซที่ใช้งานอยู่ ตัวยึดตำแหน่งที่รองรับ: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'", "warnBeforeQuit": "ตัวเลือกสำรองแบบบูลีนเดิมสำหรับการยืนยันก่อนออก ใช้ app.confirmQuit สำหรับการกำหนดค่าใหม่", "warnBeforeClosingTabXButton": "แสดงการยืนยันก่อนปิดแท็บด้วยปุ่มปิดแท็บ", "hideTabCloseButton": "ซ่อนปุ่มปิดแท็บในแถบแท็บของพาเนล", diff --git a/web/messages/tr.json b/web/messages/tr.json index b92501414bd6..3b6c37e0094b 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "cmux'un çıkmadan önce ne zaman onay isteyeceğini denetler. DEV derlemeleri bu ayardan bağımsız olarak her zaman hemen çıkar. Eski app.warnBeforeQuit boolean yedeği olarak kabul edilmeye devam eder.", + "windowTitleTemplate": "İsteğe bağlı NSWindow başlık şablonu. Boş bırakılırsa etkin çalışma alanının varsayılan başlığı korunur. Desteklenen yer tutucular: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Çıkış onayı için eski boolean yedeği. Yeni yapılandırmalar için app.confirmQuit kullanın.", "warnBeforeClosingTabXButton": "Sekme kapatma düğmesiyle bir sekme kapatılmadan önce onay göster.", "hideTabCloseButton": "Bölme sekme çubuğundaki sekme kapatma düğmelerini gizle.", diff --git a/web/messages/uk.json b/web/messages/uk.json index 899e37ee60c4..7a622ea08082 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "Керує тим, коли cmux запитує підтвердження перед виходом. DEV-збірки завжди завершуються одразу незалежно від цього налаштування. Застарілий app.warnBeforeQuit і надалі приймається як булевий запасний варіант.", + "windowTitleTemplate": "Необов'язковий шаблон заголовка NSWindow. Порожнє значення зберігає стандартний заголовок активного робочого простору. Підтримувані заповнювачі: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Застарілий булевий запасний варіант для підтвердження виходу. Для нових конфігурацій використовуйте app.confirmQuit.", "warnBeforeClosingTabXButton": "Показувати підтвердження перед закриттям вкладки кнопкою закриття вкладки.", "hideTabCloseButton": "Приховувати кнопки закриття вкладок на панелі вкладок області.", diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index 03341a21a422..24d071b4995e 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "控制 cmux 在退出前何时请求确认。无论此设置如何,DEV 构建都会立即退出。旧的 app.warnBeforeQuit 仍作为布尔回退值接受。", + "windowTitleTemplate": "可选的 NSWindow 标题模板。留空会保留基于活动工作区的默认标题。支持的占位符:'{windowId}'、'{windowToken}'、'{activeWorkspace}'、'{activeDirectory}'、'{defaultTitle}'、'{appName}'。", "warnBeforeQuit": "用于退出确认的旧布尔回退值。新配置请使用 app.confirmQuit。", "warnBeforeClosingTabXButton": "使用标签关闭按钮关闭标签前显示确认。", "hideTabCloseButton": "在窗格标签栏中隐藏标签关闭按钮。", diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index 1476187a4d63..6d2e8123ea79 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -764,6 +764,7 @@ }, "app": { "confirmQuit": "控制 cmux 在退出前何時要求確認。無論此設定為何,DEV 建置都會立即退出。舊的 app.warnBeforeQuit 仍會作為布林後備值接受。", + "windowTitleTemplate": "可選的 NSWindow 標題範本。留空會保留基於作用中工作區的預設標題。支援的預留位置:'{windowId}'、'{windowToken}'、'{activeWorkspace}'、'{activeDirectory}'、'{defaultTitle}'、'{appName}'。", "warnBeforeQuit": "用於退出確認的舊版布林後備值。新設定請使用 app.confirmQuit。", "warnBeforeClosingTabXButton": "使用分頁關閉按鈕關閉分頁前顯示確認。", "hideTabCloseButton": "在窗格分頁列中隱藏分頁關閉按鈕。", From f39774ac521eb9f09c1ed8b500d7c7088761798b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 11:31:07 -0700 Subject: [PATCH 04/11] Clarify window title template fallback wording --- web/data/cmux.schema.json | 2 +- web/messages/ar.json | 2 +- web/messages/bs.json | 2 +- web/messages/da.json | 2 +- web/messages/de.json | 2 +- web/messages/en.json | 2 +- web/messages/es.json | 2 +- web/messages/fr.json | 2 +- web/messages/it.json | 2 +- web/messages/ja.json | 2 +- web/messages/km.json | 2 +- web/messages/ko.json | 2 +- web/messages/no.json | 2 +- web/messages/pl.json | 2 +- web/messages/pt-BR.json | 2 +- web/messages/ru.json | 2 +- web/messages/th.json | 2 +- web/messages/tr.json | 2 +- web/messages/uk.json | 2 +- web/messages/zh-CN.json | 2 +- web/messages/zh-TW.json | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 2a0180247dcb..b8dab172c02b 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -270,7 +270,7 @@ "type": "string", "default": "", "descriptionKey": "schemaDescriptions.app.windowTitleTemplate", - "description": "Optional NSWindow title template. Blank keeps the default active-workspace title. Supported placeholders: {windowId}, {windowToken}, {activeWorkspace}, {activeDirectory}, {defaultTitle}, {appName}." + "description": "Optional NSWindow title template. Blank preserves cmux's existing default title behavior, including the current-directory fallback. Supported placeholders: {windowId}, {windowToken}, {activeWorkspace}, {activeDirectory}, {defaultTitle}, {appName}." }, "menuBarOnly": { "type": "boolean", diff --git a/web/messages/ar.json b/web/messages/ar.json index e0aec4eb8a26..c3b48eebfae8 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "يتحكم في وقت طلب cmux التأكيد قبل الإنهاء. تنتهي إصدارات DEV فوراً دائماً بغض النظر عن هذا الإعداد. يظل app.warnBeforeQuit القديم مقبولاً كخيار احتياطي منطقي.", - "windowTitleTemplate": "قالب اختياري لعنوان NSWindow. اتركه فارغاً للحفاظ على العنوان الافتراضي لمساحة العمل النشطة. العناصر النائبة المدعومة: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "قالب اختياري لعنوان NSWindow. اتركه فارغاً للحفاظ على سلوك العنوان الافتراضي الحالي في cmux، بما في ذلك الرجوع إلى الدليل الحالي. العناصر النائبة المدعومة: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "خيار منطقي قديم احتياطي لتأكيد الإنهاء. استخدم app.confirmQuit للإعدادات الجديدة.", "warnBeforeClosingTabXButton": "اعرض تأكيدًا قبل إغلاق تبويب بزر إغلاق التبويب.", "hideTabCloseButton": "أخفِ أزرار إغلاق التبويبات في شريط تبويبات الجزء.", diff --git a/web/messages/bs.json b/web/messages/bs.json index 0cf0b86621aa..5de6575b903b 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Određuje kada cmux traži potvrdu prije izlaska. DEV verzije se uvijek odmah zatvaraju bez obzira na ovu postavku. Naslijeđeni app.warnBeforeQuit se i dalje prihvata kao boolean rezervna opcija.", - "windowTitleTemplate": "Opcionalni predložak za NSWindow naslov. Prazno zadržava zadani naslov aktivnog radnog prostora. Podržani rezervisani izrazi: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Opcionalni predložak za NSWindow naslov. Prazno zadržava postojeće zadano ponašanje naslova u cmux-u, uključujući rezervu na trenutni direktorij. Podržani rezervisani izrazi: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Naslijeđena boolean rezervna opcija za potvrdu izlaska. Za nove konfiguracije koristite app.confirmQuit.", "warnBeforeClosingTabXButton": "Prikaži potvrdu prije zatvaranja taba dugmetom za zatvaranje taba.", "hideTabCloseButton": "Sakrij dugmad za zatvaranje tabova na traci tabova okna.", diff --git a/web/messages/da.json b/web/messages/da.json index a9f81a5147f0..68d2468b820c 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Styrer hvornår cmux beder om bekræftelse før afslutning. DEV-builds afsluttes altid straks uanset denne indstilling. Den ældre app.warnBeforeQuit accepteres stadig som boolesk fallback.", - "windowTitleTemplate": "Valgfri skabelon for NSWindow-titlen. Tom beholder den standardtitel, der følger det aktive workspace. Understøttede pladsholdere: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Valgfri skabelon for NSWindow-titlen. Tom bevarer cmux' eksisterende standardtiteladfærd, inklusive fallback til den aktuelle mappe. Understøttede pladsholdere: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Ældre boolesk fallback til afslutningsbekræftelse. Brug app.confirmQuit til nye konfigurationer.", "warnBeforeClosingTabXButton": "Vis en bekræftelse, før en fane lukkes med fanens luk-knap.", "hideTabCloseButton": "Skjul fanernes luk-knapper i rudens fanelinje.", diff --git a/web/messages/de.json b/web/messages/de.json index 5e39652153af..5c137ce71a77 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Steuert, wann cmux vor dem Beenden eine Bestätigung anfordert. DEV-Builds beenden sich unabhängig von dieser Einstellung immer sofort. Das ältere app.warnBeforeQuit wird weiterhin als boolescher Fallback akzeptiert.", - "windowTitleTemplate": "Optionale Vorlage für den NSWindow-Titel. Leer behält den Standardtitel des aktiven Arbeitsbereichs bei. Unterstützte Platzhalter: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Optionale Vorlage für den NSWindow-Titel. Leer behält das bestehende Standardtitelverhalten von cmux bei, einschließlich Fallback auf das aktuelle Verzeichnis. Unterstützte Platzhalter: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Älterer boolescher Fallback für die Beenden-Bestätigung. Verwenden Sie app.confirmQuit für neue Konfigurationen.", "warnBeforeClosingTabXButton": "Vor dem Schließen eines Tabs über die Tab-Schließen-Schaltfläche eine Bestätigung anzeigen.", "hideTabCloseButton": "Tab-Schließen-Schaltflächen in der Pane-Tableiste ausblenden.", diff --git a/web/messages/en.json b/web/messages/en.json index 3f19fe593948..24ecd8abc647 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -865,7 +865,7 @@ }, "app": { "confirmQuit": "Control when cmux asks for confirmation before quitting. DEV builds always quit immediately regardless of this setting. Legacy app.warnBeforeQuit is still accepted as a boolean fallback.", - "windowTitleTemplate": "Optional NSWindow title template. Blank keeps the default active-workspace title. Supported placeholders: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Optional NSWindow title template. Blank preserves cmux's existing default title behavior, including the current-directory fallback. Supported placeholders: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Legacy boolean fallback for quit confirmation. Use app.confirmQuit for new configs.", "warnBeforeClosingTabXButton": "Show a confirmation before closing a tab with the tab close button.", "hideTabCloseButton": "Hide tab close buttons in the pane tab bar.", diff --git a/web/messages/es.json b/web/messages/es.json index 0fd61a75aa34..a1f3b912603c 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Controla cuándo cmux pide confirmación antes de salir. Las compilaciones DEV siempre se cierran inmediatamente sin importar esta opción. El app.warnBeforeQuit heredado aún se acepta como alternativa booleana.", - "windowTitleTemplate": "Plantilla opcional para el título de NSWindow. En blanco conserva el título predeterminado del espacio de trabajo activo. Marcadores admitidos: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Plantilla opcional para el título de NSWindow. En blanco conserva el comportamiento de título predeterminado existente de cmux, incluida la alternativa del directorio actual. Marcadores admitidos: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Alternativa booleana heredada para la confirmación de salida. Usa app.confirmQuit para configuraciones nuevas.", "warnBeforeClosingTabXButton": "Mostrar una confirmación antes de cerrar una pestaña con el botón de cerrar pestaña.", "hideTabCloseButton": "Ocultar los botones de cerrar pestaña en la barra de pestañas del panel.", diff --git a/web/messages/fr.json b/web/messages/fr.json index 3b29b3b05ab0..83247748eb53 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Contrôle quand cmux demande confirmation avant de quitter. Les builds DEV quittent toujours immédiatement, quel que soit ce réglage. L'ancien app.warnBeforeQuit reste accepté comme fallback booléen.", - "windowTitleTemplate": "Modèle facultatif pour le titre NSWindow. Vide, il conserve le titre par défaut de l'espace de travail actif. Espaces réservés pris en charge : '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Modèle facultatif pour le titre NSWindow. Vide, il conserve le comportement de titre par défaut existant de cmux, y compris le repli sur le répertoire courant. Espaces réservés pris en charge : '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Fallback booléen hérité pour la confirmation de fermeture. Utilisez app.confirmQuit pour les nouvelles configurations.", "warnBeforeClosingTabXButton": "Afficher une confirmation avant de fermer un onglet avec le bouton de fermeture d’onglet.", "hideTabCloseButton": "Masquer les boutons de fermeture d’onglet dans la barre d’onglets du volet.", diff --git a/web/messages/it.json b/web/messages/it.json index a6216f7c8faf..9cbc859655d7 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Controlla quando cmux chiede conferma prima di uscire. Le build DEV escono sempre immediatamente indipendentemente da questa impostazione. Il vecchio app.warnBeforeQuit è ancora accettato come fallback booleano.", - "windowTitleTemplate": "Template opzionale per il titolo NSWindow. Se vuoto, mantiene il titolo predefinito dell'area di lavoro attiva. Segnaposto supportati: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Template opzionale per il titolo NSWindow. Se vuoto, conserva il comportamento predefinito esistente del titolo di cmux, incluso il fallback alla directory corrente. Segnaposto supportati: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Fallback booleano legacy per la conferma di uscita. Usa app.confirmQuit per le nuove configurazioni.", "warnBeforeClosingTabXButton": "Mostra una conferma prima di chiudere una scheda con il pulsante di chiusura scheda.", "hideTabCloseButton": "Nasconde i pulsanti di chiusura scheda nella barra delle schede del riquadro.", diff --git a/web/messages/ja.json b/web/messages/ja.json index 098a1dd0c685..b1ea6382608a 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -824,7 +824,7 @@ }, "app": { "confirmQuit": "cmux の終了前に確認を求めるタイミングを制御します。DEV ビルドではこの設定に関係なく常にすぐ終了します。従来の app.warnBeforeQuit も boolean フォールバックとして引き続き受け付けます。", - "windowTitleTemplate": "任意の NSWindow タイトルテンプレートです。空欄のままなら、アクティブなワークスペース名を使う既定のタイトルを保ちます。対応するプレースホルダー: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'。", + "windowTitleTemplate": "任意の NSWindow タイトルテンプレートです。空欄のままなら、現在のディレクトリへのフォールバックを含む cmux 既存の既定タイトル動作を保ちます。対応するプレースホルダー: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'。", "warnBeforeQuit": "終了確認用の従来の boolean フォールバックです。新しい設定では app.confirmQuit を使ってください。", "warnBeforeClosingTabXButton": "タブの閉じるボタンでタブを閉じる前に確認を表示します。", "hideTabCloseButton": "ペインのタブバーでタブの閉じるボタンを非表示にします。", diff --git a/web/messages/km.json b/web/messages/km.json index 0f8ed8ba6f9e..c7e18ebbd94f 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "កំណត់ពេលដែល cmux ស្នើសុំការបញ្ជាក់មុនពេលចាកចេញ។ កំណែ DEV តែងតែចាកចេញភ្លាមៗ ដោយមិនគិតពីការកំណត់នេះទេ។ app.warnBeforeQuit ចាស់នៅតែត្រូវបានទទួលយកជាជម្រើសបម្រុង boolean។", - "windowTitleTemplate": "គំរូចំណងជើង NSWindow ជាជម្រើស។ ទុកទទេ ដើម្បីរក្សាចំណងជើងលំនាំដើមរបស់ workspace សកម្ម។ Placeholder ដែលគាំទ្រ៖ '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'។", + "windowTitleTemplate": "គំរូចំណងជើង NSWindow ជាជម្រើស។ ទុកទទេ ដើម្បីរក្សាឥរិយាបថចំណងជើងលំនាំដើមដែលមានស្រាប់របស់ cmux រួមទាំង fallback ទៅថតបច្ចុប្បន្ន។ Placeholder ដែលគាំទ្រ៖ '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'។", "warnBeforeQuit": "ជម្រើសបម្រុង boolean ចាស់សម្រាប់ការបញ្ជាក់មុនចាកចេញ។ ប្រើ app.confirmQuit សម្រាប់ការកំណត់ថ្មី។", "warnBeforeClosingTabXButton": "បង្ហាញការបញ្ជាក់ មុនពេលបិទផ្ទាំងដោយប៊ូតុងបិទផ្ទាំង។", "hideTabCloseButton": "លាក់ប៊ូតុងបិទផ្ទាំងនៅក្នុងរបារផ្ទាំងរបស់ផ្ទាំងបង្អួច។", diff --git a/web/messages/ko.json b/web/messages/ko.json index d1df18c543ac..04d8622b59e5 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "cmux가 종료 전에 확인을 요청하는 시점을 제어합니다. DEV 빌드는 이 설정과 관계없이 항상 즉시 종료됩니다. 기존 app.warnBeforeQuit도 boolean 대체값으로 계속 허용됩니다.", - "windowTitleTemplate": "선택적 NSWindow 제목 템플릿입니다. 비워 두면 활성 워크스페이스 제목을 사용하는 기본 제목을 유지합니다. 지원되는 자리표시자: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "선택적 NSWindow 제목 템플릿입니다. 비워 두면 현재 디렉터리 대체 동작을 포함해 cmux의 기존 기본 제목 동작을 유지합니다. 지원되는 자리표시자: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "종료 확인을 위한 기존 boolean 대체값입니다. 새 구성에는 app.confirmQuit을 사용하세요.", "warnBeforeClosingTabXButton": "탭 닫기 버튼으로 탭을 닫기 전에 확인을 표시합니다.", "hideTabCloseButton": "창 영역 탭 막대에서 탭 닫기 버튼을 숨깁니다.", diff --git a/web/messages/no.json b/web/messages/no.json index fc72a2e2030c..e520674cfb4d 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Styrer når cmux ber om bekreftelse før avslutning. DEV-bygg avslutter alltid umiddelbart uansett denne innstillingen. Den eldre app.warnBeforeQuit godtas fortsatt som boolsk fallback.", - "windowTitleTemplate": "Valgfri mal for NSWindow-tittelen. Tom verdi beholder standardtittelen for aktivt arbeidsområde. Støttede plassholdere: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Valgfri mal for NSWindow-tittelen. Tom verdi bevarer cmux sin eksisterende standardtittelatferd, inkludert fallback til gjeldende katalog. Støttede plassholdere: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Eldre boolsk fallback for avslutningsbekreftelse. Bruk app.confirmQuit for nye konfigurasjoner.", "warnBeforeClosingTabXButton": "Vis en bekreftelse før en fane lukkes med fanens lukkeknapp.", "hideTabCloseButton": "Skjul lukkeknapper for faner i faneraden i ruten.", diff --git a/web/messages/pl.json b/web/messages/pl.json index f53f3311f288..3e3c7396f4b4 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Określa, kiedy cmux prosi o potwierdzenie przed zamknięciem. Buildy DEV zawsze zamykają się natychmiast niezależnie od tego ustawienia. Starsze app.warnBeforeQuit jest nadal akceptowane jako zapasowa wartość logiczna.", - "windowTitleTemplate": "Opcjonalny szablon tytułu NSWindow. Pusta wartość zachowuje domyślny tytuł aktywnego obszaru roboczego. Obsługiwane symbole zastępcze: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Opcjonalny szablon tytułu NSWindow. Pusta wartość zachowuje istniejące domyślne zachowanie tytułu cmux, w tym fallback do bieżącego katalogu. Obsługiwane symbole zastępcze: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Starsza zapasowa wartość logiczna dla potwierdzenia zamknięcia. W nowych konfiguracjach używaj app.confirmQuit.", "warnBeforeClosingTabXButton": "Pokaż potwierdzenie przed zamknięciem karty przyciskiem zamykania karty.", "hideTabCloseButton": "Ukryj przyciski zamykania kart na pasku kart panelu.", diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 96f815d04d75..c50eb1b4369c 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Controla quando o cmux pede confirmação antes de sair. Builds DEV sempre saem imediatamente, independentemente desta configuração. O app.warnBeforeQuit legado ainda é aceito como fallback booleano.", - "windowTitleTemplate": "Modelo opcional para o título NSWindow. Em branco mantém o título padrão do workspace ativo. Espaços reservados compatíveis: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Modelo opcional para o título NSWindow. Em branco preserva o comportamento de título padrão existente do cmux, incluindo o fallback para o diretório atual. Espaços reservados compatíveis: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Fallback booleano legado para confirmação de saída. Use app.confirmQuit em novas configurações.", "warnBeforeClosingTabXButton": "Mostrar uma confirmação antes de fechar uma aba com o botão de fechar aba.", "hideTabCloseButton": "Ocultar os botões de fechar aba na barra de abas do painel.", diff --git a/web/messages/ru.json b/web/messages/ru.json index 3d59fd5becff..5d5c82a52940 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Управляет тем, когда cmux запрашивает подтверждение перед выходом. DEV-сборки всегда завершаются сразу независимо от этой настройки. Устаревший app.warnBeforeQuit по-прежнему принимается как булевый запасной вариант.", - "windowTitleTemplate": "Необязательный шаблон заголовка NSWindow. Пустое значение сохраняет стандартный заголовок активной рабочей области. Поддерживаемые заполнители: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Необязательный шаблон заголовка NSWindow. Пустое значение сохраняет существующее стандартное поведение заголовка cmux, включая откат к текущему каталогу. Поддерживаемые заполнители: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Устаревший булевый запасной вариант для подтверждения выхода. Для новых конфигураций используйте app.confirmQuit.", "warnBeforeClosingTabXButton": "Показывать подтверждение перед закрытием вкладки кнопкой закрытия вкладки.", "hideTabCloseButton": "Скрывать кнопки закрытия вкладок на панели вкладок области.", diff --git a/web/messages/th.json b/web/messages/th.json index 8714bd15d40a..54c84ac48ded 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "ควบคุมว่า cmux จะขอการยืนยันก่อนออกเมื่อใด บิลด์ DEV จะออกทันทีเสมอไม่ว่าค่านี้จะเป็นอย่างไร ค่า app.warnBeforeQuit แบบเดิมยังคงใช้เป็นตัวเลือกสำรองแบบบูลีนได้", - "windowTitleTemplate": "เทมเพลตชื่อหน้าต่าง NSWindow แบบไม่บังคับ เว้นว่างไว้เพื่อคงชื่อเริ่มต้นของเวิร์กสเปซที่ใช้งานอยู่ ตัวยึดตำแหน่งที่รองรับ: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'", + "windowTitleTemplate": "เทมเพลตชื่อหน้าต่าง NSWindow แบบไม่บังคับ เว้นว่างไว้เพื่อคงพฤติกรรมชื่อเริ่มต้นเดิมของ cmux รวมถึงการถอยกลับไปใช้ไดเรกทอรีปัจจุบัน ตัวยึดตำแหน่งที่รองรับ: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'", "warnBeforeQuit": "ตัวเลือกสำรองแบบบูลีนเดิมสำหรับการยืนยันก่อนออก ใช้ app.confirmQuit สำหรับการกำหนดค่าใหม่", "warnBeforeClosingTabXButton": "แสดงการยืนยันก่อนปิดแท็บด้วยปุ่มปิดแท็บ", "hideTabCloseButton": "ซ่อนปุ่มปิดแท็บในแถบแท็บของพาเนล", diff --git a/web/messages/tr.json b/web/messages/tr.json index 3b6c37e0094b..123eec5b4a8d 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "cmux'un çıkmadan önce ne zaman onay isteyeceğini denetler. DEV derlemeleri bu ayardan bağımsız olarak her zaman hemen çıkar. Eski app.warnBeforeQuit boolean yedeği olarak kabul edilmeye devam eder.", - "windowTitleTemplate": "İsteğe bağlı NSWindow başlık şablonu. Boş bırakılırsa etkin çalışma alanının varsayılan başlığı korunur. Desteklenen yer tutucular: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "İsteğe bağlı NSWindow başlık şablonu. Boş bırakılırsa geçerli dizin yedeği dahil cmux'un mevcut varsayılan başlık davranışı korunur. Desteklenen yer tutucular: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Çıkış onayı için eski boolean yedeği. Yeni yapılandırmalar için app.confirmQuit kullanın.", "warnBeforeClosingTabXButton": "Sekme kapatma düğmesiyle bir sekme kapatılmadan önce onay göster.", "hideTabCloseButton": "Bölme sekme çubuğundaki sekme kapatma düğmelerini gizle.", diff --git a/web/messages/uk.json b/web/messages/uk.json index 7a622ea08082..8ac9cf761b3f 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Керує тим, коли cmux запитує підтвердження перед виходом. DEV-збірки завжди завершуються одразу незалежно від цього налаштування. Застарілий app.warnBeforeQuit і надалі приймається як булевий запасний варіант.", - "windowTitleTemplate": "Необов'язковий шаблон заголовка NSWindow. Порожнє значення зберігає стандартний заголовок активного робочого простору. Підтримувані заповнювачі: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Необов'язковий шаблон заголовка NSWindow. Порожнє значення зберігає наявну стандартну поведінку заголовка cmux, включно з відступом до поточного каталогу. Підтримувані заповнювачі: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Застарілий булевий запасний варіант для підтвердження виходу. Для нових конфігурацій використовуйте app.confirmQuit.", "warnBeforeClosingTabXButton": "Показувати підтвердження перед закриттям вкладки кнопкою закриття вкладки.", "hideTabCloseButton": "Приховувати кнопки закриття вкладок на панелі вкладок області.", diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index 24d071b4995e..44be0ed4e8f4 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "控制 cmux 在退出前何时请求确认。无论此设置如何,DEV 构建都会立即退出。旧的 app.warnBeforeQuit 仍作为布尔回退值接受。", - "windowTitleTemplate": "可选的 NSWindow 标题模板。留空会保留基于活动工作区的默认标题。支持的占位符:'{windowId}'、'{windowToken}'、'{activeWorkspace}'、'{activeDirectory}'、'{defaultTitle}'、'{appName}'。", + "windowTitleTemplate": "可选的 NSWindow 标题模板。留空会保留 cmux 现有的默认标题行为,包括回退到当前目录。支持的占位符:'{windowId}'、'{windowToken}'、'{activeWorkspace}'、'{activeDirectory}'、'{defaultTitle}'、'{appName}'。", "warnBeforeQuit": "用于退出确认的旧布尔回退值。新配置请使用 app.confirmQuit。", "warnBeforeClosingTabXButton": "使用标签关闭按钮关闭标签前显示确认。", "hideTabCloseButton": "在窗格标签栏中隐藏标签关闭按钮。", diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index 6d2e8123ea79..737517d0e368 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "控制 cmux 在退出前何時要求確認。無論此設定為何,DEV 建置都會立即退出。舊的 app.warnBeforeQuit 仍會作為布林後備值接受。", - "windowTitleTemplate": "可選的 NSWindow 標題範本。留空會保留基於作用中工作區的預設標題。支援的預留位置:'{windowId}'、'{windowToken}'、'{activeWorkspace}'、'{activeDirectory}'、'{defaultTitle}'、'{appName}'。", + "windowTitleTemplate": "可選的 NSWindow 標題範本。留空會保留 cmux 既有的預設標題行為,包括回退到目前目錄。支援的預留位置:'{windowId}'、'{windowToken}'、'{activeWorkspace}'、'{activeDirectory}'、'{defaultTitle}'、'{appName}'。", "warnBeforeQuit": "用於退出確認的舊版布林後備值。新設定請使用 app.confirmQuit。", "warnBeforeClosingTabXButton": "使用分頁關閉按鈕關閉分頁前顯示確認。", "hideTabCloseButton": "在窗格分頁列中隱藏分頁關閉按鈕。", From a20552e76728e1d8d6c088311acfb1fb8a144db4 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 11:47:56 -0700 Subject: [PATCH 05/11] Refresh templated titles on directory changes --- Sources/TabManager+WindowTitle.swift | 5 +++ Sources/TabManager.swift | 13 ++++++++ cmuxTests/WindowTitleTemplateTests.swift | 41 ++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/Sources/TabManager+WindowTitle.swift b/Sources/TabManager+WindowTitle.swift index e198bb5ede81..faa5cc036b7f 100644 --- a/Sources/TabManager+WindowTitle.swift +++ b/Sources/TabManager+WindowTitle.swift @@ -6,6 +6,11 @@ extension TabManager { updateWindowTitleForSelectedTab() } + func workspaceCurrentDirectoryDidChange(workspaceId: UUID) { + guard workspaceId == selectedTabId else { return } + refreshWindowTitle() + } + func updateWindowTitleForSelectedTab() { guard let selectedTabId, let tab = tabs.first(where: { $0.id == selectedTabId }) else { diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 1a22fa557bb3..3b64308ccdda 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -1241,6 +1241,19 @@ class TabManager: ObservableObject { focusedSurfaceTitleDidChange(tabId: tabId) } }) + observers.append(NotificationCenter.default.addObserver( + forName: .workspaceCurrentDirectoryDidChange, + object: nil, + queue: nil + ) { [weak self] notification in + MainActor.assumeIsolated { [weak self] in + guard let self else { return } + let workspaceId = notification.userInfo?["workspaceId"] as? UUID + ?? (notification.object as? Workspace)?.id + guard let workspaceId else { return } + workspaceCurrentDirectoryDidChange(workspaceId: workspaceId) + } + }) startAgentPIDSweepTimer() observers.append(NotificationCenter.default.addObserver( diff --git a/cmuxTests/WindowTitleTemplateTests.swift b/cmuxTests/WindowTitleTemplateTests.swift index 53465b8a6c17..1abc43555b3a 100644 --- a/cmuxTests/WindowTitleTemplateTests.swift +++ b/cmuxTests/WindowTitleTemplateTests.swift @@ -1,4 +1,5 @@ import Foundation +import AppKit import Testing #if canImport(cmux_DEV) @@ -89,6 +90,46 @@ struct WindowTitleTemplateTests { #expect(defaults.string(forKey: WindowTitleTemplate.userDefaultsKey) == "[cmux:{windowToken}] {activeWorkspace}") } + @MainActor + @Test func selectedWorkspaceDirectoryChangeRefreshesActiveDirectoryTitle() throws { + let defaults = UserDefaults.standard + let previousValues: [String: Any?] = [ + WindowTitleTemplate.userDefaultsKey: defaults.object(forKey: WindowTitleTemplate.userDefaultsKey), + ] + defer { + restore(previousValues, defaults: defaults) + } + defaults.set("[cmux:{windowToken}] {activeDirectory}", forKey: WindowTitleTemplate.userDefaultsKey) + + let windowId = try #require(UUID(uuidString: "01234567-89AB-CDEF-0123-456789ABCDEF")) + let manager = TabManager( + initialWorkspaceTitle: "Build", + initialWorkingDirectory: "/tmp/old", + autoWelcomeIfNeeded: false + ) + manager.windowId = windowId + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 640, height: 420), + styleMask: [.titled], + backing: .buffered, + defer: false + ) + manager.window = window + defer { + manager.window = nil + window.close() + } + + manager.refreshWindowTitle() + #expect(window.title == "[cmux:01234567] /tmp/old") + + let workspace = try #require(manager.tabs.first) + let panelId = try #require(workspace.focusedPanelId) + #expect(workspace.updatePanelDirectory(panelId: panelId, directory: "/tmp/new")) + #expect(window.title == "[cmux:01234567] /tmp/new") + } + private func isolatedDefaults() throws -> UserDefaults { let suiteName = "cmux.WindowTitleTemplateTests.\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: suiteName)) From 43df3dc8c0f7789c7756c1b21dfda8dcf6e4ce42 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 11:58:40 -0700 Subject: [PATCH 06/11] Deliver directory title refresh on main queue --- Sources/TabManager.swift | 2 +- cmuxTests/WindowTitleTemplateTests.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 3b64308ccdda..f8fe8e51eba1 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -1244,7 +1244,7 @@ class TabManager: ObservableObject { observers.append(NotificationCenter.default.addObserver( forName: .workspaceCurrentDirectoryDidChange, object: nil, - queue: nil + queue: .main ) { [weak self] notification in MainActor.assumeIsolated { [weak self] in guard let self else { return } diff --git a/cmuxTests/WindowTitleTemplateTests.swift b/cmuxTests/WindowTitleTemplateTests.swift index 1abc43555b3a..d6a1f3e602ca 100644 --- a/cmuxTests/WindowTitleTemplateTests.swift +++ b/cmuxTests/WindowTitleTemplateTests.swift @@ -127,6 +127,7 @@ struct WindowTitleTemplateTests { let workspace = try #require(manager.tabs.first) let panelId = try #require(workspace.focusedPanelId) #expect(workspace.updatePanelDirectory(panelId: panelId, directory: "/tmp/new")) + manager.workspaceCurrentDirectoryDidChange(workspaceId: workspace.id) #expect(window.title == "[cmux:01234567] /tmp/new") } From 64d274aa5407168d04f491acc37e0f66995141eb Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 12:00:08 -0700 Subject: [PATCH 07/11] Address window title review comments --- Sources/AppDelegate.swift | 2 +- web/messages/uk.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 9840b0d57257..f2b1e22cf466 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1050,7 +1050,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent /// Tracks the cascade point for new windows, matching Ghostty's upstream algorithm. /// Reset to `.zero` so the first window seeds the point from its own position. private var lastCascadePoint = NSPoint.zero - var startupSessionSnapshot: AppSessionSnapshot? + private(set) var startupSessionSnapshot: AppSessionSnapshot? private var didPrepareStartupSessionSnapshot = false var didAttemptStartupSessionRestore = false private var isApplyingSessionRestore = false diff --git a/web/messages/uk.json b/web/messages/uk.json index 8ac9cf761b3f..29fa3ab5bbf8 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -764,7 +764,7 @@ }, "app": { "confirmQuit": "Керує тим, коли cmux запитує підтвердження перед виходом. DEV-збірки завжди завершуються одразу незалежно від цього налаштування. Застарілий app.warnBeforeQuit і надалі приймається як булевий запасний варіант.", - "windowTitleTemplate": "Необов'язковий шаблон заголовка NSWindow. Порожнє значення зберігає наявну стандартну поведінку заголовка cmux, включно з відступом до поточного каталогу. Підтримувані заповнювачі: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", + "windowTitleTemplate": "Необов'язковий шаблон заголовка NSWindow. Порожнє значення зберігає наявну стандартну поведінку заголовка cmux, включно з використанням поточного каталогу як запасного варіанта. Підтримувані заповнювачі: '{windowId}', '{windowToken}', '{activeWorkspace}', '{activeDirectory}', '{defaultTitle}', '{appName}'.", "warnBeforeQuit": "Застарілий булевий запасний варіант для підтвердження виходу. Для нових конфігурацій використовуйте app.confirmQuit.", "warnBeforeClosingTabXButton": "Показувати підтвердження перед закриттям вкладки кнопкою закриття вкладки.", "hideTabCloseButton": "Приховувати кнопки закриття вкладок на панелі вкладок області.", From 537db18a60915c2da9492ac0002d8789c63ca540 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 12:08:45 -0700 Subject: [PATCH 08/11] Remove TabManager notification observers on teardown --- Sources/TabManager.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index f8fe8e51eba1..6ba1b0218332 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -1276,6 +1276,10 @@ class TabManager: ObservableObject { } deinit { + for observer in observers { + NotificationCenter.default.removeObserver(observer) + } + observers.removeAll() workspaceCycleCooldownTask?.cancel() agentPIDSweepTimer?.cancel() // The sidebar git/PR services cancel their own poll, probe, snapshot, From 1fd3d42d3f7fedc141fcc7457e9abd8b3de1297c Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 12:18:16 -0700 Subject: [PATCH 09/11] Split window title template context --- Sources/App/WindowTitleTemplate.swift | 12 ++---------- Sources/App/WindowTitleTemplateContext.swift | 9 +++++++++ Sources/TabManager+WindowTitle.swift | 2 +- cmux.xcodeproj/project.pbxproj | 4 ++++ cmuxTests/WindowTitleTemplateTests.swift | 4 ++-- 5 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 Sources/App/WindowTitleTemplateContext.swift diff --git a/Sources/App/WindowTitleTemplate.swift b/Sources/App/WindowTitleTemplate.swift index 950686838e0c..5982ee89d7eb 100644 --- a/Sources/App/WindowTitleTemplate.swift +++ b/Sources/App/WindowTitleTemplate.swift @@ -1,14 +1,6 @@ import Foundation struct WindowTitleTemplate: Equatable, Sendable { - struct Context: Equatable, Sendable { - var defaultTitle: String - var activeWorkspace: String - var activeDirectory: String - var windowId: UUID - var appName: String - } - static let userDefaultsKey = "windowTitleTemplate" static let defaultRawValue = "" @@ -22,7 +14,7 @@ struct WindowTitleTemplate: Equatable, Sendable { return WindowTitleTemplate(rawValue: rawValue) } - func resolved(context: Context) -> String { + func resolved(context: WindowTitleTemplateContext) -> String { let replacements = Dictionary(uniqueKeysWithValues: replacements(context: context)) var resolved = "" var index = rawValue.startIndex @@ -46,7 +38,7 @@ struct WindowTitleTemplate: Equatable, Sendable { return resolved } - private func replacements(context: Context) -> [(String, String)] { + private func replacements(context: WindowTitleTemplateContext) -> [(String, String)] { [ ("windowId", context.windowId.uuidString.lowercased()), ("windowToken", Self.windowToken(for: context.windowId)), diff --git a/Sources/App/WindowTitleTemplateContext.swift b/Sources/App/WindowTitleTemplateContext.swift new file mode 100644 index 000000000000..cc62e09e88fa --- /dev/null +++ b/Sources/App/WindowTitleTemplateContext.swift @@ -0,0 +1,9 @@ +import Foundation + +struct WindowTitleTemplateContext: Equatable, Sendable { + var defaultTitle: String + var activeWorkspace: String + var activeDirectory: String + var windowId: UUID + var appName: String +} diff --git a/Sources/TabManager+WindowTitle.swift b/Sources/TabManager+WindowTitle.swift index faa5cc036b7f..90ae5665de07 100644 --- a/Sources/TabManager+WindowTitle.swift +++ b/Sources/TabManager+WindowTitle.swift @@ -51,7 +51,7 @@ extension TabManager { .trimmingCharacters(in: .whitespacesAndNewlines) } ?? "" let activeDirectory = tab?.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let resolvedTitle = template.resolved(context: WindowTitleTemplate.Context( + let resolvedTitle = template.resolved(context: WindowTitleTemplateContext( defaultTitle: defaultTitle, activeWorkspace: workspaceTitle.isEmpty ? defaultTitle : workspaceTitle, activeDirectory: activeDirectory, diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 0419ceda574c..75b16743dacb 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -751,6 +751,7 @@ 313585583035A1E685010A97 /* WindowKeyDownReplayGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */; }; 8770D0F2D2BB45359D6BE5E3 /* WindowKeyDownReplayGuardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */; }; 604500100000000000000001 /* WindowTitleTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000002 /* WindowTitleTemplate.swift */; }; + 604500100000000000000005 /* WindowTitleTemplateContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000006 /* WindowTitleTemplateContext.swift */; }; 604500100000000000000003 /* WindowTitleTemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000004 /* WindowTitleTemplateTests.swift */; }; A5001209 /* WindowToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001219 /* WindowToolbarController.swift */; }; 9FC97DA1B422776BB1EEC821 /* Workspace+CustomSidebarPullRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C2726A34F27E7A43B77368 /* Workspace+CustomSidebarPullRequests.swift */; }; @@ -1560,6 +1561,7 @@ 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowKeyDownReplayGuard.swift; sourceTree = ""; }; B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowKeyDownReplayGuardTests.swift; sourceTree = ""; }; 604500100000000000000002 /* WindowTitleTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplate.swift; sourceTree = ""; }; + 604500100000000000000006 /* WindowTitleTemplateContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplateContext.swift; sourceTree = ""; }; 604500100000000000000004 /* WindowTitleTemplateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTitleTemplateTests.swift; sourceTree = ""; }; A5001219 /* WindowToolbarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowToolbarController.swift; sourceTree = ""; }; 42C2726A34F27E7A43B77368 /* Workspace+CustomSidebarPullRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CustomSidebarPullRequests.swift"; sourceTree = ""; }; @@ -1920,6 +1922,7 @@ C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */, B42A82C6AA614E74873D9A5F /* ShortcutRoutingSupport.swift */, 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */, + 604500100000000000000006 /* WindowTitleTemplateContext.swift */, 604500100000000000000002 /* WindowTitleTemplate.swift */, AB7F2E9143904957AAA70726 /* ShortcutBareStartRouting.swift */, E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */, @@ -3353,6 +3356,7 @@ D0B10024A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift in Sources */, 313585583035A1E685010A97 /* WindowKeyDownReplayGuard.swift in Sources */, 604500100000000000000001 /* WindowTitleTemplate.swift in Sources */, + 604500100000000000000005 /* WindowTitleTemplateContext.swift in Sources */, A5001209 /* WindowToolbarController.swift in Sources */, 9FC97DA1B422776BB1EEC821 /* Workspace+CustomSidebarPullRequests.swift in Sources */, D7AB00000000000000000015 /* Workspace+DetachedSurfaceTransfer.swift in Sources */, diff --git a/cmuxTests/WindowTitleTemplateTests.swift b/cmuxTests/WindowTitleTemplateTests.swift index d6a1f3e602ca..67fde8b1b240 100644 --- a/cmuxTests/WindowTitleTemplateTests.swift +++ b/cmuxTests/WindowTitleTemplateTests.swift @@ -19,7 +19,7 @@ struct WindowTitleTemplateTests { rawValue: "[cmux:{windowToken}] {activeWorkspace} {activeDirectory} {windowId} {defaultTitle} {appName} {unknown}" ) - let resolved = template.resolved(context: WindowTitleTemplate.Context( + let resolved = template.resolved(context: WindowTitleTemplateContext( defaultTitle: "Fallback", activeWorkspace: "Build", activeDirectory: "/tmp/project", @@ -41,7 +41,7 @@ struct WindowTitleTemplateTests { let windowId = try #require(UUID(uuidString: "01234567-89AB-CDEF-0123-456789ABCDEF")) let template = WindowTitleTemplate(rawValue: "{activeWorkspace} {appName}") - let resolved = template.resolved(context: WindowTitleTemplate.Context( + let resolved = template.resolved(context: WindowTitleTemplateContext( defaultTitle: "Fallback", activeWorkspace: "{windowId}", activeDirectory: "{windowToken}", From 8a5c8f5bb36e484da85a7b4de2874ec437413680 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 12:21:35 -0700 Subject: [PATCH 10/11] Fix window title context project IDs --- cmux.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 75b16743dacb..27be107fcfd0 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -751,7 +751,7 @@ 313585583035A1E685010A97 /* WindowKeyDownReplayGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */; }; 8770D0F2D2BB45359D6BE5E3 /* WindowKeyDownReplayGuardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */; }; 604500100000000000000001 /* WindowTitleTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000002 /* WindowTitleTemplate.swift */; }; - 604500100000000000000005 /* WindowTitleTemplateContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000006 /* WindowTitleTemplateContext.swift */; }; + 604500200000000000000001 /* WindowTitleTemplateContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500200000000000000002 /* WindowTitleTemplateContext.swift */; }; 604500100000000000000003 /* WindowTitleTemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604500100000000000000004 /* WindowTitleTemplateTests.swift */; }; A5001209 /* WindowToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001219 /* WindowToolbarController.swift */; }; 9FC97DA1B422776BB1EEC821 /* Workspace+CustomSidebarPullRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C2726A34F27E7A43B77368 /* Workspace+CustomSidebarPullRequests.swift */; }; @@ -1561,7 +1561,7 @@ 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowKeyDownReplayGuard.swift; sourceTree = ""; }; B79482F1ECA54E98BE5C8953 /* WindowKeyDownReplayGuardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowKeyDownReplayGuardTests.swift; sourceTree = ""; }; 604500100000000000000002 /* WindowTitleTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplate.swift; sourceTree = ""; }; - 604500100000000000000006 /* WindowTitleTemplateContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplateContext.swift; sourceTree = ""; }; + 604500200000000000000002 /* WindowTitleTemplateContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/WindowTitleTemplateContext.swift; sourceTree = ""; }; 604500100000000000000004 /* WindowTitleTemplateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTitleTemplateTests.swift; sourceTree = ""; }; A5001219 /* WindowToolbarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowToolbarController.swift; sourceTree = ""; }; 42C2726A34F27E7A43B77368 /* Workspace+CustomSidebarPullRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CustomSidebarPullRequests.swift"; sourceTree = ""; }; @@ -1922,7 +1922,7 @@ C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */, B42A82C6AA614E74873D9A5F /* ShortcutRoutingSupport.swift */, 81B8CFAF8FCA4231C702D16A /* WindowKeyDownReplayGuard.swift */, - 604500100000000000000006 /* WindowTitleTemplateContext.swift */, + 604500200000000000000002 /* WindowTitleTemplateContext.swift */, 604500100000000000000002 /* WindowTitleTemplate.swift */, AB7F2E9143904957AAA70726 /* ShortcutBareStartRouting.swift */, E5C0F1A1E5C0F1A1E5C0F1A1 /* TerminalFindEscapeRouting.swift */, @@ -3356,7 +3356,7 @@ D0B10024A1B2C3D4E5F60001 /* WindowInputRoutingContext.swift in Sources */, 313585583035A1E685010A97 /* WindowKeyDownReplayGuard.swift in Sources */, 604500100000000000000001 /* WindowTitleTemplate.swift in Sources */, - 604500100000000000000005 /* WindowTitleTemplateContext.swift in Sources */, + 604500200000000000000001 /* WindowTitleTemplateContext.swift in Sources */, A5001209 /* WindowToolbarController.swift in Sources */, 9FC97DA1B422776BB1EEC821 /* Workspace+CustomSidebarPullRequests.swift in Sources */, D7AB00000000000000000015 /* Workspace+DetachedSurfaceTransfer.swift in Sources */, From d54240d9593b926fb4274aa0838690657d58e555 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 13 Jun 2026 12:29:35 -0700 Subject: [PATCH 11/11] Isolate window title settings test fallbacks --- cmuxTests/WindowTitleTemplateTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/cmuxTests/WindowTitleTemplateTests.swift b/cmuxTests/WindowTitleTemplateTests.swift index 67fde8b1b240..70e64ecebe8b 100644 --- a/cmuxTests/WindowTitleTemplateTests.swift +++ b/cmuxTests/WindowTitleTemplateTests.swift @@ -84,6 +84,7 @@ struct WindowTitleTemplateTests { _ = KeyboardShortcutSettingsFileStore( primaryPath: settingsFileURL.path, fallbackPath: nil, + additionalFallbackPaths: [], startWatching: false )