spb/zyquo-local Public MIT
Native macOS AI chat that runs LLMs 100% locally on Apple Silicon with MLX — no cloud, no API keys.
Swift 97.2%
Shell 1.8%
Makefile 1%
1//2// ZyquoLocalApp.swift3// Zyquo Local4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import SwiftUI1011struct ZyquoLocalApp: App {12 @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate13 @State private var app = AppModel()14 @Environment(\.openWindow) private var openWindow1516 var body: some Scene {17 WindowGroup {18 if HardwareGate.isAppleSilicon {19 RootView()20 .environment(app)21 .frame(minWidth: 980, minHeight: 640)22 .onAppear {23 QuickChatPanelController.shared.setup(app: app)24 }25 } else {26 UnsupportedHardwareView()27 }28 }29 .defaultSize(width: 1240, height: 800)30 .windowStyle(.hiddenTitleBar)31 .windowToolbarStyle(.unifiedCompact)32 .commands {33 CommandGroup(replacing: .newItem) {34 Button("New Chat") { app.newConversation() }35 .keyboardShortcut("n", modifiers: .command)36 }37 CommandMenu("Model") {38 Button("Model Switcher…") {39 NotificationCenter.default.post(name: .zyquoOpenModelSwitcher, object: nil)40 }41 .keyboardShortcut("k", modifiers: .command)42 Button("Library") { app.route = .library }43 .keyboardShortcut("l", modifiers: .command)44 Divider()45 Button("Compare Models…") { openWindow(id: "compare") }46 Button("Quick Chat") { QuickChatPanelController.shared.toggle() }47 .keyboardShortcut(.space, modifiers: .option)48 Divider()49 Button("Unload Model") { Task { await app.unloadModel() } }50 .disabled(app.loadedModelID == nil)51 }52 CommandGroup(after: .textEditing) {53 Button("Search Chats") {54 NotificationCenter.default.post(name: .zyquoFocusSearch, object: nil)55 }56 .keyboardShortcut("f", modifiers: .command)57 }58 CommandGroup(replacing: .importExport) {59 Button("Export Conversation…") {60 NotificationCenter.default.post(name: .zyquoExportConversation, object: nil)61 }62 .keyboardShortcut("e", modifiers: [.command, .shift])63 }64 }6566 Window("Compare Models", id: "compare") {67 CompareView()68 .environment(app)69 }70 .defaultSize(width: 1100, height: 700)7172 Settings {73 SettingsView()74 .environment(app)75 }7677 MenuBarExtra(isInserted: menuBarBinding) {78 MenuBarExtraContent()79 .environment(app)80 } label: {81 Image(nsImage: menuBarIcon)82 }83 .menuBarExtraStyle(.menu)84 }8586 private var menuBarBinding: Binding<Bool> {87 Binding(88 get: { app.settings.menuBarExtraEnabled },89 set: { app.settings.menuBarExtraEnabled = $0 }90 )91 }9293 private var menuBarIcon: NSImage {94 if let url = Bundle.main.url(forResource: "MenuBarIcon", withExtension: "png"),95 let image = NSImage(contentsOf: url)96 {97 image.isTemplate = true98 image.size = NSSize(width: 18, height: 18)99 return image100 }101 let fallback = NSImage(102 systemSymbolName: "bolt.circle", accessibilityDescription: "Zyquo Local")!103 fallback.isTemplate = true104 return fallback105 }106}107108/// Menu bar extra: state at a glance + quick actions.109struct MenuBarExtraContent: View {110 @Environment(AppModel.self) private var app111 @Environment(\.openWindow) private var openWindow112113 var body: some View {114 Group {115 if let repoID = app.loadedModelID {116 Text("Loaded: \(shortModelName(repoID))")117 } else {118 Text("No model loaded")119 }120 if app.downloads.activeCount > 0 {121 Text("Downloading \(app.downloads.activeCount) model\(app.downloads.activeCount == 1 ? "" : "s") · \(Int(app.downloads.overallFraction * 100))%")122 }123 Divider()124 Button("Quick Chat ⌥Space") { QuickChatPanelController.shared.toggle() }125 Button("Open Zyquo Local") {126 NSApp.activate(ignoringOtherApps: true)127 NSApp.windows.first { $0.canBecomeMain }?.makeKeyAndOrderFront(nil)128 }129 Divider()130 Button("Quit Zyquo Local") { NSApp.terminate(nil) }131 }132 }133}134135final class AppDelegate: NSObject, NSApplicationDelegate {136 func applicationDidFinishLaunching(_ notification: Notification) {137 // Proper foreground activation when launched from a terminal.138 NSApp.setActivationPolicy(.regular)139 NSApp.activate(ignoringOtherApps: true)140 }141}142