Files
OSGKeyboard/CHANGELOG.md
T
Rocky a803a27a88 fix: 3 review issues from the keyboard preview / onboarding flow
1) Preview chips weren't actually buttons.
   `modeChip` and `localeChip` in `KeyboardPreviewStub` were
   decorative HStacks — no `Button`, no action, no callback. The
   chevron-down glyph made them *look* like pickers, so a user
   tapping them got nothing. The screenshot the user shared
   ("润色 ▾" / "中文(简体) ▾") shows exactly that surface.

   Fix: wrap each chip in a `Button(action: ...)` with
   `.buttonStyle(.plain)`. The stub now takes `modeId`, `localeId`,
   `onModeCycle`, `onLocaleCycle` and the sheet's `cycleMode` /
   `cycleLocale` advance the config:
     - mode cycles [off → transcribe → polish] (mirrors Settings)
     - locale cycles [auto → zh-Hans → zh-Hant → en-US → ja-JP → ko-KR]
   Mid-recording locale switches call `asr.stop()` because ASR
   sessions are bound to the locale they were started with.
   `modeId == "off"` also stops any in-flight recording so the
   disc isn't recording into a mode that won't insert.

   The mode chip's icon also follows the mode (mic.slash /
   mic / wand) as a redundant visual cue, and both chips get
   accessibility labels (preview.modeChip.cycle /
   preview.localeChip.cycle) so VoiceOver users can use them.

2) Onboarding's "Next" stays enabled when local engine is picked
   but no API key is filled in. Root cause: `ProviderConfig.isConfigured`
   checks `!apiKey.isEmpty && !baseURL.isEmpty && !model.isEmpty` —
   it never asks whether the user *needs* a key. The local engine
   (on-device ASR) doesn't round-trip through the LLM, so an
   empty key on the local path is correct, not a configuration gap.

   Fix: short-circuit `isConfigured` to `true` when
   `engineMode == "local"`. The onboarding "Next" button is
   already disabled on the API page when `!isConfigured`; this
   just makes the gate respect the engine choice. New test
   `testIsConfiguredTrueForLocalEngineWithoutAPIKey` locks the
   behaviour in (local → true, cloud → false, flip back).

3) Add the two new accessibility keys to all four
   `Localizable.strings` files (en + zh-Hans, main app + ext)
   so VoiceOver and the cycle button labels resolve in both
   languages.

Build: BUILD SUCCEEDED.
Tests: 22/22 pass (1 new).

🤖 Generated with Claude Code
2026-06-18 20:17:30 +08:00

7.0 KiB
Raw Blame History

Changelog

All notable changes to OSGKeyboard will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Changed

  • iPhone only: Set TARGETED_DEVICE_FAMILY to "1" for both OSGKeyboard and OSGKeyboardExt targets. Removed UIRequiresFullScreen and trimmed UISupportedInterfaceOrientations to Portrait only (iPad is no longer a supported device).
  • Remove top divider line: Deleted the 0.5 pt palette.divider overlay from KeyboardRootView — the subtle highlight gradient is retained; the hard separator line is gone.
  • Keyboard preview always dark: KeyboardPreviewSheet now injects .environment(\.themePalette, Palette.dark) alongside .environment(\.colorScheme, .dark) on KeyboardPreviewStub, so the preview palette is always the dark variant regardless of the app's active theme.

[0.1.2] - Planned

Fixed

  • Light/Dark mode consistency: cardSurface(), primaryButton(), secondaryButton(), and pillChip() view modifiers in Theme.swift now use ViewModifier structs that read from @Environment(\.themePalette). Previously they used hardcoded dark Palette constants, causing cards and buttons to always render in dark mode even when the main App was in light mode.
  • TestFlight error 90474 (Invalid bundle): Added UIRequiresFullScreen: true and all four UISupportedInterfaceOrientations values to Info.plist via project.yml. The app targets iPhone + iPad (TARGETED_DEVICE_FAMILY: "1,2"), and Apple requires all four orientations for iPad multitasking; setting UIRequiresFullScreen opts out of slide-over/split-view while still satisfying the validator.
  • Keyboard Preview cycling: The "Tap the disc to cycle states" prompt now actually works. KeyboardPreviewStub gained an onTap closure wired to cyclePhase() in KeyboardPreviewSheet, which rotates .idle → .recording → .processing → .idle with animation. Sample transcript text is shown during the .recording phase.

Added

  • Dynamic ASR locale picker: Settings now loads the full list of supported locales from SFSpeechRecognizer.supportedLocales() on appear (off the main thread). Each locale shows an on-device badge (iPhone icon) when the device supports on-device recognition for that language — giving users confidence about which locales avoid sending audio to the cloud. A static fallback list is shown while the async load is in progress.
  • Speech.framework linked to the main App target in project.yml (needed by the new dynamic locale loader in SettingsView).

Changed

  • pillChip(foreground:) signature changed from foreground: Color = Palette.textSecondary to foreground: Color? = nil; callers that pass an explicit color are unaffected.

Note: v0.1.1 polish — this is a small follow-up to v0.1.0 focused on review-driven cleanup (theme follow-up, ASR robustness, debug-print hygiene, docs). No features are removed. The iOS 26 SpeechAnalyzer path remains deferred to 0.2.0 (see below); v0.1.1 users continue to ship with the iOS 18 SFSpeechRecognizer path that shipped in v0.1.0. User experience is unchanged from v0.1.0.

Fixed

  • PrivacyInfo.xcprivacy audited for honesty: removed the three undeclared NSPrivacyAccessedAPIType entries (FileTimestamp / DiskSpace / SystemBootTime) the project doesn't actually use, and added ActiveKeyboards (reason DDA9.1) to the keyboard extension's manifest because advanceToNextInputMode() is in the tap path. The main App now declares only UserDefaults (reason CA92.1), which is the only Required Reason API it touches.
  • Theme follows system appearance: main App now renders a true light palette in light mode via ThemedRoot + EnvironmentKey<ThemePalette>. The keyboard extension deliberately stays dark (Apple's default) and now uses a transparent .background(Color.clear) so the system UI chrome shows through.
  • Speech Recognition permission requested on first press: added NSSpeechRecognitionUsageDescription to both targets' Info.plist and an explicit SFSpeechRecognizer.requestAuthorization call inside pressBegan(). Without these the iOS 18 ASR path silently returned .denied and the user heard nothing.
  • ASRService emits a DEBUG warning when on-device recognition isn't supported, so it's obvious during dev that the request fell back to cloud.
  • App Group fallback behaviour:
    • In DEBUG, a missing App Group now fatalErrors with a precise remediation message (was a soft print + .standard fallback, which desynced the keyboard extension from the main App).
    • In release, the fallback is preserved but logged via NSLog.
  • KeyboardViewController.loadPersistedLocale self-check (DEBUG only) prints the active provider / baseURL / masked API key / mode / locale, so the keyboard extension's view of the App Group is visible in the device console.
  • KeyboardViewController.handleFinalTranscript typed-error routing: LLMError.noAPIKey (401) and LLMError.http(429) now surface as red, explicit error messages instead of silently inserting the raw transcript. Network / timeout errors still fall back to the raw transcript + error badge (no data loss).
  • PolishingService timeout raised from 12 s to 15 s to align with LLMClient's URL request timeout. Previously the polisher would race the network call and discard a successful response that arrived in the 1215 s window.
  • DEBUG print cleanup: the four 🔥 [OSGKeyboardApp] … instrumentation prints in OSGKeyboardApp.init() and root views are now wrapped in #if DEBUG.

Added

  • "Test connection" button in APISettingsCard: fires a single polish("ping") round-trip and surfaces success or the typed LLM error inline. Helps the user confirm the App Group + key are working without leaving the main App.
  • 5 new unit tests in OSGKeyboardTests: App Group cross-process persistence, 401 / 429 / timeout / noAPIKey catch paths, and mode = .off short-circuit.

Changed

  • README + README.zh.md: replaced <OWNER> placeholder with hkgood and rephrased the iOS 26 SpeechAnalyzer line as "planned for the next release" (the iOS 18 SFSpeechRecognizer path remains the only working ASR for v0.1).

Known limitations

  • iOS 26 SpeechAnalyzer + DictationTranscriber on-device ASR is planned for 0.2.0 (moved out of Unreleased scope to keep the v0.1 release honest about what ships).
  • The keyboard does not work in password fields (iOS limitation).
  • Microphone requires "Allow Full Access" to be enabled in iOS Settings.
  • Whisper.cpp / on-device LLM polish is intentionally out of scope for v1 (cloud-only).

[0.2.0] - Planned

Added

  • iOS 26+ SpeechAnalyzer + DictationTranscriber on-device ASR (lower latency, more locales).
  • Bilingual UI (中文 / English) driven by a real Localizable.strings table; will land alongside a Settings → Language picker.

[0.1.0] - 2026-06-17

Added

  • First public pre-release.