From c6f9f7a935b2b96086619d01d4e3a29641dfdfb0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 17:48:46 +0000 Subject: [PATCH 1/5] Add Electron accessibility research note to guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the 13 defects and missing capabilities blocking accessibility-driven text manipulation in Electron apps, allocated by whether Chromium, Electron, or Apple owns each fix. Item IDs (C1-C6, E1-E4, A1-A4) are stable and quotable in upstream bug reports. Cross-linked from the KindaVim guide, which already refers to the Keyboard fallback strategy without explaining why it is needed, and added to the docs landing page. Confidence is marked inline: C3 is inference from observed symptoms rather than verified against Blink source, and the 2023-24 recency of the upstream activity is called out as possibly stale. Not yet copied to gh-pages — destination still pending review. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SZ3bAW2wKrgVMtXxbnc7Kn --- docs.md | 1 + guides/electron-text-parity.md | 195 +++++++++++++++++++++++++++++++++ guides/kindavim.md | 1 + 3 files changed, 197 insertions(+) create mode 100644 guides/electron-text-parity.md diff --git a/docs.md b/docs.md index c539517d7..ebb500b0a 100644 --- a/docs.md +++ b/docs.md @@ -63,6 +63,7 @@ theme: parchment
  • Works With Your Keyboard
  • KindaVim
  • Neovim in the Terminal
  • +
  • Why Text Tools Break in Electron Apps
  • diff --git a/guides/electron-text-parity.md b/guides/electron-text-parity.md new file mode 100644 index 000000000..ce43bcc2e --- /dev/null +++ b/guides/electron-text-parity.md @@ -0,0 +1,195 @@ +--- +layout: default +title: "Why Text Tools Break in Electron Apps" +description: "Research note on the accessibility gaps that stop vim modes and text automation from working in Slack, Discord, and VS Code — and who can fix each one" +theme: parchment +permalink: /guides/electron-text-parity/ +--- + + +# Why Text Tools Break in Electron Apps + +If you use [KindaVim]({{ '/guides/kindavim/' | relative_url }}), a text expander, or any other +keyboard-driven text tool, you have probably noticed the same thing: it works beautifully in Mail, +Notes, and Xcode, then falls apart in Slack. + +This is a research note explaining why. It is written for people who want to fix the problem +upstream, not for people configuring KeyPath — if you just want KindaVim working today, the +[KindaVim guide]({{ '/guides/kindavim/' | relative_url }}) covers the practical workarounds. + +**Short version:** the failure is mostly Chromium's, not Apple's, and most of it is ordinary +bug-fixing rather than missing API. + +--- + +## Five Things Have to Work + +Parity with a native macOS text field is not one capability, it is a chain: + +| # | Capability | Native field | Electron field | +|---|------------|--------------|----------------| +| 1 | Accessibility tree exists | Works | Manual, per app | +| 2 | Tree is trustworthy | Works | Falsely claims support | +| 3 | Reads are correct | Works | Offsets wrong | +| 4 | Writes propagate to the app | Works | Model desync | +| 5 | Changes are observable | Works | Timing unreliable | + +Stage 2 is the load-bearing failure. Because Electron answers "yes, I support accessibility" and +then returns wrong data, no tool can auto-detect its way out — which is why every assistive tool +surveyed ships a hand-curated per-app blocklist instead. + +--- + +## The Gap Register + +Item IDs are stable and intended to be quotable in upstream bug reports. + +### Chromium — 6 gaps, the bulk of the work + +Almost everything that makes Electron text fields unusable lives here, and almost all of it is +ordinary bug-fixing. No Apple dependency. + +**C1 — Text offset mapping is wrong.** A line starting with whitespace reports a character range +off by one. Selecting exactly to end-of-line silently swallows the following line break. This is +Blink's mapping between DOM positions and flat macOS character indices. The reporter shipped a +reproducible Swift test case noting native apps don't behave this way. + +For a navigation tool this misplaces the cursor. For a tool that *replaces* a range, it eats the +user's characters — same bug, an order of magnitude more damage. **This is the highest-value fix.** + +**C2 — Rich `contenteditable` has no coherent linearization.** Slack's composer, Notion, and +Discord are DOM trees, not strings. Mentions, emoji, and code blocks become opaque nodes. There is +no stable contract for how a rich editor projects onto a flat `AXValue` plus character offsets, so +the offsets don't correspond to what the user sees. WebKit handles this better, so it's tractable, +but this is design work rather than a bug fix — the hardest item on the board. + +**C3 — Write-path fidelity.** Setting `AXValue` or `AXSelectedTextRange` needs to route through +Blink's editing pipeline so `beforeinput` and `input` fire and the page's JavaScript observes the +change. If it mutates the DOM without that, editors built on React, Lexical, or ProseMirror +desync: the visible text changes, the app's model doesn't, the message sends stale, and undo +breaks. + +*Unverified — this is reasoning from observed symptoms, not from reading Blink. Check the source +before filing.* + +**C4 — No granular `AXMode` for runtime clients.** The `form-controls` bundle already exists, but +only as the `--force-renderer-accessibility` startup flag. A client enabling accessibility at +runtime gets all-or-nothing `complete`. A text tool wants exactly `form-controls` — cheap enough +that the "off by default for performance" objection evaporates. This is what makes always-on +politically possible rather than merely technically possible. + +**C5 — No tree-readiness signal.** Activation is asynchronous. Enable accessibility, query +immediately, get an empty or partial tree. There's no documented "ready" notification, so every +tool polls or guesses. + +**C6 — No conformance tests for the Mac text APIs.** Without round-trip range tests, C1 regresses +forever. The boring item that decides whether the others stay fixed. + +### Electron — 4 gaps, small surface, decisive impact + +**E1 — Stop advertising accessibility support until the tree is real.** The cheapest +high-leverage change available. It is the reason every assistive tool ships a curated blocklist. +Fix it and auto-detection starts working everywhere, for every tool, immediately — no Apple +involvement, no performance trade-off, small diff. + +**E2 — `AXManualAccessibility` robustness.** The attribute was silently unsettable for roughly two +years: handled internally but never advertised via `accessibilityAttributeNames`, so setting it +returned `kAXErrorAttributeUnsupported`. Fixed in 2023 by an outside contributor. Still needs a +regression test. + +**E3 — Expose the granular mode.** `app.setAccessibilitySupportEnabled` is a boolean. Once C4 +exists it should take a level. + +**E4 — Own the text layer.** The offset bug was closed as not planned with no determination of +whether it was an Electron or a Chromium defect. That triage vacuum — not the difficulty of the +fix — is why these sit for years. + +### Apple — 4 gaps, genuinely nobody else can + +These aren't bugs, they're missing contracts. + +**A1 — There is no documented text-position API.** `AXTextMarker` and `AXTextMarkerRange` are +private, undocumented functions in the HIServices framework. Browsers implement them for VoiceOver +precisely because the public character-index APIs are insufficient for web content. The working +path is undocumented and the documented path is the broken one. This sets the ceiling on how well +any third-party tool can ever handle rich editors. + +**A2 — No first-class assistive-technology activation.** `AXEnhancedUserInterface` means "VoiceOver +is running," not "a tool needs data," and it carries side effects — Vimac removed it after it +caused window-positioning problems with window managers like Magnet. `AXManualAccessibility` is +Electron's private invention that other frameworks don't honor. + +**A3 — Custom attributes have no modern equivalent.** `accessibilityAttributeNames` and +`accessibilitySetValue:forAttribute:` belong to the deprecated informal protocol, with no +`NSAccessibility` replacement. A2's workaround is built on API Apple deprecated without a +successor — which is exactly what made E2's two-year bug possible. + +**A4 — No capability negotiation.** Nothing lets an app declare "my text ranges are correct." That's +why E1 can only ever be a convention Electron chooses to honor, not a contract the system enforces. + +--- + +## Five Tools Hit This Independently + +None of them fixed it. They all routed around it — and that convergence is the strongest evidence +for where the gaps actually are. + +| Tool | What they found | Route around | +|------|-----------------|--------------| +| KindaVim | Slack returns line numbers where caret offsets belong; Firefox reports accessibility enabled when it isn't | Two strategies + curated plist | +| Vimac | `AXEnhancedUserInterface` broke third-party window managers; filed with Apple, Chromium, and Firefox | Manual attribute, frontmost only | +| Espanso | Keystroke injection fails in VS Code, Slack, and Discord | `force_mode: clipboard` | +| Shortcat | "Effectiveness varies depending on the application's accessibility implementation" | Documented caveat | +| Electron #36337 | Reproducible Swift test case for the offset bug | Closed as not planned | + +Every project reached the same three conclusions without coordinating: force-enable the tree, don't +trust the reads, and mutate through the clipboard or synthesized keys rather than through +accessibility writes. + +--- + +## Shortest Path to Parity + +**C1 + C3 + E1** gets you correct reads, writes that stick, and honest detection — roughly 90% of +the gap, with zero Apple dependency. + +**C4** turns that from a per-app opt-in into something shippable on by default. **A1** is the +ceiling on rich `contenteditable`; everything else on Apple's side is ergonomics. + +The practical read for today, while none of this is fixed: Espanso's finding points squarely at +paste-based mutation. It routes around C3 entirely, because the app handles paste through its own +native path — so the JavaScript model updates correctly without anyone touching Blink. + +--- + +## Confidence + +- **High** — C1, C4, C5, E1–E4, and A1–A3 are each grounded in a cited source below. +- **Inference** — C3's specifics are reasoning from symptoms, not from reading Blink. +- **Stale** — Most recent activity found is 2023–24. Nothing surfaced for 2025–26 on the + text-range side, but that's absence of search results, not proof of inactivity. Check the + Chromium tracker before filing against C1. + +--- + +## Sources + +- [electron#36337 ↗](https://github.com/electron/electron/issues/36337) — offset bug, closed not planned +- [electron#37465 ↗](https://github.com/electron/electron/issues/37465) — attribute unsettable +- [electron#10305 ↗](https://github.com/electron/electron/pull/10305) — `AXManualAccessibility` added +- [vimac#78 ↗](https://github.com/nchudleigh/vimac/issues/78) — activation side effects +- [kindaVim#70 ↗](https://github.com/godbout/kindaVim.docs/issues/70) — Firefox false positive +- [kindaVim: reaching inputs ↗](https://github.com/godbout/kindaVim.blahblah/discussions/43) — author's diagnosis +- [Espanso backends ↗](https://espanso.org/docs/matches/basics/) — inject vs clipboard +- [Chromium accessibility overview ↗](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/accessibility/overview.md) — AXMode bundles +- [Chromium Mac accessibility ↗](https://www.chromium.org/developers/accessibility/mac-accessibility/) — activation model +- [hs.axuielement.axtextmarker ↗](https://www.hammerspoon.org/docs/hs.axuielement.axtextmarker.html) — private HIServices API +- [Chromium accessibility performance ↗](https://developer.chrome.com/blog/chromium-accessibility-performance) — the default-off rationale +- [Shortcat ↗](https://shortcat.app/) — compatibility caveat + +--- + +## Where to Go Next + +- **[KindaVim]({{ '/guides/kindavim/' | relative_url }})** — Real vim modes system-wide, and the practical workarounds for apps like Slack +- **[Back to Docs](https://malpern.github.io/KeyPath/docs)** diff --git a/guides/kindavim.md b/guides/kindavim.md index 1a34d87ec..788434756 100644 --- a/guides/kindavim.md +++ b/guides/kindavim.md @@ -115,6 +115,7 @@ The overlay vanishes back to its normal state the instant KindaVim flips to Inse - **[Keyboard Concepts]({{ '/guides/concepts/' | relative_url }})** — Layers, modifiers, and dual-role keys explained - **[Shortcuts Without Reaching]({{ '/guides/home-row-mods/' | relative_url }})** — Combine KindaVim with home row modifiers - **[Neovim in the Terminal]({{ '/guides/neovim-terminal/' | relative_url }})** — Lighter alternative: Leader-layer shortcuts with a HUD reference card +- **[Why Text Tools Break in Electron Apps]({{ '/guides/electron-text-parity/' | relative_url }})** — Research note on why the Keyboard fallback is needed in Slack, and what would have to change upstream - **[One Key, Multiple Actions]({{ '/guides/tap-hold/' | relative_url }})** — Tap-hold for the rare key you want to remap *outside* of vim mode - **[Back to Docs](https://malpern.github.io/KeyPath/docs)** From 2c86b0a7e2484b4387a6d027234c8ce2ee5b3a93 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 17:57:10 +0000 Subject: [PATCH 2/5] Fix accuracy issues in Electron accessibility note Three corrections from review: - The convergence table listed electron#36337 as a fifth "tool" that "routed around" the problem. It is a bug report, and it was closed as not planned, which is the opposite of routing around. Table is now four tools; the bug report moves to prose, where the contrast between workarounds succeeding and upstream fixes stalling is the actual point. - The confidence breakdown rated C3 as inference but silently omitted C2, C6, and A4, which are also uncited assertions. All four are now listed with what specifically is unverified about each. The document invites readers to quote item IDs upstream, so an unmarked assertion is a real hazard. - Gap count was stated as 13; 6 + 4 + 4 is 14. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SZ3bAW2wKrgVMtXxbnc7Kn --- guides/electron-text-parity.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/guides/electron-text-parity.md b/guides/electron-text-parity.md index ce43bcc2e..c3e16cb7b 100644 --- a/guides/electron-text-parity.md +++ b/guides/electron-text-parity.md @@ -129,7 +129,7 @@ why E1 can only ever be a convention Electron chooses to honor, not a contract t --- -## Five Tools Hit This Independently +## Four Tools Hit This Independently None of them fixed it. They all routed around it — and that convergence is the strongest evidence for where the gaps actually are. @@ -140,12 +140,16 @@ for where the gaps actually are. | Vimac | `AXEnhancedUserInterface` broke third-party window managers; filed with Apple, Chromium, and Firefox | Manual attribute, frontmost only | | Espanso | Keystroke injection fails in VS Code, Slack, and Discord | `force_mode: clipboard` | | Shortcat | "Effectiveness varies depending on the application's accessibility implementation" | Documented caveat | -| Electron #36337 | Reproducible Swift test case for the offset bug | Closed as not planned | Every project reached the same three conclusions without coordinating: force-enable the tree, don't trust the reads, and mutate through the clipboard or synthesized keys rather than through accessibility writes. +The contrast with the one serious attempt to fix it upstream is the real finding. Electron +[#36337 ↗](https://github.com/electron/electron/issues/36337) reported the offset bug with a +reproducible Swift test case and was closed as not planned. Working around the gap succeeds; +fixing it does not get traction. + --- ## Shortest Path to Parity @@ -164,8 +168,13 @@ native path — so the JavaScript model updates correctly without anyone touchin ## Confidence -- **High** — C1, C4, C5, E1–E4, and A1–A3 are each grounded in a cited source below. -- **Inference** — C3's specifics are reasoning from symptoms, not from reading Blink. +- **Cited** — C1, C4, C5, E1–E4, and A1–A3 each rest on a primary source listed below. +- **Inference** — Four items are reasoning rather than citation, and should be verified before + anyone quotes them upstream. **C3** (write-path fidelity) argues from observed symptoms rather + than from reading Blink. **C2**'s claim that WebKit linearizes rich editors better is domain + judgement, not a measured comparison. **C6** (absence of conformance tests) is inferred from C1 + persisting, not confirmed against Chromium's test suite. **A4** (no capability negotiation) is an + observation that no such API exists — easy to falsify if one does. - **Stale** — Most recent activity found is 2023–24. Nothing surfaced for 2025–26 on the text-range side, but that's absence of search results, not proof of inactivity. Check the Chromium tracker before filing against C1. From 32f94f762ce9f9f6109839cc60c3c9be19fb59f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 18:04:51 +0000 Subject: [PATCH 3/5] Normalize kindaVim citation URLs to current repo name The sources list cited godbout/kindaVim.docs for issue 70 and godbout/kindaVim.blahblah for discussion 43. Both resolve, because the repository was renamed and GitHub redirects the old path, but citing two names for one project reads as an error. Both now point at kindaVim.blahblah, which is the current name. Verified live: the repository has 993 stars and 582 commits, and issue 70 and discussion 43 both exist under it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SZ3bAW2wKrgVMtXxbnc7Kn --- guides/electron-text-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/electron-text-parity.md b/guides/electron-text-parity.md index c3e16cb7b..932031c14 100644 --- a/guides/electron-text-parity.md +++ b/guides/electron-text-parity.md @@ -187,7 +187,7 @@ native path — so the JavaScript model updates correctly without anyone touchin - [electron#37465 ↗](https://github.com/electron/electron/issues/37465) — attribute unsettable - [electron#10305 ↗](https://github.com/electron/electron/pull/10305) — `AXManualAccessibility` added - [vimac#78 ↗](https://github.com/nchudleigh/vimac/issues/78) — activation side effects -- [kindaVim#70 ↗](https://github.com/godbout/kindaVim.docs/issues/70) — Firefox false positive +- [kindaVim#70 ↗](https://github.com/godbout/kindaVim.blahblah/issues/70) — Firefox false positive - [kindaVim: reaching inputs ↗](https://github.com/godbout/kindaVim.blahblah/discussions/43) — author's diagnosis - [Espanso backends ↗](https://espanso.org/docs/matches/basics/) — inject vs clipboard - [Chromium accessibility overview ↗](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/accessibility/overview.md) — AXMode bundles From 7f1d3b119548375634df586012ca2e7e29a969a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 18:13:43 +0000 Subject: [PATCH 4/5] Trim frontmatter description to match guide convention At 152 characters it was the longest description in guides/, against a median of 85 and a next-highest of 125. Now 121, in line with the existing range. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SZ3bAW2wKrgVMtXxbnc7Kn --- guides/electron-text-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/electron-text-parity.md b/guides/electron-text-parity.md index 932031c14..7d540f656 100644 --- a/guides/electron-text-parity.md +++ b/guides/electron-text-parity.md @@ -1,7 +1,7 @@ --- layout: default title: "Why Text Tools Break in Electron Apps" -description: "Research note on the accessibility gaps that stop vim modes and text automation from working in Slack, Discord, and VS Code — and who can fix each one" +description: "Why vim modes and text automation break in Slack, Discord, and VS Code — the accessibility gaps, and who can fix each one" theme: parchment permalink: /guides/electron-text-parity/ --- From 80c7d1e846227988a49d8ba8d31cf3c9d7aea652 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 18:16:51 +0000 Subject: [PATCH 5/5] Correct E2 attribution and downgrade A3 to inference Verified against the linked sources rather than assumed: - E2 claimed the AXManualAccessibility fix came from "an outside contributor". It did not. electron#38102 was authored by @codebytere, an Electron Member, merged April 2023 and backported to 23/24/25. The claim is replaced with the verifiable facts. The original attribute (electron#10305) was from ivmirx with no visible member badge, but that association is not confirmed either, so no equivalent claim is made about it. - A3 was listed as resting on a primary source. It does not. electron#10305 contains no discussion of NSAccessibility custom attributes; that claim traces to a search-engine summary. Moved to the inference list with the reason stated. Also ties the guide to KindaVim's "Keyboard strategy" terminology, so the cross-link from kindavim.md lands on the concept it promises. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SZ3bAW2wKrgVMtXxbnc7Kn --- guides/electron-text-parity.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/guides/electron-text-parity.md b/guides/electron-text-parity.md index 7d540f656..e7f8d03e0 100644 --- a/guides/electron-text-parity.md +++ b/guides/electron-text-parity.md @@ -20,6 +20,10 @@ upstream, not for people configuring KeyPath — if you just want KindaVim worki **Short version:** the failure is mostly Chromium's, not Apple's, and most of it is ordinary bug-fixing rather than missing API. +It is also the reason KindaVim runs its degraded Keyboard strategy in apps like Slack instead of the +full Accessibility strategy — the accessibility data it would need there isn't trustworthy. See +[KindaVim]({{ '/guides/kindavim/' | relative_url }}) for what that costs you in practice. + --- ## Five Things Have to Work @@ -94,8 +98,9 @@ involvement, no performance trade-off, small diff. **E2 — `AXManualAccessibility` robustness.** The attribute was silently unsettable for roughly two years: handled internally but never advertised via `accessibilityAttributeNames`, so setting it -returned `kAXErrorAttributeUnsupported`. Fixed in 2023 by an outside contributor. Still needs a -regression test. +returned `kAXErrorAttributeUnsupported`. Fixed by +[electron#38102 ↗](https://github.com/electron/electron/pull/38102), merged April 2023 and +backported to the 23/24/25 branches. Still needs a regression test. **E3 — Expose the granular mode.** `app.setAccessibilitySupportEnabled` is a boolean. Once C4 exists it should take a level. @@ -168,13 +173,15 @@ native path — so the JavaScript model updates correctly without anyone touchin ## Confidence -- **Cited** — C1, C4, C5, E1–E4, and A1–A3 each rest on a primary source listed below. -- **Inference** — Four items are reasoning rather than citation, and should be verified before +- **Cited** — C1, C4, C5, E1–E4, and A1–A2 each rest on a primary source listed below. +- **Inference** — Five items are reasoning rather than citation, and should be verified before anyone quotes them upstream. **C3** (write-path fidelity) argues from observed symptoms rather than from reading Blink. **C2**'s claim that WebKit linearizes rich editors better is domain judgement, not a measured comparison. **C6** (absence of conformance tests) is inferred from C1 - persisting, not confirmed against Chromium's test suite. **A4** (no capability negotiation) is an - observation that no such API exists — easy to falsify if one does. + persisting, not confirmed against Chromium's test suite. **A3**'s claim that `NSAccessibility` + offers no replacement for custom attributes traces to a search-engine summary, not to anything in + the source list — electron#10305 itself does not discuss it. **A4** (no capability negotiation) is + an observation that no such API exists — easy to falsify if one does. - **Stale** — Most recent activity found is 2023–24. Nothing surfaced for 2025–26 on the text-range side, but that's absence of search results, not proof of inactivity. Check the Chromium tracker before filing against C1.