Add unsaved-changes save prompt and crash-recovery draft
Replaces heuristic auto-save (goodbye-phrase detection, idle timeout, min-message count, on-model-switch) with a standard macOS unsaved-changes gate (Save/Don't Save/Cancel) on New Chat, Clear Chat, Load Conversation, and Quit. The Save dialog gained a folder picker with inline "New Folder…" creation. Separately, the in-progress conversation is periodically mirrored to disk (DraftRecoveryService, configurable interval in Settings, default 10s) and offered back on next launch if oAI crashes or is force-quit, including the model that was selected. Two real bugs found via ObjectIdentifier/log-based diagnosis before this worked correctly: - oAIApp.init() wired AppDelegate.chatViewModel from its own @State read, which returned a throwaway ChatViewModel instance distinct from the one ContentView actually renders. Wiring moved to ContentView.onAppear. - NSApplication.shared.delegate as? AppDelegate always failed silently: @NSApplicationDelegateAdaptor registers an internal SwiftUI.AppDelegate wrapper as the real NSApp.delegate (same name, different type in a different module), which forwards protocol methods but isn't castable to our type. AppDelegate now tracks itself via a static `shared`. Also guards checkForCrashRecoveryDraft() against running under XCTestConfigurationFilePath — oAITests is app-hosted, so xcodebuild test launches this same app, and a leftover draft file on disk would otherwise hang the entire test run on a blocking NSAlert with no one to click it.
This commit is contained in:
@@ -54,6 +54,21 @@ struct ContentView: View {
|
||||
.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) {
|
||||
@@ -68,12 +83,8 @@ struct ContentView: View {
|
||||
models: chatViewModel.availableModels,
|
||||
selectedModel: chatViewModel.selectedModel,
|
||||
onSelect: { model in
|
||||
let oldModel = chatViewModel.selectedModel
|
||||
chatViewModel.selectModel(model)
|
||||
chatViewModel.showModelSelector = false
|
||||
Task {
|
||||
await chatViewModel.onModelSwitch(from: oldModel, to: model)
|
||||
}
|
||||
}
|
||||
)
|
||||
.task {
|
||||
|
||||
Reference in New Issue
Block a user