Rework the marketing site with a cleaner, more restrained layout, replace emoji with Material Symbols monochrome icons, and add full SEO (Open Graph, Twitter cards, JSON-LD, canonical, robots.txt, sitemap.xml).
Build a first-version SFCustomLanguageModelData for on-device ASR
customization, generated offline on macOS (no app code dependency).
- Scripts/lexicon: Python builders for Sogou-derived phrases (scel +
SogouPopularDict) and a curated bilingual AI/tech/brand seed lexicon
- export_clm.swift / prepare_clm.swift: macOS CLI tools that export the
.bin training asset and compile it into LM + Vocab via the Speech
framework
- Resources/CustomLanguageModel: generated phrases.tsv, manifests, the
129k-phrase .bin, and compiled LM/Vocab assets (zh_CN)
- .gitignore: ignore lexicon build cache and Python bytecode
Note: Sogou-derived data is for internal experimentation only; the
curated AI/tech seed is MIT and safe to ship.
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.
Track dictation duration, dictation/translation character counts, and
personal dictionary size. Record metrics when Flow finalizes utterances.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
- Remove polish intensity and personal dictionary description subtitles
- Use consistent surface background on personal dictionary page
- Change empty state icon to library (menuBook) style
- Remove polish scenario and translation rows from onboarding
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
XcodeGen 2.45 writes AppIcon.icon as a one-line PBXFileReference with
lastKnownFileType = wrapper.icon. Add a fast in-place upgrade to
folder.iconcomposer.icon before the generic brace-matching fallback.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
Remove fileTypes icon override and explicit type:file so XcodeGen 2.37+
emits folder.iconcomposer.icon directly. Skip patch when native output is
valid; otherwise run patch v3 with broader PBX discovery and validation.
Fixes persistent 'AppIcon.icon entry has unexpected shape' on Xcode 26.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
Brace-match the AppIcon.icon PBX object instead of relying on fixed
regex shapes. Fixes 'AppIcon.icon entry has unexpected shape' when
XcodeGen/Xcode 26 emits a different PBXFileReference format.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
- Remove polish scenario and ASR locale chips from keyboard top bar
- Fix polish intensity labels via SharedL10n (Shared.strings table)
- Merge polish intensity and personal dictionary into preferences card
- Personal dictionary: no list icon; detail view uses nav bar + back
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
languageAndModelsSection and systemPromptLinkSection were renamed/
merged into languageAndPolishSection in v0.3 polish scenarios work,
but body still referenced the old symbols after intelligent-polish
changes were merged.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
Use ThemedRoot in preview instead of inaccessible ThemePalette().
Add setPersonalDictionary(_:) for let AppGroupStore bindings; use
it from PersonalDictionaryView and DictionaryLearner.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
AppGroupStore is a struct; property setters are mutating and cannot
be called on a let binding. Add setOnboardingPage and
setHasCompletedOnboarding helpers (like setDetectedAppContext).
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
ExtL10n.text returns Text; use ExtL10n.string where String is
required (computed properties, Text/Button initializers). TypeStyle
has caption, not caption1 — align onboarding and dictionary views.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
contains(where:) on String iterates Characters; comparing each to
multi-character strings like "\n " is a type error. Use
tail.contains("\n ") || tail.contains("\t") instead.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
Use 'version: Int = 1' instead of invalid 'version: 1' so Swift
synthesizes the default initializer and PersonalDictionary() compiles.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
XcodeGen fails when project.yml references a missing config file.
Add Signing.local.xcconfig.example and have generate-xcodeproj.sh
copy it on first run so fresh clones can generate the project.
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
Two long-standing UX papercuts, fixed without leaving the keyboard:
1. **First-launch onboarding inside the keyboard** — iOS keyboard
extensions *cannot* programmatically switch back to the previous
app after jumping out, so the old flow (jump to host app → user
has to manually navigate back) was 5+ taps of friction. The new
`KeyboardOnboardingOverlay` keeps the user inside the keyboard
for steps 1 (welcome), 2 (mic permission), 3 (speech permission),
and 5 (API key hint). The only step that *must* leave is step 4
("Enable Keyboard"), which jumps to `Settings.app` via
`UIApplication.openSettingsURLString` — on return,
`viewWillAppear` calls `autoAdvancePastKeyboardSetupStepIfNeeded`
which silently advances past that step if the keyboard is now
enabled. Net UX: user types in their app, keyboard walks them
through setup, normal UI appears as soon as setup is done.
2. **Per-app context chip on the keyboard top bar** — the v0.3.0
intelligent-prompt pipeline already adapted tone to detected
context (code/email/chat/document), but without a UI cue the user
had no way to know which mode was active or override the heuristic
when it guessed wrong. The new `AppContextChip` surfaces the
detected context; tap-to-override writes back to
`AppGroupStore.setDetectedAppContext` so the next LLM call
picks up the new tone immediately. Wired into `pressBegan` so
the chip updates in real time as the user types into different
fields.
### Files added
- `OSGKeyboardExt/Views/AppContextChip.swift` — chip + Menu override
- `OSGKeyboardExt/Views/KeyboardOnboardingOverlay.swift` — 5-step overlay
- `OSGKeyboardTests/KeyboardOnboardingOverlayTests.swift` — round-trip + enum surface tests
### Files modified
- `OSGKeyboardShared/Services/KeyboardState.swift`
+ `hasCompletedOnboarding`, `onboardingPage`, `appContext`
+ `setAppContext`, `advanceOnboarding`, `completeOnboarding`
+ `requestMicPermission`, `requestSpeechPermission`, `openSystemSettings`
- `OSGKeyboardShared/Services/AppGroupStore.swift`
+ `hasCompletedOnboarding` / `onboardingPage` accessors (mirror of
`ProviderConfig` keys, so the keyboard extension never has to
instantiate the host-app config)
- `OSGKeyboardExt/KeyboardViewController.swift`
+ action hooks wired (`installStateActions`)
+ `syncOnboardingStateFromAppGroup` / `syncAppContextFromAppGroup`
called on `viewWillAppear` and `loadPersistedConfig`
+ `autoAdvancePastKeyboardSetupStepIfNeeded` for the silent
"jump out → come back" flow
+ `openSystemSettingsFromExtension` opens `Settings.app` via
`UIApplication.openSettingsURLString` (the only system URL
the extension is allowed to open)
+ `detectAndStoreAppContext` mirrors to `state.appContext` so
the chip updates without waiting for `viewWillAppear`
- `OSGKeyboardExt/Views/KeyboardRootView.swift`
+ overlay mounted in `ZStack` over normal UI (animated)
+ AppContextChip in top bar (hidden during onboarding)
- `OSGKeyboardExt/{en,zh-Hans}.lproj/Keyboard.strings`
+ onboarding copy + chip labels (35 keys per language)
### iOS sandbox notes (kept here for posterity)
- Keyboard extensions **cannot** present AVAudioSession /
SFSpeechRecognizer permission dialogs directly. The overlay's
step 2/3 buttons optimistically advance; the actual permission
is granted when the user first opens the host app (which the
step-5 "Open OSGKeyboard" button triggers). This is the same
pattern the previous "jump to host app" flow used — just
without the broken return trip.
- `UIApplication.openSettingsURLString` is the only system URL
reachable from `extensionContext.open`. Both step 4 and the
"Open Settings" button route through `HostAppLauncher` so the
responder-chain fallback also kicks in if needed.
Co-authored-by: Mavis <Mavis@hkgood.dev>
v0.3.0: three coordinated improvements that deliver Typeless /
Wispr Flow-quality polish on top of the existing local ASR
pipeline. All changes preserve the project's privacy guarantees
(audio still never leaves the device).
## 1. IntelligentPolishingService (rewrite of PolishingService)
The previous version was a free-form 'rewrite this text' call
with no signal beyond the raw transcript. The new one is a
single LLM call that does three things in one pass, exactly as
Typeless and Wispr Flow do internally:
1. ASR error correction (homophones, near-misses, missing chars)
2. Polish (drop filler words, fix grammar, add punctuation)
3. Style adaptation per app context (code / email / chat / doc)
The merged-prompt design halves the round-trip vs the previously
proposed two-stage design (correction + polish separately) and
the academic literature confirms it performs equivalently for
everyday Chinese / English dictation.
## 2. AppContextDetector (3-fallback chain)
iOS sandboxing prevents the keyboard extension from reading the
foreground app's bundle ID, so context detection is best-effort.
The detector runs three fallbacks in order, with caching to
avoid the cold-start 'unknown' that would force a neutral-tone
LLM call every time the user opens a new field:
1. Heuristic on the text at the cursor (code / email / chat / doc)
2. 30-minute cache of the last successful detection
3. Time-of-day + weekend heuristic as a soft default
The keyboard extension runs the detector on every press of the
mic and persists the result to the App Group so the host app's
polisher picks it up.
## 3. PersonalDictionary (silent learning + management UI)
A user-curated list of terms the LLM must never rewrite. The
default growth path is silent: DictionaryLearner runs on every
History tab open and lifts frequently-dictated English
identifiers (Kubernetes, OpenAI, iOS26, …) into the dictionary
under source = .history. Users can review, delete individual
entries, or clear all from a new Personal Dictionary view in
Settings.
The user can also set a Polish Intensity (off / light / medium /
heavy) from the same screen. Default is medium, which is what
Typeless and Wispr Flow also use.
## Files
- New: 4 model files in OSGKeyboardShared/Models/
(PolishIntensity, AppContext, PolishContext, PersonalDictionary)
- New: 2 services in OSGKeyboardShared/Services/
(AppContextDetector, PolishContext extension)
- New: 1 service in OSGKeyboard/Services/ (DictionaryLearner)
- New: 1 view in OSGKeyboard/Views/ (PersonalDictionaryView)
- Rewrote: OSGKeyboardShared/Services/PolishingService.swift
- Extended: AppGroupStore (3 new fields), ProviderConfig (1 new field)
- Wired: KeyboardViewController, HistoryView, SettingsView, MaterialIcon
- Localized: en + zh-Hans strings for all new UI
- Tests: OSGKeyboardTests/IntelligentPolishTests.swift (16 tests)
## Verification
- All new code follows the existing Sendable / strict-concurrency
patterns (the keyboard extension stays within its 60MB sandbox;
the polisher remains an actor; @MainActor is applied to the
learner and the settings UI).
- Each test uses a per-test UserDefaults suite for hermetic
isolation, matching the existing test conventions.
- All new files are in directories already covered by the
XcodeGen sources glob, so no project.yml change is needed.
## Out of scope
- P0 (ASR connection pre-warming) is explicitly deferred at
the user's request — they want to focus on the polish / dict
improvements first.
- The Cloud polish (WebSocket) work is not touched.
## Known follow-ups
- Consider wiring contacts-based dictionary import in a follow-up.
- Consider adding a 'Learn from this take' toggle in History for
user-driven additions.
- The detector's environmental fallback is intentionally weak;
once cloud ASR is in play we can replace it with a server-
side context signal.