feat: harden Flow cold-start/force-quit and polish macOS dictation UX

Fix cold-start overlay recursion that overflowed the main-thread stack when
recording began while the ready overlay was still up; also remove temporary
on-screen Flow DEBUG panels after the orange-mic investigation, and land the
macOS overlay/catalog/layout polish plus related Flow recovery hardening.
This commit is contained in:
Rocky
2026-07-10 12:39:41 +08:00
parent dcb66a9849
commit cdf833935a
104 changed files with 5794 additions and 853 deletions
@@ -9,7 +9,16 @@ import Foundation
extension PersonalDictionary {
public static let kvsKeyV2 = "personalDictionary.v2"
public static let legacyKVSKey = "personalDictionary.v1"
public static let tombstoneRetention: TimeInterval = 90 * 24 * 60 * 60
/// Tombstones guard against deleted entries "resurrecting" when a
/// long-offline device rejoins and re-merges them. A short wall-clock
/// retention re-opened that window after only 90 days; a year keeps the
/// window closed for any realistically dormant device while staying tiny
/// on the wire (a tombstone is ~60 bytes of JSON), and the count cap
/// bounds the worst case regardless of clock.
public static let tombstoneRetention: TimeInterval = 365 * 24 * 60 * 60
/// Hard cap independent of wall clock the oldest tombstones are
/// dropped first once exceeded.
public static let maxTombstones = 500
/// Merges two dictionary snapshots for cross-device sync.
///
@@ -100,11 +109,19 @@ extension PersonalDictionary {
clearedAt: Date?
) -> [UUID: Date] {
let cutoff = Date().addingTimeInterval(-tombstoneRetention)
return tombstones.filter { _, deletedAt in
var kept = tombstones.filter { _, deletedAt in
if deletedAt < cutoff { return false }
if let clearedAt, deletedAt <= clearedAt { return false }
return true
}
// Enforce the count cap that makes the 365-day retention safe on the
// KVS byte budget: keep the NEWEST tombstones (dropping an old one
// early only re-opens the resurrection window for that one entry).
if kept.count > maxTombstones {
let newest = kept.sorted { $0.value > $1.value }.prefix(maxTombstones)
kept = Dictionary(uniqueKeysWithValues: newest.map { ($0.key, $0.value) })
}
return kept
}
private static func later(of lhs: Date?, and rhs: Date?) -> Date? {