Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4ab83d3
test: stop tests waiting out real timeouts
teamleaderleo Sep 28, 2026
7c1819d
Merge main (8b23dd7a7650) into test/injectable-test-timeouts
teamleaderleo Sep 28, 2026
c0abe39
test: reject non-finite CLI test delays
teamleaderleo Sep 28, 2026
ec053fd
fix: reject non-finite CLI test delays
teamleaderleo Sep 28, 2026
3f52a25
test: bound injected CLI retry delays
teamleaderleo Sep 28, 2026
a520e2e
test: give the shortened deadlines room for process startup
teamleaderleo Sep 28, 2026
d464890
test: the real-process section no longer only uses the default spawnC…
teamleaderleo Sep 28, 2026
2e447dd
test: keep injected VM polling paced
teamleaderleo Sep 28, 2026
70b0c5a
Merge remote-tracking branch 'upstream/main' into test/injectable-tes…
teamleaderleo Sep 28, 2026
49a8a23
Merge branch 'main' into test/injectable-test-timeouts
teamleaderleo Sep 28, 2026
04870a7
Merge main (90e686206adc) into codex/pr15381-cleanup-20260928
teamleaderleo Sep 28, 2026
cfbf55c
test: drive cloud deadlines manually and await shell readiness
teamleaderleo Sep 28, 2026
d44da40
Merge remote-tracking branch 'mf/main' into repair/pr15381-current-main
teamleaderleo Sep 28, 2026
c0540de
test: isolate minimal-mode measurement from file explorer
teamleaderleo Sep 28, 2026
7c62715
test: isolate file explorer persistence store
teamleaderleo Sep 28, 2026
638f8a3
fix: keep custom sidebar availability on injected defaults
teamleaderleo Sep 28, 2026
bc65b8c
fix: route injected sidebar defaults through availability
teamleaderleo Sep 28, 2026
3a6440f
fix: keep available sidebar modes on injected defaults
teamleaderleo Sep 28, 2026
a9eddd9
Merge remote-tracking branch 'mf/main' into repair/pr15381-current-main
teamleaderleo Sep 28, 2026
bbd4aec
Merge origin/main into test/injectable-test-timeouts
teamleaderleo Sep 30, 2026
d940a85
test: assert VM poll fallback directly
teamleaderleo Sep 30, 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
2 changes: 1 addition & 1 deletion CLI/CMUXCLI+VMDev.swift
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ extension CMUXCLI {
lastFailure = error.message
}
if attempt < Self.vmLayoutOpenAttempts {
Thread.sleep(forTimeInterval: Self.vmLayoutOpenRetryDelay)
Thread.sleep(forTimeInterval: Self.vmLayoutOpenRetryDelay())
}
}
throw CLIError(message: "vm dev: the layout is built in workspace \(remoteWorkspace) on \(machine), but it could not be opened here yet (\(lastFailure)). Open it with: cmux vm workspace open \(machine) \(remoteWorkspace)")
Expand Down
17 changes: 15 additions & 2 deletions CLI/CMUXCLI+VMLayoutEnv.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,20 @@ extension CMUXCLI {

/// How long `--open` keeps trying before handing the human the manual command.
static let vmLayoutOpenAttempts = 5
static let vmLayoutOpenRetryDelay: TimeInterval = 1
/// Seconds between open attempts. `CMUX_VM_LAYOUT_OPEN_RETRY_DELAY_SECONDS`
/// overrides the default so tests against a mock socket do not wait it out.
static func vmLayoutOpenRetryDelay(
environment: [String: String] = ProcessInfo.processInfo.environment
) -> TimeInterval {
guard let raw = environment["CMUX_VM_LAYOUT_OPEN_RETRY_DELAY_SECONDS"],
let parsed = TimeInterval(raw),
parsed.isFinite,
parsed > 0,
parsed <= 1 else {
return 1
}
return parsed
}

/// The geometry-honoring open (`vm.workspace_open`, the same method the sidebar row
/// and `cmux vm workspace open` use) for a workspace the shim built moments ago.
Expand Down Expand Up @@ -231,7 +244,7 @@ extension CMUXCLI {
lastFailure = error.message
}
if attempt < Self.vmLayoutOpenAttempts {
Thread.sleep(forTimeInterval: Self.vmLayoutOpenRetryDelay)
Thread.sleep(forTimeInterval: Self.vmLayoutOpenRetryDelay())
}
}
throw CLIError(message: String(format: String(localized: "cli.vm.layout.applyTheLayoutIsAppliedToWorkspace", defaultValue: "vm layout apply: the layout is applied to workspace %1$@ on %2$@, but it could not be opened here yet (%3$@). Open it with: cmux vm workspace open %4$@ %5$@"), String(describing: remoteWorkspace), String(describing: machine), String(describing: lastFailure), String(describing: machine), String(describing: remoteWorkspace)))
Expand Down
20 changes: 19 additions & 1 deletion CLI/CMUXCLI+VMTransfer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -832,10 +832,28 @@ extension CMUXCLI {
guard Date() < deadline else {
throw CLIError(message: "Timed out after \(timeoutSeconds)s waiting for \(vmID) (last status: \(status)). Re-run with --timeout <seconds> to wait longer.")
}
Thread.sleep(forTimeInterval: 3)
let remainingSeconds = deadline.timeIntervalSinceNow
if remainingSeconds > 0 {
Thread.sleep(forTimeInterval: min(Self.vmReadyPollInterval(), remainingSeconds))
}
}
}

/// Seconds between `vm.status` polls. `CMUX_VM_WAIT_POLL_SECONDS` overrides the
/// default so tests against a mock socket do not wait out the real cadence.
static func vmReadyPollInterval(
environment: [String: String] = ProcessInfo.processInfo.environment
) -> TimeInterval {
guard let raw = environment["CMUX_VM_WAIT_POLL_SECONDS"],
let parsed = TimeInterval(raw),
parsed.isFinite,
parsed >= 0.01,
parsed <= 3 else {
return 3
}
return parsed
}

// MARK: - transfer plumbing

/// Chunk progress: rewrites one line on a TTY, but emits whole lines when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension CloudMachineLinkManager {
let host = primary.contains(":") ? "[\(primary)]" : primary
return "ws://\(host):1337/v1/link"
}
let connected = try await CloudHubConnector().connect(
let connected = try await privateRouteConnector.connect(
endpoint: .unix(path: hub.socketPath),
target: CloudPortForwardTarget(host: primary, port: 1337, fallbackHosts: Array(addresses.dropFirst())),
queue: DispatchQueue.global(qos: .userInitiated)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public actor CloudMachineLinkManager {
/// carrier or enrolled session immediately, so anything slower than this is
/// a broken route rather than a slow one.
private let connectTimeout: Duration = .seconds(60)
/// Races the private addresses of a dual-stack machine through the hub.
/// Tests that expect every address to fail pass a short deadline.
let privateRouteConnector: CloudHubConnector
/// This Mac's resolved Ghostty default colors ("#rrggbb"), pushed to each machine as
/// its cmux-tui session defaults (`set-default-colors`) so remote panes render with
/// the local theme. Injected so tests need no Ghostty runtime.
Expand All @@ -101,8 +104,10 @@ public actor CloudMachineLinkManager {
operations: CloudOperationRecorder? = nil,
isCloudEnabled: @escaping @Sendable () -> Bool = { true },
hostThemeColors: @escaping @Sendable () async -> (foreground: String, background: String)?,
breadcrumb: @escaping @Sendable (_ event: String, _ fields: [String: String]) -> Void = { _, _ in }
breadcrumb: @escaping @Sendable (_ event: String, _ fields: [String: String]) -> Void = { _, _ in },
privateRouteConnector: CloudHubConnector = CloudHubConnector()
) {
self.privateRouteConnector = privateRouteConnector
self.breadcrumb = breadcrumb
self.isCloudEnabled = isCloudEnabled
self.operations = operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public actor CloudTelemetryUploader: CloudTelemetrySending {
private let baseURL: URL
private let client: CloudTelemetryClient
private let queueURL: URL
private let batchDelay: Duration
private let logger = Logger(subsystem: "com.cmuxterm.app", category: "CloudDiagnostics")
private var entries: [Entry] = []
private var loaded = false
Expand All @@ -51,12 +52,14 @@ public actor CloudTelemetryUploader: CloudTelemetrySending {
baseURL: URL,
client: CloudTelemetryClient,
session: URLSession = URLSession(configuration: .ephemeral),
queueURL: URL? = nil
queueURL: URL? = nil,
batchDelay: Duration = .seconds(2)
) {
self.auth = auth
self.baseURL = baseURL
self.client = client
self.session = session
self.batchDelay = batchDelay
self.queueURL = queueURL ?? FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent(Bundle.main.bundleIdentifier ?? "com.cmuxterm.app")
.appendingPathComponent("cloud-diagnostics.json")
Expand Down Expand Up @@ -99,9 +102,10 @@ public actor CloudTelemetryUploader: CloudTelemetrySending {
private func schedule() {
guard uploadTask == nil else { return }
let currentGeneration = generation
let batchDelay = batchDelay
uploadTask = Task { [weak self] in
// Batch cadence, not a UI synchronization delay.
try? await Task.sleep(for: .seconds(2))
try? await Task.sleep(for: batchDelay)
await self?.drain(generation: currentGeneration)
}
}
Expand Down
7 changes: 5 additions & 2 deletions Sources/AgentForkCommandOutputRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ actor AgentForkCommandOutputRunner {
private let arguments: [String]
private let environment: [String: String]?
private let workingDirectory: String?
private let outputTimeoutNanoseconds: Int64
private var processIdentifier: pid_t?
private var probeRootProcessIdentifier: pid_t?
private var probeRootStartMicroseconds: Int64?
Expand All @@ -52,8 +53,10 @@ actor AgentForkCommandOutputRunner {
executable: String,
arguments: [String],
environment: [String: String]?,
workingDirectory: String?
workingDirectory: String?,
outputTimeoutNanoseconds: Int64 = AgentForkSupport.commandOutputTimeoutNanoseconds
) {
self.outputTimeoutNanoseconds = outputTimeoutNanoseconds
self.executable = executable
self.arguments = arguments
self.environment = environment
Expand Down Expand Up @@ -246,7 +249,7 @@ actor AgentForkCommandOutputRunner {

private func startTimeoutTimer() {
let timer = DispatchSource.makeTimerSource(queue: .global(qos: .utility))
timer.schedule(deadline: .now() + .nanoseconds(Int(AgentForkSupport.commandOutputTimeoutNanoseconds)))
timer.schedule(deadline: .now() + .nanoseconds(Int(outputTimeoutNanoseconds)))
timer.setEventHandler { [weak self] in
self?.cancel()
}
Expand Down
21 changes: 15 additions & 6 deletions Sources/AgentForkSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,26 @@ enum AgentForkSupport {

static func supportsFork(
snapshot: SessionRestorableAgentSnapshot,
isRemoteContext: Bool = false
isRemoteContext: Bool = false,
probeOutputTimeoutNanoseconds: Int64 = commandOutputTimeoutNanoseconds
) async -> Bool {
let executableIdentityResolver = AgentForkExecutableIdentityResolver()
let forkCapabilityProbeCache = ForkCapabilityProbeResultCache()
return await supportsFork(
snapshot: snapshot,
isRemoteContext: isRemoteContext,
executableIdentityResolver: executableIdentityResolver,
forkCapabilityProbeCache: forkCapabilityProbeCache
forkCapabilityProbeCache: forkCapabilityProbeCache,
probeOutputTimeoutNanoseconds: probeOutputTimeoutNanoseconds
)
}

static func supportsFork(
snapshot: SessionRestorableAgentSnapshot,
isRemoteContext: Bool = false,
executableIdentityResolver: AgentForkExecutableIdentityResolver,
forkCapabilityProbeCache: ForkCapabilityProbeResultCache
forkCapabilityProbeCache: ForkCapabilityProbeResultCache,
probeOutputTimeoutNanoseconds: Int64 = commandOutputTimeoutNanoseconds
) async -> Bool {
guard forkCommandIdentityParts(snapshot: snapshot) != nil else { return false }
if isRemoteContext,
Expand Down Expand Up @@ -124,6 +127,7 @@ enum AgentForkSupport {
executableIdentityResolver: executableIdentityResolver,
forkCapabilityProbeCache: forkCapabilityProbeCache,
probeFromDefaultDirectoryWhenWorkingDirectoryIsMissing: true,
probeOutputTimeoutNanoseconds: probeOutputTimeoutNanoseconds,
outputSupportsFork: { output in
piFamilyVersionSupportsFork(
output,
Expand Down Expand Up @@ -151,6 +155,7 @@ enum AgentForkSupport {
cacheDiscriminator: "opencode-version",
executableIdentityResolver: executableIdentityResolver,
forkCapabilityProbeCache: forkCapabilityProbeCache,
probeOutputTimeoutNanoseconds: probeOutputTimeoutNanoseconds,
outputSupportsFork: { output in
openCodeVersionSupportsFork(output)
}
Expand Down Expand Up @@ -523,6 +528,7 @@ enum AgentForkSupport {
executableIdentityResolver: AgentForkExecutableIdentityResolver,
forkCapabilityProbeCache: ForkCapabilityProbeResultCache,
probeFromDefaultDirectoryWhenWorkingDirectoryIsMissing: Bool = false,
probeOutputTimeoutNanoseconds: Int64,
outputSupportsFork: @Sendable (String) -> Bool
) async -> Bool {
let requestedWorkingDirectory = probeWorkingDirectory(snapshot: snapshot)
Expand Down Expand Up @@ -551,7 +557,8 @@ enum AgentForkSupport {
executable: probe.executable,
arguments: probe.arguments,
environment: snapshot.launchCommand?.environment,
workingDirectory: workingDirectory
workingDirectory: workingDirectory,
outputTimeoutNanoseconds: probeOutputTimeoutNanoseconds
) else {
return false
}
Expand Down Expand Up @@ -1078,13 +1085,15 @@ enum AgentForkSupport {
executable: String,
arguments: [String],
environment: [String: String]?,
workingDirectory: String?
workingDirectory: String?,
outputTimeoutNanoseconds: Int64
) async -> String? {
let runner = AgentForkCommandOutputRunner(
executable: executable,
arguments: arguments,
environment: environment,
workingDirectory: workingDirectory
workingDirectory: workingDirectory,
outputTimeoutNanoseconds: outputTimeoutNanoseconds
)
return await withTaskCancellationHandler {
await runner.start()
Expand Down
6 changes: 5 additions & 1 deletion Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11083,13 +11083,17 @@ enum CmuxExtensionSidebarSelection {
/// Synchronous read of the experimental custom-sidebars flag, mirroring
/// ``isEnabled`` for the AppKit/static paths (the picker menu).
static var customSidebarsEnabled: Bool {
customSidebarsEnabled(defaults: .standard)
}

static func customSidebarsEnabled(defaults: UserDefaults) -> Bool {
// `DisableCustomSidebars` (MDM): interpreted sidebars are user- or
// agent-authored code that can dispatch `cmux(...)` commands.
guard !ManagedDevicePolicy().isEnforced(.disableCustomSidebars) else { return false }
// See ``isEnabled``: read only the beta-features section so a body-path
// access does not allocate the entire `SettingCatalog` (issue #5970).
let key = BetaFeaturesCatalogSection().customSidebars
return Bool.decodeFromUserDefaults(UserDefaults.standard.object(forKey: key.userDefaultsKey)) ?? key.defaultValue
return Bool.decodeFromUserDefaults(defaults.object(forKey: key.userDefaultsKey)) ?? key.defaultValue
}

/// Directory custom sidebars are authored into.
Expand Down
22 changes: 13 additions & 9 deletions Sources/FileExplorerState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SwiftUI
final class FileExplorerState: ObservableObject {
private static let modeKey = "rightSidebar.mode"
private static let customSidebarNameKey = "rightSidebar.customSidebarName"
private let defaults: UserDefaults

@Published var isVisible: Bool {
didSet { persistVisibility() }
Expand All @@ -18,21 +19,21 @@ final class FileExplorerState: ObservableObject {
}

private func persistVisibility() {
UserDefaults.standard.set(isVisible || isAutoCollapsed, forKey: "fileExplorer.isVisible")
defaults.set(isVisible || isAutoCollapsed, forKey: "fileExplorer.isVisible")
}
@Published var width: CGFloat {
didSet { UserDefaults.standard.set(Double(width), forKey: "fileExplorer.width") }
didSet { defaults.set(Double(width), forKey: "fileExplorer.width") }
}

/// Proportion of sidebar height allocated to the tab list (0.0-1.0).
/// The file explorer gets the remaining space below.
@Published var dividerPosition: CGFloat {
didSet { UserDefaults.standard.set(Double(dividerPosition), forKey: "fileExplorer.dividerPosition") }
didSet { defaults.set(Double(dividerPosition), forKey: "fileExplorer.dividerPosition") }
}

/// Whether hidden files (dotfiles) are shown in the tree.
@Published var showHiddenFiles: Bool {
didSet { UserDefaults.standard.set(showHiddenFiles, forKey: "fileExplorer.showHidden") }
didSet { defaults.set(showHiddenFiles, forKey: "fileExplorer.showHidden") }
}

@Published private var storedMode: RightSidebarMode
Expand All @@ -59,8 +60,8 @@ final class FileExplorerState: ObservableObject {
storedCustomSidebarName
}

init() {
let defaults = UserDefaults.standard
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
self.isVisible = defaults.bool(forKey: "fileExplorer.isVisible")
let storedWidth = defaults.double(forKey: "fileExplorer.width")
self.width = storedWidth > 0 ? CGFloat(storedWidth) : 220
Expand All @@ -79,11 +80,13 @@ final class FileExplorerState: ObservableObject {
/// `mode` set (which may reveal a user-hidden tab: CLI, palette,
/// notification routing), restore and preference changes never resurrect a
/// hidden tab.
func refreshModeAvailability(defaults: UserDefaults = .standard) {
func refreshModeAvailability(defaults: UserDefaults? = nil) {
let defaults = defaults ?? self.defaults
setMode(Self.visibleMode(storedMode, defaults: defaults), defaults: defaults)
}

func selectCustomSidebar(name rawName: String, defaults: UserDefaults = .standard) {
func selectCustomSidebar(name rawName: String, defaults: UserDefaults? = nil) {
let defaults = defaults ?? self.defaults
let name = rawName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty else { return }
storedCustomSidebarName = name
Expand Down Expand Up @@ -122,7 +125,8 @@ final class FileExplorerState: ObservableObject {
}
}

private func setMode(_ mode: RightSidebarMode, defaults: UserDefaults = .standard) {
private func setMode(_ mode: RightSidebarMode, defaults: UserDefaults? = nil) {
let defaults = defaults ?? self.defaults
let nextMode = Self.availableMode(mode, defaults: defaults)
guard storedMode != nextMode else {
if defaults.string(forKey: Self.modeKey) != nextMode.rawValue {
Expand Down
6 changes: 5 additions & 1 deletion Sources/RightSidebarMode+Availability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ extension RightSidebarMode {
}

func isAvailable(defaults: UserDefaults = .standard) -> Bool {
isAvailable(
if self == .customSidebar {
return CmuxExtensionSidebarSelection.customSidebarsEnabled(defaults: defaults)
&& FileExplorerState.persistedCustomSidebarName(defaults: defaults) != nil
}
return isAvailable(
feedEnabled: RightSidebarBetaFeatureSettings.isFeedEnabled(defaults: defaults),
machinesEnabled: CloudMachinesFeature.offMainIsEnabled(defaults: defaults),
devicesEnabled: false
Expand Down
Loading
Loading