// // ContentView.swift // Confab // // Root navigation container — NavigationSplitView with collapsible sidebar // // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Copyright (C) 2026 Rune Olsen // // This file is part of Confab. // // Confab 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 Confab 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 // for // the full license text. For commercial licensing, contact Rune // Olsen via . import SwiftUI #if os(macOS) import Darwin // uname, sysctlbyname #endif struct ContentView: View { @Environment(ChatViewModel.self) var chatViewModel private var updateService = UpdateCheckService.shared @State private var columnVisibility: NavigationSplitViewVisibility = SettingsService.shared.sidebarVisible ? .all : .detailOnly @State private var showIntelWarning = false var body: some View { @Bindable var vm = chatViewModel NavigationSplitView(columnVisibility: $columnVisibility) { SidebarView() .navigationSplitViewColumnWidth(min: 200, ideal: 240, max: 340) } detail: { ChatView( onModelSelect: { chatViewModel.showModelSelector = true }, onProviderChange: { newProvider in chatViewModel.changeProvider(newProvider) } ) } .frame(minWidth: 860, minHeight: 560) .onChange(of: columnVisibility) { _, newValue in SettingsService.shared.sidebarVisible = newValue != .detailOnly } #if os(macOS) .onAppear { NSApplication.shared.windows.forEach { $0.tabbingMode = .disallowed } checkIntelWarning() // Wire the real, environment-injected chatViewModel into the app delegate for Quit // interception — `oAIApp.init()` used to do this by reading its own `@State`, but // that returned a throwaway instance distinct from the one actually rendered here // (confirmed via ObjectIdentifier logging). Deferred one run-loop tick via // `DispatchQueue.main.async` so the modal alert reliably presents — calling it // directly from `.onAppear` was tried before and silently failed to ever show it. // Uses `AppDelegate.shared`, NOT `NSApplication.shared.delegate as? AppDelegate` — // the latter always fails since `@NSApplicationDelegateAdaptor` registers an internal // `SwiftUI.AppDelegate` wrapper as the real `NSApp.delegate`, a same-named-but-different // type (confirmed via logging). AppDelegate.shared?.chatViewModel = chatViewModel DispatchQueue.main.async { chatViewModel.checkForCrashRecoveryDraft() } } .onKeyPress(.return, phases: .down) { press in if press.modifiers.contains(.command) { chatViewModel.sendMessage() return .handled } return .ignored } #endif .sheet(isPresented: $vm.showModelSelector) { ModelSelectorView( models: chatViewModel.availableModels, selectedModel: chatViewModel.selectedModel, onSelect: { model in chatViewModel.selectModel(model) chatViewModel.showModelSelector = false } ) .task { if chatViewModel.availableModels.count <= 10 { await chatViewModel.loadAvailableModels() } } } .sheet(isPresented: $vm.showSettings, onDismiss: { chatViewModel.syncFromSettings() }) { SettingsView(chatViewModel: chatViewModel) } .sheet(isPresented: $vm.showStats) { StatsView( stats: chatViewModel.sessionStats, model: chatViewModel.selectedModel, provider: chatViewModel.currentProvider ) } .sheet(isPresented: $vm.showHelp) { HelpView() } .sheet(isPresented: $vm.showCredits) { CreditsView(provider: chatViewModel.currentProvider) } .sheet(isPresented: $vm.showConversations) { ConversationListView( onLoad: { conversation in chatViewModel.loadConversation(conversation) }, onRename: { id, newName in chatViewModel.didRenameConversation(id: id, newName: newName) } ) } .sheet(item: $vm.modelInfoTarget) { model in ModelInfoView(model: model) } .sheet(isPresented: $vm.showHistory) { HistoryView(onSelect: { input in chatViewModel.inputText = input }) } .sheet(item: $vm.releaseNotesRequest) { request in ReleaseNotesView(request: request) } .alert("Intel Mac Support Ending", isPresented: $showIntelWarning) { Button("Got It") { UserDefaults.standard.set(true, forKey: "hasShownIntelWarning") } } message: { Text("Confab (formerly oAI) v2.4 was the last version to support Intel Macs and Rosetta. Starting with macOS 28, Confab will require Apple Silicon. Consider upgrading your Mac to continue receiving updates.") } .alert("Software Update", isPresented: Binding( get: { updateService.manualCheckMessage != nil }, set: { if !$0 { updateService.manualCheckMessage = nil } } )) { if updateService.updateAvailable { if let url = updateService.downloadURL { Button("Download v\(updateService.latestVersion ?? "")") { NSWorkspace.shared.open(url) } } if let latest = updateService.latestVersion { Button("Read Release Notes") { vm.releaseNotesRequest = ReleaseNotesRequest(versionTag: "v\(latest)", isCurrentlyInstalled: false) } } Button("Later", role: .cancel) { } .keyboardShortcut(.cancelAction) } else { Button("OK", role: .cancel) { } .keyboardShortcut(.cancelAction) } } message: { Text(updateService.manualCheckMessage ?? "") } } #if os(macOS) private func checkIntelWarning() { guard !UserDefaults.standard.bool(forKey: "hasShownIntelWarning") else { return } guard isIntelNative || isRosetta else { return } showIntelWarning = true } private var isIntelNative: Bool { var systemInfo = utsname() uname(&systemInfo) let machine = withUnsafeBytes(of: &systemInfo.machine) { String(cString: $0.bindMemory(to: CChar.self).baseAddress!) } return machine.contains("x86_64") } private var isRosetta: Bool { var ret: Int32 = 0 var size = MemoryLayout.size sysctlbyname("sysctl.proc_translated", &ret, &size, nil, 0) return ret == 1 } #endif } #Preview { ContentView() .environment(ChatViewModel()) }