feat: sync force-quit Flow teardown and polish macOS local ASR UX

End Live Activities and release the audio session synchronously on
applicationWillTerminate, and continue macOS local-model install,
onboarding, and settings polish on this branch.
This commit is contained in:
Rocky
2026-07-09 17:13:07 +08:00
parent 200265fbd6
commit dcb66a9849
34 changed files with 1936 additions and 340 deletions
+8
View File
@@ -59,6 +59,14 @@ final class AppURLHandler: NSObject, UIApplicationDelegate {
configuration.delegateClass = AppSceneDelegate.self
return configuration
}
/// running 5
/// + Live Activity
func applicationWillTerminate(_ application: UIApplication) {
MainActor.assumeIsolated {
FlowTerminationCoordinator.performSynchronousTerminationCleanup()
}
}
}
final class AppSceneDelegate: NSObject, UIWindowSceneDelegate {
@@ -116,4 +116,21 @@ enum FlowLiveActivityController {
}
}
}
/// `applicationWillTerminate` `end` 退
nonisolated static func endAllSynchronouslyOnTerminate() {
let semaphore = DispatchSemaphore(value: 0)
Task.detached(priority: .userInitiated) {
let activities = Activity<FlowActivityAttributes>.activities
let count = activities.count
for activity in activities {
await activity.end(activity.content, dismissalPolicy: .immediate)
}
FlowDiagnostics.log("Live Activity ended synchronously on terminate (count=\(count))")
semaphore.signal()
}
semaphore.wait()
currentPhase = .idle
currentActivity = nil
}
}
@@ -88,6 +88,7 @@ final class FlowSessionManager: ObservableObject {
capture.onEngineLiveChanged = { [weak self] _ in
self?.refreshHostReady()
}
FlowTerminationCoordinator.register(self)
}
// MARK: - Public
@@ -210,6 +211,73 @@ final class FlowSessionManager: ObservableObject {
AppPermissions.openSystemSettings()
}
/// teardown ASR/LLMLive Activity
/// `FlowTerminationCoordinator` `end`
func prepareForProcessTermination() {
debug("prepareForProcessTermination")
coldStartContext = nil
isColdStartHandoff = false
coldStartRecoveryTask?.cancel()
coldStartRecoveryTask = nil
startTask?.cancel()
startTask = nil
commandObserver = nil
pollingTask?.cancel()
pollingTask = nil
heartbeatTask?.cancel()
heartbeatTask = nil
expiryTask?.cancel()
expiryTask = nil
levelTask?.cancel()
levelTask = nil
finalizeTask?.cancel()
finalizeTask = nil
utteranceSafetyTask?.cancel()
utteranceSafetyTask = nil
asrTask?.cancel()
asrTask = nil
chunkedPipeline = nil
if isUtteranceRecording || isUtteranceProcessing {
capture.cancelUtterance()
asr.cancel()
}
capture.cancelUtterance()
if capture.running {
capture.stop()
}
endBackgroundKeepAlive()
ScreenWakeLock.release()
sessionASR?.cancel()
sessionASR = nil
sessionASREngineMode = nil
sessionASRWarmedLocaleID = nil
if isActive || FlowSessionBridge.isSessionActive() {
FlowSessionBridge.markSessionInactive()
FlowSessionDarwin.postSessionChanged()
}
activeSessionId = nil
currentUtteranceId = nil
currentCommandSeq = 0
lastHandledCommandSeq = 0
isUtteranceRecording = false
isUtteranceProcessing = false
isActive = false
isStarting = false
sessionExpiresAt = nil
sessionWarning = nil
currentPartial = ""
lastFinal = ""
chunkWarnings = []
FlowSessionBridge.setHostReady(false)
}
func endSession() {
guard isActive else { return }
debug("Flow session ended")
@@ -0,0 +1,24 @@
// FlowTerminationCoordinator.swift
// OSGKeyboard · Main App
//
// `UIApplicationDelegate.applicationWillTerminate` `FlowSessionManager`
// SwiftUI `FlowSessionManager` `@StateObject`AppDelegate
// 退 5 Live Activity
import Foundation
@MainActor
enum FlowTerminationCoordinator {
private static weak var sessionManager: FlowSessionManager?
/// `FlowSessionManager.init()`
static func register(_ manager: FlowSessionManager) {
sessionManager = manager
}
/// / 线`applicationWillTerminate`
static func performSynchronousTerminationCleanup() {
sessionManager?.prepareForProcessTermination()
FlowLiveActivityController.endAllSynchronouslyOnTerminate()
}
}