Skip to content

Releases: hoangperry/hp-translator

v1.0.0 — First stable release

Choose a tag to compare

@hoangperry hoangperry released this 26 May 22:40

First stable release 🎉

v1.0.0 is a commitment to API + UX stability, not a re-implementation. The bundle is byte-identical in behaviour to v0.11.1 — same code path, same backend wire format, same prompts, same hotkey UX. The 1.0.0 number marks the boundary where the app is feature-complete + production-hardened.

What 1.0.0 covers

  • Feature set is complete for every workflow the app was built for:
    • Inbound translate (selection → user's primary language)
    • Outbound translate (per-persona, per-tone)
    • Contextual rewrite (per-tone bindings + tone picker)
    • OCR capture (screen region → translate)
    • App Intents (Shortcuts.app + Spotlight + Siri)
    • Prompt Engineer mode (VN keywords → EN prompt for AI coding assistants)
  • Dual-mode translation backend:
    • Bring-your-own-API direct providers: Gemini, OpenAI-compatible, Ollama, DeepL, Google Translate, LibreTranslate
    • SaaS-hosted Contextual MT Cloud with quota / device cap / per-user encrypted cache
  • Distribution pipeline is hardened end-to-end: Apple Developer-ID signed, Apple-notarized, stapled, AppleDouble-clean ZIP + DMG, Sparkle EdDSA auto-update, GitHub Pages workflow deploy for the appcast.
  • 400 tests guard the public contract.

What 1.0.0 means going forward

Bump When
Patch (1.0.1, 1.0.2, …) Bug fixes
Minor (1.1.0, 1.2.0, …) Backward-compatible feature additions
Major (2.0.0) Breaking changes to UserDefaults / Keychain layout / backend wire format

The on-disk layout (UserDefaults keys, Keychain accounts, file names, signing identifiers) and the backend wire format (direction field values, dual-emit body shape, snake_case + camelCase parity) are now considered stable surface area.

No behaviour changes vs v0.11.1

Just the version string + this entry. v0.11.0 → v0.11.1 → v1.0.0 upgrade is the cheapest in the app's history.

Install

  • Recommended: download the DMG below — open, drag the app into Applications, launch.
  • Sparkle auto-updates from v0.10.3+ pull this in within an hour.

Both DMG and ZIP are signed (Developer ID JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.

Thanks

The 6 releases shipped today (v0.10.1 → v1.0.0) closed the long tail of issues that kept the app in alpha: SaaS backend wire format, AppleDouble Gatekeeper trap, TCC permission recovery, transparent-titlebar regression, Input Monitoring removal, SaaS rewrite + prompt expansion, pronoun preservation. The app is ready for daily use.

v0.11.1 — Rewrite pronoun preservation

Choose a tag to compare

@hoangperry hoangperry released this 26 May 21:54

Rewrite pronoun-preservation hotfix

User-reported bug: a draft addressed with "em" was coming back as "anh/chị" (or vice versa) after a rewrite, inverting the entire social relationship between speaker and addressee. In Vietnamese pronouns ARE the relationship — switching them is a correctness failure, not a style choice.

Root cause

The pronoun-preservation rule was a single buried line in a bullet list AND two of the few-shot examples in the prompt itself silently switched "tôi" → "em", actively teaching the model the opposite of what the rule said. The Supabase server prompt didn't mention Vietnamese pronouns at all.

Fixed — Mac app PromptBuilder.rewriteSystemPrompt

  • Pronoun-preservation rule promoted from a bullet to an ABSOLUTE rule — PRONOUNS: top-level section with explicit per-pronoun guarantees: "em" stays "em", "tôi" stays "tôi", "mình" stays "mình", every addressing pronoun (anh / chị / em / cháu / bạn / mình / quý khách / ông / bà / thầy / cô) preserved verbatim.
  • Example #2 fixed: input uses "tôi" → output now keeps "tôi" (was wrongly outputting "em").
  • Example #3 fixed: input uses "tôi" → output now keeps "tôi" (was wrongly outputting "em").
  • New example #4 demonstrates "em → anh" fidelity across a friendly tone shift.
  • Other-language formality markers ("tu"/"vous", "tú"/"usted", "你"/"您", "ты"/"вы") added so the fix isn't VN-only.

Fixed — Supabase REWRITE_SYSTEM_PROMPT (already deployed)

  • Server-side prompt was about a third the size of the Mac-side prompt and had no pronoun rule at all. Now mirrors PromptBuilder verbatim so SaaS users get the same pronoun-preserving quality as direct-API users.

Tests

  • rewriteSystemPromptHasFewShot updated to pin the ABSOLUTE pronoun-rule heading + per-pronoun guarantee strings so a future prompt refactor cannot quietly drop the protection. 400 tests pass.

Risk-free upgrade

UserDefaults + Keychain layout unchanged from v0.11.0.

Install

  • Recommended: download the DMG below — open, drag the app into Applications, launch.
  • Sparkle auto-updates from v0.10.3+ work.

Both DMG and ZIP are signed (Developer ID JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.

v0.11.0 — Prompt Engineer mode

Pre-release

Choose a tag to compare

@hoangperry hoangperry released this 26 May 21:03

Prompt Engineer mode

Type a minimal Vietnamese keyword sketch of a coding task. Press a hotkey. The app pastes back a complete, well-structured English prompt ready for Claude Code, Codex, ChatGPT, or Claude Desktop.

Same hotkey machinery as translate/rewrite — different system prompt, different temperature, different intent. This release makes the use case documented at contextmt.dev/recipes/prompt-expander first-class shipping infrastructure.

Added

  • TranslationDirection.expand enum case + new PromptBinding data type (id, name, hotkey, targetLanguage default en, styleInstruction defaults to shared template when blank).
  • Settings → Prompt Engineer section parallel to Contextual rewrite. Per-binding rows: name, target language, optional custom expansion guidelines, hotkey recorder, delete. Gated on rewriteAvailable.
  • TranslationWorkflow.expandAndSend(binding:) — capture selection → translate with direction = .expand → always preview (output is N× longer than input) → paste on confirm.
  • AppDelegate registers settings.promptBindings on the same gate as rewrite bindings.
  • PromptBuilder.expandSystemPrompt mirrors the Supabase EXPAND_SYSTEM_PROMPT so direct-API providers (Gemini direct, OpenAI direct) get the same expansion quality without a backend round-trip.

Server side (already deployed)

  • Supabase translate Edge Function accepts direction: "expand", swaps to EXPAND_SYSTEM_PROMPT, bumps temperature 0.3 → 0.6, and drops the Source language line from the user prompt. Cache key includes direction so expand / rewrite / translate of the same input get separate slots.

Tests

  • +10 new testsPromptBindingTests (7: Codable, default fallback, custom precedence, style() pinning, persistence, conflict detection), ExpandDirectionWireFormatTests (2: raw value + JSON shape), BackendStreamingTests (1: end-to-end direction=expand reaches wire body).
  • 400 tests pass (was 390).

Risk-free upgrade

UserDefaults adds one new key (translator.promptBindings) defaulting to empty array. Users opt in by adding a binding in Settings → Prompt Engineer. No migration needed.

Install

  • Recommended: download the DMG below — open, drag the app into Applications, launch.
  • Sparkle auto-updates from v0.10.3+ work.

Both DMG and ZIP are signed (Developer ID JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.

How to use

  1. Settings → Prompt Engineer → Add binding. A default hotkey (⌥P) is suggested; change with the recorder if it collides.
  2. Optionally open Custom expansion guidelines to override the default template (e.g. pin a specific tech stack: "I work in Swift 6 + SwiftUI on macOS 14+").
  3. From any chat input field (Claude Code, Codex web UI, ChatGPT, etc.), type a minimal sketch in Vietnamese — fix bug login keychain swift, prompt password lặp — then press ⌥P. The PreviewHUD shows the expanded prompt; confirm to paste.

v0.10.6 — Rewrite on SaaS backend

Pre-release

Choose a tag to compare

@hoangperry hoangperry released this 26 May 20:18

Rewrite works on SaaS backend

User-reported bug: pressing a tone rewrite hotkey (Polite / Professional / Friendly etc.) did nothing when the app was configured to use Contextual MT Cloud or a self-hosted backend. The hotkey registration was silently skipped because the historical rewriteAvailable gate hardcoded "only direct-API providers can rewrite".

Fixed — Mac app

  • SettingsStore.rewriteAvailable now returns true for both customBackend and firstPartyBackend translation sources. Direct API providers stay gated on each provider's individual supportsRewrite flag.

Added — SaaS backend (already deployed)

  • The Supabase translate Edge Function accepts a new direction: "rewrite" field that switches Gemini to a same-language tone-rewrite system prompt and drops Source/Target language lines from the user prompt (otherwise the model would translate VN→VN instead of adjusting tone).
  • Backward-compatible: any other direction value (including missing or the older "inbound" / "outbound" raw values) lands on the regular translate path.
  • Cache key includes direction so a translate-of-X never returns a previously-cached rewrite-of-X.

Internal

  • Refactored the v0.10.4 auto-open Settings tests to drive the decision synchronously via a new checkAutoOpenSettings() method on PermissionManager. Production code still schedules a 1.5 s grace task; tests just exercise the decision body deterministically (the original async version raced the cooperative scheduler in full-suite runs).
  • 390 tests pass.

Risk-free upgrade

UserDefaults + Keychain layout unchanged from v0.10.5.

Install

  • Recommended: download the DMG below — open, drag the app into Applications, launch.
  • Sparkle auto-updates from v0.10.3 onwards work.

Both DMG and ZIP are signed (Developer ID JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.

v0.10.5 — Window chrome hotfix

Pre-release

Choose a tag to compare

@hoangperry hoangperry released this 26 May 19:22

Window chrome hotfix

Three app windows (Onboarding, Settings, What's New) carried a transparent-titlebar / clear-background combination from an earlier macOS 26 "Liquid Glass" experiment. On real macOS, the title text rendered straight onto the desktop wallpaper with zero contrast — unreadable. User-visible symptom: top bar fully transparent, app title overlapping window controls and desktop content.

Fixed

  • OnboardingWindowController, SettingsWindowController, WhatsNewWindowController — reverted the transparent-titlebar settings. Standard opaque titlebar restored on all three windows. The SwiftUI content's liquidGlassBackground modifier still applies its material to the content area on macOS 26; only the titlebar chrome changes.

Tests

  • Bumped two PermissionManager test sleeps 2s → 3s to absorb scheduler jitter that produced intermittent zero-count failures on the auto-open-Settings contract added in v0.10.4. 390 tests pass.

Risk-free upgrade

UserDefaults + Keychain layout unchanged from v0.10.4.

Install

  • Recommended: download the DMG below — open, drag the app into Applications, launch.
  • Sparkle auto-updates from v0.10.3 onwards work.

Both DMG and ZIP are signed (Developer ID JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.

v0.10.4 — Permission UX cleanup

Pre-release

Choose a tag to compare

@hoangperry hoangperry released this 26 May 19:03

Permission UX cleanup

Drops Input Monitoring from the onboarding and Settings surface entirely — the app never actually needed it, and the Request button could not recover after the first denial (macOS silently suppresses the prompt forever once dismissed).

The real fix: this app does not need Input Monitoring at all. Carbon RegisterEventHotKey (global hotkeys) and CGEvent posting (paste-at-caret) both run on Accessibility alone.

Removed

  • Input Monitoring permission row from onboarding and Settings.
  • PermissionManager.inputMonitoringGranted + requestInputMonitoringIfNeeded deleted. If a future feature genuinely needs CGEvent tap, the probe + request closure pattern is easy to add back.

Improved

  • Auto-open Settings fallback for Accessibility — when requestAccessibilityIfNeeded() fires and the prompt is silently suppressed (because the user previously denied), a 1.5-second grace task re-checks the live grant. If still not granted, System Settings opens to the Accessibility pane automatically. No more "I clicked Request and nothing happened" dead end.

Build pipeline

  • scripts/package_dmg.sh now codesigns the DMG wrapper with Developer ID before notarization. v0.10.3's manually-built DMG had this; the script did not.

Tests

  • PermissionManagerTests drops 2 Input Monitoring assertions and adds 2 new ones pinning the auto-open Settings contract: fires when grant misses the grace window; skipped when grant arrives in time. 390 tests pass (was 388, +2).

Risk-free upgrade

UserDefaults + Keychain layout unchanged from v0.10.3.

Install

  • Recommended: download the DMG below — open, drag the app into Applications, launch.
  • Sparkle auto-updates from v0.10.3 onwards work too (pulls the ZIP).

Both the DMG and the ZIP are signed (Developer ID JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.

v0.10.3 — Distribution hotfix (Gatekeeper acceptance)

Choose a tag to compare

@hoangperry hoangperry released this 26 May 17:05

Distribution hotfix — Gatekeeper acceptance

v0.10.0, v0.10.1, and v0.10.2 all shipped distribution ZIPs that, on the user's machine, failed Gatekeeper verification with "sealed resource is missing or invalid" — surfacing as "app chưa sign" when Sparkle tried to install an update.

The bug: hidden AppleDouble metadata files (the ._* files) inside the embedded Sparkle.framework, introduced by SwiftPM's xcframework extraction on macOS. They were invisible to codesign --sign (which signs the real files), so the build chain succeeded — but post-extraction codesign --verify --deep --strict treated them as foreign content and rejected the bundle.

v0.10.3 carries zero application code changes — it is purely the v0.10.2 codebase re-packaged with the build pipeline fixed.

How to install

Recommended: download the v0.10.3 ZIP from this page — Sparkle on a v0.10.0 install that already aborted a v0.10.1/v0.10.2 update is likely in an inconsistent state. Unzip, drag the .app into /Applications replacing the old copy, run it. Future Sparkle updates (v0.10.4 onwards) install cleanly through Sparkle.

If Sparkle's Check for Updates does work — even better. The v0.10.3 bundle is correctly signed + notarized + stapled, and the distribution ZIP no longer contains the offending metadata.

Build pipeline fixes

  • scripts/package_app.sh strips ._* files BEFORE and AFTER the codesign chain (defense in depth).
  • The script's final step runs codesign --verify --deep --strict as an invariant check — if a future toolchain combination re-introduces the problem, the build fails loudly.
  • The distribution ZIP is now created with ditto --norsrc --noextattr --noacl so that ditto does not re-pack metadata into the ZIP via xattr packaging.

Verified end-to-end

$ unzip Contextual-Mac-Translator-v0.10.3-macos-arm64.zip
$ find "Contextual Mac Translator.app" -name "._*" | wc -l
0
$ codesign --verify --deep --strict --verbose=2 "Contextual Mac Translator.app"
valid on disk; satisfies its Designated Requirement
$ spctl --assess --type execute --verbose "Contextual Mac Translator.app"
accepted; source=Notarized Developer ID
$ xcrun stapler validate "Contextual Mac Translator.app"
The validate action worked!

Apple Silicon only. macOS 14 or later.

v0.10.2 — Permission UX & TCC stability

Choose a tag to compare

@hoangperry hoangperry released this 26 May 11:29

Permission UX & TCC stability

A user-reported pain point: every Sparkle upgrade felt like macOS was clearing the app's Accessibility grant — translate hotkeys went silent until you manually re-granted, with no in-app surface telling you what happened.

v0.10.2 fixes the user experience and hardens the signature so the underlying TCC issue is much less likely to recur in the first place.

Added — Auto-recovery onboarding

  • Permission-loss detection on launchAppDelegate reads the persisted lastKnownAccessibilityGranted flag before refreshing the live grant, so the true→false transition that signals a TCC reset is detectable. When detected, the onboarding window pops automatically in recovery mode with copy that acknowledges you already did this once.
  • OnboardingMode.permissionRecovery swaps the title + intro: "Welcome back. macOS cleared the app's Accessibility grant after the recent update — translate hotkeys are silent until you re-grant. Click Request below, or Open Settings if the prompt does not appear."
  • Hotkeys still register in recovery mode so any grants that survived (e.g. Input Monitoring) keep working — the onboarding pops on top so the issue cannot be missed.

Hardened — Codesign stability

  • --identifier "app.lookerlab.translator" is now passed explicitly to the main-app codesign call instead of being inferred from CFBundleIdentifier. The value baked into the signature is byte-stable across releases. Verified post-build: v0.10.0, v0.10.1, and v0.10.2 all sign with byte-identical designated requirements, so TCC should persist the grant across upgrade.

Internal — PermissionManager testability

  • Constructor accepts injected accessibilityProbe / inputMonitoringProbe / requestAccessibilityAction / requestInputMonitoringAction closures so unit tests exercise grant-sync wiring without touching the real TCC database.
  • refresh() writes the live grant back to SettingsStore.lastKnownAccessibilityGranted (guarded by didSet so the 1-second polling loop does not spam UserDefaults).
  • refreshLater() settle delay bumped 1s → 2s — gives the TCC database room after the user clicks "Allow".

Tests

  • +11 new tests — 6 in PermissionManagerTests (grant-sync, init non-mutation, request action, nil-settings), 5 in LaunchRecoveryDecisionTreeTests (fresh / steady / revoked / never-granted / lost-then-regranted).
  • 388 tests pass (was 377).

Risk-free upgrade

UserDefaults adds one new key (translator.lastKnownAccessibilityGranted) defaulting to false on existing installs — so the recovery flow does not fire spuriously on the v0.10.1 → v0.10.2 upgrade itself. The first refresh writes the current live state forward; from v0.10.3 onwards the recovery signal is armed end-to-end.

Install

  • Sparkle — existing installs receive this automatically via the appcast at https://hoangperry.github.io/hp-translator/appcast.xml.
  • Fresh install — download Contextual-Mac-Translator-v0.10.2-macos-arm64.zip below, unzip, drag the .app to /Applications. Signed (Developer ID JP4XV8A9JF) + notarized + stapled.

Apple Silicon only. macOS 14 or later.

v0.10.1 — SaaS backend hotfix

Pre-release

Choose a tag to compare

@hoangperry hoangperry released this 26 May 10:51

SaaS backend hotfix

v0.10.0 shipped right as the production SaaS Edge Function at app.contextmt.dev went live, and the two wire formats turned out to be incompatible. Translates failed immediately with HTTP 400: target_language is required; once that cleared, a second failure surfaced — the streaming flow expected SSE on /translate/stream, but Supabase returns one-shot JSON, so the stream loop never saw a data:-prefixed frame and threw "The backend response did not include a translation."

Both fixes are backward-compatible with the legacy self-hosted FastAPI server.

Fixed

  • Dual wire formatBackendRequestBody now emits every routing field under both camelCase (legacy self-hosted FastAPI in translator-server/server.py) and snake_case (SaaS Supabase Edge Function). One payload, both backends. Pinned by a new bodyEmitsDualNamingConventions contract test so a future refactor cannot silently re-break one backend.
  • Streaming non-SSE fallbackBackendProvider.streamSSE sniffs Content-Type; if it's not text/event-stream (Supabase returns application/json), the body is drained and decoded as a one-shot TranslationResult then emitted as a single .done(...) update. Self-hosted SSE keeps its real streaming UX. Pinned by nonSSEJSONFallback test.

Tests

  • 377 tests pass (was 376; +1 fallback test).
  • Removed a stale "0.10.0" entry from WhatsNewHighlightsTests.unknownVersionReturnsNil — v0.10.0 added its own highlight set so the value moved out of the catch-all bucket.

Risk-free upgrade

UserDefaults + Keychain layout byte-identical to v0.10.0. Existing settings, hotkey bindings, register card, and glossary entries all round-trip clean.

Install

  • Sparkle — existing v0.10.0 installs will receive this update automatically via the appcast at https://hoangperry.github.io/hp-translator/appcast.xml.
  • Fresh install — download Contextual-Mac-Translator-v0.10.1-macos-arm64.zip below, unzip, drag the .app to /Applications. Signed (Developer ID JP4XV8A9JF) + notarized + stapled.

Apple Silicon only. macOS 14 or later.

v0.10.0 — Cultural Precision & Privacy (VN Register + Privacy badge + Glossary v2)

Choose a tag to compare

@hoangperry hoangperry released this 25 May 16:47

Cultural Precision & Privacy

The first feature-loaded minor since v0.9.0. Three new sub-features anchored on "Apple Intelligence can't do per-locale Vietnamese precision" — pin your social register, see the privacy class of every translation, and replace the free-text glossary blob with typed entries the LLM follows exactly. Risk-free upgrade — 375/79 tests stay GREEN, every v0.9.x setting + binding + hotkey + glossary blob round-trips byte-identical, Sparkle OTA from v0.6.1+ users auto-updates with no manual action.

Added — VN Social Register Card (anchor)

  • RegisterCard with 3 axes — Dialect (Bắc/Nam), Kinship (anh/chị/em/cháu/bạn), Formality (formal/neutral/casual) — plus optional roleHint ("e.g. TikTok Shop seller addressing customer").
  • Settings → Contextual Rewrite → Vietnamese register card — 3 Picker dropdowns, roleHint TextField, Reset button, live composed-prompt preview so you see exactly what the LLM receives.
  • Composition is prepend, not replace — your per-binding tone instructions flow through unchanged below the [Tone] tag.
  • nil = disabled (default) → v0.9.x behaviour byte-identical for users who never open the panel.

Added — Local-LLM Privacy mode

  • ProviderPrivacyClass enum (.local / .cloud / .hosted) — every provider classifies explicitly. Ollama = .local, BackendProvider = .hosted, everything else (Gemini direct + CLI, Codex CLI, DeepL, Google Translate, LibreTranslate, OpenAI-compatible) = .cloud.
  • PreviewHUD Privacy badge — every translation surfaces the provider's class (🛡 Local / ☁ Cloud / 🏢 Hosted) with .help() tooltip showing the full provider name. Stamped eagerly into TranslationStyle at workflow construction so SwiftUI render never reaches back into providerFactory().
  • Settings → Privacy section — active-provider ribbon with Vietnamese headline ("🛡 Local only — không gửi dữ liệu khách ra nước ngoài" when Ollama is active), collapsed Ollama onboarding card with download link + two curated ollama pull commands (each with a Copy button) + Browse-more-models link + one-click "Test Ollama connection" button (2s timeout, exact-name model match via /api/tags JSON parse, inline green/red status).

Added — Glossary v2-Lite (typed entries)

  • GlossaryEntry Codable with 3 kinds — .dontTranslate(term:) for brand names / code identifiers, .alias(from:to:) for casing & spelling normalisations, .alwaysTranslate(term:to:) for jargon.
  • Forward-compatible partial recovery — unknown KindTag from a future v0.10.x is dropped at the element level (not the whole list) via GlossaryEntry.decodeArray(from:). A user who briefly opens v0.10.1, adds one .scoped entry, then downgrades, keeps every other v0.10.0 entry intact.
  • GlossaryComposer pure function composes typed entries + legacy free-text blob. dontTranslate grouped under one bullet; alias + alwaysTranslate render one-per-line directional pairs. 50-entry render cap.
  • Settings → Glossary editor — Add-menu for the 3 kinds, type pill tinted per kind, 1-or-2 TextFields per row, delete button, drag-to-reorder via .onMove. Legacy free-text TextEditor moved to a collapsed DisclosureGroup below; rules apply structured-first.

What's-New on first launch

A small window pops once for v0.10.0 upgrades with 3 highlight cards (Register Card / Privacy badge / Glossary v2). Doesn't steal focus — click "Got it" to dismiss.

Compatibility

  • Every v0.9.x persisted state round-trips clean: RewriteBinding, HotkeyConfig, TranslationStyle, SaaSConfig, legacy glossary string blob — all unchanged in storage.
  • TranslationStyle gains 3 default-safe fields (registerCard: nil, privacyClass: nil, providerDisplayName: "").
  • macOS 14 minimum preserved.

Install

  • Sparkle OTA: v0.6.1+ users auto-update from inside the app — no action needed.
  • Fresh install: download the DMG below → drag to /Applications.

Verify

  • App: 375 Swift / 79 suites GREEN (+56 from v0.9.2's 319).
  • Build: signed (Developer ID), notarized + stapled (DMG + Sparkle zip), EdDSA-signed appcast.

Deliver-phase code review

Independent code-review agent before tag: 0 CRITICAL / 3 HIGH / 4 MED / 2 LOW. All 3 HIGH + 3 MED fixed inline (commit 285f519):

  • H1SettingsPrivacySection was allocating TranslationProviderFactory on every SwiftUI render. Cached in @State, refreshed only on provider-change.
  • H2 — Force-unwrapped URL literals in body position. Lifted to static let constants.
  • H3 — Glossary array decode was all-or-nothing; one unknown KindTag from a future v0.10.x would nuke the user's whole list. Switched to per-element partial-recovery decode.
  • M1 — Concurrent taps of "Test Ollama connection" could race on testResult. Added isTesting guard.
  • M2RegisterCard.prompted was appending raw prefix instead of trimmed prefix, opening a double-blank gap when callers passed trailing whitespace.
  • M3 — Ollama /api/tags model match was substring (bodyText.contains(...)) which gave false positives. Switched to JSON-parsed exact-name match.

Deferred to v0.10.1: M4 (stamp() helper duplication between TranslationWorkflow + CaptureOrchestrator), L1 (roleHint character-vs-byte cap clarification), L2 (LibreTranslate self-hosted shows ☁ Cloud despite localhost endpoint).

Roadmap (next)

  • v0.10.1 patch backlog: M4 + L1 + L2 above, per-binding register-card override toggle (carried from v0.10.0 deferred list).
  • v0.11.0 candidates: voice input via WhisperKit (Moonshine-Medium is production-ready for VN by 2026), Glossary v2 scoped entries (per-language + per-app).
    EOF