import HostRegistry import SwiftUI /// T-iOS-15 · Root of the app: route switch (Pairing / SessionList), terminal /// push, add-host sheet, scenePhase forwarding and the privacy shade. /// /// The shade is the TOPMOST layer of this ZStack and appears whenever /// `scenePhase != .active` (exact rule — PrivacyShadePolicy + tests); it /// covers the whole navigation tree, terminal included. Sheets live above any /// overlay, but no sheet renders terminal bytes (pairing / plan gate only). struct RootView: View { @Bindable var coordinator: AppCoordinator @Environment(\.scenePhase) private var scenePhase var body: some View { ZStack { NavigationStack { rootContent .navigationDestination(isPresented: terminalBinding) { terminalDestination } } if PrivacyShadePolicy.isShadeVisible(for: scenePhase) { PrivacyShadeView() } } .task { await coordinator.bootstrap() } .onChange(of: scenePhase) { _, phase in coordinator.handleScenePhase(phase) } .sheet( isPresented: $coordinator.isAddHostPresented, onDismiss: { coordinator.addHostDismissed() } ) { addHostSheet } } // MARK: - Route switch @ViewBuilder private var rootContent: some View { switch coordinator.route { case .loading: ProgressView() case .pairing: firstRunPairing case .sessions: sessionList } } @ViewBuilder private var firstRunPairing: some View { if let viewModel = coordinator.rootPairingViewModel { PairingScreen(viewModel: viewModel) { host in coordinator.completeFirstPairing(host) } } else { ProgressView() } } private var sessionList: some View { SessionListScreen( viewModel: coordinator.sessionList, onOpen: { coordinator.open($0) }, onAddHost: { coordinator.presentAddHost() } ) .safeAreaInset(edge: .bottom) { continueLastBanner } } // MARK: - "继续上次" highlight (cold start step 5) @ViewBuilder private var continueLastBanner: some View { if coordinator.continueLastSessionId != nil { Button { coordinator.openContinueLast() } label: { Label(RootCopy.continueLast, systemImage: "arrow.uturn.forward.circle.fill") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .padding(.horizontal, RootMetrics.bannerHorizontalPadding) .padding(.vertical, RootMetrics.bannerVerticalPadding) .background(.thinMaterial) } } // MARK: - Terminal push private var terminalBinding: Binding { Binding( get: { coordinator.terminalController != nil }, set: { presented in guard !presented else { return } coordinator.closeTerminal() // back → engine.close() (detach) } ) } @ViewBuilder private var terminalDestination: some View { if let controller = coordinator.terminalController { TerminalContainerView(controller: controller) } } // MARK: - Add-host sheet (multi-host entry, list header) @ViewBuilder private var addHostSheet: some View { if let viewModel = coordinator.addHostPairingViewModel { NavigationStack { PairingScreen(viewModel: viewModel) { host in coordinator.completeAddHost(host) } } } } } private enum RootMetrics { static let bannerHorizontalPadding: CGFloat = 16 static let bannerVerticalPadding: CGFloat = 8 } private enum RootCopy { static let continueLast = "继续上次会话" }