Skip to content

feat(hints): show hints in any Electron app without whitelisting it - #1015

Closed
gabrielecirulli wants to merge 4 commits into
y3owk1n:mainfrom
gabrielecirulli:refactor/ax-enablement-manual-on-all
Closed

feat(hints): show hints in any Electron app without whitelisting it#1015
gabrielecirulli wants to merge 4 commits into
y3owk1n:mainfrom
gabrielecirulli:refactor/ax-enablement-manual-on-all

Conversation

@gabrielecirulli

@gabrielecirulli gabrielecirulli commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Disclaimer: Both this code and this PR description were written by AI. I still need to review this description to make sure it is fully correct, and I will do that soon.

Draft: this branch also needs review and testing before it moves forward.

Rationale

Hints only appear inside an Electron app if its bundle ID is on a built-in list or added to additional_electron_bundles by hand. That list decides whether neru sets AXManualAccessibility, the attribute that wakes an Electron or Chromium accessibility tree. Any unlisted Electron app shows no hints at all.

What this changes

neru now sets AXManualAccessibility on every focused app while hints are enabled. The attribute wakes Electron and Chromium trees and is a harmless no-op on apps that do not implement it, with no window side effect. So it needs no whitelist and no cross-process tree-walk probe to decide when to apply it.

AXEnhancedUserInterface exposes Chromium and Firefox web-page content but can shift windows under tiling window managers, so it stays gated. It is set only on Chromium and Firefox browsers, and only when hints.additional_ax_support.enable is on. Electron apps no longer receive it on their own.

Changes

  • electron.go: replace the three per-family Ensure* functions, the depth-10 tree-walk probe, and the retry loop with one EnsureAppAccessibility that sets manual always and enhanced only when asked. Each successful set is cached per pid, keyed with the bundle ID to survive pid reuse. A failed set is retried on a later focus and logged once, so a real failure is visible without spamming the log every time an app that lacks the attribute is focused.
  • lifecycle.go: run the enablement whenever hints are enabled, and derive the enhanced flag from the Chromium and Firefox lists behind the setting.
  • enable_test.go: cover the cache, retry-on-failure, log-once, enhanced gating, and pid reuse with a fake attribute setter and a log observer.
  • Docs and default config: Electron works out of the box, the setting describes browser web-content hints, the troubleshooting log lines match the code, and a tiling-window-manager caveat covers the enhanced attribute.

The Electron and Chromium bundle lists stay. tree.go still uses them to prune noisy web DOM trees while scanning.

Testing the whole feature

This PR is a prerequisite and does not exercise the feature on its own. To test auto-refresh end to end, pull and run the tip branch feat/hints-auto-refresh-config (#1018), which stacks all four changes. Its description lists the exact config to set and the steps to follow.

Merge order

These four PRs are one series and must merge in order, each only after the one before it lands:

  1. feat(hints): show hints in any Electron app without whitelisting it #1015refactor/ax-enablement-manual-on-all — show hints in any Electron app without whitelisting it ← this PR
  2. refactor(config): rename hints.additional_ax_support to hints.web_content_hints #1016refactor/rename-web-content-hints — rename hints.additional_ax_support to hints.web_content_hints
  3. feat(axobserver): add push-based AX observer service #1017feat/ax-observer-service — push-based AX observer service (inert)
  4. feat(hints): opt-in auto-refresh that keeps hints live on macOS #1018feat/hints-auto-refresh-config — opt-in hints auto-refresh

Each targets main, so until the ones before it merge, its diff also shows their commits.

@gabrielecirulli

Copy link
Copy Markdown
Contributor Author

@greptileai ?

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR broadens accessibility wake-up support for hint mode.

  • Sets manual accessibility for every focused app.
  • Limits enhanced accessibility to configured Chromium and Firefox targets.
  • Adds PID-based attribute caching and updates related tests and docs.

Confidence Score: 4/5

The process cache and launch-time accessibility path can leave hints unavailable.

  • Same-bundle PID reuse carries successful attribute state into a new process.
  • A transient AX readiness failure is no longer retried during the initial focus event.

internal/core/infra/electron/electron.go; internal/app/lifecycle.go

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex attempted the generated focused same-PID/same-bundle setter-count test, but the Linux CGO build blocked due to missing wayland-client pkg-config.
  • T-Rex retried the focused test with CGO disabled, but it could not execute because unrelated Linux overlay methods are absent from the non-CGO build.
  • T-Rex modeled a launch-time AX failure and observed that the run was blocked before tests could start because the Wayland client development package was missing.
  • T-Rex retried the CGO-disabled path, bypassing the package lookup but failing due to missing overlay methods in the Linux environment.
  • The blocker evidence confirms that pkg-config cannot locate wayland-client.pc because libwayland-dev is unavailable in this Linux checkout.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
internal/core/infra/electron/electron.go Replaces framework-specific enablement with a shared per-PID cache for manual and enhanced AX attributes.
internal/app/lifecycle.go Runs accessibility setup on every focused app while hints are enabled and removes the prior retry loop.
internal/core/infra/electron/enable_test.go Adds coverage for sequential cache, failure, enhanced-gating, and cross-bundle PID-reuse behavior.
configs/default-config.toml Clarifies that enhanced AX support is for browser web content.
docs/CONFIGURATION.md Updates AX support documentation and the window-management caveat.
docs/TROUBLESHOOTING.md Updates Electron and browser hint troubleshooting guidance and log messages.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
internal/core/infra/electron/electron.go:89-91
**Same-bundle PID reuse skips setup**

When a process exits and macOS later reuses its PID for a new instance of the same bundle, this comparison preserves the old `manual` and `enhanced` flags. The attributes are process-local, so the new process skips `AXManualAccessibility` and can keep its Electron accessibility tree asleep, leaving hints unavailable until it receives another PID.

### Issue 2 of 2
internal/app/lifecycle.go:439
**Launch-time AX failure is not retried**

A newly launched Electron or Chromium app can receive its focus event before its AX endpoint is ready. This now makes one lookup/set attempt and returns; unlike the removed backoff loop, it does not retry during that activation, so opening hints immediately after launching a slow app can show no targets until the user focuses the app again.

Reviews (1): Last reviewed commit: "feat(hints): show hints in any Electron ..." | Re-trigger Greptile

Comment on lines 89 to 91
if !strings.EqualFold(state.bundle, bundleID) {
state = axState{bundle: bundleID}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Same-bundle PID reuse skips setup

When a process exits and macOS later reuses its PID for a new instance of the same bundle, this comparison preserves the old manual and enhanced flags. The attributes are process-local, so the new process skips AXManualAccessibility and can keep its Electron accessibility tree asleep, leaving hints unavailable until it receives another PID.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/core/infra/electron/electron.go
Line: 89-91

Comment:
**Same-bundle PID reuse skips setup**

When a process exits and macOS later reuses its PID for a new instance of the same bundle, this comparison preserves the old `manual` and `enhanced` flags. The attributes are process-local, so the new process skips `AXManualAccessibility` and can keep its Electron accessibility tree asleep, leaving hints unavailable until it receives another PID.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/app/lifecycle.go
delay *= backoffFactor
}
}()
go electron.EnsureAppAccessibility(bundleID, useEnhanced, a.logger)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Launch-time AX failure is not retried

A newly launched Electron or Chromium app can receive its focus event before its AX endpoint is ready. This now makes one lookup/set attempt and returns; unlike the removed backoff loop, it does not retry during that activation, so opening hints immediately after launching a slow app can show no targets until the user focuses the app again.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/app/lifecycle.go
Line: 439

Comment:
**Launch-time AX failure is not retried**

A newly launched Electron or Chromium app can receive its focus event before its AX endpoint is ready. This now makes one lookup/set attempt and returns; unlike the removed backoff loop, it does not retry during that activation, so opening hints immediately after launching a slow app can show no targets until the user focuses the app again.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR unifies accessibility enablement for focused applications. The main changes are:

  • Set manual accessibility for every focused app.
  • Gate enhanced browser accessibility on the existing setting.
  • Cache successful attribute sets by PID and bundle ID.
  • Update configuration, troubleshooting docs, and unit tests.

Confidence Score: 4/5

The focused-app accessibility path can miss slow-starting apps, and disabling enhanced support does not clear its active browser state.

A one-shot AX setup replaces the prior retry and readiness wait.

Enhanced browser accessibility remains active for an existing process after configuration disables it.

T-Rex T-Rex Logs

What T-Rex did

  • Generated a focused Go test harness to exercise one-shot AX enablement without refocus.
  • Generated a focused Go test harness for enhanced accessibility toggle and attempted to compile, but the build was blocked by a missing wayland-client dependency.
  • Documented environmental blockers that prevented Linux and Darwin AX test runs, with no findings reproduced due to pkg-config and non-CGO overlay issues.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
internal/app/lifecycle.go Runs unified accessibility setup for every activation while hints are enabled.
internal/core/infra/electron/electron.go Replaces per-framework retries with PID-scoped accessibility attribute state.
internal/core/infra/electron/enable_test.go Adds coverage for attribute caching, retries, logging, enhanced gating, and PID reuse.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
internal/app/lifecycle.go:439
**Slow AX startup loses enablement**

When an Electron or Chromium app gains focus before its AX application/tree is ready, this single asynchronous call returns without setting up accessibility. The old path retried with backoff and waited for a usable tree; now hints stay unavailable until the user leaves and refocuses the app.

### Issue 2 of 2
internal/core/infra/electron/electron.go:115
**Enhanced accessibility remains enabled**

After enhanced support is enabled for a browser PID, changing the setting off makes `useEnhanced` false and skips this branch, but no path clears `AXEnhancedUserInterface`. The browser can therefore keep the documented tiling-manager relayout/window-move side effect until it exits even though the configuration now disables enhanced support.

Reviews (2): Last reviewed commit: "feat(hints): show hints in any Electron ..." | Re-trigger Greptile

Comment thread internal/app/lifecycle.go
delay *= backoffFactor
}
}()
go electron.EnsureAppAccessibility(bundleID, useEnhanced, a.logger)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Slow AX startup loses enablement

When an Electron or Chromium app gains focus before its AX application/tree is ready, this single asynchronous call returns without setting up accessibility. The old path retried with backoff and waited for a usable tree; now hints stay unavailable until the user leaves and refocuses the app.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/app/lifecycle.go
Line: 439

Comment:
**Slow AX startup loses enablement**

When an Electron or Chromium app gains focus before its AX application/tree is ready, this single asynchronous call returns without setting up accessibility. The old path retried with backoff and waited for a usable tree; now hints stay unavailable until the user leaves and refocuses the app.

How can I resolve this? If you propose a fix, please make it concise.


return true
}
if useEnhanced && !state.enhanced {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Enhanced accessibility remains enabled

After enhanced support is enabled for a browser PID, changing the setting off makes useEnhanced false and skips this branch, but no path clears AXEnhancedUserInterface. The browser can therefore keep the documented tiling-manager relayout/window-move side effect until it exits even though the configuration now disables enhanced support.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/core/infra/electron/electron.go
Line: 115

Comment:
**Enhanced accessibility remains enabled**

After enhanced support is enabled for a browser PID, changing the setting off makes `useEnhanced` false and skips this branch, but no path clears `AXEnhancedUserInterface`. The browser can therefore keep the documented tiling-manager relayout/window-move side effect until it exits even though the configuration now disables enhanced support.

How can I resolve this? If you propose a fix, please make it concise.

@y3owk1n

y3owk1n commented Jul 12, 2026

Copy link
Copy Markdown
Owner

First, thanks for all the hard work on this matter! Appreciate all the efforts and energy that you brings in to the project.

Since this is the first tip of the stacked PRs, I will just comment on this first and ignore the rest of the PRs until this is merged, as it's very scoped and easy to look through and comment on it.

I still think that we should keep the gate of hints.additional_ax_support.enable. If it's disabled (which should be the default still), we should not attempt to set the attributes. If it's enabled, we can set the attributes accordingly.

I am just thinking if setting it on every app is a good practice or not. Should we look into a way to detect electron bundles or framework and set only on those instead? A project of my friend has some example that seems working nicely, feel free to check it out. https://github.com/blindFS/Glyphlow/blob/a5d7e5ba3f5a61f1641b309e2ee66bf7a05b79e3/src/os_util.rs. By detecting the framework properly, we probably don't have to simply set the attributes to every app, saves an additional AX call and avoid potential side effects on existing native apps too (if it ever will, not sure).

And also if this works as intended, we could also remove the additional_electron_bundles from the config in this PR straight. If I understand it correctly, at this point, this field is almost considered dead and does nothing.

Just a side note, even if it's breaking change, don't do feat(hints)!: ...., as release please will bump this as major, which will bump to v2. Just mention the breaking change in the PR description will do.

Comment thread internal/app/lifecycle.go
gabrielecirulli and others added 3 commits July 13, 2026 14:38
Hints previously appeared inside an Electron app only if its bundle ID was on a
built-in list or added to additional_electron_bundles by hand, because that list
decided whether neru set AXManualAccessibility to wake the app's accessibility
tree. Unlisted Electron apps showed no hints.

neru now sets AXManualAccessibility on every focused app whenever hints are
enabled. The attribute wakes Electron and Chromium trees and is a harmless no-op
on apps that do not implement it, with no window side effect, so it needs no
whitelist and no cross-process tree-walk probe to decide when to apply it.

AXEnhancedUserInterface, which exposes Chromium/Firefox web-page content but can
move windows under tiling window managers, stays gated. It is set only on
Chromium/Firefox browsers, and only when hints.additional_ax_support.enable is
on. Electron apps no longer receive it on their own.

Changes:
- electron.go: replace the three per-family Ensure* functions, the depth-10
  tree-walk probe, and the retry loop with one EnsureAppAccessibility that sets
  manual always and enhanced only when asked. Each successful set is cached per
  pid (keyed with the bundle id to survive pid reuse); a failed set is retried
  on a later focus and logged once so a real failure is visible without spamming
  the log on every focus of an app that does not support the attribute.
- lifecycle.go: run the enablement whenever hints are enabled rather than only
  when the setting is on, and derive the enhanced flag from the Chromium/Firefox
  lists behind the setting.
- enable_test.go: cover the cache, retry-on-failure, log-once, enhanced gating,
  and pid reuse with a fake attribute setter and a log observer.
- electron_test.go: drop the tests for the deleted Electron-only helpers.
- docs and default config: Electron works out of the box, the setting now
  describes browser web-content hints, the troubleshooting log lines match the
  code, and a tiling-window-manager caveat covers the enhanced attribute.

The Electron and Chromium bundle lists stay: tree.go still uses them to prune
noisy web DOM trees while scanning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses review feedback on the accessibility enablement.

- Retry with exponential backoff when an app is focused before its
  accessibility tree is ready, so hints appear in a slow-launching app without
  a manual refocus. The retry lives in EnsureAppAccessibility, and its burst is
  confined to an app's first encounter, so ordinary native apps (which do not
  take the attribute) are not re-probed on every focus.

- Clear AXEnhancedUserInterface on the next focus of a browser after
  hints.additional_ax_support is turned off, so its tiling-window side effect
  does not outlast the setting.

- Drop an app's cached accessibility state when it terminates, so a process
  that later reuses the retired pid has its attributes set again instead of
  inheriting stale per-process flags. This also bounds cache growth.

Tests cover readiness reporting, first-encounter retry gating, the enhanced
teardown, and the terminate-time cache reset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gabrielecirulli
gabrielecirulli force-pushed the refactor/ax-enablement-manual-on-all branch from 740d9df to f3d1b87 Compare July 13, 2026 12:41
neru wakes a focused app's accessibility tree so it can read hint targets.
It sets AXManualAccessibility on every focused app, and sets
AXEnhancedUserInterface on browsers that need it to expose web-area content.

AXEnhancedUserInterface can relayout or move windows under tiling window
managers, so it should stay off where it is not needed. Chromium browsers
(Chrome, Arc, Brave) expose their web-area hint targets with
AXManualAccessibility alone, so this restricts the enhanced attribute to
Firefox browsers, which still require it. Chromium browsers now get
AXManualAccessibility only.

This also removes the electron.ShouldEnableChromiumSupport and
electron.IsLikelyChromiumBundle helpers and their tests, which only fed the
dropped Chromium branch of the enhanced-attribute gate.

The additional_chromium_bundles config field and KnownChromiumBundles stay:
the accessibility-tree traversal (isChromiumOrElectron) uses them to find
web-area hint targets in Chromium and Electron apps, which is independent of
the enhanced attribute.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gabrielecirulli

gabrielecirulli commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

I've made a couple relevant findings:

  • Electron seems to build accessibility trees even without setting AXManualAccessibility. The only difference is that the tree isn't available on first launch and only appears after I Cmd+Tab away and then back to the Electron app.
    • This looks like an implicit electron behavior because I don't see any code on our end that does it.
  • Chromium-based browsers (tested Arc.app, Google Chrome.app) do NOT require AXEnhancedUserInterface. Setting AXManualAccessibility is sufficient to get hints.
    • I've torn out the AXEnhancedUserInterface setting for Chromium in dddbf4a
    • Am I missing something? Was AXEnhancedUserInterface needed for something else?

For now the code that sets AXManualAccessibility on all apps is still in because it seems necessary for Electron on first launch.

--

@y3owk1n following up on your comment above:

I still think that we should keep the gate of hints.additional_ax_support.enable. If it's disabled (which should be the default still), we should not attempt to set the attributes. If it's enabled, we can set the attributes accordingly.

IMHO, Neru should show hints in browsers and Electron by default and without config. That's the reason why I suggested blindly setting AXManualAccessibility, justified by the fact that it's a no-op on non-Chromium apps and the call adds minimal overhead.

As things currently stand in this PR, that's the only attribute needed (with the exception of Firefox, where AXEnhancedUserInterface is still required). Given what we know, it doesn't seem worth gating behind config.

AXEnhancedUserInterface is still gated. Right now it's only used for Firefox.

I am just thinking if setting it on every app is a good practice or not. Should we look into a way to detect electron bundles or framework and set only on those instead? A project of my friend has some example that seems working nicely, feel free to check it out. https://github.com/blindFS/Glyphlow/blob/a5d7e5ba3f5a61f1641b309e2ee66bf7a05b79e3/src/os_util.rs. By detecting the framework properly, we probably don't have to simply set the attributes to every app, saves an additional AX call and avoid potential side effects on existing native apps too (if it ever will, not sure).

Personally, if AXManualAccessibility is a no-op on apps that don't support it, I don't see a problem setting it on any app.

Detecting Electron apps would add complexity. Although your friend's project uses a fairly elegant approach, it's still extra maintenance burden.

There's always a minor risk that someone somewhere will implement a custom attribute with exactly the same name as AXManualAccessibility that behaves differently, but it seems unlikely.

And also if this works as intended, we could also remove the additional_electron_bundles from the config in this PR straight. If I understand it correctly, at this point, this field is almost considered dead and does nothing.

Right now AdditionalElectronBundles is still used by tree.go to tell whether the app is Chromium or Electron and alter the behavior of buildTreeRecursive() and shouldIncludeElement().

I haven't checked exactly what it changes, but I assume there's a valid reason why those exceptions were made.

I do agree we should remove AdditionalElectronBundles as it's quite limited and won't account for apps we're not aware of.

We should ask ourselves the following:

  • Are the custom Chromium and Electron behaviors in tree.go still useful?
    • If yes:
      • We should improve Electron detection. Using your friend's approach here would make sense.
      • If we implement your friend's approach, we can stop spraying AXManualAccessibility on all apps, but we'd then be forced use AdditionalChromiumBundles to make sure browsers get the attribute.
    • If no:
      • We should rip out Chromium and Electron detection and related config entirely

Just a side note, even if it's breaking change, don't do feat(hints)!: ...., as release please will bump this as major, which will bump to v2. Just mention the breaking change in the PR description will do.

AFAICT there was only one commit in the stack and one PR title with ! in it. They've been removed.

@y3owk1n

y3owk1n commented Jul 13, 2026

Copy link
Copy Markdown
Owner

I've made a couple relevant findings:

  • Electron seems to build accessibility trees even without setting AXManualAccessibility. The only difference is that the tree isn't available on first launch and only appears after I Cmd+Tab away and then back to the Electron app.

Based on my experience working with accessibility (i could be wrong), is that once you set the AXManualAccessibility on an electron app, it will be set until you restart the app. Restarting neru doesn't reset the AXManualAccessibility, and that's why you probably still get the tree. If you can try to quit and repoen an electron app, not set the AXManualAccessibility, probably you'll just get a accessibility tree with just macOS native window frame.

  • This looks like an implicit electron behavior because I don't see any code on our end that does it.

in lifecycle.go, we do have a heuristic retry for electron to ensure we poll until we get a proper tree, removing that will likely cause the issue where it's set, but need another focus to actually load the tree. As far as i remember.

  • Chromium-based browsers (tested Arc.app, Google Chrome.app) do NOT require AXEnhancedUserInterface. Setting AXManualAccessibility is sufficient to get hints.

I think it's still required as they are 2 different things for 2 different platform (maybe I am wrong), but same as electron, attributes that are set doesnt get unset when you restart neru. You have to restart the actual app instead. Note that if you have other apps like homerow for example that is running, it will automatically set these attributes too without neru acknoweledges, and neru will still be able to get the tree without needing to set any attributes.

  • I've torn out the AXEnhancedUserInterface setting for Chromium in dddbf4a
  • Am I missing something? Was AXEnhancedUserInterface needed for something else?

As far as I remember, AXEnhancedUserInterface is the only way to ask chrome to report back with voice over compatible tree (meaning accessibility tree). And AXManualAccessibility is something that invented by electron projects.

For now the code that sets AXManualAccessibility on all apps is still in because it seems necessary for Electron on first launch.

Feel free to try what I said above, quit all apps that will implicitly set those attributes (e.g. homerow, shortcat, hammerspoon. etc), and also quit the chrome or electron apps and restart them, so that these attributes are unset. At this point, you shouldn't get any hints for the web page content at all. And then try to enable those attributes and re-test again. To turn off the attributes, you need to restart the app, not neru.

I'll leave the next part uncommented first, if what I said above is true, you might have a different conclusion for the comment follow up section, let me know if you have tested the above.

@gabrielecirulli

Copy link
Copy Markdown
Contributor Author

Based on my experience working with accessibility (i could be wrong), is that once you set the AXManualAccessibility on an electron app, it will be set until you restart the app. Restarting neru doesn't reset the AXManualAccessibility, and that's why you probably still get the tree. If you can try to quit and repoen an electron app, not set the AXManualAccessibility, probably you'll just get a accessibility tree with just macOS native window frame.

That's what I initially suspected too, but during testing I made sure to recompile and rerun Neru and restart each app every time. I was still seeing that behavior.

In any case it's not a very useful behavior since it doesn't help on first launch, unless we find a way to force it there without setting the flag.

I think it's still required as they are 2 different things for 2 different platform (maybe I am wrong), but same as electron, attributes that are set doesnt get unset when you restart neru. You have to restart the actual app instead. Note that if you have other apps like homerow for example that is running, it will automatically set these attributes too without neru acknoweledges, and neru will still be able to get the tree without needing to set any attributes.

Hmm, just like with the Electron apps I was restarting the browsers each time. Chrome did seem to keep working whenever I recompiled Neru to only send AXManualAccessibility even when foregoing AXEnhancedUserInterface.

As far as I remember, AXEnhancedUserInterface is the only way to ask chrome to report back with voice over compatible tree (meaning accessibility tree). And AXManualAccessibility is something that invented by electron projects.

When I researched it this morning, it looked like AXManualAccessibility is a feature in Chromium-based browsers too.

Maybe you should give it a spin as well, commenting out handleAdditionalAccessibility's content in this branch and checking if you can observe the same behavior.

@gabrielecirulli

gabrielecirulli commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, I was probably wrong about Chromium supporting AXManualAccessibility. I'm still confused by what I observed however:

  • When disabling the code path that sets AXEnhancedUserInterface, I would still get hints in Chrome/Arc
  • When also disabling the code path that sets AXManualAccessibility I would get no hints whatsoever in either browser

I'm 99% sure I was restarting the browser each run.

I'll need to test again, but I have no good explanation for what I saw, other than maybe some other part of Neru's code setting AXEnhancedUserInterface, or a query causing the browser to set up the tree, or some other app on my system (though I'm not sure which it might be) turning on the flag.

@y3owk1n

y3owk1n commented Jul 13, 2026

Copy link
Copy Markdown
Owner

That's what I initially suspected too, but during testing I made sure to recompile and rerun Neru and restart each app every time. I was still seeing that behavior.

Hey, you're right about this, i actually spin up a test manually with AXManualAccessibility to be set on my brave browser, and it works (good finding about this). This changes the story for this PR, i am sorry that I didn't spend time to actual test it out.

The only thing is that, I am not sure how recent is this tho, if we are all-in into electron + chromium with AXManualAccessibility auto detection, i hope that it doesnt break user's browser when they are on older versions (not sure and I can't confirm anything about this... lol)

In any case it's not a very useful behavior since it doesn't help on first launch, unless we find a way to force it there without setting the flag.

I think in our main branch right now, we already have a function to poll on accessibility waitForAccessibility, what it does is that it will keep retrying to crawl the tree, until it gets AXWebArea or AXScrollArea, which is an indication that we have a usable tree.

I quickly glance through your code, seems like you're doing ensureAppAccessibilityOnce where it try to set it once. Not sure how difference it will be without polling the tree. You can build from main and compare the behaviour.

I think it's still required as they are 2 different things for 2 different platform (maybe I am wrong), but same as electron, attributes that are set doesnt get unset when you restart neru. You have to restart the actual app instead. Note that if you have other apps like homerow for example that is running, it will automatically set these attributes too without neru acknoweledges, and neru will still be able to get the tree without needing to set any attributes.

Hmm, just like with the Electron apps I was restarting the browsers each time. Chrome did seem to keep working whenever I recompiled Neru to only send AXManualAccessibility even when foregoing AXEnhancedUserInterface.

As far as I remember, AXEnhancedUserInterface is the only way to ask chrome to report back with voice over compatible tree (meaning accessibility tree). And AXManualAccessibility is something that invented by electron projects.

When I researched it this morning, it looked like AXManualAccessibility is a feature in Chromium-based browsers too.

Maybe you should give it a spin as well, commenting out handleAdditionalAccessibility's content in this branch and checking if you can observe the same behavior.

Forget on what I said previously, as I can confirmed that AXManualAccessibility now works at least on my brave browser, did not test other chromium builds tho.


Again I am sorry that I did not validate what I said properly and just based on what I remember, that's a huge mistake. What we should do next is that:

  1. We can set AXManualAccessibility to every app (we have 2 ways here, 1 way is to always run a hasUsableAccessibilityTree that runs a shallow tree traverse to try to get AXWebArea or AXScrollArea, if we get one of these, skip setting AXManualAccessibility, another way is to always set AXManualAccessibility regardless. Not sure which is better or which is more performant in long run.
  2. We can then remove the electron and chromium in configuration.
  3. For firefox, it's a little complicated, i just tested it. AXManualAccessibility will allow a shallow tree for some reason, and AXEnhancedUserInterface will allow us to get a full accessible tree. If we are going to have fully auto, i would like to think a way to automatically enable AXEnhancedUserInterface for the right app (maybe we need some heuristic detection here). Maybe you can verify about this too.

If all of the above are possible, i would say, go for fully automatic. Remove additional_ax_support once and for all. For tree.go those are more like heuristic early exits for different type of app (for performance). What i would do for these is that, we should probably have a simple check that pass all the way down to tree.go about the likelihood if it's electron or firefox or chromium or webkit. Maybe through a list of hardcoded items or framework detection or however, not sure yet.


Sorry, I was probably wrong about Chromium supporting AXManualAccessibility. I'm still confused by what I observed however:

  • When disabling the code path that sets AXEnhancedUserInterface, I would still get hints in Chrome/Arc
  • When also disabling the code path that sets AXManualAccessibility I would get no hints whatsoever in either browser

I'm 99% sure I was restarting the browser each run.

I'll need to test again, but I have no good explanation for what I saw, other than maybe some other part of Neru's code setting AXEnhancedUserInterface, or a query causing the browser to set up the tree, or some other app on my system (though I'm not sure which it might be) turning on the flag.

I am equally confused now as I came to the same conclusion as what you're describing. And i've checked that only 1 place were setting AXEnhancedUserInterface. The behaviour that I am observing now:

chromium:

  • set AXManualAccessibility works, i don't know why

firefox:

  • set AXManualAccessibility works, but just gave me a shallow tree
  • set AXEnhancedUserInterface works, but gave me the full tree

another finding:

i commented out all of the setting attribute thingy, it seems like as long as manually query the tree a couple of times, and it got activated? I don't know yet. Please allow me some time to verify and do a thorough test... I am confused too...

@gabrielecirulli

gabrielecirulli commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Some more observations (and updates on what I reported earlier)

  • When Neru sets NO flags: hints mode never works after right starting the browser (even if attempted multiple times), but does start working after you Cmd+Tab to another app and back. Same behavior I reported with Electron earlier.
  • When Neru only sets AXManualAccessibility: hints mode works immediately.

Since we've established that nothing in Chromium understands the AXManualAccessibility, my theory is that Chromium does listen to incoming AX calls, and opportunistically sets up the tree when it gets them, even if the content of the calls itself is not usable to Chromium directly.

That would explain why keeping AXManualAccessibility in Neru causes Chromium to provide a tree.

Though it doesn't fully explain why, when we set no flags, the tree does start working only after Cmd+Tab (and not when making repeated attempts at running hints mode before that). Maybe Neru sends something else when the app refocuses? Or something else changes? It couldn't be waitForAccessibility, because this branch removes it.

@gabrielecirulli

Copy link
Copy Markdown
Contributor Author

I've set Claude on reviewing the Neru codebase side by side with the latest Chromium source to see if it turns up something that explains this.

@gabrielecirulli

gabrielecirulli commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Disclaimer: written by Claude, after reviewing the Neru source and the current Chromium source. I've read through it but haven't yet written any code to confirm Claude's findings.

AXManualAccessibility genuinely does nothing in Chromium (zero references in the whole tree). What actually wakes the web tree is an assistive client reading the AXRole of specific elements. On modern macOS the one that matters is the web-contents view, an AXScrollArea inside the window: reading its role is what flips web accessibility on. So a no-flags wake is: read the app element's role, descend and read that AXScrollArea's role, then re-read its children after a beat (activation is async). This is also why your waitForAccessibility poll on main works at all, it crawls down and reads that scroll area, which is the trigger.

However, the code says a second read should return the tree, which does not match what we saw (needs a Cmd+Tab, repeated reads don't do it). No Chromium code reacts to app/window activation, so I can't yet prove where the Cmd+Tab dependency comes from.

The section titled "The wake sequence (no flags)" below shows how an application could theoretically turn on Chrome's (and potentially Electron's) accessibility tree solely by means of accessibility queries, wihtout setting attributes on the process.


Full technical findings (Neru + Chromium source)

What I investigated

  • Neru's hint read path on macOS: internal/core/infra/accessibility/{adapter.go,query.go,tree.go,infra_client.go} and the ObjC in internal/core/infra/platform/darwin/{accessibility_window_darwin.m,accessibility_element_darwin.m}, on branch refactor/ax-enablement-manual-on-all.
  • The current Chromium source (main): the macOS accessibility activation path, mode plumbing, and renderer tree serialization.
  • Cross-checked by two independent source dives and an adversarial review of the conclusion.

AXManualAccessibility does nothing in Chromium

  • Zero occurrences of AXManualAccessibility / ManualAccessibility in the entire Chromium tree.
  • The only recognized attribute is AXEnhancedUserInterface; everything else falls through with no effect (chrome/browser/chrome_browser_application_mac.mm:467-475).

How Chromium actually turns web content on

Chromium activates accessibility lazily, purely as a side effect of an assistive client reading AXRole on specific elements. Two activation points:

  • Application element role (chrome/browser/chrome_browser_application_mac.mm:483-500): on macOS Sonoma+ with the SonomaAccessibilityActivationRefinements feature on, reading it sets only kNativeAPIs; with that feature off, it sets full kAXModeBasic (which includes web content).
  • Web-contents view role (content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm:2272-2288): the RenderWidgetHostViewCocoa is an AXScrollArea (its role literal is NSAccessibilityScrollAreaRole at :2287). Reading its role sets kAXModeBasic = kNativeAPIs | kWebContents, gated on the Sonoma refinement flag. On Sonoma+ this is the only thing that sets kWebContents.
  • Without kWebContents, only native (non-web) mode reaches the page (content/browser/accessibility/browser_accessibility_state_impl.cc:250-252).

The wake sequence (no flags)

  1. Read the AXRole of the application element (AXUIElementCreateApplication(pid)). This is the entry the getters are designed around, and it's the sole web-activating path on the non-Sonoma branch. Neru currently roots traversal at the focused window (internal/core/infra/platform/darwin/accessibility_window_darwin.m:239-318), so it never performs this read.
  2. Descend to and read the AXRole of the web-contents AXScrollArea. That fires the getter above and sets kWebContents. Confirm the traversal doesn't prune the ancestor chain before reaching it, the bounds prune (internal/core/infra/accessibility/tree.go:465, Chromium-only), MaxDepth (default 50), and the hidden/invisible gate (tree.go:450) all run on ancestors and could stop the descent early.
  3. Re-read after one renderer round-trip. The view returns web children only once the browser-side manager exists (render_widget_host_view_cocoa.mm:2255-2260, content/browser/renderer_host/render_widget_host_view_mac.mm:1918-1925), and that manager is built only when the renderer serializes and pushes its tree (content/browser/renderer_host/render_frame_host_impl.cc:11724). The scoped mode set in step 2 persists (render_widget_host_view_cocoa.mm:2279, assigned once, never reset), so a second read after a short delay returns the web root. There is no synchronous "build now" query; every GetOrCreateBrowserAccessibilityManager call site builds off the renderer push.

This is consistent with why waitForAccessibility on main works: repeatedly crawling toward AXWebArea/AXScrollArea ends up reading that scroll area's role, which is the trigger. Removing that poll removes the read.

The unresolved part

The above implies a delayed second read should succeed, which contradicts the observation that with no flags, repeated reads never wake it but a Cmd+Tab away-and-back does. No Chromium code reacts to app activation, window-key, or focus changes in the accessibility path (render_widget_host_view_cocoa.mm:1970-1978, render_widget_host_view_mac.mm:2000-2007, 1779-1791), and the notification path only fires to registered observers (Neru polls, so that's not it). So the Cmd+Tab trigger is not yet pinned to a code cause.

Concrete next step to settle it: instrument Neru to explicitly read the app-element role and the web AXScrollArea role, and log whether that role read happens and the child count on the first read versus a delayed second read. That distinguishes "traversal never reaches the scroll area" (a Neru fix) from "we read it and Chromium still won't serve children to a poller" (deeper).

Caveats

  • Mechanism is version-dependent: the Sonoma-refinements branch changes whether the app-role read alone is enough, worth keeping in mind for older browser builds.
  • For regular browser windows the RenderWidgetHostViewCocoa lives in the browser process; for PWA/app-shim windows it lives in a helper process, which can change reachability.
  • Findings cover Chromium only. Firefox's shallow-vs-full-tree split under AXManualAccessibility vs AXEnhancedUserInterface was not investigated here.

@y3owk1n

y3owk1n commented Jul 14, 2026

Copy link
Copy Markdown
Owner

@gabrielecirulli thanks for all the research, I would like to spare some time to actually do some experiment about this locally and come back to you how should we go about next. It's becoming more interesting now.

I am testing with minimal changes on a new branch at feat/simplify-ax (appreciate if you can help to confirm too). It seems like even on firefox, waking the ax tree will just work, no need to set anything. In the branch, i had removed all gates and attribute settings. What it does it purely just for every app, we wake and detect if the tree has what we want, mark it and continue. This works on electron, native, chromium (including pwa) and firefox. Let me know if it's working for you. You could check out the diff too.

If this works fine, it's pretty cool, and we can head towards this direction.

@y3owk1n

y3owk1n commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Actually, would you like to join our discord channel and discuss over there too? I feel like it's easier to discuss than replying in Gh issues.

@y3owk1n

y3owk1n commented Jul 14, 2026

Copy link
Copy Markdown
Owner

In my branch, I am able to get a reliable heuristic checks for bundle type too ("chromium", "electron", "firefox", "webkit"). Tho it will for sure require maintenance effort in the long run, as these are heuristics detection but that's fine. Tested on my side:

  • Safari
  • Brave Browser
  • Brave Browser in PWA
  • Firefox
  • Discord (electron)

Everything seems working fine and smooth with the POC from feat/simplify-ax.

Summary of the flow that i have here:

  • if hints enabled, during every app activation, we wake the tree (no attribute settings at all)
  • when hints activated, we will detect the bundle type to do some type based filtering later for performance (and the detected type will be cache)

UX changes

  • no longer need any configuration at all, everything is fully automated

@gabrielecirulli

Copy link
Copy Markdown
Contributor Author

Superseded by #1035

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants