From 1b17f12b8067de3bac63656594ff1ba920210b21 Mon Sep 17 00:00:00 2001 From: xell Date: Thu, 25 Jun 2026 23:27:25 +1200 Subject: [PATCH 1/2] feat(editor): expose editor to macOS accessibility clients On macOS, Chromium builds its renderer accessibility tree lazily, only once an assistive client performs the activation handshake. Tools that read the tree without performing that handshake (e.g. the Grammarly desktop app and similar proofreaders) were left blind: they found the app window but never the CodeMirror content, so they appeared to attach yet did nothing. - main: call app.setAccessibilitySupportEnabled(true) in whenReady (darwin), exposing editor content to that class of accessibility clients. - heading fold arrow: aria-hidden. It is an opacity:0 but AX-visible role="button" at the start of every heading line; as a field's leading element it made some clients treat the whole field as a non-prose control and disengage (e.g. any document starting with a heading). Folding is unchanged and still available by mouse, keyboard, and vim. - editor surface: aria-label so clients announce it as a named text field. No rendering or editing behavior changes. Co-Authored-By: Claude Opus 4.8 --- apps/desktop/src/main/index.ts | 13 +++++++++++++ packages/app-core/src/components/EditorPane.tsx | 6 ++++++ packages/app-core/src/lib/cm-heading-fold.ts | 8 ++++++++ 3 files changed, 27 insertions(+) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 6e92c8fd..b2c90da1 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -3258,6 +3258,19 @@ app.whenReady().then(async () => { return } + // Force Chromium's renderer accessibility tree on. By default Chromium builds + // it lazily, only once it detects an assistive client through the macOS + // accessibility activation handshake. Tools that read the tree without + // performing that handshake are otherwise left blind: they find the native + // window but never see the CodeMirror contentEditable text. Enabling it + // unconditionally exposes editor content to that whole class of accessibility + // clients (e.g. the Grammarly desktop app and similar proofreaders, which + // would otherwise show their UI but report nothing). macOS-only to avoid the + // small always-on cost where it isn't needed. + if (process.platform === 'darwin') { + app.setAccessibilitySupportEnabled(true) + } + await migrateLegacyRemoteWorkspaceSecrets() protocol.handle(LOCAL_ASSET_SCHEME, async (request) => { diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index b39aabd3..993e7a74 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -1461,6 +1461,12 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { doc: initialBody, extensions: [ appMarkdownSnippetExtension(), + // Give the editable surface an accessible name so accessibility + // clients (screen readers, proofreaders such as Grammarly) identify + // it as a text field. + EditorView.contentAttributes.of({ + 'aria-label': 'Note editor' + }), vimCompartment.of(s0.vimMode ? vim() : []), historyCompartment.of(history()), drawSelection(), diff --git a/packages/app-core/src/lib/cm-heading-fold.ts b/packages/app-core/src/lib/cm-heading-fold.ts index 78aa80cd..9547bbd3 100644 --- a/packages/app-core/src/lib/cm-heading-fold.ts +++ b/packages/app-core/src/lib/cm-heading-fold.ts @@ -168,6 +168,14 @@ class HeadingFoldArrow extends WidgetType { el.setAttribute('role', 'button') el.setAttribute('aria-label', this.folded ? 'Expand heading' : 'Collapse heading') el.setAttribute('aria-expanded', String(!this.folded)) + // Keep this decorative affordance out of the accessibility tree. It sits + // (via side:-1) at the very start of the heading line, so as an AX-visible + // `role="button"` it becomes the editable field's leading element. Some + // accessibility clients (e.g. Grammarly) inspect a field's leading content + // and, finding a button, treat the whole field as a non-prose control and + // disengage. Folding stays reachable by mouse, keyboard, and vim, so hiding + // it from the accessibility tree costs nothing. + el.setAttribute('aria-hidden', 'true') el.textContent = this.folded ? '▸' : '▾' // Eat mousedown so CodeMirror's own handler doesn't interpret the // click as a caret position and steal focus before we dispatch From 759e89639edc1cad9702a8af8fad3b99438cb1c3 Mon Sep 17 00:00:00 2001 From: xell Date: Thu, 25 Jun 2026 23:27:35 +1200 Subject: [PATCH 2/2] chore(editor): hide decorative live-preview bullet from the accessibility tree The rendered list bullet is decorative, so mark it aria-hidden to keep it out of the field's text value for clients that read it (screen readers, proofreaders). The underlying `- ` source text is unchanged. Co-Authored-By: Claude Opus 4.8 --- packages/app-core/src/lib/cm-wysiwyg-blocks.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/app-core/src/lib/cm-wysiwyg-blocks.ts b/packages/app-core/src/lib/cm-wysiwyg-blocks.ts index 0a73317c..0e198528 100644 --- a/packages/app-core/src/lib/cm-wysiwyg-blocks.ts +++ b/packages/app-core/src/lib/cm-wysiwyg-blocks.ts @@ -40,6 +40,10 @@ class BulletWidget extends WidgetType { const span = document.createElement('span') span.className = 'cm-wq-bullet' span.textContent = '•' + // Decorative marker only — keep it out of the accessibility tree so it + // doesn't pollute the field's text value for clients that read it (screen + // readers, proofreaders). The underlying `- ` is still the source text. + span.setAttribute('aria-hidden', 'true') return span } ignoreEvent(): boolean {