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
@@ -61,6 +61,7 @@ final class SettingsCloudSyncTests: XCTestCase {
cursorDragNavigationEnabled: SyncedField(value: true, updatedAt: stampA, deviceID: deviceA),
keyboardHapticIntensity: SyncedField(value: .light, updatedAt: stampA, deviceID: deviceA),
polishIntensity: SyncedField(value: .light, updatedAt: stampA, deviceID: deviceA),
aiResponseLength: SyncedField(value: .medium, updatedAt: stampA, deviceID: deviceA),
activePolishStyleId: SyncedField(value: "builtin.light", updatedAt: stampA, deviceID: deviceA),
llmThinkingEnabled: SyncedField(value: false, updatedAt: stampA, deviceID: deviceA),
flowSkipAppSwitch: SyncedField(value: true, updatedAt: stampA, deviceID: deviceA),
@@ -83,6 +84,7 @@ final class SettingsCloudSyncTests: XCTestCase {
cursorDragNavigationEnabled: SyncedField(value: false, updatedAt: stampB, deviceID: deviceB),
keyboardHapticIntensity: SyncedField(value: .strong, updatedAt: stampB, deviceID: deviceB),
polishIntensity: SyncedField(value: .heavy, updatedAt: stampB, deviceID: deviceB),
aiResponseLength: SyncedField(value: .detailed, updatedAt: stampB, deviceID: deviceB),
activePolishStyleId: SyncedField(value: "builtin.formal", updatedAt: stampB, deviceID: deviceB),
llmThinkingEnabled: SyncedField(value: true, updatedAt: stampB, deviceID: deviceB),
flowSkipAppSwitch: SyncedField(value: false, updatedAt: stampB, deviceID: deviceB),
@@ -96,6 +98,7 @@ final class SettingsCloudSyncTests: XCTestCase {
XCTAssertEqual(merged.localeId.value, "ja")
XCTAssertEqual(merged.engineMode.value, "local")
XCTAssertEqual(merged.polishIntensity.value, .heavy)
XCTAssertEqual(merged.aiResponseLength.value, .detailed)
}
func testLegacyKeepAliveFieldDecodesButIsNotReencoded() throws {