Files
oai-swift/oAI/oAIApp.swift
T
rune 86027001c7 Roll back native Help Viewer integration; fix duplicate View menu
Per Rune: the NSHelpManager-based Cmd+? fix from the last few commits
technically worked (registration was correct) but opened Apple's
broken generic Tips landing page instead of oAI's own content on the
macOS 27 beta this is built against — worse than the original
browser-tab behavior. Revisit at macOS 27 RC1 (see CLAUDE.md).

- openHelp() reverts to NSWorkspace.open() on index.html directly.
- Removed the "In-App Help" Ctrl+Cmd+H menu item entirely — HelpView's
  panel (search already fully working) is reachable only via /help
  from the input field now, by design.
- Renamed the custom CommandMenu("View") to CommandMenu("Chat") — it
  was colliding with the "View" menu macOS auto-adds for
  NavigationSplitView (Enter Full Screen, etc.), producing two
  identically-titled top-level menus in the menu bar.
2026-07-20 08:17:33 +02:00

174 lines
7.4 KiB
Swift

//
// oAIApp.swift
// oAI
//
// Main app entry point
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
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()
// Start external MCP servers
Task { @MainActor in ExternalMCPManager.shared.startAll() }
// 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
Task { @MainActor in ExternalMCPManager.shared.stopAll() }
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)
Button("Clear Chat") { chatViewModel.clearChat() }
.keyboardShortcut("k", modifiers: .command)
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
}
CommandGroup(after: .newItem) {
Button("Open Chat…") { chatViewModel.showConversations = true }
.keyboardShortcut("o", modifiers: .command)
Button("Search Conversations") { chatViewModel.showConversations = true }
.keyboardShortcut("l", modifiers: .command)
}
CommandGroup(replacing: .saveItem) {
Button("Save Chat") { chatViewModel.saveFromMenu() }
.keyboardShortcut("s", modifiers: .command)
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
Button("Save Chat As…") { chatViewModel.saveAsFromMenu() }
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
Button("Stats") { chatViewModel.showStats = true }
.keyboardShortcut("s", modifiers: [.command, .shift])
}
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)
}
// ── Chat menu ─────────────────────────────────────────────────
// Named "Chat" (not "View") to avoid colliding with the "View" menu
// macOS auto-adds for NavigationSplitView (Enter Full Screen, etc.) —
// two menus both titled "View" would otherwise appear in the menu bar.
CommandMenu("Chat") {
Button("Select Model") { chatViewModel.showModelSelector = true }
.keyboardShortcut("m", modifiers: .command)
Button("Model Info") {
chatViewModel.modelInfoTarget = chatViewModel.selectedModel
}
.keyboardShortcut("i", modifiers: .command)
.disabled(chatViewModel.selectedModel == nil)
Divider()
Button("Command History") { chatViewModel.showHistory = true }
.keyboardShortcut("h", modifiers: [.command, .shift])
Button("Credits") { chatViewModel.showCredits = true }
Divider()
Button(chatViewModel.onlineMode ? "Online Mode: On" : "Online Mode: Off") {
chatViewModel.onlineMode.toggle()
}
.keyboardShortcut("o", modifiers: [.command, .shift])
}
// ── Help menu ─────────────────────────────────────────────────
CommandGroup(replacing: .help) {
Button("oAI Help") { openHelp() }
.keyboardShortcut("?", modifiers: .command)
Divider()
Button(UpdateCheckService.shared.isCheckingManually ? "Checking…" : "Check for Updates…") {
UpdateCheckService.shared.checkForUpdatesManually()
}
.disabled(UpdateCheckService.shared.isCheckingManually)
}
}
#endif
}
#if os(macOS)
private func openHelp() {
// Opens the Help Book's index.html directly in the default browser rather than
// through NSHelpManager/Help Viewer — see CLAUDE.md's macOS 27 beta note for why
// (Apple's Tips.app replacement for Help Viewer can't resolve anchors on this beta).
// Revisit once macOS 27 reaches RC.
if let helpBookURL = Bundle.main.url(forResource: "oAI.help", withExtension: nil) {
NSWorkspace.shared.open(helpBookURL.appendingPathComponent("Contents/Resources/en.lproj/index.html"))
}
}
#endif
}