From 54f1b21791c0dc6a79c556c843b338681aec237b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 03:39:23 +0000 Subject: [PATCH 01/12] test(next): route chat size keys to font action --- .../Tests/CmuxNextActionsTests/HomeZoomKeyTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextActionsTests/HomeZoomKeyTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextActionsTests/HomeZoomKeyTests.swift index c92dbcf1ef04..8836e34b4836 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextActionsTests/HomeZoomKeyTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextActionsTests/HomeZoomKeyTests.swift @@ -3,7 +3,7 @@ import CmuxNextActions import Testing /// Cmd-= (and Cmd-+), Cmd-- and Cmd-0 on Home zoom the Chief chat (`home.zoomIn`, `home.zoomOut`, -/// `home.zoomReset`); elsewhere the keys keep their owner (an agent chat's zoom, a terminal's font +/// `home.zoomReset`); elsewhere the keys keep their owner (an agent chat's text size, a terminal's font /// size). The binding table resolves each key in the contexts the key router builds. @MainActor @Suite struct HomeZoomKeyTests { static let keys: [(Shortcut, String)] = [ @@ -38,9 +38,9 @@ import Testing @Test func zoomKeysStayWithTheAgentChatAndTheTerminal() { var agent = KeyContext(bits: [.agentPaneFocused]) agent[KeyContext.surfaceKind] = .string("agent") - #expect(Self.winner(Shortcut("=", modifiers: [.command]), in: agent) == "agentPaneZoomIn") - #expect(Self.winner(Shortcut("-", modifiers: [.command]), in: agent) == "agentPaneZoomOut") - #expect(Self.winner(Shortcut("0", modifiers: [.command]), in: agent) == "agentPaneZoomReset") + #expect(Self.winner(Shortcut("=", modifiers: [.command]), in: agent) == "increaseWorkspaceTerminalFontSize") + #expect(Self.winner(Shortcut("-", modifiers: [.command]), in: agent) == "decreaseWorkspaceTerminalFontSize") + #expect(Self.winner(Shortcut("0", modifiers: [.command]), in: agent) == "resetWorkspaceTerminalFontSize") var terminal = KeyContext(bits: [.terminalFocused]) terminal[KeyContext.surfaceKind] = .string("terminal") for (key, _) in Self.keys { From e4e182fb9c0111cc7d8476f97a567ce484620272 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 03:44:12 +0000 Subject: [PATCH 02/12] fix(next): route chat font shortcuts to text size --- .../Catalog/AgentActionCatalog.swift | 6 +-- .../Handlers/PaneHandlers+Verbs.swift | 28 ++++++++++ .../AgentPaneDisplaySetting.swift | 7 +++ .../AgentChatFontSizeShortcutTests.swift | 54 +++++++++++++++++++ .../KeybindingsFocusConflictTests.swift | 2 +- docs/mdm/managed-preferences.md | 3 +- plans/cmux-next/action-surfaces.json | 21 ++------ plans/cmux-next/palette-audit.md | 6 +-- schemas/defaults/cmux-next.json | 3 -- 9 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/AgentActionCatalog.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/AgentActionCatalog.swift index 7139e1f01a02..6918bed13178 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/AgentActionCatalog.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/AgentActionCatalog.swift @@ -6,21 +6,21 @@ nonisolated enum AgentActionCatalog: ActionCatalogGroup { ActionDescriptor( id: "agentPaneZoomIn", title: String(localized: "action.agentPaneZoomIn", defaultValue: "Agent Chat: Zoom In", bundle: .module), - keywords: ["agent", "chat", "zoom", "bigger"], defaultShortcut: Shortcut("=", modifiers: [.command]), + keywords: ["agent", "chat", "zoom", "bigger"], category: .agents, symbol: "plus.magnifyingglass", surfaces: [.palette, .keyboard, .menu, .contextMenu], requires: [.agentPaneFocused], targets: [.pane], cliName: "agent zoom-in", mainMenu: .view ), ActionDescriptor( id: "agentPaneZoomOut", title: String(localized: "action.agentPaneZoomOut", defaultValue: "Agent Chat: Zoom Out", bundle: .module), - keywords: ["agent", "chat", "zoom", "smaller"], defaultShortcut: Shortcut("-", modifiers: [.command]), + keywords: ["agent", "chat", "zoom", "smaller"], category: .agents, symbol: "minus.magnifyingglass", surfaces: [.palette, .keyboard, .menu, .contextMenu], requires: [.agentPaneFocused], targets: [.pane], cliName: "agent zoom-out", mainMenu: .view ), ActionDescriptor( id: "agentPaneZoomReset", title: String(localized: "action.agentPaneZoomReset", defaultValue: "Agent Chat: Actual Size", bundle: .module), - keywords: ["agent", "chat", "zoom", "reset"], defaultShortcut: Shortcut("0", modifiers: [.command]), + keywords: ["agent", "chat", "zoom", "reset"], category: .agents, symbol: "1.magnifyingglass", surfaces: [.palette, .keyboard, .menu, .contextMenu], requires: [.agentPaneFocused], targets: [.pane], cliName: "agent actual-size", mainMenu: .view ), diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift index 734e13814795..98a787221159 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift @@ -5,6 +5,7 @@ import CmuxNextDaemon import CmuxNextDaemonService import CmuxNextDesign import CmuxNextLayout +import CmuxNextSettings // Swap, close, rename, flash and workspace font size. extension PaneHandlers { @@ -137,6 +138,12 @@ extension PaneHandlers { ] for (id, binding) in bindings { registry.bind(id, invoke: { invocation in + // The same keys change the chat's text size when its composer or transcript + // owns focus. The page-zoom actions remain available from the View menu/palette. + if let pane = ctx.scope(invocation).pane, case .agent = pane.currentContent { + changeChatFontSize(id, ctx) + return + } guard let content = ctx.content(invocation) else { return } var applied = 0 for pane in content.panes.values { @@ -148,4 +155,25 @@ extension PaneHandlers { }) } } + + /// One point per key press, within the Settings row's range. An absent value is its 14 pt + /// default; reset removes the override so later default changes still apply. + private static func changeChatFontSize(_ id: ActionID, _ ctx: AppActionContext) { + guard let settings = ctx.services.settings else { return ctx.refuse(RefusalStrings.settingsNotLoaded) } + let path = AgentPaneDisplaySetting.fontSizePath + if let managed = settings.managedKey(forPath: path) { + return ctx.refuse(RefusalStrings.settingManaged(managed.key)) + } + if id == "resetWorkspaceTerminalFontSize" { + ctx.writeSetting("reset chat font size", path, nil, reloadOnFailure: true) + return + } + let current = AgentPaneDisplaySetting(fileRoot: settings.fileRoot) + .values[path.joined(separator: ".")]?.doubleValue ?? AgentPaneDisplaySetting.fontSizeFallback + let delta = id == "increaseWorkspaceTerminalFontSize" ? 1.0 : -1.0 + let range = AgentPaneDisplaySetting.fontSizeRange + let next = min(max(current + delta, range.lowerBound), range.upperBound) + guard next != current else { return } + ctx.writeSetting("change chat font size", path, .number(next), reloadOnFailure: true) + } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/AgentPaneDisplaySetting.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/AgentPaneDisplaySetting.swift index 21279374ff46..d2e9d2892f3a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/AgentPaneDisplaySetting.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/AgentPaneDisplaySetting.swift @@ -17,6 +17,13 @@ public nonisolated struct AgentPaneDisplaySetting: Sendable, Hashable { /// The app keys the chat follows: Minimal Mode (``SidebarSectionsSetting/minimalPath``). public static let followedAppPaths: [[String]] = [SidebarSectionsSetting.minimalPath] + /// The Chat Font Size setting read by both the page and the app's Cmd +/− actions. + public static let fontSizePath = ["agentPane", "display", "font", "size"] + public static var fontSizeRange: ClosedRange { + Double(AgentPaneDisplaySettingsSchema.fontSizeRange.lowerBound)...Double(AgentPaneDisplaySettingsSchema.fontSizeRange.upperBound) + } + public static var fontSizeFallback: Double { AgentPaneDisplaySettingsSchema.fontSizeFallback } + /// The accepted values by dotted key. public var values: [String: JSONValue] diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift new file mode 100644 index 000000000000..e335b4427606 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift @@ -0,0 +1,54 @@ +import AppKit +import CmuxNextActions +@testable import CmuxNextApp +import CmuxNextAgentPane +import CmuxNextDesign +@testable import CmuxNextSettings +import Foundation +import Testing + +/// Lawrence via Leo: "make sure font size everywhere (like text input area) is customizable". +/// The same action bound to Cmd +/− scales chat text when a chat owns focus, and keeps the +/// workspace terminal path when it does not. +@MainActor @Suite(.serialized, .timeLimit(.minutes(3)), .enabled(if: SameTreeDaemonBinary.url != nil, "needs the same-tree cmux-tui")) +struct AgentChatFontSizeShortcutTests { + @Test func fontActionsWriteTheChatSettingOnlyInAChat() async throws { + try await PasswordManagerRouteTests.withApp { services, _ in + services.agentTabs.localHost = AgentSessionRef.host(installID: "cn-chat-font-key") + try #require(services.agentTabs.canHostChat, "this build has no agent page") + let root = FileManager.default.temporaryDirectory.appending(path: "cn-chat-font-key-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: root) } + let url = root.appending(path: "cmux.json") + try Data("{}".utf8).write(to: url) + let settings = SettingsController(registry: services.registry, design: DesignSettings(), fileURL: url, + managedReader: FixedManagedPreferenceReader(ManagedPreferences()), managedWatchFiles: []) + await settings.reload() + services.settings = settings + let key = AgentPaneDisplaySetting.fontSizePath + + // The first tab is a terminal. Its shortcut must not write the chat setting. + #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + #expect(settings.fileRoot.value(at: key) == nil) + + let pane = try #require(services.windows.active?.focusedPane) + pane.newAgentTab() + try await PasswordManagerRouteTests.until({ + guard let focused = services.windows.active?.focusedPane, + case .agent = focused.currentContent else { return false } + return true + }, report: { "focused content: \(String(describing: services.windows.active?.focusedPane?.currentContent))" }) + + #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == .number(15) }) + #expect(services.registry.perform("decreaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == .number(14) }) + + try await settings.set(.number(28), at: key) + #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + #expect(settings.fileRoot.value(at: key) == .number(28), "the upper Settings limit is kept") + #expect(services.registry.perform("resetWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == nil }) + } + } +} diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/KeybindingsFocusConflictTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/KeybindingsFocusConflictTests.swift index 0b6470511c62..a40cd9c10b81 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/KeybindingsFocusConflictTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/KeybindingsFocusConflictTests.swift @@ -37,7 +37,7 @@ import Testing }) } - static let actualSizes = ["browserZoomReset", "agentPaneZoomReset", "markdownZoomReset", "fileEditorZoomReset", + static let actualSizes = ["browserZoomReset", "markdownZoomReset", "fileEditorZoomReset", "canvasZoomReset", "terminal.resetFontSize", "resetWorkspaceTerminalFontSize"] /// Shipped defaults share keys by design (the Chrome tab keys and Cmd-1…9, Home's chat keys, diff --git a/docs/mdm/managed-preferences.md b/docs/mdm/managed-preferences.md index 661b064ebe84..3e955a610e51 100644 --- a/docs/mdm/managed-preferences.md +++ b/docs/mdm/managed-preferences.md @@ -571,7 +571,8 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `agentPane.spellChecking` | boolean | `false` | | Check Spelling While Typing. Underlines misspellings and offers system suggestions in the composer. | | `agentPane.attachmentView` | string | `"inline"` | `inline`, `strip` | Attachment View. How pictures and files show in the composer and in sent messages. Inline puts each one in the text where you added it; Strip shows them in a row. The message and its order stay the same. | | `agentPane.scrollToBottomOnSend` | boolean | `true` | | Scroll to Bottom When I Send. When off, sending while you are reading up keeps your place and shows new messages below. | -| `agentPane.zoom` | real | `1` | 0.5 to 2 | Agent Chat Zoom. The display size for agent chat. Cmd-0 resets it. | +| `agentPane.zoom` | real | `1` | 0.5 to 2 | Agent Chat Zoom. The display size for agent chat; its View menu action resets it. | +| `agentPane.display.font.size` | real | `14` | 10 to 28 | Chat text size, including the composer. Cmd-Plus, Cmd-Minus and Cmd-0 change or reset it while a chat is focused. | | `agents.defaultPermissionMode` | string | `"fullAccess"` | `fullAccess`, `standard`, `ask` | Permission Mode for New Chats. What a new chat may do without asking. A mode you pick in a chat applies to that chat only. | | `diagnostics.hangReports` | boolean | `false` | | Send Hang Reports. Nightly builds send a daily report of hangs and slow frames: call stacks of cmux and macOS code, timings, your app and macOS versions, your Mac's chip and memory size, and rounded counts of open windows and tabs. It never includes file names, web addresses, terminal text, names or anything you type. View Hang Report shows exactly what is sent. | | `macScreen.systemShortcuts` | boolean | `false` | | Send System Shortcuts to the Remote Mac. While a Mac Screen tab has the keyboard, shortcuts like ⌘Tab, ⌘Space, Mission Control and ⌘Q go to the other Mac. Needs Accessibility. | diff --git a/plans/cmux-next/action-surfaces.json b/plans/cmux-next/action-surfaces.json index a2e297517d96..de927fdffc8d 100644 --- a/plans/cmux-next/action-surfaces.json +++ b/plans/cmux-next/action-surfaces.json @@ -21908,12 +21908,7 @@ ], "default_chord" : null, - "default_shortcut" : { - "key" : "=", - "modifiers" : [ - "cmd" - ] - }, + "default_shortcut" : null, "id" : "agentPaneZoomIn", "main_menu" : "view", "mcp" : "systemChange", @@ -21942,12 +21937,7 @@ ], "default_chord" : null, - "default_shortcut" : { - "key" : "-", - "modifiers" : [ - "cmd" - ] - }, + "default_shortcut" : null, "id" : "agentPaneZoomOut", "main_menu" : "view", "mcp" : "systemChange", @@ -21976,12 +21966,7 @@ ], "default_chord" : null, - "default_shortcut" : { - "key" : "0", - "modifiers" : [ - "cmd" - ] - }, + "default_shortcut" : null, "id" : "agentPaneZoomReset", "main_menu" : "view", "mcp" : "systemChange", diff --git a/plans/cmux-next/palette-audit.md b/plans/cmux-next/palette-audit.md index 42585cf21eb7..e85d714ec75c 100644 --- a/plans/cmux-next/palette-audit.md +++ b/plans/cmux-next/palette-audit.md @@ -945,9 +945,9 @@ Niche: Allow Site Request, Always Center Focused Column, Attempt Update, Base Ke | Agents | `agentPane.searchFiles` | Search Files… | | b. standard | its section, catalog order | | Agents | `agentPane.switchModel` | Switch Model… | ⌃⌘M | b. standard | its section, catalog order | | Agents | `agentPane.toggleInspector` | Show ACP Inspector | | b. standard | its section, catalog order | -| Agents | `agentPaneZoomIn` | Agent Chat: Zoom In | ⌘= | b. standard | its section, catalog order | -| Agents | `agentPaneZoomOut` | Agent Chat: Zoom Out | ⌘- | b. standard | its section, catalog order | -| Agents | `agentPaneZoomReset` | Agent Chat: Actual Size | ⌘0 | b. standard | its section, catalog order | +| Agents | `agentPaneZoomIn` | Agent Chat: Zoom In | — | b. standard | its section, catalog order | +| Agents | `agentPaneZoomOut` | Agent Chat: Zoom Out | — | b. standard | its section, catalog order | +| Agents | `agentPaneZoomReset` | Agent Chat: Actual Size | — | b. standard | its section, catalog order | | Agents | `chief.moveTo` | Move Chief To… | | b. standard | its section, catalog order | | Agents | `chief.openMemoryInspector` | Chief: Open Memory Inspector | | b. standard | its section, catalog order | | Agents | `chief.openUsage` | Chief Usage | | b. standard | its section, catalog order | diff --git a/schemas/defaults/cmux-next.json b/schemas/defaults/cmux-next.json index 2a929af2e491..2d66b84e9a54 100644 --- a/schemas/defaults/cmux-next.json +++ b/schemas/defaults/cmux-next.json @@ -20,9 +20,6 @@ "key.agentPane.previousTurn": ["ctrl+opt+up"], "key.agentPane.searchChats": ["cmd+k when surfaceKind != terminal && !codeEditorFocused"], "key.agentPane.switchModel": ["ctrl+cmd+m"], - "key.agentPaneZoomIn": ["cmd+="], - "key.agentPaneZoomOut": ["cmd+-"], - "key.agentPaneZoomReset": ["cmd+0"], "key.appearance.uiScale.decrease": ["ctrl+shift+cmd+-"], "key.appearance.uiScale.increase": ["ctrl+shift+cmd+="], "key.appearance.uiScale.reset": ["ctrl+shift+cmd+0"], From 62576a3b6360cebf1d8e6df5d4f68138d8a3a294 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 03:55:47 +0000 Subject: [PATCH 03/12] fix(next): serialize repeated chat font shortcuts --- .../Handlers/PaneHandlers+Verbs.swift | 45 ++++++++++++++----- .../AgentChatFontSizeShortcutTests.swift | 6 ++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift index 98a787221159..d329e21a678d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift @@ -156,6 +156,11 @@ extension PaneHandlers { } } + // Key repeats must read after the preceding write, or two quick Cmd-Plus presses + // both read 14 and save 15. Keep the chain only until its last write settles. + @MainActor private static var chatFontWrite: Task? + @MainActor private static var chatFontWriteGeneration = 0 + /// One point per key press, within the Settings row's range. An absent value is its 14 pt /// default; reset removes the override so later default changes still apply. private static func changeChatFontSize(_ id: ActionID, _ ctx: AppActionContext) { @@ -164,16 +169,36 @@ extension PaneHandlers { if let managed = settings.managedKey(forPath: path) { return ctx.refuse(RefusalStrings.settingManaged(managed.key)) } - if id == "resetWorkspaceTerminalFontSize" { - ctx.writeSetting("reset chat font size", path, nil, reloadOnFailure: true) - return + let writer = SettingWriter.currentRun() + let previous = chatFontWrite + chatFontWriteGeneration += 1 + let generation = chatFontWriteGeneration + let task: Task = Task { @MainActor in + _ = await previous?.value + defer { + if chatFontWriteGeneration == generation { chatFontWrite = nil } + } + let value: JSONValue? + if id == "resetWorkspaceTerminalFontSize" { + value = nil + } else { + let current = AgentPaneDisplaySetting(fileRoot: settings.fileRoot) + .values[path.joined(separator: ".")]?.doubleValue ?? AgentPaneDisplaySetting.fontSizeFallback + let delta = id == "increaseWorkspaceTerminalFontSize" ? 1.0 : -1.0 + let range = AgentPaneDisplaySetting.fontSizeRange + let next = min(max(current + delta, range.lowerBound), range.upperBound) + guard next != current else { return nil } + value = .number(next) + } + do { + try await settings.setSetting(at: path, to: value, by: writer) + return nil + } catch { + await settings.reload() + return ActionWorkFailure(String(describing: error)) + } } - let current = AgentPaneDisplaySetting(fileRoot: settings.fileRoot) - .values[path.joined(separator: ".")]?.doubleValue ?? AgentPaneDisplaySetting.fontSizeFallback - let delta = id == "increaseWorkspaceTerminalFontSize" ? 1.0 : -1.0 - let range = AgentPaneDisplaySetting.fontSizeRange - let next = min(max(current + delta, range.lowerBound), range.upperBound) - guard next != current else { return } - ctx.writeSetting("change chat font size", path, .number(next), reloadOnFailure: true) + chatFontWrite = task + ctx.registry.track(task) } } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift index e335b4427606..b6074b964bd2 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift @@ -40,9 +40,11 @@ struct AgentChatFontSizeShortcutTests { }, report: { "focused content: \(String(describing: services.windows.active?.focusedPane?.currentContent))" }) #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) - try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == .number(15) }) + #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == .number(16) }, + report: { "two quick increases saved \(String(describing: settings.fileRoot.value(at: key)))" }) #expect(services.registry.perform("decreaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) - try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == .number(14) }) + try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == .number(15) }) try await settings.set(.number(28), at: key) #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) From 3da8d5026cbcc75ce6fc76987173fae0bc315b54 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 03:56:13 +0000 Subject: [PATCH 04/12] fix(next): keep chat font write queue per app --- .../Sources/CmuxNextApp/AppServices.swift | 6 ++++++ .../CmuxNextApp/Handlers/PaneHandlers+Verbs.swift | 15 +++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices.swift index 6dac0692204e..222f7ca2d24f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices.swift @@ -116,6 +116,12 @@ final class AppServices { let actionBudget: ActionBudgetMonitor /// Every action; timed by the always-on `action.*` signposts and `actionBudget`, in Debug and Release. let registry: ActionRegistry + /// Serializes Cmd +/− chat font writes so key repeats cannot lose a step. + @MainActor final class ChatFontSizeWriteQueue { + var latest: Task? + var generation = 0 + } + let agentChatFontSizeWrites = ChatFontSizeWriteQueue() /// The main-thread blocking detector's handle (threading-model.md): the Debug menu, `debug.blocking`, and its log sink (``BlockingLogSink``). let blocking = BlockingDetector.forProcess() /// Sparkle updates (release builds) or read-only feed probes (DEV). diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift index d329e21a678d..ce7adb77a7cf 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift @@ -156,10 +156,8 @@ extension PaneHandlers { } } - // Key repeats must read after the preceding write, or two quick Cmd-Plus presses + // Key repeats read after the preceding write, or two quick Cmd-Plus presses // both read 14 and save 15. Keep the chain only until its last write settles. - @MainActor private static var chatFontWrite: Task? - @MainActor private static var chatFontWriteGeneration = 0 /// One point per key press, within the Settings row's range. An absent value is its 14 pt /// default; reset removes the override so later default changes still apply. @@ -170,13 +168,14 @@ extension PaneHandlers { return ctx.refuse(RefusalStrings.settingManaged(managed.key)) } let writer = SettingWriter.currentRun() - let previous = chatFontWrite - chatFontWriteGeneration += 1 - let generation = chatFontWriteGeneration + let queue = ctx.services.agentChatFontSizeWrites + let previous = queue.latest + queue.generation += 1 + let generation = queue.generation let task: Task = Task { @MainActor in _ = await previous?.value defer { - if chatFontWriteGeneration == generation { chatFontWrite = nil } + if queue.generation == generation { queue.latest = nil } } let value: JSONValue? if id == "resetWorkspaceTerminalFontSize" { @@ -198,7 +197,7 @@ extension PaneHandlers { return ActionWorkFailure(String(describing: error)) } } - chatFontWrite = task + queue.latest = task ctx.registry.track(task) } } From b420c748d0f754836c8aa761c2ae457a1429be1e Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 04:14:24 +0000 Subject: [PATCH 05/12] docs(next): refresh chat shortcut catalog copy --- web/data/cmux-shortcuts.generated.json | 102 ------------------------- 1 file changed, 102 deletions(-) diff --git a/web/data/cmux-shortcuts.generated.json b/web/data/cmux-shortcuts.generated.json index 3fb84d9e4af7..0bd5d76cefd0 100644 --- a/web/data/cmux-shortcuts.generated.json +++ b/web/data/cmux-shortcuts.generated.json @@ -7810,108 +7810,6 @@ "zh-TW": "代理程式" }, "shortcuts": [ - { - "id": "agentPaneZoomIn", - "title": { - "en": "Agent Chat: Zoom In", - "ar": "دردشة الوكيل: تكبير", - "bs": "Chat s agentom: uvećaj", - "da": "Agentchat: Zoom ind", - "de": "Agent-Chat: Vergrößern", - "es": "Chat del agente: acercar", - "fr": "Chat de l’agent : zoom avant", - "it": "Chat dell’agente: ingrandisci", - "ja": "エージェントチャット: 拡大", - "km": "ការជជែកជាមួយភ្នាក់ងារ៖ ពង្រីក", - "ko": "에이전트 채팅: 확대", - "no": "Agentchat: Zoom inn", - "pl": "Czat z agentem: Powiększ", - "pt-BR": "Chat do agente: Ampliar", - "ru": "Чат с агентом: увеличить", - "th": "แชตเอเจนต์: ซูมเข้า", - "tr": "Ajan sohbeti: Yakınlaştır", - "uk": "Чат з агентом: збільшити", - "vi": "Trò chuyện với tác tử: Phóng to", - "zh-CN": "智能体聊天:放大", - "zh-TW": "代理聊天:放大" - }, - "shortcut": { - "key": "=", - "modifiers": [ - "cmd" - ] - }, - "chord": null, - "aliases": [] - }, - { - "id": "agentPaneZoomOut", - "title": { - "en": "Agent Chat: Zoom Out", - "ar": "دردشة الوكيل: تصغير", - "bs": "Chat s agentom: umanji", - "da": "Agentchat: Zoom ud", - "de": "Agent-Chat: Verkleinern", - "es": "Chat del agente: alejar", - "fr": "Chat de l’agent : zoom arrière", - "it": "Chat dell’agente: riduci", - "ja": "エージェントチャット: 縮小", - "km": "ការជជែកជាមួយភ្នាក់ងារ៖ បង្រួម", - "ko": "에이전트 채팅: 축소", - "no": "Agentchat: Zoom ut", - "pl": "Czat z agentem: Pomniejsz", - "pt-BR": "Chat do agente: Reduzir", - "ru": "Чат с агентом: уменьшить", - "th": "แชตเอเจนต์: ซูมออก", - "tr": "Ajan sohbeti: Uzaklaştır", - "uk": "Чат з агентом: зменшити", - "vi": "Trò chuyện với tác tử: Thu nhỏ", - "zh-CN": "智能体聊天:缩小", - "zh-TW": "代理聊天:縮小" - }, - "shortcut": { - "key": "-", - "modifiers": [ - "cmd" - ] - }, - "chord": null, - "aliases": [] - }, - { - "id": "agentPaneZoomReset", - "title": { - "en": "Agent Chat: Actual Size", - "ar": "دردشة الوكيل: الحجم الفعلي", - "bs": "Chat s agentom: stvarna veličina", - "da": "Agentchat: Faktisk størrelse", - "de": "Agent-Chat: Originalgröße", - "es": "Chat del agente: tamaño real", - "fr": "Chat de l’agent : taille réelle", - "it": "Chat dell’agente: dimensioni reali", - "ja": "エージェントチャット: 実寸表示", - "km": "ការជជែកជាមួយភ្នាក់ងារ៖ ទំហំពិត", - "ko": "에이전트 채팅: 실제 크기", - "no": "Agentchat: Faktisk størrelse", - "pl": "Czat z agentem: Rzeczywisty rozmiar", - "pt-BR": "Chat do agente: Tamanho Real", - "ru": "Чат с агентом: фактический размер", - "th": "แชตเอเจนต์: ขนาดจริง", - "tr": "Ajan sohbeti: Gerçek Boyut", - "uk": "Чат з агентом: реальний розмір", - "vi": "Trò chuyện với tác tử: Kích thước thực", - "zh-CN": "智能体聊天:实际大小", - "zh-TW": "代理聊天:實際大小" - }, - "shortcut": { - "key": "0", - "modifiers": [ - "cmd" - ] - }, - "chord": null, - "aliases": [] - }, { "id": "palette.newAgentChat", "title": { From 964e1ae8844c7ccf92c13e386448e0e2f3eefdad Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 05:10:34 +0000 Subject: [PATCH 06/12] fix(next): disambiguate chat setting value type --- .../Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift index ce7adb77a7cf..6f8ba8b1dfa2 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift @@ -177,7 +177,7 @@ extension PaneHandlers { defer { if queue.generation == generation { queue.latest = nil } } - let value: JSONValue? + let value: CmuxNextSettings.JSONValue? if id == "resetWorkspaceTerminalFontSize" { value = nil } else { From b513dbfae95c12da5da13b1ea8654aabab484976 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 06:21:56 -0700 Subject: [PATCH 07/12] test(next): await the chat font limit write Agent-Session: https://cmux.com/deeplink/agent?id=aid-3173b5a2797f53e96698 --- .../Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift index b6074b964bd2..a4d874560bb2 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift @@ -48,6 +48,7 @@ struct AgentChatFontSizeShortcutTests { try await settings.set(.number(28), at: key) #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + #expect(await services.agentChatFontSizeWrites.latest?.value == nil) #expect(settings.fileRoot.value(at: key) == .number(28), "the upper Settings limit is kept") #expect(services.registry.perform("resetWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == nil }) From dd020bcb78587dbba897290ec84f4201971f74ae Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 06:44:26 -0700 Subject: [PATCH 08/12] test(next): report failed chat font shortcut writes Agent-Session: https://cmux.com/deeplink/agent?id=aid-3173b5a2797f53e96698 --- .../AgentChatFontSizeShortcutTests.swift | 100 ++++++++++++++---- 1 file changed, 81 insertions(+), 19 deletions(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift index a4d874560bb2..bdb76d8c4474 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift @@ -13,31 +13,14 @@ import Testing @MainActor @Suite(.serialized, .timeLimit(.minutes(3)), .enabled(if: SameTreeDaemonBinary.url != nil, "needs the same-tree cmux-tui")) struct AgentChatFontSizeShortcutTests { @Test func fontActionsWriteTheChatSettingOnlyInAChat() async throws { - try await PasswordManagerRouteTests.withApp { services, _ in - services.agentTabs.localHost = AgentSessionRef.host(installID: "cn-chat-font-key") - try #require(services.agentTabs.canHostChat, "this build has no agent page") - let root = FileManager.default.temporaryDirectory.appending(path: "cn-chat-font-key-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: root) } - let url = root.appending(path: "cmux.json") - try Data("{}".utf8).write(to: url) - let settings = SettingsController(registry: services.registry, design: DesignSettings(), fileURL: url, - managedReader: FixedManagedPreferenceReader(ManagedPreferences()), managedWatchFiles: []) - await settings.reload() - services.settings = settings + try await withSettings { services, settings, _ in let key = AgentPaneDisplaySetting.fontSizePath // The first tab is a terminal. Its shortcut must not write the chat setting. #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) #expect(settings.fileRoot.value(at: key) == nil) - let pane = try #require(services.windows.active?.focusedPane) - pane.newAgentTab() - try await PasswordManagerRouteTests.until({ - guard let focused = services.windows.active?.focusedPane, - case .agent = focused.currentContent else { return false } - return true - }, report: { "focused content: \(String(describing: services.windows.active?.focusedPane?.currentContent))" }) + try await openChat(services) #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) @@ -54,4 +37,83 @@ struct AgentChatFontSizeShortcutTests { try await PasswordManagerRouteTests.until({ settings.fileRoot.value(at: key) == nil }) } } + + @Test func managedFontActionsRefuseWithoutWriting() async throws { + let key = AgentPaneDisplaySetting.fontSizePath + let dotted = key.joined(separator: ".") + try await withSettings(managed: ManagedPreferences(forced: [dotted: .number(22)])) { services, settings, url in + try await openChat(services) + #expect(settings.managedKey(forPath: key) != nil) + var notices: [(String, Bool)] = [] + let previous = services.registry.refusalObserver + services.registry.refusalObserver = { notices.append(($0, $1)) } + defer { services.registry.refusalObserver = previous } + for id: ActionID in ["increaseWorkspaceTerminalFontSize", "decreaseWorkspaceTerminalFontSize", "resetWorkspaceTerminalFontSize"] { + #expect(services.registry.perform(id, invocation: ActionInvocation(origin: .user))) + } + #expect(notices.count == 3) + #expect(notices.allSatisfy { $0.0.contains(dotted) && !$0.1 }) + #expect(services.agentChatFontSizeWrites.latest == nil) + #expect(settings.fileRoot.value(at: key) == nil) + #expect(try String(contentsOf: url, encoding: .utf8) == "{}") + } + } + + @Test(arguments: [false, true]) func failedFontWritesReachTheKeyboardOrWaitingCaller(captured: Bool) async throws { + try await withSettings { services, settings, url in + try await openChat(services) + // A broken user file must remain untouched, and the failed write must not be silent. + try Data("{".utf8).write(to: url) + var notices: [(String, Bool)] = [] + let previous = services.registry.refusalObserver + services.registry.refusalObserver = { notices.append(($0, $1)) } + defer { services.registry.refusalObserver = previous } + if captured { + let work = services.registry.capturingWork { + #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + } + #expect(work.count == 1) + let task = try #require(work.first) + let failure = try #require(await task.value) + #expect(!failure.message.isEmpty) + #expect(notices.isEmpty, "the waiting caller receives the failure without a duplicate notice") + } else { + #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + let task = try #require(services.agentChatFontSizeWrites.latest) + #expect(await task.value != nil) + try await PasswordManagerRouteTests.until({ notices.count == 1 }, report: { "a failed keyboard write showed \(notices.count) refusals" }) + #expect(notices.first?.0.isEmpty == false && notices.first?.1 == false) + } + #expect(settings.fileRoot.value(at: AgentPaneDisplaySetting.fontSizePath) == nil) + #expect(try String(contentsOf: url, encoding: .utf8) == "{") + } + } + + private func withSettings(managed: ManagedPreferences = ManagedPreferences(), + _ body: (AppServices, SettingsController, URL) async throws -> Void) async throws { + try await PasswordManagerRouteTests.withApp { services, _ in + services.agentTabs.localHost = AgentSessionRef.host(installID: "cn-chat-font-key") + try #require(services.agentTabs.canHostChat, "this build has no agent page") + let root = FileManager.default.temporaryDirectory.appending(path: "cn-chat-font-key-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: root) } + let url = root.appending(path: "cmux.json") + try Data("{}".utf8).write(to: url) + let settings = SettingsController(registry: services.registry, design: DesignSettings(), fileURL: url, + managedReader: FixedManagedPreferenceReader(managed), managedWatchFiles: []) + await settings.reload() + services.settings = settings + try await body(services, settings, url) + } + } + + private func openChat(_ services: AppServices) async throws { + let pane = try #require(services.windows.active?.focusedPane) + pane.newAgentTab() + try await PasswordManagerRouteTests.until({ + guard let focused = services.windows.active?.focusedPane, + case .agent = focused.currentContent else { return false } + return true + }, report: { "focused content: \(String(describing: services.windows.active?.focusedPane?.currentContent))" }) + } } From da70c5621b1579dd815b8bdada131ecb1274aa79 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 06:44:54 -0700 Subject: [PATCH 09/12] fix(next): surface chat font write failures and refresh docs Agent-Session: https://cmux.com/deeplink/agent?id=aid-3173b5a2797f53e96698 --- .../Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift | 2 +- web/data/next-docs/palette-reference.generated.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift index 6f8ba8b1dfa2..2d80b3b43c7b 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift @@ -198,6 +198,6 @@ extension PaneHandlers { } } queue.latest = task - ctx.registry.track(task) + ctx.report { await task.value } } } diff --git a/web/data/next-docs/palette-reference.generated.json b/web/data/next-docs/palette-reference.generated.json index c7014b3b1647..0004c3e9d3ad 100644 --- a/web/data/next-docs/palette-reference.generated.json +++ b/web/data/next-docs/palette-reference.generated.json @@ -2,7 +2,7 @@ "version": 1, "counts": { "entries": 1212, - "shortcuts": 189 + "shortcuts": 186 }, "sections": [ { @@ -5532,19 +5532,19 @@ { "id": "agentPaneZoomReset", "title": "Agent Chat: Actual Size", - "shortcut": "⌘0", + "shortcut": null, "command": "cmux agent actual-size" }, { "id": "agentPaneZoomIn", "title": "Agent Chat: Zoom In", - "shortcut": "⌘=", + "shortcut": null, "command": "cmux agent zoom-in" }, { "id": "agentPaneZoomOut", "title": "Agent Chat: Zoom Out", - "shortcut": "⌘-", + "shortcut": null, "command": "cmux agent zoom-out" }, { From 942182574c7213d02304750290b46b9173e1794f Mon Sep 17 00:00:00 2001 From: Leo Li Date: Fri, 30 Oct 2026 22:58:25 -0700 Subject: [PATCH 10/12] docs: refresh CLI reference after base merge Agent-Session: https://cmux.com/deeplink/agent?id=aid-b14e80c6998b6f900360 --- .../next-docs/cli-reference.generated.json | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/web/data/next-docs/cli-reference.generated.json b/web/data/next-docs/cli-reference.generated.json index 6dc2e406a695..8b272cc742f7 100644 --- a/web/data/next-docs/cli-reference.generated.json +++ b/web/data/next-docs/cli-reference.generated.json @@ -1,8 +1,8 @@ { "version": 1, "counts": { - "verbs": 760, - "mcp": 678 + "verbs": 764, + "mcp": 680 }, "nouns": [ { @@ -1082,12 +1082,40 @@ { "noun": "chief", "verbs": [ + { + "command": "cmux chief look-at-screen", + "title": "Chief: Look at Screen Now", + "id": "chief.screenShare.look", + "mcp": true, + "mcpTool": "app_chief_look_at_screen" + }, { "command": "cmux chief memory", "title": "Chief: Open Memory Inspector", "id": "chief.openMemoryInspector", "mcp": true, "mcpTool": "app_chief_memory" + }, + { + "command": "cmux chief share-screen", + "title": "Share Screen with Chief", + "id": "chief.screenShare.start", + "mcp": false, + "mcpTool": null + }, + { + "command": "cmux chief stop-sharing-screen", + "title": "Stop Sharing Screen with Chief", + "id": "chief.screenShare.stop", + "mcp": true, + "mcpTool": "app_chief_stop_sharing_screen" + }, + { + "command": "cmux chief toggle-screen-share", + "title": "Chief: Toggle Screen Sharing", + "id": "chief.screenShare.toggle", + "mcp": false, + "mcpTool": null } ] }, @@ -5549,6 +5577,6 @@ "mcpExempt": { "credentials": 6, "endsApp": 4, - "systemChange": 72 + "systemChange": 74 } } From 48e2ee896974662f8c5f8b5fa66309d30011fa5a Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 31 Oct 2026 02:17:49 -0700 Subject: [PATCH 11/12] test(chat): select a terminal before checking shortcut isolation Agent-Session: https://cmux.com/deeplink/agent?id=aid-b14e80c6998b6f900360 --- .../AgentChatFontSizeShortcutTests.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift index b12de68c2303..d1733938661f 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift @@ -17,8 +17,17 @@ struct AgentChatFontSizeShortcutTests { try await withSettings { services, settings, _ in let key = AgentPaneDisplaySetting.fontSizePath - // The first tab is a terminal. Its shortcut must not write the chat setting. + // Startup may land on a chat. Select a real terminal before testing its shortcut, + // or an asynchronous chat-font write can leak into the two-press assertion below. + let pane = try #require(services.windows.active?.focusedPane) + pane.newTerminalTab() + try await PasswordManagerRouteTests.until({ + guard let focused = services.windows.active?.focusedPane, + case .terminal = focused.currentContent else { return false } + return true + }) #expect(services.registry.perform("increaseWorkspaceTerminalFontSize", invocation: ActionInvocation(origin: .user))) + #expect(services.agentChatFontSizeWrites.latest == nil) #expect(settings.fileRoot.value(at: key) == nil) try await openChat(services) From aeab59d02e1ec29188f98b8671d0440074e7f051 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 31 Oct 2026 02:27:21 -0700 Subject: [PATCH 12/12] fix(chat): keep automated workspace font actions on terminals Agent-Session: https://cmux.com/deeplink/agent?id=aid-b14e80c6998b6f900360 --- .../Handlers/PaneHandlers+Verbs.swift | 5 +++-- .../AgentChatFontSizeShortcutTests.swift | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift index d504f646ca9b..cf1b04c8e332 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/PaneHandlers+Verbs.swift @@ -145,8 +145,9 @@ extension PaneHandlers { // palette/menu runs (which have no key context) target-scoped, but let a sidebar // or native control retain the workspace-terminal action while a chat remains // selected. - let chatOwnsKeyboard = invocation.keyContext?.contains(.agentPaneFocused) == true - let targetIsChat = invocation.keyContext == nil + let inAppUser = invocation.origin == .user && !invocation.viaControlSocket + let chatOwnsKeyboard = inAppUser && invocation.keyContext?.contains(.agentPaneFocused) == true + let targetIsChat = inAppUser && invocation.keyContext == nil && ctx.scope(invocation).pane.map { if case .agent = $0.currentContent { true } else { false } } == true if chatOwnsKeyboard || targetIsChat { changeChatFontSize(id, ctx) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift index d1733938661f..5131840d6895 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AgentChatFontSizeShortcutTests.swift @@ -90,6 +90,25 @@ struct AgentChatFontSizeShortcutTests { } } + @Test func automationKeepsTheWorkspaceFontActionWhileAChatIsSelected() async throws { + try await withSettings { services, settings, _ in + try await openChat(services) + let key = AgentPaneDisplaySetting.fontSizePath + try await settings.set(.number(20), at: key) + for origin in ActionOrigin.allCases { + var invocation = ActionInvocation(origin: origin) + // A socket caller can claim user origin; it still is not an in-app gesture. + invocation.viaControlSocket = origin == .user || origin == .cli || origin == .mcp + for id: ActionID in ["increaseWorkspaceTerminalFontSize", "decreaseWorkspaceTerminalFontSize", "resetWorkspaceTerminalFontSize"] { + #expect(services.registry.perform(id, invocation: invocation)) + #expect(services.agentChatFontSizeWrites.latest == nil, + "\(origin) must retain the workspace-terminal action") + #expect(settings.fileRoot.value(at: key) == .number(20)) + } + } + } + } + @Test(arguments: [false, true]) func failedFontWritesReachTheKeyboardOrWaitingCaller(captured: Bool) async throws { try await withSettings { services, settings, url in try await openChat(services)