feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish

Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
This commit is contained in:
Rocky
2026-08-13 01:00:51 +08:00
parent fd6e0d3e7e
commit 9f308fadd2
202 changed files with 10897 additions and 5962 deletions
@@ -0,0 +1,187 @@
// ProviderToolRequestCoordinatorTests.swift
// OSGKeyboard · Shared iOS/macOS tests
import Foundation
import XCTest
#if os(macOS)
@testable import OSGKeyboard
#else
@testable import OSGKeyboardShared
#endif
private actor SuspendedValue<Value: Sendable> {
private var continuation: CheckedContinuation<Value, Never>?
private var pendingValue: Value?
func value() async -> Value {
if let pendingValue {
self.pendingValue = nil
return pendingValue
}
return await withCheckedContinuation { continuation in
self.continuation = continuation
}
}
func resume(returning value: Value) {
if let continuation {
self.continuation = nil
continuation.resume(returning: value)
} else {
pendingValue = value
}
}
}
@MainActor
final class ProviderToolRequestCoordinatorTests: XCTestCase {
func testSlowACompletingAfterFastBDoesNotOverwriteB() async {
let slowA = SuspendedValue<String>()
let fastB = SuspendedValue<String>()
let coordinator = ProviderToolRequestCoordinator()
var committed: [String] = []
coordinator.start(providerIdentity: "provider") {
await slowA.value()
} commit: {
committed.append($0)
}
coordinator.start(providerIdentity: "provider") {
await fastB.value()
} commit: {
committed.append($0)
}
await fastB.resume(returning: "B")
await waitUntil { committed == ["B"] }
await slowA.resume(returning: "A")
await drainTasks()
XCTAssertEqual(committed, ["B"])
XCTAssertEqual(coordinator.generation, 2)
}
func testInvalidatedTaskThatIgnoresCancellationCannotCommit() async {
let ignoredCancellation = SuspendedValue<String>()
let coordinator = ProviderToolRequestCoordinator()
var committed: String?
coordinator.start(providerIdentity: "provider-a") {
await ignoredCancellation.value()
} commit: {
committed = $0
}
coordinator.invalidate()
await ignoredCancellation.resume(returning: "stale")
await drainTasks()
XCTAssertNil(committed)
XCTAssertFalse(coordinator.isRunning)
XCTAssertEqual(coordinator.generation, 2)
}
func testCancellationErrorsReturnCancelledInsteadOfFailure() async {
let cancellation = await ProviderToolRunner.runValidate(
runningMessage: "running",
successMessage: "success"
) {
throw CancellationError()
}
let urlCancellation = await ProviderToolRunner.runValidate(
runningMessage: "running",
successMessage: "success"
) {
throw URLError(.cancelled)
}
let llmCancellation = await ProviderToolRunner.runValidate(
runningMessage: "running",
successMessage: "success"
) {
throw LLMError.cancelled
}
XCTAssertEqual(cancellation, .cancelled)
XCTAssertEqual(urlCancellation, .cancelled)
XCTAssertEqual(llmCancellation, .cancelled)
}
func testProviderIdentitySwitchRejectsOldCompletion() async {
let providerA = SuspendedValue<String>()
let providerB = SuspendedValue<String>()
let coordinator = ProviderToolRequestCoordinator()
var committed: String?
coordinator.start(providerIdentity: "provider-a") {
await providerA.value()
} commit: {
committed = $0
}
coordinator.start(providerIdentity: "provider-b") {
await providerB.value()
} commit: {
committed = $0
}
await providerA.resume(returning: "A")
await drainTasks()
XCTAssertNil(committed)
await providerB.resume(returning: "B")
await waitUntil { committed == "B" }
XCTAssertEqual(committed, "B")
}
func testFetchCompletionCannotOverwriteNewerModel() async {
let fetchedModels = SuspendedValue<[String]>()
let coordinator = ProviderToolRequestCoordinator()
var model = ""
var models = ["existing"]
let requestModel = model
coordinator.start(providerIdentity: "provider") {
await ProviderToolRunner.runFetchModels(
runningMessage: "running",
loadedMessage: { "loaded \($0)" },
emptyMessage: "empty",
currentModel: requestModel
) {
await fetchedModels.value()
}
} commit: { outcome in
guard case .completed(let state, let selectedModel) = outcome else { return }
models = state.models
if let selectedModel {
model = selectedModel
}
}
model = "new-user-model"
coordinator.invalidate()
await fetchedModels.resume(returning: ["old-fetched-model"])
await drainTasks()
XCTAssertEqual(model, "new-user-model")
XCTAssertEqual(models, ["existing"])
}
private func waitUntil(
_ predicate: @MainActor () -> Bool,
file: StaticString = #filePath,
line: UInt = #line
) async {
for _ in 0..<100 {
if predicate() {
return
}
await Task.yield()
}
XCTFail("Timed out waiting for asynchronous commit", file: file, line: line)
}
private func drainTasks() async {
for _ in 0..<10 {
await Task.yield()
}
}
}
+22 -9
View File
@@ -16,11 +16,12 @@
"OSGKeyboardTests/AppGroupConfigurationTests",
"OSGKeyboardTests/AppGroupOnboardingStoreTests",
"OSGKeyboardTests/ConfigurationStoreTests",
"OSGKeyboardTests/KeychainTests"
"OSGKeyboardTests/KeychainTests",
"OSGKeyboardTests/ProviderToolRequestCoordinatorTests"
]
},
"sync": {
"description": "iCloud KVS merge/sync for settings, dictionary, speech history, usage stats",
"description": "iCloud KVS merge/sync for settings, local-only clipboard consent, dictionary, speech history, usage stats",
"platform": "ios",
"tests": [
"OSGKeyboardTests/SettingsCloudSyncTests",
@@ -28,7 +29,8 @@
"OSGKeyboardTests/PersonalDictionaryMergeTests",
"OSGKeyboardTests/SpeechHistoryCloudSyncTests",
"OSGKeyboardTests/SpeechHistoryDayDeletionTests",
"OSGKeyboardTests/UsageStatisticsCloudSyncTests"
"OSGKeyboardTests/UsageStatisticsCloudSyncTests",
"OSGKeyboardTests/AIHistoryAndUsageTests"
]
},
"polish": {
@@ -40,7 +42,10 @@
"OSGKeyboardTests/PolishOutputValidatorTests",
"OSGKeyboardTests/PolishStylePackTests",
"OSGKeyboardTests/LLMClientTests",
"OSGKeyboardTests/TranscriptLanguageDetectorTests"
"OSGKeyboardTests/TranscriptLanguageDetectorTests",
"OSGKeyboardTests/AIModeLLMClientTests",
"OSGKeyboardTests/AIQuestionServiceTests",
"OSGKeyboardTests/AIClipboardPromptTests"
]
},
"edit_last_input": {
@@ -52,7 +57,8 @@
"OSGKeyboardTests/FlowStartTransactionPolicyTests",
"OSGKeyboardTests/SpeechHistoryRevisionTests",
"OSGKeyboardTests/EditTransactionStoreTests",
"OSGKeyboardTests/RecordButtonGesturePolicyTests"
"OSGKeyboardTests/RecordButtonGesturePolicyTests",
"OSGKeyboardExtTests/EditHintSchedulerTests"
]
},
"cloud_asr": {
@@ -88,6 +94,7 @@
"OSGKeyboardTests/UtteranceTranscriptGuardTests",
"OSGKeyboardTests/UtteranceBatchFallbackPolicyTests",
"OSGKeyboardTests/ProgressiveDictationTranscriptAccumulatorTests",
"OSGKeyboardTests/FlowASRPostProcessorTests",
"OSGKeyboardExtTests/FinalChunkRecoveryTests"
]
},
@@ -104,15 +111,17 @@
"OSGKeyboardTests/FlowUtterancePCMStoreTests",
"OSGKeyboardTests/MicVoiceAvailabilityTests",
"OSGKeyboardTests/FlowKeyboardPoliciesTests",
"OSGKeyboardTests/FlowReliabilityTests"
"OSGKeyboardTests/FlowReliabilityTests",
"OSGKeyboardTests/AISessionStateTests"
]
},
"keyboard": {
"description": "Typing surface, English lexicon, Rime schema/integration, candidate panel, cursor navigation, translation chip grace",
"description": "Typing surface, clipboard privacy/storage policy, secure-field UI, English lexicon, Rime, cursor navigation",
"platform": "ios",
"tests": [
"OSGKeyboardExtTests/EnglishTypingTests",
"OSGKeyboardExtTests/CandidatePanelExpandTests",
"OSGKeyboardExtTests/ClipboardSuggestionLifecycleTests",
"OSGKeyboardExtTests/KeyboardStateTests",
"OSGKeyboardExtTests/KeyboardSurfaceStateTests",
"OSGKeyboardExtTests/KeyHitTestingTests",
@@ -121,7 +130,10 @@
"OSGKeyboardExtTests/RimeSchemaGeneratorTests",
"OSGKeyboardExtTests/RimePersonalDictionaryExporterTests",
"OSGKeyboardTests/CursorNavigationTests",
"OSGKeyboardTests/KeyboardTranslationConfigProtectionTests"
"OSGKeyboardTests/KeyboardTranslationConfigProtectionTests",
"OSGKeyboardTests/AIHintPoolTests",
"OSGKeyboardTests/ClipboardHistoryPolicyTests",
"OSGKeyboardTests/ClipboardHistoryStoreTests"
]
},
"host_misc": {
@@ -146,7 +158,8 @@
"tests": [
"OSGKeyboardMacTests/MacAudioRecorderSnapshotStreamTests",
"OSGKeyboardMacTests/MacDictationViewModelTests",
"OSGKeyboardMacTests/MacTextInsertionServiceTests"
"OSGKeyboardMacTests/MacTextInsertionServiceTests",
"OSGKeyboardMacTests/ProviderToolRequestCoordinatorTests"
]
},
"live_api": {