Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3a4505a
Add customSidebars.renderer setting (remote | inProcess, JSON-backed)
azooz2003-bit Jun 11, 2026
69b00b1
Contain pathological sidebar sources with an evaluation node budget
azooz2003-bit Jun 11, 2026
e98f78e
Mount custom sidebars through CustomSidebarSurface, switchable in-pro…
azooz2003-bit Jun 11, 2026
c24ada3
SPIKE STUB (not for merge): textField RenderNode kind to prove input …
azooz2003-bit Jun 11, 2026
ab0d42e
Render worker: timestamped debug instrumentation for geometry/repaint…
azooz2003-bit Jun 11, 2026
1603778
Render worker: display-link pump + immediate geometry invalidation
azooz2003-bit Jun 11, 2026
f8cf908
Render worker: park the display link at pump completion
azooz2003-bit Jun 11, 2026
8002cb3
Settings UI: Custom Sidebars section with enable toggle and renderer …
azooz2003-bit Jun 11, 2026
c936271
Wire Custom Sidebars settings section into navigation, search, and CLI
azooz2003-bit Jun 11, 2026
09ba00c
Default custom sidebars beta to on
azooz2003-bit Jun 11, 2026
61ea11d
Revert "SPIKE STUB (not for merge): textField RenderNode kind to prov…
azooz2003-bit Jun 11, 2026
24eb42a
Default custom sidebars to the in-process renderer
azooz2003-bit Jun 11, 2026
2ab3306
Merge remote-tracking branch 'origin/main' into feat-worker-displayli…
azooz2003-bit Jun 11, 2026
e716b09
Merge remote-tracking branch 'origin/main' into feat-sidebar-inproces…
azooz2003-bit Jun 11, 2026
2ed8a47
Merge remote-tracking branch 'origin/feat-worker-displaylink-pump' in…
azooz2003-bit Jun 11, 2026
8560725
Fix CustomSidebarSurface for window-owned worker client after main merge
azooz2003-bit Jun 11, 2026
36e53e7
Accept file-length budget growth for renderer default + settings section
azooz2003-bit Jun 11, 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
8 changes: 4 additions & 4 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
32955 CLI/cmux.swift
22576 Sources/TerminalController.swift
19955 Sources/Workspace.swift
19248 Sources/ContentView.swift
19256 Sources/ContentView.swift
18057 Sources/AppDelegate.swift
16539 Sources/GhosttyTerminalView.swift
13608 Sources/Panels/BrowserPanel.swift
Expand Down Expand Up @@ -127,7 +127,7 @@
683 Sources/Panels/CodexAppServerSession.swift
681 Sources/Panels/AgentSessionProcessStore.swift
680 Sources/FileExplorerSearchController.swift
669 Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
683 Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
668 cmuxTests/FeedCoordinatorTests.swift
668 cmuxTests/KeyboardShortcutContextTests.swift
654 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
Expand All @@ -146,7 +146,7 @@
596 cmuxTests/CmuxEventBusTests.swift
594 Sources/SessionIndexModels.swift
594 cmuxTests/PortalTabDragRoutingTests.swift
589 Sources/SettingsNavigation.swift
599 Sources/SettingsNavigation.swift
588 cmuxTests/CommandPaletteShortcutCustomizationTests.swift
586 Sources/JSONCParser.swift
585 Sources/Cloud/VMClient.swift
Expand All @@ -170,7 +170,7 @@
528 cmuxTests/CLINotifyProcessTestSupport.swift
528 cmuxUITests/AutomationSocketUITests.swift
527 CLI/CLISocketPathResolver.swift
523 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift
531 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift
522 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttyRuntime.swift
520 CLI/CMUXCLI+AmpExtension.swift
520 cmuxTests/MainWindowVisibilityControllerTests.swift
Expand Down
9 changes: 6 additions & 3 deletions CLI/CMUXCLI+DocsSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -369,9 +369,10 @@ extension CMUXCLI {
docs Print the same output as `cmux docs settings`.

Targets:
account, app, terminal, sidebar-appearance, automation, browser,
browser-import, global-hotkey, keyboard-shortcuts, shortcuts,
workspace-colors, cmux-json, json, reset
account, app, terminal, sidebar-appearance, custom-sidebars,
automation, browser, browser-import, global-hotkey,
keyboard-shortcuts, shortcuts, workspace-colors, cmux-json,
json, reset

Config file:
\(Self.primarySettingsDisplayPath)
Expand Down Expand Up @@ -404,6 +405,8 @@ extension CMUXCLI {
return "terminal"
case "sidebar", "sidebar-appearance", "sidebarappearance":
return "sidebarAppearance"
case "custom-sidebars", "customsidebars":
return "customSidebars"
case "automation":
return "automation"
case "browser":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ public struct BetaFeaturesCatalogSection: SettingCatalogSection {

/// Custom sidebars: user/agent-authored sidebars (interpreted Swift or
/// JSON) discovered from `~/.config/cmux/sidebars/` and selectable in the
/// sidebar button's provider picker. Defaults off; while off, no custom
/// sidebar button's provider picker. Defaults on; while off, no custom
/// sidebar appears in the picker and a persisted custom selection falls
/// back to the default workspaces sidebar.
public let customSidebars = DefaultsKey<Bool>(
id: "customSidebars.beta.enabled",
defaultValue: false,
defaultValue: true,
userDefaultsKey: "customSidebars.beta.enabled"
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

/// Settings for custom (user/agent-authored) sidebars, the `customSidebars.*`
/// keys. The beta gate that lists custom sidebars in the picker lives in
/// ``BetaFeaturesCatalogSection/customSidebars``; this section holds how a
/// selected custom sidebar behaves.
public struct CustomSidebarsCatalogSection: SettingCatalogSection {
/// Which renderer a selected custom sidebar uses: `inProcess` (default;
/// native in-host SwiftUI with real hover/focus/keyboard) or `remote`
/// (the crash-isolated out-of-process worker for untrusted sources).
///
/// JSON-backed so it can be flipped by editing `~/.config/cmux/cmux.json`:
///
/// ```json
/// { "customSidebars": { "renderer": "remote" } }
/// ```
public let renderer = JSONKey<CustomSidebarRendererMode>(
id: "customSidebars.renderer",
defaultValue: .inProcess
)

public init() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public struct SettingCatalog: SettingCatalogSection {
/// Settings for Mobile pairing and sync.
public let mobile = MobileCatalogSection()
public let betaFeatures = BetaFeaturesCatalogSection()
/// Settings for custom (user/agent-authored) sidebars (the `customSidebars.*` keys).
public let customSidebars = CustomSidebarsCatalogSection()
public let shortcuts = KeyboardShortcutsCatalogSection()
public let integrations = IntegrationsCatalogSection()
public let account = AccountCatalogSection()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

/// Where a custom sidebar's interpreted source is rendered.
///
/// Stored under the catalog entry ``CustomSidebarsCatalogSection/renderer``
/// (`customSidebars.renderer` in `~/.config/cmux/cmux.json`). The raw values
/// are the on-disk strings, so they must not be renamed without a migration.
public enum CustomSidebarRendererMode: String, CaseIterable, Sendable, SettingCodable {
/// The containment lane: an out-of-process render worker
/// interprets and renders the file; the host only composites the worker's
/// remote layer, so an interpreter fault cannot crash the host. Input is
/// limited to forwarded clicks (no hover, focus, or keyboard).
case remote

/// The default lane: the file is interpreted and rendered as real
/// SwiftUI in the host process, gaining native input (hover, focus,
/// keyboard) and same-frame resize. A renderer fault shares the host
/// process, so only use this for sidebars you authored yourself.
case inProcess
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation
import Testing
@testable import CmuxSettings

/// Behavior of the `customSidebars.renderer` setting through the real JSON
/// store: the on-disk strings users put in `~/.config/cmux/cmux.json` must
/// decode to the right renderer, and anything else must fall back to the
/// default (native in-process rendering).
@Suite("customSidebars.renderer")
struct CustomSidebarRendererSettingTests {
private func makeStore() -> (JSONConfigStore, URL) {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-renderer-tests-\(UUID().uuidString)", isDirectory: true)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let fileURL = tempDir.appendingPathComponent("cmux.json", isDirectory: false)
return (JSONConfigStore(fileURL: fileURL), fileURL)
}

@Test func defaultsToInProcessWhenUnset() async {
let (store, _) = makeStore()
let value = await store.value(for: SettingCatalog().customSidebars.renderer)
#expect(value == .inProcess)
}

@Test func readsRemoteFromHandEditedConfigFile() async throws {
let (store, fileURL) = makeStore()
try #"{ "customSidebars": { "renderer": "remote" } }"#
.write(to: fileURL, atomically: true, encoding: .utf8)
let value = await store.value(for: SettingCatalog().customSidebars.renderer)
#expect(value == .remote)
}

@Test func unknownRawValueFallsBackToTheDefault() async throws {
let (store, fileURL) = makeStore()
try #"{ "customSidebars": { "renderer": "yolo" } }"#
.write(to: fileURL, atomically: true, encoding: .utf8)
let value = await store.value(for: SettingCatalog().customSidebars.renderer)
#expect(value == .inProcess)
}

@Test func roundTripsThroughTheStore() async throws {
let (store, fileURL) = makeStore()
try await store.set(.remote, for: SettingCatalog().customSidebars.renderer)
let value = await store.value(for: SettingCatalog().customSidebars.renderer)
#expect(value == .remote)

// The on-disk representation is the raw string, hand-editable.
let parsed = try JSONSerialization.jsonObject(with: Data(contentsOf: fileURL)) as? [String: Any]
let section = parsed?["customSidebars"] as? [String: Any]
#expect(section?["renderer"] as? String == "remote")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ extension Array where Element == CuratedSettingEntry {
.init(section: .mobile, id: "iOSPairingPort", title: String(localized: "settings.mobile.port", defaultValue: "Pairing Port"), synonyms: "mobile.iOSPairingHost.port ios iphone mobile pairing port tcp listener firewall conflict"),
.init(section: .mobile, id: "iOSPairingDisplayName", title: String(localized: "settings.mobile.displayName", defaultValue: "Display Name"), synonyms: "mobile.iOSPairingHost.displayName ios iphone mobile pairing display name mac hostname device label"),

// Custom Sidebars
.init(section: .customSidebars, id: "enabled", title: String(localized: "settings.customSidebars.enabled", defaultValue: "Show Custom Sidebars"), synonyms: "custom sidebars enable show vibe swift json interpreted picker beta"),
.init(section: .customSidebars, id: "renderer", title: String(localized: "settings.customSidebars.renderer", defaultValue: "Renderer"), synonyms: "customSidebars.renderer renderer in-process in app remote worker isolated process hover focus typing input"),

// Beta
.init(section: .betaFeatures, id: "feed", title: "Feed", synonyms: "feed right sidebar agent decisions permissions questions approval beta unstable"),
.init(section: .betaFeatures, id: "dock", title: "Dock", synonyms: "dock right sidebar terminal controls tui beta unstable"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public enum SettingsSectionID: String, CaseIterable, Identifiable, Sendable, Has
/// Mobile pairing and sync settings.
case mobile
case sidebarAppearance
/// User/agent-authored custom sidebars: enable gate and renderer choice.
case customSidebars
case betaFeatures
case automation
case browser
Expand All @@ -38,6 +40,7 @@ public enum SettingsSectionID: String, CaseIterable, Identifiable, Sendable, Has
case .textBox: return String(localized: "settings.section.textBox", defaultValue: "TextBox (Beta)")
case .mobile: return String(localized: "settings.section.mobile", defaultValue: "Mobile")
case .sidebarAppearance: return "Sidebar"
case .customSidebars: return String(localized: "settings.section.customSidebars", defaultValue: "Custom Sidebars")
case .betaFeatures: return "Beta Features"
case .automation: return "Automation"
case .browser: return "Browser"
Expand All @@ -59,6 +62,7 @@ public enum SettingsSectionID: String, CaseIterable, Identifiable, Sendable, Has
case .textBox: return "textformat"
case .mobile: return "iphone"
case .sidebarAppearance: return "sidebar.left"
case .customSidebars: return "sidebar.squares.left"
case .betaFeatures: return "exclamationmark.triangle"
case .automation: return "wand.and.sparkles"
case .browser: return "globe"
Expand All @@ -82,6 +86,7 @@ public enum SettingsSectionID: String, CaseIterable, Identifiable, Sendable, Has
case .textBox: return "textbox text box rich input prompt default new terminal workspace split tab focus show beta"
case .mobile: return "ios iphone ipad mobile pairing local network sync"
case .sidebarAppearance: return "sidebar details branches material terminal background"
case .customSidebars: return "custom sidebars vibe swift json interpreted renderer in-process remote worker isolated"
case .betaFeatures: return "beta experimental unstable feed dock right sidebar"
case .automation: return "socket integrations hooks ports claude cursor gemini"
case .browser: return "search engine links history theme"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,14 @@ public struct SettingsWindowRoot: View {
SidebarSection(defaultsStore: defaultsStore, catalog: catalog, hostActions: hostActions)
.id(anchorID(for: .sidebarAppearance))

CustomSidebarsSection(
defaultsStore: defaultsStore,
jsonStore: jsonStore,
catalog: catalog,
errorLog: runtime.errorLog
)
.id(anchorID(for: .customSidebars))

BetaFeaturesSection(defaultsStore: defaultsStore, catalog: catalog)
.id(anchorID(for: .betaFeatures))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import CmuxSettings
import SwiftUI

/// **Custom Sidebars** section — the user/agent-authored sidebar
/// surface: the enable toggle (shared with the Beta Features gate) and
/// the renderer picker choosing between the crash-isolated helper
/// process and native in-app rendering.
@MainActor
public struct CustomSidebarsSection: View {
@State private var enabled: DefaultsValueModel<Bool>
@State private var renderer: JSONValueModel<CustomSidebarRendererMode>

public init(
defaultsStore: UserDefaultsSettingsStore,
jsonStore: JSONConfigStore,
catalog: SettingCatalog,
errorLog: SettingsErrorLog
) {
_enabled = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.customSidebars))
_renderer = State(initialValue: JSONValueModel(
store: jsonStore,
key: catalog.customSidebars.renderer,
errorLog: errorLog
))
}

public var body: some View {
Group {
SettingsSectionHeader(String(localized: "settings.section.customSidebars", defaultValue: "Custom Sidebars"), section: .customSidebars)
SettingsCard {
enabledRow
SettingsCardDivider()
rendererRow
SettingsCardDivider()
SettingsCardNote(
String(localized: "settings.customSidebars.note", defaultValue: "Custom sidebars are SwiftUI-style files in ~/.config/cmux/sidebars. Pick one from the sidebar toggle button's right-click menu; edits hot-reload on save. Use the in-app renderer only for sidebars you trust.")
)
}
}
}

@ViewBuilder
private var enabledRow: some View {
SettingsCardRow(
configurationReview: .settingsOnly,
searchAnchorID: "setting:customSidebars:enabled",
String(localized: "settings.customSidebars.enabled", defaultValue: "Show Custom Sidebars"),
subtitle: enabled.current
? String(localized: "settings.customSidebars.enabled.subtitleOn", defaultValue: "Lists your sidebars from ~/.config/cmux/sidebars in the sidebar picker.")
: String(localized: "settings.customSidebars.enabled.subtitleOff", defaultValue: "Hides custom sidebars from the sidebar picker until you enable them here.")
) {
Toggle("", isOn: Binding(get: { enabled.current }, set: { enabled.set($0) }))
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsCustomSidebarsEnabledToggle")
}
}

@ViewBuilder
private var rendererRow: some View {
SettingsCardRow(
configurationReview: .json("customSidebars.renderer"),
String(localized: "settings.customSidebars.renderer", defaultValue: "Renderer"),
subtitle: renderer.current.rendererDescription
) {
Picker("", selection: Binding(get: { renderer.current }, set: { renderer.set($0) })) {
ForEach(CustomSidebarRendererMode.uiCases, id: \.self) { mode in
Text(mode.displayName).tag(mode)
}
}
.labelsHidden()
.pickerStyle(.menu)
.disabled(!enabled.current)
.accessibilityIdentifier("SettingsCustomSidebarsRendererPicker")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import CmuxSettings
import Foundation

/// UI-facing labels for ``CustomSidebarRendererMode``, shown by the
/// Custom Sidebars settings section's renderer picker.
extension CustomSidebarRendererMode {
/// Canonical UI ordering of the modes in the picker.
static var uiCases: [CustomSidebarRendererMode] {
[.remote, .inProcess]
}

/// Short label shown in the renderer picker.
var displayName: String {
switch self {
case .remote:
return String(localized: "customSidebarRenderer.remote.name", defaultValue: "Isolated process")
case .inProcess:
return String(localized: "customSidebarRenderer.inProcess.name", defaultValue: "In-app (full input)")
}
}

/// One-sentence row subtitle explaining the tradeoff.
var rendererDescription: String {
switch self {
case .remote:
return String(localized: "customSidebarRenderer.remote.description", defaultValue: "Renders in a crash-isolated helper process. Clicks only: no hover, focus, or typing.")
case .inProcess:
return String(localized: "customSidebarRenderer.inProcess.description", defaultValue: "Renders as native SwiftUI inside cmux with hover, focus, and typing. A faulty sidebar shares the app process.")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ struct SettingsRowAnchorResolutionTests {
"browser.showSearchSuggestions",
"browser.theme",
"browser.urlsToAlwaysOpenExternally",
"customSidebars.renderer",
"fileEditor.wordWrap",
"notifications.command",
"notifications.dockBadge",
Expand Down Expand Up @@ -132,6 +133,7 @@ struct SettingsRowAnchorResolutionTests {
"setting:betaFeatures:feed",
"setting:betaFeatures:dock",
"setting:betaFeatures:customSidebars",
"setting:customSidebars:enabled",
"setting:browser:history",
"setting:browser:http-allowlist",
"setting:workspaceColors:palette",
Expand Down
7 changes: 7 additions & 0 deletions Packages/CmuxSidebarInterpreterService/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,12 @@ let package = Package(
.swiftLanguageMode(.v6),
]
),
.testTarget(
name: "CmuxSidebarRemoteRenderTests",
dependencies: ["CmuxSidebarRemoteRender"],
swiftSettings: [
.swiftLanguageMode(.v6),
]
),
]
)
Loading
Loading