a803a27a88
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
71 lines
7.0 KiB
Markdown
71 lines
7.0 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to OSGKeyboard will be documented in this file.
|
||
|
||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||
|
||
## [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 `fatalError`s 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 12–15 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.
|