From 491a1e6ffec45589299e5e3be092ff7fafd06de0 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 21 Mar 2026 10:56:17 +0100 Subject: [PATCH] fix: corrected loading of app with doubleclick on chat document --- MLXServer/ContentView.swift | 61 ++++++++++++++++++++++--- MLXServer/MLXServerApp.swift | 9 ---- MLXServer/ViewModels/ModelManager.swift | 31 ++++++++++--- 3 files changed, 80 insertions(+), 21 deletions(-) diff --git a/MLXServer/ContentView.swift b/MLXServer/ContentView.swift index bf05743..c929c1d 100644 --- a/MLXServer/ContentView.swift +++ b/MLXServer/ContentView.swift @@ -14,6 +14,8 @@ struct ContentView: View { @State private var exportDocument: ChatExportDocument? @State private var documentErrorMessage: String? @State private var exportErrorMessage: String? + @State private var startupTask: Task? + @State private var isOpeningDocument = false private var isRunningTests: Bool { ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil @@ -37,14 +39,9 @@ struct ContentView: View { if Preferences.apiAutoStart && !isRunningTests { vm.startAPIServer() } - // Restore autosaved session if no document is being opened - if !documentController.hasPendingOpenRequests && !isRunningTests { - Task { - await vm.restoreFromAutosave() - } - } } + scheduleStartupWork() processPendingOpenRequests() } .onChange(of: modelManager.currentModel) { @@ -62,6 +59,7 @@ struct ContentView: View { showLoadError = modelManager.errorMessage != nil } .onChange(of: documentController.openRequestNonce) { + startupTask?.cancel() processPendingOpenRequests() } } @@ -376,11 +374,58 @@ struct ContentView: View { Task { while let url = documentController.consumeNextOpenRequest() { + startupTask?.cancel() 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 { if !skipUnsavedCheck { let shouldContinue = confirmDiscardUnsavedChanges( @@ -390,6 +435,10 @@ struct ContentView: View { guard shouldContinue else { return } } + startupTask?.cancel() + isOpeningDocument = true + defer { isOpeningDocument = false } + do { try await chatVM?.loadDocument(from: url) } catch { diff --git a/MLXServer/MLXServerApp.swift b/MLXServer/MLXServerApp.swift index cb37ff4..f7abf88 100644 --- a/MLXServer/MLXServerApp.swift +++ b/MLXServer/MLXServerApp.swift @@ -46,15 +46,6 @@ struct MLXServerApp: App { .environment(documentController) .environment(modelManager) .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) .defaultSize(width: 800, height: 700) diff --git a/MLXServer/ViewModels/ModelManager.swift b/MLXServer/ViewModels/ModelManager.swift index a6e914c..1950237 100644 --- a/MLXServer/ViewModels/ModelManager.swift +++ b/MLXServer/ViewModels/ModelManager.swift @@ -34,6 +34,22 @@ final class ModelManager { private var idleTimer: Timer? 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. /// Prefers the local snapshot from ~/.cache/huggingface/hub/ (shared with the Python server). @@ -43,7 +59,10 @@ final class ModelManager { return // already loaded } - unloadModel() + let requestID = UUID() + latestLoadRequestID = requestID + clearLoadedState() + MLX.GPU.clearCache() isLoading = true downloadProgress = 0 loadingModelName = config.displayName @@ -94,15 +113,18 @@ final class ModelManager { ) } + guard latestLoadRequestID == requestID else { return } self.isDownloading = false self.modelContainer = container self.currentModel = config touchActivity() } catch { + guard latestLoadRequestID == requestID else { return } self.isDownloading = false self.errorMessage = "Failed to load model: \(error.localizedDescription)" } + guard latestLoadRequestID == requestID else { return } isLoading = false } @@ -115,11 +137,8 @@ final class ModelManager { /// Unload the current model and free GPU memory. func unloadModel() { - idleTimer?.invalidate() - idleTimer = nil - lastUsed = nil - modelContainer = nil - currentModel = nil + latestLoadRequestID = UUID() + clearLoadedState() MLX.GPU.clearCache() }