What's new.
All updates and improvements for WunderType.
What's New
Calmer Progress Pill
A calmer progress indicator and a refreshed Settings window.
- A quieter progress pill — while a correction runs, a small indicator now appears right next to your cursor, and only when there’s actually a wait. Quick corrections stay completely out of your way, and a checkmark confirms when the text is ready
- Clear feedback when a model is loading — if an on-device model needs a moment to warm up, the indicator now tells you instead of just spinning, so a slow first correction never looks stuck
- Refreshed Settings — every section now has a clear, labeled heading, making the General, Prompts, Personalization, and About tabs easier to scan at a glance
Replaced the ModernCorrectionHUD + ModernCorrectionHUDHost + CursorSpinnerHost trio with a single adaptive CorrectionPillHUD/CorrectionPillHost: one content-sized pill anchored just below-right of the cursor that owns the whole correction lifecycle (working spinner, escalation to a wait message, success checkmark, error text).
IMPROVEThe pill now stays hidden behind a 300ms grace period so fast corrections never flash any UI, and only escalates to a 'loading the model' explanation after 6 seconds (raised from 4) — cloud providers get a generic 'Still working…' instead.
IMPROVETightened the pill to the cursor (cursorGap 12→4pt) and made it more compact (mini spinner, footnote text/icons, reduced padding) so it takes up less of the screen and reads as less of a distraction.
IMPROVERewired CorrectionCoordinator and StreamingCorrector onto the new pill host lifecycle (begin/noteResponseStarted/succeed/fail/dismiss), generation-gated so a stale correction can never resolve or dismiss a successor's pill; success only shows a checkmark when the pill is already visible.
IMPROVEAdded a CorrectionPillTests Swift Testing suite covering phase→content mapping, per-provider escalation messages, grace/escalation timing invariants, and panel-placement geometry (edge clamping and above-cursor flip).
NEWSettings visual polish: a shared SettingsSectionHeader (accent-tinted, uppercase, SF Symbol) applied consistently across the General, Prompts, Personalization, and About tabs.
IMPROVENew On-Device Models
Fresh on-device models and an easier way to manage them.
- Two new on-device models — Qwen3 4B joins the lineup as a multilingual specialist (200+ languages), and the new Granite 4.0 Micro replaces Granite 3.3 with a small, fast model that now preserves your Markdown formatting
- Manage your downloads — a new list in Settings shows how much space each on-device model takes and lets you delete the ones you don’t need, so retired models never linger on your disk
- Tidier prompt editor — when a custom instruction runs long, only the text box scrolls now, keeping the Save and Shortcut controls in place
Added Qwen3 4B Instruct-2507 (mlx-community/Qwen3-4B-Instruct-2507-4bit) to the On-Device model lineup — a non-thinking, multilingual (200+ language) model that passes the full MLX smoke suite (grammar, Markdown preservation, German language retention, short-input sizing).
NEWReplaced Granite 3.3 2B with Granite 4.0 Micro (mlx-community/granite-4.0-h-micro-4bit) in SettingsManager.availableMLXModels: a 3B hybrid model that preserves Markdown where the 2B stripped it. Users who had Granite 3.3 selected migrate to the Phi-4 Mini default via the existing availableMLXModels fallback in the mlxModel getter.
IMPROVENew Manage Downloads UI in MLXProviderSettings: a disclosure listing every on-device model on disk (including orphans removed from the picker) with its size and a confirmation-gated Delete. Backed by new MLXService helpers downloadedModelIDs(), modelDiskSizeBytes(), deleteDownloadedModel(), and modelID(fromCacheDirectoryName:).
NEWdeleteDownloadedModel() unloads the resident model before removing its weights and refuses to delete a model that is mid-download (guards on _loadingModelID under the loader lock), preventing a corrupt half-downloaded cache.
IMPROVEFixed the custom prompt editor (PromptEditorSheet): a long instruction now scrolls inside a fixed-height text area instead of scrolling the whole sheet, and the sheet is taller so the Shortcut row is no longer clipped.
FIXQuick Mode Switching
Quicker mode switching and a more flexible local setup.
- A shortcut for every mode — give Correct Grammar, Improve Writing, Make Concise, Make Formal, and Make Casual each their own keyboard shortcut, and jump straight to any of them from any app without changing your default mode first. They’re in the menu bar too, under “Correct As”
- Custom Ollama server address — point WunderType at Ollama on a different port, through a proxy, or on another machine, instead of only the default local address
- Ollama connection status — Settings now shows at a glance whether your Ollama server is reachable, so a wrong address is easy to spot
CorrectionCoordinator.performCorrection gained a `mode:` override that runs a one-shot correction in a specific built-in mode without persisting the user's selected default mode. Standard-correction semantics (single-line preservation, trailing-commentary stripping, silent no-changes) are preserved by keeping the gating on the `systemPrompt` parameter, so a mode-override correction never behaves like a custom prompt.
NEWPer-mode global shortcuts — each CorrectionMode exposes a stable, no-default KeyboardShortcuts.Name (mode_*), so opt-in bindings never collide with the primary Cmd+Shift+G trigger. Handlers register once at launch (registerModeShortcuts); a new 'Correct As' menu-bar submenu and a per-mode Recorder section in the General settings tab surface them for discoverability.
NEWThe currently-selected default mode is filtered out of the Mode Shortcuts list and its dedicated binding is cleared — live on the Mode-picker onChange and defensively at launch — so it can't fire as an invisible duplicate of the global shortcut.
IMPROVESettingsManager.ollamaBaseURL + OllamaService.normalizedBaseURL — user-configurable Ollama server address (custom port, proxy, or remote host). Normalized at use time (prepends http:// when scheme-less, strips trailing slashes, empty falls back to the default) and read dynamically per request so a change takes effect without relaunch. Works on both the sandboxed App Store build and the DMG build.
NEWGeneralSettingsTab — the Ollama provider section gains a Connected / Not reachable status line (parity with OpenAI and OpenRouter), re-checked on provider switch and ~1s after the server address is edited; the model picker clears when the server is unreachable.
IMPROVEOn-device stack validated against mlx-swift-lm 3.31.4 (build + full unit suite including real-model MLX smoke tests all green); not pinned, as Package.resolved is intentionally gitignored and the graph floats to latest-in-constraint at build time.
IMPROVETests — ModeShortcutTests (per-mode shortcut-identifier stability, uniqueness, and namespacing) and OllamaBaseURLTests (URL normalization shapes: scheme prepend, trailing-slash strip, whitespace/empty fallback).
NEWPersonalization
WunderType now learns how you like your text. When you keep changing a correction back, it remembers and stops making that change — and everything it learns stays on your Mac.
- Learns what you change back — when you repeatedly undo the same correction, WunderType notices and stops making it. Switch it on in Settings → Personalization, or when you first set the app up
- Everything stays on your Mac — it only ever remembers short word rules, never the text you write, and nothing is ever uploaded
- See and control everything it learns — the new Personalization tab lists every rule in plain language. Turn any one off, edit it, delete it, or clear everything with a single click
- Add your own rules — set exact word swaps that are always applied to every correction, independent of the AI model — for example, always change “utilise” to “use”
- Off until you turn it on — you decide whether to enable it; nothing is learned unless you opt in
PersonalizationStore — new on-device adaptive-personalization layer (@MainActor) that learns the substitutions a user keeps reverting and re-applies their preferred form to future model output. Fully deterministic string processing — no model inference, no prompt injection — so it behaves identically across model sizes and cannot regress correction behavior. Durable state is a rules-only JSON profile in Application Support (short from→to token rules + counters, never user prose); raw correction text lives exclusively in an in-memory ring buffer that is cleared on quit.
NEWReversion detection is MAS-safe: rather than reading the document back after paste (Accessibility, blocked by the App Store sandbox), each new correction input is LCS-diffed against WunderType's own recent outputs, so the edits a user made are recovered from text the coordinator already holds. Gated by token similarity (>=0.6) and edit-cluster count (<=3) so rewrites are not mistaken for reversions; enforcement is whole-word ICU (\b) substitution, Unicode-aware. Learned rules promote at hitCount 2; manual rules apply immediately.
NEWPersonalizationSettingsTab — new Settings tab that doubles as a transparency inspector: every learned and manual rule is listed in plain language with its status (Learning / Ready / Active), and each is individually toggleable, editable, and deletable, with a Clear Everything action. Users can also add manual always-on word-swap rules by hand.
NEWConsent is tri-state (undecided/enabled/disabled) so the existing-user vs deliberately-off distinction is never conflated: new users choose on the onboarding Completion step (default off), existing users get a one-time disclosure card gated on its own marker, and .undecided behaves as off. All three CorrectionCoordinator hooks (observe/enforce/recordOutput) are a strict no-op unless the user has opted in.
NEWCorrectionCoordinator — recordOutput runs only on the confirmed-write tail so corrections aborted mid-flight (cancellation, selection-changed) never enter the reversion-detection buffer as text that was never pasted.
IMPROVESettings — grouped Forms drop their gray canvas (scrollContentBackground hidden) and the window uses a full-size, transparent, title-less titlebar for a cleaner multi-tab surface; the Prompts tab clarifies that Writing Context is soft, model-interpreted guidance versus Personalization's exact, always-applied word swaps.
IMPROVEStability & Performance
A stability and performance release — lighter on battery and memory, quicker to respond, and more careful with your data. No new features; everything you already use just runs better.
- Lighter on battery — cloud providers (OpenAI and OpenRouter) now check their connection far less often in the background, with an instant status refresh the moment you open the menu
- Lower memory use — On-Device AI now releases its model from memory automatically when your Mac runs low, then reloads it on your next correction
- Your clipboard stays private — while a correction runs, the temporary clipboard contents are marked private so clipboard-manager apps don’t record your text
- More reliable API keys — saving an API key is now safer, with no moment where it could be lost, and WunderType tells you if a save ever fails
- Snappier menu — the menu bar menu and your custom prompts open faster
- Cancellable downloads — you can now stop an On-Device model download that’s in progress
MLXService — critical memory-pressure monitor (DispatchSource .critical) unloads the resident On-Device model when macOS reports memory pressure and calls Memory.clearCache() so freed Metal buffers actually return to the OS; the model reloads from the disk cache on the next correction. Warning-level pressure is intentionally ignored (transient), and the prefix cache is not repopulated if the model was evicted mid-generation.
IMPROVEAppDelegate health polling — cloud providers (OpenAI/OpenRouter) now background-poll every 5 min instead of 30s, sleeping in 30s slices so a provider switch is picked up promptly; polling pauses while a correction is in flight, and menu-open triggers a throttled one-shot refresh so the status line stays current. Fast 10→60s backoff retained when unhealthy.
IMPROVEKeychainHelper — API-key saves use update-then-add (no delete-then-add window where a key could be lost), return a Bool, and surface failures via NotificationService; the legacy UserDefaults→Keychain migration deletes the plaintext copy only after a confirmed Keychain write.
FIXClipboardService.writeString — correction text written to the pasteboard is now marked org.nspasteboard.TransientType + ConcealedType so clipboard-manager apps don't record it during the copy → AI → paste cycle; target apps still read the plain string.
IMPROVEAppDelegate menu + SettingsManager — the status menu is built once and only its dynamic items (connection status, Launch-at-Login) refresh per open, with the custom-prompt section rebuilt only on change notification; the decoded customPrompts array is cached behind an NSLock and invalidated by the setter, removing per-open JSON decode churn.
IMPROVEMLXService — inference is now cancellable: the token stream checks Task.isCancelled and stops the GPU loop promptly instead of running to maxTokens, freeing the serialized container for the next correction; On-Device model downloads in Settings are cancellable too.
IMPROVEOpenAICompatibleProvider — OpenAI and OpenRouter unified behind a shared provider protocol over OpenAIChatClient, collapsing duplicated REST/error-mapping code; ResponseArtifacts centralizes the stop-marker / preamble / filler-prefix lists shared by the MLX atomic cleaner and the streaming cleaner.
IMPROVEAppDelegate Ollama warm-up loop — re-checks the keepOllamaLoaded toggle every 30s (warm-up cadence still 600s) so enabling it takes effect within ~30s instead of up to 10 minutes.
IMPROVEMLXProviderSettings — model unload is gated on app state rather than a view's .onDisappear, so switching Settings tabs no longer evicts the loaded On-Device model; MLXModelLoadController extracts the load/cancel/progress state machine from the view.
FIXTests — added PromptPersistenceTests (legacy decode, round-trip, bundled starter-pack) and ProviderResponseParsingTests (ChatResponse/ErrorResponse decoding, extractResult rules, full mapError matrix for OpenAI + OpenRouter); seedDefaultPromptsIfNeeded split into a pure, tested seedingDecision plus an I/O wrapper.
NEWDownload Progress & Cleanup
Downloading an On-Device model now shows real progress — and WunderType tidies up after itself.
- Accurate download progress — when you download an On-Device model, the progress bar now climbs with a live percentage as the model downloads, instead of a spinner that looked stuck
- Automatic disk cleanup — interrupted or cancelled model downloads no longer leave large temporary files behind; WunderType reclaims that space automatically
MLXDownloadProgressMonitor — new disk-based progress source for On-Device model downloads: sums the HuggingFace blobs directory plus the in-flight CFNetworkDownload temp file against each model's approximate size to drive a determinate progress bar. Works around swift-huggingface #48, where the library's own progress callback never reports incremental bytes for the large safetensors download on the LFS transport (didWriteData does not fire for the redirected CDN transfer), leaving the old bar stuck near 1% then jumping to 100%.
NEWMLXService — uncached model downloads now report accurate progress via MLXDownloadProgressMonitor instead of the unreliable library callback; added modelCacheDirectory(_:) and downloadSizeBytes(for:) helpers, and a per-tick progress fraction capped at 99% until the model finishes loading.
IMPROVEMLXService.cleanUpOrphanedDownloadTempFiles() — sweeps orphaned CFNetworkDownload_*.tmp files (multi-GB leftovers from cancelled/failed downloads) older than 10 minutes; runs at app launch (off-main) and before each new download.
FIXSettingsManager — added approxDownloadBytes to each MLXModelInfo entry, used as the denominator for the download progress bar.
IMPROVEMLXDownloadProgressMonitorTests — new Swift Testing suite covering the fraction cap math and the on-disk byte accounting (baseline orphan exclusion, temp-file matching, missing directories).
NEWStreaming Corrections
Watch your corrections appear. A new experimental option types the corrected text into place as the On-Device model writes it, instead of replacing your selection all at once.
- Streaming corrections (experimental) — switch it on in Settings → General → Experimental, and On-Device corrections are typed into place live, as the model generates them
- On-Device only — streaming uses the local MLX model for instant output; OpenAI, OpenRouter, and Ollama keep the standard one-shot replacement
- Works in more apps than before — because the text is typed in like a keyboard, the streaming effect reaches apps where live editing previously wasn’t possible
- Keep the target app in front while text streams in — switching away stops the correction, and ⌘Z undoes any partial text
- Accessibility and polish — clearer VoiceOver descriptions throughout onboarding and provider setup, plus a lighter, smoother progress cursor while corrections run
StreamingCorrector — new experimental streaming correction engine: types the On-Device model's output into the focused control as it generates, paced at ~280 chars/sec, with prefix-hold (identical output never touches the document), focus-change abort, and clipboard-paste paragraph separators (typed newlines would send messages in chat apps)
NEWTypingService — posts synthetic Unicode keystrokes (CGEvent keyboardSetUnicodeString) for sandbox-compatible insertion that also reaches Chromium/Electron apps which swallow Accessibility writes; waits for the correction shortcut's modifiers to be released before typing so keystrokes don't register as shortcuts
NEWMLXService.streamCorrectText — ChatSession-based streaming returning an AsyncThrowingStream<String, Error>, with cancellation wired through the stream's onTermination; added readyContainerSnapshot and effectiveSystemPrompt helpers now shared with the atomic correctText path
NEWStreamingResponseCleaner — incremental counterpart to MLXService.cleanResponse; filters thinking blocks, preamble lines, code fences, leaked EOS markers, and "Okay, here's the corrected…" filler prefixes before each chunk is emitted, since typed text cannot be un-typed
NEWResponseCommentary — shared detection of trailing model commentary ("(Note: I corrected…)"), now multilingual across the languages WunderType supports, used by both the atomic and streaming correction paths
NEWSettings — "Stream corrections into the document" toggle (default off) under a new Experimental section in General, gated to Apple Silicon, with inline known-limitations guidance when enabled
NEWCorrectionCoordinator — routes On-Device corrections through StreamingCorrector when streaming is enabled, with full outcome handling (completed, no-changes, cancelled, failed, aborted-mid-stream); the standard atomic path now also strips trailing model commentary for parity
IMPROVETests — added ResponseCommentaryTests and StreamingResponseCleanerTests (21 cases) covering multilingual commentary detection, legitimate-parenthetical preservation, and incremental artifact/filler filtering across chunk boundaries
NEWStreamingCorrector / TypingService — cancellation now propagates into the detached typing task (withTaskCancellationHandler + Task.isCancelled between keystroke chunks), so a focus change or user cancel aborts at the next keystroke boundary instead of finishing the in-flight slice into whatever app gained focus
IMPROVEMLXPromptPrefixCache — extracted the prompt-prefix match into a pure reusablePrefixLength(promptTokens:cachedTokens:cachedOffset:) function, documented the precondition that take/store must run inside ModelContainer.perform, and added MLXPromptPrefixCacheTests (7 cases) pinning the reuse boundaries
IMPROVECursorSpinnerHost — coalesced the global mouse-move handler to reposition the progress spinner at most once per frame (~60 Hz) instead of a window-server setFrameOrigin round trip on every event for the full correction duration
IMPROVEOnboarding — replaced the Int-keyed step state machine with a type-checked OnboardingStep enum (centralized progress mapping, no dead default branch); extracted shared APIKeyField (debounced OpenAI/OpenRouter key validation), CopyCommandRow, and AboutRow components; made the cloud provider cards data-driven
IMPROVEAccessibility — added VoiceOver labels for the API-key validation status icons and the onboarding progress indicator; moved PromptEditorSheet's generation-error alert from a derived Binding(get:set:) to a Bool state flag
IMPROVEOn-Device AI
On-Device AI is here. Correct text locally on your Mac — no Ollama, no API key, no internet after setup.
- New \“On-Device\” AI provider runs models directly on your Mac’s GPU using Apple’s MLX framework
- Download a model once, then correct text entirely offline — your text never leaves your Mac
- Four models to choose from: Gemma 3 1B (fastest, ~0.7 GB), Granite 2B (fast, ~1.3 GB), Phi-4 Mini (balanced, ~2.0 GB), or Gemma 3 4B (best quality, ~3.0 GB)
- Built for speed — the model warms up at launch and reuses its prepared instructions between corrections, so corrections start instantly
- Zero setup — no external apps, no API keys. Select On-Device during onboarding and go
- Requires Apple Silicon (M1 or later)
MLXService — new AI provider using Apple's mlx-swift-lm framework for in-process Metal GPU inference. Supports model download from HuggingFace, automatic caching, EOS token handling, and response cleaning (thinking blocks, code fences, preambles, leaked special tokens)
NEWAIProvider — added .mlx ("On-Device") case wired through UnifiedAIService checkHealth, correctText, generateSystemPrompt, and showError
NEWMLXSetupStep — new onboarding step with model picker, download with indeterminate progress spinner, model descriptions, error display, and download cancellation on Back
NEWGeneralSettingsTab — On-Device provider section with model picker, descriptions, download/load/unload buttons, error display, and model-change state reset
NEWProviderSelectionStep — added On-Device provider card with cpu icon and Recommended badge, reorganized from 3-column to a 2x2 grid with compacted card spacing to fit the onboarding window; Intel Macs keep the original 3-card row with Ollama recommended
IMPROVEAppDelegate — MLX model pre-loading at launch when On-Device is the configured provider, with stored task handle for cancellation on termination
NEWMLXService.cleanResponse — shared post-processing strips thinking blocks, EOS tokens (Gemma, Phi, Llama, Qwen), code fences, and preamble lines from all model output
NEWSmart prompt routing — each entry in the model list declares whether it needs a compact prompt; Gemma 3 1B receives a compact grammar-focused prompt, while 2B+ models receive the full commonRules system prompt (matching Ollama behavior)
NEWRace-safe model loading — concurrent calls wait for in-flight loads instead of duplicating downloads, with error and cancellation propagation to waiters; waiters time out after 120 seconds for cached loads or 30 minutes for active downloads
IMPROVEApple Silicon gating — MLXService.isDeviceSupported hides the On-Device provider on Intel Macs (onboarding card and Settings picker), coerces a stored On-Device provider setting back to Ollama, and loadModel throws unsupportedDevice as a final guard; the app binary remains universal
NEWMLX memory management — switching the AI provider away from On-Device (in Settings or onboarding) now frees the model weights, and loading a different model releases the previous one first instead of transiently holding two models in unified memory
FIXCorrection shortcut never triggers a silent multi-GB download — if the selected on-device model isn't cached (e.g. macOS purged the cache), the correction fails fast with a "Model not loaded" HUD instead of blocking for minutes while downloading
FIXCorrections always run on the model selected in Settings — correctText reloads when the in-memory model diverges from the persisted selection, instead of silently serving from the previously loaded model
FIXModel cache detection resolves the HuggingFace cache directory through the library's own CacheLocationProvider (sandbox- and env-var-aware) and requires config.json alongside the weights, so partial or interrupted downloads aren't mistaken for cached models
FIXGeneralSettingsTab — model picker disabled while a load is in flight, loaded state verified against the service after each load, Reload Model actually unloads first (was a no-op), Unload guarded during loads, and cache checks moved off the SwiftUI render path
FIXMLXSetupStep — restores the previously chosen model and loaded state when re-entering the step, no longer shows a download error when the user cancels via Back, and progress copy shows the model size and distinguishes downloading from loading
IMPROVESettingsManager — MLX model list converted from a tuple to a typed MLXModelInfo struct; a stored model ID that's no longer in the list falls back to the default instead of blanking the picker and dropping its EOS token configuration
IMPROVEDownload progress handler removed for uncached models — workaround for swift-huggingface issue #48 where @MainActor progress callbacks stall large downloads
FIXCompletionStep — model summary row now shows the display name for all four providers including On-Device
IMPROVEAboutSettingsTab — model name switch handles .mlx case, displaying the friendly model name from the available models list
IMPROVECorrectionCoordinator — hudMessage(for:) handles MLXServiceError with user-facing messages for model-not-loaded, download-failed, and correction-failed states
IMPROVEOnboardingView — default provider changed from .ollama to .mlx for new users, step 9 added for MLX setup with correct progress bar mapping
IMPROVEMLXPromptPrefixCache — KV-cache prefix reuse for on-device corrections: the templated system prompt (~150–430 tokens) is prefilled once and reused across corrections via token-level prefix matching, so each correction only prefills the selected text (~0.3–0.5s saved per correction). Mode switches, custom prompts, and model changes invalidate naturally; the stored cache is bounded to 512 tokens to keep idle memory predictable
NEWMLXService — generation moved from ChatSession to a TokenIterator-based loop to enable prefix caching, with per-correction timing instrumentation (prefill/generation token counts, tok/s, and cached-token reuse) logged via os.Logger
IMPROVEMLX warm-up — a tiny throwaway generation after model load (launch pre-load, Settings load button, onboarding download) compiles the Metal kernels and pre-builds the system-prompt prefix cache, so the user's first correction no longer pays that cost
IMPROVEGemma 3 1B no longer flips messy English input into German — the compact system prompt named languages ("German stays German, French stays French"), which primed the 1B model to translate. Reworded to name no languages; validated 0/4 German after vs 6/6 before, with German input still staying German
FIXMLXService.cleanResponse — strips filler-prefixed preambles ("Okay, here's the corrected text:") that previously leaked past the preamble check into the corrected output
FIXMLXCleanResponseTests — 27 unit tests covering EOS stripping (5 token formats), thinking block removal, code fence unwrapping, preamble stripping, and combined scenarios
NEWMLXSmokeTests — parameterized integration tests running real inference across all shipped models, checking grammar correction, markdown preservation, German language preservation, and response sizing; plus a prefix-cache reuse test (asserts cached-token reuse with clean output, including identical resubmission) and a Gemma 1B language-stability regression test
NEWSwiftPM dependencies added: mlx-swift-lm (3.0.0+), swift-huggingface (0.9.0+), swift-transformers (1.3.0+) with MLXLLM, MLXLMCommon, MLXHuggingFace, HuggingFace, and Tokenizers product dependencies
NEWOnboarding Improvements
Onboarding improvements based on user feedback.
- You can now choose your keyboard shortcut during onboarding — no need to dig into Settings if the default conflicts with your apps
- Ollama detection is faster and clearer — a live indicator shows it’s still checking when Ollama isn’t running
- The completion screen no longer cuts off the button on smaller windows
- Fixed the onboarding progress bar jumping when using the OpenAI provider path
ShortcutStep — new onboarding step between Accessibility and Completion that presents a KeyboardShortcuts.Recorder, letting users customize their global shortcut before finishing setup
NEWOnboardingView — added progressStep computed property that maps all internal step numbers to correct logical positions; previously the OpenAI setup path (step 3) showed progress position 3 instead of 2, causing the progress bar to jump
FIXOllamaSetupStep — added a visible ProgressView spinner and "Launch Ollama and it will be detected automatically" hint in the not-running state, so users know polling is active without navigating away
IMPROVEOllamaSetupStep — first 3 health checks now run at 500ms intervals before settling to the 2s polling cadence, improving detection speed when Ollama is already running but slow to respond
IMPROVEOllamaSetupStep — running-state transition now animates smoothly with withAnimation instead of snapping
IMPROVECompletionStep — removed the large green checkmark icon that was pushing the "Start Using WunderType" button off-screen on the 600×500 onboarding window
FIXCompletionStep — summary section now shows the configured keyboard shortcut, and the Quick Start instructions display the actual shortcut (e.g. "Press ⌘⇧G") instead of generic "Press your keyboard shortcut"
IMPROVESetup & Custom Prompt Fixes
A small reliability release for setup and custom prompts.
- Your \“Install starter prompts\” choice during setup is now respected — opting out no longer installs them anyway
- Custom prompt shortcuts you add or change now work immediately, without restarting the app
- Smart Selection now puts your cursor back where it was when a correction makes no changes, instead of leaving the whole paragraph selected (requires non-sandboxed build)
AppDelegate — default-prompt seeding moved from applicationDidFinishLaunching into post-onboarding setup. Previously seedDefaultPromptsIfNeeded() ran before the Completion step wrote the installDefaultPrompts toggle, so the seed was version-stamped first and a user who opted out still received all 8 starter prompts. The opt-out is now honored
FIXAppDelegate / PromptsSettingsTab — adding, editing, or deleting a custom prompt now posts a customPromptsDidChange notification; AppDelegate re-registers the onKeyUp shortcut handlers and rebuilds the menu. Newly added or re-bound shortcuts now fire without relaunching the app. v1.4.1 fixed edits to existing prompts (trigger-time lookup); this fixes brand-new shortcuts, which had a key binding but no action handler until restart
FIXSettingsManager — the customPrompts getter now logs decode failures via os.Logger instead of silently returning an empty array. A silent decode failure is the mechanism behind the v1.4.1 data-loss bug (an empty load gets overwritten on the next save); logging makes any future recurrence observable
IMPROVECorrectionCoordinator — the direct-write (Accessibility) path re-reads the selection immediately before writing and aborts with a "Selection changed" HUD if it moved during the AI call, instead of overwriting the wrong text. A pre-write cancellation check was also added. The write now returns a three-state outcome (written / needs-fallback / selection-changed) so a moved selection aborts cleanly rather than pasting into the wrong place. Affects non-sandboxed builds (the AX path)
FIXCorrectionCoordinator / AccessibilityService — Smart Selection now restores the original caret position when no replacement happens (the AI returns identical text, errors, or is cancelled), so the user isn't left with the whole paragraph selected. New getSelectedRange / setSelectedRange helpers capture and restore the kAXSelectedTextRange. Affects non-sandboxed builds (the AX path)
FIXCustom Prompt Fixes
A quick fix release for custom prompt users.
- Fixed a bug where updating from v1.3.x could cause existing custom prompts to disappear
- Custom prompt keyboard shortcuts now pick up edits immediately — no restart needed
- Custom prompts now show a \“No changes made\” HUD when the AI returns your text unchanged
- Brief custom prompt instructions are now automatically framed for better AI compliance
- Improved VoiceOver labels for prompt editing buttons
CustomPrompt — added custom init(from:) decoder with decodeIfPresent for isDefault field, fixing a crash when decoding v1.3.x prompts that lack the field. Previously the decode failed silently, returning an empty array, which caused seedDefaultPromptsIfNeeded to overwrite existing user prompts with defaults
FIXCustom prompt keyboard shortcuts now read the prompt from SettingsManager at trigger time instead of capturing the systemPrompt string at registration. Edits to prompt text take effect immediately without restarting the app
FIXCorrectionCoordinator — when a custom prompt returns text identical to the input, a "No changes made" HUD is now shown instead of silently skipping. Built-in correction modes still skip silently (unchanged text means already correct)
FIXPromptEditorSheet — on save, bare instructions that don't start with "You are" are automatically framed with "You are an in-place text editor" prefix and "Return ONLY the result" suffix for more reliable AI compliance
IMPROVEPromptsSettingsTab — writingContext switched from @AppStorage to @State with explicit SettingsManager sync, ensuring the 500-character clamp is always enforced
FIXPromptsSettingsTab — Edit and Delete buttons now have per-prompt VoiceOver accessibility labels
FIXPromptEditorSheet — keyboard shortcut recorder now has a VoiceOver accessibility label
FIXWriting Context, Smart Selection & Built-in Prompts
The biggest update yet — WunderType learns your preferences, ships with ready-to-use prompts, and keeps Ollama fast.
- Writing Context — teach WunderType your preferred names, terms, and style notes, applied to every correction
- 8 built-in prompts: Email Polish, Slack Reply, Translate to English, TL;DR, Bullet Points, Prose from Bullets, LinkedIn Post, Commit Message
- Ollama keep-alive toggle keeps your model loaded in memory between corrections
- Redesigned tabbed Settings with dedicated Prompts tab for inline editing
- Slimmer menu bar dropdown
Writing Context — new persistent text field in Settings (Prompts tab) appended to every AI system prompt. 500-character cap enforced at both UI and storage level
NEW8 curated default prompts bundled as default-prompts.json, seeded on first launch with stable UUIDs for deduplication. Users can edit or delete defaults
NEWOllama keep-alive — pass keep_alive: -1 on API requests when enabled, plus periodic 10-minute warm-up ping to prevent model unloading. New toggle in Settings under Ollama provider
NEWTabbed Settings — SettingsView refactored into GeneralSettingsTab, PromptsSettingsTab, and AboutSettingsTab with TabView container. Custom prompt CRUD moved from NSAlert dialogs to SwiftUI sheet (PromptEditorSheet)
IMPROVEMenu bar dropdown slimmed — removed Add/Edit/Remove prompt items; prompt management now in Settings. Dropdown shows only executable prompts
IMPROVENSApp.activate(ignoringOtherApps:) replaced with NSApp.activate() across 3 call sites (deprecated since macOS 14)
FIXCorrectionCoordinator.normalizeUnicode — replaced Foundation replacingOccurrences(of:with:) with Swift-native replacing(_:with:)
FIXOllamaService.GenerateRequest — custom encode(to:) with encodeIfPresent for keep_alive to avoid sending null (key omission instead)
FIXSmart selection AX calls collapsed from 3 sequential Task.detached round-trips into a single detached block with 50ms focus-settle delay for macOS 15+ compatibility
FIXSettingsManager.seedDefaultPromptsIfNeeded — decode errors now logged via os.Logger instead of silently swallowed
FIXCode Quality & Accessibility
A code quality and accessibility pass across the app.
- Fixed a potential crash in the correction HUD when displaying error messages
- Copy buttons in Ollama setup are now accessible to VoiceOver
- HUD text now respects your Dynamic Type size preference
- Ollama setup polling uses modern lifecycle management for better reliability
- Consistent font styling across all onboarding screens
ModernCorrectionHUD — replaced force unwrap on optional message with safe optional chaining to prevent potential crash
FIXOllamaSetupStep — copy-to-clipboard buttons now have VoiceOver-accessible text labels with .labelStyle(.iconOnly)
FIXModernCorrectionHUD — replaced hardcoded font size with semantic .caption for Dynamic Type support
FIXOllamaSetupStep — replaced onAppear/onDisappear polling with .task {} for automatic cancellation on view disappear
IMPROVEOnboarding screens — replaced fontWeight(.bold) with bold() across 8 files for correct system weight selection
IMPROVEOpenRouter Onboarding & Brand Refresh
We’re constantly working to make WunderType better for you.
- OpenRouter is now available as a provider during onboarding — pick from hundreds of models on first launch
- Updated brand colors across the app for a warmer, more polished look
- Small bug fixes and improvements
OpenRouterSetupStep — new onboarding step with API key validation and model ID input, matching the existing OpenAI setup flow
NEWProviderSelectionStep — added OpenRouter as a third provider card alongside Ollama and OpenAI
IMPROVEOnboardingView — routing updated to handle Ollama (step 2), OpenAI (step 3), and OpenRouter (step 7) setup paths with correct progress bar normalization
IMPROVEAccentColor asset added — app-wide accent color changed from system blue to a warm brown matching the WunderType brand
IMPROVEASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME build setting added for both Debug and Release configurations
FIXApp Store metadata updated — revised keywords, promotional text, and description to better showcase custom prompts and improve discoverability
IMPROVEClearer Error Messages
A focused fix release — error popups now tell you what went wrong instead of showing a silent warning icon.
- Error HUD now displays a clear message (e.g. \“No text selected\”, \“API key not configured\”, \“Ollama is not running\”) instead of just a warning icon
- Error HUD stays visible longer (2.5s) so you have time to read the message
- Previously, error details were only shown via macOS notifications — if notification permissions weren’t granted, you’d see a blank warning triangle with no explanation
ModernCorrectionHUD — added optional message parameter; HUD widens from 64pt to 180pt when a message is present and displays it below the icon
IMPROVEModernCorrectionHUDHost — show() and update() now accept an optional message string passed through to the HUD view
IMPROVECorrectionCoordinator — new hudMessage(for:) helper maps errors to short user-facing strings ("No text selected", "API key not configured", "Invalid API key", "Ollama is not running", "Model not found", "Insufficient credits", "Correction failed")
NEWCorrectionCoordinator — error HUD display time increased from 1.5s to 2.5s to give users time to read the message
IMPROVECorrectionCoordinator — "No text selected" now shows the error HUD with message in addition to the system notification
FIXOnboarding, Accessibility & Safety
A polish release that rounds out the v1.3 series with better onboarding, smarter corrections, and accessibility improvements.
- Onboarding now asks for notification permission before accessibility — so you’ll see error messages from day one
- If the AI returns your text unchanged, WunderType skips the replacement entirely — no more cursor flicker on already-perfect text
- The HUD and cursor spinner now respect Reduce Motion for users with vestibular sensitivity
- Unusually long AI responses are now truncated automatically to prevent runaway pastes
- Updated app icons and logo with refreshed color scheme
NotificationStep — new onboarding step that requests notification permission before the Accessibility step, with inline grant/deny handling and skip option
NEWOnboarding flow expanded from 5 to 6 steps: Welcome → Provider → Provider Setup → Notifications → Accessibility → Completion
IMPROVECorrectionCoordinator — skip AX/clipboard replacement when the AI returns text identical to the input, avoiding unnecessary cursor flicker on already-perfect text
IMPROVECorrectionCoordinator — defensive output truncation caps AI responses at max(2× input length, 50K characters) with a user notification when triggered
NEWModernCorrectionHUD — reads accessibilityReduceMotion environment and disables the .symbolEffect(.bounce) animation when Reduce Motion is enabled
FIXCursorSpinnerHost — checks NSWorkspace.shared.accessibilityDisplayShouldReduceMotion and shows a static indicator instead of the spinning progress indicator
FIXNotificationStep icon uses @ScaledMetric for Dynamic Type support, consistent with other onboarding steps
FIXUpdated app icons and logo images with refreshed color scheme for improved visual consistency
IMPROVESmarter Prompts & Clearer Settings
A quality release focused on smarter corrections. The AI follows your instructions more reliably, and the Settings screen is clearer about what each mode actually does.
- Tightened correction prompts: markdown formatting, code blocks, brand names, and the language you wrote in are now reliably preserved across every mode
- Make Concise reliably trims at least 25% — no more \“shortened\” results that came back the same length
- Settings → Mode picker now shows what each mode does in plain language, instead of leaking the internal AI prompt
- Improved reliability of the direct text-replacement path in native Mac apps
Anti-asterisk rule in commonRules — small models (gemma3:4b, gemma3:1b) had a tendency to wrap changed words in *…* to highlight edits. Explicit rule prevents this
FIXCorrectionMode prompts — refactored into shared commonRules block applied to every built-in mode. Enforces same-language responses, markdown preservation (in both directions — never strip, never add), opaque code blocks, proper-noun preservation, paragraph structure, and voice/rhythm preservation
IMPROVEMake Concise prompt — strengthened from "aim to reduce by 25–40%" to imperative "MUST be at least 25% shorter; count words and verify before responding". Validated across 4 Ollama models
IMPROVECorrectionMode — new user-facing description property; Settings Mode picker now shows plain-language descriptions instead of the raw multi-paragraph system prompt
IMPROVEAccessibilityService — added per-PID focused-element fallback when AXUIElementCreateSystemWide() returns -25204. Recurring macOS 15 quirk when AX is queried from a global-shortcut handler; the per-PID query is more reliable for non-sandboxed builds
IMPROVEAccessibilityService — new frontmostApplicationPID() helper to safely fetch the PID on MainActor (NSWorkspace is MainActor-isolated) before dispatching AX work to Task.detached
IMPROVEdocs/BUILD_DMG.md — documented the leftover sandbox container gotcha. Flipping ENABLE_APP_SANDBOX from YES to NO leaves ~/Library/Containers/com.wunderpen.app behind, and macOS keeps routing preference I/O through the container until it's manually deleted
IMPROVEdocs/STREAMING_RESEARCH.md — captured the streaming-insertion spike as research. Validated the technical approach but not shipped due to App Store sandbox restrictions on cross-process Accessibility writes. Code removed from the binary; design and findings preserved for revival if Apple ever permits sandboxed AX text manipulation
IMPROVEAccessibility & Polish
An accessibility and code-quality update.
- Provider cards in onboarding are now proper buttons, so VoiceOver announces them correctly and reads the selected state
- Onboarding screens now respect your text-size preference — icons and pickers scale with Dynamic Type
- Modernized internal notification and concurrency code for better reliability under macOS 15
- Refactored the onboarding and HUD source files into smaller, single-responsibility modules
ProviderCard — replaced onTapGesture with Button so VoiceOver recognizes provider selection cards and announces the chosen provider via accessibilityAddTraits(.isSelected)
FIXDynamic Type — onboarding icons (lock, completion checkmark, provider-card glyph) now use @ScaledMetric so they grow with the user's text-size preference
FIXOnboarding pickers — Ollama and OpenAI model pickers switched from fixed frame(width:) to frame(maxWidth:) so long model IDs aren't truncated at larger Dynamic Type sizes
FIXOnboardingView — split the 707-line file into 10 single-responsibility files under WunderType/Onboarding/ for clearer ownership and easier navigation
IMPROVEModernCorrectionHUD — split into three files under WunderType/HUD/ and converted the static-only host types (ModernCorrectionHUDHost, CursorSpinnerHost) from final class to enum namespaces
IMPROVENotificationService — replaced closure-based UNUserNotificationCenter.requestAuthorization and add(request:) with async/await variants for better Swift 6 strict-concurrency safety
IMPROVEConcurrency cleanup — removed redundant MainActor.run wrappers from AppDelegate health polling, system-prompt generation, and onboarding poll tasks (these already inherit MainActor isolation)
IMPROVEProviderCard — collapsed the background + overlay shape pair into a single chained RoundedRectangle.fill().stroke() (modern iOS 17+ Shape API)
IMPROVEQuality & Polish
A quality and polish update that makes WunderType leaner and more reliable.
- Refreshed Settings design with a cleaner, more spacious layout
- Fixed an issue where correcting with no text selected could process unrelated clipboard content
- Internal code improvements for better maintainability
OpenAIChatClient — shared chat completions client for OpenAI-compatible providers, eliminating ~200 lines of duplicated code between OpenAIService and OpenRouterService
IMPROVEAccessibilityService — extracted focusedElement() helper to replace 5x duplicated AX boilerplate
IMPROVECorrectionCoordinator — consolidated clipboard restore logic into a single helper
IMPROVECached menu bar icon instead of recreating it on every health poll tick
IMPROVERefreshed Settings window with larger layout (580x580) and cleaner footer with version and links
IMPROVERemoved last-resort clipboard fallback that could silently correct stale clipboard content when no text was selected
FIXRemoved unused AIProvider.isAvailable property
FIXRemoved dead getSelectedTextBounds() and getSelectedTextEndPosition() methods from AccessibilityService
FIXOpenRouter Integration
WunderType now supports OpenRouter, giving you access to hundreds of AI models through a single API key. Use Claude, Gemini, Llama, Mistral, DeepSeek, and many more — all from within WunderType.
- New AI provider: OpenRouter — access hundreds of models with one API key
- Choose any model by entering its ID in Settings
- Secure API key storage in macOS Keychain
OpenRouterService — new AI provider using OpenAI-compatible chat completions API at openrouter.ai
NEWOpenRouter API key storage in macOS Keychain via KeychainHelper
NEWOpenRouter provider section in Settings with API key, model ID text field, and connection status
NEWOpenRouter error handling with user-facing notifications (rate limits, insufficient credits, invalid key)
NEWUnifiedAIService routing extended to support three providers (Ollama, OpenAI, OpenRouter)
IMPROVEDefault OpenRouter model set to mistralai/mistral-small-3.2-24b-instruct
IMPROVESecurity & Connectivity
A small but meaningful update that strengthens WunderType’s security posture and makes it easier to stay connected.
- Added quick links to changelog, website, and contact in Settings
- Under-the-hood security improvements
Implemented cooldown mechanism to prevent re-triggers from synthetic keystrokes
FIXAdded character limit validation for text correction requests
FIXAdded unit test coverage (entitlements, input validation, keychain)
NEWImproved OpenAI API key handling in settings
IMPROVEUpdated sandbox entitlements for network access
IMPROVEReliability & Security Improvements
We’re constantly working to make WunderType better for you. This update focuses on reliability and security, making WunderType more robust for everyday use.
- Fixed an issue where the correction shortcut could trigger twice
- Improved security for API key storage
- Crisp, high-resolution logo on Retina displays
- General stability and reliability improvements
Correction shortcut could fire twice due to synthetic keystroke re-triggers — added cooldown mechanism
FIXCharacter limit enforcement on text correction requests to prevent oversized API calls
FIXMore secure OpenAI API key handling
IMPROVEBetter task cancellation and isProcessing state management in CorrectionCoordinator
IMPROVEAI response character count now logged for diagnostics
NEWUnit testing target and scheme configuration
NEWRebranded to WunderType
This update includes performance improvements, visual enhancements, and bug fixes to ensure the best possible experience.
- Improved performance and stability during text corrections
- Sharper, high-resolution logo on Retina displays
- Smoother onboarding experience with automatic accessibility permission handling
- Updated app icon and menu bar icon
- General bug fixes and improvements
Renamed from WunderPen to WunderType across the entire project
NEWReplaced all Task.sleep(nanoseconds:) calls with Duration-based API for clarity
IMPROVEHigh-resolution logo assets (1x + 2x) for crisp rendering on Retina displays
IMPROVEOnboarding now automatically requests Accessibility permission on appear
IMPROVECleaned up unused duplicate service files from old WunderPen structure
FIXLive Cursor Indicator
A big step forward for the correction experience — WunderType now shows a native spinning cursor while your text is being processed, so you always know it’s working.
- Live spinning cursor indicator during text corrections
- Refreshed app icon and menu bar icon
- Streamlined AI model selection
- Smoother settings experience with smarter API key input
CursorSpinnerHost — native spinning cursor indicator shown during text correction
NEWAccessibilityService methods to retrieve selected text and its screen bounds
NEWDebounce logic for OpenAI API key input in Settings
NEWSimplified OpenAI model list to gpt-4o-mini only (removed deprecated models)
IMPROVERefactored AppDelegate to use dedicated MenuBarIcon for the status item
IMPROVECorrectionCoordinator cancels ongoing tasks more efficiently
IMPROVECursorSpinnerHost safety timeout set to 150 seconds
IMPROVEWelcome to WunderType!
Welcome to WunderType! Correct and improve your text anywhere on your Mac with a single keyboard shortcut.
- System-wide text correction powered by AI
- Works with OpenAI or local Ollama models
- Guided onboarding to get you set up in minutes
- Secure API key storage in macOS Keychain
- Lives in your menu bar — lightweight and always ready
System-wide text correction via keyboard shortcut
NEWOpenAI and Ollama AI provider support
NEW5-step onboarding flow
NEWmacOS Keychain integration for API key storage
NEWAccessibility API text replacement with clipboard fallback
NEWMenu bar agent (no Dock icon)
NEWApp Sandbox with network client entitlement
NEW