Skip to content

Let scanning hooks run outside a provider - #2

Merged
enaboapps merged 1 commit into
mainfrom
optional-provider
Oct 9, 2026
Merged

enaboapps merged 1 commit into
mainfrom
optional-provider

Conversation

@enaboapps

Copy link
Copy Markdown
Contributor

Switchify Remote renders shared buttons both inside and outside scanned screens, and its component tests render them on their own. Until now the hooks threw without a ScanProvider.

  • Outside a provider, useScanController returns one shared controller that is never started, so items register harmlessly and are never highlighted.
  • Version bumped to 0.3.0.

Validation

  • npm test: 61 tests pass, including a new one for a provider-less render.
  • npm run lint is clean.

🤖 Generated with Claude Code

- Outside a ScanProvider the hooks use a shared controller that is never started, so shared components can call them whether or not their screen is scanned

- Bump to 0.3.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@enaboapps
enaboapps merged commit ec92bfd into main Oct 9, 2026
2 checks passed
@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium impact] Scanning hooks now work outside their provider context.

Not safe to merge until provider-less scanner actions cannot start scanning or activate registered controls. Historical-ID retention is a separate, non-blocking concern.

Findings

  1. P1 Provider-less controls can start scanning ▶
  2. P2 Old control IDs accumulate ▶
Fix with agent prompt
### Issue 1
src/react/index.tsx:102
The fallback is a working, shared `ScanController`, not an inert one. Outside a `ScanProvider`, `useScanner().start()` or `dispatch("select")` starts it, and a bound key press through `useKeyboardSwitches` does the same. Controls in unrelated provider-less React trees then share its highlights and activation callbacks. This was reproduced with two separate roots: keyboard input highlighted and activated the second root's control without clicking it. Make the fallback unable to start scanning or activate items, and cover these entry points before merging.

### Issue 2
src/react/index.tsx:93
The module-wide fallback retains every distinct item and group ID it has registered. `ScanController.sequenceOf()` stores IDs in `sequences`, but unregistering removes only the live registrations. Because this fallback lasts for the app session, provider-less lists with changing IDs keep growing that map after their rows unmount. Rendering and cleaning up two independent roots retained 400 historical IDs with no live items, groups, or subscriptions. This is a non-blocking memory-retention concern for long-running apps; avoid storing registration history in the inert fallback.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

T-Rex evidence

▶ Recording of the check

  • Rendered two separate providerless React roots and pressed Space, Enter, and Space on the parent revision, showing hook errors rather than a registered scanner.

▶ Recording of the check

  • Pressed Space, Enter, and Space on the PR revision, showing scanning start, Tree B highlighting, and its handler count increase without clicking its button.

Both providerless roots report hook errors before the PR

  • Captured the parent revision after the keyboard sequence, showing that providerless hooks reject mounting instead of enabling scanning.

Unrelated Tree B is highlighted after the PR

  • Captured the PR revision after Space and Enter, showing an active scanner highlighting the separate providerless root.

Command output from the check

  • Ran all three providerless entry-point cases against the parent source and captured passing assertions that both roots throw before registration.

Command output from the check

  • Ran all three entry-point cases against the PR source and captured passing assertions for cross-root highlighting and handler invocation.

Command output from the check

  • Executed the browser harness and captured DOM observations for both revisions, confirming that the PR enables unrelated-tree activation.

Command output from the check

  • Printed revision hashes, both React implementations, related numbered source, and the production diff, locating F1 at line 102 with no production changes.

Evidence from the check

  • Contains the rendered two-root tests and observed-state logging for public start, public select, and keyboard activation, making the reproduction rerunnable.

Evidence from the check

  • Loads the exact parent React source for the before run without editing production files, keeping the two test runs comparable.

Evidence from the check

  • Runs the supplied command and saves its actual output with command, working directory, and exit code, preserving execution provenance.

Evidence from the check

  • Provides the two-root layout and scan-state styling used in both recordings, making the unrelated-tree highlight visible.

Evidence from the check

  • Mounts two separate roots with real scanning hooks and rendered callback counters, exposing cross-root activation without an item click handler.

Evidence from the check

  • Serves each source revision, performs keyboard and public-start interactions, checks rendered results, and captures videos and posters, reproducing F1 in Chromium.

Evidence from the check

  • This executed integration test changes item and group IDs across two independent roots and inspects controller maps after cleanup, proving historical IDs remain.

Evidence from the check

  • This executed runner extracts the parent React module and captures both test runs with their commands, working directory, and exit codes, making the comparison reproducible.

Evidence from the check

  • The before test executes this parent-revision module with relocated core import paths, showing providerless rendering was previously rejected.

Command output from the check

  • The runner captured revision hashes, the React diff, numbered registration and sequence code, and repository status, tying the runtime result to the requested source location.

Command output from the check

  • The parent-revision test rendered the providerless component tree and captured the expected provider-required exception, showing no fallback controller was created.

Command output from the check

  • The current-revision test captured every generation and unmount checkpoint, showing sequence retention grows from 200 to 400 IDs while live registrations return to zero.

▶ Recording of the check

  • Rendered two separate providerless React roots and pressed Space, Enter, and Space on the parent revision, showing hook errors rather than a registered scanner.

▶ Recording of the check

  • Pressed Space, Enter, and Space on the PR revision, showing scanning start, Tree B highlighting, and its handler count increase without clicking its button.

Both providerless roots report hook errors before the PR

  • Captured the parent revision after the keyboard sequence, showing that providerless hooks reject mounting instead of enabling scanning.

Unrelated Tree B is highlighted after the PR

  • Captured the PR revision after Space and Enter, showing an active scanner highlighting the separate providerless root.

Command output from the check

  • Ran all three providerless entry-point cases against the parent source and captured passing assertions that both roots throw before registration.

Command output from the check

  • Ran all three entry-point cases against the PR source and captured passing assertions for cross-root highlighting and handler invocation.

Command output from the check

  • Executed the browser harness and captured DOM observations for both revisions, confirming that the PR enables unrelated-tree activation.

Command output from the check

  • Printed revision hashes, both React implementations, related numbered source, and the production diff, locating F1 at line 102 with no production changes.

Evidence from the check

  • Contains the rendered two-root tests and observed-state logging for public start, public select, and keyboard activation, making the reproduction rerunnable.

Evidence from the check

  • Loads the exact parent React source for the before run without editing production files, keeping the two test runs comparable.

Evidence from the check

  • Runs the supplied command and saves its actual output with command, working directory, and exit code, preserving execution provenance.

Evidence from the check

  • Provides the two-root layout and scan-state styling used in both recordings, making the unrelated-tree highlight visible.

Evidence from the check

  • Mounts two separate roots with real scanning hooks and rendered callback counters, exposing cross-root activation without an item click handler.

Evidence from the check

  • Serves each source revision, performs keyboard and public-start interactions, checks rendered results, and captures videos and posters, reproducing F1 in Chromium.

Evidence from the check

  • This executed integration test changes item and group IDs across two independent roots and inspects controller maps after cleanup, proving historical IDs remain.

Evidence from the check

  • This executed runner extracts the parent React module and captures both test runs with their commands, working directory, and exit codes, making the comparison reproducible.

Evidence from the check

  • The before test executes this parent-revision module with relocated core import paths, showing providerless rendering was previously rejected.

Command output from the check

  • The runner captured revision hashes, the React diff, numbered registration and sequence code, and repository status, tying the runtime result to the requested source location.

Command output from the check

  • The parent-revision test rendered the providerless component tree and captured the expected provider-required exception, showing no fallback controller was created.

Command output from the check

  • The current-revision test captured every generation and unmount checkpoint, showing sequence retention grows from 200 to 400 IDs while live registrations return to zero.

View artifacts

Summary

This PR lets scanning hooks render outside a ScanProvider by returning a shared fallback controller.

  • Scanning hooks work both inside and outside a provider.

Do not merge until the fallback is genuinely inert. Public scanner actions and keyboard input were reproduced starting the shared scanner, highlighting controls in an unrelated provider-less React tree, and invoking their handlers. Historical registration IDs also accumulate after unmounting; that memory-retention concern is non-blocking.

Controls continue to follow their current provider because registration effects and snapshot subscriptions depend on the controller.

Reviews (1) · Last reviewed commit: "Let scanning hooks run outside a provide..." · Reviewed by Greptile

Comment thread src/react/index.tsx
if (!controller) throw new Error("Scanning hooks must be used inside a ScanProvider.");
return controller;
if (controller) return controller;
inert ??= new ScanController();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Provider-less controls can start scanning

The fallback is a working, shared ScanController, not an inert one. Outside a ScanProvider, useScanner().start() or dispatch("select") starts it, and a bound key press through useKeyboardSwitches does the same. Controls in unrelated provider-less React trees then share its highlights and activation callbacks. This was reproduced with two separate roots: keyboard input highlighted and activated the second root's control without clicking it. Make the fallback unable to start scanning or activate items, and cover these entry points before merging.

Artifacts

▶ Recording of the check

  • Rendered two separate providerless React roots and pressed Space, Enter, and Space on the parent revision, showing hook errors rather than a registered scanner.

▶ Recording of the check

  • Pressed Space, Enter, and Space on the PR revision, showing scanning start, Tree B highlighting, and its handler count increase without clicking its button.

Both providerless roots report hook errors before the PR

  • Captured the parent revision after the keyboard sequence, showing that providerless hooks reject mounting instead of enabling scanning.

Unrelated Tree B is highlighted after the PR

  • Captured the PR revision after Space and Enter, showing an active scanner highlighting the separate providerless root.

Command output from the check

  • Ran all three providerless entry-point cases against the parent source and captured passing assertions that both roots throw before registration.

Command output from the check

  • Ran all three entry-point cases against the PR source and captured passing assertions for cross-root highlighting and handler invocation.

Command output from the check

  • Executed the browser harness and captured DOM observations for both revisions, confirming that the PR enables unrelated-tree activation.

Command output from the check

  • Printed revision hashes, both React implementations, related numbered source, and the production diff, locating F1 at line 102 with no production changes.

Evidence from the check

  • Contains the rendered two-root tests and observed-state logging for public start, public select, and keyboard activation, making the reproduction rerunnable.

Evidence from the check

  • Loads the exact parent React source for the before run without editing production files, keeping the two test runs comparable.

Evidence from the check

  • Runs the supplied command and saves its actual output with command, working directory, and exit code, preserving execution provenance.

Evidence from the check

  • Provides the two-root layout and scan-state styling used in both recordings, making the unrelated-tree highlight visible.

Evidence from the check

  • Mounts two separate roots with real scanning hooks and rendered callback counters, exposing cross-root activation without an item click handler.

Evidence from the check

  • Serves each source revision, performs keyboard and public-start interactions, checks rendered results, and captures videos and posters, reproducing F1 in Chromium.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/react/index.tsx
Line: 102

Comment:
**Provider-less controls can start scanning**

The fallback is a working, shared `ScanController`, not an inert one. Outside a `ScanProvider`, `useScanner().start()` or `dispatch("select")` starts it, and a bound key press through `useKeyboardSwitches` does the same. Controls in unrelated provider-less React trees then share its highlights and activation callbacks. This was reproduced with two separate roots: keyboard input highlighted and activated the second root's control without clicking it. Make the fallback unable to start scanning or activate items, and cover these entry points before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/react/index.tsx
return <ControllerContext.Provider value={controller}>{children}</ControllerContext.Provider>;
}

let inert: ScanController | undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Old control IDs accumulate

The module-wide fallback retains every distinct item and group ID it has registered. ScanController.sequenceOf() stores IDs in sequences, but unregistering removes only the live registrations. Because this fallback lasts for the app session, provider-less lists with changing IDs keep growing that map after their rows unmount. Rendering and cleaning up two independent roots retained 400 historical IDs with no live items, groups, or subscriptions. This is a non-blocking memory-retention concern for long-running apps; avoid storing registration history in the inert fallback.

Artifacts

Evidence from the check

  • This executed integration test changes item and group IDs across two independent roots and inspects controller maps after cleanup, proving historical IDs remain.

Evidence from the check

  • This executed runner extracts the parent React module and captures both test runs with their commands, working directory, and exit codes, making the comparison reproducible.

Evidence from the check

  • The before test executes this parent-revision module with relocated core import paths, showing providerless rendering was previously rejected.

Command output from the check

  • The runner captured revision hashes, the React diff, numbered registration and sequence code, and repository status, tying the runtime result to the requested source location.

Command output from the check

  • The parent-revision test rendered the providerless component tree and captured the expected provider-required exception, showing no fallback controller was created.

Command output from the check

  • The current-revision test captured every generation and unmount checkpoint, showing sequence retention grows from 200 to 400 IDs while live registrations return to zero.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/react/index.tsx
Line: 93

Comment:
**Old control IDs accumulate**

The module-wide fallback retains every distinct item and group ID it has registered. `ScanController.sequenceOf()` stores IDs in `sequences`, but unregistering removes only the live registrations. Because this fallback lasts for the app session, provider-less lists with changing IDs keep growing that map after their rows unmount. Rendering and cleaning up two independent roots retained 400 historical IDs with no live items, groups, or subscriptions. This is a non-blocking memory-retention concern for long-running apps; avoid storing registration history in the inert fallback.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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