feat: cursor navigation, key sounds, dictionary tooling, key security

Batch of in-progress app work from the working tree.

- feat(keyboard): CursorNavigation + CursorDragPad for caret movement;
  KeyboardSoundFeedback for system key click sounds
- feat(dictionary): DictionaryAliasGenerator + PersonalDictionaryEntrySheet;
  TranscriptPostProcessor quality gate; retire DictionaryLearner
- feat(ui): TabBarVisibility handling; drop PageHeaderRow /
  PageHeaderConfirmButton; refresh views and localizable strings
- fix(security): move the hardcoded DeepSeek key out of
  PreconfiguredKeys.swift into a gitignored PreconfiguredKeys.local.swift
  (seeded from .example by generate-xcodeproj.sh)
- docs(agents): add Conventional Commits versioning + bilingual changelog rules
- chore(gitignore): ignore PreconfiguredKeys.local.swift, .cache/, pycache

Custom language model / lexicon work stays on
feature/custom-language-model-asr. Changelog bullets added under
[Unreleased]; no version bump.
This commit is contained in:
Rocky
2026-07-05 18:27:23 +08:00
parent 074e24d87f
commit 05e005e9ce
60 changed files with 3247 additions and 1388 deletions
+11
View File
@@ -12,10 +12,14 @@ final class KeychainTests: XCTestCase {
override func setUpWithError() throws {
try? Keychain.deleteAPIKey()
try? Keychain.deleteLegacyAPIKey()
try? Keychain.deleteAPIKey(for: "qwen")
}
override func tearDownWithError() throws {
try? Keychain.deleteAPIKey()
try? Keychain.deleteLegacyAPIKey()
try? Keychain.deleteAPIKey(for: "qwen")
}
// MARK: - Round-trip
@@ -49,6 +53,13 @@ final class KeychainTests: XCTestCase {
XCTAssertNil(Keychain.apiKey(), "Empty write must delete, not store empty string")
}
func testProviderScopedKeysDoNotMix() throws {
try Keychain.setAPIKey("sk-openai", for: "openai")
try Keychain.setAPIKey("sk-qwen", for: "qwen")
XCTAssertEqual(Keychain.apiKey(for: "openai"), "sk-openai")
XCTAssertEqual(Keychain.apiKey(for: "qwen"), "sk-qwen")
}
/// Deleting a non-existent entry must be a no-op (idempotent), not an
/// error callers like `ProviderConfig.reset()` invoke it
/// unconditionally.