diff --git a/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift b/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift index f11834f17d7..6923b0c1402 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift @@ -183,8 +183,7 @@ struct SettingsPanel: View { store: store, threadManager: threadManager, onClose: onClose, - daemonClient: daemonClient, - onNavigateToConnect: { selectedTab = .connect } + daemonClient: daemonClient ) } } diff --git a/clients/macos/vellum-assistant/Features/Settings/SettingsAdvancedTab.swift b/clients/macos/vellum-assistant/Features/Settings/SettingsAdvancedTab.swift index a65ee2ed936..34215bae2df 100644 --- a/clients/macos/vellum-assistant/Features/Settings/SettingsAdvancedTab.swift +++ b/clients/macos/vellum-assistant/Features/Settings/SettingsAdvancedTab.swift @@ -3,17 +3,14 @@ import SwiftUI import VellumAssistantShared /// Advanced settings tab — computer usage limits, private threads, -/// archived threads, iOS device pairing, and developer tools. +/// archived threads, and developer tools. @MainActor struct SettingsAdvancedTab: View { @ObservedObject var store: SettingsStore @ObservedObject var threadManager: ThreadManager var onClose: () -> Void var daemonClient: DaemonClient? - var onNavigateToConnect: (() -> Void)? - @State private var iosPairingEnabled: Bool = false - @State private var showingPairingWarning: Bool = false @State private var showingRetireConfirmation: Bool = false @State private var isRetiring: Bool = false @State private var lockfileAssistants: [LockfileAssistant] = [] @@ -21,9 +18,6 @@ struct SettingsAdvancedTab: View { @State private var identity: IdentityInfo? @State private var remoteIdentity: RemoteIdentityInfo? @State private var flagStates: [(flag: FeatureFlag, enabled: Bool)] = [] - @AppStorage(PairingConfiguration.overrideEnabledKey) private var iosPairingUseOverride: Bool = false - @AppStorage(PairingConfiguration.gatewayOverrideKey) private var iosPairingGatewayOverride: String = "" - @AppStorage(PairingConfiguration.tokenOverrideKey) private var iosPairingTokenOverride: String = "" #if DEBUG @State private var showingEnvVars = false @@ -37,7 +31,6 @@ struct SettingsAdvancedTab: View { computerUsageSection privateThreadSection archivedThreadsSection - iosDeviceSection switchAssistantSection retireAssistantSection hatchNewAssistantSection @@ -48,8 +41,6 @@ struct SettingsAdvancedTab: View { #endif } .onAppear { - let flagPath = NSHomeDirectory() + "/.vellum/ios-pairing-enabled" - iosPairingEnabled = FileManager.default.fileExists(atPath: flagPath) lockfileAssistants = LockfileAssistant.loadAll() selectedAssistantId = UserDefaults.standard.string(forKey: "connectedAssistantId") ?? "" identity = IdentityInfo.load() @@ -265,158 +256,6 @@ struct SettingsAdvancedTab: View { } } - // MARK: - iOS Device - - private var iosDeviceSection: some View { - VStack(alignment: .leading, spacing: VSpacing.md) { - Text("iOS Device") - .font(VFont.sectionTitle) - .foregroundColor(VColor.textPrimary) - - // Pairing toggle - HStack { - VStack(alignment: .leading, spacing: VSpacing.xs) { - Text("Enable iOS Pairing") - .font(VFont.bodyMedium) - .foregroundColor(VColor.textPrimary) - Text("Allow your iPhone to connect via the gateway (bearer-token authenticated).") - .font(VFont.caption) - .foregroundColor(VColor.textSecondary) - } - Spacer() - Toggle("", isOn: $iosPairingEnabled) - .toggleStyle(.switch) - .labelsHidden() - .onChange(of: iosPairingEnabled) { _, enabled in - if enabled { - if !UserDefaults.standard.bool(forKey: "ios_pairing_warning_shown") { - showingPairingWarning = true - } else { - setIOSPairingEnabled(true) - } - } else { - setIOSPairingEnabled(false) - } - } - } - - // Global Gateway & Token readout - if iosPairingEnabled { - iosGlobalConfigCard - iosOverrideSection - } - } - .padding(VSpacing.lg) - .vCard(background: VColor.surfaceSubtle) - .alert("Enable iOS Pairing", isPresented: $showingPairingWarning) { - Button("Cancel", role: .cancel) { - iosPairingEnabled = false - } - Button("Enable") { - UserDefaults.standard.set(true, forKey: "ios_pairing_warning_shown") - setIOSPairingEnabled(true) - } - } message: { - Text("Your iPhone will connect through the gateway. Only devices with a valid session token can reach your assistant.") - } - } - - /// Card showing the resolved gateway URL and masked bearer token for iOS pairing. - private var iosGlobalConfigCard: some View { - VStack(alignment: .leading, spacing: VSpacing.sm) { - Text("Using Global Gateway & Token") - .font(VFont.caption) - .foregroundColor(VColor.textMuted) - .textCase(.uppercase) - - HStack(alignment: .top) { - Text("Gateway URL") - .font(VFont.caption) - .foregroundColor(VColor.textMuted) - .frame(width: 90, alignment: .leading) - Text(store.resolvedIosGatewayUrl.isEmpty ? "Not configured" : store.resolvedIosGatewayUrl) - .font(VFont.mono) - .foregroundColor(store.resolvedIosGatewayUrl.isEmpty ? VColor.textMuted : VColor.textPrimary) - .lineLimit(1) - .truncationMode(.middle) - Spacer() - } - - HStack(alignment: .top) { - Text("Bearer Token") - .font(VFont.caption) - .foregroundColor(VColor.textMuted) - .frame(width: 90, alignment: .leading) - Text(store.resolvedIosBearerToken.isEmpty - ? "Not configured" - : String(repeating: "\u{2022}", count: 8)) - .font(VFont.mono) - .foregroundColor(store.resolvedIosBearerToken.isEmpty ? VColor.textMuted : VColor.textPrimary) - Spacer() - } - - Button("Manage in Connect tab") { - onNavigateToConnect?() - } - .font(VFont.caption) - .foregroundColor(VColor.accent) - } - .padding(VSpacing.md) - .background(VColor.surface.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) - .overlay( - RoundedRectangle(cornerRadius: VRadius.md) - .stroke(VColor.surfaceBorder.opacity(0.3), lineWidth: 1) - ) - } - - /// Collapsed override section for per-integration iOS gateway/token customization. - private var iosOverrideSection: some View { - DisclosureGroup("Override") { - VStack(alignment: .leading, spacing: VSpacing.sm) { - Toggle("Use custom gateway for iOS", isOn: $iosPairingUseOverride) - .toggleStyle(.switch) - .font(VFont.body) - .foregroundColor(VColor.textSecondary) - - if iosPairingUseOverride { - VStack(alignment: .leading, spacing: VSpacing.xs) { - Text("Gateway URL Override") - .font(VFont.caption) - .foregroundColor(VColor.textSecondary) - TextField("https://custom-gateway.example.com", text: $iosPairingGatewayOverride) - .vInputStyle() - .font(VFont.body) - .foregroundColor(VColor.textPrimary) - } - - VStack(alignment: .leading, spacing: VSpacing.xs) { - Text("Token Override") - .font(VFont.caption) - .foregroundColor(VColor.textSecondary) - SecureField("Custom bearer token", text: $iosPairingTokenOverride) - .vInputStyle() - .font(VFont.body) - .foregroundColor(VColor.textPrimary) - } - } - } - .padding(.top, VSpacing.sm) - } - .font(VFont.caption) - .foregroundColor(VColor.textSecondary) - } - - private func setIOSPairingEnabled(_ enabled: Bool) { - let flagPath = NSHomeDirectory() + "/.vellum/ios-pairing-enabled" - if enabled { - FileManager.default.createFile(atPath: flagPath, contents: nil) - } else { - try? FileManager.default.removeItem(atPath: flagPath) - } - } - - // MARK: - Switch Assistant @ViewBuilder diff --git a/clients/macos/vellum-assistant/Features/Settings/SettingsConnectTab.swift b/clients/macos/vellum-assistant/Features/Settings/SettingsConnectTab.swift index 6abcfcd73c8..2a4e645feb0 100644 --- a/clients/macos/vellum-assistant/Features/Settings/SettingsConnectTab.swift +++ b/clients/macos/vellum-assistant/Features/Settings/SettingsConnectTab.swift @@ -18,6 +18,9 @@ struct SettingsConnectTab: View { @State private var gatewayTargetCopied: Bool = false @State private var showingPairingQR: Bool = false @State private var showingRegenerateConfirmation: Bool = false + @State private var iosPairingEnabled: Bool = false + @State private var showingPairingWarning: Bool = false + @State private var devPairingExpanded: Bool = false // Telegram credential entry @State private var telegramBotTokenText = "" @@ -42,11 +45,11 @@ struct SettingsConnectTab: View { var body: some View { VStack(alignment: .leading, spacing: VSpacing.xl) { + pairingSection gatewaySection bearerTokenSection telegramCard twilioCard - pairingSection statusSection testConnectionSection developerLocalPairingSection @@ -57,6 +60,7 @@ struct SettingsConnectTab: View { refreshBearerToken() store.refreshChannelGuardianStatus(channel: "telegram") store.refreshChannelGuardianStatus(channel: "sms") + iosPairingEnabled = FileManager.default.fileExists(atPath: NSHomeDirectory() + "/.vellum/ios-pairing-enabled") } .onChange(of: store.ingressPublicBaseUrl) { _, newValue in if !isGatewayUrlFocused { @@ -76,6 +80,17 @@ struct SettingsConnectTab: View { } message: { Text("This will replace the current bearer token and restart the daemon. Any paired devices will need to reconnect.") } + .alert("Enable iOS Pairing", isPresented: $showingPairingWarning) { + Button("Cancel", role: .cancel) { + iosPairingEnabled = false + } + Button("Enable") { + UserDefaults.standard.set(true, forKey: "ios_pairing_warning_shown") + setIOSPairingEnabled(true) + } + } message: { + Text("Your iPhone will connect through the gateway. Only devices with a valid session token can reach your assistant.") + } .sheet(isPresented: $showingPairingQR) { PairingQRCodeSheet( ingressEnabled: store.ingressEnabled, @@ -822,6 +837,34 @@ struct SettingsConnectTab: View { .font(VFont.sectionTitle) .foregroundColor(VColor.textPrimary) + // Enable iOS Pairing toggle + HStack { + VStack(alignment: .leading, spacing: VSpacing.xs) { + Text("Enable iOS Pairing") + .font(VFont.bodyMedium) + .foregroundColor(VColor.textPrimary) + Text("Allow your iPhone to connect via the gateway (bearer-token authenticated).") + .font(VFont.caption) + .foregroundColor(VColor.textSecondary) + } + Spacer() + Toggle("", isOn: $iosPairingEnabled) + .toggleStyle(.switch) + .labelsHidden() + .onChange(of: iosPairingEnabled) { _, enabled in + if enabled { + if !UserDefaults.standard.bool(forKey: "ios_pairing_warning_shown") { + showingPairingWarning = true + } else { + setIOSPairingEnabled(true) + } + } else { + setIOSPairingEnabled(false) + } + } + } + + // QR code button HStack { VStack(alignment: .leading, spacing: VSpacing.xs) { Text("Pair an iOS device") @@ -836,6 +879,46 @@ struct SettingsConnectTab: View { showingPairingQR = true } } + + // Gateway & token readout (when pairing is enabled) + if iosPairingEnabled { + Divider().background(VColor.surfaceBorder) + + VStack(alignment: .leading, spacing: VSpacing.sm) { + HStack(alignment: .top) { + Text("Gateway URL") + .font(VFont.caption) + .foregroundColor(VColor.textMuted) + .frame(width: 90, alignment: .leading) + Text(store.resolvedIosGatewayUrl.isEmpty ? "Not configured" : store.resolvedIosGatewayUrl) + .font(VFont.mono) + .foregroundColor(store.resolvedIosGatewayUrl.isEmpty ? VColor.textMuted : VColor.textPrimary) + .lineLimit(1) + .truncationMode(.middle) + Spacer() + } + + HStack(alignment: .top) { + Text("Bearer Token") + .font(VFont.caption) + .foregroundColor(VColor.textMuted) + .frame(width: 90, alignment: .leading) + Text(store.resolvedIosBearerToken.isEmpty + ? "Not configured" + : String(repeating: "\u{2022}", count: 8)) + .font(VFont.mono) + .foregroundColor(store.resolvedIosBearerToken.isEmpty ? VColor.textMuted : VColor.textPrimary) + Spacer() + } + } + .padding(VSpacing.md) + .background(VColor.surface.opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: VRadius.md)) + .overlay( + RoundedRectangle(cornerRadius: VRadius.md) + .stroke(VColor.surfaceBorder.opacity(0.3), lineWidth: 1) + ) + } } .padding(VSpacing.lg) .vCard(background: VColor.surfaceSubtle) @@ -965,10 +1048,20 @@ struct SettingsConnectTab: View { private var developerLocalPairingSection: some View { VStack(alignment: .leading, spacing: VSpacing.md) { - Text("Developer Local Pairing (LAN-only)") - .font(VFont.sectionTitle) - .foregroundColor(VColor.textPrimary) + DisclosureGroup(isExpanded: $devPairingExpanded) { + developerLocalPairingContent + } label: { + Text("Developer Local Pairing (LAN-only)") + .font(VFont.sectionTitle) + .foregroundColor(VColor.textPrimary) + } + } + .padding(VSpacing.lg) + .vCard(background: VColor.surfaceSubtle) + } + private var developerLocalPairingContent: some View { + VStack(alignment: .leading, spacing: VSpacing.md) { // Enable toggle HStack { VStack(alignment: .leading, spacing: VSpacing.xs) { @@ -1071,8 +1164,6 @@ struct SettingsConnectTab: View { } } } - .padding(VSpacing.lg) - .vCard(background: VColor.surfaceSubtle) } // MARK: - Connection Status Helpers @@ -1151,6 +1242,17 @@ struct SettingsConnectTab: View { return "\(hours) hours ago" } + // MARK: - iOS Pairing Helpers + + private func setIOSPairingEnabled(_ enabled: Bool) { + let flagPath = NSHomeDirectory() + "/.vellum/ios-pairing-enabled" + if enabled { + FileManager.default.createFile(atPath: flagPath, contents: nil) + } else { + try? FileManager.default.removeItem(atPath: flagPath) + } + } + // MARK: - Token Helpers private func refreshBearerToken() {