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
@@ -34,13 +34,19 @@ public final class SpeechHistoryStore: ObservableObject {
public func append(
id: UUID = UUID(),
text: String,
engineMode: String? = nil
engineMode: String? = nil,
source: SpeechHistoryEntry.Source = .dictation
) -> SpeechHistoryEntry? {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
rebaseOnPersistedStateBeforeMutation()
let entry = SpeechHistoryEntry(id: id, text: trimmed, engineMode: engineMode)
let entry = SpeechHistoryEntry(
id: id,
text: trimmed,
engineMode: engineMode,
source: source
)
payload.entries.insert(entry, at: 0)
payload.trimEntries()
payload.updatedAt = Date()
@@ -68,7 +74,8 @@ public final class SpeechHistoryStore: ObservableObject {
let entry = SpeechHistoryEntry(
id: mutation.entryID,
text: text,
engineMode: mutation.engineMode
engineMode: mutation.engineMode,
source: mutation.source ?? .dictation
)
payload.entries.insert(entry, at: 0)
finishMutation(mutationID: mutation.id)
@@ -82,7 +89,11 @@ public final class SpeechHistoryStore: ObservableObject {
guard let index = payload.entries.firstIndex(where: { $0.id == mutation.entryID })
else {
// The original row may have been deleted or trimmed remotely.
let fallback = SpeechHistoryEntry(text: text, engineMode: mutation.engineMode)
let fallback = SpeechHistoryEntry(
text: text,
engineMode: mutation.engineMode,
source: mutation.source ?? .dictation
)
payload.entries.insert(fallback, at: 0)
finishMutation(mutationID: mutation.id)
return fallback
@@ -91,7 +102,11 @@ public final class SpeechHistoryStore: ObservableObject {
if let expected = mutation.expectedRevision, existing.revision != expected {
// Never overwrite a newer cloud edit. Preserve this local result
// as a new row instead.
let conflictCopy = SpeechHistoryEntry(text: text, engineMode: mutation.engineMode)
let conflictCopy = SpeechHistoryEntry(
text: text,
engineMode: mutation.engineMode,
source: mutation.source ?? existing.source
)
payload.entries.insert(conflictCopy, at: 0)
finishMutation(mutationID: mutation.id)
return conflictCopy
@@ -102,7 +117,8 @@ public final class SpeechHistoryStore: ObservableObject {
createdAt: existing.createdAt,
modifiedAt: Date(),
revision: existing.revision + 1,
engineMode: mutation.engineMode ?? existing.engineMode
engineMode: mutation.engineMode ?? existing.engineMode,
source: mutation.source ?? existing.source
)
payload.entries[index] = updated
finishMutation(mutationID: mutation.id)