Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 Sources/Cloud/CloudTreeCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ final class CloudTreeCellView: NSTableCellView {
let showsHelp = showsCloudVPNWarning && node.isPortsGroup
displayHost.isHidden = showsCallout
displayHost.rootView = AnyView(
CloudTreeRowContentView(kind: node.kind, style: style, showsCloudVPNWarning: showsCloudVPNWarning)
CloudTreeRowContentView(kind: node.kind, style: style)
.modifier(CloudSidebarRowDecoration(isPinned: node.isPinned, showsAttentionSlot: node.showsAttentionSlot, hasUnreadNotification: node.hasUnreadAttention))
.frame(maxWidth: .infinity, alignment: .leading)
)
Expand Down
40 changes: 0 additions & 40 deletions Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,6 @@ extension CmuxTuiSurfaceProvider {
}
}

extension CmuxTuiSurfaceProvider {
/// Type `text` into the remote terminal exactly as given (no newline appended).
func sendText(terminalID: String, text: String) async throws {
try await writeBytes(terminalID: terminalID, data: Data(text.utf8))
}


/// Press named keys (`enter`, `ctrl+c`, …) in the remote terminal, in order.
func sendKeys(terminalID: String, keys: [String]) async throws {
let connected = try await links.connected(machineID: machineID)
guard let link = await links.link(machineID: machineID) else { throw ProviderError.machineAsleep(machineID) }
_ = try await link.run(arguments: CloudTuiCommandLine.keysArguments(socketPath: connected.socketPath, terminalID: terminalID, keys: keys))
}

/// The remote terminal's visible screen, as the daemon reports it
/// (`cols`, `rows`, `cursor_row`, `cursor_col`, `cursor_visible`, `text`).
func readScreen(terminalID: String) async throws -> [String: Any] {
let connected = try await links.connected(machineID: machineID)
guard let link = await links.link(machineID: machineID) else { throw ProviderError.machineAsleep(machineID) }
let data = try await link.run(arguments: CloudTuiCommandLine.screenReadArguments(socketPath: connected.socketPath, terminalID: terminalID))
return (try JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:]
}

/// Block until the screen matches `pattern` (or the daemon-side timeout elapses):
/// `{matched, text}`. The link call itself is given headroom beyond the timeout.
func waitForScreen(terminalID: String, pattern: String, timeoutMs: Int?) async throws -> [String: Any] {
let connected = try await links.connected(machineID: machineID)
guard let link = await links.link(machineID: machineID) else { throw ProviderError.machineAsleep(machineID) }
// Non-positive requests mean the daemon default, so the link headroom is computed
// from the same value the daemon will use; huge requests are clamped so the
// Duration math cannot overflow.
let effectiveMs = Self.clampedWaitTimeoutMs(timeoutMs)
let linkTimeout = Duration.milliseconds(effectiveMs + 5_000)
let data = try await link.run(
arguments: CloudTuiCommandLine.screenWaitArguments(socketPath: connected.socketPath, terminalID: terminalID, pattern: pattern, timeoutMs: effectiveMs),
timeout: linkTimeout
)
return (try JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:]
}

}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ extension CmuxTuiSurfaceProvider {

/// Type `text` into the remote terminal exactly as given (no newline appended).
func sendText(terminalID: String, text: String) async throws {
let connected = try await links.connected(machineID: machineID)
guard let link = await links.link(machineID: machineID) else { throw ProviderError.machineAsleep(machineID) }
_ = try await link.run(arguments: CloudTuiCommandLine.writeArguments(socketPath: connected.socketPath, terminalID: terminalID, text: text))
// Keep payload bytes out of argv so terminal input cannot be truncated or
// exposed through process listings when it contains secrets or large text.
try await writeBytes(terminalID: terminalID, data: Data(text.utf8))
}

/// Press named keys (`enter`, `ctrl+c`, …) in the remote terminal, in order.
Expand Down
28 changes: 0 additions & 28 deletions Sources/Surfaces/CmuxTuiSurfaceProvider+WorkspaceLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,6 @@ import Foundation

@MainActor
extension CmuxTuiSurfaceProvider {
/// `terminal <id> close`; a terminal whose process already exited is gone from
/// cmux-tui's selectors, so its tab is closed instead. Either way the resource
/// leaves the catalog now and the next snapshot confirms.
func closeTerminal(_ id: SurfaceResourceID) async throws {
try await closeTerminal(id, fallbackTabID: nil)
}

func closeTerminal(_ id: SurfaceResourceID, fallbackTabID: String?) async throws {
pendingRemoteCreations.removeValue(forKey: id)
do {
_ = try await runCloseCommand { CloudTuiCommandLine.closeTerminalArguments(socketPath: $0, terminalID: id.key) }
} catch {
guard let tabID = fallbackTabID ?? tabByTerminal[id.key], Self.isSelectorNotFound(error) else { throw error }
_ = try await runCloseCommand { CloudTuiCommandLine.closeTabArguments(socketPath: $0, tabID: tabID) }
}
closeLocalPanes(showing: [id])
catalog.remove(id, from: self)
scheduleRefresh()
}

/// A closed terminal has no pane to show any more: every local pane that projected it
/// goes too, instead of lingering as a dead attach the person has to close by hand.
private func closeLocalPanes(showing ids: [SurfaceResourceID]) {
let wanted = Set(ids)
for projection in catalog.snapshot.projections where wanted.contains(projection.resource) {
SurfacePaneFactory.close(panelID: projection.panelID, in: projection.workspaceID)
}
}

/// Runs one close-family command, reconnecting and retrying once when the attempt
/// died with the link. Close verbs are idempotent, so the retry is safe.
Expand Down
180 changes: 0 additions & 180 deletions Sources/Surfaces/CmuxTuiSurfaceProviders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -688,178 +688,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider {
}
}

/// Merges pending mutation receipts into derived rows until an accepted
/// graph reaches each receipt. The canonical graph is never edited here.
/// A generation change, or a cursorless snapshot after a versioned receipt,
/// retires the overlay because the old placement cannot be proven to exist.
private func resourcesWithPendingCreations(
_ resources: [SurfaceResource],
state: CloudVMState?
) -> [SurfaceResource] {
var merged = resources
var completed: [SurfaceResourceID] = []
for (resourceID, pending) in pendingRemoteCreations where resourceID.machine == machine {
if let state {
if let receipt = pending.receipt {
guard let cursor = state.cursor,
cursor.generation == receipt.generation else {
completed.append(resourceID)
continue
}
if cursor.revision >= receipt.revision {
// At or beyond the commit, the accepted graph is the
// source of truth, including an intentional close.
completed.append(resourceID)
continue
}
} else if pendingCreationIsVisible(pending, in: state) {
// Legacy mutation responses have no ordering fence. Stop
// overlaying as soon as the exact path is observed.
completed.append(resourceID)
continue
}
}
mergePendingCreation(pending, into: &merged)
}
for resourceID in completed {
pendingRemoteCreations.removeValue(forKey: resourceID)
}
return merged
}

private func pendingCreationIsVisible(
_ pending: PendingRemoteCreation,
in state: CloudVMState
) -> Bool {
guard state.lookupIndex.terminal(id: pending.resource.id.key) != nil else { return false }
guard let tabID = pending.tabID else { return true }
return state.lookupIndex.tab(id: tabID) != nil
}

private func mergePendingCreation(
_ pending: PendingRemoteCreation,
into resources: inout [SurfaceResource]
) {
guard let pendingView = pending.resource.remoteViews?.first else {
if !resources.contains(where: { $0.id == pending.resource.id }) {
resources.append(pending.resource)
}
return
}
guard let index = resources.firstIndex(where: { $0.id == pending.resource.id }) else {
resources.append(pending.resource)
return
}
var resource = resources[index]
var views = resource.remoteViews ?? []
if !views.contains(where: { $0.tabID == pendingView.tabID }) {
views.append(pendingView)
resource.remoteViews = views
if resource.remoteWorkspace == nil {
resource.remoteWorkspace = pendingView.workspace
}
}
resources[index] = resource
}

private func remoteWorkspaces(for state: CloudVMState?) -> [SurfaceRemoteWorkspace]? {
var result = state.map(Self.remoteWorkspaces) ?? info.remoteWorkspaces ?? []
var seen = Set(result.map(\.id))
for pending in pendingRemoteCreations.values {
guard let workspace = pending.resource.remoteWorkspace,
seen.insert(workspace.id).inserted else { continue }
result.append(workspace)
}
return result.isEmpty ? nil : result
}

private func pendingMutationMetadata() -> [CloudVMPendingMutation] {
var writes = pendingRemoteCreations.map { resourceID, pending in
CloudVMPendingMutation(
kind: .terminalCreate,
resource: resourceID,
remoteWorkspaceID: pending.resource.remoteWorkspace?.id,
remoteTabID: pending.tabID,
name: pending.resource.remoteViews?.first?.name,
receipt: pending.receipt
)
}
writes.append(contentsOf: pendingRemoteRenames.map { key, pending in
switch key {
case .workspace(let id):
return CloudVMPendingMutation(
kind: .workspaceRename,
resource: nil,
remoteWorkspaceID: id,
remoteTabID: nil,
name: pending.name,
receipt: pending.receipt
)
case .tab(let id):
return CloudVMPendingMutation(
kind: .tabRename,
resource: nil,
remoteWorkspaceID: nil,
remoteTabID: id,
name: pending.name,
receipt: pending.receipt
)
}
})
return writes.sorted { left, right in
if left.kind.rawValue != right.kind.rawValue {
return left.kind.rawValue < right.kind.rawValue
}
let leftID = left.resource?.rawValue ?? left.remoteWorkspaceID ?? left.remoteTabID ?? ""
let rightID = right.resource?.rawValue ?? right.remoteWorkspaceID ?? right.remoteTabID ?? ""
return leftID < rightID
}
}

private func observationWithPendingWrites(
_ base: CloudVMStateObservation = .current
) -> CloudVMStateObservation {
var observation = base
let pending = pendingMutationMetadata()
observation.pendingWrites = pending.isEmpty ? nil : pending
return observation
}

private func publishPendingMutationMetadata() {
catalog.updateCloudPendingWrites(
on: machine,
writes: pendingMutationMetadata(),
from: self
)
}

private func pendingCreation(for resourceID: SurfaceResourceID) -> PendingRemoteCreation? {
pendingRemoteCreations[resourceID]
}

func pendingCreation(forTabID tabID: String) -> PendingRemoteCreation? {
pendingRemoteCreations.values.first { $0.tabID == tabID }
}

/// Advances a pending receipt after a follow-up rename commits before the
/// creation snapshot arrives. This keeps the optimistic row and its tab
/// label coherent without inventing a second canonical graph.
func recordPendingRename(tabID: String, name: String, revision: UInt64) {
for resourceID in Array(pendingRemoteCreations.keys) {
guard var pending = pendingRemoteCreations[resourceID], pending.tabID == tabID else { continue }
if let receipt = pending.receipt {
guard revision >= receipt.revision else { continue }
pending.receipt = CloudVMCursor(generation: receipt.generation, revision: revision)
}
if var views = pending.resource.remoteViews,
let viewIndex = views.firstIndex(where: { $0.tabID == tabID }) {
views[viewIndex].name = name
pending.resource.remoteViews = views
}
pendingRemoteCreations[resourceID] = pending
}
publishPendingMutationMetadata()
}

private func recordPendingRemoteRename(
workspaceID: String,
Expand Down Expand Up @@ -1414,14 +1242,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider {
}
}

/// Compatibility fallback for callers that only have a terminal identity. A terminal with
/// several views has no safe implicit placement. Returning nil keeps the projection
/// placement-neutral until a caller supplies an exact tab id.
static func defaultRemoteView(for resource: SurfaceResource) -> SurfaceRemoteView? {
guard resource.kind != .display, let views = resource.remoteViews, views.count == 1 else { return nil }
return views[0]
}

private func attachCommand(terminalID: String) async throws -> String {
let connected = try await links.connected(machineID: machineID)
guard let clientURL = CloudTuiClientPaths.clientURL() else {
Expand Down
15 changes: 15 additions & 0 deletions Sources/Surfaces/Workspace+CloudPaneRouting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,21 @@ final class CloudWorkspaceRenameService {
return (found.0, found.1)
}

/// Converts a local title into the daemon name, removing only the generated
/// machine prefix used by legacy unbound Cloud workspaces.
func remoteName(
fromLocalTitle title: String,
machine: SurfaceMachineID,
stripGeneratedPrefix: Bool = true
) -> String? {
var name = title.trimmingCharacters(in: .whitespacesAndNewlines)
let prefix = "\(machine.rawValue): "
if stripGeneratedPrefix, name.hasPrefix(prefix) {
name = String(name.dropFirst(prefix.count)).trimmingCharacters(in: .whitespacesAndNewlines)
}
return name.isEmpty ? nil : name
}

/// Resolves the daemon tab represented by one local projection. An explicit
/// tab id is authoritative. A legacy projection may infer a tab only when
/// its workspace id agrees with the resource's sole current view. A stale
Expand Down
Loading