Files
win-dictate/dev-task-list.md
T

322 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Win Dictation — Developer Task List
A prioritised, junior-friendly backlog to finish polishing the app. Each task says **what**, **which files**, **steps**, **the code** (inline, or a pointer to the verbatim block in a companion doc), and **how to know it's done**.
**Companion docs (full code lives here — don't retype, copy from them):**
- `ARCHITECTURE-AND-DEVGUIDE.md` — Part B1 (chrome removal), Part B2 (progress + ETA). Verbatim code.
- `MODERN-UI-AND-FIXES.md` — the modern UI design + GDI+ helpers.
- `FINDINGS-FIXES-TESTS.md` — the test harness (`tests/test_core.cpp`).
**Baseline (already done — do NOT redo):** batch record-then-transcribe; crash fixes (null-context guard, `g_modelOk`, `g_initializing`, `m_cfg_mtx`); append behaviour (`text_util.h`); no-hide + safe auto-paste; Clear button; `run_inference`/`transcribe_sync` refactor; modern dark UI (GDI+, rounded panel, owner-draw buttons, custom VU, dark caption); resizable window; `test-core` CMake target.
**Conventions:** Effort = XS (<30 min) · S (≤2 h) · M (half-day) · L (12 days). Do phases in order; tasks within a phase are mostly independent unless "Depends on" says otherwise. After every task: `cmake --build build --config Release` must succeed with **zero new warnings**, and the app must still launch.
**Color tokens (already in `main.cpp`, reuse — never hard-code hex elsewhere):** `C_BG #0F1115`, `C_SURFACE #181B22`, `C_SURFACEHI #20242D`, `C_BORDER #262B36`, `C_TEXT #E7E9EE`, `C_TEXTDIM #9AA0AB`, `C_ACCENT #6E8BFF`, `C_DANGER #FF5C5C`, `C_GOOD #46D39A`.
---
## Phase 0 — Repo hygiene (do first; clears traps)
### Task 0.1 — Delete the stale `src/CMakeLists.txt`
- **Goal:** Remove a build file that references **removed** APIs (`init()`, `is_using_gpu()`) and a `test-audio` target the real build ignores. The authoritative build is the **root** `CMakeLists.txt`.
- **Files:** `src/CMakeLists.txt` (delete).
- **Steps:** Confirm `build.ps1` configures from repo root (`-S $RepoRoot`). It does. Delete `src/CMakeLists.txt`.
- **Done when:** Clean build from root still works; no other file `add_subdirectory(src)`.
- **Effort:** XS
### Task 0.2 — Remove the broken `src/test-audio.cpp` (replaced in Phase 4)
- **Goal:** `src/test-audio.cpp` calls `m_transcriber.init(...)` / `is_using_gpu()` which no longer exist — it cannot compile. It's superseded by `tests/test_core.cpp` (Task 4.1).
- **Files:** `src/test-audio.cpp` (delete), `src/record-test-audio.ps1` (keep — still useful for capturing WAVs), `src/TESTING.md` (mark superseded in Task 4.4).
- **Done when:** No target references `test-audio.cpp`.
- **Effort:** XS
### Task 0.3 — Create the `tests/` folder + placeholder
- **Goal:** The root `CMakeLists.txt` already declares `add_executable(test-core tests/test_core.cpp ...)`, but the file doesn't exist yet → configure fails if anyone builds `test-core`.
- **Steps:** Create `tests/` and add `tests/test_core.cpp` (full content in Task 4.1). Until then, the `test-core` target can stay; just don't build it.
- **Done when:** `tests/test_core.cpp` exists and `cmake --build build --target test-core` compiles (after Task 4.1).
- **Effort:** XS · **Depends on:** 4.1 for real content
---
## Phase 1 — Visual polish & assets
### Task 1.1 — App icon asset (custom)
- **Goal:** Replace the placeholder icon with a clean, modern app icon used for the window, taskbar, and tray.
- **Files:** `assets/icon-source.png` (new, 1024×1024), `src/icon.ico` (generated), `src/convert_icon.py` (fix paths), `src/win-dictation.rc` (already references `101 ICON "icon.ico"`).
- **Design spec:** Flat, minimal. A single rounded **microphone** glyph, centered, on a dark charcoal rounded-square (`#15171C`). Mic filled with the indigo accent (`#6E8BFF`), subtle top-down gradient to `#5B7BFF`. No text. Must read clearly at **16×16**. Keep ~12% padding around the glyph.
- Ready-to-use generation prompt (AI image tool): *"Minimalist modern app icon, a single simple microphone glyph centered on a dark charcoal rounded square, microphone filled indigo #6E8BFF with a soft vertical gradient, flat design, crisp clean edges, no text, high contrast, legible at small sizes, 1024×1024."*
- Or design in Figma/Inkscape and export 1024×1024 PNG.
- **Steps:**
1. Put the source PNG at `assets/icon-source.png`.
2. Fix `convert_icon.py` to use real paths and multi-size output:
```python
from PIL import Image
img = Image.open("assets/icon-source.png").convert("RGBA")
img.save("src/icon.ico", format="ICO",
sizes=[(256,256),(64,64),(48,48),(32,32),(16,16)])
print("wrote src/icon.ico")
```
3. Run it (`python src/convert_icon.py` from repo root). Confirm `src/icon.ico` exists.
4. Rebuild; the resource compiler picks up `src/icon.ico` via the `.rc`.
- **Done when:** The new icon shows on the title bar, taskbar, Alt-Tab, and tray — sharp at all sizes.
- **Effort:** S
### Task 1.2 — Tray icon reflects recording state (optional but nice)
- **Goal:** When recording (window may be hidden), the **tray** icon turns red so state is visible at a glance.
- **Files:** `assets/icon-rec-source.png` (new), `src/icon-rec.ico`, `src/win-dictation.rc` (add `102 ICON "icon-rec.ico"`), `src/main.cpp`.
- **Steps:**
1. Create a red variant of the icon (mic in `#FF5C5C`). Convert to `src/icon-rec.ico` (same sizes), add `102 ICON "icon-rec.ico"` to the `.rc`.
2. In `main.cpp`, load both icons once: `HICON g_icoIdle, g_icoRec;` via `LoadIcon(hInst, MAKEINTRESOURCE(101/102))`.
3. Add a helper `void SetTrayIcon(bool rec){ nid.uFlags = NIF_ICON; nid.hIcon = rec?g_icoRec:g_icoIdle; Shell_NotifyIcon(NIM_MODIFY,&nid); }`.
4. Call `SetTrayIcon(true)` when recording starts, `SetTrayIcon(false)` on stop/cancel/result.
- **Done when:** Start recording, hide the window — the tray icon is red; after transcription it returns to normal.
- **Effort:** S · **Depends on:** 1.1
### Task 1.3 — Kill the button hairlines + focus rectangles (B1.1)
- **Goal:** Remove the thin light line around *Pinned* and the left/top lines on Copy/Paste/Clear.
- **Files:** `src/main.cpp`; link `uxtheme.lib`.
- **Steps (full code in `ARCHITECTURE-AND-DEVGUIDE.md` §B1.1):**
1. Add `#include <uxtheme.h>` and `#pragma comment(lib, "uxtheme.lib")`.
2. After creating each owner-draw button (Record, Pin, Copy, Paste, Clear), call `SetWindowTheme(hBtn, L"", L"");` (before/after `SetWindowSubclass` is fine).
3. Add `WS_CLIPCHILDREN` to the main window style in `CreateWindowExW`.
4. After all controls are created (end of the create block), call once:
`SendMessageW(hMainWnd, WM_CHANGEUISTATE, MAKEWPARAM(UIS_SET, UISF_HIDEFOCUS), 0);`
- **Done when:** No hairline around any button; tabbing between controls draws no dotted focus rect.
- **Effort:** S
### Task 1.4 — Replace comboboxes with custom dropdowns (B1.2)
- **Goal:** Remove the native Windows dropdown button (the "second arrow") on the mic + model selectors.
- **Files:** `src/main.cpp`.
- **Steps (full code — `DrawSelect`, `PopupProc`, `ShowSelectPopup`, `WM_APP_SELECT` — in `ARCHITECTURE-AND-DEVGUIDE.md` §B1.2):**
1. Add label/selection state: `std::vector<std::wstring> g_audioItems; int g_audioSel=0;` and `g_modelItems`/`g_modelSel`. Populate them in `RefreshAudioDevices`/`RefreshModelList` (keep populating `g_modelComboPaths` in parallel).
2. Replace the two `COMBOBOX` creations with `BS_OWNERDRAW` buttons `ID_SEL_AUDIO`/`ID_SEL_MODEL`; subclass with `BtnProc`; `SetWindowTheme(.., L"", L"")`.
3. Add `#define WM_APP_SELECT (WM_USER + 5)` and `#define ID_SEL_AUDIO/ID_SEL_MODEL`.
4. Paste `DrawSelect`, route both IDs in `WM_DRAWITEM`. Paste `PopupProc` + `ShowSelectPopup`. On `WM_COMMAND` for the two IDs call `ShowSelectPopup(...)`. Handle `WM_APP_SELECT` to apply the choice (set `capture_id` / reload model).
5. Delete the now-unused `DrawCombo`, `WM_MEASUREITEM` combo branch, and `WM_CTLCOLORLISTBOX`.
6. Update `LayoutControls` to position `ID_SEL_AUDIO`/`ID_SEL_MODEL` where the combos were.
- **Done when:** Each selector shows exactly one (our) chevron, opens a dark rounded popup, hover highlights rows, selecting reloads the model / switches mic, and clicking elsewhere dismisses it.
- **Effort:** M · **Depends on:** 1.3 (shared `SetWindowTheme`)
---
## Phase 2 — Progress feedback & control
### Task 2.1 — Transcription progress + ETA (B2)
- **Goal:** Replace the static "Transcribing…" with a moving progress bar + live status like **"Transcribing 1:40 · 45% · ~9s left"**.
- **Files:** `src/transcriber.h`, `src/transcriber.cpp`, `src/main.cpp`.
- **Steps (full code in `ARCHITECTURE-AND-DEVGUIDE.md` §B2.1–§B2.3):**
1. `transcriber.h`: add `set_progress_callback`, `audio_seconds()`, private `m_on_progress`, `m_audio_seconds`, and the static `s_progress` trampoline.
2. `transcriber.cpp`: implement `s_progress`; in `run_inference` set `m_audio_seconds`, `wp.progress_callback = &Transcriber::s_progress; wp.progress_callback_user_data = this;`.
3. `main.cpp`: add `#define WM_APP_PROGRESS (WM_USER + 6)`, `std::atomic<int> g_progress{0};`, `DWORD g_busyStart;`. Register `set_progress_callback([](int p){ PostMessage(hMainWnd, WM_APP_PROGRESS, p, 0); })`.
4. Set `g_busyStart = GetTickCount(); g_progress = 0;` right before `stop_and_transcribe()`.
5. Handle `WM_APP_PROGRESS` (store + invalidate `g_vuRect`). Replace the `is_busy()` branch of `UpdateStatus` with the ETA formatter. Add `DrawProgress` and, in `WM_PAINT`, draw the progress bar in `g_vuRect` while busy (VU otherwise). In `WM_TIMER`, also invalidate `g_vuRect` while busy so the bar/ETA tick.
- **Edge cases:** `progress < 3%` → show "Transcribing m:ss of audio…" (ETA not stable yet). Clamp `remain >= 0`.
- **Done when:** A 12 min clip shows a filling bar + percentage + shrinking ETA and completes; short clips still feel instant.
- **Effort:** M
### Task 2.2 — Cancel a running transcription (B2.4)
- **Goal:** Let the user abort a long/incorrect transcription instead of waiting it out.
- **Files:** `src/transcriber.h/.cpp`, `src/main.cpp`.
- **Steps:**
1. `transcriber.h`: add `void request_cancel(){ m_abort = true; }`, private `std::atomic<bool> m_abort{false};`, static `s_abort`.
2. `transcriber.cpp`: in `run_inference`, `m_abort = false;` at the top, and set `wp.abort_callback = &Transcriber::s_abort; wp.abort_callback_user_data = this;` (skip if your `whisper.h` lacks `abort_callback`).
3. `main.cpp`: at the very top of the `HK_TOGGLE` handler add `if (g_tx.is_busy()) { g_tx.request_cancel(); SetStatus(hWnd, L"Cancelling…"); break; }` — so the Record button becomes "cancel" while busy.
4. In `WM_APP_RESULT`, when the result is empty *and* a cancel was requested, show "Cancelled" instead of "No speech detected" (track a `g_cancelRequested` flag, reset each Stop).
- **Done when:** Pressing the button (or hotkey) mid-transcription stops it within ~1 s and the status reads "Cancelled".
- **Effort:** S · **Depends on:** 2.1
---
## Phase 3 — Robustness & persistence
### Task 3.1 — Persist settings between launches
- **Goal:** Remember mic, model, pin state, auto-paste, auto-hide, and window position. Today everything resets each launch.
- **Files:** `src/settings.h` (new, header-only), `src/main.cpp`.
- **Code (`src/settings.h`):**
```cpp
#pragma once
#include <windows.h>
#include <string>
struct AppSettings {
int captureId = 0;
std::wstring modelFile; // e.g. L"models\\ggml-tiny.en.bin" ("" = auto)
bool pinned = true, autoPaste = true, autoHide = false;
int winX = CW_USEDEFAULT, winY = CW_USEDEFAULT, winW = 400, winH = 340;
};
inline std::wstring SettingsPath() {
wchar_t buf[MAX_PATH]; GetModuleFileNameW(nullptr, buf, MAX_PATH);
std::wstring p(buf); p = p.substr(0, p.find_last_of(L"\\/"));
return p + L"\\win-dictation.ini";
}
inline int GetIni(const wchar_t* k, int d){ return GetPrivateProfileIntW(L"app", k, d, SettingsPath().c_str()); }
inline void PutIni(const wchar_t* k, int v){ wchar_t b[32]; wsprintfW(b, L"%d", v); WritePrivateProfileStringW(L"app", k, b, SettingsPath().c_str()); }
inline void LoadSettings(AppSettings& s){
s.captureId = GetIni(L"captureId", s.captureId);
s.pinned = GetIni(L"pinned", s.pinned) != 0;
s.autoPaste = GetIni(L"autoPaste", s.autoPaste) != 0;
s.autoHide = GetIni(L"autoHide", s.autoHide) != 0;
s.winX = GetIni(L"winX", s.winX); s.winY = GetIni(L"winY", s.winY);
s.winW = GetIni(L"winW", s.winW); s.winH = GetIni(L"winH", s.winH);
wchar_t m[MAX_PATH]; GetPrivateProfileStringW(L"app", L"modelFile", L"", m, MAX_PATH, SettingsPath().c_str());
s.modelFile = m;
}
inline void SaveSettings(const AppSettings& s){
PutIni(L"captureId", s.captureId); PutIni(L"pinned", s.pinned);
PutIni(L"autoPaste", s.autoPaste); PutIni(L"autoHide", s.autoHide);
PutIni(L"winX", s.winX); PutIni(L"winY", s.winY); PutIni(L"winW", s.winW); PutIni(L"winH", s.winH);
WritePrivateProfileStringW(L"app", L"modelFile", s.modelFile.c_str(), SettingsPath().c_str());
}
```
- **Wiring (`main.cpp`):**
1. Add a global `AppSettings g_set;` Call `LoadSettings(g_set);` **before** creating the window.
2. Apply: use `g_set.winX/Y/W/H` in `CreateWindowExW` (validate on-screen; fall back to `CW_USEDEFAULT` if off all monitors). Set `g_pinned = g_set.pinned`, `g_autoPaste = g_set.autoPaste`, `g_autoHide = g_set.autoHide`, `g_config.capture_id = g_set.captureId`. If `g_set.modelFile` is non-empty and the file exists, use it instead of `SelectOptimalModel`.
3. Save on change: after toggling pin/auto-paste/auto-hide, after a model/mic change, and on `WM_EXITSIZEMOVE` (window moved/resized → store rect) and `WM_DESTROY` (final save). A `void PersistNow()` that copies the live globals into `g_set` then `SaveSettings(g_set)` keeps it DRY.
- **Done when:** Change mic/model, move/resize the window, toggle pin, quit, relaunch → everything is restored. Deleting the `.ini` restores defaults.
- **Effort:** M
### Task 3.2 — Quick toggles in the tray menu
- **Goal:** Expose Auto-paste, Always-on-top, and Auto-hide without building a settings panel.
- **Files:** `src/main.cpp` (`ShowContextMenu`, `WM_COMMAND`).
- **Steps:** Add checkable items to the tray popup (`MF_STRING | (flag?MF_CHECKED:0)`) with new IDs (`ID_TRAY_AUTOPASTE`, `ID_TRAY_TOPMOST`, `ID_TRAY_AUTOHIDE`). In `WM_COMMAND`, flip the matching global, apply (for top-most call `SetWindowPos(... HWND_TOPMOST/NOTOPMOST ...)`), then `PersistNow()`.
- **Done when:** Right-click tray → toggles show check state, take effect immediately, and survive a relaunch.
- **Effort:** S · **Depends on:** 3.1
### Task 3.3 — Bound the recording length
- **Goal:** A forgotten recording shouldn't grow memory without limit (~1.9 MB/30 s today, uncapped).
- **Files:** `src/transcriber.h/.cpp`, `src/main.cpp`.
- **Steps:**
1. `transcriber.h`: add `float recorded_seconds() const;` returning `m_capture` size / `WHISPER_SAMPLE_RATE` under `m_capture_mtx` (or maintain an atomic sample counter incremented in `on_audio`).
2. `main.cpp` `WM_TIMER` (recording branch): if `g_tx.recorded_seconds() >= kMaxRecordSeconds` (e.g. 600), auto-stop by posting the same path as a manual Stop, and set status "Max length reached — transcribing".
- **Done when:** Recording auto-stops at the cap and transcribes what was captured; normal short clips unaffected.
- **Effort:** S
### Task 3.4 — Surface hotkey-registration failures + make hotkeys configurable
- **Goal:** Today `RegisterHotKey` return values are ignored — if another app owns `Ctrl+Shift+Space`, the hotkey silently dies. Also allow remapping.
- **Files:** `src/main.cpp`, `src/settings.h`.
- **Steps:**
1. Capture the return of both `RegisterHotKey` calls. If either fails, show a non-blocking status ("Hotkey in use — set another in win-dictation.ini") and still allow the on-screen Record button to work.
2. Read modifiers + key from the INI (`hkMods`, `hkVk`, defaulting to `MOD_CONTROL|MOD_SHIFT` + `VK_SPACE` / `'H'`); register those. (Full remap UI is a Phase 5 stretch — INI is enough now.)
- **Done when:** With a conflicting global hotkey registered by another app, the app launches, warns, and the button still records; editing the INI changes the hotkey.
- **Effort:** S · **Depends on:** 3.1
### Task 3.5 — Lightweight logging
- **Goal:** One small log file so field issues are diagnosable without a debugger.
- **Files:** `src/logging.h` (new), `src/main.cpp`, `src/transcriber.cpp`.
- **Code (`src/logging.h`):**
```cpp
#pragma once
#include <windows.h>
#include <cstdio>
#include <string>
inline void LogLine(const char* msg) {
wchar_t buf[MAX_PATH]; GetModuleFileNameW(nullptr, buf, MAX_PATH);
std::wstring p(buf); p = p.substr(0, p.find_last_of(L"\\/")) + L"\\win-dictation.log";
FILE* f = _wfopen(p.c_str(), L"a"); if (!f) return;
SYSTEMTIME t; GetLocalTime(&t);
fprintf(f, "%04d-%02d-%02d %02d:%02d:%02d %s\n", t.wYear,t.wMonth,t.wDay,t.wHour,t.wMinute,t.wSecond, msg);
fclose(f);
}
```
- **Log at minimum:** startup (model path, threads, GPU on/off), model load success/failure, mic-open failure, and each transcription (audio seconds, elapsed ms, output char count). Keep messages one line, ASCII.
- **Optional:** if the file exceeds ~1 MB at startup, rename to `.log.1` (simple 1-file rotation).
- **Done when:** `win-dictation.log` appears next to the exe and records a startup line + one line per transcription.
- **Effort:** S
---
## Phase 4 — Tests, docs & packaging
### Task 4.1 — Create `tests/test_core.cpp`
- **Goal:** Headless regression tests that catch the bugs we already fixed and lock in progress reporting.
- **Files:** `tests/test_core.cpp` (new — **full content in `FINDINGS-FIXES-TESTS.md` §4.2**).
- **Steps:**
1. Copy the test program from `FINDINGS-FIXES-TESTS.md` §4.2 into `tests/test_core.cpp`.
2. Add the progress assertion from `ARCHITECTURE-AND-DEVGUIDE.md` §B3 (max progress ≥95, monotonic).
3. Build + run:
```powershell
cmake --build build --config Release --target test-core
cd build\bin\Release
copy ..\..\..\deps\SDL2-2.28.5\lib\x64\SDL2.dll . # if missing
.\test-core.exe models\ggml-tiny.en.bin ..\..\..\samples\jfk.wav
```
- **Tests covered:** append rule; bad model path → no crash + empty; real clip → contains "country"; short audio → no crash; progress reaches ~100% and is non-decreasing.
- **Done when:** `test-core.exe` prints "ALL TESTS PASSED" and exits 0.
- **Effort:** S · **Depends on:** 2.1 (for the progress test)
### Task 4.2 — (Optional) CI workflow
- **Goal:** Run the build + `test-core` on every push.
- **Files:** `.github/workflows/build.yml` (new).
- **Steps:** Windows runner → configure CMake (CPU-only) → build `win-dictation` + `test-core` → download `ggml-tiny.en.bin` → run `test-core` with `jfk.wav`. Fail the job on non-zero exit.
- **Done when:** A pushed branch shows a green check that actually ran the tests.
- **Effort:** M · **Depends on:** 4.1
### Task 4.3 — Desktop / Start-Menu shortcut on install
- **Goal:** One-click launch (the user already pins to taskbar; a shortcut makes first run easy).
- **Files:** `src/package.ps1` (extend) or a new `install-shortcut.ps1`.
- **Steps:** After packaging, create a `.lnk` via `WScript.Shell` with `TargetPath` = the exe and `WorkingDirectory` = its folder (so `models\` resolves even though we also use `exe_dir()`), `IconLocation` = the exe. (Snippet in `MODERN-UI-AND-FIXES.md` §9 / spec doc §9.)
- **Done when:** Running the script creates a working Desktop shortcut that launches the app with the icon.
- **Effort:** S
### Task 4.4 — Fix the documentation (it describes the OLD app)
- **Goal:** `README.md`, `src/README.md`, `src/CHANGES.md`, `src/CUDA-SETUP.md`, `src/QUICK-REBUILD-GPU.md`, `src/TESTING.md`, `src/DESIGN.md`, `src/FIXES-APPLIED.md` still describe the **streaming / ring-buffer / 24-thread / Ctrl+Shift+R** design and RTX-3090 benchmarks — all now wrong/misleading.
- **Steps:**
1. Rewrite the top-level `README.md` to describe the **current** app: push-to-talk batch transcription, `Ctrl+Shift+Space` to record/stop, `Ctrl+Shift+H` to hide, tray, always-on-top, copy + auto-paste, model/mic selectors, CPU-tuned (physical-core threads, tiny.en default). Remove ring-buffer/VAD/24-thread/streaming claims and the RTX benchmarks (or move GPU notes to an "optional" aside).
2. Update `build.ps1` end-of-run messages ("Hotkey: Ctrl+Shift+R", "Model: base.en") to match reality.
3. Mark `CHANGES.md`, `FIXES-APPLIED.md`, `TESTING.md`, `QUICK-REBUILD-GPU.md`, `CUDA-SETUP.md` as **historical/superseded** (a one-line banner at top), or fold the still-true bits into the README and delete the rest. Keep `DESIGN.md` only if updated to the batch architecture.
- **Done when:** A new reader following `README.md` gets accurate hotkeys, model behaviour, and build steps; no doc claims a ring buffer or 24 threads.
- **Effort:** M
---
## Phase 5 — Stretch features (nice-to-have)
### Task 5.1 — Hold-to-talk mode
- **Goal:** Option to record only while a key is held (vs. toggle).
- **Files:** `src/main.cpp`, `src/settings.h`.
- **Steps:** Add a low-level keyboard hook (`SetWindowsHookEx(WH_KEYBOARD_LL, ...)`); on key-down of the chosen key start recording, on key-up `stop_and_transcribe`; debounce auto-repeat with a flag. Gate behind a `holdToTalk` INI setting; keep toggle as default. (Outline in the original spec doc §7.)
- **Done when:** With the setting on, holding the key records and releasing transcribes; toggle mode still available.
- **Effort:** M
### Task 5.2 — Settings panel (graduate from tray toggles + INI)
- **Goal:** A small in-app settings popup (reuse the custom popup window from Task 1.4) for mic, model, auto-paste, auto-hide, hold-to-talk, and hotkey capture.
- **Done when:** All settings are editable in-app and persist (Task 3.1).
- **Effort:** L · **Depends on:** 1.4, 3.1
### Task 5.3 — Export / save transcript
- **Goal:** Save the transcript box to a `.txt` (and timestamped filename) from the tray menu or a button.
- **Effort:** S
### Task 5.4 — Multi-language support
- **Goal:** Allow non-English models + a language selector (currently hard-wired `en`). Swap to a multilingual model (`ggml-base.bin`) and set `WhisperConfig.language` from a selector.
- **Effort:** M · **Depends on:** 1.4 (selector), 3.1
---
## Definition of Done (per task)
- [ ] Builds clean (`cmake --build build --config Release`), **zero new warnings**.
- [ ] App launches, records, transcribes, appends, copies/pastes — no regressions.
- [ ] `test-core.exe` exits 0 (after Phase 4).
- [ ] Any new setting persists across relaunch (after Phase 3).
- [ ] Change is reflected in `README.md` if user-facing.
## Suggested order (fastest path to "feels finished")
1. **0.10.3** (hygiene) → **1.3** (hairlines, 5-min win) → **2.1** (progress — biggest UX gain).
2. **1.1** (icon) → **1.4** (custom dropdowns) → **1.2** (tray state) → **2.2** (cancel).
3. **3.1** (persistence) → **3.2** (tray toggles) → **3.4** (hotkey safety) → **3.3** (length cap) → **3.5** (logging).
4. **4.1** (tests) → **4.4** (docs) → **4.3** (shortcut) → **4.2** (CI).
5. Stretch (**5.x**) as desired.
## Verification matrix (final smoke test)
| Area | Check |
|---|---|
| Crash-free | Record/stop 10× incl. a 2-min clip; window never vanishes; process stable |
| Progress | 2-min clip shows filling bar + % + shrinking ETA; cancel works |
| Chrome | No hairlines/focus rects; selectors have one chevron + dark popup |
| Paste | Hotkey-from-another-app pastes the latest utterance; button = copy only |
| Persistence | mic/model/pin/auto-paste/window pos restored after relaunch |
| Assets | New icon crisp in title bar, taskbar, Alt-Tab, tray; red tray icon while recording |
| Robustness | Missing model → clear message (no crash); hotkey conflict → warned; long record auto-stops |
| Tests/docs | `test-core` green; README matches actual hotkeys/behaviour |