From 528d92f06ccbafa810fd1e6dceabdcdee6957f91 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 13 Sep 2026 17:01:29 -0700 Subject: [PATCH] fix: deduplicate Cloud provider implementations --- Sources/Cloud/CloudTreeCellView.swift | 2 +- .../CmuxTuiSurfaceProvider+TerminalIO.swift | 40 ---- ...uiSurfaceProvider+TerminalPrimitives.swift | 6 +- ...uiSurfaceProvider+WorkspaceLifecycle.swift | 28 --- .../Surfaces/CmuxTuiSurfaceProviders.swift | 180 ------------------ .../Surfaces/Workspace+CloudPaneRouting.swift | 15 ++ 6 files changed, 19 insertions(+), 252 deletions(-) diff --git a/Sources/Cloud/CloudTreeCellView.swift b/Sources/Cloud/CloudTreeCellView.swift index 01862addfb44..04a6b793693d 100644 --- a/Sources/Cloud/CloudTreeCellView.swift +++ b/Sources/Cloud/CloudTreeCellView.swift @@ -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) ) diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift index c13694d54a50..ed41109924ff 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalIO.swift @@ -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]) ?? [:] - } - } diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalPrimitives.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalPrimitives.swift index 1d598300db96..c93038b07dc9 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalPrimitives.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+TerminalPrimitives.swift @@ -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. diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+WorkspaceLifecycle.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+WorkspaceLifecycle.swift index 5a325b2689a7..80ab3b766b3a 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+WorkspaceLifecycle.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+WorkspaceLifecycle.swift @@ -2,34 +2,6 @@ import Foundation @MainActor extension CmuxTuiSurfaceProvider { - /// `terminal 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. diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index 95349b617eb6..0778f04c99b9 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -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, @@ -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 { diff --git a/Sources/Surfaces/Workspace+CloudPaneRouting.swift b/Sources/Surfaces/Workspace+CloudPaneRouting.swift index a39d5bc62d80..65e97a3ef88b 100644 --- a/Sources/Surfaces/Workspace+CloudPaneRouting.swift +++ b/Sources/Surfaces/Workspace+CloudPaneRouting.swift @@ -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