fix: corrected loading of app with doubleclick on chat document

This commit is contained in:
2026-03-21 10:56:17 +01:00
parent 365f4e0531
commit 491a1e6ffe
3 changed files with 80 additions and 21 deletions

View File

@@ -14,6 +14,8 @@ struct ContentView: View {
@State private var exportDocument: ChatExportDocument? @State private var exportDocument: ChatExportDocument?
@State private var documentErrorMessage: String? @State private var documentErrorMessage: String?
@State private var exportErrorMessage: String? @State private var exportErrorMessage: String?
@State private var startupTask: Task<Void, Never>?
@State private var isOpeningDocument = false
private var isRunningTests: Bool { private var isRunningTests: Bool {
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
@@ -37,14 +39,9 @@ struct ContentView: View {
if Preferences.apiAutoStart && !isRunningTests { if Preferences.apiAutoStart && !isRunningTests {
vm.startAPIServer() vm.startAPIServer()
} }
// Restore autosaved session if no document is being opened
if !documentController.hasPendingOpenRequests && !isRunningTests {
Task {
await vm.restoreFromAutosave()
}
}
} }
scheduleStartupWork()
processPendingOpenRequests() processPendingOpenRequests()
} }
.onChange(of: modelManager.currentModel) { .onChange(of: modelManager.currentModel) {
@@ -62,6 +59,7 @@ struct ContentView: View {
showLoadError = modelManager.errorMessage != nil showLoadError = modelManager.errorMessage != nil
} }
.onChange(of: documentController.openRequestNonce) { .onChange(of: documentController.openRequestNonce) {
startupTask?.cancel()
processPendingOpenRequests() processPendingOpenRequests()
} }
} }
@@ -376,11 +374,58 @@ struct ContentView: View {
Task { Task {
while let url = documentController.consumeNextOpenRequest() { while let url = documentController.consumeNextOpenRequest() {
startupTask?.cancel()
await openDocument(at: url) await openDocument(at: url)
} }
} }
} }
private func scheduleStartupWork() {
guard let chatVM else { return }
startupTask?.cancel()
startupTask = Task {
try? await Task.sleep(nanoseconds: 250_000_000)
guard !Task.isCancelled else { return }
if documentController.hasPendingOpenRequests {
await MainActor.run {
processPendingOpenRequests()
}
return
}
guard !isOpeningDocument else { return }
if !isRunningTests, ChatViewModel.hasAutosavedSession {
let restored = await chatVM.restoreFromAutosave()
guard !Task.isCancelled else { return }
guard !isOpeningDocument else { return }
if restored || documentController.hasPendingOpenRequests {
await MainActor.run {
processPendingOpenRequests()
}
return
}
}
guard !Task.isCancelled else { return }
guard !isOpeningDocument else { return }
guard !documentController.hasPendingOpenRequests else {
await MainActor.run {
processPendingOpenRequests()
}
return
}
guard modelManager.currentModel == nil else { return }
let modelId = Preferences.defaultModelId ?? Preferences.lastModelId ?? ModelConfig.default.id
if let config = ModelConfig.availableModels.first(where: { $0.id == modelId }) {
await modelManager.loadModel(config)
}
}
}
private func openDocument(at url: URL, skipUnsavedCheck: Bool = false) async { private func openDocument(at url: URL, skipUnsavedCheck: Bool = false) async {
if !skipUnsavedCheck { if !skipUnsavedCheck {
let shouldContinue = confirmDiscardUnsavedChanges( let shouldContinue = confirmDiscardUnsavedChanges(
@@ -390,6 +435,10 @@ struct ContentView: View {
guard shouldContinue else { return } guard shouldContinue else { return }
} }
startupTask?.cancel()
isOpeningDocument = true
defer { isOpeningDocument = false }
do { do {
try await chatVM?.loadDocument(from: url) try await chatVM?.loadDocument(from: url)
} catch { } catch {

View File

@@ -46,15 +46,6 @@ struct MLXServerApp: App {
.environment(documentController) .environment(documentController)
.environment(modelManager) .environment(modelManager)
.environment(sceneStore) .environment(sceneStore)
.task {
guard !documentController.hasPendingOpenRequests else { return }
guard !ChatViewModel.hasAutosavedSession else { return }
// Auto-load: configured default last used built-in default
let modelId = Preferences.defaultModelId ?? Preferences.lastModelId ?? ModelConfig.default.id
if let config = ModelConfig.availableModels.first(where: { $0.id == modelId }) {
await modelManager.loadModel(config)
}
}
} }
.windowStyle(.titleBar) .windowStyle(.titleBar)
.defaultSize(width: 800, height: 700) .defaultSize(width: 800, height: 700)

View File

@@ -34,6 +34,22 @@ final class ModelManager {
private var idleTimer: Timer? private var idleTimer: Timer?
private(set) var lastUsed: Date? private(set) var lastUsed: Date?
private var latestLoadRequestID = UUID()
private func clearLoadedState() {
idleTimer?.invalidate()
idleTimer = nil
lastUsed = nil
modelContainer = nil
currentModel = nil
isLoading = false
isDownloading = false
downloadProgress = 0
loadingModelName = ""
downloadFilesTotal = 0
downloadFilesCompleted = 0
downloadSpeed = 0
}
/// Load a model, unloading the current one first. /// Load a model, unloading the current one first.
/// Prefers the local snapshot from ~/.cache/huggingface/hub/ (shared with the Python server). /// Prefers the local snapshot from ~/.cache/huggingface/hub/ (shared with the Python server).
@@ -43,7 +59,10 @@ final class ModelManager {
return // already loaded return // already loaded
} }
unloadModel() let requestID = UUID()
latestLoadRequestID = requestID
clearLoadedState()
MLX.GPU.clearCache()
isLoading = true isLoading = true
downloadProgress = 0 downloadProgress = 0
loadingModelName = config.displayName loadingModelName = config.displayName
@@ -94,15 +113,18 @@ final class ModelManager {
) )
} }
guard latestLoadRequestID == requestID else { return }
self.isDownloading = false self.isDownloading = false
self.modelContainer = container self.modelContainer = container
self.currentModel = config self.currentModel = config
touchActivity() touchActivity()
} catch { } catch {
guard latestLoadRequestID == requestID else { return }
self.isDownloading = false self.isDownloading = false
self.errorMessage = "Failed to load model: \(error.localizedDescription)" self.errorMessage = "Failed to load model: \(error.localizedDescription)"
} }
guard latestLoadRequestID == requestID else { return }
isLoading = false isLoading = false
} }
@@ -115,11 +137,8 @@ final class ModelManager {
/// Unload the current model and free GPU memory. /// Unload the current model and free GPU memory.
func unloadModel() { func unloadModel() {
idleTimer?.invalidate() latestLoadRequestID = UUID()
idleTimer = nil clearLoadedState()
lastUsed = nil
modelContainer = nil
currentModel = nil
MLX.GPU.clearCache() MLX.GPU.clearCache()
} }