Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9e24d37
refactor: move the surface catalog's value types into a package
teamleaderleo Sep 24, 2026
1edb2c1
Justify the three static namespaces that moved into the catalog package
teamleaderleo Sep 24, 2026
4519d36
refactor: move the cloud tui transport into a CmuxCloudTui package
teamleaderleo Sep 24, 2026
0723906
Merge remote-tracking branch 'upstream/main' into wt/catalog
teamleaderleo Sep 24, 2026
bb7a8ff
Justify the two static namespaces that moved into CmuxCloudTui
teamleaderleo Sep 24, 2026
abf74d2
Merge remote-tracking branch 'upstream/refactor/surface-catalog-model…
teamleaderleo Sep 24, 2026
b89b3bc
Merge remote-tracking branch 'upstream/main' into wt/cloudtui
teamleaderleo Sep 24, 2026
86034c7
ci: give CmuxCloudTui's package tests GhosttyKit
teamleaderleo Sep 24, 2026
ff7f77a
test: import CmuxSurfaceCatalogModel outside the cmux_DEV block
teamleaderleo Sep 24, 2026
7aaa003
Merge commit 'ff7f77a8f4bceda26887334b3c854acd87879aac' into wt/cloudtui
teamleaderleo Sep 24, 2026
415c751
build: link CmuxTerminal into cmuxTests and stub libghostty for CmuxC…
teamleaderleo Sep 24, 2026
5a47902
Merge remote-tracking branch 'upstream/main' into wt/cloudtui
teamleaderleo Sep 24, 2026
058b8cb
Merge remote-tracking branch 'upstream/main' into wt/catalog
teamleaderleo Sep 24, 2026
8e4d588
Merge commit '058b8cb90e3b4ae38fe2bf0c3d8897117a1f4ed5' into wt/cloudtui
teamleaderleo Sep 24, 2026
fcd0019
Merge main (#13135 squashed as 1fc4b83688) into refactor/cloud-tui-pa…
teamleaderleo Sep 24, 2026
280279d
build: resolve CmuxCloudTui through the host app instead of linking i…
teamleaderleo Sep 24, 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
5 changes: 3 additions & 2 deletions .github/workflows/ci-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2268,6 +2268,7 @@ jobs:
CmuxBrowser
CmuxCanvasUI
CmuxCloudMachines
CmuxCloudTui
CmuxComputerUse
CmuxCore
CmuxRemoteDaemon
Expand Down Expand Up @@ -2330,7 +2331,7 @@ jobs:
echo "selected_packages=$selected" >> "$GITHUB_OUTPUT"
echo "selected_count=$count" >> "$GITHUB_OUTPUT"

if grep -qxE 'CmuxTerminal|CmuxTerminalCore' "$selected"; then
if grep -qxE 'CmuxTerminal|CmuxTerminalCore|CmuxCloudTui' "$selected"; then
echo "needs_ghosttykit=true" >> "$GITHUB_OUTPUT"
else
echo "needs_ghosttykit=false" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -2577,7 +2578,7 @@ jobs:
CmuxAgentChat|CmuxAuthRuntime|CmuxFoundation|CmuxIrohTransport|CmuxIrxTransport)
./scripts/ci/run-swift-testing-suites.sh "$pkgdir"
;;
CmuxTerminal|CmuxTerminalCore)
CmuxTerminal|CmuxTerminalCore|CmuxCloudTui)
test_status=0
output="$(swift test "${swift_test_args[@]}" 2>&1)" || test_status=$?
printf '%s\n' "$output"
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/cloud-command-deadlines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ on:
pull_request:
paths:
- Sources/Cloud/CloudTuiPersistent*.swift
- Sources/Cloud/CloudTuiManualIO*.swift
- Packages/macOS/CmuxCloudTui/Sources/CmuxCloudTui/CloudTuiManualIO*.swift
- Packages/macOS/CmuxCloudTui/Sources/CmuxCloudTui/CloudTuiPersistentRequestBuilder.swift
- Packages/macOS/CmuxSurfaceCatalogModel/Sources/CmuxSurfaceCatalogModel/CloudTuiTerminalProjectionTarget.swift
- Sources/Cloud/CloudTuiRemoteColors.swift
- Packages/macOS/CmuxCloudTui/Sources/CmuxCloudTui/CloudTuiRemoteColors.swift
- cmuxTests/CloudCommand*.swift
- cmuxTests/CloudTuiManualIOConnectionTests.swift
- tests/fixtures/cloud-command-deadlines/**
Expand Down
30 changes: 30 additions & 0 deletions Packages/macOS/CmuxCloudTui/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
name: "CmuxCloudTui",
platforms: [.macOS(.v14)],
products: [.library(name: "CmuxCloudTui", targets: ["CmuxCloudTui"])],
dependencies: [
.package(path: "../CmuxCloudImagePaste"),
.package(path: "../CmuxFoundation"),
.package(path: "../CmuxSurfaceCatalogModel"),
.package(path: "../CmuxTerminal")
],
targets: [
.target(
name: "CmuxCloudTui",
dependencies: ["CmuxCloudImagePaste", "CmuxFoundation", "CmuxSurfaceCatalogModel", "CmuxTerminal"],
// The files moved out of the app target unchanged; keep its language mode.
swiftSettings: [.swiftLanguageMode(.v5)]
),
.testTarget(
name: "CmuxCloudTuiTests",
dependencies: [
"CmuxCloudTui",
// CmuxTerminal binds libghostty, which SwiftPM cannot link here.
.product(name: "GhosttyRuntimeTestStubs", package: "CmuxTerminal"),
]
)
]
)
27 changes: 27 additions & 0 deletions Packages/macOS/CmuxCloudTui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# CmuxCloudTui

The cmux-tui client transport for cloud terminals: request and argv builders for the daemon, the manual-IO socket (framing, decoding, input routing, resize coalescing), the manual-mirror deadlines and watchdog, the remote color document, and the on-disk client paths. No app imports; depends on `CmuxSurfaceCatalogModel` (cursor and projection targets), `CmuxTerminal` (raw sizing samples and manual input), `CmuxCloudImagePaste` and `CmuxFoundation`.

```swift
import CmuxCloudTui

var scheduler = CloudTuiManualIOResizeScheduler()
if let grid = CloudTuiManualIOGrid(columns: 120, rows: 40),
let send = scheduler.sample(grid, canSend: true) {
connection.send(CloudTuiManualIOCommand().resize(surfaceID: id, columns: send.columns, rows: send.rows))
}
```

## Moved files

From `Sources/Cloud/`: `CloudTuiClientPaths.swift`, `CloudTuiCommandLine.swift`, `CloudTuiCommandLine+Placement.swift`, `CloudTuiLegacySnapshotParser.swift`, `CloudTuiManualIOCommand.swift`, `CloudTuiManualIOConnection.swift`, `CloudTuiManualIODescriptorLease.swift`, `CloudTuiManualIOFrame.swift`, `CloudTuiManualIOFrameDecoder.swift`, `CloudTuiManualIOGrid.swift`, `CloudTuiManualIOGrid+SizingSample.swift`, `CloudTuiManualIOInputRouter.swift`, `CloudTuiManualIOResizeScheduler.swift`, `CloudTuiManualMirrorDeadlines.swift`, `CloudTuiManualMirrorPhase.swift`, `CloudTuiManualMirrorRequestKind.swift`, `CloudTuiManualMirrorWatchdog.swift`, `CloudTuiPersistentRequestBuilder.swift`, `CloudTuiRemoteColors.swift`, `CloudTuiResolvedSurface.swift`.

## What stays in the app

- `CloudTuiManualMirrorSession` (and `+Capabilities`): it binds a `TerminalSurface`.
- `CloudTuiCommandRunning`, `CloudTuiDaemonAnswer`, `CloudTuiPersistentResourceConnection`: they speak `CloudMachineLink.LinkError`, which is nested in the app's `CloudMachineLink`. Moving `LinkError` out is the next step for them.

## What changed besides the move

- Declarations became `public`. Structs that relied on the synthesized initializer (`CloudTuiClientPaths.DeviceRecord`, `CloudTuiLegacySnapshotParser`, `CloudTuiManualIOFrameDecoder`, `CloudTuiManualIOResizeScheduler`) got an explicit `public init` with the same parameters.
- `CloudTuiManualIOResizeScheduler`'s `private(set)` state is publicly readable, as the app's tests read it.
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,51 @@ import Security
/// `cmux vm-tui-connect` must present the same device to a machine's daemon, so one
/// enrollment covers both. The CLI helpers are compiled only into the CLI target, hence
/// the duplicate paths here.
struct CloudTuiClientPaths: Sendable {
public struct CloudTuiClientPaths: Sendable {
/// One enrolled device per machine, as the CLI stores it (`vm-tui-devices.json`).
struct DeviceRecord: Codable, Sendable, Equatable {
let deviceFingerprint: String
let updatedAtUnix: Int
public struct DeviceRecord: Codable, Sendable, Equatable {
public let deviceFingerprint: String
public let updatedAtUnix: Int

public init(deviceFingerprint: String, updatedAtUnix: Int) {
self.deviceFingerprint = deviceFingerprint
self.updatedAtUnix = updatedAtUnix
}
}

/// Stored in place of a device fingerprint for a machine this Mac reaches over
/// the trusted-carrier listener: there is no enrolled device, and the next link
/// dials `--carrier` again with no control-plane call. A real fingerprint means
/// the Mac enrolled before the machine's daemon served the trusted listener and
/// keeps presenting its stored key. Mirrored by the CLI's own store.
static let carrierDeviceMarker = "carrier"
public static let carrierDeviceMarker = "carrier"

let home: URL
public let home: URL

init(home: URL = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) {
public init(home: URL = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) {
self.home = home
}

/// Per-Mac cmux-tui client state (device key, known daemons).
var stateDir: URL {
public var stateDir: URL {
home.appendingPathComponent(".cmuxterm", isDirectory: true)
.appendingPathComponent("cmux-tui-client", isDirectory: true)
}

var devicesStoreURL: URL {
public var devicesStoreURL: URL {
home.appendingPathComponent(".cmuxterm", isDirectory: true)
.appendingPathComponent("vm-tui-devices.json", isDirectory: false)
}

func ensureStateDir() throws {
public func ensureStateDir() throws {
try FileManager.default.createDirectory(
at: stateDir,
withIntermediateDirectories: true,
attributes: [.posixPermissions: 0o700]
)
}

func loadDevices() -> [String: DeviceRecord] {
public func loadDevices() -> [String: DeviceRecord] {
guard let data = try? Data(contentsOf: devicesStoreURL),
let store = try? JSONDecoder().decode([String: DeviceRecord].self, from: data) else {
return [:]
Expand All @@ -59,11 +64,11 @@ struct CloudTuiClientPaths: Sendable {
/// one per install: `mac-` plus 32 hex digits, minted on first use and kept in
/// the client state dir beside the device key. Every machine sees the same id,
/// so per-client read state follows the install, not the machine.
var notificationClientIDURL: URL {
public var notificationClientIDURL: URL {
stateDir.appendingPathComponent("notification-client-id", isDirectory: false)
}

func notificationClientID() -> String {
public func notificationClientID() -> String {
if let existing = try? String(contentsOf: notificationClientIDURL, encoding: .utf8) {
let trimmed = existing.trimmingCharacters(in: .whitespacesAndNewlines)
if Self.isValidNotificationClientID(trimmed) { return trimmed }
Expand All @@ -78,16 +83,16 @@ struct CloudTuiClientPaths: Sendable {
}

/// The daemon's rule: 1 to 128 printable ASCII bytes, no spaces.
static func isValidNotificationClientID(_ value: String) -> Bool {
public static func isValidNotificationClientID(_ value: String) -> Bool {
!value.isEmpty && value.utf8.count <= 128
&& value.utf8.allSatisfy { $0 > 0x20 && $0 < 0x7f }
}

func deviceFingerprint(for machineID: String) -> String? {
public func deviceFingerprint(for machineID: String) -> String? {
loadDevices()[machineID]?.deviceFingerprint
}

func saveDeviceFingerprint(_ fingerprint: String, for machineID: String) {
public func saveDeviceFingerprint(_ fingerprint: String, for machineID: String) {
var store = loadDevices()
store[machineID] = DeviceRecord(deviceFingerprint: fingerprint, updatedAtUnix: Int(Date().timeIntervalSince1970))
guard let data = try? JSONEncoder().encode(store) else { return }
Expand All @@ -97,14 +102,14 @@ struct CloudTuiClientPaths: Sendable {
}

/// The app and CLI share a local label that never needs hostname resolution.
static func deviceName(hostName: String? = nil) -> String {
public static func deviceName(hostName: String? = nil) -> String {
(hostName.map { RemoteClientDeviceName(hostName: $0) } ?? RemoteClientDeviceName()).value
}

/// The cmux-tui client the app drives: the bundled one
/// (`Contents/Resources/bin/cmux-tui`, installed by scripts/install-cmux-tui-client.sh),
/// else `CMUX_TUI_CLIENT`. No PATH search: the app must not pick up a stray binary.
static func clientURL(
public static func clientURL(
bundle: Bundle = .main,
environment: [String: String] = ProcessInfo.processInfo.environment
) -> URL? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ extension CloudTuiCommandLine {
/// `tab <tab_id> move --workspace <ws> --screen <screen> --pane <pane> --index <n>`
/// (spec `tab.move`, the destination grammar `terminal … project` uses): re-parents
/// one placement; the terminal or browser behind it is untouched.
static func moveTabArguments(
public static func moveTabArguments(
socketPath: String,
tabID: String,
target: CloudTuiTerminalProjectionTarget,
Expand Down
Loading
Loading