Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
25a4077
Auto-tint split terminal panes
Jun 26, 2026
8cbc9e4
Refresh Swift file length budget
Jun 26, 2026
0cbe918
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
Jun 26, 2026
87ecfa1
Use value type for split tint settings
Jun 26, 2026
a718f2f
Refresh PR checks
Jun 27, 2026
e826224
Isolate split pane tint tests
Jun 27, 2026
4857e2c
Fix split tint planner warning
Jun 27, 2026
948f9cd
Refresh Vercel checks
Jun 27, 2026
ce90f6f
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
Jun 29, 2026
df604a8
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
Jun 29, 2026
387049d
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
Jun 29, 2026
06114dc
Address split pane tint review
Jun 29, 2026
64818a7
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
austinywang Jun 29, 2026
7946c2c
Document autoTintSplitPanes catalog key
austinywang Jun 30, 2026
5f34468
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
austinywang Jul 2, 2026
06a0ca5
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
austinywang Jul 2, 2026
342f5c7
Document config-reload tint-reset as a known v1 limitation
austinywang Jul 2, 2026
6c3d43f
Fix WorkspaceSplitPaneTintTests palette-exhaustion assertion
austinywang Jul 2, 2026
d4e17bb
Persist split-pane tints only when cmux owns the background slot
austinywang Jul 2, 2026
f516ba5
Plan split tints against the true terminal background, not the source…
austinywang Jul 2, 2026
6095b56
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
austinywang Jul 2, 2026
50a3d36
Merge remote-tracking branch 'origin/main' into issue-5975-auto-assig…
austinywang Jul 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
17954 Sources/AppDelegate.swift
16427 Sources/ContentView.swift
14270 Sources/TerminalController.swift
13172 Sources/Workspace.swift
13349 Sources/Workspace.swift
12348 cmuxTests/AppDelegateShortcutRoutingTests.swift
12296 Sources/GhosttyTerminalView.swift
12324 Sources/GhosttyTerminalView.swift
11669 Sources/Panels/BrowserPanel.swift
9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8032 Sources/Panels/BrowserPanelView.swift
Expand All @@ -24,8 +24,8 @@
4483 Sources/cmuxApp.swift
4482 Sources/Panels/FilePreviewPanel.swift
4367 cmuxTests/BrowserPanelTests.swift
4283 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
4121 Sources/BrowserWindowPortal.swift
3964 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
3934 Sources/Feed/FeedPanelView.swift
3926 cmuxTests/TabManagerUnitTests.swift
3896 cmuxTests/WindowAndDragTests.swift
Expand All @@ -45,7 +45,7 @@
2229 Sources/TerminalWindowPortal.swift
2126 cmuxTests/CmuxConfigTests.swift
2085 cmuxTests/ShortcutAndCommandPaletteTests.swift
2078 Sources/SessionPersistence.swift
2081 Sources/SessionPersistence.swift
2011 Sources/KeyboardShortcutSettingsFileStore.swift
1944 Sources/RestorableAgentSession.swift
1900 cmuxTests/NotificationAndMenuBarTests.swift
Expand Down Expand Up @@ -214,14 +214,14 @@
549 Sources/Panels/BrowserAutomation.swift
547 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift
546 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift
546 Sources/App/WorkspaceRuntimeSettings.swift
544 cmuxUITests/DisplayResolutionRegressionUITests.swift
540 Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGridReplay.swift
539 CLI/CMUXCLI+Themes.swift
539 CLI/CodexTeamsApprovalBridge.swift
538 Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/MobileTerminalRenderGridTests.swift
538 Packages/macOS/CmuxRemoteWorkspace/Sources/CmuxRemoteWorkspace/PTYBridge/RemotePTYBridgeSession.swift
536 cmuxTests/CmuxConfigContextMenuTests.swift
535 Sources/App/WorkspaceRuntimeSettings.swift
533 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift
530 Sources/BrowserPaneDropTargetView.swift
530 Sources/TerminalController+ControlSurfaceContext3.swift
Expand Down Expand Up @@ -250,8 +250,8 @@
506 Sources/App/MainWindowVisibilityController.swift
504 Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/UserDefaultsSettingsStoreTests.swift
504 cmuxTests/TerminalNotificationSocketActionTests.swift
503 Sources/RemoteTmuxSessionMirror.swift
503 Sources/Settings/ConfigSource.swift
502 Sources/CmuxEventPublishing.swift
502 Sources/RemoteTmuxSessionMirror.swift
500 Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
500 Sources/KeyboardShortcutRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ public struct TerminalCatalogSection: SettingCatalogSection {
userDefaultsKey: "terminal.autoResumeAgentSessions"
)

/// Whether new terminal split panes automatically receive subtle, distinct
/// background tints so adjacent panes stay visually distinguishable. On by default.
public let autoTintSplitPanes = DefaultsKey<Bool>(
id: "terminal.autoTintSplitPanes",
defaultValue: true,
userDefaultsKey: "terminal.autoTintSplitPanes"
)

public let agentHibernationEnabled = DefaultsKey<Bool>(
id: "terminal.agentHibernation.enabled",
defaultValue: false,
Expand Down
11 changes: 11 additions & 0 deletions Sources/App/WorkspaceRuntimeSettings.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Darwin
import CmuxSettings
import Foundation

enum WorkspaceTitlebarSettings {
Expand Down Expand Up @@ -203,6 +204,16 @@ enum TerminalCopyOnSelectSettings {
}
}

struct TerminalSplitPaneTintSettings {
private static let autoTintSplitPanes = TerminalCatalogSection().autoTintSplitPanes
static var autoTintSplitPanesKey: String { autoTintSplitPanes.userDefaultsKey }
static var defaultAutoTintSplitPanes: Bool { autoTintSplitPanes.defaultValue }

func isEnabled(defaults: UserDefaults = .standard) -> Bool {
Self.autoTintSplitPanes.value(in: defaults)
}
}

enum TerminalManagedGhosttySettings {
static func ghosttyConfigContents(defaults: UserDefaults = .standard, emitsCopyOnSelectFalse: Bool = true) -> String? {
let lines = [
Expand Down
6 changes: 6 additions & 0 deletions Sources/CmuxSettingsJSONPathSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ enum TerminalSettingsFileMapping {
defaultsKey: AgentSessionAutoResumeSettings.autoResumeAgentSessionsKey,
invalidPath: "terminal.autoResumeAgentSessions"
),
.init(
jsonKey: "autoTintSplitPanes",
defaultsKey: TerminalSplitPaneTintSettings.autoTintSplitPanesKey,
invalidPath: "terminal.autoTintSplitPanes"
),
]
}

Expand Down Expand Up @@ -369,6 +374,7 @@ extension CmuxSettingsFileStore {
"terminal.scrollSpeed",
"terminal.copyOnSelect",
"terminal.autoResumeAgentSessions",
"terminal.autoTintSplitPanes",
"terminal.showTextBoxOnNewTerminals",
"terminal.focusTextBoxOnNewTerminals",
"terminal.textBoxDefaultSubmitAction",
Expand Down
28 changes: 28 additions & 0 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,16 @@ extension TerminalSurface {
}
return hosted
}

var paneBackgroundOverrideColor: NSColor? {
get { hostedView.paneBackgroundOverrideColor }
set { hostedView.paneBackgroundOverrideColor = newValue }
}

var autoAssignedSplitTintHex: String? {
get { hostedView.autoAssignedSplitTintHex }
set { hostedView.autoAssignedSplitTintHex = newValue }
}
}

// The engine's Metal layer reports vended drawables through this seam
Expand Down Expand Up @@ -8036,6 +8046,24 @@ final class GhosttySurfaceScrollView: NSView {
return window
}

var paneBackgroundOverrideColor: NSColor? {
get { surfaceView.backgroundColor }
set {
surfaceView.backgroundColor = newValue
surfaceView.applySurfaceBackground()
surfaceView.applyWindowBackgroundIfActive()
}
}

/// Hex of the split-pane tint cmux most recently assigned to this surface,
/// or nil when cmux never auto-tinted it. This is provenance for session
/// persistence only: `paneBackgroundOverrideColor` is a shared slot that
/// terminal OSC background changes (`GHOSTTY_ACTION_COLOR_CHANGE`) and
/// Ghostty config reloads (`GHOSTTY_ACTION_CONFIG_CHANGE`) also write, so a
/// live override is a cmux-owned tint worth persisting only while it still
/// equals this value. See `TerminalSplitPaneTintPlanner.persistableTintHex`.
var autoAssignedSplitTintHex: String?

func forwardKeyDownToSurface(_ event: NSEvent) {
surfaceView.keyDown(with: event)
}
Expand Down
1 change: 1 addition & 0 deletions Sources/KeyboardShortcutSettingsFileStore+Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ extension CmuxSettingsFileStore {
"scrollSpeed": TerminalScrollSpeedSettings.defaultMultiplier,
"copyOnSelect": TerminalCopyOnSelectSettings.defaultCopyOnSelect,
"autoResumeAgentSessions": AgentSessionAutoResumeSettings.defaultAutoResumeAgentSessions,
"autoTintSplitPanes": TerminalSplitPaneTintSettings.defaultAutoTintSplitPanes,
"showTextBoxOnNewTerminals": TerminalTextBoxInputSettings.defaultShowOnNewTerminals,
"focusTextBoxOnNewTerminals": TerminalTextBoxInputSettings.defaultFocusOnNewTerminals,
"textBoxDefaultSubmitAction": TerminalTextBoxInputSettings.defaultSubmitActionID,
Expand Down
1 change: 1 addition & 0 deletions Sources/RemoteTmuxSessionMirror.swift
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ final class RemoteTmuxSessionMirror {
panelId: panelId,
connection: connection,
layout: window.layout,
paneTintDefaults: workspace.terminalSplitPaneTintDefaults,
makePanel: { [weak workspace, weak connection] tmuxPaneId in
workspace?.makeRemoteTmuxPanePanel(onInput: { data in
Task { @MainActor in connection?.sendKeys(paneId: tmuxPaneId, data: data) }
Expand Down
32 changes: 32 additions & 0 deletions Sources/RemoteTmuxWindowMirror.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AppKit
import Bonsplit
import CmuxFoundation
import CmuxTerminal
import Foundation
import Observation
Expand All @@ -25,6 +26,7 @@ final class RemoteTmuxWindowMirror {
@ObservationIgnored private weak var connection: RemoteTmuxControlConnection?
/// Creates a configured manual-I/O pane panel whose input goes to `tmuxPaneId`.
@ObservationIgnored private let makePanel: (_ tmuxPaneId: Int) -> TerminalPanel?
@ObservationIgnored private let paneTintDefaults: UserDefaults

/// The window's current pane layout — drives the SwiftUI split container.
private(set) var layout: RemoteTmuxLayoutNode
Expand All @@ -44,12 +46,14 @@ final class RemoteTmuxWindowMirror {
panelId: UUID,
connection: RemoteTmuxControlConnection,
layout: RemoteTmuxLayoutNode,
paneTintDefaults: UserDefaults = .standard,
makePanel: @escaping (_ tmuxPaneId: Int) -> TerminalPanel?
) {
self.windowId = windowId
self.panelId = panelId
self.connection = connection
self.makePanel = makePanel
self.paneTintDefaults = paneTintDefaults
self.layout = layout
reconcile(layout: layout)
}
Expand Down Expand Up @@ -95,6 +99,34 @@ final class RemoteTmuxWindowMirror {
if activePaneId == paneId { activePaneId = nil }
}
if layout != newLayout { layout = newLayout }
applyAutomaticPaneTints()
}

private func applyAutomaticPaneTints() {
guard TerminalSplitPaneTintSettings().isEnabled(defaults: paneTintDefaults) else { return }
let baseColor = GhosttyApp.shared.defaultBackgroundColor
var usedHexes = Set(
panelsByPaneId.values.compactMap { panel in
panel.surface.paneBackgroundOverrideColor?.hexString()
}
)
var selectedHexes = Set<String>()
for paneId in paneIDsInOrder {
guard let panel = panelsByPaneId[paneId],
panel.surface.paneBackgroundOverrideColor == nil,
let color = TerminalSplitPaneTintPlanner.nextColor(
baseColor: baseColor,
usedHexes: usedHexes,
excludedHexes: selectedHexes
) else {
continue
}
panel.surface.paneBackgroundOverrideColor = color
let hex = color.hexString()
panel.surface.autoAssignedSplitTintHex = hex
usedHexes.insert(hex)
selectedHexes.insert(hex)
}
}

/// Routes a tmux `%output` to the surface for `paneId` (no-op if unknown).
Expand Down
3 changes: 3 additions & 0 deletions Sources/SessionPersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,7 @@ struct SessionTerminalPanelSnapshot: Codable, Sendable {
var textBoxDraft: SessionTextBoxInputDraftSnapshot?
var isRemoteTerminal: Bool?
var remotePTYSessionID: String?
var backgroundColorHex: String?
/// Whether the agent process was actively running when this snapshot was captured.
/// Nil means unknown (legacy snapshots); treated as true for backwards compatibility.
var wasAgentRunning: Bool?
Expand All @@ -1409,6 +1410,7 @@ struct SessionTerminalPanelSnapshot: Codable, Sendable {
textBoxDraft: SessionTextBoxInputDraftSnapshot? = nil,
isRemoteTerminal: Bool? = nil,
remotePTYSessionID: String? = nil,
backgroundColorHex: String? = nil,
wasAgentRunning: Bool? = nil
) {
self.workingDirectory = workingDirectory
Expand All @@ -1420,6 +1422,7 @@ struct SessionTerminalPanelSnapshot: Codable, Sendable {
self.textBoxDraft = textBoxDraft
self.isRemoteTerminal = isRemoteTerminal
self.remotePTYSessionID = remotePTYSessionID
self.backgroundColorHex = backgroundColorHex
self.wasAgentRunning = wasAgentRunning
}
}
Expand Down
Loading
Loading