Skip to content

fix(direct): sentence-window context + remove per-keystroke re-reads (Outlook canvas reset) - #68

Merged
willwade merged 5 commits into
mainfrom
fix/outlook-sentence-context
Sep 14, 2026
Merged

willwade merged 5 commits into
mainfrom
fix/outlook-sentence-context

Conversation

@willwade

@willwade willwade commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes the Outlook canvas-reset problem (Heide's video report).

Root cause

The per-keystroke UIA selection watch (#56, RFC 0019 clause 6) re-read the target on every selection change. Outlook fires TextSelectionChanged on every injected character and its UIA provider returns inconsistent full-document reads (signatures, formatting, timing) — the shadow-compare against the full text could never reliably match, so every keystroke triggered a seed_buffer → full model rebuild → visible canvas reset. BlueMail's "stutters but usable" was the same mechanism at lower severity.

v5 never had this problem because it never re-read the target within a field — its internal EDIT control accumulated whatever was typed through Dasher, full stop.

Fix (Heide's suggestion, v5-aligned)

  1. Sentence-window seeding: target reads trimmed to the sentence around the caret (last sentence boundary, max 200 chars back) before seeding. The LM only needs local context; a small seed makes the shadow-compare stable — during typing, the sentence and the engine buffer grow together character by character, so echoes match and are skipped.

  2. Selection-change watch removed entirely. Context seeds once on focus/app switch (the focus hooks), then the engine buffer accumulates internally (v5 parity). No per-keystroke re-reads. Clicking into a different field/app fires the focus hooks and re-seeds with that field's sentence.

  3. The long-document seed cap is superseded by the sentence window for the target path (always ≤ 200 chars); EditorSeedPolicy remains for the editor pane.

Tests

10 new SentenceWindowTests: boundary detection (. ! ? \n ; :), the typing-flow sync invariant (sentence grows in lockstep with engine buffer → shadow-compare skips), multi-sentence clicking, caret-at-boundary → empty context, max-lookback cap, empty inputs. Full suite 108/108.

Manual: publish-test built — test in Notepad and Outlook (type a word, verify no canvas reset per keystroke; switch between fields; the keyboard_debug.log shows sentence "..." (N of M chars) lines).

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no outstanding previous findings or actionable new failures identified.

Summary

  • Adds bounded, surrogate-safe sentence-window extraction and focused unit coverage.
  • Preserves same-field caret and external-edit synchronization through the UI Automation selection watch while avoiding rebuilds for matching injection echoes.
  • Adds a manual re-anchor action and ensures selection-watch lifecycle and failed-arm retries are handled correctly.
  • Removes target sentence content from diagnostics and safely reseeds whitespace-only divergences.
  • Advances DasherCore for aggregated invalid-UTF-8 training-data warnings.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Target[Focused target field] --> Read[Read text and caret]
    Watch[UIA selection-change watch] --> Read
    Manual[Manual re-anchor] --> Read
    Read --> Window[Extract bounded sentence window]
    Engine[Native engine output] --> EngineWindow[Extract matching sentence window]
    Window --> Compare{Sentence comparison}
    EngineWindow --> Compare
    Compare -->|Exact match| Offset[Cheap offset update or skip]
    Compare -->|Whitespace-only difference| Reseed[Reseed current sentence]
    Compare -->|Content difference| Reseed
    Offset --> Canvas[Preserve prediction canvas]
    Reseed --> Canvas
Loading

Reviews (5) · Last reviewed commit: "chore: pin DasherCore to v0.2.24 + UTF-8..."

…(Outlook reset)

Heide's video report: in Outlook, "H goes in, then she navigates to 'e'
and the canvas resets — 'e' never crosses the line." The per-keystroke
UIA selection watch (#56, RFC 0019 clause 6) was the cause: Outlook's
UIA provider fires TextSelectionChanged on every injected character and
returns inconsistent full-document reads (signatures, formatting, timing
differences) — the shadow-compare against the full text could never
reliably match, so every keystroke triggered a full model rebuild =
visible canvas reset. BlueMail's "stutters but usable" was the same
mechanism at lower severity. v5 never had this problem because it never
re-read the target within a field.

Fix (Heide's suggestion, v5-aligned):

1. Sentence-window seeding: target reads are trimmed to the SENTENCE
   around the caret (last sentence boundary up to 200 chars back) before
   seeding the engine. The engine's language model only needs local
   context; a small seed makes re-seeds cheap and the shadow-compare
   stable — during typing, the sentence and the engine buffer grow
   together character by character, so echoes match and are skipped.

2. The UIA selection-change watch is REMOVED entirely. Context is seeded
   once on focus/app switch (the focus hooks), then the engine buffer
   accumulates internally (v5 parity). No per-keystroke re-reads.
   Clicking within the same field does not re-read; clicking into a
   different field or app fires the focus hooks and re-seeds with that
   field's sentence.

3. The long-document seed cap (EditorSeedPolicy) is superseded by the
   sentence window for the target path (the sentence is always <= 200
   chars); the EditorSeedPolicy remains for the editor pane path.

10 unit tests for SentenceWindow (boundary detection, typing-flow sync
invariant, caret-at-boundary, multi-sentence clicking, max-lookback cap,
empty inputs). Full suite 108/108.

Signed-off-by: will wade <willwade@gmail.com>
Comment thread src/Dasher.Windows/Views/MainWindow.axaml.cs
Comment thread src/Dasher.Windows/Views/MainWindow.axaml.cs
Comment thread src/Dasher.Windows/Services/SentenceWindow.cs
… re-anchor button

User testing: clicking within the same field (e.g. mid-sentence in an email reply) didn't re-anchor — the focus hooks can't see same-field caret moves, so the user had to switch apps and back. Two additions: (1) re-enable the UIA selection-change watch, now SAFE because the sentence-window shadow-compare is immune to the full-document read inconsistency that caused the Outlook resets — during typing, sentence and engine buffer grow together (match, skip); on a genuine caret click, the sentence changes (mismatch, re-seed with the new sentence). (2) A manual re-anchor button (Locate icon) on the mini-bar forces a fresh read for edge cases where the watch misses or the user wants certainty.

Signed-off-by: will wade <willwade@gmail.com>
Comment thread src/Dasher.Windows/Views/MainWindow.axaml.cs
…e guards

Review-loop round 1 score 6/10 → round 2 score 7/10; all findings addressed:

1. CRITICAL — symmetric trimming: the engine buffer and target sentence are BOTH trimmed via SentenceWindow.Trim before the shadow-compare. Without this, typing a period/question mark/newline through Dasher broke the lockstep invariant (engine grew, sentence trimmed to empty at boundary → mismatch → reset on every sentence end). CRLF divergence also handled naturally.

2. Watch lifecycle: StartSelectionWatch now returns bool; ArmSelectionWatch only marks _lastWatchTarget on confirmed success (a sticky field on silent UIA failure permanently disabled re-arming); StopSelectionWatch called on mode exit.

3. Surrogate guards both sides: start slides past low surrogates at the 200-char cap; caret steps back from lone high surrogates (UIA can return mid-pair offsets).

4. Trailing-whitespace tolerance in the shadow-compare: engine lagging a space no longer causes churn re-seeds.

6 new tests: boundary-typing lockstep, boundary-resume lockstep, CRLF divergence, consecutive boundaries, surrogate-at-cap, caret-beyond-length. 114/114.

Signed-off-by: will wade <willwade@gmail.com>
Comment thread src/Dasher.Windows/Views/MainWindow.axaml.cs Outdated
…ence re-seeds

Two Greptile P1s on #68:

1. SECURITY "private target text is logged": the seed log line recorded
   the actual sentence content — up to 200 chars of a private email
   appended to keyboard_debug.log. Removed: the log now records lengths
   and offsets only, never content. Verified no other KbLog line
   references target text.

2. "Whitespace hides buffer divergence": TrimEnd() on both sides made
   different buffers compare as equal, but the subsequent set_offset
   used a byte offset computed from the LONGER (untrimmed) target
   sentence — placing the engine offset beyond its own buffer. Now:
   exact match → cheap set_offset (as before); whitespace-only match →
   full re-seed to absorb the difference (correct, and the convergence
   cost is one seed, not an out-of-range offset).

Signed-off-by: will wade <willwade@gmail.com>
Post-v0.2.24 commit: aggregates invalid-UTF-8 training-data warnings into one summary message instead of per-byte toasts. Not yet tagged — the pin tracks the merged head until the next DasherCore release.

Signed-off-by: will wade <willwade@gmail.com>
@willwade
willwade merged commit d27d624 into main Sep 14, 2026
3 checks passed
@willwade
willwade deleted the fix/outlook-sentence-context branch September 14, 2026 00:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant