diff --git a/apps/macos/Sources/Clawdis/Constants.swift b/apps/macos/Sources/Clawdis/Constants.swift index bcefc6b8e..e509bf371 100644 --- a/apps/macos/Sources/Clawdis/Constants.swift +++ b/apps/macos/Sources/Clawdis/Constants.swift @@ -2,7 +2,7 @@ import Foundation let launchdLabel = "com.steipete.clawdis" let onboardingVersionKey = "clawdis.onboardingVersion" -let currentOnboardingVersion = 3 +let currentOnboardingVersion = 4 let pauseDefaultsKey = "clawdis.pauseEnabled" let iconAnimationsEnabledKey = "clawdis.iconAnimationsEnabled" let swabbleEnabledKey = "clawdis.swabbleEnabled" diff --git a/apps/macos/Sources/Clawdis/GeneralSettings.swift b/apps/macos/Sources/Clawdis/GeneralSettings.swift index 24e582be2..92821bfef 100644 --- a/apps/macos/Sources/Clawdis/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdis/GeneralSettings.swift @@ -125,18 +125,9 @@ struct GeneralSettings: View { TextField("user@host[:22]", text: self.$state.remoteTarget) .textFieldStyle(.roundedBorder) .frame(maxWidth: .infinity) - Menu { - if self.masterDiscovery.masters.isEmpty { - Button(self.masterDiscovery.statusText) {}.disabled(true) - } else { - ForEach(self.masterDiscovery.masters) { master in - Button(master.displayName) { self.applyDiscoveredMaster(master) } - } - } - } label: { - Image(systemName: "dot.radiowaves.left.and.right") + MasterDiscoveryMenu(discovery: self.masterDiscovery) { master in + self.applyDiscoveredMaster(master) } - .help("Discover Clawdis masters on your LAN") Button { Task { await self.testRemote() } } label: { diff --git a/apps/macos/Sources/Clawdis/MasterDiscoveryMenu.swift b/apps/macos/Sources/Clawdis/MasterDiscoveryMenu.swift new file mode 100644 index 000000000..78ba211aa --- /dev/null +++ b/apps/macos/Sources/Clawdis/MasterDiscoveryMenu.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct MasterDiscoveryMenu: View { + @ObservedObject var discovery: MasterDiscoveryModel + var onSelect: (MasterDiscoveryModel.DiscoveredMaster) -> Void + + var body: some View { + Menu { + if self.discovery.masters.isEmpty { + Button(self.discovery.statusText) {} + .disabled(true) + } else { + ForEach(self.discovery.masters) { master in + Button(master.displayName) { self.onSelect(master) } + } + } + } label: { + Image(systemName: "dot.radiowaves.left.and.right") + } + .help("Discover Clawdis masters on your LAN") + } +} + diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index c0b7532f5..f1d147524 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -44,16 +44,19 @@ struct OnboardingView: View { @State private var cliStatus: String? @State private var copied = false @State private var monitoringPermissions = false + @State private var monitoringDiscovery = false @State private var cliInstalled = false @State private var cliInstallLocation: String? @State private var gatewayStatus: GatewayEnvironmentStatus = .checking @State private var gatewayInstalling = false @State private var gatewayInstallMessage: String? + @StateObject private var masterDiscovery = MasterDiscoveryModel() @ObservedObject private var state = AppStateStore.shared @ObservedObject private var permissionMonitor = PermissionMonitor.shared private let pageWidth: CGFloat = 680 private let contentHeight: CGFloat = 520 + private let connectionPageIndex = 1 private let permissionsPageIndex = 3 private var pageCount: Int { 7 } private var buttonTitle: String { self.currentPage == self.pageCount - 1 ? "Finish" : "Next" } @@ -91,12 +94,18 @@ struct OnboardingView: View { .background(Color(NSColor.windowBackgroundColor)) .onAppear { self.currentPage = 0 - self.updatePermissionMonitoring(for: 0) + self.updateMonitoring(for: 0) } .onChange(of: self.currentPage) { _, newValue in - self.updatePermissionMonitoring(for: newValue) + self.updateMonitoring(for: newValue) + } + .onChange(of: self.state.connectionMode) { _, _ in + self.updateDiscoveryMonitoring(for: self.currentPage) + } + .onDisappear { + self.stopPermissionMonitoring() + self.stopDiscovery() } - .onDisappear { self.stopPermissionMonitoring() } .task { await self.refreshPerms() self.refreshCLIStatus() @@ -145,9 +154,14 @@ struct OnboardingView: View { if self.state.connectionMode == .remote { VStack(alignment: .leading, spacing: 8) { LabeledContent("SSH target") { - TextField("user@host[:22]", text: self.$state.remoteTarget) - .textFieldStyle(.roundedBorder) - .frame(width: 300) + HStack(spacing: 8) { + TextField("user@host[:22]", text: self.$state.remoteTarget) + .textFieldStyle(.roundedBorder) + .frame(width: 300) + MasterDiscoveryMenu(discovery: self.masterDiscovery) { master in + self.applyDiscoveredMaster(master) + } + } } DisclosureGroup("Advanced") { @@ -256,6 +270,17 @@ struct OnboardingView: View { } } + private func applyDiscoveredMaster(_ master: MasterDiscoveryModel.DiscoveredMaster) { + let host = master.tailnetDns ?? master.lanHost + guard let host else { return } + let user = NSUserName() + var target = "\(user)@\(host)" + if master.sshPort != 22 { + target += ":\(master.sshPort)" + } + self.state.remoteTarget = target + } + private func permissionsPage() -> some View { self.onboardingPage { Text("Grant permissions") @@ -549,12 +574,35 @@ struct OnboardingView: View { } } + private func updateDiscoveryMonitoring(for pageIndex: Int) { + let isConnectionPage = pageIndex == self.connectionPageIndex + let shouldMonitor = isConnectionPage && self.state.connectionMode == .remote + if shouldMonitor, !self.monitoringDiscovery { + self.monitoringDiscovery = true + self.masterDiscovery.start() + } else if !shouldMonitor, self.monitoringDiscovery { + self.monitoringDiscovery = false + self.masterDiscovery.stop() + } + } + + private func updateMonitoring(for pageIndex: Int) { + self.updatePermissionMonitoring(for: pageIndex) + self.updateDiscoveryMonitoring(for: pageIndex) + } + private func stopPermissionMonitoring() { guard self.monitoringPermissions else { return } self.monitoringPermissions = false PermissionMonitor.shared.unregister() } + private func stopDiscovery() { + guard self.monitoringDiscovery else { return } + self.monitoringDiscovery = false + self.masterDiscovery.stop() + } + private func installCLI() async { guard !self.installingCLI else { return } self.installingCLI = true