Skip to content

feat(uia): proper Narrator support — host provider, system caret, full Text pattern#48

Merged
yeroo merged 2 commits into
mainfrom
feat/uia-host-provider-and-caret
Jul 9, 2026
Merged

feat(uia): proper Narrator support — host provider, system caret, full Text pattern#48
yeroo merged 2 commits into
mainfrom
feat/uia-host-provider-and-caret

Conversation

@yeroo

@yeroo yeroo commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Delivers the "proper Narrator support" the terminal was missing — driven by live testing (Narrator read the prompt but not dir output; the focus rectangle landed somewhere random; Settings weren't narrated).

Three layers, all verified with a UIAutomationClient probe (the same COM path Narrator uses)

1. Host provider (routing + anchoring)

GetHostRawElementProviderUiaHostProviderFromHwnd (was 0). The provider was unparented, so Narrator couldn't anchor its focus box and events didn't route (Settings silence). Now it reports a real BoundingRectangle and hosts properly.

2. System caret (Magnifier / IME / caret follow)

Hidden CreateCaret/SetCaretPos tracks the active pane's text cursor each paint — Magnifier "follow the text cursor" and IME placement now work. Hidden so it never double-draws over our rendered cursor.

3. Full Text pattern (ITextProvider + ITextRangeProvider) — the big one

The pane's recent scrollback + live screen flatten into one document. Ranges support GetText, ExpandToEnclosingUnit (char/word/line/document), Move/MoveEndpointByUnit, GetSelection (caret), GetVisibleRanges, FindText, and GetBoundingRectangles — a degenerate caret range returns a one-cell box at the caret, which is what makes the focus rectangle track where you type. A text-changed event fires on new output.

Probe results

patterns: Window, Text, Transform
DocumentRange.GetText → full buffer (incl. typed lines)
GetSelection → 1 range (caret); ExpandToEnclosingUnit(Line) → the caret line
GetBoundingRectangles → 737,330,14,21   (tight one-cell box at the caret)
Move(Line,+1) walks the buffer line by line

110/110 Core, 38/38 Pty.

Still Stage 3 (backlog)

Word-move refinement, live selection via Select(), and alt-screen TUI (Far/vim) reading — those redraw in place and need a different model. This covers reading, caret tracking, and line/char navigation — the core of using Claude Code with a screen reader.

🤖 Generated with Claude Code

yeroo and others added 2 commits July 9, 2026 13:55
…eaders

Foundation for real Narrator support (T2-14). Two root-cause fixes verified
with a UIAutomationClient probe:

- GetHostRawElementProvider now returns UiaHostProviderFromHwnd instead of 0.
  The provider was UNPARENTED — Narrator couldn't anchor its focus rectangle
  (it landed somewhere random) and notification/property events didn't route
  reliably (Settings interactions weren't spoken). Now the element reports a
  real BoundingRectangle and the HWND Window/Transform patterns, and events
  route through the hosted element.
- System caret (CreateCaret/SetCaretPos/HideCaret, WM_SETFOCUS/WM_KILLFOCUS):
  a hidden caret tracks the active pane's text cursor each paint, so Magnifier
  "follow the text cursor", IME composition placement, and screen-reader caret
  queries have a correct position. Hidden so it never double-draws over our
  rendered cursor.

Probe confirms: ControlType=Document, LocalizedControlType="terminal",
Name=live screen text, BoundingRectangle=window rect (was empty). Tight
per-caret focus + line navigation is the follow-on ITextProvider stage.
110/110 Core, 38/38 Pty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(proper Narrator support)

Stage 2 of T2-14. Implements ITextProvider + ITextRangeProvider so Narrator/
NVDA can read and navigate the terminal like a document and box the caret
where you type — the fix for "Narrator reads the prompt but not the output"
and "the focus rectangle lands somewhere random."

- The active pane's recent scrollback (capped 2000 lines) + live screen are
  flattened into one document (lines joined by '\n'); a text range is a
  [start,end) char span. Program.BuildUiaTextSnapshot builds it under the
  session lock with the caret offset + a document-line -> on-screen-row map.
- ITextRangeProvider: GetText, ExpandToEnclosingUnit (char/word/line/document),
  Move / MoveEndpointByUnit (char + line), GetBoundingRectangles (screen rects
  for the visible portion — degenerate caret range -> a 1-cell box), GetSelection
  (caret), GetVisibleRanges, FindText, Clone/Compare/CompareEndpoints. SAFEARRAY
  builders (VT_R8 rects, VT_UNKNOWN range arrays) for the source-gen COM out-params.
- GetPatternProvider advertises the Text pattern (10014); a text-changed event
  fires on new output so the reader re-reads.

Verified with a UIAutomationClient probe (the identical COM path Narrator uses):
TextPattern advertised; DocumentRange.GetText returns the buffer; GetSelection
-> caret; ExpandToEnclosingUnit(Line) reads the caret line; GetBoundingRectangles
-> a tight one-cell box AT the caret (737,330,14,21); Move(Line,+1) walks the
buffer line by line. 110/110 Core, 38/38 Pty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@yeroo
yeroo merged commit 1680c1b into main Jul 9, 2026
1 check passed
@yeroo
yeroo deleted the feat/uia-host-provider-and-caret branch July 9, 2026 11:03
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