diff --git a/Sources/AppDelegate+CloudTerminalNavigation.swift b/Sources/AppDelegate+CloudTerminalNavigation.swift new file mode 100644 index 000000000000..20cc2d8a592e --- /dev/null +++ b/Sources/AppDelegate+CloudTerminalNavigation.swift @@ -0,0 +1,15 @@ +import Foundation + +extension AppDelegate { + /// Keeps focus and workspace teardown with the live app owner at effect time. + static func makeCloudTerminalNavigationHost() -> CloudTerminalNavigationHost { + CloudTerminalNavigationHost( + focus: { panelID, workspaceID in SurfacePaneFactory.focus(panelID: panelID, in: workspaceID) }, + closeWorkspace: { workspaceID in + guard let manager = AppDelegate.shared?.tabManagerFor(tabId: workspaceID), + let workspace = manager.tabs.first(where: { $0.id == workspaceID }) else { return } + _ = manager.closeWorkspaceNonInteractively(workspace, recordHistory: false, allowPinned: true) + } + ) + } +} diff --git a/Sources/Cloud/CloudTerminalNavigationCatalog.swift b/Sources/Cloud/CloudTerminalNavigationCatalog.swift new file mode 100644 index 000000000000..6b242ce783f4 --- /dev/null +++ b/Sources/Cloud/CloudTerminalNavigationCatalog.swift @@ -0,0 +1,13 @@ +import Foundation + +/// Catalog capabilities consumed by terminal navigation, without owning a catalog or its stores. +@MainActor +protocol CloudTerminalNavigationCatalog: AnyObject { + func checkCloudWorkspaceNavigation(machine: SurfaceMachineID, workspaceID: String) throws + func localWorkspaceShowing(remoteWorkspaceID: String, placements: [SurfaceResourcePlacement]) -> UUID? + func projectTerminal(_ resource: SurfaceResourceID, in workspaceID: UUID, view: SurfaceRemoteView?) async throws -> SurfaceProjection + func terminalWorkspaceLayout(machine: SurfaceMachineID, workspaceID: String) async -> SurfaceProjectionLayout? + func openTerminalWorkspace(_ group: SurfaceResourceGroup, title: String, layout: SurfaceProjectionLayout?) async throws + -> (workspaceID: UUID, projections: [SurfaceProjection]) + func bindTerminalWorkspace(localWorkspaceID: UUID, machine: SurfaceMachineID, remoteWorkspaceID: String, generatedTitle: String) +} diff --git a/Sources/Cloud/CloudTerminalNavigationHost.swift b/Sources/Cloud/CloudTerminalNavigationHost.swift new file mode 100644 index 000000000000..b1fa711685bc --- /dev/null +++ b/Sources/Cloud/CloudTerminalNavigationHost.swift @@ -0,0 +1,8 @@ +import Foundation + +/// App-owned presentation effects used after catalog navigation succeeds or is cancelled. +@MainActor +struct CloudTerminalNavigationHost { + var focus: @MainActor (_ panelID: UUID, _ workspaceID: UUID) -> Void + var closeWorkspace: @MainActor (UUID) -> Void +} diff --git a/Sources/Cloud/CloudTerminalNavigationScheduling.swift b/Sources/Cloud/CloudTerminalNavigationScheduling.swift new file mode 100644 index 000000000000..e4731e2f75b2 --- /dev/null +++ b/Sources/Cloud/CloudTerminalNavigationScheduling.swift @@ -0,0 +1,10 @@ +import Foundation + +/// Submits one navigation per workspace to the app's existing operation owner. +@MainActor +protocol CloudTerminalNavigationScheduling: AnyObject { + typealias Operation = @MainActor () async throws -> Void + + @discardableResult + func start(key: String, _ operation: @escaping Operation) -> Bool +} diff --git a/Sources/Cloud/CloudTreeNodeActions+WorkspaceLifecycle.swift b/Sources/Cloud/CloudTreeNodeActions+WorkspaceLifecycle.swift index 67490433fef0..7723fcb8660a 100644 --- a/Sources/Cloud/CloudTreeNodeActions+WorkspaceLifecycle.swift +++ b/Sources/Cloud/CloudTreeNodeActions+WorkspaceLifecycle.swift @@ -7,8 +7,7 @@ extension CloudTreeNodeActions { /// machine's raw provider id. `hostName` (the machine's friendly label) /// only shows up when the workspace itself has no name to show. static func localWorkspaceTitle(hostName: String, group: SurfaceResourceGroup) -> String { - let name = group.title.trimmingCharacters(in: .whitespacesAndNewlines) - return name.isEmpty ? hostName : name + group.localWorkspaceTitle(hostName: hostName) } /// The machine's friendly label — `SurfaceMachineInfo.name` (the same /// preferred name its own sidebar row shows), never the raw provider VM diff --git a/Sources/Cloud/CloudTreeNodeActions.swift b/Sources/Cloud/CloudTreeNodeActions.swift index eabb6f7dd5c8..5cb03ed26468 100644 --- a/Sources/Cloud/CloudTreeNodeActions.swift +++ b/Sources/Cloud/CloudTreeNodeActions.swift @@ -53,6 +53,7 @@ struct CloudTreeNodeActions { @MainActor static func bound( + navigationHost: CloudTerminalNavigationHost, catalog: @escaping @MainActor () -> SurfaceCatalog, selectedWorkspaceID: @escaping @MainActor () -> UUID?, selectLocalWorkspace: @escaping @MainActor (UUID) -> Void, @@ -410,10 +411,13 @@ struct CloudTreeNodeActions { ) actions.organize = { action, id, _ in catalog().organizeSidebar(action, nodeID: id) } actions.refreshMachine = refreshMachine - let navigationRun: CloudTreeTerminalNavigationCoordinator.Run = run + let navigationRun: CloudTreeTerminalNavigationCoordinator.Run = { label, operation in + run(label) { catalog in try await operation(catalog) } + } let navigation = CloudTreeTerminalNavigationCoordinator( machineName: machineName, run: navigationRun, + host: navigationHost, operationController: operationController ?? AppDelegate.shared?.cloudWorkspaceOperationController ) actions.openRemoteTerminal = { navigation.open(machine: $0, group: $1, resource: $2, view: $3, openIn: $4) } diff --git a/Sources/Cloud/CloudTreeOutlineView+TerminalNavigation.swift b/Sources/Cloud/CloudTreeOutlineView+TerminalNavigation.swift new file mode 100644 index 000000000000..01124f9fc2f5 --- /dev/null +++ b/Sources/Cloud/CloudTreeOutlineView+TerminalNavigation.swift @@ -0,0 +1,27 @@ +import Foundation + +extension CloudTreeOutlineView.Coordinator { + /// Resolves a terminal row's workspace parent before dispatching its open verb. + func openTerminalRow(_ node: CloudTreeNode, row: CloudTreeTerminalRow) { + if let parent = outlineView?.parent(forItem: node) as? CloudTreeNode, + case .workspace(let machine, let workspace, _, _, let openIn) = parent.kind { + guard machine == row.resource.machine, + let group = parent.dragGroup, + group.remoteWorkspaceID == workspace.id, + row.remoteView?.workspace.id == nil || row.remoteView?.workspace.id == workspace.id else { + #if DEBUG + cmuxDebugLog("cloudTree.open terminal staleOwner resource=\(row.resource.id.rawValue)") + #endif + return + } + nodeActions.openRemoteTerminal(machine, group, row.resource.id, row.remoteView, openIn) + } else if let view = row.remoteView { + #if DEBUG + cmuxDebugLog("cloudTree.open terminal missingOwner resource=\(row.resource.id.rawValue) view=\(view.tabID)") + #endif + } else { + // Pool terminals have no owner; retain their selected-workspace behavior. + nodeActions.project(row.resource.id, .tab, true) + } + } +} diff --git a/Sources/Cloud/CloudTreeStyleGalleryWindow.swift b/Sources/Cloud/CloudTreeStyleGalleryWindow.swift index 0c55cb84a2e3..b5b04b3f8bae 100644 --- a/Sources/Cloud/CloudTreeStyleGalleryWindow.swift +++ b/Sources/Cloud/CloudTreeStyleGalleryWindow.swift @@ -106,6 +106,7 @@ private struct CloudTreeStyleGalleryColumn: View { localWorkspaces: localWorkspaces, machineActions: MachineRowActions.bound(onDidMutate: {}), nodeActions: CloudTreeNodeActions.bound( + navigationHost: AppDelegate.makeCloudTerminalNavigationHost(), catalog: { SurfaceCatalog.shared }, selectedWorkspaceID: { AppDelegate.shared?.tabManager?.selectedTabId }, selectLocalWorkspace: { workspaceID in diff --git a/Sources/Cloud/CloudTreeTerminalNavigationCoordinator.swift b/Sources/Cloud/CloudTreeTerminalNavigationCoordinator.swift index 54bc1346780c..89b689ec10bf 100644 --- a/Sources/Cloud/CloudTreeTerminalNavigationCoordinator.swift +++ b/Sources/Cloud/CloudTreeTerminalNavigationCoordinator.swift @@ -7,20 +7,23 @@ import Foundation final class CloudTreeTerminalNavigationCoordinator { typealias Run = @MainActor ( _ label: String, - _ operation: @escaping @MainActor (SurfaceCatalog) async throws -> Void + _ operation: @escaping @MainActor (any CloudTerminalNavigationCatalog) async throws -> Void ) -> Task private let machineName: @MainActor (SurfaceMachineID) -> String private let run: Run - private let operationController: CloudWorkspaceOperationController? + private let host: CloudTerminalNavigationHost + private let operationController: (any CloudTerminalNavigationScheduling)? init( machineName: @escaping @MainActor (SurfaceMachineID) -> String, run: @escaping Run, - operationController: CloudWorkspaceOperationController? + host: CloudTerminalNavigationHost, + operationController: (any CloudTerminalNavigationScheduling)? ) { self.machineName = machineName self.run = run + self.host = host self.operationController = operationController } @@ -68,7 +71,7 @@ final class CloudTreeTerminalNavigationCoordinator { } private func navigate( - catalog: SurfaceCatalog, + catalog: any CloudTerminalNavigationCatalog, machine: SurfaceMachineID, group: SurfaceResourceGroup, resource: SurfaceResourceID, @@ -77,66 +80,35 @@ final class CloudTreeTerminalNavigationCoordinator { openIn: UUID? ) async throws { try catalog.checkCloudWorkspaceNavigation(machine: machine, workspaceID: remoteWorkspaceID) - let localWorkspaceID = CloudTreeNodeBuilder.localWorkspaceShowing( + let localWorkspaceID = catalog.localWorkspaceShowing( remoteWorkspaceID: remoteWorkspaceID, - placements: group.placements, - snapshot: catalog.snapshot + placements: group.placements ) ?? openIn if let localWorkspaceID { - let opened: (projection: SurfaceProjection, reused: Bool) - if let view { - opened = try await catalog.project( - resource, - into: .workspace(id: localWorkspaceID, placement: .tab), - focus: true, - reuseExisting: true, - reuseInWorkspace: localWorkspaceID, - remoteView: view - ) - } else { - opened = try await catalog.project( - resource, - into: .workspace(id: localWorkspaceID, placement: .tab), - focus: true, - reuseExisting: true, - reuseInWorkspace: localWorkspaceID - ) - } - SurfacePaneFactory.focus( - panelID: opened.projection.panelID, - in: opened.projection.workspaceID - ) + let projection = try await catalog.projectTerminal(resource, in: localWorkspaceID, view: view) + host.focus(projection.panelID, projection.workspaceID) return } - let layout = await CloudWorkspaceLayoutTranslator.fetch( + let layout = await catalog.terminalWorkspaceLayout( machine: machine, - workspaceID: remoteWorkspaceID, - catalog: catalog + workspaceID: remoteWorkspaceID ) try catalog.checkCloudWorkspaceNavigation(machine: machine, workspaceID: remoteWorkspaceID) - let opened = try await catalog.projectGroupAsNewLocalWorkspace( + let opened = try await catalog.openTerminalWorkspace( group, - title: CloudTreeNodeActions.localWorkspaceTitle( - hostName: machineName(machine), - group: group - ), - focus: true, - host: .appOptimistic, + title: group.localWorkspaceTitle(hostName: machineName(machine)), layout: layout ) guard !Task.isCancelled else { - closeOpenedWorkspace(opened.workspaceID) + host.closeWorkspace(opened.workspaceID) throw CancellationError() } - catalog.bindCloudWorkspace( + catalog.bindTerminalWorkspace( localWorkspaceID: opened.workspaceID, machine: machine, remoteWorkspaceID: remoteWorkspaceID, - generatedTitle: CloudTreeNodeActions.localWorkspaceTitle( - hostName: machineName(machine), - group: group - ) + generatedTitle: group.localWorkspaceTitle(hostName: machineName(machine)) ) guard let target = targetProjection( in: opened.projections, @@ -144,12 +116,12 @@ final class CloudTreeTerminalNavigationCoordinator { view: view, remoteWorkspaceID: remoteWorkspaceID ) else { - closeOpenedWorkspace(opened.workspaceID) + host.closeWorkspace(opened.workspaceID) throw SurfaceCatalogError.destinationNotFound( String(localized: "cloudTree.error.terminalRestoreFailed", defaultValue: "The clicked Cloud terminal could not be restored in its workspace.") ) } - SurfacePaneFactory.focus(panelID: target.panelID, in: target.workspaceID) + host.focus(target.panelID, target.workspaceID) } private func targetProjection( @@ -166,42 +138,4 @@ final class CloudTreeTerminalNavigationCoordinator { } return matches.count == 1 ? matches[0] : nil } - - private func closeOpenedWorkspace(_ workspaceID: UUID) { - guard let manager = AppDelegate.shared?.tabManagerFor(tabId: workspaceID), - let workspace = manager.tabs.first(where: { $0.id == workspaceID }) else { - return - } - _ = manager.closeWorkspaceNonInteractively( - workspace, - recordHistory: false, - allowPinned: true - ) - } -} - -extension CloudTreeOutlineView.Coordinator { - /// Resolves a terminal row's workspace parent before dispatching its open verb. - func openTerminalRow(_ node: CloudTreeNode, row: CloudTreeTerminalRow) { - if let parent = outlineView?.parent(forItem: node) as? CloudTreeNode, - case .workspace(let machine, let workspace, _, _, let openIn) = parent.kind { - guard machine == row.resource.machine, - let group = parent.dragGroup, - group.remoteWorkspaceID == workspace.id, - row.remoteView?.workspace.id == nil || row.remoteView?.workspace.id == workspace.id else { - #if DEBUG - cmuxDebugLog("cloudTree.open terminal staleOwner resource=\(row.resource.id.rawValue)") - #endif - return - } - nodeActions.openRemoteTerminal(machine, group, row.resource.id, row.remoteView, openIn) - } else if let view = row.remoteView { - #if DEBUG - cmuxDebugLog("cloudTree.open terminal missingOwner resource=\(row.resource.id.rawValue) view=\(view.tabID)") - #endif - } else { - // Pool terminals have no owner; retain their selected-workspace behavior. - nodeActions.project(row.resource.id, .tab, true) - } - } } diff --git a/Sources/Cloud/CloudWorkspaceOperationController+TerminalNavigation.swift b/Sources/Cloud/CloudWorkspaceOperationController+TerminalNavigation.swift new file mode 100644 index 000000000000..b022f83424d1 --- /dev/null +++ b/Sources/Cloud/CloudWorkspaceOperationController+TerminalNavigation.swift @@ -0,0 +1 @@ +extension CloudWorkspaceOperationController: CloudTerminalNavigationScheduling {} diff --git a/Sources/Cloud/MachinesPanelView.swift b/Sources/Cloud/MachinesPanelView.swift index 3778de900a68..d07fa3c8f767 100644 --- a/Sources/Cloud/MachinesPanelView.swift +++ b/Sources/Cloud/MachinesPanelView.swift @@ -506,6 +506,7 @@ struct MachinesPanelView: View { viewModel.bindMachineOrdering(to: &machineActions) machineActions.create = MachineCreateRowActions.bound(coordinator: viewModel.createCoordinator) let nodeActions = CloudTreeNodeActions.bound( + navigationHost: AppDelegate.makeCloudTerminalNavigationHost(), catalog: { SurfaceCatalog.shared }, selectedWorkspaceID: { tabManager?.selectedTabId }, selectLocalWorkspace: { workspaceID in diff --git a/Sources/Cloud/SurfaceResourceGroup+CloudNavigation.swift b/Sources/Cloud/SurfaceResourceGroup+CloudNavigation.swift new file mode 100644 index 000000000000..5f774d7b5882 --- /dev/null +++ b/Sources/Cloud/SurfaceResourceGroup+CloudNavigation.swift @@ -0,0 +1,9 @@ +import Foundation + +extension SurfaceResourceGroup { + /// Uses the workspace's own name, falling back to the machine's friendly label. + func localWorkspaceTitle(hostName: String) -> String { + let name = title.trimmingCharacters(in: .whitespacesAndNewlines) + return name.isEmpty ? hostName : name + } +} diff --git a/Sources/Surfaces/SurfaceCatalog+CloudTerminalNavigation.swift b/Sources/Surfaces/SurfaceCatalog+CloudTerminalNavigation.swift new file mode 100644 index 000000000000..93d5a601a788 --- /dev/null +++ b/Sources/Surfaces/SurfaceCatalog+CloudTerminalNavigation.swift @@ -0,0 +1,41 @@ +import Foundation + +extension SurfaceCatalog: CloudTerminalNavigationCatalog { + func localWorkspaceShowing(remoteWorkspaceID: String, placements: [SurfaceResourcePlacement]) -> UUID? { + CloudTreeNodeBuilder.localWorkspaceShowing( + remoteWorkspaceID: remoteWorkspaceID, placements: placements, snapshot: snapshot + ) + } + + func projectTerminal(_ resource: SurfaceResourceID, in workspaceID: UUID, view: SurfaceRemoteView?) async throws -> SurfaceProjection { + let opened: (projection: SurfaceProjection, reused: Bool) + if let view { + opened = try await project( + resource, into: .workspace(id: workspaceID, placement: .tab), + focus: true, reuseExisting: true, reuseInWorkspace: workspaceID, remoteView: view + ) + } else { + opened = try await project( + resource, into: .workspace(id: workspaceID, placement: .tab), + focus: true, reuseExisting: true, reuseInWorkspace: workspaceID + ) + } + return opened.projection + } + + func terminalWorkspaceLayout(machine: SurfaceMachineID, workspaceID: String) async -> SurfaceProjectionLayout? { + await CloudWorkspaceLayoutTranslator.fetch(machine: machine, workspaceID: workspaceID, catalog: self) + } + + func openTerminalWorkspace(_ group: SurfaceResourceGroup, title: String, layout: SurfaceProjectionLayout?) async throws + -> (workspaceID: UUID, projections: [SurfaceProjection]) { + try await projectGroupAsNewLocalWorkspace(group, title: title, focus: true, host: .appOptimistic, layout: layout) + } + + func bindTerminalWorkspace(localWorkspaceID: UUID, machine: SurfaceMachineID, remoteWorkspaceID: String, generatedTitle: String) { + bindCloudWorkspace( + localWorkspaceID: localWorkspaceID, machine: machine, remoteWorkspaceID: remoteWorkspaceID, + generatedTitle: generatedTitle + ) + } +} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 98844b5e0041..61f8bd529270 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -215,6 +215,7 @@ C4A570010000000000000001 /* AppDelegate+CanvasShortcutRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A570010000000000000002 /* AppDelegate+CanvasShortcutRouting.swift */; }; C4160A030000000000000001 /* AppDelegate+ClosedItemHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */; }; AC329BBA70590E415B129F2F /* AppDelegate+CloudDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D0A6FA47518B9D3AF658084 /* AppDelegate+CloudDiagnostics.swift */; }; + C72B971AA11641AD9D61CDA3 /* AppDelegate+CloudTerminalNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33981D3B415C488D8406A09E /* AppDelegate+CloudTerminalNavigation.swift */; }; 7A0CE1000000000000000045 /* AppDelegate+CloudTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0CE1000000000000000019 /* AppDelegate+CloudTunnel.swift */; }; C54860050000000000000001 /* AppDelegate+CmuxNavigationDeepLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54860050000000000000002 /* AppDelegate+CmuxNavigationDeepLinks.swift */; }; C3677004000000000000001 /* AppDelegate+CmuxSSHURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3677004000000000000002 /* AppDelegate+CmuxSSHURL.swift */; }; @@ -907,6 +908,12 @@ 1414948CB9244AF09AEC702C /* CloudTerminalMutationSocketRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4ECAEE65C944D489A1F10D /* CloudTerminalMutationSocketRunner.swift */; }; A12D2BC4AB714CE0B40293DC /* CloudTerminalMutationSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71EFC80DF0A14843B8F87A25 /* CloudTerminalMutationSocketTests.swift */; }; A0385D6EB944431E95F2A932 /* CloudTerminalMutationTestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7A546300D34EC4BFECBA2D /* CloudTerminalMutationTestTransport.swift */; }; + 2792FEC971414E8D88E5C8C0 /* CloudTerminalNavigationCapabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11FEF4AEE3FD4900B8C8437E /* CloudTerminalNavigationCapabilityTests.swift */; }; + 4CA9D21B229D4120869D15F0 /* CloudTerminalNavigationCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70BB98008F8488F83EF63DE /* CloudTerminalNavigationCatalog.swift */; }; + 98B7E20CB5B2471FB588E62A /* CloudTerminalNavigationFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B84121942246E093607E66 /* CloudTerminalNavigationFixture.swift */; }; + 7CBF8A81C0E44648A38A8AF0 /* CloudTerminalNavigationHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7A4936423D4532A3BF790E /* CloudTerminalNavigationHost.swift */; }; + 83665D64943042889E47B38C /* CloudTerminalNavigationScheduling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9EA87EF407F48C2BF178279 /* CloudTerminalNavigationScheduling.swift */; }; + F82B489D3D384DB6B503AE25 /* CloudTerminalNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D853971C8DC24A10B8A923D6 /* CloudTerminalNavigationTests.swift */; }; BD5C6A44F9B646C29565CF59 /* CloudTerminalOverlayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF976CA511F242519ADA8204 /* CloudTerminalOverlayCoordinator.swift */; }; FA0000000000000000000005 /* CloudTerminalPaneClosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0000000000000000000006 /* CloudTerminalPaneClosure.swift */; }; FA0000000000000000000003 /* CloudTerminalPaneClosureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0000000000000000000004 /* CloudTerminalPaneClosureTests.swift */; }; @@ -957,6 +964,7 @@ E639673A813D2A174CE3BAE1 /* CloudTreeOutlineView+MachineMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412FF88FEF5819465A8157DE /* CloudTreeOutlineView+MachineMenu.swift */; }; A1308600000000000000000C /* CloudTreeOutlineView+MachineOrdering.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1308600000000000000000B /* CloudTreeOutlineView+MachineOrdering.swift */; }; FB6DB41BCE7B4EC987BF2F0B /* CloudTreeOutlineView+Organization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A6150D65B1F43F683253998 /* CloudTreeOutlineView+Organization.swift */; }; + AA8DF35A97744AC7ACE5D626 /* CloudTreeOutlineView+TerminalNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBFE3E4A634A418D82976C33 /* CloudTreeOutlineView+TerminalNavigation.swift */; }; 669EEED531127C99F4EF6B7D /* CloudTreeOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CACA5A7C96FFDF0B931E7AA /* CloudTreeOutlineView.swift */; }; C986A00B0000000000000001 /* CloudTreePendingDrag.swift in Sources */ = {isa = PBXBuildFile; fileRef = C986A00A0000000000000001 /* CloudTreePendingDrag.swift */; }; FE1EC2963BB87449593B5474 /* CloudTreePendingMachineRowContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7492FFBAD4190E9582BB6863 /* CloudTreePendingMachineRowContent.swift */; }; @@ -1079,6 +1087,7 @@ 341E0463C183CD4C0CB655DE /* CloudWorkspaceLayoutTranslatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCBB7EB399B58A6D076EAF1E /* CloudWorkspaceLayoutTranslatorTests.swift */; }; 04EA20A26351429BA2023F92 /* CloudWorkspaceLiveProjectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4088DB52FA564E2A8E670921 /* CloudWorkspaceLiveProjectionTests.swift */; }; 4779459FFFB041CB8FD46938 /* CloudWorkspaceMembershipTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F739BB5F7AEF47A6A38D6B7C /* CloudWorkspaceMembershipTests.swift */; }; + 775ECED002DE499C96F792E7 /* CloudWorkspaceOperationController+TerminalNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15054BF8BBFD492AAFBE5F08 /* CloudWorkspaceOperationController+TerminalNavigation.swift */; }; 15E5F36E592C4D2894AF93CF /* CloudWorkspaceOperationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C2B9391A62B4D0E8AC7575F /* CloudWorkspaceOperationController.swift */; }; DF2AFF34D42A4505BC7EF20A /* CloudWorkspaceProjectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A9286A232F4C3FAD571015 /* CloudWorkspaceProjectionCoordinator.swift */; }; D59CC3EA15344B47915B3779 /* CloudWorkspaceProjectionEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFE35D27805468DBA885560 /* CloudWorkspaceProjectionEnvironment.swift */; }; @@ -3176,6 +3185,7 @@ 78F8D449BA0D4860AE1162C7 /* SurfaceCatalog+CloudDirectoryMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2248ED1422420F90E0D71D /* SurfaceCatalog+CloudDirectoryMetadata.swift */; }; F11347000000000000000001 /* SurfaceCatalog+CloudPorts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11347000000000000000002 /* SurfaceCatalog+CloudPorts.swift */; }; E71A3B8404534D5699AA2E9B /* SurfaceCatalog+CloudRenameSubmission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4A80857B8A4530BD5BC498 /* SurfaceCatalog+CloudRenameSubmission.swift */; }; + D4E2A030E899483F96AF0C9E /* SurfaceCatalog+CloudTerminalNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B841AA21EA4B58B34D228B /* SurfaceCatalog+CloudTerminalNavigation.swift */; }; 7E02669974964356A46288EB /* SurfaceCatalog+CloudWorkspaceProjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB74882A29884E378E36D645 /* SurfaceCatalog+CloudWorkspaceProjection.swift */; }; DD3A13AD76F5196C5FC8C52A /* SurfaceCatalog+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660286CE22B478E887361407 /* SurfaceCatalog+Groups.swift */; }; C2DF9D7B62A64277A656C356 /* SurfaceCatalog+NameAuthority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798894FD4FBD473BAA1D9DF6 /* SurfaceCatalog+NameAuthority.swift */; }; @@ -3213,6 +3223,7 @@ 91007F48E782474596D8FB7D /* SurfaceResource+CloudTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E2A1163D9B452EA3D55B5D /* SurfaceResource+CloudTitle.swift */; }; F96CAE8A14774122A5F8613B /* SurfaceResourceDragPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48A1307BCCDB493F49179E0B /* SurfaceResourceDragPayload.swift */; }; 3E66E7A1B2B320EC850BB174 /* SurfaceResourceDragRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627B5FB396A1BF22407E8B45 /* SurfaceResourceDragRegistry.swift */; }; + 12D94A2FDF834F9495F22AEE /* SurfaceResourceGroup+CloudNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EBE986DDD446A694D6EA07 /* SurfaceResourceGroup+CloudNavigation.swift */; }; 891B4C987DA84C9B975AD832 /* SurfaceResourcePlacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65E70E1AE6E42F890ADD7EF /* SurfaceResourcePlacement.swift */; }; 842300000000000000000007 /* SurfaceResumeAgentBindingGenerationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842300000000000000000008 /* SurfaceResumeAgentBindingGenerationTests.swift */; }; F6572032A1B2C3D4E5F60718 /* SurfaceResumeAgentHookDowngradeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6572033A1B2C3D4E5F60718 /* SurfaceResumeAgentHookDowngradeTests.swift */; }; @@ -4308,6 +4319,7 @@ C4A570010000000000000002 /* AppDelegate+CanvasShortcutRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CanvasShortcutRouting.swift"; sourceTree = ""; }; C4160A030000000000000002 /* AppDelegate+ClosedItemHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+ClosedItemHistory.swift"; sourceTree = ""; }; 0D0A6FA47518B9D3AF658084 /* AppDelegate+CloudDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CloudDiagnostics.swift"; sourceTree = ""; }; + 33981D3B415C488D8406A09E /* AppDelegate+CloudTerminalNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate+CloudTerminalNavigation.swift; sourceTree = ""; }; 7A0CE1000000000000000019 /* AppDelegate+CloudTunnel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CloudTunnel.swift"; sourceTree = ""; }; C54860050000000000000002 /* AppDelegate+CmuxNavigationDeepLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CmuxNavigationDeepLinks.swift"; sourceTree = ""; }; C3677004000000000000002 /* AppDelegate+CmuxSSHURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CmuxSSHURL.swift"; sourceTree = ""; }; @@ -4991,6 +5003,12 @@ BA4ECAEE65C944D489A1F10D /* CloudTerminalMutationSocketRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalMutationSocketRunner.swift; sourceTree = ""; }; 71EFC80DF0A14843B8F87A25 /* CloudTerminalMutationSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalMutationSocketTests.swift; sourceTree = ""; }; 3B7A546300D34EC4BFECBA2D /* CloudTerminalMutationTestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalMutationTestTransport.swift; sourceTree = ""; }; + 11FEF4AEE3FD4900B8C8437E /* CloudTerminalNavigationCapabilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalNavigationCapabilityTests.swift; sourceTree = ""; }; + A70BB98008F8488F83EF63DE /* CloudTerminalNavigationCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalNavigationCatalog.swift; sourceTree = ""; }; + E8B84121942246E093607E66 /* CloudTerminalNavigationFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalNavigationFixture.swift; sourceTree = ""; }; + BC7A4936423D4532A3BF790E /* CloudTerminalNavigationHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalNavigationHost.swift; sourceTree = ""; }; + D9EA87EF407F48C2BF178279 /* CloudTerminalNavigationScheduling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalNavigationScheduling.swift; sourceTree = ""; }; + D853971C8DC24A10B8A923D6 /* CloudTerminalNavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalNavigationTests.swift; sourceTree = ""; }; DF976CA511F242519ADA8204 /* CloudTerminalOverlayCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalOverlayCoordinator.swift; sourceTree = ""; }; FA0000000000000000000006 /* CloudTerminalPaneClosure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalPaneClosure.swift; sourceTree = ""; }; FA0000000000000000000004 /* CloudTerminalPaneClosureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTerminalPaneClosureTests.swift; sourceTree = ""; }; @@ -5041,6 +5059,7 @@ 412FF88FEF5819465A8157DE /* CloudTreeOutlineView+MachineMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudTreeOutlineView+MachineMenu.swift"; sourceTree = ""; }; A1308600000000000000000B /* CloudTreeOutlineView+MachineOrdering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sidebar/CloudTreeOutlineView+MachineOrdering.swift"; sourceTree = ""; }; 5A6150D65B1F43F683253998 /* CloudTreeOutlineView+Organization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sidebar/CloudTreeOutlineView+Organization.swift"; sourceTree = ""; }; + FBFE3E4A634A418D82976C33 /* CloudTreeOutlineView+TerminalNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTreeOutlineView+TerminalNavigation.swift; sourceTree = ""; }; 1CACA5A7C96FFDF0B931E7AA /* CloudTreeOutlineView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudTreeOutlineView.swift; sourceTree = ""; }; C986A00A0000000000000001 /* CloudTreePendingDrag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTreePendingDrag.swift; sourceTree = ""; }; 7492FFBAD4190E9582BB6863 /* CloudTreePendingMachineRowContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudTreePendingMachineRowContent.swift; sourceTree = ""; }; @@ -5160,6 +5179,7 @@ FCBB7EB399B58A6D076EAF1E /* CloudWorkspaceLayoutTranslatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudWorkspaceLayoutTranslatorTests.swift; sourceTree = ""; }; 4088DB52FA564E2A8E670921 /* CloudWorkspaceLiveProjectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudWorkspaceLiveProjectionTests.swift"; sourceTree = ""; }; F739BB5F7AEF47A6A38D6B7C /* CloudWorkspaceMembershipTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudWorkspaceMembershipTests.swift; sourceTree = ""; }; + 15054BF8BBFD492AAFBE5F08 /* CloudWorkspaceOperationController+TerminalNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudWorkspaceOperationController+TerminalNavigation.swift; sourceTree = ""; }; 7C2B9391A62B4D0E8AC7575F /* CloudWorkspaceOperationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudWorkspaceOperationController.swift"; sourceTree = ""; }; A5A9286A232F4C3FAD571015 /* CloudWorkspaceProjectionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudWorkspaceProjectionCoordinator.swift"; sourceTree = ""; }; 0DFE35D27805468DBA885560 /* CloudWorkspaceProjectionEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudWorkspaceProjectionEnvironment.swift"; sourceTree = ""; }; @@ -7131,6 +7151,7 @@ 7B2248ED1422420F90E0D71D /* SurfaceCatalog+CloudDirectoryMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudDirectoryMetadata.swift"; sourceTree = ""; }; F11347000000000000000002 /* SurfaceCatalog+CloudPorts.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudPorts.swift"; sourceTree = ""; }; 4A4A80857B8A4530BD5BC498 /* SurfaceCatalog+CloudRenameSubmission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudRenameSubmission.swift"; sourceTree = ""; }; + A9B841AA21EA4B58B34D228B /* SurfaceCatalog+CloudTerminalNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceCatalog+CloudTerminalNavigation.swift; sourceTree = ""; }; AB74882A29884E378E36D645 /* SurfaceCatalog+CloudWorkspaceProjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudWorkspaceProjection.swift"; sourceTree = ""; }; 660286CE22B478E887361407 /* SurfaceCatalog+Groups.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+Groups.swift"; sourceTree = ""; }; 798894FD4FBD473BAA1D9DF6 /* SurfaceCatalog+NameAuthority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+NameAuthority.swift"; sourceTree = ""; }; @@ -7168,6 +7189,7 @@ A7E2A1163D9B452EA3D55B5D /* SurfaceResource+CloudTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceResource+CloudTitle.swift"; sourceTree = ""; }; 48A1307BCCDB493F49179E0B /* SurfaceResourceDragPayload.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SurfaceResourceDragPayload.swift; sourceTree = ""; }; 627B5FB396A1BF22407E8B45 /* SurfaceResourceDragRegistry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SurfaceResourceDragRegistry.swift; sourceTree = ""; }; + 54EBE986DDD446A694D6EA07 /* SurfaceResourceGroup+CloudNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceResourceGroup+CloudNavigation.swift; sourceTree = ""; }; D65E70E1AE6E42F890ADD7EF /* SurfaceResourcePlacement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceResourcePlacement.swift; sourceTree = ""; }; 842300000000000000000008 /* SurfaceResumeAgentBindingGenerationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceResumeAgentBindingGenerationTests.swift; sourceTree = ""; }; F6572033A1B2C3D4E5F60718 /* SurfaceResumeAgentHookDowngradeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceResumeAgentHookDowngradeTests.swift; sourceTree = ""; }; @@ -8480,6 +8502,7 @@ 1FE7223A65E72C46927A4F02 /* SurfacePaneFactory.swift */, C28D6AEDE6B00F5C633ED2A8 /* CmuxTuiSnapshotParser.swift */, EA63D377742D3A6A0BAB499B /* SurfaceCatalog.swift */, + A9B841AA21EA4B58B34D228B /* SurfaceCatalog+CloudTerminalNavigation.swift */, F11347000000000000000008 /* SurfaceCatalog+CloudBinding.swift */, 7867A0020000000000000001 /* SurfaceProjectionRestoreStore.swift */, 7305B43543CF4AF29D9966C8 /* Workspace+SurfaceOwnership.swift */, @@ -8722,6 +8745,12 @@ B93142FA1472ACA28B993606 /* CloudTreeExpansionStore.swift */, 0F73B922CC243AB9A365C1B5 /* CloudTreeNodeActions.swift */, 255F538190CCB12E129B2B3F /* CloudTreeNodeActions+WorkspaceLifecycle.swift */, + A70BB98008F8488F83EF63DE /* CloudTerminalNavigationCatalog.swift */, + BC7A4936423D4532A3BF790E /* CloudTerminalNavigationHost.swift */, + D9EA87EF407F48C2BF178279 /* CloudTerminalNavigationScheduling.swift */, + 15054BF8BBFD492AAFBE5F08 /* CloudWorkspaceOperationController+TerminalNavigation.swift */, + FBFE3E4A634A418D82976C33 /* CloudTreeOutlineView+TerminalNavigation.swift */, + 54EBE986DDD446A694D6EA07 /* SurfaceResourceGroup+CloudNavigation.swift */, A12357000000000000000001 /* CloudTreeTerminalNavigationCoordinator.swift */, D12406000000000000000001 /* CloudTreeResizeMenu.swift */, A7BA0E69E17F61ECF69C9025 /* CloudAgentSkillLauncher.swift */, @@ -9902,6 +9931,7 @@ A5001620 /* AppleScriptSupport.swift */, F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */, D1320AA0D1320AA0D1320AA4 /* AppIconDockTilePlugin.swift */, + 33981D3B415C488D8406A09E /* AppDelegate+CloudTerminalNavigation.swift */, A5001090 /* AppDelegate.swift */, 2830130F95FE58F35F5220A8 /* AppDelegate+DisplayPlacement.swift */, 7A0CE1000000000000000019 /* AppDelegate+CloudTunnel.swift */, @@ -11962,6 +11992,9 @@ C12575000000000000000033 /* CloudWorkspaceDeletionLedgerTests.swift */, C12575000000000000000051 /* CloudRenameOptimismTests.swift */, F31EDA1AEA6A437B8E75A489 /* CloudToolPaneTests.swift */, + E8B84121942246E093607E66 /* CloudTerminalNavigationFixture.swift */, + 11FEF4AEE3FD4900B8C8437E /* CloudTerminalNavigationCapabilityTests.swift */, + D853971C8DC24A10B8A923D6 /* CloudTerminalNavigationTests.swift */, ADC9BE77DF3A402B8EC51665 /* CloudTreeMachineMenuTests.swift */, FE002010 /* FileExplorerDoubleClickActionTests.swift */, FE002003 /* FileSearchRipgrepParserTests.swift */, @@ -12879,6 +12912,7 @@ C4A570010000000000000001 /* AppDelegate+CanvasShortcutRouting.swift in Sources */, C4160A030000000000000001 /* AppDelegate+ClosedItemHistory.swift in Sources */, AC329BBA70590E415B129F2F /* AppDelegate+CloudDiagnostics.swift in Sources */, + C72B971AA11641AD9D61CDA3 /* AppDelegate+CloudTerminalNavigation.swift in Sources */, 7A0CE1000000000000000045 /* AppDelegate+CloudTunnel.swift in Sources */, C54860050000000000000001 /* AppDelegate+CmuxNavigationDeepLinks.swift in Sources */, C3677004000000000000001 /* AppDelegate+CmuxSSHURL.swift in Sources */, @@ -13283,6 +13317,9 @@ C12537000000000000000223 /* CloudTerminalLayoutCreationResult.swift in Sources */, 192D8036059F463091D7324B /* CloudTerminalMutationCommandRunner.swift in Sources */, C456E44419C541B694BF9421 /* CloudTerminalMutationQueue.swift in Sources */, + 4CA9D21B229D4120869D15F0 /* CloudTerminalNavigationCatalog.swift in Sources */, + 7CBF8A81C0E44648A38A8AF0 /* CloudTerminalNavigationHost.swift in Sources */, + 83665D64943042889E47B38C /* CloudTerminalNavigationScheduling.swift in Sources */, BD5C6A44F9B646C29565CF59 /* CloudTerminalOverlayCoordinator.swift in Sources */, FA0000000000000000000005 /* CloudTerminalPaneClosure.swift in Sources */, C12537000000000000000305 /* CloudTerminalPaneReservation.swift in Sources */, @@ -13320,6 +13357,7 @@ E639673A813D2A174CE3BAE1 /* CloudTreeOutlineView+MachineMenu.swift in Sources */, A1308600000000000000000C /* CloudTreeOutlineView+MachineOrdering.swift in Sources */, FB6DB41BCE7B4EC987BF2F0B /* CloudTreeOutlineView+Organization.swift in Sources */, + AA8DF35A97744AC7ACE5D626 /* CloudTreeOutlineView+TerminalNavigation.swift in Sources */, 669EEED531127C99F4EF6B7D /* CloudTreeOutlineView.swift in Sources */, C986A00B0000000000000001 /* CloudTreePendingDrag.swift in Sources */, FE1EC2963BB87449593B5474 /* CloudTreePendingMachineRowContent.swift in Sources */, @@ -13410,6 +13448,7 @@ C12575000000000000000030 /* CloudWorkspaceDeletionLedger.swift in Sources */, 5D999EE2FD784319A73C3945 /* CloudWorkspaceGroupDestination.swift in Sources */, 0B2534A7872AF46D59819D49 /* CloudWorkspaceLayoutTranslator.swift in Sources */, + 775ECED002DE499C96F792E7 /* CloudWorkspaceOperationController+TerminalNavigation.swift in Sources */, 15E5F36E592C4D2894AF93CF /* CloudWorkspaceOperationController.swift in Sources */, DF2AFF34D42A4505BC7EF20A /* CloudWorkspaceProjectionCoordinator.swift in Sources */, D59CC3EA15344B47915B3779 /* CloudWorkspaceProjectionEnvironment.swift in Sources */, @@ -14634,6 +14673,7 @@ 78F8D449BA0D4860AE1162C7 /* SurfaceCatalog+CloudDirectoryMetadata.swift in Sources */, F11347000000000000000001 /* SurfaceCatalog+CloudPorts.swift in Sources */, E71A3B8404534D5699AA2E9B /* SurfaceCatalog+CloudRenameSubmission.swift in Sources */, + D4E2A030E899483F96AF0C9E /* SurfaceCatalog+CloudTerminalNavigation.swift in Sources */, 7E02669974964356A46288EB /* SurfaceCatalog+CloudWorkspaceProjection.swift in Sources */, DD3A13AD76F5196C5FC8C52A /* SurfaceCatalog+Groups.swift in Sources */, C2DF9D7B62A64277A656C356 /* SurfaceCatalog+NameAuthority.swift in Sources */, @@ -14668,6 +14708,7 @@ 91007F48E782474596D8FB7D /* SurfaceResource+CloudTitle.swift in Sources */, F96CAE8A14774122A5F8613B /* SurfaceResourceDragPayload.swift in Sources */, 3E66E7A1B2B320EC850BB174 /* SurfaceResourceDragRegistry.swift in Sources */, + 12D94A2FDF834F9495F22AEE /* SurfaceResourceGroup+CloudNavigation.swift in Sources */, 891B4C987DA84C9B975AD832 /* SurfaceResourcePlacement.swift in Sources */, 883700000000000000000003 /* SurfaceResumeApprovalLookup.swift in Sources */, 875900000000000000000005 /* SurfaceResumeApprovalSigningSecretCache.swift in Sources */, @@ -15880,6 +15921,9 @@ 1414948CB9244AF09AEC702C /* CloudTerminalMutationSocketRunner.swift in Sources */, A12D2BC4AB714CE0B40293DC /* CloudTerminalMutationSocketTests.swift in Sources */, A0385D6EB944431E95F2A932 /* CloudTerminalMutationTestTransport.swift in Sources */, + 2792FEC971414E8D88E5C8C0 /* CloudTerminalNavigationCapabilityTests.swift in Sources */, + 98B7E20CB5B2471FB588E62A /* CloudTerminalNavigationFixture.swift in Sources */, + F82B489D3D384DB6B503AE25 /* CloudTerminalNavigationTests.swift in Sources */, FA0000000000000000000003 /* CloudTerminalPaneClosureTests.swift in Sources */, C12537000000000000000311 /* CloudTerminalPaneReservationTests.swift in Sources */, 2D412FC9EE3C4099815B1DD2 /* CloudTerminalPlacementTestProvider.swift in Sources */, diff --git a/cmuxTests/CloudPortOpenRegressionTests.swift b/cmuxTests/CloudPortOpenRegressionTests.swift index ac379087f28a..c80bd76e92a8 100644 --- a/cmuxTests/CloudPortOpenRegressionTests.swift +++ b/cmuxTests/CloudPortOpenRegressionTests.swift @@ -395,6 +395,7 @@ struct CloudPortOpenRegressionTests { var completion: AsyncStream.Continuation! let completionStream = AsyncStream { completion = $0 } let actions = CloudTreeNodeActions.bound( + navigationHost: AppDelegate.makeCloudTerminalNavigationHost(), catalog: { catalog }, selectedWorkspaceID: { unrelatedWorkspaceID }, selectLocalWorkspace: { _ in }, diff --git a/cmuxTests/CloudSidebarOrderingTests.swift b/cmuxTests/CloudSidebarOrderingTests.swift index b55f428e1bc8..36fe2b796da4 100644 --- a/cmuxTests/CloudSidebarOrderingTests.swift +++ b/cmuxTests/CloudSidebarOrderingTests.swift @@ -197,6 +197,7 @@ final class CloudSidebarOrderingFixture { promptRename: { _, _ in }, resizeDisk: { _, _ in }, promptUpgrade: {} ), nodeActions: CloudTreeNodeActions.bound( + navigationHost: AppDelegate.makeCloudTerminalNavigationHost(), catalog: { catalog }, selectedWorkspaceID: { nil }, selectLocalWorkspace: { _ in }, onWillMutate: { _ in }, onDidMutate: {}, onFailure: { _ in }, refresh: {} diff --git a/cmuxTests/CloudTerminalNavigationCapabilityTests.swift b/cmuxTests/CloudTerminalNavigationCapabilityTests.swift new file mode 100644 index 000000000000..5879d95ce16b --- /dev/null +++ b/cmuxTests/CloudTerminalNavigationCapabilityTests.swift @@ -0,0 +1,131 @@ +import Foundation +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@MainActor +struct CloudTerminalNavigationCapabilityTests { + @Test("Existing projection wins over a supplied destination; fallback uses that destination", arguments: [false, true]) + func reusesExistingProjection(hasProjection: Bool) async { + let fixture = CloudTerminalNavigationFixture() + let supplied = UUID() + fixture.existingWorkspaceID = hasProjection ? fixture.workspaceID : nil + let navigation = fixture.makeNavigation() + navigation.open(machine: fixture.machine, group: fixture.group, resource: fixture.resource, view: fixture.view, openIn: supplied) + await fixture.wait() + #expect(fixture.events == ["check", "lookup", "project", "focus"]) + #expect(fixture.projectedWorkspaceID == (hasProjection ? fixture.workspaceID : supplied)) + #expect(fixture.projectedView == fixture.view) + #expect(fixture.focused.first?.0 == fixture.panelID) + #expect(fixture.focused.first?.1 == fixture.workspaceID) + #expect(fixture.failures.isEmpty) + } + + @Test("Restore retains layout, owner identity, exact clicked tab and generated title", arguments: [false, true]) + func restoresExactTab(blankTitle: Bool) async { + let fixture = CloudTerminalNavigationFixture() + var other = fixture.projection + other.panelID = UUID() + other.remoteTabID = "tab-other" + fixture.restoredProjections = [other, fixture.projection] + let group = blankTitle + ? SurfaceResourceGroup(title: " \n ", placements: fixture.group.placements, remoteWorkspaceID: fixture.remoteWorkspace.id) + : fixture.group + let navigation = fixture.makeNavigation() + navigation.open(machine: fixture.machine, group: group, resource: fixture.resource, view: fixture.view, openIn: nil) + await fixture.wait() + #expect(fixture.events == ["check", "lookup", "layout", "check", "open", "bind", "focus"]) + #expect(fixture.receivedLayout == fixture.layout) + #expect(fixture.receivedGroup == group) + #expect(fixture.openedTitle == (blankTitle ? "Friendly machine" : "Project")) + #expect(fixture.boundTitle == fixture.openedTitle) + #expect(fixture.focused.count == 1) + #expect(fixture.focused.first?.0 == fixture.panelID) + #expect(fixture.failures.isEmpty) + } + + @Test("Missing or ambiguous clicked projections close only the opened workspace and return one original error", arguments: 0..<4) + func rejectsMissingOrAmbiguousProjection(variant: Int) async { + let fixture = CloudTerminalNavigationFixture() + var wrong = fixture.projection + switch variant { + case 0: wrong.remoteTabID = "different-tab" + case 1: wrong.remoteWorkspaceID = "different-workspace" + case 2: wrong.resource = SurfaceResourceID(machine: fixture.machine, kind: .terminal, key: "different-resource") + default: break + } + fixture.restoredProjections = variant == 3 ? [wrong, wrong] : [wrong] + let navigation = fixture.makeNavigation() + navigation.open(machine: fixture.machine, group: fixture.group, resource: fixture.resource, view: fixture.view, openIn: nil) + await fixture.wait() + #expect(fixture.events == ["check", "lookup", "layout", "check", "open", "bind", "close"]) + #expect(fixture.closed == [fixture.workspaceID]) + #expect(fixture.focused.isEmpty) + #expect(fixture.failures.count == 1) + #expect(fixture.failures.first as? SurfaceCatalogError == .destinationNotFound( + String(localized: "cloudTree.error.terminalRestoreFailed", defaultValue: "The clicked Cloud terminal could not be restored in its workspace.") + )) + } + + @Test("Deletion admission before or during layout fetch cancels without opening", arguments: [1, 2]) + func checksDeletionAcrossSuspension(check: Int) async { + let fixture = CloudTerminalNavigationFixture() + fixture.checkFailureAt = check == 1 ? 1 : nil + fixture.delayLayout = check == 2 + let navigation = fixture.makeNavigation() + navigation.open(machine: fixture.machine, group: fixture.group, resource: fixture.resource, view: fixture.view, openIn: nil) + if check == 2 { + for await _ in fixture.layoutStarted.stream { break } + fixture.checkFailureAt = 2 + fixture.releaseLayout() + } + await fixture.wait() + #expect(fixture.events == (check == 1 ? ["check"] : ["check", "lookup", "layout", "check"])) + #expect(fixture.failures.count == 1) + #expect(fixture.failures.first is CancellationError) + #expect(fixture.closed.isEmpty) + #expect(fixture.focused.isEmpty) + } + + @Test("Cancellation after restore closes the opened workspace before binding") + func cancelledRestoreClosesProjection() async { + let fixture = CloudTerminalNavigationFixture() + fixture.cancelAfterOpen = true + let navigation = fixture.makeNavigation() + navigation.open(machine: fixture.machine, group: fixture.group, resource: fixture.resource, view: fixture.view, openIn: nil) + await fixture.wait() + #expect(fixture.events == ["check", "lookup", "layout", "check", "open", "close"]) + #expect(fixture.closed == [fixture.workspaceID]) + #expect(fixture.failures.first is CancellationError) + #expect(fixture.focused.isEmpty) + } + + @Test("The operation owner can reject feature-disabled navigation without touching the catalog") + func disabledFeatureDoesNotStart() async { + let fixture = CloudTerminalNavigationFixture() + fixture.available = false + let navigation = fixture.makeNavigation() + navigation.open(machine: fixture.machine, group: fixture.group, resource: fixture.resource, view: fixture.view, openIn: nil) + await fixture.wait() + #expect(fixture.events.isEmpty) + #expect(fixture.failures.isEmpty) + } + + @Test("A catalog failure is returned once with its original type", arguments: [false, true]) + func reportsFailureOnce(reuse: Bool) async { + let fixture = CloudTerminalNavigationFixture() + fixture.existingWorkspaceID = reuse ? fixture.workspaceID : nil + fixture.failure = SurfaceCatalogError.unknownResource(fixture.resource) + let navigation = fixture.makeNavigation() + navigation.open(machine: fixture.machine, group: fixture.group, resource: fixture.resource, view: fixture.view, openIn: nil) + await fixture.wait() + #expect(fixture.failures.count == 1) + #expect(fixture.failures.first as? SurfaceCatalogError == .unknownResource(fixture.resource)) + #expect(fixture.closed.isEmpty) + #expect(fixture.focused.isEmpty) + } +} diff --git a/cmuxTests/CloudTerminalNavigationFixture.swift b/cmuxTests/CloudTerminalNavigationFixture.swift new file mode 100644 index 000000000000..2b78608e5605 --- /dev/null +++ b/cmuxTests/CloudTerminalNavigationFixture.swift @@ -0,0 +1,132 @@ +import Foundation + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +/// Executes the production navigation policy without an app, window, or catalog. +@MainActor +final class CloudTerminalNavigationFixture: CloudTerminalNavigationCatalog, CloudTerminalNavigationScheduling { + let machine = SurfaceMachineID.cloud("navigation-fixture") + let remoteWorkspace = SurfaceRemoteWorkspace(id: "ws-1", name: "Project", index: 0, focused: false) + let workspaceID = UUID() + let panelID = UUID() + var existingWorkspaceID: UUID? + var available = true + var checkFailureAt: Int? + var failure: (any Error)? + var cancelAfterOpen = false + var delayLayout = false + let layoutStarted = AsyncStream.makeStream() + private var layoutWaiter: CheckedContinuation? + var restoredProjections: [SurfaceProjection]? + var events: [String] = [] + var failures: [any Error] = [] + var focused: [(UUID, UUID)] = [] + var closed: [UUID] = [] + var projectedWorkspaceID: UUID? + var projectedView: SurfaceRemoteView? + var openedTitle: String? + var boundTitle: String? + var receivedLayout: SurfaceProjectionLayout? + var receivedGroup: SurfaceResourceGroup? + private var checks = 0 + private var tasks: [Task] = [] + + var resource: SurfaceResourceID { SurfaceResourceID(machine: machine, kind: .terminal, key: "term-1") } + var view: SurfaceRemoteView { SurfaceRemoteView(tabID: "tab-clicked", workspace: remoteWorkspace) } + var group: SurfaceResourceGroup { + SurfaceResourceGroup(title: " Project ", placements: [SurfaceResourcePlacement(resource: resource, remoteView: view)], + remoteWorkspaceID: remoteWorkspace.id) + } + var projection: SurfaceProjection { + SurfaceProjection(resource: resource, workspaceID: workspaceID, panelID: panelID, + remoteWorkspaceID: remoteWorkspace.id, remoteTabID: view.tabID) + } + var layout: SurfaceProjectionLayout { .leaf(placements: group.placements) } + + func makeNavigation() -> CloudTreeTerminalNavigationCoordinator { + CloudTreeTerminalNavigationCoordinator( + machineName: { _ in "Friendly machine" }, + run: { [unowned self] _, operation in + Task { @MainActor in + do { try await operation(self) } + catch { failures.append(error) } + } + }, + host: CloudTerminalNavigationHost( + focus: { [unowned self] panel, workspace in + events.append("focus"); focused.append((panel, workspace)) + }, + closeWorkspace: { [unowned self] workspace in + events.append("close"); closed.append(workspace) + } + ), + operationController: self + ) + } + + func start(key: String, _ operation: @escaping CloudTerminalNavigationScheduling.Operation) -> Bool { + guard available else { return false } + tasks.append(Task { @MainActor in + do { try await operation() } + catch { failures.append(error) } + }) + return true + } + + func wait() async { for task in tasks { await task.value } } + + func releaseLayout() { + layoutWaiter?.resume() + layoutWaiter = nil + } + + func checkCloudWorkspaceNavigation(machine: SurfaceMachineID, workspaceID: String) throws { + events.append("check") + checks += 1 + if checks == checkFailureAt { throw CancellationError() } + } + + func localWorkspaceShowing(remoteWorkspaceID: String, placements: [SurfaceResourcePlacement]) -> UUID? { + events.append("lookup") + return existingWorkspaceID + } + + func projectTerminal(_ resource: SurfaceResourceID, in workspaceID: UUID, view: SurfaceRemoteView?) async throws -> SurfaceProjection { + events.append("project") + projectedWorkspaceID = workspaceID + projectedView = view + if let failure { throw failure } + return projection + } + + func terminalWorkspaceLayout(machine: SurfaceMachineID, workspaceID: String) async -> SurfaceProjectionLayout? { + events.append("layout") + if delayLayout { + await withCheckedContinuation { continuation in + layoutWaiter = continuation + layoutStarted.continuation.yield(()) + } + } + return layout + } + + func openTerminalWorkspace(_ group: SurfaceResourceGroup, title: String, layout: SurfaceProjectionLayout?) async throws + -> (workspaceID: UUID, projections: [SurfaceProjection]) { + events.append("open") + openedTitle = title + receivedLayout = layout + receivedGroup = group + if let failure { throw failure } + if cancelAfterOpen { withUnsafeCurrentTask { $0?.cancel() } } + return (workspaceID, restoredProjections ?? [projection]) + } + + func bindTerminalWorkspace(localWorkspaceID: UUID, machine: SurfaceMachineID, remoteWorkspaceID: String, generatedTitle: String) { + events.append("bind") + boundTitle = generatedTitle + } +} diff --git a/cmuxTests/CloudTerminalNavigationTests.swift b/cmuxTests/CloudTerminalNavigationTests.swift new file mode 100644 index 000000000000..dce7b44fd851 --- /dev/null +++ b/cmuxTests/CloudTerminalNavigationTests.swift @@ -0,0 +1,101 @@ +import Foundation +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@MainActor +struct CloudTerminalNavigationTests { + @Test("Invalid owner coordinates never submit a navigation operation", arguments: 0..<4) + func rejectsInvalidOwner(variant: Int) async { + let machine = SurfaceMachineID.cloud("navigation") + let resource = SurfaceResourceID(machine: machine, kind: .terminal, key: "term-1") + let workspace = SurfaceRemoteWorkspace(id: "ws-1", name: "Workspace", index: 0, focused: false) + let view = SurfaceRemoteView(tabID: "tab-1", workspace: workspace) + let controller = CloudWorkspaceOperationController(isAvailable: { true }, notificationCenter: NotificationCenter()) + var submissions = 0 + let navigation = CloudTreeTerminalNavigationCoordinator( + machineName: { _ in "Machine" }, + run: { _, _ in submissions += 1; return Task {} }, + host: AppDelegate.makeCloudTerminalNavigationHost(), + operationController: controller + ) + let group = SurfaceResourceGroup( + title: "Workspace", resources: [resource], + remoteWorkspaceID: variant == 1 ? "ws-other" : variant == 2 ? "" : nil + ) + navigation.open( + machine: variant == 0 ? .cloud("other") : machine, + group: group, resource: resource, view: variant < 2 ? view : nil, openIn: nil + ) + await controller.waitForPendingOperations() + #expect(submissions == 0) + } + + @Test("Duplicate valid activations submit one operation with the machine's display name") + func deduplicatesActivations() async { + let machine = SurfaceMachineID.cloud("navigation") + let resource = SurfaceResourceID(machine: machine, kind: .terminal, key: "term-1") + let workspace = SurfaceRemoteWorkspace(id: "ws-1", name: "Workspace", index: 0, focused: false) + let view = SurfaceRemoteView(tabID: "tab-1", workspace: workspace) + let group = SurfaceResourceGroup(title: "Workspace", resources: [resource], remoteWorkspaceID: workspace.id) + let controller = CloudWorkspaceOperationController(isAvailable: { true }, notificationCenter: NotificationCenter()) + var labels: [String] = [] + let navigation = CloudTreeTerminalNavigationCoordinator( + machineName: { _ in "Friendly machine" }, + run: { label, _ in labels.append(label); return Task {} }, + host: AppDelegate.makeCloudTerminalNavigationHost(), + operationController: controller + ) + for _ in 0..<2 { + navigation.open(machine: machine, group: group, resource: resource, view: view, openIn: nil) + } + await controller.waitForPendingOperations() + #expect(labels.count == 1) + #expect(labels.first?.contains("Friendly machine") == true) + } + + @Test("Navigation reuses the exact daemon-tab projection through the real catalog") + func reusesProjection() async throws { + try await AppContextSerialGate.withExclusiveAppContext { + let fixture = try CloudWorkspaceCreationSidebarFixture() + defer { fixture.close() } + let workspace = try await fixture.provider.createRemoteWorkspace(name: "Navigation") + let resource = try #require(fixture.catalog.snapshot.resources.first { $0.id.machine == fixture.provider.machine }) + let view = try #require(resource.remoteViews?.first) + let initial = try await fixture.catalog.project( + resource.id, into: .workspace(id: fixture.originalWorkspaceID, placement: .tab), + focus: false, reuseExisting: true, reuseInWorkspace: fixture.originalWorkspaceID, remoteView: view + ) + let group = SurfaceResourceGroup( + title: workspace.name, placements: [SurfaceResourcePlacement(resource: resource.id, remoteView: view)], + remoteWorkspaceID: workspace.id + ) + let controller = CloudWorkspaceOperationController(isAvailable: { true }, notificationCenter: NotificationCenter()) + var failures: [any Error] = [] + let navigation = CloudTreeTerminalNavigationCoordinator( + machineName: { _ in "Machine" }, + run: { _, operation in + Task { @MainActor in + do { try await operation(fixture.catalog) } + catch { failures.append(error) } + } + }, + host: AppDelegate.makeCloudTerminalNavigationHost(), + operationController: controller + ) + navigation.open(machine: fixture.provider.machine, group: group, resource: resource.id, view: view, openIn: nil) + await controller.waitForPendingOperations() + #expect(failures.isEmpty) + let projections = fixture.catalog.projections.filter { $0.resource == resource.id } + #expect(projections.count == 1) + #expect(projections.first?.panelID == initial.projection.panelID) + #expect(projections.first?.remoteTabID == view.tabID) + #expect(fixture.manager.tabs.count == 1) + #expect(fixture.manager.selectedTabId == initial.projection.workspaceID) + } + } +} diff --git a/cmuxTests/CloudTerminalPlacementTests.swift b/cmuxTests/CloudTerminalPlacementTests.swift index cea41480105f..9d72003b99da 100644 --- a/cmuxTests/CloudTerminalPlacementTests.swift +++ b/cmuxTests/CloudTerminalPlacementTests.swift @@ -342,6 +342,7 @@ struct CloudTerminalPlacementTests { var finished = false // No live local destination makes these exercise the awaited sidebar path. let actions = CloudTreeNodeActions.bound( + navigationHost: AppDelegate.makeCloudTerminalNavigationHost(), catalog: { catalog }, selectedWorkspaceID: { UUID() }, selectLocalWorkspace: { _ in }, onWillMutate: { _ in }, onDidMutate: { finished = true }, onFailure: { failures.append($0) }, refresh: {} @@ -373,6 +374,7 @@ struct CloudTerminalPlacementTests { var failures: [String] = [] var finished = false let actions = CloudTreeNodeActions.bound( + navigationHost: AppDelegate.makeCloudTerminalNavigationHost(), catalog: { catalog }, selectedWorkspaceID: { selectedDestination }, selectLocalWorkspace: { _ in }, onWillMutate: { _ in }, onDidMutate: { finished = true }, onFailure: { failures.append($0) }, refresh: {} diff --git a/cmuxTests/CloudTreeWorkspaceTitleLayoutTests.swift b/cmuxTests/CloudTreeWorkspaceTitleLayoutTests.swift index 1c4ea0b0a2b7..b7a4e57b60d8 100644 --- a/cmuxTests/CloudTreeWorkspaceTitleLayoutTests.swift +++ b/cmuxTests/CloudTreeWorkspaceTitleLayoutTests.swift @@ -59,6 +59,7 @@ struct CloudTreeWorkspaceTitleLayoutTests { func containerDocumentFillsScrollViewportAtWideAndNarrowSizes() { let machineActions = MachineRowActions.bound(onDidMutate: {}) let nodeActions = CloudTreeNodeActions.bound( + navigationHost: AppDelegate.makeCloudTerminalNavigationHost(), catalog: { SurfaceCatalog.shared }, selectedWorkspaceID: { nil }, selectLocalWorkspace: { _ in }, onWillMutate: { _ in }, onDidMutate: {}, onFailure: { _ in }, refresh: {} diff --git a/cmuxTests/CloudWorkspaceCreationSidebarTests.swift b/cmuxTests/CloudWorkspaceCreationSidebarTests.swift index 5005e53859d7..dc9398dcb57c 100644 --- a/cmuxTests/CloudWorkspaceCreationSidebarTests.swift +++ b/cmuxTests/CloudWorkspaceCreationSidebarTests.swift @@ -272,6 +272,7 @@ struct CloudWorkspaceCreationSidebarTests { fixture.provider.usesReceipt = true let completed = CloudLinkFirstValue() let actions = CloudTreeNodeActions.bound( + navigationHost: AppDelegate.makeCloudTerminalNavigationHost(), catalog: { fixture.catalog }, selectedWorkspaceID: { fixture.manager.selectedTabId }, selectLocalWorkspace: { fixture.manager.selectedTabId = $0 }, onWillMutate: { _ in }, onDidMutate: { completed.resolve(true) }, diff --git a/scripts/stage-cloud-navigation-tests.py b/scripts/stage-cloud-navigation-tests.py new file mode 100644 index 000000000000..1ca4d2494d56 --- /dev/null +++ b/scripts/stage-cloud-navigation-tests.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Stage the production navigation policy and its value closure for standalone SwiftPM tests. + +This is a test fixture, not a shipping package or a replacement for app integration. +The temporary package contains verbatim production declarations and the same fake-capability +tests wired into cmuxTests. It makes the remaining value-package prerequisite explicit while +proving that navigation no longer needs app/window/catalog implementations to execute. +""" + +import hashlib +import json +from pathlib import Path + + +def main(): + root = Path(__file__).resolve().parent.parent + output = root / ".local" / "cloud-navigation-probe" + sources = output / "Sources" / "CloudNavigationProbe" + tests = output / "Tests" / "CloudNavigationProbeTests" + sources.mkdir(parents=True, exist_ok=True) + tests.mkdir(parents=True, exist_ok=True) + manifest = [] + + def stage(path, declaration=None, test=False): + text = (root / path).read_text() + if declaration: + # These known value declarations use a column-zero closing brace. Refuse + # changed declaration anchors instead of silently staging another type. + start = text.index(declaration) + end = text.index("\n}", start) + 2 + content = "import Foundation\n\n" + text[start:end] + "\n" + name = declaration.split(":")[0].split()[-1] + ".swift" + else: + content = text + name = Path(path).name + if test: + start = content.index("#if canImport(cmux_DEV)") + end = content.index("#endif", start) + len("#endif") + content = content[:start] + "@testable import CloudNavigationProbe" + content[end:] + target = (tests if test else sources) / name + # Preserve timestamps for controlled body/interface edit comparisons. + if not target.exists() or target.read_text() != content: + target.write_text(content) + manifest.append({"source": path, "declaration": declaration, + "source_sha256": hashlib.sha256(text.encode()).hexdigest(), + "staged_sha256": hashlib.sha256(content.encode()).hexdigest()}) + + for name in ["CloudTreeTerminalNavigationCoordinator", "CloudTerminalNavigationCatalog", + "CloudTerminalNavigationHost", "CloudTerminalNavigationScheduling", + "SurfaceResourceGroup+CloudNavigation"]: + stage(f"Sources/Cloud/{name}.swift") + for kind, name in [("enum", "SurfaceMachineID"), ("enum", "SurfaceResourceKind"), + ("enum", "SurfaceLifecycle"), ("struct", "SurfaceAgentBadge"), + ("struct", "CloudCreationAttachment"), ("struct", "SurfaceResource"), + ("struct", "SurfaceResourceID"), ("struct", "SurfaceRemoteWorkspace"), + ("struct", "SurfaceRemoteView"), ("struct", "SurfaceProjection"), + ("enum", "SurfaceSplitDirection"), ("enum", "SurfaceCatalogError")]: + stage("Sources/Surfaces/SurfaceCatalogModel.swift", f"{kind} {name}:") + stage("Sources/Surfaces/SurfaceResourcePlacement.swift") + stage("Sources/Surfaces/SurfaceCatalog+Groups.swift", "struct SurfaceResourceGroup:") + stage("Sources/Surfaces/CloudWorkspaceLayoutTranslator.swift", "indirect enum SurfaceProjectionLayout:") + for name in ["CloudTerminalNavigationFixture", "CloudTerminalNavigationCapabilityTests"]: + stage(f"cmuxTests/{name}.swift", test=True) + package = '''// swift-tools-version: 6.0 +import PackageDescription +let package = Package( + name: "CloudNavigationProbe", + platforms: [.macOS(.v14)], + targets: [ + .target(name: "CloudNavigationProbe", swiftSettings: [.swiftLanguageMode(.v5)]), + .testTarget(name: "CloudNavigationProbeTests", dependencies: ["CloudNavigationProbe"]) + ] +) +''' + package_path = output / "Package.swift" + if not package_path.exists() or package_path.read_text() != package: + package_path.write_text(package) + (output / "sources.json").write_text(json.dumps(manifest, indent=2) + "\n") + print(f"Staged {len(manifest)} production/test inputs at {output.relative_to(root)}") + print("Run SwiftPM tests through the native build owner; this fixture does not validate app/CLI linkage.") + + +if __name__ == "__main__": + main()