feat(keyboard): ship AI mode surface with streaming search answers

Add the AI keyboard tab, Agent settings, and user-owned LLM key path for 1.7.0, including streaming answers and web-search transports without the built-in DeepSeek fallback.
This commit is contained in:
Rocky
2026-08-11 01:06:27 +08:00
parent f6212dcd2c
commit 3de665d254
81 changed files with 4467 additions and 398 deletions
+39 -2
View File
@@ -11,21 +11,28 @@ public struct UsageStatistics: Codable, Equatable, Sendable {
public var dictationDurationSeconds: TimeInterval
public var dictationCharacterCount: Int
public var translationCharacterCount: Int
public var aiCharacterCount: Int
public init(
updatedAt: Date = Date(),
dictationDurationSeconds: TimeInterval = 0,
dictationCharacterCount: Int = 0,
translationCharacterCount: Int = 0
translationCharacterCount: Int = 0,
aiCharacterCount: Int = 0
) {
self.updatedAt = updatedAt
self.dictationDurationSeconds = dictationDurationSeconds
self.dictationCharacterCount = dictationCharacterCount
self.translationCharacterCount = translationCharacterCount
self.aiCharacterCount = aiCharacterCount
}
public static let zero = UsageStatistics(updatedAt: .distantPast)
public var totalInputCharacterCount: Int {
dictationCharacterCount + translationCharacterCount + aiCharacterCount
}
/// Combine lifetime totals from two devices. After merge, each device
/// continues accumulating locally so `max` converges to the union.
public static func merge(local: UsageStatistics, remote: UsageStatistics) -> UsageStatistics {
@@ -33,9 +40,39 @@ public struct UsageStatistics: Codable, Equatable, Sendable {
updatedAt: max(local.updatedAt, remote.updatedAt),
dictationDurationSeconds: max(local.dictationDurationSeconds, remote.dictationDurationSeconds),
dictationCharacterCount: max(local.dictationCharacterCount, remote.dictationCharacterCount),
translationCharacterCount: max(local.translationCharacterCount, remote.translationCharacterCount)
translationCharacterCount: max(local.translationCharacterCount, remote.translationCharacterCount),
aiCharacterCount: max(local.aiCharacterCount, remote.aiCharacterCount)
)
}
private enum CodingKeys: String, CodingKey {
case updatedAt
case dictationDurationSeconds
case dictationCharacterCount
case translationCharacterCount
case aiCharacterCount
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
dictationDurationSeconds = try container.decodeIfPresent(
TimeInterval.self,
forKey: .dictationDurationSeconds
) ?? 0
dictationCharacterCount = try container.decodeIfPresent(
Int.self,
forKey: .dictationCharacterCount
) ?? 0
translationCharacterCount = try container.decodeIfPresent(
Int.self,
forKey: .translationCharacterCount
) ?? 0
aiCharacterCount = try container.decodeIfPresent(
Int.self,
forKey: .aiCharacterCount
) ?? 0
}
}
public enum UsageStatisticsStorage {