Files
win-dictate/old_text_files/history-not-populating-fix.md
T

318 lines
12 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.
# Fix 05 — New transcriptions never reach history (+ live session archiving)
**Builds on:** Fix-03/04 (popup history list — those are fine and unchanged).
**Files touched:** `src/main.cpp` only. `history.h` already has everything we need.
**Estimated effort:** 3045 minutes including testing.
---
## 1. Root cause — one line poisons every archive path
History files are only ever written by `ArchiveSession()`, which is called from three
places, all guarded the same way:
| Where | Guard |
|---|---|
| Clear button (`OnClick``WK::Clear`) | `if (!cur.empty() && cur != g_lastLoadedText)` |
| Picking a history entry (`WM_APP_SELECT``ID_SEL_HISTORY`) | same |
| App exit (`WM_DESTROY`) | same |
`g_lastLoadedText` exists for ONE purpose: it remembers text that was loaded **FROM**
history, so that flipping between entries doesn't re-archive an unmodified copy and
create duplicates. It is supposed to be set in exactly one place — the history-load
handler.
But look at `WM_APP_RESULT` (the handler that runs when a transcription finishes).
After inserting the new text into the transcript box it does:
```cpp
UpdatePlaceholder(hWnd);
g_lastLoadedText = GetEditText(hWnd); // ← THE BUG
SetClipboardTextUtf8(hWnd, *res);
```
That line stamps the freshly-transcribed text as "this came from history". From that
moment, `cur == g_lastLoadedText` is true, so:
- **Clear** skips the archive — but still shows the (now lying) "Saved to history" status;
- **loading another history entry** silently discards the current transcription;
- **exiting the app** discards it too.
Net effect: exactly what you reported — no new transcription ever lands in `history\`.
You can confirm the diagnosis before fixing: transcribe → **manually type one extra
character** in the box → press Clear → the entry DOES appear (the edit makes
`cur != g_lastLoadedText` again).
## 2. Second problem — sessions only archived at boundaries the user rarely hits
Even with that line deleted, a session is only archived on Clear / entry-switch / exit.
The real dictation workflow is: hotkey → speak → auto-paste → keep working, app lives in
the tray. Clear is optional, exit is rare. So history would still feel "missing" most of
the time, and a crash would lose the whole session.
**Fix: live session archiving.** Every successful transcription immediately writes the
session's history file:
- the **first** clip of a session **creates** a new timestamped file (via the existing
`ArchiveSession`, which already returns the path it wrote);
- every **subsequent** clip **rewrites that same file** with the full transcript —
one file per session, never duplicates;
- Clear / entry-switch / exit just *finalize* the session (capture any manual edits made
after the last clip, then start a fresh session).
Result: open the History popup right after dictating and the session is already there,
at the top, kept current as you append. Crash-safe for free.
---
## 3. Step 1 — Add the session-path global
In `src/main.cpp`, find:
```cpp
std::vector<HistoryEntry> g_history;
std::wstring g_lastLoadedText;
```
Add one line below them:
```cpp
std::vector<HistoryEntry> g_history;
std::wstring g_lastLoadedText;
std::wstring g_sessionPath; // history file backing the CURRENT session ("" = none yet)
```
**Do NOT delete `g_lastLoadedText`** — it still guards against re-archiving an
unmodified entry that was loaded from history (Step 4 below still uses it).
## 4. Step 2 — Add two small helpers
Paste these **immediately above** `void OnClick(HWND hWnd, WK kind)` (right after the
`SelectsRowFullWidthAnchor()` helper from Fix-04):
```cpp
// True if the string contains anything that isn't whitespace.
static bool HasInk(const std::wstring& s) {
for (wchar_t c : s) if (!iswspace(c)) return true;
return false;
}
// End the current session: make sure whatever is in the transcript box is in
// history (including manual edits made after the last clip), then reset the
// session so the next transcription starts a new history file.
// Returns true if the session is saved in history.
static bool FinalizeSession(HWND hWnd) {
std::wstring cur = GetEditText(hWnd);
bool saved = false;
if (!g_sessionPath.empty()) {
// Live archiving already created the file; just capture any edits
// the user made after the last transcription.
if (HasInk(cur)) WriteFileUtf8(g_sessionPath, cur);
saved = true;
} else if (HasInk(cur) && cur != g_lastLoadedText) {
// Text that was typed (never transcribed) — archive it once.
// The g_lastLoadedText guard stops unmodified loaded entries
// from being archived a second time.
saved = !ArchiveSession(cur).empty();
}
g_sessionPath.clear();
return saved;
}
```
(`WriteFileUtf8` and `ArchiveSession` are both `inline` in `history.h`, which
`main.cpp` already includes — nothing to add there.)
## 5. Step 3 — Fix `WM_APP_RESULT` (delete the bug, add live archiving)
In `WndProc`'s `WM_APP_RESULT` case, find these four lines:
```cpp
SendMessageW(hEdit, EM_SCROLLCARET, 0, 0);
UpdatePlaceholder(hWnd);
g_lastLoadedText = GetEditText(hWnd);
SetClipboardTextUtf8(hWnd, *res);
```
Replace with:
```cpp
SendMessageW(hEdit, EM_SCROLLCARET, 0, 0);
UpdatePlaceholder(hWnd);
{
// Live-archive: keep this session's history file current.
// First clip creates the file; later clips rewrite it.
std::wstring all = GetEditText(hWnd);
if (g_sessionPath.empty()) g_sessionPath = ArchiveSession(all);
else WriteFileUtf8(g_sessionPath, all);
}
SetClipboardTextUtf8(hWnd, *res);
```
Two things happened here — make sure both did:
1. `g_lastLoadedText = GetEditText(hWnd);` is **GONE**. This is the actual bug fix.
Do not move it somewhere else; it must only ever be assigned in the history-load
handler (Step 5b) and cleared on Clear.
2. The live-archive block was added in its place.
## 6. Step 4 — Route the three session boundaries through `FinalizeSession`
### 4a. Clear button — `OnClick`, `case WK::Clear`
Find:
```cpp
case WK::Clear: {
std::wstring cur = GetEditText(hWnd);
if (!cur.empty() && cur != g_lastLoadedText)
ArchiveSession(cur);
g_history = LoadHistoryIndex();
g_lastLoadedText.clear();
g_editDirty = false;
SetDlgItemText(hWnd, ID_EDIT_TEXT, L"");
UpdatePlaceholder(hWnd);
SetStatus(hWnd, L"Saved to history");
break;
}
```
Replace with:
```cpp
case WK::Clear: {
bool saved = FinalizeSession(hWnd);
g_history = LoadHistoryIndex();
g_lastLoadedText.clear();
g_editDirty = false;
SetDlgItemText(hWnd, ID_EDIT_TEXT, L"");
UpdatePlaceholder(hWnd);
SetStatus(hWnd, saved ? L"Saved to history" : L"Cleared");
break;
}
```
(Bonus fix: the status no longer claims "Saved to history" when nothing was saved —
clearing an empty box now honestly says "Cleared".)
### 4b. Picking a history entry — `WM_APP_SELECT`, the `ID_SEL_HISTORY` branch
Find:
```cpp
} else if (ctrlId == ID_SEL_HISTORY && idx >= 0 && idx < (int)g_history.size()) {
std::wstring cur = GetEditText(hWnd);
if (!cur.empty() && cur != g_lastLoadedText)
ArchiveSession(cur);
std::wstring text = ReadFileUtf8(g_history[idx].path);
```
Replace the first three body lines with one call (the rest of the branch stays):
```cpp
} else if (ctrlId == ID_SEL_HISTORY && idx >= 0 && idx < (int)g_history.size()) {
FinalizeSession(hWnd); // save the in-progress session before swapping
std::wstring text = ReadFileUtf8(g_history[idx].path);
```
Leave `g_lastLoadedText = text;` and everything after it in this branch exactly as it
is — this is the ONE place `g_lastLoadedText` is supposed to be assigned.
Note: loading an entry does NOT make it the live session (`FinalizeSession` cleared
`g_sessionPath`). If you dictate on top of a loaded entry, the next clip archives the
combined text as a **new** file — old history entries are never mutated.
### 4c. App exit — `WM_DESTROY`
Find:
```cpp
case WM_DESTROY: {
std::wstring cur = GetEditText(hWnd);
if (!cur.empty() && cur != g_lastLoadedText) ArchiveSession(cur);
PersistNow();
PostQuitMessage(0);
break;
}
```
Replace with:
```cpp
case WM_DESTROY: {
FinalizeSession(hWnd);
PersistNow();
PostQuitMessage(0);
break;
}
```
### 4d. Legacy hidden Clear button — `WM_COMMAND`, `case ID_BTN_CLEAR`
This branch belongs to a hidden legacy child button and never fires, but update it to
match 4a anyway so the two Clear paths can't drift apart:
```cpp
case ID_BTN_CLEAR: {
bool saved = FinalizeSession(hWnd);
g_history = LoadHistoryIndex();
g_lastLoadedText.clear();
g_editDirty = false;
SetDlgItemText(hWnd, ID_EDIT_TEXT, L"");
UpdatePlaceholder(hWnd);
SetStatus(hWnd, saved ? L"Saved to history" : L"Cleared");
break;
}
```
## 7. How the pieces behave now (mental model for the dev)
```
record clip 1 ──► WM_APP_RESULT ──► ArchiveSession(text) → creates 2026-06-11_HHMMSS.txt, g_sessionPath = that file
record clip 2 ──► WM_APP_RESULT ──► WriteFileUtf8(sessionPath) → same file rewritten with full text
edit by hand ──► (nothing yet — captured at the next clip or at finalize)
Clear / pick entry / exit ──► FinalizeSession → final write incl. edits, g_sessionPath = ""
next clip ──► new session file
```
- One file per session. Appending clips never creates duplicates.
- Cancelled clips / "No speech detected" change nothing (the result is empty, so the
live-archive block isn't reached).
- The session file keeps its creation timestamp/filename while it grows — it sorts in
the popup by when the session *started*. That's intended.
- `ArchiveSession` already refuses whitespace-only text and prunes to 100 files; both
behaviors are reused untouched.
## 8. Build & test checklist
1. **The headline fix:** launch with an empty box → dictate one clip → open History
(no Clear!) → the new session is the top entry with the right preview.
2. **Appending:** dictate a second clip → open History → still ONE entry for this
session, now containing both clips. No duplicate rows.
3. **Clear:** press Clear → "Saved to history" → box empties → dictate again → History
now shows TWO entries (old session + new session).
4. **Clear with empty box:** status says "Cleared" and no empty file appears in `history\`.
5. **Exit:** dictate → edit a word by hand → Exit via tray → relaunch → the entry
contains the hand-edit.
6. **Switching:** dictate → open History → pick an older entry → the in-progress
session was saved (visible in the list) and the older text loads.
7. **No duplicate on unmodified load:** load an entry, change nothing, press Clear →
no new file is created (status "Cleared"); the entry appears once in the list.
8. **Dictating onto a loaded entry:** load an entry → dictate → a NEW combined entry is
created; the original old file is unchanged.
9. **Typed-only session:** type text manually without dictating → Clear → archived once.
10. **Cancel:** record → hotkey again mid-transcription to cancel → no history file.
11. **Diagnosis confirmation (optional, before applying the fix):** on the OLD build,
transcribe → type one character → Clear → entry appears. That proves the
`g_lastLoadedText` poisoning was the culprit.
## 9. Do NOT touch
- `history.h``ArchiveSession`, `WriteFileUtf8`, `PruneHistory`, `LoadHistoryIndex`
all unchanged.
- The popup code from Fix-03/04 (`PopupProc`, `ShowSelectPopup`,
`SelectsRowFullWidthAnchor`) — unchanged.
- `g_lastLoadedText` — keep it; it is still assigned in the history-load branch and
cleared on Clear. Just never assign it anywhere else (that was the bug).
- `g_editDirty` — currently informational only; leave as is.