spb/zyquo-router Public MIT
One local endpoint, every AI provider — a private OpenAI-compatible LLM gateway for your Mac (170 models, 12 providers).
Swift 95.7%
Python 2.3%
Shell 1.2%
Makefile 0.9%
1//2// SettingsView.swift3// Zyquo Router4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Native settings tabs (720×540): Server, Logging, Usage & Pricing,9// Appearance, Shortcuts, Advanced. Server + Appearance are fully live in10// Phase 4; the rest fill in with Phase 6 features.11//1213import ServiceManagement14import SwiftUI15import UniformTypeIdentifiers1617struct SettingsView: View {18 var body: some View {19 TabView {20 ServerSettings()21 .tabItem { Label("Server", systemImage: "server.rack") }22 LoggingSettings()23 .tabItem { Label("Logging", systemImage: "doc.text") }24 UsageSettings()25 .tabItem { Label("Usage & Pricing", systemImage: "dollarsign.circle") }26 AppearanceSettings()27 .tabItem { Label("Appearance", systemImage: "paintpalette") }28 ShortcutsSettings()29 .tabItem { Label("Shortcuts", systemImage: "keyboard") }30 AdvancedSettings()31 .tabItem { Label("Advanced", systemImage: "gearshape.2") }32 }33 .frame(width: ZyquoMetrics.settingsWidth, height: ZyquoMetrics.settingsHeight)34 }35}3637private struct ServerSettings: View {38 @EnvironmentObject private var server: ServerController39 @AppStorage("autoStartServer") private var autoStart = false40 @AppStorage("keepServingWhenClosed") private var keepServing = true41 @AppStorage("menuBarExtraEnabled") private var menuBarExtra = true42 @State private var launchAtLogin = SMAppService.mainApp.status == .enabled4344 var body: some View {45 Form {46 TextField("Default port:", value: $server.port, format: .number.grouping(.never))47 .frame(width: 200)48 Picker("Bind:", selection: $server.bindLAN) {49 Text("Localhost only").tag(false)50 Text("LAN (requires a local API key)").tag(true)51 }52 .frame(width: 340)53 Toggle("Start the server when the app launches", isOn: $autoStart)54 Toggle("Keep serving when the window is closed", isOn: $keepServing)55 Toggle("Launch Zyquo Router at login", isOn: $launchAtLogin)56 .onChange(of: launchAtLogin) { enabled in57 do {58 if enabled {59 try SMAppService.mainApp.register()60 } else {61 try SMAppService.mainApp.unregister()62 }63 } catch {64 launchAtLogin = SMAppService.mainApp.status == .enabled65 }66 }67 Toggle("Show the menu bar extra", isOn: $menuBarExtra)68 Text("Request body limit: 32 MB · streaming timeout: 15 min")69 .font(ZyquoFont.caption)70 .foregroundStyle(ZyquoColor.textSecondary)71 }72 .padding(ZyquoSpacing.xl)73 }74}7576private struct LoggingSettings: View {77 @AppStorage("logBodiesRevealed") private var reveal = false7879 var body: some View {80 Form {81 Toggle("Reveal request/response bodies in the inspector (this session)", isOn: $reveal)82 Text("Bodies are redacted by default. Metadata (model, status, latency, tokens, cost) is always recorded; provider keys are never logged.")83 .font(ZyquoFont.caption)84 .foregroundStyle(ZyquoColor.textSecondary)85 }86 .padding(ZyquoSpacing.xl)87 }88}8990private struct UsageSettings: View {91 @EnvironmentObject private var catalog: ModelCatalog92 @EnvironmentObject private var server: ServerController93 @State private var confirmingReset = false9495 var body: some View {96 Form {97 Text("Costs are estimated from the catalog's per-model pricing (\(catalog.all.count) models). Usage counters reset daily at midnight.")98 .font(ZyquoFont.body())99 .foregroundStyle(ZyquoColor.textSecondary)100 Button("Reset usage counters…") { confirmingReset = true }101 .confirmationDialog("Clear all recorded usage and the request log?", isPresented: $confirmingReset) {102 Button("Reset", role: .destructive) {103 let log = server.requestLog104 Task { await log.clear() }105 }106 }107 }108 .padding(ZyquoSpacing.xl)109 }110}111112private struct AppearanceSettings: View {113 @EnvironmentObject private var appearance: AppearanceStore114115 var body: some View {116 Form {117 Picker("Theme:", selection: $appearance.themeMode) {118 ForEach(ThemeMode.allCases) { mode in119 Text(mode.displayName).tag(mode)120 }121 }122 .pickerStyle(.segmented)123 .frame(width: 320)124125 Picker("Accent:", selection: $appearance.accent) {126 ForEach(AccentChoice.allCases) { accent in127 Text(accent.displayName).tag(accent)128 }129 }130 .frame(width: 320)131132 Slider(value: $appearance.fontSize, in: 12...16, step: 0.5) {133 Text("Font size: \(appearance.fontSize, format: .number.precision(.fractionLength(1)))pt")134 }135 .frame(width: 380)136 }137 .padding(ZyquoSpacing.xl)138 }139}140141private struct ShortcutsSettings: View {142 var body: some View {143 Form {144 shortcut("⌘R", "Start / stop the server")145 shortcut("⌘1–6", "Switch sections")146 shortcut("⌘⇧C", "Copy endpoint URL")147 shortcut("⌘K", "Command palette")148 shortcut("⌘F", "Filter requests")149 shortcut("⌘⏎", "Send in Playground")150 }151 .padding(ZyquoSpacing.xl)152 }153154 private func shortcut(_ keys: String, _ label: String) -> some View {155 HStack {156 Text(keys)157 .font(ZyquoFont.mono(size: 12, weight: .medium))158 .foregroundStyle(ZyquoColor.textPrimary)159 .frame(width: 70, alignment: .leading)160 Text(label)161 .font(ZyquoFont.body())162 .foregroundStyle(ZyquoColor.textSecondary)163 }164 }165}166167private struct AdvancedSettings: View {168 @EnvironmentObject private var routerConfig: RouterConfigStore169 @State private var importError: String?170171 var body: some View {172 Form {173 Button("Reveal data folder in Finder") {174 NSWorkspace.shared.activateFileViewerSelecting([PersistenceService.shared.rootDirectory])175 }176 HStack {177 Button("Export config…") { exportConfig() }178 Button("Import config…") { importConfig() }179 }180 if let importError {181 Text(importError)182 .font(ZyquoFont.caption)183 .foregroundStyle(ZyquoColor.danger)184 }185 Text("Config export covers aliases, fallback chains, disabled models, and favorites — provider keys stay in the encrypted vault (vault.zq) and are never exported in plaintext.")186 .font(ZyquoFont.caption)187 .foregroundStyle(ZyquoColor.textSecondary)188 }189 .padding(ZyquoSpacing.xl)190 }191192 private func exportConfig() {193 guard let data = try? routerConfig.exportData() else { return }194 let panel = NSSavePanel()195 panel.allowedContentTypes = [.json]196 panel.nameFieldStringValue = "zyquo-router-config.json"197 if panel.runModal() == .OK, let url = panel.url {198 try? data.write(to: url)199 }200 }201202 private func importConfig() {203 let panel = NSOpenPanel()204 panel.allowedContentTypes = [.json]205 panel.allowsMultipleSelection = false206 if panel.runModal() == .OK, let url = panel.url {207 do {208 try routerConfig.importData(try Data(contentsOf: url))209 importError = nil210 } catch {211 importError = "Not a valid Zyquo Router config file."212 }213 }214 }215}216