Fixed a bug with some edit field types that would cause every event to read the whole field.

This commit is contained in:
Storm Dragon committed 2026-10-23 12:21:34 -04:00
1 parent 736f5a0f68
commit 03e2b31e09
11 files changed
+282 -21

No files matched your search

@@ -4,7 +4,7 @@
pkgbase=cthulhu-git
pkgname=cthulhu-git
_pkgname=cthulhu
pkgver=2026.10.22.r70.gd7b1efe
pkgver=2026.10.23.r72.g736f5a0
pkgrel=1
pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support"
url="https://git.stormux.org/storm/cthulhu"
+49
View File
@@ -0,0 +1,49 @@
# Browser editor typing
Chromium contenteditable textboxes can keep native focus on an `entry` while
editable child paragraphs emit insertion, deletion, caret and selection events.
The native adapter records the nearest focusable writable editor as
`EditableRoot`, just as it records an editable document for document editors.
The event loop accepts that relationship only while that exact editor owns focus.
Lookup stops at document, window, password and read-only boundaries.
The reported ChatGPT failure was captured in the debug log on 2026-10-23:
paragraph text changes were rejected as outside the focused editor because
`editable_root` was empty. Following caret events then spoke the growing line.
Recognizing the parent editor restores configured typing echo and the existing
suppression of caret speech following a text change. This uses accessibility
identity and states; it does not depend on a website, label or DOM class.
## Automated checks
```sh
env CTHULHU_NATIVE_BROWSER_TESTS=editor python3 tests/native_browser.py
env CTHULHU_NATIVE_BROWSER_TESTS=editing,selection,live python3 tests/native_browser.py
python3 tests/native_test_session.py go test ./internal/app ./internal/atspi ./internal/presentation -count=1
```
The editor proof uses real Chromium paragraph events and the reader's event
pipeline on a private Xvfb display and D-Bus session. It checks quiet typing with
echo disabled, parent identity, character and line reading, deletion feedback,
and movement into another editor. Unit coverage checks disabled, character,
word and sentence echo, delayed events after focus leaves, existing editable
document handling, and Braille ownership.
## Live acceptance
After rebuilding and restarting the logging reader through its usual launcher:
1. In ChatGPT, compose several words, pause, and continue typing. With typing
echo disabled, each key must not reread the draft. With character or word
echo enabled, hear only the configured echo.
2. Use Left/Right, Control+Left/Right and Up/Down in a multiline draft. Confirm
character, word and line reading, Backspace/Delete feedback, and Shift-arrow
selection. Check Braille text and cursor placement if available.
3. Tab and Shift+Tab out of and back into the composer. Confirm focus labels
and that typing in another field cannot announce the old draft.
4. Check browser address-bar typing and suggestions, ordinary form fields,
password fields, and live status/response announcements. Check an editable
document body as well as its separate header fields when available.
Private test results do not establish audible speech or physical Braille
acceptance on the original ChatGPT page. That acceptance remains pending.
+148
View File
@@ -0,0 +1,148 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"log/slog"
"os"
"os/exec"
"strings"
"sync/atomic"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
"git.stormux.org/storm/cthulhu/internal/presentation"
)
func TestNativeBrowserEditorTyping(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" {
t.Skip("requires private browser session")
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
adapter := atspi.New()
if err := adapter.Start(ctx); err != nil {
t.Fatal(err)
}
defer adapter.Stop(context.Background())
listener := xorg.New()
if err := listener.Start(ctx); err != nil {
t.Fatal(err)
}
defer listener.Stop(context.Background())
var focus atomic.Pointer[atspi.Snapshot]
var insertion atomic.Pointer[atspi.Event]
observed := &browserEventProbe{Adapter: adapter, events: make(chan atspi.Event, 64)}
relayDone := make(chan struct{})
go func() {
defer close(relayDone)
for {
select {
case event := <-adapter.Events():
if isFocusGainEvent(event) {
focus.Store(&event.Source)
}
if event.Type == atspi.TextInsertedEvent && event.Source.Role == "paragraph" {
insertion.Store(&event)
}
select {
case observed.events <- event:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}()
defer func() { cancel(); <-relayDone }()
queue := &fakeSpeechQueue{spoken: make(chan string, 128)}
done := make(chan struct{})
go func() {
defer close(done)
options := presentation.DefaultOptions()
options.EnableKeyEcho = false
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), queue, observed, nil, nil,
20*time.Millisecond, nil, listener, nil, nil, options)
}()
defer func() { cancel(); <-done }()
keys := func(chord string) {
t.Helper()
if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", chord).CombinedOutput(); err != nil {
t.Fatalf("keys: %v %s", err, out)
}
}
collect := func() []string {
t.Helper()
timer := time.NewTimer(180 * time.Millisecond)
defer timer.Stop()
var texts []string
for {
select {
case text := <-queue.spoken:
texts = append(texts, text)
case <-timer.C:
return texts
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
}
for attempt := 0; attempt < 20; attempt++ {
keys("Tab")
collect()
if current := focus.Load(); current != nil && current.Name == "Message composer" {
break
}
}
editor := focus.Load()
if editor == nil || editor.Name != "Message composer" || !editor.Editable || editor.Role != "entry" {
t.Fatalf("composer focus = %+v", editor)
}
keys("End")
collect()
for _, letter := range []string{"h", "e", "l", "l", "o"} {
keys(letter)
if got := collect(); len(got) != 0 {
t.Fatalf("typing %q with echo disabled spoke %q", letter, got)
}
}
if event := insertion.Load(); event == nil || event.Source.EditableRoot != editor.ID() {
t.Fatalf("paragraph insertion lost editor ownership: %+v", event)
}
keys("Left")
if got := collect(); len(got) != 1 || got[0] != "o" {
t.Fatalf("character reading = %q", got)
}
keys("Down")
if got := collect(); len(got) != 1 || got[0] != "Following line" {
t.Fatalf("next line reading = %q", got)
}
keys("Up")
if got := collect(); len(got) != 1 || got[0] != "Draft:hello" {
t.Fatalf("line reading = %q", got)
}
keys("End")
collect()
keys("BackSpace")
if got := collect(); len(got) != 1 || got[0] != "o" {
t.Fatalf("deletion = %q", got)
}
keys("Tab")
if got := collect(); len(got) == 0 || !strings.Contains(strings.Join(got, " "), "Other editor") {
t.Fatalf("next editor focus = %q", got)
}
keys("x")
if got := collect(); len(got) != 0 {
t.Fatalf("typing in second editor spoke %q", got)
}
if current, event := focus.Load(), insertion.Load(); current == nil || current.Name != "Other editor" ||
event == nil || event.Source.EditableRoot != current.ID() || event.Source.EditableRoot == editor.ID() {
t.Fatalf("second editor ownership: focus=%+v insertion=%+v", current, event)
}
}
+57
View File
@@ -13,8 +13,65 @@ import (
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/braille"
"git.stormux.org/storm/cthulhu/internal/presentation"
)
func TestEditorParagraphUsesConfiguredEcho(t *testing.T) {
for _, echo := range []string{"disabled", "character", "word", "sentence"} {
t.Run(echo, func(t *testing.T) {
options := presentation.DefaultOptions()
payload, contextText, want := "o", "Hello", "o"
switch echo {
case "disabled":
options.EnableKeyEcho, want = false, ""
case "character":
options.EnableEchoByCharacter = true
case "word":
options.EnableEchoByWord = true
payload, contextText, want = " ", "Hello ", "Hello"
case "sentence":
options.EnableEchoBySentence = true
payload, contextText, want = " ", "Hello. ", "Hello."
}
queue := &fakeSpeechQueue{spoken: make(chan string, 16)}
adapter := &fakeAdapter{events: make(chan atspi.Event, 16)}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
defer close(done)
processEvents(ctx, slog.New(slog.DiscardHandler), queue, adapter, 0, options)
}()
defer func() { cancel(); <-done }()
editor := atspi.Snapshot{BusName: ":1.7", Path: "/editor", Name: "Composer", Role: "entry", Editable: true}
paragraph := atspi.Snapshot{BusName: editor.BusName, Path: "/paragraph", Role: "paragraph", Editable: true,
EditableRoot: editor.ID(), Text: contextText, TextBeforeCaret: contextText}
adapter.events <- atspi.NewEvent(atspi.FocusEvent, editor)
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, payload)
adapter.events <- atspi.NewEvent(atspi.CaretEvent, paragraph)
adapter.events <- atspi.NewEvent(atspi.FocusEvent, atspi.Snapshot{BusName: editor.BusName, Path: "/next", Role: "button", Name: "Next"})
// Once focus leaves, delayed events from that editor stay quiet.
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, "delayed edit")
adapter.events <- atspi.NewEvent(atspi.CaretEvent, paragraph)
adapter.events <- atspi.NewEvent(atspi.FocusEvent, atspi.Snapshot{BusName: editor.BusName, Path: "/end", Role: "button", Name: "End"})
wants := []string{"Composer, entry"}
if want != "" {
wants = append(wants, want)
}
wants = append(wants, "Next, button", "End, button")
for _, want := range wants {
select {
case got := <-queue.spoken:
if got != want {
t.Fatalf("speech = %q, want %q", got, want)
}
case <-time.After(time.Second):
t.Fatalf("missing %q", want)
}
}
})
}
}
func TestEditableDocumentParagraphEvents(t *testing.T) {
queue := &fakeSpeechQueue{spoken: make(chan string, 16)}
adapter := &fakeAdapter{events: make(chan atspi.Event, 16)}
@@ -116,7 +116,7 @@ func (adapter *Adapter) captureNativeEventSnapshot(event *C.AtspiEvent, target *
snapshot := snapshotFromNativeWithCaret(target, captureCaret, eventType == CaretEvent, false, adapter.captureSound.Load(), adapter.captureTextAttributes.Load())
if snapshot.Editable && (eventType == CaretEvent || eventType == TextInsertedEvent ||
eventType == TextDeletedEvent || eventType == TextSelectionChangedEvent) {
if editor := C.cthulhu_editable_document(target); editor != nil {
if editor := C.cthulhu_editable_root(target); editor != nil {
snapshot.EditableRoot = nativeObjectID(editor)
C.g_object_unref(C.gpointer(editor))
}
+14 -15
View File
@@ -581,25 +581,24 @@ AtspiAccessible *cthulhu_window_ancestor(AtspiAccessible *source) {
return NULL;
}
// Editable documents can retain native focus while their paragraphs emit
// typing/caret/selection events. Stop at the nearest document boundary so an
// unrelated outer editor cannot claim events from a nested read-only document.
AtspiAccessible *cthulhu_editable_document(AtspiAccessible *source) {
// Editors can retain native focus while their paragraphs emit typing/caret/
// selection events. Use the nearest focusable writable ancestor or editable
// document. Never cross a document, password, or read-only editor boundary.
AtspiAccessible *cthulhu_editable_root(AtspiAccessible *source) {
AtspiAccessible *current = source != NULL ? g_object_ref(source) : NULL;
for (gint depth = 0; current != NULL && depth < 64; depth++) {
AtspiRole role = atspi_accessible_get_role(current, NULL);
if (role == ATSPI_ROLE_DOCUMENT_WEB || role == ATSPI_ROLE_DOCUMENT_FRAME) {
AtspiStateSet *states = atspi_accessible_get_state_set(current);
gboolean editable = states != NULL &&
atspi_state_set_contains(states, ATSPI_STATE_EDITABLE) &&
!atspi_state_set_contains(states, ATSPI_STATE_READ_ONLY) &&
!atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
if (states != NULL) g_object_unref(states);
if (editable) return current;
g_object_unref(current);
return NULL;
}
if (role == ATSPI_ROLE_FRAME || role == ATSPI_ROLE_WINDOW || role == ATSPI_ROLE_APPLICATION) break;
AtspiStateSet *states = atspi_accessible_get_state_set(current);
gboolean blocked = states == NULL || role == ATSPI_ROLE_PASSWORD_TEXT ||
atspi_state_set_contains(states, ATSPI_STATE_READ_ONLY) ||
atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
gboolean editable = !blocked && atspi_state_set_contains(states, ATSPI_STATE_EDITABLE);
gboolean focusable = !blocked && atspi_state_set_contains(states, ATSPI_STATE_FOCUSABLE);
if (states != NULL) g_object_unref(states);
gboolean document = role == ATSPI_ROLE_DOCUMENT_WEB || role == ATSPI_ROLE_DOCUMENT_FRAME;
if (editable && (focusable || document)) return current;
if (blocked || document || focusable) break;
AtspiAccessible *parent = atspi_accessible_get_parent(current, NULL);
g_object_unref(current);
current = parent;
+1 -1
View File
@@ -131,7 +131,7 @@ const gchar *cthulhu_accessible_bus_name(AtspiAccessible *source);
AtspiAccessible *cthulhu_window_ancestor(AtspiAccessible *source);
gboolean cthulhu_may_be_background_notification(AtspiAccessible *source);
AtspiAccessible *cthulhu_document_ancestor(AtspiAccessible *source);
AtspiAccessible *cthulhu_editable_document(AtspiAccessible *source);
AtspiAccessible *cthulhu_editable_root(AtspiAccessible *source);
AtspiEventListenerCB cthulhu_event_callback_pointer(void);
gboolean cthulhu_request_event_loop_quit(void);
gboolean cthulhu_request_tree_capture(gpointer userData);
+2 -2
View File
@@ -90,8 +90,8 @@ type Snapshot struct {
Checked bool
Defunct bool
Editable bool
// EditableRoot identifies the nearest editable web document, copied while
// capturing native events. Paragraph events can belong to its native focus.
// EditableRoot identifies the nearest focusable writable editor or editable
// web document. Paragraph events can belong to its native focus.
EditableRoot ObjectID
Expandable bool
Expanded bool
+1 -1
View File
@@ -14,7 +14,7 @@ background-event-filtering,Orca src/orca/event_manager.py,focus,Reject routine b
xterm-focus-handoff,src/cthulhu/input_event_manager.py,input,Suspend Cthulhu while XTerm owns focus and restore when a known non-XTerm window takes focus,Storm Dragon,internal/app internal/atspi internal/platform/xorg,implemented,NO_COLOR=1 GOCACHE=/tmp/cthulhu-build-cache go test ./internal/app -run '^(TestXTermFocus|TestProcessEvents)' -count=1; NO_COLOR=1 GOCACHE=/tmp/cthulhu-build-cache go test ./internal/platform/xorg -count=1; python3 tests/native_x11.py,Private Xvfb/D-Bus proof passes: XTerm focus suspends speech and command grabs; Insert+Q reaches a real XTerm; an unknown active window remains suspended; a known non-XTerm window restores all existing command and release checks. Maintainer confirms constant live use of XTerm handoff and recovery with good results on 2026-10-22.,,Maintainer confirms XTerm handoff and recovery work very well during constant use on 2026-10-22. See docs/testing/release-acceptance.md.,,Follow-up gates delegated modifier interruption and echo; commands dispatch on press with balanced shutdown draining. Go retains the existing XTerm identity matching focus monitoring grab suspension and event suppression boundary. Notifications bypass suppression. Unknown active windows remain suspended so an incomplete match cannot strand a delegated terminal reader; live XTerm handoff and recovery are accepted by the maintainer on 2026-10-22.
focus-speech,src/cthulhu/scripts/default.py,speech,Speak the focused accessible object including common control states and range values,Storm Dragon,internal/atspi internal/presentation internal/app,implemented,go test ./internal/atspi ./internal/presentation ./internal/app -count=1,Maintainer accepted post-change YAD value and LibreOffice focus value and menu behavior on physical Xorg on 2026-09-12. The combined log had no accessibility-event speech or sound queue overflow; 599 debug diagnostics were dropped during a LibreOffice state-event burst.,,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,Focus presentation includes common states and range values. Focused value changes speak only the value; non-focused state and value events are filtered; menu selection and table-cell verbosity are reduced. Relations position context background progress updates complete role-specific formatting and diagnostics saturation acceptance remain pending. Parity review corrections and remaining physical gates: docs/testing/parity-review-follow-up.md. September 25 review: window-owned title tracking suppresses repeated name events and preserves empty-to-restored title changes; event-loop regression passes. Combo selection follow-up: focus/name/value event regressions verify selection-only speech without repeated combo cues and restore the cue on re-entry; focused app/presentation race checks pass. Current general native focus listening is accepted by the maintainer on 2026-10-22.
terminal-caret-character,src/cthulhu/scripts/default.py,terminal,Speak the character at the caret,Storm Dragon,internal/app,implemented,go test ./internal/atspi ./internal/presentation ./internal/app -count=1,Maintainer accepted terminal caret behavior in the launcher's LXTerminal test application on Xorg during the Stage 1 physical checklist on 2026-09-11.,,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,Uses AT-SPI character granularity with line fallback debounces caret movement and suppresses unchanged path-and-announcement combinations; the maintainer confirms thorough native and terminal live testing on 2026-10-22.
terminal-text-change-echo,src/cthulhu/scripts/terminal/script.py,terminal,Speak sanitized inserted or deleted text from terminals and editable text controls,Storm Dragon,internal/app,implemented,go test ./internal/atspi ./internal/presentation ./internal/app -count=1,"Maintainer accepted terminal insertion in LXTerminal on Xorg on 2026-09-11, accepted the post-change LibreOffice Writer typing behavior on physical Xorg on 2026-09-12, and confirmed YAD close-and-prompt coalescing on physical Xorg on 2026-09-13. Physical acceptance of terminal deletion-redraw coalescing is pending.",,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,Text changes are limited to terminals and editable text or paragraph roles. A change suppresses the next caret event from the same object so typing does not replay the line. Same-terminal insertion within the event-coalescing window suppresses a redundant interrupting terminal-focus presentation. A terminal deletion waits 50 ms and is suppressed only when the same terminal inserts replacement text at the same AT-SPI offset; standalone and different-offset deletions remain presentable. Debug speech delivery records retain the final utterance with cause and policy metadata; normal logging omits speech content. Broader application coverage remains incomplete.
terminal-text-change-echo,src/cthulhu/scripts/terminal/script.py,terminal,Speak sanitized inserted or deleted text from terminals and editable text controls,Storm Dragon,internal/app,implemented,go test ./internal/atspi ./internal/presentation ./internal/app -count=1,"Maintainer accepted terminal insertion in LXTerminal on Xorg on 2026-09-11, accepted the post-change LibreOffice Writer typing behavior on physical Xorg on 2026-09-12, and confirmed YAD close-and-prompt coalescing on physical Xorg on 2026-09-13. Physical acceptance of terminal deletion-redraw coalescing is pending.",,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,"Text changes are limited to terminals and editable text or paragraph roles. A change suppresses the next caret event from the same object so typing does not replay the line. Same-terminal insertion within the event-coalescing window suppresses a redundant interrupting terminal-focus presentation. A terminal deletion waits 50 ms and is suppressed only when the same terminal inserts replacement text at the same AT-SPI offset; standalone and different-offset deletions remain presentable. Debug speech delivery records retain the final utterance with cause and policy metadata; normal logging omits speech content. Broader application coverage remains incomplete. Focusable writable browser editors also own their editable paragraph events, preventing rejected insertions from replaying the growing line through caret speech. Private Chromium coverage and live acceptance steps are in docs/testing/browser-editor.md; original-page ChatGPT listening and physical Braille acceptance remain pending."
managed-descendant-presentation,src/cthulhu/scripts/default.py,focus,Resolve focused container selection and active-descendant changes to the managed child,Storm Dragon,internal/atspi internal/app,implemented,go test ./internal/atspi ./internal/app -count=1; python3 tests/native_x11.py; python3 tests/native_atspi.py --table-only,The isolated GTK/Xvfb/D-Bus selection fixture and physical I38 run-dialog completion selection speech passed on 2026-09-12 without regressing X11 pass-through command consumption release ordering lock states lifecycle or grab cleanup.,,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,The generic adapter registers both selection and active-descendant events resolves their managed child preserves the originating container path and filters events unless the container or child owns focus. Duplicate target presentations are suppressed. No I38-specific workaround was added. Broader GTK and Qt managed-descendant coverage remains pending. October 17: GTK controls with list role and Table interface retain selected-cell focus and table ownership without normalizing ordinary lists or choosing among multiple selected rows. Private native focus and recorded-speech tests cover first Down and genuine Shift+Tab exit; activation descriptions are not repeated on first row selection. Current physical listening and Braille acceptance remain pending. See docs/testing/gui-list-tables.md.
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,implemented,go test -race ./... -count=1; python3 tests/native_browser.py,Private Brave/Xvfb structural and mode/chrome sequences pass on 2026-09-20.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md.,,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.
1 id python_source subsystem behavior owner go_destination status automated_evidence x11_evidence wayland_evidence speech_evidence braille_evidence notes
14 xterm-focus-handoff src/cthulhu/input_event_manager.py input Suspend Cthulhu while XTerm owns focus and restore when a known non-XTerm window takes focus Storm Dragon internal/app internal/atspi internal/platform/xorg implemented NO_COLOR=1 GOCACHE=/tmp/cthulhu-build-cache go test ./internal/app -run '^(TestXTermFocus|TestProcessEvents)' -count=1; NO_COLOR=1 GOCACHE=/tmp/cthulhu-build-cache go test ./internal/platform/xorg -count=1; python3 tests/native_x11.py Private Xvfb/D-Bus proof passes: XTerm focus suspends speech and command grabs; Insert+Q reaches a real XTerm; an unknown active window remains suspended; a known non-XTerm window restores all existing command and release checks. Maintainer confirms constant live use of XTerm handoff and recovery with good results on 2026-10-22. Maintainer confirms XTerm handoff and recovery work very well during constant use on 2026-10-22. See docs/testing/release-acceptance.md. Follow-up gates delegated modifier interruption and echo; commands dispatch on press with balanced shutdown draining. Go retains the existing XTerm identity matching focus monitoring grab suspension and event suppression boundary. Notifications bypass suppression. Unknown active windows remain suspended so an incomplete match cannot strand a delegated terminal reader; live XTerm handoff and recovery are accepted by the maintainer on 2026-10-22.
15 focus-speech src/cthulhu/scripts/default.py speech Speak the focused accessible object including common control states and range values Storm Dragon internal/atspi internal/presentation internal/app implemented go test ./internal/atspi ./internal/presentation ./internal/app -count=1 Maintainer accepted post-change YAD value and LibreOffice focus value and menu behavior on physical Xorg on 2026-09-12. The combined log had no accessibility-event speech or sound queue overflow; 599 debug diagnostics were dropped during a LibreOffice state-event burst. Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md. Focus presentation includes common states and range values. Focused value changes speak only the value; non-focused state and value events are filtered; menu selection and table-cell verbosity are reduced. Relations position context background progress updates complete role-specific formatting and diagnostics saturation acceptance remain pending. Parity review corrections and remaining physical gates: docs/testing/parity-review-follow-up.md. September 25 review: window-owned title tracking suppresses repeated name events and preserves empty-to-restored title changes; event-loop regression passes. Combo selection follow-up: focus/name/value event regressions verify selection-only speech without repeated combo cues and restore the cue on re-entry; focused app/presentation race checks pass. Current general native focus listening is accepted by the maintainer on 2026-10-22.
16 terminal-caret-character src/cthulhu/scripts/default.py terminal Speak the character at the caret Storm Dragon internal/app implemented go test ./internal/atspi ./internal/presentation ./internal/app -count=1 Maintainer accepted terminal caret behavior in the launcher's LXTerminal test application on Xorg during the Stage 1 physical checklist on 2026-09-11. Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md. Uses AT-SPI character granularity with line fallback debounces caret movement and suppresses unchanged path-and-announcement combinations; the maintainer confirms thorough native and terminal live testing on 2026-10-22.
17 terminal-text-change-echo src/cthulhu/scripts/terminal/script.py terminal Speak sanitized inserted or deleted text from terminals and editable text controls Storm Dragon internal/app implemented go test ./internal/atspi ./internal/presentation ./internal/app -count=1 Maintainer accepted terminal insertion in LXTerminal on Xorg on 2026-09-11, accepted the post-change LibreOffice Writer typing behavior on physical Xorg on 2026-09-12, and confirmed YAD close-and-prompt coalescing on physical Xorg on 2026-09-13. Physical acceptance of terminal deletion-redraw coalescing is pending. Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md. Text changes are limited to terminals and editable text or paragraph roles. A change suppresses the next caret event from the same object so typing does not replay the line. Same-terminal insertion within the event-coalescing window suppresses a redundant interrupting terminal-focus presentation. A terminal deletion waits 50 ms and is suppressed only when the same terminal inserts replacement text at the same AT-SPI offset; standalone and different-offset deletions remain presentable. Debug speech delivery records retain the final utterance with cause and policy metadata; normal logging omits speech content. Broader application coverage remains incomplete. Text changes are limited to terminals and editable text or paragraph roles. A change suppresses the next caret event from the same object so typing does not replay the line. Same-terminal insertion within the event-coalescing window suppresses a redundant interrupting terminal-focus presentation. A terminal deletion waits 50 ms and is suppressed only when the same terminal inserts replacement text at the same AT-SPI offset; standalone and different-offset deletions remain presentable. Debug speech delivery records retain the final utterance with cause and policy metadata; normal logging omits speech content. Broader application coverage remains incomplete. Focusable writable browser editors also own their editable paragraph events, preventing rejected insertions from replaying the growing line through caret speech. Private Chromium coverage and live acceptance steps are in docs/testing/browser-editor.md; original-page ChatGPT listening and physical Braille acceptance remain pending.
18 managed-descendant-presentation src/cthulhu/scripts/default.py focus Resolve focused container selection and active-descendant changes to the managed child Storm Dragon internal/atspi internal/app implemented go test ./internal/atspi ./internal/app -count=1; python3 tests/native_x11.py; python3 tests/native_atspi.py --table-only The isolated GTK/Xvfb/D-Bus selection fixture and physical I38 run-dialog completion selection speech passed on 2026-09-12 without regressing X11 pass-through command consumption release ordering lock states lifecycle or grab cleanup. Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md. The generic adapter registers both selection and active-descendant events resolves their managed child preserves the originating container path and filters events unless the container or child owns focus. Duplicate target presentations are suppressed. No I38-specific workaround was added. Broader GTK and Qt managed-descendant coverage remains pending. October 17: GTK controls with list role and Table interface retain selected-cell focus and table ownership without normalizing ordinary lists or choosing among multiple selected rows. Private native focus and recorded-speech tests cover first Down and genuine Shift+Tab exit; activation descriptions are not repeated on first row selection. Current physical listening and Braille acceptance remain pending. See docs/testing/gui-list-tables.md.
19 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.
20 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 implemented go test -race ./... -count=1; python3 tests/native_browser.py Private Brave/Xvfb structural and mode/chrome sequences pass on 2026-09-20. Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md. 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.
+7
View File
@@ -0,0 +1,7 @@
<!doctype html>
<html lang="en"><meta charset="utf-8"><title>Cthulhu Browser Probe</title>
<body>
<div contenteditable="true" role="textbox" aria-label="Message composer" aria-multiline="true"><p>Draft:</p><p>Following line</p></div>
<div contenteditable="true" role="textbox" aria-label="Other editor" aria-multiline="true"><p>Other text</p></div>
<button>After editors</button>
</body></html>
+1
View File
@@ -33,6 +33,7 @@ BROWSER_PROOFS = {
"live": ("./internal/atspi", "^TestNativeBrowserLiveRegion$", 30, 35),
"document": ("./internal/app", "^TestNativeBrowserDocumentCommands$", 45, 50),
"editing": ("./internal/app", "^TestNativeBrowserEditing$", 45, 50),
"editor": ("./internal/app", "^TestNativeBrowserEditorTyping$", 30, 35, "browser-editor.html"),
"selection": ("./internal/app", "^TestNativeBrowserSelection$", 35, 40),
"modes": ("./internal/app", "^TestNativeBrowserModes$", 45, 50),
"keys": ("./internal/platform/xorg", "^TestNativeBrowseKeyTransitions$", 20, 25),