From 51ebd8f92f09dc6c79edb473c1f5168a0c745260 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 20 Oct 2026 23:58:16 -0700 Subject: [PATCH 1/9] cmux-next: main pane layout, an agent chat's opens land in one column on its right `layout.mainPane` ("agent-chats" by default, "off"), with a Debug Settings override (`panes.mainPane`: follow the setting, on, off). An agent chat that is its screen's main pane (the chat dock, or a chat alone on its screen) never holds what it opens: - links, files and folders it opens (LinkOpener) are a tab in the strip pane right of the chat dock, or, from a lone chat, open unselected in its pane while the chat docks left, so that pane becomes the column on its right; - subagent and side chat panes (AgentTabStore.openBeside) are tabs there too instead of splits stacked beside the chat. The placement is ChatColumnPlacement's, the rule a person's new tab from the chat already follows. A person's click focuses the new tab; an agent's open keeps focus in the chat. Closing the column's last tab folds the dock back into a plain pane, so the chat gets its full width. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- .../CmuxNextApp/AgentTabs+Header.swift | 8 +- .../CmuxNextApp/AgentTabs+Wiring.swift | 7 + .../Sources/CmuxNextApp/AgentTabs.swift | 6 + .../Handlers/MainPaneLayout+Routing.swift | 140 +++++ .../CmuxNextApp/Links/LinkOpenPlacement.swift | 3 + .../CmuxNextApp/Links/LinkOpener.swift | 18 + .../CmuxNextLayout/Model/MainPaneLayout.swift | 64 +++ .../DropOverlay/DropOverlayTunables.swift | 2 +- .../CmuxConfigSnapshot+PanePlacement.swift | 4 +- .../CmuxNextSettings/CmuxConfigSnapshot.swift | 2 + .../CmuxNextSettings/Localizable.xcstrings | 524 ++++++++++++++++++ .../CmuxNextSettings/MainPaneSetting.swift | 21 + .../Schema/SettingsSchema+PanePlacement.swift | 17 +- .../MainPaneLayoutTests.swift | 64 +++ .../MainPaneSettingTests.swift | 35 ++ .../next/highlights/agent-chat-main-pane.md | 4 + 16 files changed, 912 insertions(+), 7 deletions(-) create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/MainPaneSetting.swift create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/MainPaneLayoutTests.swift create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/MainPaneSettingTests.swift create mode 100644 release-notes/next/highlights/agent-chat-main-pane.md diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Header.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Header.swift index 9a1914476fdc..f1d5908973f0 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Header.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Header.swift @@ -32,7 +32,7 @@ extension AgentTabStore { watch.read() }, openSide: { [weak self] chat, fork in // the tracker (AgentSidePanes) picks the side pane's tab or a new split; the app opens there guard let self else { return }; AgentSidePanes.chat(chat).open(fork, chat: resolve(provisional), pane: { [weak self] in self?.locate(self?.resolve($0) ?? $0).map { "\(ObjectIdentifier($0.daemon).hashValue):\($0.pane.rawValue)" } }) { [weak self] anchor, created, finished in - self?.openBeside(anchor ?? provisional, split: anchor == nil, open: { [weak self] pane, daemon in guard let self else { throw CancellationError() }; return try open(in: pane, of: daemon, session: fork) }, created: created, finished: { _ in finished() }) ?? false } + self?.openBeside(anchor ?? provisional, split: anchor == nil, main: provisional, open: { [weak self] pane, daemon in guard let self else { throw CancellationError() }; return try open(in: pane, of: daemon, session: fork) }, created: created, finished: { _ in finished() }) ?? false } }, openTab: { [weak self] session in guard let self else { return false }; return openBeside(provisional, split: false) { pane, daemon in try self.open(in: pane, of: daemon, session: session) } }, openSubagent: { [weak self] in self?.openSubagent($1, of: $0, focus: $2, beside: provisional) ?? false }, openProcess: { [weak self] viewer in guard let self else { return false }; return openProcess?(resolve(provisional), viewer) ?? false }, newChat: { [weak self] in self?.openNewTabColumn(beside: provisional, cwd: $0, split: false) ?? false }, sideTabs: { [weak self] in guard let self else { return nil } let chat = resolve(provisional) @@ -63,11 +63,13 @@ extension AgentTabStore { private func openSubagent(_ agent: String, of session: String, focus: Bool, beside provisional: String) -> Bool { // the tracker (AgentSubagentPanes) decides; the app opens and moves the tab AgentSubagentPanes.session(session).open(agent, focus: focus, chat: provisional, hooks: .init(isOpen: { [weak self] in self?.locate($0) != nil }, focus: { [weak self] key, script in _ = self?.actionRegistry?.perform("tab.focus", invocation: ActionInvocation(target: ActionTargetRef(kind: .tab, id: self?.resolve(key) ?? key), arguments: script ? ["focus": .bool(true)] : [:], origin: script ? .script : .user)) }, - place: { [weak self] anchor, down, created, finished in self?.openBeside(anchor, direction: down ? "down" : "right", origin: focus ? .user : .script, open: { [weak self] pane, daemon in guard let self else { throw CancellationError() }; return try open(in: pane, of: daemon, session: session) }, + place: { [weak self] anchor, down, created, finished in self?.openBeside(anchor, direction: down ? "down" : "right", origin: focus ? .user : .script, main: provisional, open: { [weak self] pane, daemon in guard let self else { throw CancellationError() }; return try open(in: pane, of: daemon, session: session) }, created: { [weak self] in created($0); self?.revealTurn(AgentSubagentPanes.reveal(agent), in: $0) }, finished: finished) ?? false }, again: { [weak self] request in _ = self?.openSubagent(request.agent, of: session, focus: request.focus, beside: request.chat) })) } /// A tab in chat tab `provisional`'s pane, then moved to a new split on its right when `split`; else a tab there, focused when `created` is set. + /// A split asked for chat `main` when that chat is its screen's main pane (``openInMainPane``) is a tab in the column on its right instead. @discardableResult - private func openBeside(_ provisional: String, split: Bool = true, direction: String = "right", origin: ActionOrigin = .user, open: (PaneID, DaemonService) throws -> AgentTabPending, created: (@MainActor (String) -> Void)? = nil, finished: (@MainActor (_ moved: Bool) -> Void)? = nil) -> Bool { + private func openBeside(_ provisional: String, split: Bool = true, direction: String = "right", origin: ActionOrigin = .user, main: String? = nil, open: (PaneID, DaemonService) throws -> AgentTabPending, created: (@MainActor (String) -> Void)? = nil, finished: (@MainActor (_ moved: Bool) -> Void)? = nil) -> Bool { + if split, let main, let placed = openInMainPane(resolve(main), origin == .user, open, created, finished) { return placed } guard let (pane, daemon) = locate(resolve(provisional)), let pending = try? open(pane, daemon) else { return false } guard split || created != nil else { return true } // task-owner: one tab creation, then one move (or, not `split`, one focus) of that tab diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift index e23de183af53..88967476684f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift @@ -2,6 +2,7 @@ import CmuxNextAgentPane import CmuxNextCompat import CmuxNextDaemon import CmuxNextHomeEngine +import struct CmuxNextLayout.MainPaneLayout import Foundation /// Agent chat tabs on the workspace store (cmux-tui/spec/commands.md, new-conversation-tab): the store @@ -169,6 +170,12 @@ extension AgentTabStore { guard let services, let (tab, pane) = services.locateTab(key) else { return nil } return (pane: pane.handle, daemon: services.machines.daemon(forTab: tab)) } + tabs.openInMainPane = { [weak services] chat, focus, open, created, finished in + guard let services, let (_, pane) = services.locateTab(chat), let controller = services.paneController(for: pane), + let side = MainPaneLayout.side(from: controller, services: services) else { return nil } + return MainPaneLayout.openAgentTab(from: controller, side: side, focus: focus, services: services, open: open, + created: created, finished: finished) + } // A tab's git reads (Changes, file search) go to the machine whose acpmux runs its session // (AgentPaneHostKind): a Cloud or SSH machine's session reads its folder there (cx-d0tq). tabs.gitLink = { [weak services, weak tabs] key in diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs.swift index fe9f8da6de59..65ff0fa6b229 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs.swift @@ -33,6 +33,12 @@ final class AgentTabStore { var lookup: @MainActor (String) -> (record: AgentSessionRef, store: DaemonStore)? = { _ in nil } /// The pane holding tab `key` and its daemon (New side chat opens its fork there first). var locate: @MainActor (String) -> (pane: PaneID, daemon: DaemonService)? = { _ in nil } + /// Main pane layout (``MainPaneLayout``): opens what chat tab `chat` opens beside it (through + /// `open`, focused with `focus`) in the column on the chat's right, then `created` and + /// `finished` as ``openBeside`` calls them. Nil when the chat is not its screen's main pane: + /// the caller places the tab itself. + var openInMainPane: @MainActor (_ chat: String, _ focus: Bool, _ open: (PaneID, DaemonService) throws -> AgentTabPending, + _ created: (@MainActor (String) -> Void)?, _ finished: (@MainActor (Bool) -> Void)?) -> Bool? = { _, _, _, _, _ in nil } /// Every agent tab the trees list, with its record. var listTabs: @MainActor () -> [(key: String, record: AgentSessionRef)] = { [] } /// Creates the store tab with idempotency key `key` (AppServices: `new-conversation-tab` on diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift new file mode 100644 index 000000000000..c05c3c509387 --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift @@ -0,0 +1,140 @@ +import CmuxNextActions +import CmuxNextBridge +import CmuxNextCompat +import CmuxNextDaemon +import CmuxNextLayout +import CmuxNextSettings +import CmuxNextWakeups +import Foundation + +/// Main pane layout in the app (``MainPaneLayout``, `layout.mainPane`): what an agent chat opens from its +/// pane goes beside it. ``LinkOpener`` (links, files, folders) and ``AgentTabStore``'s side and +/// subagent panes ask here first. +extension MainPaneLayout { + /// How long a dock waits for the store to list the new tab, then for the chat to move. + static let listLimit: Duration = .seconds(30) + + /// Whether main pane layout is on now: the Debug override, else `layout.mainPane` (its default + /// before settings load). + @MainActor static func isOn(services: AppServices) -> Bool { + isOn(setting: (services.settings?.snapshot.mainPane ?? MainPaneSetting.fallback) == .agentChats, override: debugOverride.value) + } + + /// Where what `controller`'s pane opens goes in main pane layout, or nil (off, or not a main pane). + @MainActor static func side(from controller: PaneController, services: AppServices) -> ChatColumnPlacement? { + guard isOn(services: services) else { return nil } + return side(of: ChatColumnPlacement.resolve(from: controller, services: services)) + } + + /// The lone chat in `controller`'s pane docks left with the agent chat role; the tab on + /// `surface`, just opened there unselected, stays and is selected, so its pane becomes the + /// column on the chat's right. With `focus` (a person's click) the tab takes keyboard focus; + /// else focus follows the chat into its dock when the chat had it (an agent's open stays in + /// the background). A failed dock selects the tab where it is, so it never stays hidden. + @MainActor static func dockChat(of controller: PaneController, keeping surface: SurfaceID, focus: Bool, services: AppServices) { + let store = controller.daemon.store, anchor = controller.pane + let chatHadFocus = controller.workspace?.focusedPane === controller + services.registry.track(Task { @MainActor [weak controller] () -> ActionWorkFailure? in + let listed = Task { @MainActor () -> TabModel? in + if let tab = store.tab(surface: surface) { return tab } + // `tabListRevision`: the tracked read hears the tab appear (AppServices.locateTab). + for await found in ObservationStream({ _ = store.tabListRevision; return store.tab(surface: surface) != nil }) where found { + return store.tab(surface: surface) + } + return nil + } + let bound = DemandTimer(owner: "MainPaneLayout.tabListed") + bound.schedule(after: listLimit) { @MainActor in listed.cancel() } + let found = await listed.value + bound.cancel() + guard let tab = found, let controller else { return "main pane: the new tab did not open" } + guard let chat = anchor.tabs.first(where: { $0.id != tab.id && ChatColumnPlacement.isChat($0, services: services) }) else { + controller.select(StripTabID(tab.id)) + return nil + } + let docked = await withCheckedContinuation { (done: CheckedContinuation) in + TabMoves.toNewDockColumn(chat, anchor: anchor, edge: .left, role: .agentChat, services: services) { done.resume(returning: $0) } + } + controller.select(StripTabID(tab.id)) + guard docked else { + if focus { controller.focusContent() } + return "main pane: the chat did not dock (see the app log)" + } + // A hidden tab bar was the chat's choice: the pane it left shows its tab bar again. + if services.paneTabBars.choice(for: controller.paneKey) == false { + services.paneTabBars.set(nil, for: controller.paneKey) + controller.apply(controller.snapshot()) + } + guard let content = controller.workspace else { return nil } + if focus { + PaneHandlers.focus(controller.layoutPaneID, in: content) + } else if chatHadFocus { + await followChat(chat.id, from: anchor, in: content, services: services) + } + return nil + }) + } + + /// Opens an agent tab (a subagent's or a side chat's pane) that chat `controller` opens beside + /// itself, where main pane layout's `side` puts it: through `open` as a tab in the strip pane + /// right of the chat dock, or in the lone chat's pane, which then docks the chat left + /// (``dockChat(of:keeping:focus:services:)``). `created` hears the new tab's key once the + /// tree lists it, `finished` whether it landed beside the chat. With `focus` the tab takes + /// keyboard focus; else it is only selected there. False when nothing opened. + @MainActor static func openAgentTab(from controller: PaneController, side: ChatColumnPlacement, focus: Bool, services: AppServices, + open: (DaemonPaneID, DaemonService) throws -> AgentTabPending, + created: (@MainActor (String) -> Void)?, finished: (@MainActor (Bool) -> Void)?) -> Bool { + let target: PaneController + switch side { + case .tab(let id): + guard let strip = controller.workspace?.panes[id] else { return false } + target = strip + case .dockChat: + target = controller + case .here, .besideDock: + return false + } + guard let pending = try? open(target.pane.handle, target.daemon) else { return false } + let locate: TabMirror.Locate = { key in + services.locateTab(key).map { (pane: $0.1.handle, daemon: services.machines.daemon(forTab: $0.0)) } + } + // task-owner: one tab creation, then one selection (or the chat's dock) for it + Task { @MainActor [weak target] in + guard let tab = try? await pending.value(), await TabMirror.reaches(tab.key, in: locate), let target else { + finished?(false) + return + } + created?(tab.key) + if target === controller { + dockChat(of: target, keeping: tab.surface, focus: focus, services: services) + } else if let model = services.locateTab(tab.key)?.0 { + target.select(StripTabID(model.id)) + if focus, let content = target.workspace { PaneHandlers.focus(target.layoutPaneID, in: content) } + } + finished?(true) + } + return true + } + + /// Focuses the pane chat tab `id` moved into from `anchor` once the store lists it there, as + /// ``AgentBesidePlacement`` does when a lone chat docks for an agent's tab. + @MainActor private static func followChat(_ id: String, from anchor: PaneModel, in content: WorkspaceContentController, + services: AppServices) async { + let moved = Task { @MainActor () -> Bool in + for await moved in ObservationStream({ services.locateTab(id).map { $0.1 !== anchor } ?? false }) where moved { + return true + } + return false + } + let bound = DemandTimer(owner: "MainPaneLayout.chatDock") + bound.schedule(after: listLimit) { @MainActor in moved.cancel() } + let found = await moved.value + bound.cancel() + guard found, let pane = services.locateTab(id)?.1 else { return } + // Not a view change of the open: the chat moved out of the focused pane, and focus follows + // it back to where the person had it (FocusCoordinator.followMovedTab does so for a move). + ActionRunScope.carrying(ActionRunScope(origin: .script, allowsViewChange: true)) { + PaneHandlers.focus(LayoutPaneID(pane.id), in: content) + } + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpenPlacement.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpenPlacement.swift index 6f4dfed967f8..813f5f835bfa 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpenPlacement.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpenPlacement.swift @@ -29,6 +29,9 @@ struct LinkOpenPlacement { /// the daemon opens it in a new column right of the dock (`pinned-dock-alone-v1`), so the /// dock never holds the tab, not even for the moment a split or column move would take. case besideDock(Pane) + /// Main pane layout (``MainPaneLayout``), from an agent chat alone on its screen: the tab opens + /// unselected in this pane, then the chat docks left and the tab's pane stays on its right. + case dockChat(Pane) } var setting: LinkPlacement diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpener.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpener.swift index 0dc746db2a07..ba45eb633278 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpener.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpener.swift @@ -12,6 +12,8 @@ import CmuxNextWakeups /// here (``LinkRouter/openInCmux``, ``openFile(_:from:)``, ``openFolder(_:from:)``), so no surface picks its own pane. /// What a page opens (a chat and its pull request link, Home, New Tab, a Markdown page) is a tab in its pane, never an /// unasked split (Leo 2026-10-20); a terminal's or browser's Cmd-click (`cmdClick`) goes by `layout.linkPlacement`. +/// An agent chat that is its screen's main pane (``MainPaneLayout``, `layout.mainPane`) never holds what it opens: that is a +/// tab in the column on its right, or the lone chat docks left to make that column. /// /// A new tab for the pane on the right opens there, selected and focused. A new split or column is /// made by opening the tab unselected in the source pane and moving it there in one daemon move @@ -63,6 +65,13 @@ final class LinkOpener { /// column order (``LinkOpenPlacement/paneRight(of:columns:takesTabs:recency:)``), not by drawn /// frames, which miss a strip column that is scrolled or not laid out yet. func target(from source: PaneController, cmdClick: Bool = false) -> LinkOpenPlacement.Target { + if let services, let side = MainPaneLayout.side(from: source, services: services) { + switch side { + case .tab(let pane): return .pane(pane) + case .dockChat: return .dockChat(source.layoutPaneID) + case .here, .besideDock: break + } + } let takesTabs = source.stripModel.showsNewTabButton let right = source.workspace.flatMap { content -> LayoutPaneID? in let takes: (LayoutPaneID) -> Bool = { content.panes[$0]?.stripModel.showsNewTabButton == true } @@ -99,6 +108,11 @@ final class LinkOpener { case .besideDock: // The daemon makes a tab aimed at a lone pinned dock in a new column right of it. return make(source, nil) + case .dockChat: + return make(source) { [weak source] surface in + guard let source else { return } + MainPaneLayout.dockChat(of: source, keeping: surface, focus: !background, services: source.services) + } } } @@ -126,6 +140,10 @@ final class LinkOpener { (source.workspace?.panes[id] ?? source).newTerminalTab(cwd: path) case .besideDock: source.workspace?.newColumn(after: source.layoutPaneID, options: SpawnOptions(cwd: path, workspace: source.workspace?.workspace.key)) + case .dockChat: + // The chat docks and leaves the folder's terminal in its pane, in one daemon commit. + let spawn = SpawnOptions(cwd: path, workspace: source.workspace?.workspace.key) + ChatColumnPlacement.route(from: source, respawn: .terminal(spawn), services: source.services)?.newTerminalTab(cwd: path) case .splitRight, .newColumn: var invocation = ActionInvocation(target: ActionTargetRef(kind: .pane, id: source.paneKey), arguments: ["cwd": .string(path)], origin: .user) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift new file mode 100644 index 000000000000..ad4c70fe1d7d --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift @@ -0,0 +1,64 @@ +public import CmuxNextDesign + +/// Main pane layout (`layout.mainPane`, Leo 2026-10-20): a screen's agent chat is its one main +/// pane, which nothing opens into. What the chat opens is a tab in the column on its right, the +/// way a person's new tab from the chat dock already opens (``ChatColumnPlacement``): in the strip +/// beside the chat dock, or, for a chat alone on its screen, the chat docks left and the tab stays +/// in its pane, which slides in on the right. When that column's last tab closes, the daemon folds +/// the dock back into a plain pane, so the chat has the whole width again. +public nonisolated struct MainPaneLayout { + public nonisolated init() {} + /// Debug Settings' override of `layout.mainPane`. + public static let debugOverride = Tunable.choice( + "panes.mainPane", .panes, "Main pane layout", + help: "An agent chat is its screen's main pane: what it opens (links, files, subagents, side chats) is a tab in the column on its right. "Follow the setting" uses layout.mainPane in cmux.json; On and Off override it.", + default: .setting, code: "MainPaneLayout.debugOverride") + + /// Whether main pane layout is on: the Debug override, else the setting (`agentChats`). + public static func isOn(setting: Bool, override: MainPaneOverride) -> Bool { + switch override { + case .setting: setting + case .on: true + case .off: false + } + } + + /// Where what `pane` opens goes in main pane layout: a tab in the strip pane right of the chat + /// dock (``ChatColumnPlacement/tab(in:)``), or the lone chat docks left and the tab stays in its + /// pane (``ChatColumnPlacement/dockChat``). Nil when `pane` is not a main pane (not the chat + /// dock, not a chat alone on its screen, a zoomed screen, or Home's pinned dock alone, which + /// opens beside itself already): the usual rules apply. Arguments are + /// ``ChatColumnPlacement/resolve(from:columns:recent:zoomed:isLoneChat:)``'s. + public static func side(from pane: PaneID, columns: [LayoutColumn], recent: [PaneID] = [], zoomed: Bool = false, + isLoneChat: (LayoutColumn) -> Bool) -> ChatColumnPlacement? { + side(of: ChatColumnPlacement.resolve(from: pane, columns: columns, recent: recent, zoomed: zoomed, isLoneChat: isLoneChat)) + } + + /// `placement` when it puts the new tab beside the chat, else nil. + public static func side(of placement: ChatColumnPlacement) -> ChatColumnPlacement? { + switch placement { + case .tab, .dockChat: return placement + case .here, .besideDock: return nil + } + } + + static var all: [TunableDescriptor] { [debugOverride.descriptor] } +} + +/// Debug Settings' choice for ``MainPaneLayout/debugOverride``. +public nonisolated enum MainPaneOverride: String, Sendable, CaseIterable, TunableChoice { + /// Follow `layout.mainPane` in cmux.json. + case setting + /// Main pane layout on, whatever cmux.json says. + case on + /// Main pane layout off, whatever cmux.json says. + case off + + public var tunableTitle: String { + switch self { + case .setting: "Follow the setting" + case .on: "On" + case .off: "Off" + } + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/DropOverlay/DropOverlayTunables.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/DropOverlay/DropOverlayTunables.swift index 7514a21f1a3a..c55690b3d588 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/DropOverlay/DropOverlayTunables.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Views/DropOverlay/DropOverlayTunables.swift @@ -150,7 +150,7 @@ public nonisolated enum LayoutTunables { public static var all: [TunableDescriptor] { [prototypeModel.descriptor, prototypeDockEdge.descriptor, prototypeOrientation.descriptor, prototypeDockMode.descriptor, - paneMotionVariant.descriptor] + DropOverlayTunables.all + [dropEdgeFraction, dropEdgeMinimum, dropEdgeMaximum, dropZoneHysteresis, newColumnDropWidth, inactivePaneDimming, + paneMotionVariant.descriptor] + MainPaneLayout.all + DropOverlayTunables.all + [dropEdgeFraction, dropEdgeMinimum, dropEdgeMaximum, dropZoneHysteresis, newColumnDropWidth, inactivePaneDimming, minimumContentWidth, minimumContentHeight, focusRingAlpha, focusGlowAlpha, focusGlowRadiusFactor].map(\.descriptor) } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+PanePlacement.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+PanePlacement.swift index d1f23d7b45bc..1139bc388a95 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+PanePlacement.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+PanePlacement.swift @@ -2,7 +2,7 @@ public import CmuxNextDesign /// Where new panes open (`PanePlacement`): `layout.newPanePlacement` (tab or /// split) and `layout.tileBrowsers` (browsers split too under split -/// placement), `layout.linkPlacement` (``LinkPlacement``). A missing key is the default with no diagnostic; a bad value +/// placement), `layout.linkPlacement` (``LinkPlacement``), `layout.mainPane` (``MainPaneSetting``). A missing key is the default with no diagnostic; a bad value /// is the default plus a diagnostic. nonisolated extension CmuxConfigSnapshot { public static let newPanePlacementPath = ["layout", "newPanePlacement"] @@ -30,6 +30,8 @@ nonisolated extension CmuxConfigSnapshot { } snapshot.linkPlacement = ColumnLayoutSettings.choice(root, LinkPlacement.configPath, fallback: LinkPlacement.fallback, diagnostics: &diagnostics) + snapshot.mainPane = ColumnLayoutSettings.choice(root, MainPaneSetting.configPath, fallback: MainPaneSetting.fallback, + diagnostics: &diagnostics) snapshot.diagnostics += diagnostics } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift index 1b1c6dcb57f4..6d97a1a04b36 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift @@ -116,6 +116,8 @@ public struct CmuxConfigSnapshot: Sendable, Equatable { public var tileBrowsers: Bool = CmuxConfigSnapshot.tileBrowsersFallback /// `layout.linkPlacement`: where an opened link or path lands (``LinkPlacement``). public var linkPlacement: LinkPlacement = .fallback + /// `layout.mainPane`: whether an agent chat is its screen's main pane (``MainPaneSetting``). + public var mainPane: MainPaneSetting = .fallback /// `workspaces.newPlacement` (`WorkspaceListSettings`). public var newWorkspacePlacement: NewWorkspacePlacement = CmuxConfigSnapshot.newWorkspacePlacementFallback /// `layout.closeFocus`; "previousNeighbor" when unset or invalid. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings index 80fead49abb2..b684c800b204 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings @@ -134708,6 +134708,530 @@ } } }, + "settings.layout.mainPane": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Main Pane" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "اللوحة الرئيسية" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Glavno okno" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Hovedrude" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hauptbereich" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Panel principal" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Panneau principal" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riquadro principale" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "メインペイン" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្ទាំងមេ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "기본 패널" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Hovedrute" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Panel główny" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Painel principal" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Основная панель" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แผงหลัก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Ana Bölme" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Основна панель" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Ngăn chính" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "主窗格" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "主窗格" + } + } + } + }, + "settings.layout.mainPane.help": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Agent Chats keeps an agent chat as the main pane: links, files, subagents and side chats it opens appear as tabs in a column on its right, and closing that column gives the chat its full width back." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يُبقي خيار «محادثات الوكلاء» محادثة الوكيل في اللوحة الرئيسية: تظهر الروابط والملفات والوكلاء الفرعيون والمحادثات الجانبية التي تفتحها كعلامات تبويب في عمود على يمينها، وعند إغلاق هذا العمود تستعيد المحادثة عرضها الكامل." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Chatovi s agentom drže chat s agentom kao glavno okno: linkovi, fajlovi, podagenti i sporedni chatovi koje otvori pojavljuju se kao kartice u koloni s njegove desne strane, a zatvaranjem te kolone chat ponovo dobija punu širinu." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Agentchats beholder en agentchat som hovedruden: links, filer, underagenter og sidechats, den åbner, vises som faner i en kolonne til højre for den, og når du lukker kolonnen, får chatten sin fulde bredde tilbage." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Agent-Chats behält einen Agent-Chat als Hauptbereich: Links, Dateien, Subagenten und Neben-Chats, die er öffnet, erscheinen als Tabs in einer Spalte rechts daneben, und wenn du diese Spalte schließt, bekommt der Chat wieder seine volle Breite." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Chats de agente mantiene un chat de agente como panel principal: los enlaces, archivos, subagentes y chats laterales que abre aparecen como pestañas en una columna a su derecha, y al cerrar esa columna el chat recupera todo su ancho." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Discussions d’agent garde une discussion d’agent comme panneau principal : les liens, fichiers, sous-agents et discussions annexes qu’elle ouvre s’affichent en onglets dans une colonne à sa droite, et fermer cette colonne rend à la discussion toute sa largeur." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chat con agenti mantiene una chat con un agente come riquadro principale: link, file, subagenti e chat laterali che apre compaiono come schede in una colonna alla sua destra, e chiudendo quella colonna la chat torna a tutta larghezza." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "「エージェントチャット」では、エージェントチャットをメインペインとして保持します。チャットが開いたリンク、ファイル、サブエージェント、サイドチャットは右側のカラムにタブとして表示され、そのカラムを閉じるとチャットが全幅に戻ります。" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ការជជែកជាមួយភ្នាក់ងារ រក្សាការជជែកជាមួយភ្នាក់ងារជាផ្ទាំងមេ៖ តំណ ឯកសារ ភ្នាក់ងាររង និងការជជែកចំហៀងដែលវាបើក បង្ហាញជាថេបនៅក្នុងជួរឈរនៅខាងស្តាំរបស់វា ហើយការបិទជួរឈរនោះ ធ្វើឱ្យការជជែកត្រឡប់មកទទឹងពេញវិញ។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "에이전트 채팅은 에이전트 채팅을 기본 패널로 유지합니다. 채팅에서 연 링크, 파일, 하위 에이전트 및 사이드 채팅은 오른쪽 열에 탭으로 나타나며, 그 열을 닫으면 채팅이 다시 전체 너비가 됩니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Agentchatter beholder en agentchat som hovedruten: lenker, filer, underagenter og sidechatter den åpner, vises som faner i en kolonne til høyre for den, og når du lukker kolonnen, får chatten full bredde igjen." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Czaty z agentem zostawiają czat z agentem jako panel główny: linki, pliki, podagenci i czaty poboczne, które otworzy, pojawiają się jako karty w kolumnie po jego prawej stronie, a zamknięcie tej kolumny przywraca czatowi pełną szerokość." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chats de agente mantém um chat de agente como painel principal: links, arquivos, subagentes e chats laterais que ele abre aparecem como abas em uma coluna à direita, e fechar essa coluna devolve ao chat a largura total." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "«Чаты с агентами» оставляют чат с агентом основной панелью: ссылки, файлы, субагенты и побочные чаты, которые он открывает, появляются вкладками в колонке справа от него, а если закрыть эту колонку, чат снова займёт всю ширину." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แชทเอเจนต์จะคงแชทเอเจนต์ไว้เป็นแผงหลัก ลิงก์ ไฟล์ เอเจนต์ย่อย และแชทข้างเคียงที่แชทเปิดจะแสดงเป็นแท็บในคอลัมน์ทางขวา และเมื่อปิดคอลัมน์นั้น แชทจะกลับมาเต็มความกว้าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Ajan Sohbetleri, bir ajan sohbetini ana bölme olarak tutar: açtığı bağlantılar, dosyalar, alt ajanlar ve yan sohbetler sağındaki bir sütunda sekme olarak görünür; bu sütunu kapatmak sohbete tam genişliğini geri verir." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "«Чати з агентами» залишають чат з агентом основною панеллю: посилання, файли, субагенти й побічні чати, які він відкриває, з’являються вкладками в колонці праворуч від нього, а якщо закрити цю колонку, чат знову займе всю ширину." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Trò chuyện với tác tử giữ một cuộc trò chuyện với tác tử làm ngăn chính: liên kết, tệp, tác tử con và cuộc trò chuyện phụ mà nó mở sẽ hiện thành thẻ trong một cột bên phải, và đóng cột đó sẽ trả lại toàn bộ chiều rộng cho cuộc trò chuyện." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "“智能体聊天”会将智能体聊天保持为主窗格:它打开的链接、文件、子智能体和侧边聊天会以标签页形式显示在其右侧的一栏中,关闭该栏后聊天会恢复完整宽度。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "「代理程式聊天」會將代理程式聊天保持為主窗格:它打開的連結、檔案、子代理程式和側邊聊天會以分頁形式顯示在其右側的一欄中,關閉該欄後聊天會恢復完整寬度。" + } + } + } + }, + "settings.choice.mainPaneAgentChats": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Agent Chats" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "محادثات الوكلاء" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Chatovi s agentom" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Agentchats" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Agent-Chats" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Chats de agente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Discussions d’agent" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chat con agenti" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エージェントチャット" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ការជជែកជាមួយភ្នាក់ងារ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "에이전트 채팅" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Agentchatter" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Czaty z agentem" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chats de agente" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Чаты с агентами" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แชทเอเจนต์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Ajan Sohbetleri" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Чати з агентами" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Trò chuyện với tác tử" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "智能体聊天" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "代理程式聊天" + } + } + } + }, + "settings.choice.mainPaneOff": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Off" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إيقاف" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Isključeno" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Fra" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactivado" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivé" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattivato" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "オフ" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បិទ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "끔" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Av" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wyłączone" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desativado" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Выкл." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ปิด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kapalı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Вимк." + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tắt" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "关闭" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "關閉" + } + } + } + }, "settings.tabs.newTabNowPlaying": { "comment": "Settings: toggle that shows the Now Playing player (the music playing on this Mac) on the New Tab page.", "extractionState": "manual", diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/MainPaneSetting.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/MainPaneSetting.swift new file mode 100644 index 000000000000..d7e87a02f13f --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/MainPaneSetting.swift @@ -0,0 +1,21 @@ +/// Main pane layout (`layout.mainPane` in cmux.json, Leo 2026-10-20): a screen's agent chat is its +/// one main pane, and nothing opens into it. What the chat opens (a link, a file or folder, a +/// subagent's or a side chat's pane) is a tab in the column on its right; a chat alone on its screen +/// docks left the first time, so that column slides in beside it. Closing that column's last tab +/// gives the chat the whole width back. +/// +/// ```jsonc +/// "layout": { "mainPane": "agent-chats" } // "off" +/// ``` +public nonisolated enum MainPaneSetting: String, Hashable, Sendable, CaseIterable { + /// An agent chat is the main pane of its screen. The default. + case agentChats = "agent-chats" + /// No main pane: what a chat opens goes where `layout.linkPlacement` and the chat's own + /// rules put it (a tab in the chat's pane, a split on its right). + case off + + /// The cmux.json key. + public static let configPath = ["layout", "mainPane"] + /// ``agentChats`` when the key is unset or invalid. + public static let fallback: MainPaneSetting = .agentChats +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+PanePlacement.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+PanePlacement.swift index 2da359def1ba..0efca612fb8e 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+PanePlacement.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+PanePlacement.swift @@ -3,9 +3,10 @@ public import CmuxNextDesign /// Where new panes open (`CmuxConfigSnapshot+PanePlacement`), in the General /// section's Columns group. nonisolated enum PanePlacementSettingsSchema { - /// Agents may change all three: layout choices, like `layout.dockColumnMode`. + /// Agents may change them all: layout choices, like `layout.dockColumnMode`. static var agentSettableKeys: Set { - [CmuxConfigSnapshot.newPanePlacementPath, CmuxConfigSnapshot.tileBrowsersPath, LinkPlacement.configPath] + [CmuxConfigSnapshot.newPanePlacementPath, CmuxConfigSnapshot.tileBrowsersPath, LinkPlacement.configPath, + MainPaneSetting.configPath] .reduce(into: []) { $0.insert($1.joined(separator: ".")) } } @@ -45,6 +46,18 @@ nonisolated enum PanePlacementSettingsSchema { default: .string(LinkPlacement.fallback.rawValue), keywords: ["link", "url", "path", "file", "open", "split", "right", "column", "pane", "tab"] ), + SettingDescriptor( + MainPaneSetting.configPath, section: .general, group: columns, + title: SettingsText.keyed("settings.layout.mainPane", "Main Pane"), + help: SettingsText.keyed("settings.layout.mainPane.help", + "Agent Chats keeps an agent chat as the main pane: links, files, subagents and side chats it opens appear as tabs in a column on its right, and closing that column gives the chat its full width back."), + kind: .choice([ + SettingChoice(MainPaneSetting.agentChats.rawValue, SettingsText.keyed("settings.choice.mainPaneAgentChats", "Agent Chats")), + SettingChoice(MainPaneSetting.off.rawValue, SettingsText.keyed("settings.choice.mainPaneOff", "Off")), + ]), + default: .string(MainPaneSetting.fallback.rawValue), + keywords: ["main", "pane", "chat", "agent", "right", "column", "side", "link", "subagent", "tab"] + ), ] } } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/MainPaneLayoutTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/MainPaneLayoutTests.swift new file mode 100644 index 000000000000..10468ee55589 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/MainPaneLayoutTests.swift @@ -0,0 +1,64 @@ +@testable import CmuxNextApp +import CmuxNextDesign +import CmuxNextLayout +import Testing + +/// Main pane layout (`layout.mainPane`, Leo 2026-10-20): an agent chat is its screen's one main +/// pane and nothing opens into it. What it opens is a tab in the column on its right, the way a +/// person's new tab from the chat dock opens; a chat alone on its screen docks left first. Any +/// other pane keeps the usual rules. +@MainActor +struct MainPaneLayoutTests { + private static let chatDock = DockColumn(edge: .left, role: .agentChat) + private static let lone: @Sendable (LayoutColumn) -> Bool = { $0.id == "chat" } + + @Test func theDebugOverrideWinsOverTheSetting() { + #expect(MainPaneLayout.isOn(setting: true, override: .setting)) + #expect(!MainPaneLayout.isOn(setting: false, override: .setting)) + #expect(MainPaneLayout.isOn(setting: false, override: .on)) + #expect(!MainPaneLayout.isOn(setting: true, override: .off)) + #expect(MainPaneLayout.debugOverride.defaultValue == .setting, "the setting decides unless Debug Settings says otherwise") + } + + /// A chat alone on its screen docks left, and what it opened stays in its pane, on the right. + @Test func aLoneChatDocksSoWhatItOpensSlidesInOnItsRight() { + let columns = [LayoutColumn(id: "chat", root: .leaf("c"))] + #expect(MainPaneLayout.side(from: "c", columns: columns, isLoneChat: Self.lone) == .dockChat) + } + + /// Beside the chat dock, every open is a tab in the strip: in the pane the person used last, + /// else the first one, never in the chat. + @Test func besideTheChatDockOpensAreTabsOnTheRight() { + let columns = [LayoutColumn(id: "dock", root: .leaf("c"), dock: Self.chatDock), + LayoutColumn(id: "right", root: .leaf("r1")), LayoutColumn(id: "more", root: .leaf("r2"))] + #expect(MainPaneLayout.side(from: "c", columns: columns, isLoneChat: Self.lone) == .tab(in: "r1")) + #expect(MainPaneLayout.side(from: "c", columns: columns, recent: ["c", "r2", "r1"], isLoneChat: Self.lone) == .tab(in: "r2")) + } + + /// Only the chat is a main pane: a pane in the strip, a terminal alone, a chat sharing its screen + /// with other columns and not docked, and a zoomed screen keep the usual rules. + @Test func otherPanesKeepTheUsualRules() { + let docked = [LayoutColumn(id: "dock", root: .leaf("c"), dock: Self.chatDock), LayoutColumn(id: "right", root: .leaf("r1"))] + #expect(MainPaneLayout.side(from: "r1", columns: docked, isLoneChat: Self.lone) == nil) + let terminal = [LayoutColumn(id: "term", root: .leaf("t"))] + #expect(MainPaneLayout.side(from: "t", columns: terminal, isLoneChat: Self.lone) == nil) + let shared = [LayoutColumn(id: "chat", root: .leaf("c")), LayoutColumn(id: "term", root: .leaf("t"))] + #expect(MainPaneLayout.side(from: "c", columns: shared, isLoneChat: Self.lone) == nil) + let alone = [LayoutColumn(id: "chat", root: .leaf("c"))] + #expect(MainPaneLayout.side(from: "c", columns: alone, zoomed: true, isLoneChat: Self.lone) == nil) + } + + /// Home's pinned dock alone already opens beside itself (`pinned-dock-alone-v1`); with a column + /// beside it, its opens are tabs there like any chat dock's. + @Test func homesPinnedDockOpensBesideItself() { + let home = LayoutColumn(id: "home", root: .leaf("h"), dock: DockColumn(edge: .left, role: .agentChat, isPinned: true)) + #expect(MainPaneLayout.side(from: "h", columns: [home], isLoneChat: Self.lone) == nil) + #expect(MainPaneLayout.side(from: "h", columns: [home, LayoutColumn(id: "right", root: .leaf("r1"))], + isLoneChat: Self.lone) == .tab(in: "r1")) + } + + /// The Debug override is listed, so Debug Settings shows it and `debug.tunables` can set it. + @Test func theOverrideIsInDebugSettings() { + #expect(TunableCatalog.all.contains { $0.key == MainPaneLayout.debugOverride.key }) + } +} diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/MainPaneSettingTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/MainPaneSettingTests.swift new file mode 100644 index 000000000000..38adac1288d6 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextSettingsTests/MainPaneSettingTests.swift @@ -0,0 +1,35 @@ +import CmuxNextSettings +import Testing + +/// `layout.mainPane`: an agent chat is its screen's main pane by default (Leo 2026-10-20); "off" +/// turns that off. Agents may set it (a layout choice, like `layout.linkPlacement`). +struct MainPaneSettingTests { + private static func parse(_ value: JSONValue?) -> CmuxConfigSnapshot { + let root: JSONValue = value.map { .object(["layout": .object(["mainPane": $0])]) } ?? .object([:]) + return CmuxConfigSnapshot.parse(root, validDensities: [], validMetrics: []) + } + + @Test func agentChatsWhenUnset() { + let snapshot = Self.parse(nil) + #expect(snapshot.mainPane == .agentChats) + #expect(snapshot.diagnostics.isEmpty) + } + + @Test(arguments: [("agent-chats", MainPaneSetting.agentChats), ("off", .off)]) + func eachChoiceLoads(raw: String, expected: MainPaneSetting) { + #expect(Self.parse(.string(raw)).mainPane == expected) + } + + @Test func aBadValueKeepsTheDefaultAndSaysWhy() { + let snapshot = Self.parse(.bool(false)) + #expect(snapshot.mainPane == .agentChats) + #expect(snapshot.diagnostics.contains { $0.path == "layout.mainPane" }) + } + + @Test func theRowMatchesTheAppAndAgentsMaySetIt() throws { + let row = try #require(SettingsSchema.descriptor(for: ["layout", "mainPane"])) + #expect(row.defaultValue == .string("agent-chats")) + #expect(SettingsSchema.agentSettableKeys.contains("layout.mainPane")) + #expect(SettingsSchema.agentRefusedKeys["layout.mainPane"] == nil) + } +} diff --git a/release-notes/next/highlights/agent-chat-main-pane.md b/release-notes/next/highlights/agent-chat-main-pane.md new file mode 100644 index 000000000000..4d77e29aa036 --- /dev/null +++ b/release-notes/next/highlights/agent-chat-main-pane.md @@ -0,0 +1,4 @@ +title: Your agent chat stays put while things open beside it +category: new + +Links, files, subagents and side chats an agent chat opens appear as tabs in one column on its right, and closing that column gives the chat its full width back. Turn it off in Settings > General > Main Pane. From c7fa6ea04765d305d656b520e152cfb677d8972b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 00:01:06 -0700 Subject: [PATCH 2/9] cmux-next media: main pane tour (lone chat docks for a link, tabs join the right column, close gives full width) Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- scripts/cmux-next/media/tours/main-pane.json | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 scripts/cmux-next/media/tours/main-pane.json diff --git a/scripts/cmux-next/media/tours/main-pane.json b/scripts/cmux-next/media/tours/main-pane.json new file mode 100644 index 000000000000..0df314c5e5f2 --- /dev/null +++ b/scripts/cmux-next/media/tours/main-pane.json @@ -0,0 +1,29 @@ +{ + "title": "Main pane", + "settings": {"layout": {"mainPane": "agent-chats"}}, + "steps": [ + {"title": "Launch", "wait": 2}, + {"title": "New workspace", "cmux": ["workspace", "new", "--name", "Main"], "shot": false}, + {"title": "An agent chat docks left, the terminal sits on its right", "cmux": ["agent", "new-chat"], "wait": 5, + "optional": "agent backend unavailable on this runner"}, + {"title": "Focus the terminal column", "cmux": ["action", "run", "focusRight"], "shot": false}, + {"title": "Closing the right column gives the chat its full width", "cmux": ["action", "run", "closePane"], "wait": 3, + "optional": "closing the terminal asked to confirm"}, + {"title": "Focus the chat", "cmux": ["action", "run", "focusLeft"], "shot": false}, + {"title": "A link clicked in the lone chat: the chat docks left and the page slides in on its right", + "rpc": {"method": "debug.terminal.links", "params": {"agentLink": "https://example.com"}}, "wait": 4, + "optional": "debug.terminal.links is DEBUG only"}, + {"title": "Focus the chat again", "cmux": ["action", "run", "focusLeft"], "shot": false}, + {"title": "A second link opens as a tab in the same right column", + "rpc": {"method": "debug.terminal.links", "params": {"agentLink": "https://example.org"}}, "wait": 4, + "optional": "debug.terminal.links is DEBUG only"}, + {"title": "Closing the right column again: the chat is full width", "cmux": ["action", "run", "closePane"], "wait": 3}, + {"title": "Main pane off (Debug override): a link from the chat opens in the chat's own pane", + "rpc": {"method": "debug.tunables", "params": {"action": "set", "key": "panes.mainPane", "value": "off"}}, "shot": false, + "optional": "debug.tunables is DEBUG only"}, + {"title": "Focus the chat", "cmux": ["action", "run", "focusLeft"], "shot": false}, + {"title": "With main pane off, the link is a tab in the chat's pane", + "rpc": {"method": "debug.terminal.links", "params": {"agentLink": "https://example.net"}}, "wait": 4, + "optional": "debug.terminal.links is DEBUG only"} + ] +} From c67aab2bb2f5e34000b33e3392e9031271b230c5 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 00:09:46 -0700 Subject: [PATCH 3/9] release notes: main pane highlight gets a New Agent Chat try-it action Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- release-notes/next/highlights/agent-chat-main-pane.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes/next/highlights/agent-chat-main-pane.md b/release-notes/next/highlights/agent-chat-main-pane.md index 4d77e29aa036..af6857ac6a14 100644 --- a/release-notes/next/highlights/agent-chat-main-pane.md +++ b/release-notes/next/highlights/agent-chat-main-pane.md @@ -1,4 +1,5 @@ title: Your agent chat stays put while things open beside it category: new +action: palette.newAgentChat | New Agent Chat Links, files, subagents and side chats an agent chat opens appear as tabs in one column on its right, and closing that column gives the chat its full width back. Turn it off in Settings > General > Main Pane. From 5ed37ecab72f0945ba559a12dc88e2aeb57911c3 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 00:19:44 -0700 Subject: [PATCH 4/9] cmux-next: escape the quotes in the main pane Debug help string The unescaped "Follow the setting" ended the string literal, so CmuxNextLayout did not compile. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- .../CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift index ad4c70fe1d7d..ac630507fe91 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/MainPaneLayout.swift @@ -11,7 +11,7 @@ public nonisolated struct MainPaneLayout { /// Debug Settings' override of `layout.mainPane`. public static let debugOverride = Tunable.choice( "panes.mainPane", .panes, "Main pane layout", - help: "An agent chat is its screen's main pane: what it opens (links, files, subagents, side chats) is a tab in the column on its right. "Follow the setting" uses layout.mainPane in cmux.json; On and Off override it.", + help: "An agent chat is its screen's main pane: what it opens (links, files, subagents, side chats) is a tab in the column on its right. \"Follow the setting\" uses layout.mainPane in cmux.json; On and Off override it.", default: .setting, code: "MainPaneLayout.debugOverride") /// Whether main pane layout is on: the Debug override, else the setting (`agentChats`). From bf31888d9f2a8f8ee3431cf134383121ad24d12f Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 00:24:56 -0700 Subject: [PATCH 5/9] cmux-next: main pane keeps focus on agent opens, finishes after the dock, keeps chats in the dock - An agent's open in the right column selects its tab without moving keyboard focus (selectWhenReportedKeepingFocus), as AgentBesidePlacement does. - A lone chat's open reports finished only once the chat is listed in its dock, so a queued second open lands as a tab in the same column instead of a split. - A past chat picked on a New Tab page in the chat dock replaces the page in the dock again; main pane routing applies to terminals, browsers and links only. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- .../AgentRecents/ChatsOpenCoordinator.swift | 4 +- .../Handlers/MainPaneLayout+Routing.swift | 66 ++++++++++++------- .../CmuxNextApp/Links/LinkOpener.swift | 11 ++-- .../Sources/CmuxNextApp/NewTabPage+Open.swift | 7 +- 4 files changed, 54 insertions(+), 34 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentRecents/ChatsOpenCoordinator.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentRecents/ChatsOpenCoordinator.swift index c1e63cdeca29..70f6c8c96378 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentRecents/ChatsOpenCoordinator.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentRecents/ChatsOpenCoordinator.swift @@ -152,9 +152,9 @@ final class ChatsOpenCoordinator { // A page of another machine never becomes a chat of this Mac (cx-k21v): the chat opens in a // new workspace of this Mac instead, as before open-here. guard pane.daemon.isLocal else { return false } - if NewTabPage.opensBeside(page, in: pane) { + if NewTabPage.opensBeside(page, in: pane, mainPane: false) { let closePage = NewTabPage.closingPage(page, in: pane) - return services.linkOpener.open(from: pane) { [services] target, placed in + return services.linkOpener.open(from: pane, mainPane: false) { [services] target, placed in services.agentTabs.openTab(in: target, seed: AgentPaneSeedSource(seed), select: placed == nil) { key in if let placed, let surface = services.locateTab(key)?.0.surface { placed(surface) } closePage() diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift index c05c3c509387..89a69bb1f0b7 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift @@ -29,11 +29,17 @@ extension MainPaneLayout { /// The lone chat in `controller`'s pane docks left with the agent chat role; the tab on /// `surface`, just opened there unselected, stays and is selected, so its pane becomes the /// column on the chat's right. With `focus` (a person's click) the tab takes keyboard focus; - /// else focus follows the chat into its dock when the chat had it (an agent's open stays in - /// the background). A failed dock selects the tab where it is, so it never stays hidden. - @MainActor static func dockChat(of controller: PaneController, keeping surface: SurfaceID, focus: Bool, services: AppServices) { + /// else no focus moves to it, and focus follows the chat into its dock when the chat had it + /// (an agent's open stays in the background). A failed dock selects the tab where it is, so it + /// never stays hidden. `then` hears whether the chat docked, once the store lists it in its + /// dock, so a later open sees the column on its right. + @MainActor static func dockChat(of controller: PaneController, keeping surface: SurfaceID, focus: Bool, services: AppServices, + then: (@MainActor (Bool) -> Void)? = nil) { let store = controller.daemon.store, anchor = controller.pane let chatHadFocus = controller.workspace?.focusedPane === controller + let reveal: @MainActor (PaneController, TabModel) -> Void = { pane, tab in + if focus { pane.select(StripTabID(tab.id)) } else { pane.selectWhenReportedKeepingFocus(surface: surface) } + } services.registry.track(Task { @MainActor [weak controller] () -> ActionWorkFailure? in let listed = Task { @MainActor () -> TabModel? in if let tab = store.tab(surface: surface) { return tab } @@ -47,17 +53,22 @@ extension MainPaneLayout { bound.schedule(after: listLimit) { @MainActor in listed.cancel() } let found = await listed.value bound.cancel() - guard let tab = found, let controller else { return "main pane: the new tab did not open" } + guard let tab = found, let controller else { + then?(false) + return "main pane: the new tab did not open" + } guard let chat = anchor.tabs.first(where: { $0.id != tab.id && ChatColumnPlacement.isChat($0, services: services) }) else { - controller.select(StripTabID(tab.id)) + reveal(controller, tab) + then?(false) return nil } let docked = await withCheckedContinuation { (done: CheckedContinuation) in TabMoves.toNewDockColumn(chat, anchor: anchor, edge: .left, role: .agentChat, services: services) { done.resume(returning: $0) } } - controller.select(StripTabID(tab.id)) + reveal(controller, tab) guard docked else { if focus { controller.focusContent() } + then?(false) return "main pane: the chat did not dock (see the app log)" } // A hidden tab bar was the chat's choice: the pane it left shows its tab bar again. @@ -65,12 +76,19 @@ extension MainPaneLayout { services.paneTabBars.set(nil, for: controller.paneKey) controller.apply(controller.snapshot()) } - guard let content = controller.workspace else { return nil } - if focus { - PaneHandlers.focus(controller.layoutPaneID, in: content) - } else if chatHadFocus { - await followChat(chat.id, from: anchor, in: content, services: services) + let dock = await movedPane(of: chat.id, from: anchor, services: services) + if let content = controller.workspace { + if focus { + PaneHandlers.focus(controller.layoutPaneID, in: content) + } else if chatHadFocus, let dock { + // Not a view change of the open: the chat moved out of the focused pane, and focus follows + // it back to where the person had it (FocusCoordinator.followMovedTab does so for a move). + ActionRunScope.carrying(ActionRunScope(origin: .script, allowsViewChange: true)) { + PaneHandlers.focus(LayoutPaneID(dock.id), in: content) + } + } } + then?(true) return nil }) } @@ -106,20 +124,25 @@ extension MainPaneLayout { } created?(tab.key) if target === controller { - dockChat(of: target, keeping: tab.surface, focus: focus, services: services) - } else if let model = services.locateTab(tab.key)?.0 { + // `finished` once the chat is in its dock: a queued open then sees the column on its right. + dockChat(of: target, keeping: tab.surface, focus: focus, services: services) { _ in finished?(true) } + return + } + if focus, let model = services.locateTab(tab.key)?.0 { target.select(StripTabID(model.id)) - if focus, let content = target.workspace { PaneHandlers.focus(target.layoutPaneID, in: content) } + if let content = target.workspace { PaneHandlers.focus(target.layoutPaneID, in: content) } + } else { + // An agent's open: selected in the column, keyboard focus stays where it is. + target.selectWhenReportedKeepingFocus(surface: tab.surface) } finished?(true) } return true } - /// Focuses the pane chat tab `id` moved into from `anchor` once the store lists it there, as - /// ``AgentBesidePlacement`` does when a lone chat docks for an agent's tab. - @MainActor private static func followChat(_ id: String, from anchor: PaneModel, in content: WorkspaceContentController, - services: AppServices) async { + /// The pane chat tab `id` moved into from `anchor`, once the store lists it there (nil after + /// ``listLimit``). + @MainActor private static func movedPane(of id: String, from anchor: PaneModel, services: AppServices) async -> PaneModel? { let moved = Task { @MainActor () -> Bool in for await moved in ObservationStream({ services.locateTab(id).map { $0.1 !== anchor } ?? false }) where moved { return true @@ -130,11 +153,6 @@ extension MainPaneLayout { bound.schedule(after: listLimit) { @MainActor in moved.cancel() } let found = await moved.value bound.cancel() - guard found, let pane = services.locateTab(id)?.1 else { return } - // Not a view change of the open: the chat moved out of the focused pane, and focus follows - // it back to where the person had it (FocusCoordinator.followMovedTab does so for a move). - ActionRunScope.carrying(ActionRunScope(origin: .script, allowsViewChange: true)) { - PaneHandlers.focus(LayoutPaneID(pane.id), in: content) - } + return found ? services.locateTab(id)?.1 : nil } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpener.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpener.swift index ba45eb633278..fca059c257e7 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpener.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Links/LinkOpener.swift @@ -64,8 +64,8 @@ final class LinkOpener { /// A source that takes no tabs (Home's pinned dock) finds the pane on its right by the layout's /// column order (``LinkOpenPlacement/paneRight(of:columns:takesTabs:recency:)``), not by drawn /// frames, which miss a strip column that is scrolled or not laid out yet. - func target(from source: PaneController, cmdClick: Bool = false) -> LinkOpenPlacement.Target { - if let services, let side = MainPaneLayout.side(from: source, services: services) { + func target(from source: PaneController, cmdClick: Bool = false, mainPane: Bool = true) -> LinkOpenPlacement.Target { + if mainPane, let services, let side = MainPaneLayout.side(from: source, services: services) { switch side { case .tab(let pane): return .pane(pane) case .dockChat: return .dockChat(source.layoutPaneID) @@ -88,11 +88,12 @@ final class LinkOpener { } /// Opens through `make` where the rule puts it. `background`: the opened tab takes no focus. - /// False with no source pane (no window) or when `make` refused. + /// `mainPane` false: main pane layout does not apply (a chat, which belongs in the dock, not + /// the column on its right). False with no source pane (no window) or when `make` refused. @discardableResult - func open(from source: PaneController?, background: Bool = false, cmdClick: Bool = false, _ make: Make) -> Bool { + func open(from source: PaneController?, background: Bool = false, cmdClick: Bool = false, mainPane: Bool = true, _ make: Make) -> Bool { guard let source = source ?? services?.windows.active?.focusedPane else { return false } - switch target(from: source, cmdClick: cmdClick) { + switch target(from: source, cmdClick: cmdClick, mainPane: mainPane) { case .pane(let id): return make(id == source.layoutPaneID ? source : source.workspace?.panes[id] ?? source, nil) case .splitRight: diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage+Open.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage+Open.swift index 110d95e41fbb..731b207d8a74 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage+Open.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTabPage+Open.swift @@ -139,11 +139,12 @@ extension NewTabPage { /// Whether what New Tab page `key` in `pane` opens goes beside it: ``LinkOpener`` puts it beside /// the page (the Debug toggle `links.pagesFollowPlacement` on, the link placement not Same Pane), the page shares its pane /// with another tab (a lone page has nothing to keep in view, so it is replaced in place), and it is no split's (a split's page, which lists Open - /// Tabs, fills the pane the person just made, NewTabPage.split). - static func opensBeside(_ key: String, in pane: PaneController) -> Bool { + /// Tabs, fills the pane the person just made, NewTabPage.split). `mainPane` false for a chat pick: + /// main pane layout keeps chats out of the column right of the chat dock (NewChatPlacement). + static func opensBeside(_ key: String, in pane: PaneController, mainPane: Bool = true) -> Bool { let tabs = pane.services.agentTabs, page = tabs.resolve(key) return tabs.newTabPages[page]?.page.openTabs == nil && pane.pane.tabs.contains(where: { $0.id != page }) - && pane.services.linkOpener.target(from: pane) != .pane(pane.layoutPaneID) + && pane.services.linkOpener.target(from: pane, mainPane: mainPane) != .pane(pane.layoutPaneID) } /// ``replace(_:with:cwd:in:keep:)`` beside the page: the terminal or browser opens through From ea6e682446d2b4c52ea085afb53f773f992386e9 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 02:10:16 -0700 Subject: [PATCH 6/9] chore(next): PR autofix (regenerated settings exports from run 39569875874) Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- docs/mdm/com.manaflow.cmux.json | 36 ++- docs/mdm/com.manaflow.cmux.plist | 65 ++++- docs/mdm/managed-preferences.md | 7 +- schemas/defaults/cmux-next.json | 9 +- .../settings-schema.sources.sha256 | 30 +- schemas/settings/settings-schema.json | 273 +++++++++++++++++- 6 files changed, 400 insertions(+), 20 deletions(-) diff --git a/docs/mdm/com.manaflow.cmux.json b/docs/mdm/com.manaflow.cmux.json index 8b187c7775b8..f87b6c5329a0 100644 --- a/docs/mdm/com.manaflow.cmux.json +++ b/docs/mdm/com.manaflow.cmux.json @@ -2388,6 +2388,16 @@ "title" : "Open Links and Files", "type" : "string" }, + "layout.mainPane" : { + "default" : "agent-chats", + "description" : "Agent Chats keeps an agent chat as the main pane: links, files, subagents and side chats it opens appear as tabs in a column on its right, and closing that column gives the chat its full width back.", + "enum" : [ + "agent-chats", + "off" + ], + "title" : "Main Pane", + "type" : "string" + }, "layout.minimumPaneHeight" : { "default" : 64, "description" : "layout.minimumPaneHeight", @@ -2672,6 +2682,18 @@ "title" : "Pulse Duration", "type" : "number" }, + "notifications.attention.flash" : { + "default" : true, + "description" : "Clicking an unread pane, typing in it or jumping to it flashes its ring as it goes.", + "title" : "Flash the Pane When Read", + "type" : "boolean" + }, + "notifications.attention.flashOnTyping" : { + "default" : true, + "description" : "notifications.attention.flashOnTyping", + "title" : "Flash When Typing Reads It", + "type" : "boolean" + }, "notifications.attention.moveToTop" : { "default" : false, "description" : "A notification moves its workspace to the top of the unpinned rows, unless you are looking at its pane.", @@ -3082,6 +3104,12 @@ "title" : "Keep Mac Awake While Agents Work", "type" : "string" }, + "sessions.promptSuggestions" : { + "default" : false, + "description" : "Agents can suggest next tasks, which you start as child chats beside the current one. Agents started after the change get it.", + "title" : "Suggest Follow-Up Tasks", + "type" : "boolean" + }, "sessions.resumeAgents" : { "default" : true, "description" : "Terminal agents reopen their session after a crash or restart.", @@ -4021,6 +4049,12 @@ "title" : "Swap Cmd-T and Cmd-N", "type" : "boolean" }, + "tabs.terminalFirst" : { + "default" : false, + "description" : "When on, Cmd-T, Cmd-D and Cmd-Shift-D open a terminal, as in cmux classic, instead of the New Tab page.", + "title" : "New Tabs and Splits Open a Terminal", + "type" : "boolean" + }, "tabs.toolbarButtons" : { "default" : [ @@ -4288,7 +4322,7 @@ "type" : "boolean" }, "workspaces.newPlacement" : { - "default" : "top", + "default" : "afterCurrent", "description" : "Where Cmd-N, the + button and the CLI put a new workspace in the sidebar. Pinned workspaces stay above it.", "enum" : [ "top", diff --git a/docs/mdm/com.manaflow.cmux.plist b/docs/mdm/com.manaflow.cmux.plist index 2141557b6996..8b54a7e8482a 100644 --- a/docs/mdm/com.manaflow.cmux.plist +++ b/docs/mdm/com.manaflow.cmux.plist @@ -203,6 +203,18 @@ pfm_type string + + pfm_default + + pfm_description + When on, Cmd-T, Cmd-D and Cmd-Shift-D open a terminal, as in cmux classic, instead of the New Tab page. + pfm_name + tabs.terminalFirst + pfm_title + New Tabs and Splits Open a Terminal + pfm_type + boolean + pfm_default afterCurrent @@ -782,6 +794,18 @@ pfm_type boolean + + pfm_default + + pfm_description + Agents can suggest next tasks, which you start as child chats beside the current one. Agents started after the change get it. + pfm_name + sessions.promptSuggestions + pfm_title + Suggest Follow-Up Tasks + pfm_type + boolean + pfm_default @@ -1140,7 +1164,24 @@ pfm_default - top + agent-chats + pfm_description + Agent Chats keeps an agent chat as the main pane: links, files, subagents and side chats it opens appear as tabs in a column on its right, and closing that column gives the chat its full width back. + pfm_name + layout.mainPane + pfm_range_list + + agent-chats + off + + pfm_title + Main Pane + pfm_type + string + + + pfm_default + afterCurrent pfm_description Where Cmd-N, the + button and the CLI put a new workspace in the sidebar. Pinned workspaces stay above it. pfm_name @@ -6775,6 +6816,28 @@ pfm_type boolean + + pfm_default + + pfm_description + Clicking an unread pane, typing in it or jumping to it flashes its ring as it goes. + pfm_name + notifications.attention.flash + pfm_title + Flash the Pane When Read + pfm_type + boolean + + + pfm_default + + pfm_name + notifications.attention.flashOnTyping + pfm_title + Flash When Typing Reads It + pfm_type + boolean + pfm_default diff --git a/docs/mdm/managed-preferences.md b/docs/mdm/managed-preferences.md index b26a86b382cf..e51e04d1e579 100644 --- a/docs/mdm/managed-preferences.md +++ b/docs/mdm/managed-preferences.md @@ -26,6 +26,7 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `app.startAgentGlobalHotKey` | boolean | `false` | | Start Agent from Any App. Start Agent (⌃⌥⌘Space) works while another app is in front. Change its key in Keyboard Shortcuts. | | `app.hideDockIconWithoutWindows` | boolean | `true` | | Hide Dock Icon When No Windows Are Open. cmux keeps running in the menu bar, out of the Dock and the app switcher, until a window opens. | | `tabs.newTabTemplate` | string | `"cards"` | `cards`, `default`, `composer`, `threads`, `console`, `classic`, `terminal` | New Tab Template. The layout of the New Tab page. Terminal skips the page and opens a terminal. The dots at the bottom of the page also change it. | +| `tabs.terminalFirst` | boolean | `false` | | New Tabs and Splits Open a Terminal. When on, Cmd-T, Cmd-D and Cmd-Shift-D open a terminal, as in cmux classic, instead of the New Tab page. | | `tabs.newTabPosition` | string | `"afterCurrent"` | `afterCurrent`, `end`, `start` | New Tab Position. Where Cmd-T and the + button put the new tab. New Tab to the Right in a tab's menu always puts it after that tab. | | `tabs.plusButton` | string | `"hover"` | `hover`, `always` | New Tab Button. On Hover shows each tab bar's + only while the pointer is over that tab bar. | | `tabs.barPosition` | string | `"top"` | `top`, `bottom` | Tab Bar Position. Where each pane's tab bar sits. Bottom also shows the standard title bar, so the window buttons never cover a pane. | @@ -65,6 +66,7 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `sessions.keepAwake` | string | `"pluggedIn"` | `off`, `pluggedIn`, `always` | Keep Mac Awake While Agents Work. Stops idle sleep while an agent on this Mac is working. Your display can still turn off, and closing the lid still puts the Mac to sleep. | | `sessions.resumeAgents` | boolean | `true` | | Resume Agents After Restart. Terminal agents reopen their session after a crash or restart. | | `sessions.allowFullAccess` | boolean | `true` | | Allow Full-Access Modes. Off, cmux won't switch an agent into a mode that skips approvals. | +| `sessions.promptSuggestions` | boolean | `false` | | Suggest Follow-Up Tasks. Agents can suggest next tasks, which you start as child chats beside the current one. Agents started after the change get it. | | `phone.allowConnections` | boolean | `false` | | Let Your Phone Use This Mac. So your phone can find this Mac, cmux registers with its servers this Mac's name, device ID and platform, the app's bundle ID, version and build channel, the Mac's relay address and connection key, and your account and team IDs. Workspace names, folders, full paths and terminal output go only over the encrypted link to your phone. None of that, and no branch name or repo URL, is sent to cmux servers. Exception: while “Show This Mac's Workspaces on Your Phone While It's Off” is on, cmux servers store each workspace's ID, resource ID, short ID, name, name revision, group, color, icon, pinned state, unread mark, unread count and whether it is active. | | `phone.startSessions` | boolean | `true` | | Let Your Phone Start New Workspaces. Off, your phone can still follow, type into and answer what's already running. | | `phone.keepAwake` | string | `"pluggedIn"` | `off`, `pluggedIn`, `always` | Keep Mac Awake for Your Phone. A sleeping Mac can't be reached from your phone until it wakes. Closing the lid still puts the Mac to sleep. | @@ -89,7 +91,8 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `layout.newPanePlacement` | string | `"tab"` | `tab`, `split` | New Terminals and Browsers. Open a Tab adds a tab to the focused pane. Split Automatically splits the largest pane, like New Pane (Auto Layout). | | `layout.tileBrowsers` | boolean | `false` | | Split for Browsers Too. With Split Automatically, new browsers also get their own pane instead of a tab. | | `layout.linkPlacement` | string | `"split-right"` | `split-right`, `new-column`, `same-pane` | Open Links and Files. Where a link or file you Command-click in a terminal or a browser page appears. Split Right uses the pane on the right, or makes one. What you open from an agent chat, Home or another page opens as a tab in its own pane. | -| `workspaces.newPlacement` | string | `"top"` | `top`, `afterCurrent`, `bottom` | New Workspace Position. Where Cmd-N, the + button and the CLI put a new workspace in the sidebar. Pinned workspaces stay above it. | +| `layout.mainPane` | string | `"agent-chats"` | `agent-chats`, `off` | Main Pane. Agent Chats keeps an agent chat as the main pane: links, files, subagents and side chats it opens appear as tabs in a column on its right, and closing that column gives the chat its full width back. | +| `workspaces.newPlacement` | string | `"afterCurrent"` | `top`, `afterCurrent`, `bottom` | New Workspace Position. Where Cmd-N, the + button and the CLI put a new workspace in the sidebar. Pinned workspaces stay above it. | | `sidebar.groupByComputer` | boolean | `false` | | Group Workspaces by Computer. Off: one list of workspaces, and a workspace on another computer names it under its title. On: a section per computer. | | `workspaces.autoName` | boolean | `true` | | Name Workspaces from Tabs. A workspace you have not named shows its front tab's name: a page's title, an agent chat's title, or a terminal's title or folder. Your own name always wins. | | `palette.scopes.tabs.prefix` | string | `"@"` | `@`, `#`, `>`, `,`, `?`, `!`, `/`, `;`, `:`, `%`, `&`, `+`, `=`, `~`, `$`, `^`, `*`, `.`, `none` | Tabs Prefix. Typed into an empty query, this character enters the scope. A prefix you assign moves from any other scope. | @@ -480,6 +483,8 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `notifications.attention.blinkCount` | real | `2` | 1 to 10 | Blinks | | `notifications.attention.duration` | real | `3` | 0.3 to 30 | Pulse Duration | | `notifications.attention.persist` | boolean | `true` | | Keep Ring Until Read | +| `notifications.attention.flash` | boolean | `true` | | Flash the Pane When Read. Clicking an unread pane, typing in it or jumping to it flashes its ring as it goes. | +| `notifications.attention.flashOnTyping` | boolean | `true` | | Flash When Typing Reads It | | `notifications.attention.showOnTab` | boolean | `true` | | Mark the Tab | | `notifications.attention.showOnSidebar` | boolean | `true` | | Mark the Sidebar Row | | `notifications.attention.moveToTop` | boolean | `false` | | Move the Workspace to the Top. A notification moves its workspace to the top of the unpinned rows, unless you are looking at its pane. | diff --git a/schemas/defaults/cmux-next.json b/schemas/defaults/cmux-next.json index eb6eab862231..940d7823e73c 100644 --- a/schemas/defaults/cmux-next.json +++ b/schemas/defaults/cmux-next.json @@ -110,6 +110,7 @@ "key.markdownBack": ["cmd+["], "key.markdownForward": ["cmd+]"], "key.markdownLink": ["shift+cmd+k"], + "key.markdownReload": ["cmd+r"], "key.markdownSave": ["cmd+s"], "key.markdownZoomIn": ["cmd+=", "shift+cmd+="], "key.markdownZoomOut": ["cmd+-"], @@ -187,6 +188,7 @@ "key.selectWorkspaceByNumber": ["cmd+1", "cmd+2", "cmd+3", "cmd+4", "cmd+5", "cmd+6", "cmd+7", "cmd+8", "cmd+9"], "key.showBrowserJavaScriptConsole": ["opt+cmd+j"], "key.showHideAllWindows": ["ctrl+opt+cmd+."], + "key.sidebar.priorityView.toggle": ["opt+cmd+k"], "key.simulatorHome": ["shift+cmd+h"], "key.simulatorRotateLeft": ["cmd+left"], "key.simulatorRotateRight": ["cmd+right"], @@ -479,6 +481,7 @@ "setting.layout.focusFollowsMouse": false, "setting.layout.frameOrientation": "columnMajor", "setting.layout.linkPlacement": "split-right", + "setting.layout.mainPane": "agent-chats", "setting.layout.minimumPaneHeight": 64, "setting.layout.minimumPaneWidth": 200, "setting.layout.newColumnWidth": "matchCurrent", @@ -514,6 +517,8 @@ "setting.notifications.attention.blinkCount": 2, "setting.notifications.attention.color": null, "setting.notifications.attention.duration": 3, + "setting.notifications.attention.flash": true, + "setting.notifications.attention.flashOnTyping": true, "setting.notifications.attention.moveToTop": false, "setting.notifications.attention.persist": true, "setting.notifications.attention.showOnSidebar": true, @@ -548,6 +553,7 @@ "setting.screenshots.folders": [], "setting.sessions.allowFullAccess": true, "setting.sessions.keepAwake": "pluggedIn", + "setting.sessions.promptSuggestions": false, "setting.sessions.resumeAgents": true, "setting.shortcuts.showModifierHoldHints": true, "setting.sidebar.border": false, @@ -686,6 +692,7 @@ "setting.tabs.overflow": "shrink", "setting.tabs.plusButton": "hover", "setting.tabs.swapCmdTAndCmdN": false, + "setting.tabs.terminalFirst": false, "setting.tabs.toolbarButtons": [], "setting.tasks.layout": "inbox", "setting.terminal.fontFamily": null, @@ -720,7 +727,7 @@ "setting.window.titlebarButtons": "hover", "setting.window.trafficLightClearance": 72, "setting.workspaces.autoName": true, - "setting.workspaces.newPlacement": "top", + "setting.workspaces.newPlacement": "afterCurrent", "steps-aside.yieldsToCodeEditor": ["all common vscode shortcuts", "focusDown", "focusLocation", "focusUp", "globalSearch", "groupSelectedWorkspaces", "moveSurfaceToPaneDown", "moveSurfaceToPaneLeft", "moveSurfaceToPaneRight", "moveSurfaceToPaneUp", "openBrowser", "palette.newAgentChat", "space.next", "space.previous", "splitRight", "toggleChecklistItemComplete", "toggleSplitZoom"], "steps-aside.yieldsToTerminal": ["decreaseWorkspaceTerminalFontSize", "focusHistoryBack", "focusHistoryForward", "groupSelectedWorkspaces", "increaseWorkspaceTerminalFontSize", "resetWorkspaceTerminalFontSize", "toggleChecklistItemComplete"], "steps-aside.yieldsToWebContent": ["toggleChecklistItemComplete"] diff --git a/schemas/settings-export/settings-schema.sources.sha256 b/schemas/settings-export/settings-schema.sources.sha256 index 6fef22a38cbf..75cb222bf452 100644 --- a/schemas/settings-export/settings-schema.sources.sha256 +++ b/schemas/settings-export/settings-schema.sources.sha256 @@ -84,7 +84,7 @@ ced8d9799e2aac9ea39e2ef08c18a9708715d078c55fbb7c7c87282904ef238e Packages/macOS c408fa0fbf9350339b7f54fb396a8dae2a05a6ed908b65fa6f575ced4f3fa107 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/ClassicPreferenceImport.swift -d56f2677c6534c16e56094ea30e0b8568547ce1db7e896c39caaf19bcafec6d7 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/ClassicSettingsMigration.swift +55526d222583049ad41720532e2dbfd9590cb85163f186a521d667cf160d42ae Packages/macOS/CmuxNext/Sources/CmuxNextSettings/ClassicSettingsMigration.swift bb31840d982f631839a11ce93ce3d33bb29464d7543a627c874a2df27cb8ddc9 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/ClassicShortcutImport.swift @@ -106,13 +106,13 @@ cff26dd51f8b6d7c2f4b4c38f228f895c5d80b2e192f8ff4192e346f06a2e94a Packages/macOS 08a9d94df2e9bee2a3007288d302ca311fbd2e060faa46f1edad684b0499e0e3 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+CommandRetention.swift -aff231edf1f015ea7ab937c22b627b92695f122d1bf36941eba314577ca34c11 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+PanePlacement.swift +f1060c27c2fb0d5e4cde39468d9840752c41d31a8ccb3e3d52390e0036254dad Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+PanePlacement.swift 6c8a49ed50c6653d99674fa8175dd133313008944d45741b0a757fae55c3d68b Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+PreviewFeatures.swift 5cefeeca69c1e55286af8c8c492c53ef32d11a8e5c37f79bb0893cd6a2641109 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+Shortcuts.swift -8bc72938630daf0f771e05193e3ff43a617ffaad5b99c45a18e84614183f92a4 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift +9b86b73907cfc9b5ab3876c9a885aa4bca3993ee9581fc2ad8e0ec5066c6db69 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift 6fb2ce5c119eb17ef80ce163deacdfbe5253ae35d88a0a0171f9cf419868fc31 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/ColorTransitionSetting.swift @@ -200,7 +200,9 @@ f6190cfeca4fa8b6e7a8fc495646e4f1febdbf801e79cac5030ae811aa5e28c1 Packages/macOS 69d7e7fe1d80c27c5ca44664c5b145d3151b9f3374f7c1885d8d66168c2ffdf4 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/LinkRules/BrowserExternalURLWildcardToken.swift -bfc6cde37f73bb298911aa23e98660484bfd87764a6e84a1ecd66b2a1ec43194 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings +7a7d5116ae696756895ce6080274302806a1351a3b6c5ce9d335172dd6c50d35 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings + +e9de7365e47f67dee0d803eb974aa58404888670588b0064be42b523dc463ffa Packages/macOS/CmuxNext/Sources/CmuxNextSettings/MainPaneSetting.swift ff3c704da19b51b151a7686af2299ae672d5c20ab9a1070b954cb5f7729c14ac Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Managed/Chrome/CFChromePolicyReader.swift @@ -282,7 +284,7 @@ a7185ea3111e2ae7a95a32cf7149a2027bbb95a3014d0066c1390f3847c8db09 Packages/macOS e27c139e32300a840adc4af02efa48b635158a8cf21a6d5065663ded1d7aa9dc Packages/macOS/CmuxNext/Sources/CmuxNextSettings/PaneFocusSettings.swift -2cc86c85435246b89c6823c6bb375f4592bca6ce9eb03b8a2ca0e525f16ef774 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/PaneRingConfigParser.swift +2795b1bdd78edf3df163ea8724df718cee019d4ec49a76cdd82938fdd9e28ede Packages/macOS/CmuxNext/Sources/CmuxNextSettings/PaneRingConfigParser.swift 8ef2fecd01f7a74806da29fde090020860c7ce8e451c8f5d38eb51c88a519de8 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/PaneToolbarButtonsSetting.swift @@ -346,7 +348,7 @@ ea1a09694740863848c15a76d98f32e0bfe1b3a6d6ab47711beb665d8e2030b6 Packages/macOS d7fe84ad82b8f7b7b23a09bcc743546a79eef64765f19dcb5f0618d318bc3ec0 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsPageLayout.swift -5797682d6312fe4ee2cc6c4cd9d7d8de4e5d0576a55183fb4165562dd0241f47 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+AgentPolicy.swift +b7bd8dd281cc7a35ef1f58e97979836ad14f64ea5999e017e5e744bb8cfa0215 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+AgentPolicy.swift c719db63465c7d1e7c457f0ec14d56ea3a6765fb9b8ca89f2048ab682470861f Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift @@ -372,13 +374,13 @@ e88319190f1a93f49e9da53a50e5e3399175e86e913a10fa4796e6e4f680cfad Packages/macOS d89ce88a0100a5d3dc925c80cd29db157343cc83998709ee5ccc1ea786323626 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Motion.swift -c94656228126013198462bd2aedcf840cf8e710a1a0381522fb4bdca08f59e98 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Notifications.swift +74891e99bc3284935a7a4098a4f0b5d3fcae11076036567eb7ed2453672351fe Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Notifications.swift a70330e4778a94ab881f5ea72cf8262395df15bd5af8b110abdf4897064f20e3 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+NowPlaying.generated.swift 3217212d118342cda697da39cfc4ff9d7ce9fc00177cd7a4040e9ac535632c77 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Palette.swift -04e26b33cac19eda19aa002c3c57e363c125083530b27b800e89ae0b7293f146 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+PanePlacement.swift +1fe6282ebd4e5b4ac2a6397d7b01d3c71f150cc0e26d3c7962a1d7a4af858252 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+PanePlacement.swift 0490475edd3744bb45ca99ac7ba7be39f7decf49de7fb5ee8566a08df79cd165 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Picker.generated.swift @@ -388,7 +390,7 @@ b1bbecb3bf92c6d816c4b5873244c97f3777fa524d2fa7aeb0904d1cf72b44cd Packages/macOS d475ef9c2c02e07fc077689d915c72ed90060003b7b45abde9ce406bf70c94c4 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Screenshots.generated.swift -626c0527d37a2ded2174bb7486f5d645652c5c97b230e0df33daf5db61056e4c Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Sessions.generated.swift +c1f52a2daa14cffa6203498aeba8e3af7797bdd4b8ddfe362a3249ddcd843952 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Sessions.generated.swift 512502b539b88853343bd858c7a6dfa2e33e116d49b895d502978493b834a517 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+SessionsChoices.generated.swift @@ -402,7 +404,7 @@ f28a2b07d1f86ffb7fdc6d8db416addf757c1181738ab429b3cc769e69255db3 Packages/macOS 915a6bf414f60297f2002e8e29607626f5abdc4e2ba030b05274b36e51973f56 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Surfaces.swift -2d144716e7e0e998edd841e9638d1ee01a89f13a2966e4da6e00c8b2416aba98 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Tabs.swift +06a0b1ea73fbdc48a1856624666a723a08b31618ad557c1e05618cd845c19ee0 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Tabs.swift 6cc54a35729a75a0bf43c37085ddcf1a56f7dd9f839afd4c2ae5acd27cd925e1 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Tasks.generated.swift @@ -416,7 +418,7 @@ d1000bfaa3a4bd923179dec223590ab559d316a2d9b2e75bf4a788a42c4ce968 Packages/macOS a7de0b99aae42c97fac914714fe8eabb24be884f64dc273eefe25cd1c45df4e5 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+UpdatesChoices.generated.swift -e4134b954d812eb44e560db0f5f72440f71f59900f6320471e8debe1baab8204 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema.swift +c0ce3651199ae4b17548bd224e83f2f395c9e1ea4dff6b61a58121a9e61f28de Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema.swift 3c27682d0f77572f7c991e2fc005119195b23e936a807b3f740843e4ff9b1fee Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchemaExport.swift @@ -432,7 +434,7 @@ edb6f168fff82bd0e3446ea193035fbd46fed24d1f5cf4fd8d3f3386d44c4b15 Packages/macOS b2facb7c55956a9de0ee28e63c355f71f4626bdba422c5d93521a35c9d9dc21f Packages/macOS/CmuxNext/Sources/CmuxNextSettings/ScreenshotsSettings.swift -8fa0e099a8439018b85265287d1e66626d428c98b424f9d4a8182278cd79fed8 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SessionSettings.swift +9f075fb6a506ee48d562531d350d2de49b9d01f27a94addd0fc31dbdc6337e21 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SessionSettings.swift 1608b082818e425b0b9879cc3ffe3a94c0b0aa9f7fc537a82d85f31e68ec87d8 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SettingChoiceActions.swift @@ -502,6 +504,8 @@ d95aaa8304c5d2deb2645696fda48aa9185b6330d7455e18fc544bae9abfe574 Packages/macOS f250d69842f057b084a992f7c2a898102b3a158d377dca482228d2bf461896b1 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/TerminalCommandHistorySetting.swift +d126c23d38a3159854e1bf51d7f700218034239330883b8f80e821603e455981 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/TerminalFirstSetting.swift + 6dfb3967b2785278873918dfb32cf65ce7b1a2562a0dd782fe13e9fca059f8fd Packages/macOS/CmuxNext/Sources/CmuxNextSettings/TerminalFontSetting.swift b9e5c809edbbaddfebf0d25ceac5f604430353a996102c090abe9106e435e2fa Packages/macOS/CmuxNext/Sources/CmuxNextSettings/TextEditorSetting.swift @@ -518,7 +522,7 @@ eeaa759cf23060cfb60ff8a87d0fa2603403bbfbc39ee9ca3b0baab5eeae2db0 Packages/macOS 2dd09e4cbd4f71815ed6d99e4c2fc0ed5f835c793abc3ca7c873fbbc1e430711 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/WindowTitlebarSetting.swift -69ba0ebb0ea56cc8e870876e8f0980a088ca02a6b63e998a6dc25400aaa6d7cd Packages/macOS/CmuxNext/Sources/CmuxNextSettings/WorkspaceListSettings.swift +5a18bcf68c7c66978888da1510802842bfdd3f7dfb6dcf5e69f15f38eacc7027 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/WorkspaceListSettings.swift 75af8de711538515b7c8042cb31fe1f83c82913d215d00de67b5196e4216e07b Packages/macOS/CmuxNext/Sources/CmuxNextSettings/WorkspaceRowSetting.swift diff --git a/schemas/settings/settings-schema.json b/schemas/settings/settings-schema.json index 68e13a70b94a..0121cf5bc413 100644 --- a/schemas/settings/settings-schema.json +++ b/schemas/settings/settings-schema.json @@ -29,6 +29,7 @@ "key" : "settings.group.tabs", "rows" : [ "tabs.newTabTemplate", + "tabs.terminalFirst", "tabs.newTabPosition", "tabs.plusButton", "tabs.barPosition", @@ -67,7 +68,8 @@ "layout.minimumPaneHeight", "layout.newPanePlacement", "layout.tileBrowsers", - "layout.linkPlacement" + "layout.linkPlacement", + "layout.mainPane" ], "title" : { "key" : "settings.group.columns", @@ -784,7 +786,8 @@ "rows" : [ "sessions.keepAwake", "sessions.resumeAgents", - "sessions.allowFullAccess" + "sessions.allowFullAccess", + "sessions.promptSuggestions" ], "title" : { "key" : "settings.group.sessions", @@ -913,6 +916,8 @@ "notifications.attention.blinkCount", "notifications.attention.duration", "notifications.attention.persist", + "notifications.attention.flash", + "notifications.attention.flashOnTyping", "notifications.attention.showOnTab", "notifications.attention.showOnSidebar", "notifications.attention.moveToTop" @@ -2131,6 +2136,57 @@ }, "validation" : "portable" }, + { + "accepts" : [ + true, + false + ], + "agent_refusal" : null, + "agent_settable" : true, + "consumers" : [ + "cmux-next" + ], + "default" : false, + "default_label" : null, + "group" : { + "key" : "settings.group.tabs", + "text" : "Tabs" + }, + "help" : { + "key" : "settings.tabs.terminalFirst.help", + "text" : "When on, Cmd-T, Cmd-D and Cmd-Shift-D open a terminal, as in cmux classic, instead of the New Tab page." + }, + "kept_on_reset_all" : false, + "key" : "tabs.terminalFirst", + "keywords" : [ + "terminal", + "classic", + "new tab", + "split", + "cmd-t", + "cmd-d", + "new tab page", + "default" + ], + "kind" : "toggle", + "page_hidden" : false, + "path" : [ + "tabs", + "terminalFirst" + ], + "refuses" : [ + "true", + 1, + null + ], + "section" : "general", + "summary" : null, + "title" : { + "key" : "settings.tabs.terminalFirst", + "text" : "New Tabs and Splits Open a Terminal" + }, + "validation" : "portable" + }, { "accepts" : [ "afterCurrent", @@ -4491,6 +4547,55 @@ }, "validation" : "portable" }, + { + "accepts" : [ + true, + false + ], + "agent_refusal" : "userOnly", + "agent_settable" : false, + "consumers" : [ + "cmux-next" + ], + "default" : false, + "default_label" : null, + "group" : { + "key" : "settings.group.sessions", + "text" : "Agent Sessions" + }, + "help" : { + "key" : "settings.sessions.promptSuggestions.help", + "text" : "Agents can suggest next tasks, which you start as child chats beside the current one. Agents started after the change get it." + }, + "kept_on_reset_all" : false, + "key" : "sessions.promptSuggestions", + "keywords" : [ + "suggestions", + "prompt", + "follow-up", + "tasks", + "child chats", + "subagents" + ], + "kind" : "toggle", + "page_hidden" : false, + "path" : [ + "sessions", + "promptSuggestions" + ], + "refuses" : [ + "true", + 1, + null + ], + "section" : "general", + "summary" : null, + "title" : { + "key" : "settings.sessions.promptSuggestions", + "text" : "Suggest Follow-Up Tasks" + }, + "validation" : "portable" + }, { "accepts" : [ true, @@ -5885,6 +5990,75 @@ }, "validation" : "portable" }, + { + "accepts" : [ + "agent-chats", + "off" + ], + "agent_refusal" : null, + "agent_settable" : true, + "choices" : [ + { + "title" : { + "key" : "settings.choice.mainPaneAgentChats", + "text" : "Agent Chats" + }, + "value" : "agent-chats" + }, + { + "title" : { + "key" : "settings.choice.mainPaneOff", + "text" : "Off" + }, + "value" : "off" + } + ], + "consumers" : [ + "cmux-next" + ], + "default" : "agent-chats", + "default_label" : null, + "group" : { + "key" : "settings.group.columns", + "text" : "Columns" + }, + "help" : { + "key" : "settings.layout.mainPane.help", + "text" : "Agent Chats keeps an agent chat as the main pane: links, files, subagents and side chats it opens appear as tabs in a column on its right, and closing that column gives the chat its full width back." + }, + "kept_on_reset_all" : false, + "key" : "layout.mainPane", + "keywords" : [ + "main", + "pane", + "chat", + "agent", + "right", + "column", + "side", + "link", + "subagent", + "tab" + ], + "kind" : "choice", + "page_hidden" : false, + "path" : [ + "layout", + "mainPane" + ], + "refuses" : [ + "__not_a_choice__", + 1, + true + ], + "section" : "general", + "summary" : null, + "title" : { + "key" : "settings.layout.mainPane", + "text" : "Main Pane" + }, + "validation" : "portable" + }, { "accepts" : [ "top", @@ -5919,7 +6093,7 @@ "consumers" : [ "cmux-next" ], - "default" : "top", + "default" : "afterCurrent", "default_label" : null, "group" : { "key" : "settings.group.sidebar", @@ -32278,6 +32452,99 @@ }, "validation" : "portable" }, + { + "accepts" : [ + true, + false + ], + "agent_refusal" : null, + "agent_settable" : false, + "consumers" : [ + "cmux-next" + ], + "default" : true, + "default_label" : null, + "group" : { + "key" : "settings.group.attention", + "text" : "Attention Ring" + }, + "help" : { + "key" : "settings.attention.flash.help", + "text" : "Clicking an unread pane, typing in it or jumping to it flashes its ring as it goes." + }, + "kept_on_reset_all" : false, + "key" : "notifications.attention.flash", + "keywords" : [ + "ring", + "flash", + "blink", + "unread" + ], + "kind" : "toggle", + "page_hidden" : false, + "path" : [ + "notifications", + "attention", + "flash" + ], + "refuses" : [ + "true", + 1, + null + ], + "section" : "notifications", + "summary" : null, + "title" : { + "key" : "settings.attention.flash", + "text" : "Flash the Pane When Read" + }, + "validation" : "portable" + }, + { + "accepts" : [ + true, + false + ], + "agent_refusal" : null, + "agent_settable" : false, + "consumers" : [ + "cmux-next" + ], + "default" : true, + "default_label" : null, + "group" : { + "key" : "settings.group.attention", + "text" : "Attention Ring" + }, + "help" : null, + "kept_on_reset_all" : false, + "key" : "notifications.attention.flashOnTyping", + "keywords" : [ + "ring", + "flash", + "keystroke", + "unread" + ], + "kind" : "toggle", + "page_hidden" : false, + "path" : [ + "notifications", + "attention", + "flashOnTyping" + ], + "refuses" : [ + "true", + 1, + null + ], + "section" : "notifications", + "summary" : null, + "title" : { + "key" : "settings.attention.flashOnTyping", + "text" : "Flash When Typing Reads It" + }, + "validation" : "portable" + }, { "accepts" : [ true, From 1f386e59ce312b7a389e6cc7e0ad071323250a4b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 04:28:31 -0700 Subject: [PATCH 7/9] chore(next): PR autofix (regenerated settings exports from run 39588693869) Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- docs/mdm/com.manaflow.cmux.json | 34 +- docs/mdm/com.manaflow.cmux.plist | 55 +++- docs/mdm/managed-preferences.md | 5 +- schemas/defaults/cmux-next.json | 3 + .../settings-schema.sources.sha256 | 38 +-- schemas/settings/settings-schema.json | 290 ++++++++++++++++-- 6 files changed, 380 insertions(+), 45 deletions(-) diff --git a/docs/mdm/com.manaflow.cmux.json b/docs/mdm/com.manaflow.cmux.json index f87b6c5329a0..2b91212ec64b 100644 --- a/docs/mdm/com.manaflow.cmux.json +++ b/docs/mdm/com.manaflow.cmux.json @@ -47,6 +47,16 @@ "title" : "Chat Full Width", "type" : "boolean" }, + "agentPane.display.inspector.placement" : { + "default" : "overlay", + "description" : "Where the Inspector opens at the right of the chat. Push narrows the chat beside it; Overlay lies over the chat.", + "enum" : [ + "push", + "overlay" + ], + "title" : "Inspector", + "type" : "string" + }, "agentPane.display.width" : { "default" : 800, "description" : "The widest the chat's column gets. On a wider pane the chat stays centered.", @@ -2297,6 +2307,15 @@ "title" : "Character", "type" : "string" }, + "home.server" : { + "default" : "hosted", + "description" : "Direct messages and group chats use cmux Cloud by default. A self-hosted server is reached over Tailscale by its MagicDNS name, for example http://mini.tail1234.ts.net:7320. No password is sent: the server knows you by your Tailscale login. Also accepts a dictionary {\"selfHosted\": \"http(s)://host[:port]\"} in a raw profile.", + "enum" : [ + "hosted" + ], + "title" : "Server", + "type" : "string" + }, "labs.classicHome" : { "default" : false, "description" : "Show Home as a full page instead of a workspace with the Chief docked on the left. Takes effect after cmux restarts.", @@ -2807,6 +2826,17 @@ "title" : "Quiet Hours", "type" : "object" }, + "notifications.showPreviews" : { + "default" : "whenUnlocked", + "description" : "Whether a banner shows what the agent said, or only where and what state.", + "enum" : [ + "always", + "whenUnlocked", + "never" + ], + "title" : "Show Message in Banners", + "type" : "string" + }, "notifications.sound" : { "default" : "default", "description" : "notifications.sound", @@ -3411,8 +3441,8 @@ }, "sidebar.showChats" : { "default" : true, - "description" : "Shows every coding agent chat on this computer, newest first, at the bottom of the sidebar.", - "title" : "Show All Chats", + "description" : "On the New Tab page, the Threads and Console styles list every coding agent chat on this computer, newest first. Off, they show the recent chats only.", + "title" : "Show All Chats on New Tab", "type" : "boolean" }, "sidebar.showProfileControl" : { diff --git a/docs/mdm/com.manaflow.cmux.plist b/docs/mdm/com.manaflow.cmux.plist index 8b54a7e8482a..7cd97337d84a 100644 --- a/docs/mdm/com.manaflow.cmux.plist +++ b/docs/mdm/com.manaflow.cmux.plist @@ -3466,6 +3466,23 @@ pfm_type real + + pfm_default + overlay + pfm_description + Where the Inspector opens at the right of the chat. Push narrows the chat beside it; Overlay lies over the chat. + pfm_name + agentPane.display.inspector.placement + pfm_range_list + + push + overlay + + pfm_title + Inspector + pfm_type + string + pfm_description A monospaced font installed on this Mac. @@ -4212,11 +4229,11 @@ pfm_default pfm_description - Shows every coding agent chat on this computer, newest first, at the bottom of the sidebar. + On the New Tab page, the Threads and Console styles list every coding agent chat on this computer, newest first. Off, they show the recent chats only. pfm_name sidebar.showChats pfm_title - Show All Chats + Show All Chats on New Tab pfm_type boolean @@ -6410,6 +6427,22 @@ pfm_type boolean + + pfm_default + hosted + pfm_description + Direct messages and group chats use cmux Cloud by default. A self-hosted server is reached over Tailscale by its MagicDNS name, for example http://mini.tail1234.ts.net:7320. No password is sent: the server knows you by your Tailscale login. Also accepts a dictionary {"selfHosted": "http(s)://host[:port]"} in a raw profile. + pfm_name + home.server + pfm_range_list + + hosted + + pfm_title + Server + pfm_type + string + pfm_default imessage @@ -6697,6 +6730,24 @@ pfm_type string + + pfm_default + whenUnlocked + pfm_description + Whether a banner shows what the agent said, or only where and what state. + pfm_name + notifications.showPreviews + pfm_range_list + + always + whenUnlocked + never + + pfm_title + Show Message in Banners + pfm_type + string + pfm_default diff --git a/docs/mdm/managed-preferences.md b/docs/mdm/managed-preferences.md index e51e04d1e579..e95285207de4 100644 --- a/docs/mdm/managed-preferences.md +++ b/docs/mdm/managed-preferences.md @@ -231,6 +231,7 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `agentPane.display.font.lineHeight` | real | `1.625` | 1.2 to 2.4 | Chat Line Height | | `agentPane.display.code.family` | string | | | Chat Code Font. The font of code in the chat. Empty uses the Markdown viewer's code font, then the terminal font. | | `agentPane.display.code.scale` | real | | 0.6 to 1.25 | Chat Code Size. Code as a share of the chat's text size. Empty uses the Markdown viewer's. | +| `agentPane.display.inspector.placement` | string | `"overlay"` | `push`, `overlay` | Inspector. Where the Inspector opens at the right of the chat. Push narrows the chat beside it; Overlay lies over the chat. | | `terminal.fontFamily` | string | | | Font Family. A monospaced font installed on this Mac. | | `terminal.fontSize` | real | | 4 to 96 | Font Size | | `editor.fontFamily` | string | | | Font Family. Empty uses the terminal font. | @@ -286,7 +287,7 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `sidebar.bottomBandMaxShare` | real | `0.25` | 0.1 to 0.9 | Bottom Sections Height. The share of the sidebar the bottom sections fill before they scroll. | | `sidebar.pinnedBandsScroll` | boolean | `true` | | Scroll Tall Sections. Off: the top and bottom sections never scroll and the workspace list gets smaller. | | `sidebar.showWorkspaceTabs` | boolean | `false` | | Show Workspace Tabs. Lists tabs beneath each workspace in the sidebar. | -| `sidebar.showChats` | boolean | `true` | | Show All Chats. Shows every coding agent chat on this computer, newest first, at the bottom of the sidebar. | +| `sidebar.showChats` | boolean | `true` | | Show All Chats on New Tab. On the New Tab page, the Threads and Console styles list every coding agent chat on this computer, newest first. Off, they show the recent chats only. | | `sidebar.sessionTree` | boolean | `false` | | Session Tree. Lists a workspace that another workspace's agent started under it, and shows each row's pull requests and the first rows' Command-number keys. | | `sidebar.showProjects` | boolean | `true` | | Show Projects | | `sidebar.groupBy` | string | `"none"` | `none`, `folder` | Group Projects By | @@ -456,6 +457,7 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `browser.passwords.generate` | boolean | `true` | | Suggest Strong Passwords. Offers a generated password when a site asks for a new one, and saves it. | | `browser.passwords.allowExport` | boolean | `false` | | Allow Password Export. Lets the Passwords page write every saved password to a plain-text CSV file, after you confirm with Touch ID or your password. | | `browser.verificationAutofill.enabled` | boolean | `false` | | Fill Codes from Verification Emails. When a site emails you a code or link, its tab offers it. Only on the sender's own site, never filled without your click, and never stored. | +| `home.server` | string | `"hosted"` | `hosted` | Server. Direct messages and group chats use cmux Cloud by default. A self-hosted server is reached over Tailscale by its MagicDNS name, for example http://mini.tail1234.ts.net:7320. No password is sent: the server knows you by your Tailscale login. Also accepts a dictionary {"selfHosted": "http(s)://host[:port]"} in a raw profile. | | `home.chiefBubbleStyle` | string | `"imessage"` | `imessage`, `theme` | Bubble Style. iMessage shows your messages in blue and the Chief's in gray. Theme uses your theme's accent and surface colors. | | `home.attachments.keepLocation` | boolean | `false` | | Keep Location in Photos and Videos. When off, location data is removed from photos and videos before they are attached. | | `home.channelsEnabled` | boolean | `false` | | Channels. Show Channels below Home in the sidebar. You can also turn it on or off from the command palette. | @@ -474,6 +476,7 @@ Files: `com.manaflow.cmux.plist` (ProfileManifests: iMazing Profile Editor, Prof | `status.runNotifyWhenVisible` | boolean | `false` | | Notify Even When the Terminal Is Visible | | `notifications.dockBadge` | boolean | `true` | | Unread Count on Dock Icon | | `notifications.dockBounce` | string | `"once"` | `off`, `once`, `untilActive` | Bounce Dock Icon. When a notification alerts while cmux isn't the active app. | +| `notifications.showPreviews` | string | `"whenUnlocked"` | `always`, `whenUnlocked`, `never` | Show Message in Banners. Whether a banner shows what the agent said, or only where and what state. | | `feed.mirrorNotifications.agents` | boolean | `true` | | Copy Agent Notifications to the Feed. Notifications from agents and cmux notify go to your cmux account's feed and can reach your iPhone. | | `feed.mirrorNotifications.terminal` | string | `"off"` | `off`, `title`, `full` | Copy Terminal Notifications to the Feed. Notifications that programs send through the terminal can contain secrets. Off sends nothing to your cmux account. | | `feed.agentPermissionPrompts` | boolean | `false` | | Send Agent Permission Requests to the Feed. Each request's tool and command summary goes to your own cmux feed, so your iPhone can answer it. | diff --git a/schemas/defaults/cmux-next.json b/schemas/defaults/cmux-next.json index 940d7823e73c..c5c1be91b573 100644 --- a/schemas/defaults/cmux-next.json +++ b/schemas/defaults/cmux-next.json @@ -230,6 +230,7 @@ "setting.agentPane.display.font.preset": "markdown", "setting.agentPane.display.font.size": 14, "setting.agentPane.display.fullWidth": false, + "setting.agentPane.display.inspector.placement": "overlay", "setting.agentPane.display.width": 800, "setting.agentPane.editedFiles.maxRows": 5, "setting.agentPane.editedFiles.scope": "turn", @@ -470,6 +471,7 @@ "setting.home.chiefBubbleStyle": "imessage", "setting.home.chiefPetEnabled": false, "setting.home.chiefPetMascot": "orb", + "setting.home.server": "hosted", "setting.labs.classicHome": false, "setting.labs.previewFeatures": false, "setting.layout.centerFocusedColumn": "never", @@ -531,6 +533,7 @@ "setting.notifications.dockBounce": "once", "setting.notifications.mutedWorkspaces": [], "setting.notifications.quietHours": null, + "setting.notifications.showPreviews": "whenUnlocked", "setting.notifications.sound": "default", "setting.notifications.sources.agent.dismissal": null, "setting.notifications.sources.cli.dismissal": null, diff --git a/schemas/settings-export/settings-schema.sources.sha256 b/schemas/settings-export/settings-schema.sources.sha256 index 75cb222bf452..78d92b5a0dbe 100644 --- a/schemas/settings-export/settings-schema.sources.sha256 +++ b/schemas/settings-export/settings-schema.sources.sha256 @@ -112,7 +112,7 @@ f1060c27c2fb0d5e4cde39468d9840752c41d31a8ccb3e3d52390e0036254dad Packages/macOS 5cefeeca69c1e55286af8c8c492c53ef32d11a8e5c37f79bb0893cd6a2641109 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot+Shortcuts.swift -9b86b73907cfc9b5ab3876c9a885aa4bca3993ee9581fc2ad8e0ec5066c6db69 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift +3b9a9e33a5551940e97f5edd9864dd97f68888677d22f854c3d7c8698acb4416 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/CmuxConfigSnapshot.swift 6fb2ce5c119eb17ef80ce163deacdfbe5253ae35d88a0a0171f9cf419868fc31 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/ColorTransitionSetting.swift @@ -162,7 +162,7 @@ e774bf33b3a64a7cab27d31edf68ffbd0acb5cd5e2a881dc12c801d5f314402b Packages/macOS 9eee926e52b8c6718c732fd71b3d0af05db9ea252d5422edece34a22d98636af Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HideDockIconSetting.swift -478f8061a3f0dba8f9c45735f9b225e6ff5c449f4997c80503d994acb63c580b Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HomeChannelsSetting.swift +96be78bffde2db38f244a13f3ef7ea319b9e35284d9dee9b8838dabcf14e6408 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HomeChannelsSetting.swift b91146520ff2f02051e0aff15667608044b5a390f44b0c8f9842e249b7f1b337 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HomeChiefBubbleStyleSetting.swift @@ -170,6 +170,8 @@ ff3a5b0e5389f96e5bddebb79f1843820eac55f5e331dfba9fc0e0898543e1f1 Packages/macOS 289d06d409505ee7e35631bf8c8c44f8e759b3e4ee2d537b9937075c605d9ba8 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HomeKeepLocationSetting.swift +54f4b23550b6c0ac0e22efcf4b524b1ae004eb834db83ba11ab82e1f209cf88e Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HomeServerSetting.swift + 83f88735839c4190d9cebd97220398282f5899062e1b59c6f4b199d64d3f5c7d Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings+Options.swift ae856418ce22482ef4262df8fb664e18e76cc91a98c72a19e6feb55f70ea4541 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/HoverCardSettings.swift @@ -200,7 +202,7 @@ f6190cfeca4fa8b6e7a8fc495646e4f1febdbf801e79cac5030ae811aa5e28c1 Packages/macOS 69d7e7fe1d80c27c5ca44664c5b145d3151b9f3374f7c1885d8d66168c2ffdf4 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/LinkRules/BrowserExternalURLWildcardToken.swift -7a7d5116ae696756895ce6080274302806a1351a3b6c5ce9d335172dd6c50d35 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings +888089004db06781f7752dbb76565d00144a7550efdf24d281cd1a09f6bb7e69 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Localizable.xcstrings e9de7365e47f67dee0d803eb974aa58404888670588b0064be42b523dc463ffa Packages/macOS/CmuxNext/Sources/CmuxNextSettings/MainPaneSetting.swift @@ -234,7 +236,7 @@ b5ba79bfadd6205e87514b79ba0aba58f104f065c2bbf53754530b9f42791458 Packages/macOS d6f335e1bf68e3ca65b74d881783d92b89a568f03e7cca24a3a77689c2c46ca6 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Managed/ManagedPreferencesManifest+Docs.swift -9c9028d64666ce0a0c11fa1565235ff4c8acc1260b1f682d319b66aa9ac7254f Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Managed/ManagedPreferencesManifest.swift +497a9fd3a49494736305fd0b011fc9825d806c4b6f20f1694c2f256765f4ddaf Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Managed/ManagedPreferencesManifest.swift c3e2254383bd31b1bf95f4a9a355ad02686463f22bc06ea41d3ca338984ccde5 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Managed/ManagedStatusReport.swift @@ -260,13 +262,13 @@ b236a2f9079c27318a4d54f86c95ea7a8253a103d24d11433375372b7600d216 Packages/macOS abf3127556a9de512a7e2858c7b56811b336dd991aa01789ea2dcf7ec1190e35 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/NewTerminalWorkspaceSetting.swift -4448d64ec314c406588edc6620c5c4afaa33fa6a43273fbbdb1014f415881e16 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/NotificationConfigParser.swift +3390949b978ce5dfe0c5703e8d6c539a9cfb24de1196c33c62a415376d35816f Packages/macOS/CmuxNext/Sources/CmuxNextSettings/NotificationConfigParser.swift 1108376131466adde6a89c2110b5d57f75fc77c1cc41061dc1e27814456bad17 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/NotificationModes.swift 37e6fae5a0f758bda1d5322fae9b45e0a66404c6d49370fa4035c040427c5fda Packages/macOS/CmuxNext/Sources/CmuxNextSettings/NotificationPolicy.swift -6aaa83a774f471cb15e7fe3adf4fb7538cc76af96ee5857122b6c25a22264c8d Packages/macOS/CmuxNext/Sources/CmuxNextSettings/NotificationPreferences.swift +7fced1c245187c265e45beb0fca6ee0ce704cd71e6b08ebf2d5830133c6f6ca7 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/NotificationPreferences.swift a9611156320b923ccd7504e408c9f327693312550f8b921d0e8d4d16178095d1 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/NotificationSource.swift @@ -302,7 +304,7 @@ ecc9448b1fb4b6db84b2375609de9dbe5f7854df0d4d11694fd7f637e667b471 Packages/macOS 772028c002f8875159bad86d5a87333cde82a4255bf1b48c73a6b6b03707e67b Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/AgentDefaultsSettingsSchema.swift -2e13c4f5047002a983991ca618dc370b47e6754f5421511f5e6903b8e6289af4 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/AgentPaneDisplaySettingsSchema.swift +07a2f899f77de526c45de4b018ecbfe1a6b1c6c706dcff4003732c5cfe79faa4 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/AgentPaneDisplaySettingsSchema.swift 1d38e78897625bcc58e2c286290f5af812ff03f5c5790a02db97f70d4293555c Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/AgentPaneEditedFilesSettingsSchema.swift @@ -318,7 +320,7 @@ f988f030daf49836ae0a32c4e5e8e8578e70c7ec35e114542452ccce985bbfd7 Packages/macOS e36a5c327fb10a60497bdfebd32c71ca655869150d9d6adf0fd2142822d84a97 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/EditorSettingsSchema.swift -c6f02355b837d0b72a2e4113c99da3e3e1d66688e2a5aa6fed437eb2a8902b6b Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/HomeSettingsSchema.swift +59819fcc3af6cccd568260ad0b21f46b9e729efb4612376662d90ee613e9fd2e Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/HomeSettingsSchema.swift 04540c79683d2f0c04ebba420b4a8719207b2c31af0fcdb0bf72977470cc8995 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/LinkRoutingSchema.swift @@ -328,27 +330,27 @@ bbdf5e4e8565cb907e76e1549d1fe64fe8b292c319041bac56348d0260753ac1 Packages/macOS 248d3a951164f84913a28cf0f13cab02297e5bc59c40e9e679c015a90b40dc41 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/OmnibarLookSettingsSchema.swift -b00d00ce7f89b335fa7fa83281c81c211ff99cf5f385fe5632afe4f479740588 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/OmnibarSettingsSchema.swift +e0cbd039a6c1573166d0853d6234053acd8bb1a435303c9e06689108dfe0de48 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/OmnibarSettingsSchema.swift 0c4e69978ddcc4433225e66801ce535e5cf8af3d86506d334f448a58f60a6b35 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingChoice.swift 483ef6fa1b59acf98caaec2efc0f891c97fa98463142ffd9be61549005e8ba89 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingConsumer.swift -b6a266c6ee8d9d37b5b5daedc48f15d41ddd86e289daf49617ac1f018941e513 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingDescriptor+Palette.swift +84f4b37884ec27c42bfd9e6788f06ea166f132d348fe60b840c497d8c426556a Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingDescriptor+Palette.swift -3f7fa17e14d04d2a428c3b98a30ae42eda99614e48cfd3fb6dc7a1922a5bdc0e Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingDescriptor+Values.swift +80bff990b7cc13a3ab5601bc17a0993d0a7c93a2e6de09020aa783e0107d3b40 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingDescriptor+Values.swift 0d04361df539d15a2629b40c11ed2a86e7c0c97a5936e4658234cab98e16a4be Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingDescriptor.swift -ea1a09694740863848c15a76d98f32e0bfe1b3a6d6ab47711beb665d8e2030b6 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingKind.swift +f0a70bd364a692fcccba585446dccd913cc20f865c5ab44f9d212bb9d6767fac Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingKind.swift 7a10cb53958ac6950522b10e71c2cfd402f10696d204caa3854ac3990ad2f326 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingPaletteExposure.swift 3df34a09959f7b325f9e8450944ee5d92410df990ed26b3a0ea09bd88f360b54 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingText.swift -d7fe84ad82b8f7b7b23a09bcc743546a79eef64765f19dcb5f0618d318bc3ec0 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsPageLayout.swift +856e17d9e490636596a1140f84c446327b5fa0eba8ab3eca561cc8c64fe50f5f Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsPageLayout.swift -b7bd8dd281cc7a35ef1f58e97979836ad14f64ea5999e017e5e744bb8cfa0215 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+AgentPolicy.swift +a6798c81e556234c550071721b4a1df92bd74905d11fd916432666734558fed5 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+AgentPolicy.swift c719db63465c7d1e7c457f0ec14d56ea3a6765fb9b8ca89f2048ab682470861f Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Appearance.swift @@ -374,7 +376,7 @@ e88319190f1a93f49e9da53a50e5e3399175e86e913a10fa4796e6e4f680cfad Packages/macOS d89ce88a0100a5d3dc925c80cd29db157343cc83998709ee5ccc1ea786323626 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Motion.swift -74891e99bc3284935a7a4098a4f0b5d3fcae11076036567eb7ed2453672351fe Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Notifications.swift +cb9bffc6f2c9c0223ec0f5a62a4ec64d0cbdb74605b30d990cffc602a6861f7e Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+Notifications.swift a70330e4778a94ab881f5ea72cf8262395df15bd5af8b110abdf4897064f20e3 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema+NowPlaying.generated.swift @@ -420,9 +422,9 @@ a7de0b99aae42c97fac914714fe8eabb24be884f64dc273eefe25cd1c45df4e5 Packages/macOS c0ce3651199ae4b17548bd224e83f2f395c9e1ea4dff6b61a58121a9e61f28de Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchema.swift -3c27682d0f77572f7c991e2fc005119195b23e936a807b3f740843e4ff9b1fee Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchemaExport.swift +362720c890c051b5776d3e21f2b02e6690ced97f3955a1b4e8a81c16bd3f2d1c Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchemaExport.swift -b8b689e10a6022afe317aa5d31be64916cec68179a1146dea915f9acaf9feecd Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchemaSamples.swift +3d6bfc54fb091753f173c30474446d07af0acc0e5eb0ed03b56434a0fdbafe8e Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSchemaSamples.swift f159e3bf124c787c42af1947c8183db9dff4633be316b69be9d7fbfdd2969a8f Packages/macOS/CmuxNext/Sources/CmuxNextSettings/Schema/SettingsSection.generated.swift @@ -478,7 +480,7 @@ c62a2a55d537c1d19be8c89149d0613ae3164260e368a78098581420e39751af Packages/macOS 43cacf7bc3348be69cf619bd177bb9b8c287c75c965169abae1c6b0d75c5b788 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SidebarPriorityViewSetting.swift -00d01ae1a1d1d9b819ab301430ed9c89f2e57b13926ff33b404f757b074baff1 Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SidebarSectionsSetting.swift +bfa63e9ccb8f4102393271b10e3d34e8e42fe2a0b91166b01b8c44a9e458d86c Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SidebarSectionsSetting.swift 7ad103169ba41aa10c5c7cd3e222603a47d00dfc2624d0f91c5901a6b13fa02e Packages/macOS/CmuxNext/Sources/CmuxNextSettings/SidebarSurfaceSetting.swift diff --git a/schemas/settings/settings-schema.json b/schemas/settings/settings-schema.json index 0121cf5bc413..51c779e14e08 100644 --- a/schemas/settings/settings-schema.json +++ b/schemas/settings/settings-schema.json @@ -42,7 +42,8 @@ "appearance.metrics.tabPinnedWidth", "tabs.cmdWClosesPinnedTabs", "tabs.swapCmdTAndCmdN", - "tabs.newTabNowPlaying" + "tabs.newTabNowPlaying", + "sidebar.showChats" ], "title" : { "key" : "settings.group.tabs", @@ -412,7 +413,6 @@ "sidebar.bottomBandMaxShare", "sidebar.pinnedBandsScroll", "sidebar.showWorkspaceTabs", - "sidebar.showChats", "sidebar.sessionTree", "sidebar.showProjects", "sidebar.groupBy", @@ -734,6 +734,7 @@ "agentPane.display.font.lineHeight", "agentPane.display.code.family", "agentPane.display.code.scale", + "agentPane.display.inspector.placement", "agentPane.links.outsideRoots", "agentPane.images.remote", "agentPane.editedFiles.show", @@ -750,16 +751,6 @@ "text" : "Agent Chat" } }, - { - "key" : "settings.group.chiefChat", - "rows" : [ - "home.chiefBubbleStyle" - ], - "title" : { - "key" : "settings.group.chiefChat", - "text" : "Chief Chat" - } - }, { "key" : "settings.group.computerUse", "rows" : [ @@ -831,7 +822,6 @@ "trail" : [ "harnesses", "agentHarnesses", - "chiefs", "computerUse" ] }, @@ -843,6 +833,16 @@ ], "groups" : [ + { + "key" : "settings.group.chiefChat", + "rows" : [ + "home.chiefBubbleStyle" + ], + "title" : { + "key" : "settings.group.chiefChat", + "text" : "Chief Chat" + } + }, { "key" : "settings.group.chiefPet", "rows" : [ @@ -855,14 +855,15 @@ } } ], - "id" : "chief-pet", + "id" : "chief", "lead" : [ + "chiefs", "chiefPet" ], - "symbol" : "pawprint", + "symbol" : "crown", "title" : { - "key" : "settings.category.chiefPet", - "text" : "Chief Pet" + "key" : "settings.category.chief", + "text" : "Chief" }, "trail" : [ @@ -886,7 +887,8 @@ "status.runNotifyMinimumSeconds", "status.runNotifyWhenVisible", "notifications.dockBadge", - "notifications.dockBounce" + "notifications.dockBounce", + "notifications.showPreviews" ], "title" : { "key" : "settings.group.banners", @@ -1251,6 +1253,16 @@ "key" : "settings.group.phone", "text" : "Phone Access" } + }, + { + "key" : "settings.group.messagesServer", + "rows" : [ + "home.server" + ], + "title" : { + "key" : "settings.group.messagesServer", + "text" : "Messages Server" + } } ], "id" : "accounts", @@ -17756,6 +17768,79 @@ }, "validation" : "portable" }, + { + "accepts" : [ + "push", + "overlay" + ], + "agent_refusal" : null, + "agent_settable" : true, + "choices" : [ + { + "title" : { + "key" : "settings.choice.agentChatInspectorPush", + "text" : "Push" + }, + "value" : "push" + }, + { + "title" : { + "key" : "settings.choice.agentChatInspectorOverlay", + "text" : "Overlay" + }, + "value" : "overlay" + } + ], + "consumers" : [ + "cmux-next" + ], + "default" : "overlay", + "default_label" : null, + "group" : { + "key" : "settings.group.agentChat", + "text" : "Agent Chat" + }, + "help" : { + "key" : "settings.agentPane.display.inspector.help", + "text" : "Where the Inspector opens at the right of the chat. Push narrows the chat beside it; Overlay lies over the chat." + }, + "kept_on_reset_all" : false, + "key" : "agentPane.display.inspector.placement", + "keywords" : [ + "agent", + "chat", + "acp", + "display", + "reading", + "inspector", + "panel", + "side", + "right", + "overlay", + "push", + "details" + ], + "kind" : "choice", + "page_hidden" : false, + "path" : [ + "agentPane", + "display", + "inspector", + "placement" + ], + "refuses" : [ + "__not_a_choice__", + 1, + true + ], + "section" : "appearance", + "summary" : null, + "title" : { + "key" : "settings.agentPane.display.inspector", + "text" : "Inspector" + }, + "validation" : "portable" + }, { "accepts" : [ @@ -21031,17 +21116,19 @@ }, "help" : { "key" : "settings.sidebar.showChats.help", - "text" : "Shows every coding agent chat on this computer, newest first, at the bottom of the sidebar." + "text" : "On the New Tab page, the Threads and Console styles list every coding agent chat on this computer, newest first. Off, they show the recent chats only." }, "kept_on_reset_all" : false, "key" : "sidebar.showChats", "keywords" : [ - "sidebar", + "new tab", "chats", "all chats", "agents", "conversations", - "history" + "history", + "threads", + "console" ], "kind" : "toggle", "page_hidden" : false, @@ -21058,7 +21145,7 @@ "summary" : null, "title" : { "key" : "settings.sidebar.showChats", - "text" : "Show All Chats" + "text" : "Show All Chats on New Tab" }, "validation" : "portable" }, @@ -30777,6 +30864,93 @@ }, "validation" : "portable" }, + { + "accepts" : [ + "hosted", + { + "selfHosted" : "http://mini.tail1234.ts.net:7320" + }, + { + "selfHosted" : "https://100.101.102.103/" + } + ], + "agent_refusal" : "privacy", + "agent_settable" : false, + "choices" : [ + { + "title" : { + "key" : "settings.choice.homeServer.hosted", + "text" : "cmux Cloud" + }, + "value" : "hosted" + } + ], + "consumers" : [ + "cmux-next" + ], + "default" : "hosted", + "default_label" : null, + "group" : { + "key" : "settings.group.messagesServer", + "text" : "Messages Server" + }, + "help" : { + "key" : "settings.home.server.help", + "text" : "Direct messages and group chats use cmux Cloud by default. A self-hosted server is reached over Tailscale by its MagicDNS name, for example http://mini.tail1234.ts.net:7320. No password is sent: the server knows you by your Tailscale login." + }, + "kept_on_reset_all" : false, + "key" : "home.server", + "keywords" : [ + "server", + "self-hosted", + "tailscale", + "messages", + "chat", + "cloud", + "home" + ], + "kind" : "choice_or_url", + "page_hidden" : false, + "path" : [ + "home", + "server" + ], + "refuses" : [ + "__not_a_choice__", + "selfHosted", + 1, + { + "selfHosted" : "ftp://mini.tail1234.ts.net" + }, + { + "selfHosted" : "http://mini.tail1234.ts.net/path" + }, + { + "selfHosted" : "http://mini.tail1234.ts.net?x=1" + }, + { + "selfHosted" : "not a url" + }, + { + "selfHosted" : 7320 + }, + { + "__other__" : "http://mini.tail1234.ts.net" + }, + { + "__extra__" : "x", + "selfHosted" : "http://mini.tail1234.ts.net" + } + ], + "section" : "home", + "summary" : null, + "title" : { + "key" : "settings.home.server", + "text" : "Server" + }, + "url_key" : "selfHosted", + "validation" : "portable" + }, { "accepts" : [ "imessage", @@ -31967,6 +32141,78 @@ }, "validation" : "portable" }, + { + "accepts" : [ + "always", + "whenUnlocked", + "never" + ], + "agent_refusal" : "userOnly", + "agent_settable" : false, + "choices" : [ + { + "title" : { + "key" : "settings.choice.previewsAlways", + "text" : "Always" + }, + "value" : "always" + }, + { + "title" : { + "key" : "settings.choice.previewsWhenUnlocked", + "text" : "When Unlocked" + }, + "value" : "whenUnlocked" + }, + { + "title" : { + "key" : "settings.choice.previewsNever", + "text" : "Never" + }, + "value" : "never" + } + ], + "consumers" : [ + "cmux-next" + ], + "default" : "whenUnlocked", + "default_label" : null, + "group" : { + "key" : "settings.group.banners", + "text" : "Banners and Sound" + }, + "help" : { + "key" : "settings.notifications.showPreviews.help", + "text" : "Whether a banner shows what the agent said, or only where and what state." + }, + "kept_on_reset_all" : false, + "key" : "notifications.showPreviews", + "keywords" : [ + "preview", + "privacy", + "message", + "lock screen", + "banner" + ], + "kind" : "choice", + "page_hidden" : false, + "path" : [ + "notifications", + "showPreviews" + ], + "refuses" : [ + "__not_a_choice__", + 1, + true + ], + "section" : "notifications", + "summary" : null, + "title" : { + "key" : "settings.notifications.showPreviews", + "text" : "Show Message in Banners" + }, + "validation" : "portable" + }, { "accepts" : [ true, From 0d3b7c79e5ce096e06688a6008b090d000809f5f Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 04:46:15 -0700 Subject: [PATCH 8/9] cmux-next: a lone chat docking for a main pane open keeps its place; only the new column slides in MainPaneLayout.dockChat hands the chat's content to the dock (LayoutModel.handOffContent, #22949), as AgentBesidePlacement does, and cancels the hint when the dock fails. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- .../CmuxNextApp/Handlers/MainPaneLayout+Routing.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift index 89a69bb1f0b7..5a4fe97c4e2f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/MainPaneLayout+Routing.swift @@ -62,11 +62,16 @@ extension MainPaneLayout { then?(false) return nil } + // The chat stays where it shows; only the tab's pane comes in, from the right (as + // AgentBesidePlacement docks a lone chat). + let layout = controller.workspace?.layoutModel + layout?.handOffContent(of: controller.layoutPaneID) let docked = await withCheckedContinuation { (done: CheckedContinuation) in TabMoves.toNewDockColumn(chat, anchor: anchor, edge: .left, role: .agentChat, services: services) { done.resume(returning: $0) } } reveal(controller, tab) guard docked else { + layout?.cancelContentHandoff() if focus { controller.focusContent() } then?(false) return "main pane: the chat did not dock (see the app log)" From 416026d91cbd77bea59b308e0c52d8357e43728d Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 21 Oct 2026 04:46:55 -0700 Subject: [PATCH 9/9] Keep base's test registry (the merge hook registered a base test; that belongs to base) Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2d-622e-7433-aed6-f3c248542f5d&host=cosmic-gray --- tests/test-execution.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test-execution.toml b/tests/test-execution.toml index 4f47d590b98b..9720649a4c19 100644 --- a/tests/test-execution.toml +++ b/tests/test-execution.toml @@ -1640,7 +1640,3 @@ lane = "linux-guard" [[test]] path = "tests/test_guard_hooks_from_base.py" lane = "linux-guard" - -[[test]] -path = "tests/test_crash_class_fast_gate.py" -lane = "linux-guard"