Fixed character/word navigation inside links.

This commit is contained in:
Storm Dragon committed 2026-10-18 23:43:16 -04:00
1 parent 396efbd7c5
commit ab14244404
12 files changed
+213 -12

No files matched your search

+5
View File
@@ -215,6 +215,11 @@ surviving noninteractive container. Ambiguous children, editable controls and
replaced parents do not inherit the old offset. Removing a native focus anchor
does not reset a still-valid virtual reading position elsewhere in that document.
Explicitly named links retain their complete accessible label when reading by
line. Left/Right, Ctrl+Left/Right, and Home/End review that label by character,
word, or boundary, including links with no native text or only a numeric badge.
The label's review position remains separate from the browser's text caret.
Enter activates the current link or clickable, including explicitly named custom
controls and clickable ancestors of ordinary text. Activation itself is silent
whether it succeeds or fails; the page's own focus changes and live regions still
+4
View File
@@ -21,6 +21,10 @@ func brailleDocumentFrame(result review.Result, options config.BrailleOptions, c
if line == nil && assembled == nil || snapshot.Role == "password text" {
frame := brailleFocusFrameWithOptions(snapshot, options, configured...)
frame.Routes = nil
if snapshot.Role == "link" && snapshot.ExplicitName && strings.TrimSpace(snapshot.Name) != "" && !snapshot.Editable {
// Accessible-label review has no native text route.
frame.Cursor = min(result.DocumentPosition.Offset, utf8.RuneCountInString(strings.TrimSpace(snapshot.Name)))
}
textLength := utf8.RuneCountInString(snapshot.Name)
if textLength == 0 {
textLength = utf8.RuneCountInString(snapshot.Text)
@@ -315,6 +315,15 @@ func TestBrowseBrailleUsesVirtualOffsetAndAncestorIndicators(t *testing.T) {
}
}
func TestBrowseBrailleNamedLinkUsesLabelCursorWithoutNativeRoutes(t *testing.T) {
result := review.Result{DocumentPosition: review.DocumentPosition{Offset: 4,
Snapshot: atspi.Snapshot{Role: "link", ExplicitName: true, Name: "É猫 Info", CharacterCount: 1}}}
frame := brailleDocumentFrame(result, config.DefaultSettings().Braille)
if !strings.HasPrefix(frame.Text, "É猫 Info") || frame.Cursor != 4 || len(frame.Routes) != 0 {
t.Fatalf("named link lost label cursor or exposed synthetic native routes: %+v", frame)
}
}
func TestBrowseBrailleFragmentLineKeepsNativeCursor(t *testing.T) {
first := atspi.NewSnapshot("/first", "", "static", "", "")
first.BusName = ":1.999"
+29
View File
@@ -60,6 +60,12 @@ func TestNativeBrowserNamedLinks(t *testing.T) {
text string
}{
{review.CaretNextLine, "Example Game 2 copies"},
{review.CaretNextCharacter, "x"},
{review.CaretPreviousCharacter, "E"},
{review.CaretNextWord, "Example"},
{review.CaretNextWord, "Game"},
{review.CaretLineEnd, "s"},
{review.CaretLineStart, "E"},
{review.CaretNextLine, "Ordinary multiline link"},
{review.CaretNextCharacter, "r"},
{review.CaretNextWord, "Ordinary"},
@@ -101,7 +107,12 @@ func TestNativeBrowserNamedLinks(t *testing.T) {
t.Fatal(err)
}
var username, emoji atspi.ObjectReference
var labelOnly atspi.ObjectReference
for _, node := range document.Nodes {
if node.Snapshot.Role == "link" && node.Snapshot.Name == "Example Info Page" {
labelOnly = node.Reference
t.Logf("label-only native link: explicit=%t count=%d children=%d", node.Snapshot.ExplicitName, node.Snapshot.CharacterCount, len(node.Children))
}
if node.Snapshot.Role != "static" {
continue
}
@@ -112,6 +123,24 @@ func TestNativeBrowserNamedLinks(t *testing.T) {
emoji = node.Reference
}
}
if !labelOnly.Valid() {
t.Fatal("label-only link missing")
}
if _, err := cursor.SyncFocus(ctx, labelOnly.ID()); err != nil {
t.Fatal(err)
}
for _, want := range []rune("xample Info Page.") {
move, err := cursor.MoveCaret(ctx, adapter, review.CaretNextCharacter)
if err != nil || !move.Moved || move.Text != string(want) {
t.Fatalf("label-only character: %+v, %v; want %q", move, err, want)
}
}
for _, want := range []rune("egaP ofnI elpmaxE") {
move, err := cursor.MoveCaret(ctx, adapter, review.CaretPreviousCharacter)
if err != nil || !move.Moved || move.Text != string(want) {
t.Fatalf("reverse label-only character: %+v, %v; want %q", move, err, want)
}
}
if !username.Valid() || !emoji.Valid() {
t.Fatal("separate display-name and emoji text objects missing")
}
+4 -2
View File
@@ -65,11 +65,13 @@ type DocumentCaretMove struct {
TableRowHeader, TableColumnHeader string
Moved bool
// Native text range used for this move, in Unicode character offsets.
// Text range used for this move, in Unicode character offsets. Named
// links use accessible-label offsets rather than native text offsets.
Start, End int
}
// MoveCaret advances the independent browse position using AT-SPI text units.
// MoveCaret advances the independent browse position using AT-SPI text units
// or accessible-label units for explicitly named links.
// It does not depend on Chromium's retained native caret. A stale captured
// document is reported to the worker for one bounded refresh.
func (cursor *DocumentCursor) MoveCaret(ctx context.Context, reader DocumentCaretReader, command DocumentCaretCommand) (DocumentCaretMove, error) {
+12
View File
@@ -50,6 +50,18 @@ func move_caret_object_line(ctx context.Context, reader DocumentCaretReader, doc
return DocumentCaretMove{Position: documentPosition(doc, *place)}, nil
}
node := doc.Nodes[indices[position]]
if named_link_label(node.Snapshot) != "" && (command == CaretLineStart || command == CaretLineEnd) {
offset = 0
if command == CaretLineEnd {
offset = caret_text_count(node.Snapshot) - 1
}
character, err := read_caret_text(ctx, reader, node, offset, atspi.DocumentCaretCharacter)
if err != nil {
return DocumentCaretMove{}, err
}
place.object, place.offset, place.origin = node.Reference, offset, CaretNavigation
return DocumentCaretMove{Position: documentPosition(doc, *place), Text: character.Text, Moved: true, Start: character.Start, End: character.End}, nil
}
if node.Snapshot.CharacterCount == 0 || wholeCaretNode(node.Snapshot) {
if (command == CaretNextLine || command == CaretPreviousLine) && node.Reference == place.object {
if backwards {
+72
View File
@@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"testing"
"time"
"unicode"
"unicode/utf8"
@@ -258,6 +259,10 @@ func TestDocumentCaretExplicitLinkPreservesNameInsteadOfBadge(t *testing.T) {
path, text string
}{
{CaretNextLine, "/card", ""},
{CaretNextCharacter, "/card", "x"},
{CaretPreviousCharacter, "/card", "E"},
{CaretNextWord, "/card", "Example "},
{CaretNextWord, "/card", "Game "},
{CaretNextLine, "/ordinary_text", "Ordinary link"},
{CaretNextCharacter, "/ordinary_text", "r"},
{CaretPreviousLine, "/card", ""},
@@ -273,6 +278,73 @@ func TestDocumentCaretExplicitLinkPreservesNameInsteadOfBadge(t *testing.T) {
}
}
func TestDocumentCaretNamedLinkCharacterBoundaries(t *testing.T) {
for _, nativeText := range []string{"", "2", "\ufffc"} {
t.Run(fmt.Sprintf("native=%q", nativeText), func(t *testing.T) {
reader := caretTextReader{webID("/text"): nativeText, webID("/next"): "."}
cursor := newCaretTextCursor(t, reader)
ctx := context.Background()
ref, err := cursor.tree.Reference(webID("/text"))
if err != nil {
t.Fatal(err)
}
snapshot, err := cursor.tree.Resolve(ref)
if err != nil {
t.Fatal(err)
}
snapshot.Role, snapshot.Name, snapshot.ExplicitName = "link", "É猫 Info", true
if err := cursor.tree.Replace(webID("/doc"), []atspi.TreeNode{
{Snapshot: atspi.Snapshot{BusName: webID("/doc").BusName, Path: "/doc", Role: "document web"}, Children: []atspi.ObjectID{webID("/text"), webID("/next")}},
{Snapshot: snapshot},
{Snapshot: atspi.Snapshot{BusName: webID("/next").BusName, Path: "/next", Role: "static", CharacterCount: 1}},
}); err != nil {
t.Fatal(err)
}
if _, err := cursor.SyncFocus(ctx, webID("/text")); err != nil {
t.Fatal(err)
}
for _, step := range []struct {
command DocumentCaretCommand
text string
offset int
}{
{CaretNextCharacter, "猫", 1},
{CaretPreviousCharacter, "É", 0},
{CaretNextCharacter, "猫", 1},
{CaretNextCharacter, " ", 2},
{CaretNextCharacter, "I", 3},
{CaretNextCharacter, "n", 4},
{CaretNextCharacter, "f", 5},
{CaretNextCharacter, "o", 6},
{CaretNextCharacter, ".", 0},
{CaretPreviousCharacter, "o", 6},
{CaretPreviousWord, "Info", 3},
{CaretPreviousWord, "É猫 ", 0},
{CaretLineEnd, "o", 6},
{CaretLineStart, "É", 0},
} {
move, err := cursor.MoveCaret(ctx, reader, step.command)
if err != nil || !move.Moved || move.Text != step.text || move.Position.Offset != step.offset {
t.Fatalf("%v: %+v, %v; want %q at %d", step.command, move, err, step.text, step.offset)
}
}
navigator := &boundaryNavigator{fakeCapturer: &fakeCapturer{}, caretTextReader: reader}
worker, err := NewWorker(navigator, time.Second)
if err != nil {
t.Fatal(err)
}
worker.SyncFocus(webID("/text"))
for index, want := range []string{"猫", " "} {
result, _ := worker.moveDocumentCaret(ctx, cursor, CaretNextCharacter, 0)
if result.Err != nil || result.CaretErr != nil || result.DocumentCaretText != want ||
result.DocumentPosition.Offset != index+1 || navigator.offset != 0 || navigator.position != result.DocumentPosition.Object {
t.Fatalf("label offset changed native anchor or lost review progress: %+v; native offset=%d", result, navigator.offset)
}
}
})
}
}
func TestDocumentCaretKeepsDocumentFocusAfterTopLeafIsRemoved(t *testing.T) {
reader := caretTextReader{webID("/first"): "First", webID("/next"): "Next"}
document := atspi.NewSnapshot("/doc", "", "document web", "", "")
+9 -8
View File
@@ -32,7 +32,7 @@ func moveCaretText(ctx context.Context, reader DocumentCaretReader, doc atspi.Do
} else if backwards {
position--
if position >= 0 {
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
offset = caret_text_count(doc.Nodes[indices[position]].Snapshot) - 1
}
} else if place.origin == StructuralNavigation {
// Structural jumps speak the first descendant at offset zero.
@@ -43,7 +43,8 @@ func moveCaretText(ctx context.Context, reader DocumentCaretReader, doc atspi.Do
return DocumentCaretMove{}, err
}
node := doc.Nodes[indices[position]]
if node.Snapshot.CharacterCount == 0 || wholeCaretNode(node.Snapshot) {
count := caret_text_count(node.Snapshot)
if count == 0 || wholeCaretNode(node.Snapshot) && named_link_label(node.Snapshot) == "" {
if node.Reference != place.object {
place.object, place.offset, place.origin = node.Reference, 0, CaretNavigation
return DocumentCaretMove{Position: documentPosition(doc, *place), Moved: true}, nil
@@ -51,12 +52,12 @@ func moveCaretText(ctx context.Context, reader DocumentCaretReader, doc atspi.Do
position += step
offset = 0
if backwards && position >= 0 {
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
offset = caret_text_count(doc.Nodes[indices[position]].Snapshot) - 1
}
continue
}
if offset >= 0 && offset < node.Snapshot.CharacterCount {
character, err := reader.ReadDocumentCaret(ctx, node.Reference, offset, atspi.DocumentCaretCharacter)
if offset >= 0 && offset < count {
character, err := read_caret_text(ctx, reader, node, offset, atspi.DocumentCaretCharacter)
if err != nil && !errors.Is(err, atspi.ErrNoDocumentCaret) {
return DocumentCaretMove{}, err
}
@@ -70,7 +71,7 @@ func moveCaretText(ctx context.Context, reader DocumentCaretReader, doc atspi.Do
place.object, place.offset, place.origin = node.Reference, character.Start, CaretNavigation
return DocumentCaretMove{Position: documentPosition(doc, *place), Text: character.Text, Misspelled: character.Misspelled, Moved: true, Start: character.Start, End: character.End}, nil
}
word, err := reader.ReadDocumentCaret(ctx, node.Reference, offset, atspi.DocumentCaretWord)
word, err := read_caret_text(ctx, reader, node, offset, atspi.DocumentCaretWord)
if err != nil {
return DocumentCaretMove{}, err
}
@@ -89,7 +90,7 @@ func moveCaretText(ctx context.Context, reader DocumentCaretReader, doc atspi.Do
nextOffset = word.End
// WORD ranges may contain a trailing delimiter. Query its
// actual character; spoken word text can be normalized.
last, err := reader.ReadDocumentCaret(ctx, node.Reference, word.End-1, atspi.DocumentCaretCharacter)
last, err := read_caret_text(ctx, reader, node, word.End-1, atspi.DocumentCaretCharacter)
if err != nil {
return DocumentCaretMove{}, err
}
@@ -105,7 +106,7 @@ func moveCaretText(ctx context.Context, reader DocumentCaretReader, doc atspi.Do
position += step
offset = 0
if backwards && position >= 0 {
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
offset = caret_text_count(doc.Nodes[indices[position]].Snapshot) - 1
}
}
return DocumentCaretMove{Position: documentPosition(doc, *place)}, nil
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package review
import (
"context"
"strings"
"unicode"
"unicode/utf8"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
func named_link_label(snapshot atspi.Snapshot) string {
if snapshot.Role == "link" && snapshot.ExplicitName && !snapshot.Editable {
return strings.TrimSpace(snapshot.Name)
}
return ""
}
func caret_text_count(snapshot atspi.Snapshot) int {
if label := named_link_label(snapshot); label != "" {
return utf8.RuneCountInString(label)
}
return snapshot.CharacterCount
}
// Named links can expose no Text interface, or only a badge/embedded object.
// Review the label used by line speech, retaining the link for activation.
// These offsets belong to the label and must never become native text offsets.
func read_caret_text(ctx context.Context, reader DocumentCaretReader, node atspi.DocumentNode, offset int, unit atspi.DocumentCaretUnit) (atspi.DocumentCaretText, error) {
if err := ctx.Err(); err != nil {
return atspi.DocumentCaretText{}, err
}
label := named_link_label(node.Snapshot)
if label == "" {
return reader.ReadDocumentCaret(ctx, node.Reference, offset, unit)
}
characters := []rune(label)
result := atspi.DocumentCaretText{CharacterCount: len(characters)}
if offset < 0 || offset >= len(characters) {
return result, nil
}
start, end := offset, offset+1
if unit == atspi.DocumentCaretWord {
for start > 0 && !unicode.IsSpace(characters[start-1]) {
start--
}
for end < len(characters) && !unicode.IsSpace(characters[end]) {
end++
}
for end < len(characters) && unicode.IsSpace(characters[end]) {
end++
}
}
result.Start, result.End, result.Text = start, end, string(characters[start:end])
return result, nil
}
+7 -1
View File
@@ -332,7 +332,13 @@ func (worker *Worker) placeDocumentCaret(ctx context.Context, result Result) Res
}
deadline, cancel := context.WithTimeout(ctx, worker.captureTimeout)
defer cancel()
result.CaretErr = navigator.SetDocumentCaretPosition(deadline, result.DocumentPosition.Object, result.DocumentFocus, result.DocumentPosition.Offset)
nativeOffset := result.DocumentPosition.Offset
if named_link_label(result.DocumentPosition.Snapshot) != "" {
// The label's review offset has no relationship to the native badge
// or embedded-object text. Place only the link's native anchor.
nativeOffset = 0
}
result.CaretErr = navigator.SetDocumentCaretPosition(deadline, result.DocumentPosition.Object, result.DocumentFocus, nativeOffset)
return result
}
+1 -1
View File
@@ -18,7 +18,7 @@ managed-descendant-presentation,src/cthulhu/scripts/default.py,focus,Resolve foc
ssip-speak-cancel,src/cthulhu/speechdispatcherfactory.py,speech,Send speech and cancellation through SSIP,Storm Dragon,internal/speech,in-progress,go test ./internal/speech -count=1,Maintainer accepted audible non-consuming Control/Shift interruption in the Stage 1 physical Xorg checklist on 2026-09-11.,,Maintainer heard lifecycle and terminal speech plus Control/Shift interruption during the accepted Stage 1 physical Xorg checklist on 2026-09-11.,,Protocol replies 230 and 225 were confirmed against the local service; broader backend and X11 coverage remains pending.
web-structural-navigation,src/cthulhu/scripts/web/script.py,web,Move by headings and other document structure,Storm Dragon,internal/input internal/review internal/app,in-progress,go test -race ./... -count=1; python3 tests/native_browser.py,Private Brave/Xvfb structural and mode/chrome sequences pass on 2026-09-20.,,Physical Xorg listening and wrap acceptance pending.,,Structural navigation follows native caret movement. Echo suppression ends on subsequent input. Failed same-object context capture retries. Delayed document carets cannot override editable focus and stale results cannot overwrite unresolved unrelated focus. All 27 browse symbols pass through after routing is disabled. Complete Python structural settings and dialogs remain pending. Parity review corrections and remaining physical gates: docs/testing/parity-review-follow-up.md.
web-document-recapture,src/cthulhu/ax_object.py,web,Recover heading navigation after browser document replacement,Storm Dragon,internal/atspi internal/review internal/app,in-progress,go test -race ./... -count=1; python3 tests/native_browser.py,Private Brave/Xvfb passes immediate H after loading a new page and after activating its return link on 2026-09-20.,,Physical forum listening acceptance pending.,,Native caches refresh on recapture. Load completion retains the new root and resets browser context and pending reading. Worker page epochs retire queued and in-flight results across reused paths. Native reload and mode/chrome tests remain covered. Installed Go preview and Brave Origin are not verified. On 2026-09-21 semantic invalidation was scoped to captured objects so browser chrome and other applications cannot discard the document. Focused race tests and the full private Brave suite pass; heading/link jumps assert capture reuse. Large-page latency and logged stale-object failures still require physical retesting. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 23 ownership correction rejects loads outside the current application or known window and after owner loss; accepted loads update the focus owner for correct defunct recovery. Five event-loop regressions and app race tests pass; physical acceptance remains pending. September 24 same-geometry window recovery now requires fresh active/showing/non-iconified state before foreground correlation; private recovery regression fails on the earlier preview and passes on the rebuilt one. Address changes outside the owned field are suppressed; popup-associated typing remains supported. September 25 review: recovery now checks the fresh tree when text/geometry recapture discovers a removed focus; deterministic regression and focused race checks pass.
web-caret-line-reading,src/cthulhu/scripts/web/script.py,web,Read displayed document content at the browser caret,Storm Dragon,internal/atspi internal/review internal/app internal/presentation,in-progress,go test -race ./... -count=1; python3 tests/native_browser.py,Private Brave/Xvfb H/Down/H and expanded page transition sequences pass on 2026-09-20.,,Physical Xorg speech and sound listening pending.,,Captured and assembled document lines use role/state sound categories and speech fallback. Unit tests cover visited links and per-object cue deduplication and sound-only versus speech-only policy. Character and word intents remain separate. Ordinary caret movement updates structural position. Initial read-all is implemented; braille and complete line-presentation parity remain pending. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 24 prerequisite review: pending caret/terminal timers extracted under the same event-loop owner; adjacent-character word navigation and whitespace and embedded-marker traversal corrected; whole caret movement deadline added. Full race and isolated Brave/X11/ownership proofs pass; physical and installed-preview gates remain pending. See docs/testing/chromium-browser.md for dependency order. September 24 log correction: browse word and line speech includes link and heading ancestors with role sound policy; character output retains character granularity. Native Brave assertions cover link words and document-start heading level. See docs/testing/chromium-browser.md. September 25 review: trailing non-text objects no longer cause false stale-location errors for line/document-edge movement; focused regression and private Brave suite pass. Physical listening remains open.
web-caret-line-reading,src/cthulhu/scripts/web/script.py,web,Read displayed document content at the browser caret,Storm Dragon,internal/atspi internal/review internal/app internal/presentation,in-progress,go test -race ./... -count=1; python3 tests/native_browser.py,Private Brave/Xvfb H/Down/H and expanded page transition sequences pass on 2026-09-20.,,Physical Xorg speech and sound listening pending.,,Captured and assembled document lines use role/state sound categories and speech fallback. Unit tests cover visited links and per-object cue deduplication and sound-only versus speech-only policy. Character and word intents remain separate. Ordinary caret movement updates structural position. Initial read-all is implemented; braille and complete line-presentation parity remain pending. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 24 prerequisite review: pending caret/terminal timers extracted under the same event-loop owner; adjacent-character word navigation and whitespace and embedded-marker traversal corrected; whole caret movement deadline added. Full race and isolated Brave/X11/ownership proofs pass; physical and installed-preview gates remain pending. See docs/testing/chromium-browser.md for dependency order. September 24 log correction: browse word and line speech includes link and heading ancestors with role sound policy; character output retains character granularity. Native Brave assertions cover link words and document-start heading level. See docs/testing/chromium-browser.md. September 25 review: trailing non-text objects no longer cause false stale-location errors for line/document-edge movement; focused regression and private Brave suite pass. Physical listening remains open. Explicitly named links retain whole-label line speech while character and word review use accessible-label offsets independently of native text. Generic unit and private Brave named-link regressions cover empty native text and badges plus both directions across following punctuation. Live speech and physical Braille acceptance remain pending.
config-read-preview,src/cthulhu/backends/toml_backend.py,settings,Read existing Cthulhu TOML and merge the startup profile,Storm Dragon,internal/config,implemented,go test ./internal/config ./cmd/cthulhu -count=1,,,,,Read-only preview bridge available only through explicit --config selection. The Go-native schema and default path are intentionally separate; the one-shot importer remains Phase 8 work.
config-speech-preview,src/cthulhu/speechdispatcherfactory.py,speech,Apply supported active-profile speech settings,Storm Dragon,internal/config internal/speech internal/app,in-progress,go test ./internal/config ./internal/speech ./internal/presentation ./internal/app -count=1,,,Pending human listening gate,,Applies enablement description punctuation capitalization and default voice settings.
typing-printable-key-echo,src/cthulhu/typing_echo_presenter.py,input,Apply printable typing-echo category settings to terminal and editable text insertions,Storm Dragon,internal/config internal/atspi internal/presentation internal/app,in-progress,go test ./internal/config ./internal/atspi ./internal/presentation ./internal/app -count=1,Maintainer reported the configured printable echo settings worked in minimal physical Xorg tests on 2026-09-12; broader application coverage remains pending.,,Maintainer heard the configured printable echo behavior in minimal tests on 2026-09-12.,,Single-character terminal and editable AT-SPI insertions honor enableKeyEcho enableEchoByCharacter and alphabetic numeric punctuation space and diacritical categories. Multi-character insertions remain presentation events. Preferences offer disabled enabled and edit-fields-only radio choices; edit-fields-only uses native editable metadata and Wine editor focus while preserving configured echo voices. Automated focus and persistence checks pass; live speech and physical Braille acceptance for the new mode remain pending.
1 id python_source subsystem behavior owner go_destination status automated_evidence x11_evidence wayland_evidence speech_evidence braille_evidence notes
18 ssip-speak-cancel src/cthulhu/speechdispatcherfactory.py speech Send speech and cancellation through SSIP Storm Dragon internal/speech in-progress go test ./internal/speech -count=1 Maintainer accepted audible non-consuming Control/Shift interruption in the Stage 1 physical Xorg checklist on 2026-09-11. Maintainer heard lifecycle and terminal speech plus Control/Shift interruption during the accepted Stage 1 physical Xorg checklist on 2026-09-11. Protocol replies 230 and 225 were confirmed against the local service; broader backend and X11 coverage remains pending.
19 web-structural-navigation src/cthulhu/scripts/web/script.py web Move by headings and other document structure Storm Dragon internal/input internal/review internal/app in-progress go test -race ./... -count=1; python3 tests/native_browser.py Private Brave/Xvfb structural and mode/chrome sequences pass on 2026-09-20. Physical Xorg listening and wrap acceptance pending. Structural navigation follows native caret movement. Echo suppression ends on subsequent input. Failed same-object context capture retries. Delayed document carets cannot override editable focus and stale results cannot overwrite unresolved unrelated focus. All 27 browse symbols pass through after routing is disabled. Complete Python structural settings and dialogs remain pending. Parity review corrections and remaining physical gates: docs/testing/parity-review-follow-up.md.
20 web-document-recapture src/cthulhu/ax_object.py web Recover heading navigation after browser document replacement Storm Dragon internal/atspi internal/review internal/app in-progress go test -race ./... -count=1; python3 tests/native_browser.py Private Brave/Xvfb passes immediate H after loading a new page and after activating its return link on 2026-09-20. Physical forum listening acceptance pending. Native caches refresh on recapture. Load completion retains the new root and resets browser context and pending reading. Worker page epochs retire queued and in-flight results across reused paths. Native reload and mode/chrome tests remain covered. Installed Go preview and Brave Origin are not verified. On 2026-09-21 semantic invalidation was scoped to captured objects so browser chrome and other applications cannot discard the document. Focused race tests and the full private Brave suite pass; heading/link jumps assert capture reuse. Large-page latency and logged stale-object failures still require physical retesting. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 23 ownership correction rejects loads outside the current application or known window and after owner loss; accepted loads update the focus owner for correct defunct recovery. Five event-loop regressions and app race tests pass; physical acceptance remains pending. September 24 same-geometry window recovery now requires fresh active/showing/non-iconified state before foreground correlation; private recovery regression fails on the earlier preview and passes on the rebuilt one. Address changes outside the owned field are suppressed; popup-associated typing remains supported. September 25 review: recovery now checks the fresh tree when text/geometry recapture discovers a removed focus; deterministic regression and focused race checks pass.
21 web-caret-line-reading src/cthulhu/scripts/web/script.py web Read displayed document content at the browser caret Storm Dragon internal/atspi internal/review internal/app internal/presentation in-progress go test -race ./... -count=1; python3 tests/native_browser.py Private Brave/Xvfb H/Down/H and expanded page transition sequences pass on 2026-09-20. Physical Xorg speech and sound listening pending. Captured and assembled document lines use role/state sound categories and speech fallback. Unit tests cover visited links and per-object cue deduplication and sound-only versus speech-only policy. Character and word intents remain separate. Ordinary caret movement updates structural position. Initial read-all is implemented; braille and complete line-presentation parity remain pending. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 24 prerequisite review: pending caret/terminal timers extracted under the same event-loop owner; adjacent-character word navigation and whitespace and embedded-marker traversal corrected; whole caret movement deadline added. Full race and isolated Brave/X11/ownership proofs pass; physical and installed-preview gates remain pending. See docs/testing/chromium-browser.md for dependency order. September 24 log correction: browse word and line speech includes link and heading ancestors with role sound policy; character output retains character granularity. Native Brave assertions cover link words and document-start heading level. See docs/testing/chromium-browser.md. September 25 review: trailing non-text objects no longer cause false stale-location errors for line/document-edge movement; focused regression and private Brave suite pass. Physical listening remains open. Captured and assembled document lines use role/state sound categories and speech fallback. Unit tests cover visited links and per-object cue deduplication and sound-only versus speech-only policy. Character and word intents remain separate. Ordinary caret movement updates structural position. Initial read-all is implemented; braille and complete line-presentation parity remain pending. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 24 prerequisite review: pending caret/terminal timers extracted under the same event-loop owner; adjacent-character word navigation and whitespace and embedded-marker traversal corrected; whole caret movement deadline added. Full race and isolated Brave/X11/ownership proofs pass; physical and installed-preview gates remain pending. See docs/testing/chromium-browser.md for dependency order. September 24 log correction: browse word and line speech includes link and heading ancestors with role sound policy; character output retains character granularity. Native Brave assertions cover link words and document-start heading level. See docs/testing/chromium-browser.md. September 25 review: trailing non-text objects no longer cause false stale-location errors for line/document-edge movement; focused regression and private Brave suite pass. Physical listening remains open. Explicitly named links retain whole-label line speech while character and word review use accessible-label offsets independently of native text. Generic unit and private Brave named-link regressions cover empty native text and badges plus both directions across following punctuation. Live speech and physical Braille acceptance remain pending.
22 config-read-preview src/cthulhu/backends/toml_backend.py settings Read existing Cthulhu TOML and merge the startup profile Storm Dragon internal/config implemented go test ./internal/config ./cmd/cthulhu -count=1 Read-only preview bridge available only through explicit --config selection. The Go-native schema and default path are intentionally separate; the one-shot importer remains Phase 8 work.
23 config-speech-preview src/cthulhu/speechdispatcherfactory.py speech Apply supported active-profile speech settings Storm Dragon internal/config internal/speech internal/app in-progress go test ./internal/config ./internal/speech ./internal/presentation ./internal/app -count=1 Pending human listening gate Applies enablement description punctuation capitalization and default voice settings.
24 typing-printable-key-echo src/cthulhu/typing_echo_presenter.py input Apply printable typing-echo category settings to terminal and editable text insertions Storm Dragon internal/config internal/atspi internal/presentation internal/app in-progress go test ./internal/config ./internal/atspi ./internal/presentation ./internal/app -count=1 Maintainer reported the configured printable echo settings worked in minimal physical Xorg tests on 2026-09-12; broader application coverage remains pending. Maintainer heard the configured printable echo behavior in minimal tests on 2026-09-12. Single-character terminal and editable AT-SPI insertions honor enableKeyEcho enableEchoByCharacter and alphabetic numeric punctuation space and diacritical categories. Multi-character insertions remain presentation events. Preferences offer disabled enabled and edit-fields-only radio choices; edit-fields-only uses native editable metadata and Wine editor focus while preserving configured echo voices. Automated focus and persistence checks pass; live speech and physical Braille acceptance for the new mode remain pending.
+1
View File
@@ -9,4 +9,5 @@
<button id="after">After links</button>
<a href="#after"><span>Username </span><span>🧻</span></a>
<p>After emoji</p>
<a href="#after" aria-label="Example Info Page"></a><span>.</span>
</body></html>