// // oAIApp.swift // oAI // // Main app entry point // // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Rune Olsen // // This file is part of oAI. // // oAI is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // oAI is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General // Public License for more details. // // You should have received a copy of the GNU Affero General Public // License along with oAI. If not, see . import SwiftUI #if os(macOS) import AppKit #endif @main struct oAIApp: App { @State private var chatViewModel = ChatViewModel() @State private var showAbout = false init() { // Start email handler on app launch EmailHandlerService.shared.start() // Sync Git changes on app launch (pull + import) Task { await GitSyncService.shared.syncOnStartup() } // Check for updates in the background UpdateCheckService.shared.checkForUpdates() } var body: some Scene { WindowGroup { ContentView() .environment(chatViewModel) .preferredColorScheme(.dark) .sheet(isPresented: $showAbout) { AboutView() } #if os(macOS) .onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in // Trigger auto-save on app quit Task { await chatViewModel.onAppWillTerminate() } } #endif } #if os(macOS) .windowStyle(.hiddenTitleBar) .windowToolbarStyle(.unified) .defaultSize(width: 1024, height: 800) .windowResizability(.contentMinSize) .commands { // ── Apple menu ──────────────────────────────────────────────── CommandGroup(replacing: .appInfo) { Button("About oAI") { showAbout = true } } CommandGroup(replacing: .appSettings) { Button("Settings…") { chatViewModel.showSettings = true } .keyboardShortcut(",", modifiers: .command) } // ── File menu ───────────────────────────────────────────────── // Replacing .newItem removes the auto-added "New Window" entry CommandGroup(replacing: .newItem) { Button("New Chat") { chatViewModel.newConversation() } .keyboardShortcut("n", modifiers: .command) } CommandGroup(after: .newItem) { Button("Open Chat…") { chatViewModel.showConversations = true } .keyboardShortcut("o", modifiers: .command) } CommandGroup(replacing: .saveItem) { Button("Save Chat") { chatViewModel.saveFromMenu() } .keyboardShortcut("s", modifiers: [.command, .shift]) .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty) } CommandGroup(after: .importExport) { Button("Export as Markdown…") { let name = chatViewModel.currentConversationName ?? "conversation" let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-") chatViewModel.exportConversation(format: "md", filename: "\(safe).md") } .disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty) } // ── Help menu ───────────────────────────────────────────────── CommandGroup(replacing: .help) { Button("oAI Help") { openHelp() } .keyboardShortcut("?", modifiers: .command) } } #endif } #if os(macOS) private func openHelp() { if let helpBookURL = Bundle.main.url(forResource: "oAI.help", withExtension: nil) { NSWorkspace.shared.open(helpBookURL.appendingPathComponent("Contents/Resources/en.lproj/index.html")) } else { // Fallback to Apple Help if help book not found NSHelpManager.shared.openHelpAnchor("", inBook: Bundle.main.object(forInfoDictionaryKey: "CFBundleHelpBookName") as? String) } } #endif }