Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
15e00f9
refactor: move the Cloud services layer into a CmuxCloud package
teamleaderleo Sep 25, 2026
2601094
Merge remote-tracking branch 'upstream/main' into refactor/cloud-slice
teamleaderleo Sep 25, 2026
bacc558
fix: restore the noVNC observer JS, expose actor constants nonisolate…
teamleaderleo Sep 25, 2026
8fdfbf2
Merge remote-tracking branch 'upstream/main' into refactor/cloud-slice
teamleaderleo Sep 25, 2026
31f65fe
fix: give CmuxCloud's nested public structs explicit public memberwis…
teamleaderleo Sep 25, 2026
4a408c7
Merge remote-tracking branch 'upstream/main' into refactor/cloud-slice
teamleaderleo Sep 25, 2026
74d3849
fix: publish CloudSidebarDebugMetrics.default
teamleaderleo Sep 25, 2026
69c6745
Merge remote-tracking branch 'upstream/main' into refactor/cloud-slice
teamleaderleo Sep 25, 2026
559c0e5
fix: give CloudLinkFirstValue a public init
teamleaderleo Sep 25, 2026
b68a174
Merge remote-tracking branch 'upstream/main' into refactor/cloud-slice
teamleaderleo Sep 25, 2026
1ca2137
fix: explicit public inits for CmuxCloud's top-level public structs
teamleaderleo Sep 25, 2026
da4fe16
Merge remote-tracking branch 'upstream/main' into refactor/cloud-slice
teamleaderleo Sep 25, 2026
fd70ab3
refactor: move the Cloud surface models into CmuxCloud
teamleaderleo Sep 25, 2026
5eaa188
fix: escape the mutation runner's validate closure and mark moved nam…
teamleaderleo Sep 25, 2026
9e68196
chore: import CmuxCloud where moved extension members are used
teamleaderleo Sep 25, 2026
0252897
Merge remote-tracking branch 'upstream/main' into refactor/cloud-surf…
teamleaderleo Sep 25, 2026
90d08c1
fix: import CmuxCloud in AppDelegate+CloudTeamPicker
teamleaderleo Sep 25, 2026
8b39130
Merge upstream/main into refactor/cloud-surface-models
teamleaderleo Sep 25, 2026
a46bb63
Merge upstream/main into refactor/cloud-surface-models
teamleaderleo Sep 25, 2026
e991f5e
Merge upstream/main into refactor/cloud-surface-models
teamleaderleo Sep 25, 2026
b190fde
Merge remote-tracking branch 'upstream/main' into refactor/cloud-surf…
teamleaderleo Sep 25, 2026
26410e5
Merge remote-tracking branch 'mf/main' into refactor/cloud-surface-mo…
teamleaderleo Sep 25, 2026
c0c5b87
Merge remote-tracking branch 'upstream/main' into refactor/cloud-surf…
teamleaderleo Sep 25, 2026
96b761e
Merge remote-tracking branch 'upstream/main' into refactor/cloud-surf…
teamleaderleo Sep 25, 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import Foundation

/// A callback's read precondition, captured before an agent computes its title.
/// Names never identify a resource: the full projection and daemon generation do.
struct CloudAgentNameContext: Hashable, Codable, Sendable {
let projection: SurfaceProjection
let generation: String
public struct CloudAgentNameContext: Hashable, Codable, Sendable {
public let projection: SurfaceProjection
public let generation: String
let nameRevision: UInt64

init?(projection: SurfaceProjection, state: CloudVMState) {
public init?(projection: SurfaceProjection, state: CloudVMState) {
guard let tabID = projection.remoteTabID,
let tab = state.lookupIndex.tab(id: tabID),
tab.contentKind == "terminal", tab.contentID == projection.resource.key,
Expand All @@ -24,19 +24,19 @@ struct CloudAgentNameContext: Hashable, Codable, Sendable {
nameRevision = authority.revision
}

init?(wire: Any?) {
public init?(wire: Any?) {
guard let wire, JSONSerialization.isValidJSONObject(wire),
let bytes = try? JSONSerialization.data(withJSONObject: wire),
let value = try? JSONDecoder().decode(Self.self, from: bytes) else { return nil }
self = value
}

var wire: [String: Any]? {
public var wire: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
}

func renameRequest(name: String) -> CloudTuiRequest {
public func renameRequest(name: String) -> CloudTuiRequest {
CloudTuiRequest("tab.rename", ["tab": projection.remoteTabID ?? "", "workspace": projection.remoteWorkspaceID ?? "",
"name": name, "source": "auto", "expected_generation": generation,
"expected_name_revision": String(nameRevision)], mutation: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
/// reports itself (`.transportClosed`, a liveness timeout); the pass only needs
/// to act on sessions that are not attached, on a changed numeric id, or on a
/// terminal that exited.
enum CloudAttachmentReconcileDecision: Equatable, Sendable {
public enum CloudAttachmentReconcileDecision: Equatable, Sendable {
/// Rebind to the resolved numeric surface (a no-op when unchanged).
case rebind(surfaceID: UInt64)
/// The terminal ended on the machine: stop the session and close its pane.
Expand All @@ -22,7 +22,7 @@ enum CloudAttachmentReconcileDecision: Equatable, Sendable {
/// Leave the working stream alone; nothing about it is in doubt.
case keep

static func decide(
public static func decide(
phase: CloudTuiManualMirrorPhase,
resolution: CloudTuiSurfaceIDResolution
) -> CloudAttachmentReconcileDecision {
Expand All @@ -41,7 +41,7 @@ enum CloudAttachmentReconcileDecision: Equatable, Sendable {
}

/// Whether the pass must arm its bounded retry because of this decision.
var needsRetry: Bool {
public var needsRetry: Bool {
if case .fence = self { return true }
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,44 @@ import Foundation
///
/// What is left exposed is only what the machine itself must hold: a root-only file on
/// its persistent volume, which forks and snapshots of that machine inherit.
enum CloudEnvDelivery {
/// lint:allow namespace-type: moved unchanged from the app target, where it was an internal static namespace; reshaping it is a separate change from this package move.
public enum CloudEnvDelivery {
/// Absolute: the daemon's own PATH is not a login shell's.
static let receiverCommand = ["/usr/local/bin/cmux", "env", "receive"]
static let receiverTitle = "cmux env"
static let readyMarker = "CMUX-ENV-READY"
static let endMarker = "CMUX-ENV-END"
static let resultPattern = "CMUX-ENV-(OK|ERR)"
public static let receiverCommand = ["/usr/local/bin/cmux", "env", "receive"]
public static let receiverTitle = "cmux env"
public static let readyMarker = "CMUX-ENV-READY"
public static let endMarker = "CMUX-ENV-END"
public static let resultPattern = "CMUX-ENV-(OK|ERR)"
/// The receiver is a shell script that starts in well under a second; a machine that
/// is waking or whose shim predates the verb shows neither marker in this time.
static let readyTimeoutMs = 15_000
static let resultTimeoutMs = 30_000
public static let readyTimeoutMs = 15_000
public static let resultTimeoutMs = 30_000
/// One `terminal write` per chunk: small enough that the receiver's line reads keep
/// pace with the PTY's input buffer, large enough that an ordinary `.env` is 1–2 writes.
static let chunkBytes = 1_024
static let maxPayloadBytes = 256 * 1_024
static let base64LineWidth = 76
public static let chunkBytes = 1_024
public static let maxPayloadBytes = 256 * 1_024
public static let base64LineWidth = 76

struct Entry: Equatable, Sendable {
let key: String
let value: String
public struct Entry: Equatable, Sendable {
public init(
key: String,
value: String
) {
self.key = key
self.value = value
}

public let key: String
public let value: String
}

enum Outcome: Equatable, Sendable {
public enum Outcome: Equatable, Sendable {
case ok(keys: Int, path: String?)
case failed(String)
}

@MainActor
static func withReceiverWorkspace(
public static func withReceiverWorkspace(
createWorkspace: @MainActor () async throws -> String,
closeWorkspace: @escaping @MainActor (String) async throws -> Void,
operation: @MainActor (String) async throws -> Outcome
Expand All @@ -74,7 +83,7 @@ enum CloudEnvDelivery {
}

@MainActor
static func removeReceiverResources(
public static func removeReceiverResources(
terminalIDs: [String],
discoverTerminalIDs: @MainActor () async throws -> [String] = { [] },
closeTerminal: @MainActor (String) async throws -> Void,
Expand All @@ -97,11 +106,19 @@ enum CloudEnvDelivery {
}
}

struct OperationAndCleanupError: Error, LocalizedError {
let operationError: any Error
let cleanupError: DeliveryError
public struct OperationAndCleanupError: Error, LocalizedError {
public init(
operationError: any Error,
cleanupError: DeliveryError
) {
self.operationError = operationError
self.cleanupError = cleanupError
}

public let operationError: any Error
public let cleanupError: DeliveryError

var errorDescription: String? {
public var errorDescription: String? {
let primaryDescription: String
if let deliveryError = operationError as? DeliveryError {
primaryDescription = deliveryError.localizedDescription
Expand All @@ -114,7 +131,7 @@ enum CloudEnvDelivery {
}
}

enum DeliveryError: Error, LocalizedError, Equatable {
public enum DeliveryError: Error, LocalizedError, Equatable {
case invalidKey(String)
case multilineValue(String)
case emptyPayload
Expand All @@ -125,7 +142,7 @@ enum CloudEnvDelivery {
case noResult(String)
case workspaceCleanupFailed(String)

var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .invalidKey(let key):
return "invalid variable name '\(key)' (keys match [A-Za-z_][A-Za-z0-9_]*)"
Expand Down Expand Up @@ -154,7 +171,7 @@ enum CloudEnvDelivery {
}
}

static func requireReady(_ response: [String: Any], machineID: String) throws {
public static func requireReady(_ response: [String: Any], machineID: String) throws {
let value = (response["value"] as? [String: Any]) ?? response
guard (value["matched"] as? Bool) == true else {
let screen = (value["text"] as? String) ?? ""
Expand All @@ -165,7 +182,7 @@ enum CloudEnvDelivery {
}
}

static func requireOutcome(_ response: [String: Any]) throws -> Outcome {
public static func requireOutcome(_ response: [String: Any]) throws -> Outcome {
let value = (response["value"] as? [String: Any]) ?? response
let screen = (value["text"] as? String) ?? ""
guard let outcome = outcome(fromScreen: screen) else {
Expand All @@ -177,12 +194,12 @@ enum CloudEnvDelivery {
return outcome
}

static func isValidKey(_ key: String) -> Bool {
public static func isValidKey(_ key: String) -> Bool {
key.range(of: "^[A-Za-z_][A-Za-z0-9_]*$", options: .regularExpression) != nil
}

/// The receiver's payload: one `KEY=VALUE` line per entry, values byte-literal.
static func payload(_ entries: [Entry]) throws -> Data {
public static func payload(_ entries: [Entry]) throws -> Data {
guard !entries.isEmpty else { throw DeliveryError.emptyPayload }
var text = ""
for entry in entries {
Expand All @@ -200,7 +217,7 @@ enum CloudEnvDelivery {
/// What gets typed into the receiver's PTY: base64 of the payload, wrapped to
/// 76 columns (a canonical-mode line is capped at 4095 bytes; keep far below it),
/// then the end marker on its own line.
static func wire(_ payload: Data) -> Data {
public static func wire(_ payload: Data) -> Data {
let encoded = payload.base64EncodedString()
var lines: [String] = []
var index = encoded.startIndex
Expand All @@ -215,7 +232,7 @@ enum CloudEnvDelivery {

/// `wire` split for one `terminal write` per piece. Cut points fall anywhere: the
/// receiver reassembles lines from the byte stream, not from write boundaries.
static func chunks(_ wire: Data, size: Int = chunkBytes) -> [Data] {
public static func chunks(_ wire: Data, size: Int = chunkBytes) -> [Data] {
guard size > 0, !wire.isEmpty else { return wire.isEmpty ? [] : [wire] }
var pieces: [Data] = []
var offset = 0
Expand All @@ -229,7 +246,7 @@ enum CloudEnvDelivery {

/// The receiver's verdict, from the screen text after `CMUX-ENV-(OK|ERR)` matched.
/// The LAST such line wins: a retried receiver's earlier lines may still be visible.
static func outcome(fromScreen text: String) -> Outcome? {
public static func outcome(fromScreen text: String) -> Outcome? {
var result: Outcome?
for rawLine in text.split(whereSeparator: { $0 == "\n" || $0 == "\r" }) {
let line = rawLine.trimmingCharacters(in: .whitespaces)
Expand All @@ -252,7 +269,7 @@ enum CloudEnvDelivery {

/// A shim without the verb falls through to cmux-tui, which answers with an unknown
/// resource scope; the shim's own dispatcher says "unknown env command".
static func looksLikeOutdatedShim(_ screen: String) -> Bool {
public static func looksLikeOutdatedShim(_ screen: String) -> Bool {
screen.contains("unknown resource scope") || screen.contains("unknown env command")
}
}
Loading
Loading