Skip to content

v1.0.17

Choose a tag to compare

@owenpkent owenpkent released this 09 May 00:46

Rolls up everything that was sitting in Unreleased plus the auto-update relauncher fix that was prepared as 1.0.16 but never published. Headlines: opt-in telemetry pipeline scaffolded (off by default), single-section Analytics dashboard, prediction pills mirror typed capitals onto every candidate (not just strict-prefix matches), Backspace double-fire boundary widened from ~620 ms to ~800 ms, hyphen no longer eats the preceding word on prediction click, plus the auto-update relauncher fix that gives you back a keyboard after silent updates.

Fixed

  • Clicking a prediction after a hyphen no longer eats the word before the hyphen. Reported as: typing "word1-word2", then clicking a suggestion for "word2", deleted both "word1" and the hyphen. Root cause: hyphen wasn't a word boundary for _current_word tracking, so after typing "word1-wo" the bridge thought the typed prefix was the whole 8-character "word1-wo". Clicking a suggestion for "world" failed the case-sensitive prefix match in pressPrediction ("world".startswith("word1-wo") is False), fell through to the replace_text clobber path, and BackSpaced 8 characters off the screen — wiping "word1-" along with the partial "wo". Hyphen now resets _current_word the same way comma / semicolon / colon do, but without the auto-trailing-space those add (the user types "word1-word2" with no space after the hyphen, unlike "Hello, world"). The character on screen is preserved (it was already sent before the boundary logic runs); only the prediction state changes. Same fix extended to /, \, (, [, {, < for the same bug class — paths, URLs, and bracketed expressions hit the same trap. Apostrophe is deliberately excluded so contractions like "don't" stay as one token. Three regression tests in TestContextTracking (hyphen reset, suffix-only path on the second word, slash spot-check).
  • Analytics dashboard "Top Words" bar chart now scales bars to relative frequency. Previous formula was width = clamp(count * 3, 4, 60), which hit the 60 px cap the moment a word was used 20+ times. For top words like "and" / "to" / "the" with counts in the thousands, every bar saturated and the chart drew identical-length bars. Now scales proportionally to the #1 word's count: top word fills the full 80 px slot, the rest scale to slot_width * count / max_count. The opacity gradient (each rank ~15 % more transparent than the one above) still provides a secondary by-rank cue.
  • Prediction pills now mirror typed capitals onto fuzzy / autocorrect candidates, not only onto strict-prefix completions. Reported as "if I type a word with a capital letter, it's not capitalized... only sometimes." Root cause: KeyboardBridge._display_cased mirrored typed uppercase positions onto each pill only when the pill's lowercase form strict-prefix-matched the typed letters. Spelling-correction candidates from the fuzzy recogniser routinely don't strict-match (typing "Hwl" for "Hel" surfaces "hello"; "hello".lower() doesn't start with "hwl"), so on every typo the cap was silently dropped and the pills came back lowercase. The mirror now runs unconditionally: typed uppercase positions are reflected onto every pill, prefix-match or not. "Hwl" → "Hello", "Heilo" → "Hello", "HEilo" → "HEllo". Two regression tests in TestCapsLockDisplayCasing.
  • Backspace double-fire boundary widened from ~620 ms to ~800 ms. Reported as "sometimes pushing backspace does two backspaces". The KeyButton.qml repeat timer fired its first auto-repeat keystroke repeatInterval (120 ms default) after the warm-up tick at repeatDelay (500 ms default), so any press held between 500 ms and 620 ms produced exactly two keystrokes. Slow-motor users systematically tipped into that window. Added a third timer phase: after the warm-up tick, schedule the first auto-repeat at warmUpGrace (300 ms default) instead of repeatInterval. Once auto-repeat is genuinely engaged the cadence stays at the configured repeatInterval, so bulk-delete speed is unchanged. Any press shorter than repeatDelay + warmUpGrace (default 800 ms) now produces exactly one keystroke.
  • Auto-update left the user with no keyboard. Reported as: "the new keyboard never opens" after a successful update. Root cause: the relaunch in installer.nsh::customInstall (Exec '"$WINDIR\explorer.exe" "$INSTDIR\alpha-osk.exe"') is fire-and-forget across an integrity-level boundary. The elevated installer asks user-IL explorer.exe to spawn the new exe, and Windows can refuse that handoff silently on some configurations (anti-malware, Group Policy, AppLocker, or just the default IL rules around an elevated parent spawning user-mode children). With no error surface, the OSK simply never came back. New mechanism: a detached user-IL relauncher process spawned by the updater before the installer fires. Because the helper is launched while we're still running at user IL (before UAC elevation), it inherits a user-mode token and there is no IL handoff to fail. Flow: (1) download_and_install re-invokes ourselves with --update-relauncher + DETACHED_PROCESS flags, (2) the installer fires and taskkills the OSK, (3) the helper polls for our exit, waits for the installer's file copy, then launches the freshly-installed alpha-osk.exe directly via subprocess.Popen from the user session. The in-installer Exec explorer.exe trick stays as a fallback, but the helper is now the primary mechanism. New module: src/_update_relauncher.py. New CLI dispatch in src/keyboard_app.py::main for --update-relauncher (skips singleton lock + QApplication setup since the helper doesn't open a window). 14 unit tests in tests/test_update_relauncher.py covering process polling, mtime-newer-than-parent-death checks, launch failure handling, and the full happy-path flow.

Added

  • Opt-in telemetry pipeline scaffolded (off by default, no endpoint configured yet). New Settings → Privacy section with a "Share anonymous usage stats" toggle and a "Delete my contributed data" two-step button. When enabled, the client sends a weekly POST containing nine integer counters (the same lifetime numbers shown on the Analytics dashboard: keystrokes, words, predictions, keystrokes_saved, minutes, sessions, prediction_offers, plus a UUID4 anon_id, app_version, and os string). Never sent: content, word frequencies, key frequencies, IP, hostname, or any per-session breakdown. anon_id is generated on first opt-in and cleared on opt-out so re-opt-in cycles cannot be linked. Privacy mode counters are already excluded at the analytics layer, so password-field activity never enters the totals. Backend lives in backend/cf-worker/ (Cloudflare Worker + D1) with POST /v1/submit, GET /v1/aggregate, POST /v1/forget, plus a daily cron that prunes users inactive for >365 days. Endpoint URL (DEFAULT_ENDPOINT in src/telemetry.py) is currently empty, so even with the toggle on no data leaves the machine; will be set per-build once the worker is deployed and the schema is verified against real submissions. Full design in docs/TELEMETRY.md; user-facing data policy in docs/PRIVACY.md. 26 regression tests in tests/test_telemetry.py cover consent gating, anon_id lifecycle, weekly cadence, payload shape + clamping, retry on 5xx/429/network error, and on-quit submission.

  • Post-update confirmation toast. After an auto-update completes, the freshly-launched OSK flashes "✓ Updated to v1.0.17 from v{prior}" at the top of the window for 4 seconds. The relauncher writes update_handoff.json (version, previous_version, completed_at) to $APPDATA/alpha-osk/ after launching the new OSK. On startup, Main.qml::Component.onCompleted calls keyboard.consumeUpdateHandoff() which reads + deletes the file (single-use breadcrumb) and returns the version pair. If non-empty, the new updateAppliedToast Popup flashes. Five-minute freshness window. Anything older is treated as no handoff, since the user has obviously been using the new build for a while. Privacy: the breadcrumb only contains version strings, no user data. 5 bridge tests in tests/test_keyboard_bridge.py::TestUpdateHandoffConsumption.

Changed

  • Analytics dashboard collapsed into a single section, composite Quality Score removed. Previous layout layered four visually distinct subsections (green-bordered hero card showing "X keystrokes saved", three-pill row of words/sessions/hours, horizontal divider, Lifetime/Session toggle, then the 2x2 stat grid) which read as four disconnected views of the same data. Collapsed to one section: scope toggle + a 2x2 grid of Keystrokes Saved, Time Saved, Effort Saved, and Acceptance (= prediction picks ÷ prediction offers). The composite 0-100 Prediction Quality Score is gone — a single weighted number that didn't tell the user which lever to pull. Time Saved is now derived from the user's own keystroke pace (keystrokes_saved × seconds_per_keystroke, fallback 0.5 s/key for new installs) rather than a constant, so the number reflects their actual experience. New persisted top_pick_count counter tracks rank-1 prediction picks (incremented inside record_prediction_selected only when rank == 1); surfaced in getAnalytics() as topPickRate / alltimeTopPickRate and consumed by the Model Visualization Dashboard's stat-card row in place of the old Quality value. Analytics section moved to the top of Settings, since lifetime savings is the most rewarding read on every open. The dashboard's root Item now sizes to its content (was hardcoded 460 px in the panel), so This Session view no longer leaves a tall empty band above the tile grid when the sparkline / top-words sections collapse to zero height. Subtext colour brightened from #666 to #aaa and bumped from 9 px to 10 px for readability. 11 regression tests in tests/test_analytics.py cover top-pick increment, time-saved math (incl. fallback pace), backwards-compatible load of pre-this-change analytics.json files, and confirmation that the quality score is fully gone.
  • Settings → Input UX cleanup. "Hold-to-Repeat Delay" renamed to "Wait before auto-repeat" and "Hold-to-Repeat Rate" renamed to "Time between repeats". The "Rate" label conflicted with its ms unit, and its +/ direction (+ decreased ms, making it faster) ran opposite to the Delay row. Both rows now read as time labels with consistent direction: + raises the displayed ms value, lowers it.
  • Compatibility Mode collapsed from two toggles to a single 3-card picker. The pair "Auto-Enable Compatibility Mode" + "Compatibility Mode (always on)" exposed three meaningful states (Off / Auto / Always On) as a confusing boolean composition where the user-visible state of the OSK was manual OR (auto_enabled AND auto_active). Replaced with a single picker: Off (predictions inserted with the fast suffix-only path), Auto (recommended) (switches to backspace+retype when a known problem app has focus), Always On (forces backspace+retype everywhere). Each card carries a short description and a hover tooltip explaining when to pick it. The two underlying booleans (compatMode + compatAutoDetect) are unchanged so existing settings persistence and the bridge slots keep working; the picker writes both on every selection so toggling Always→Off doesn't leave auto-detect silently armed.
  • Theme animation overlays removed. The vaporwave gradient, neon pulse, and spaceship star-field overlays in Main.qml are gone. The animations were eye-catching but added a Canvas redraw on every frame for a purely cosmetic effect, and the keyboard's accessibility audience generally prefers calm visuals over motion. Theme colours are unaffected.