Skip to content
This repository was archived by the owner on Oct 11, 2026. It is now read-only.

feat(scanning): add switch scanning of the Remote interface - #197

Merged
enaboapps merged 1 commit into
mainfrom
codex/switch-scanning-196
Oct 9, 2026
Merged

enaboapps merged 1 commit into
mainfrom
codex/switch-scanning-196

Conversation

@enaboapps

Copy link
Copy Markdown
Contributor

Closes #196

Summary

Switch users can now operate Switchify Remote itself in the web app. It uses @switchify/scanning v0.3.1, the TypeScript port of Switchify PC's item scanner, pinned to a GitHub tag. npm builds it on install through its prepare script.

  • Switch input: keyboard-style switch interfaces work on the web. Each switch has a press action and an optional hold action, Escape stops scanning, and automatic movement pauses while a switch is held. Keys still reach a text field while one has focus.
  • What's scanned: ControlButton, ActionButton, IconButton, ListRow, SelectorField, the PC switcher and the tab buttons all go through one useScannable hook, which draws a 4px ring in the text colour. Surface sections and settings cards scan as groups, ending with a labelled "Leave section" stop.
  • Dialogs and tabs: selectors, the PC switcher, the layout editor and the action picker confine scanning to themselves. Hidden tabs are never scanned, and the highlighted control scrolls into view.
  • Settings → Switch scanning (off by default): automatic or manual, scan speed, sections first or every control, and switches assigned by pressing them. Settings that cannot drive scanning are refused.
  • Storage: settings are saved under preferences.scanning, and unknown or unusable values fall back field by field.
  • Native: unchanged. React Native has no global key events, so scanning is web-only, and the settings card only appears on web.

Library changes made for this, each merged with CI: exclusive groups (switchifyapp/switchify-scanning#1), hooks that work outside a provider (#2), and a fix where undefined options reset a controller to automatic (#3). That bug had made Jest hang on exit.

Validation

  • npm run validate: lint, typecheck, 685 Jest tests (including new preferences, key-capture and scannable-control tests) and 21/21 Expo Doctor checks pass. Jest exits cleanly.
  • Driven in headless Chrome against a production web export with a fake Web Bluetooth PC:
    • Space starts scanning, and Next visits the Surface selector, Edit layout, each section, the PC switcher and the tabs.
    • Entering Movement and selecting Click sent mouse.click to the PC.
    • The end of a section highlights "Leave section", and selecting it returns to the section.
    • The Surface dialog scans only its options and Close, and choosing Typing switched surface.
    • Escape stops scanning.
    • Automatic mode advances every second and pauses while a switch is held.
    • Settings → Set key assigned Digit1 without that press acting on the page. Digit1 then started scanning, Space was unbound, and the assignment survived a reload.

Not yet: live typing is not switch-accessible, because keys go to the text field. Physical switch-interface and screen-reader checks are still needed.

🤖 Generated with Claude Code

- Scan Remote's own controls with @switchify/scanning (switchifyapp/switchify-scanning
  v0.3.1), the TypeScript port of Switchify PC's item scanner
- Read keyboard-style switch interfaces on the web: press and hold actions, Escape to
  stop, automatic movement paused while a switch is held, and keys left to text fields
- Make buttons, rows, selectors, the PC switcher and tabs scannable through a shared
  useScannable hook with a high-contrast ring; scan surface sections and settings cards
  as groups with a labelled "Leave section" stop
- Confine scanning to open dialogs, never scan hidden tabs, and scroll the highlighted
  control into view
- Add a Switch scanning settings card (off by default): automatic or manual, speed,
  pattern, and switches assigned by pressing them, with validation
- Store settings under preferences.scanning with per-field fallback; native is unchanged
- Cover preferences, key capture and scannable controls with tests, and document it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
switchify-remote Ready Ready Preview Oct 9, 2026 12:07pm UTC

Request Review

@enaboapps
enaboapps marked this pull request as ready for review October 9, 2026 12:30
@enaboapps
enaboapps merged commit f6515ac into main Oct 9, 2026
5 checks passed
@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[Medium impact] Adds switch scanning accessibility feature to the web app.

Do not merge until action choices are switch-accessible and key capture reliably cleans up after a missed release. The settings feedback, fallback, and overlapping-edit concerns are non-blocking.

Findings

  1. P1 Action choices cannot be scanned ▶
  2. P1 Key assignment can trap input ▶
  3. P2 Rejected settings look saved ▶
  4. P2 Manual fallback stays invalid ▶
  5. P2 Second edits undo earlier changes ▶
Fix with agent prompt
### Issue 1
src/layouts/ActionPicker.tsx:35-37
The new exclusive `ScanSection` confines scanning to `ActionPicker`, but its action rows are plain `Pressable` controls that never register with `useScannable`. A switch user can open the picker from an empty layout cell, but scanning only reaches Close, not any action. This blocks adding an action through switch input and should be fixed before merging. Register the action rows with the same highlight and activation behavior as the shared buttons.

### Issue 2
src/scanning/keyCapture.ts:45-50
If the browser loses focus while the assigned key is held and the release happens elsewhere, `captureNextKey` never receives the matching `keyup`. Capture stays active after the user returns, swallows every later key including Escape, and keeps `SwitchInput` disabled until that same key is pressed and released again. Cancel capture on window blur or when the page becomes hidden, and keep its cleanup handle until capture actually closes.

### Issue 3
src/scanning/ScanningSettings.tsx:87-88
Changing the only Select binding to Next fails validation, but the Press selector discards the refused save with `.then(() => undefined)`. `SelectorField` consequently closes, displays Next, and announces it as selected while the saved binding remains Select. The unchanged `selectedKey` does not reset that display. This is a non-blocking settings feedback problem: propagate the failed save so the field keeps its saved value instead of presenting an action the switch will not perform.

### Issue 4
src/scanning/preferences.ts:54-59
When stored manual settings have missing or unusable switches, `normalizeScanning` falls back to `DEFAULT_SCANNING.switches` but keeps `automatic: false`. Those defaults have Select and Next but no Previous, so the returned settings still fail `validateSwitchSettings`. Every settings save then fails, including Off. This is a non-blocking recovery problem for damaged or outdated preferences: use manual-compatible fallback switches, or restore automatic movement with the fallback, and test that the resulting combination passes validation.

### Issue 5
src/scanning/ScanningSettings.tsx:37-44
`save` replaces all of `scanning` using the value from the current render. If a user changes two settings while the first storage write is still pending, both saves use the old value. The second queued write then undoes the first change; selecting Every control and then Off persisted the old grouped pattern. This is a non-blocking persistence concern that silently loses a preference edit. Apply each change to the latest settings inside the write queue, or prevent further edits until the save finishes.

---

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 the base picker, pressed Next eight times and Select, then clicked an action; base has no scan target but pointer assignment works.

Base ActionPicker after eight Next switch presses

  • Captured the rendered base picker after switch navigation; the controller reports no highlighted item.

▶ Recording of the check

  • Rendered head and exercised Next, Select, and pointer assignment; switches reach only Close while clicking an action assigns it.

Head ActionPicker after eight Next switch presses

  • Captured the rendered head picker after repeated switch navigation; the controller and DOM highlight identify Close as the sole target.

Command output from the check

  • Recorded actual controller snapshots, callback events, and assertions with command, working directory, and exit code; head confirms Close-only scanning.

Command output from the check

  • Captured the actual Metro build command and output; the executable web harness bundled successfully with exit code zero.

Evidence from the check

  • Contains the executed wrapper that records command, directory, exit code, and browser output; the validation is reproducible.

Evidence from the check

  • Contains base extraction, Metro build, HTTP serving, Chromium interaction, assertions, and recording capture; it executes both sides without production edits.

Evidence from the check

  • Mounts the real picker and keyboard-switch integration with three catalog actions and observed callbacks; no scanning registration or activation handlers are mocked.

Evidence from the check

  • Extracted the picker from the specified base revision and redirected its ActionButton import to the extracted base control; this preserves the before behavior.

Evidence from the check

  • Extracted the base ActionButton and adjusted local dependency paths; the before comparison does not accidentally inherit head Close scanning.

Evidence from the check

  • Contains the Metro-generated JavaScript served to Chromium for both runs; this is the exact compiled execution source.

Evidence from the check

  • The executed Playwright harness targets base and head settings with trusted keys and real tab switching, but onboarding currently blocks its decisive assertions.

Evidence from the check

  • The executed shell wrapper invokes the uploaded harness and records its command, working directory, exit code, and observed output.

Command output from the check

  • The latest executed Chromium run captured the welcome-step DOM and a Settings-locator timeout, showing that the suspected keyboard trap was not reached.

Command output from the check

  • The initial executed browser attempt timed out waiting for the Remote name field, documenting the unsuccessful direct-entry approach.

Command output from the check

  • The actual base revision was exported with Expo and exited successfully, making an authentic before-change browser comparison available.

Before: F1 Pr197 Sol 01

  • What the screen looked like at this point in the check.

▶ f1-pr197-sol-01-before.webm

  • The flow T-Rex ran, recorded end to end.

After: F1 Pr197 Sol 02

  • What the screen looked like at this point in the check.

▶ f1-pr197-sol-02-after.webm

  • The flow T-Rex ran, recorded end to end.

Before: F3 Pr197 Sol Selector Rejection 01

  • What the screen looked like at this point in the check.

▶ f3-pr197-sol-selector-rejection-01-before.webm

  • The flow T-Rex ran, recorded end to end.

After: F3 Pr197 Sol Selector Rejection 02

  • What the screen looked like at this point in the check.

▶ f3-pr197-sol-selector-rejection-02-after.webm

  • The flow T-Rex ran, recorded end to end.

Evidence from the check

  • This authored Playwright script exercises the real settings controls and asserts UI, announcement, and persisted-state behavior, making the reproduction executable.

Evidence from the check

  • This authored shell wrapper runs the browser script and records the command, working directory, exit code, and captured output, making the execution trace reproducible.

▶ Recording of the check

  • Chromium enables scanning and leaves one Select switch, then opens its selected option, showing the valid starting configuration.

Settings with the sole switch set to Select

  • The browser captures the settings after dismissing the initial selector, recording the configuration before the rejected edit.

▶ Recording of the check

  • Chromium chooses Next and reopens the selector after validation rejects the change, capturing the misleading Next display despite unchanged persisted Select.

Settings after the rejected edit and selector reopening

  • The browser captures the final settings state after reopening and dismissing the selector, recording the rejected edit’s lingering UI state.

Command output from the check

  • The successful execution records DOM values, announcement text, stored preferences, and fresh-render behavior, confirming that Next is presented while Select remains persisted.

Evidence from the check

  • This authored Jest test exercises real normalization, validation, preference reload and Settings handlers with valid and unusable manual inputs, establishing the F4 reproduction.

Evidence from the check

  • This executed runner invokes both focused Jest cases and captures each command, working directory, exit code and observed output, making the comparison reproducible.

Command output from the check

  • The valid-manual control run validates successfully and persists On and Off with one write each, showing that the tested persistence path works.

Command output from the check

  • The reproduction run returns invalid manual defaults and rejects On, Off and pattern saves without writing storage, confirming F4.

Evidence from the check

  • The authored test renders the real settings component, gates AsyncStorage completion, and checks persistence and reload, reproducing the lost edit without replacing application logic.

Evidence from the check

  • The executed shell script runs the focused Jest test twice and captures each command, working directory, exit code, and observed output, making both conditions reproducible.

Command output from the check

  • The first executed run completes the pattern write before pressing Off and checks memory, storage, and reload, showing that both edits survive.

Command output from the check

  • The second executed run presses Off before releasing the pattern write and checks both queued payloads and reload, showing that the second save restores the stale grouped pattern.

▶ Recording of the check

  • Rendered the base picker, pressed Next eight times and Select, then clicked an action; base has no scan target but pointer assignment works.

Base ActionPicker after eight Next switch presses

  • Captured the rendered base picker after switch navigation; the controller reports no highlighted item.

▶ Recording of the check

  • Rendered head and exercised Next, Select, and pointer assignment; switches reach only Close while clicking an action assigns it.

Head ActionPicker after eight Next switch presses

  • Captured the rendered head picker after repeated switch navigation; the controller and DOM highlight identify Close as the sole target.

Command output from the check

  • Recorded actual controller snapshots, callback events, and assertions with command, working directory, and exit code; head confirms Close-only scanning.

Command output from the check

  • Captured the actual Metro build command and output; the executable web harness bundled successfully with exit code zero.

Evidence from the check

  • Contains the executed wrapper that records command, directory, exit code, and browser output; the validation is reproducible.

Evidence from the check

  • Contains base extraction, Metro build, HTTP serving, Chromium interaction, assertions, and recording capture; it executes both sides without production edits.

Evidence from the check

  • Mounts the real picker and keyboard-switch integration with three catalog actions and observed callbacks; no scanning registration or activation handlers are mocked.

Evidence from the check

  • Extracted the picker from the specified base revision and redirected its ActionButton import to the extracted base control; this preserves the before behavior.

Evidence from the check

  • Extracted the base ActionButton and adjusted local dependency paths; the before comparison does not accidentally inherit head Close scanning.

Evidence from the check

  • Contains the Metro-generated JavaScript served to Chromium for both runs; this is the exact compiled execution source.

Evidence from the check

  • The executed Playwright harness targets base and head settings with trusted keys and real tab switching, but onboarding currently blocks its decisive assertions.

Evidence from the check

  • The executed shell wrapper invokes the uploaded harness and records its command, working directory, exit code, and observed output.

Command output from the check

  • The latest executed Chromium run captured the welcome-step DOM and a Settings-locator timeout, showing that the suspected keyboard trap was not reached.

Command output from the check

  • The initial executed browser attempt timed out waiting for the Remote name field, documenting the unsuccessful direct-entry approach.

Command output from the check

  • The actual base revision was exported with Expo and exited successfully, making an authentic before-change browser comparison available.

Evidence from the check

  • This authored Playwright script exercises the real settings controls and asserts UI, announcement, and persisted-state behavior, making the reproduction executable.

Evidence from the check

  • This authored shell wrapper runs the browser script and records the command, working directory, exit code, and captured output, making the execution trace reproducible.

▶ Recording of the check

  • Chromium enables scanning and leaves one Select switch, then opens its selected option, showing the valid starting configuration.

Settings with the sole switch set to Select

  • The browser captures the settings after dismissing the initial selector, recording the configuration before the rejected edit.

▶ Recording of the check

  • Chromium chooses Next and reopens the selector after validation rejects the change, capturing the misleading Next display despite unchanged persisted Select.

Settings after the rejected edit and selector reopening

  • The browser captures the final settings state after reopening and dismissing the selector, recording the rejected edit’s lingering UI state.

Command output from the check

  • The successful execution records DOM values, announcement text, stored preferences, and fresh-render behavior, confirming that Next is presented while Select remains persisted.

Evidence from the check

  • This authored Jest test exercises real normalization, validation, preference reload and Settings handlers with valid and unusable manual inputs, establishing the F4 reproduction.

Evidence from the check

  • This executed runner invokes both focused Jest cases and captures each command, working directory, exit code and observed output, making the comparison reproducible.

Command output from the check

  • The valid-manual control run validates successfully and persists On and Off with one write each, showing that the tested persistence path works.

Command output from the check

  • The reproduction run returns invalid manual defaults and rejects On, Off and pattern saves without writing storage, confirming F4.

Evidence from the check

  • The authored test renders the real settings component, gates AsyncStorage completion, and checks persistence and reload, reproducing the lost edit without replacing application logic.

Evidence from the check

  • The executed shell script runs the focused Jest test twice and captures each command, working directory, exit code, and observed output, making both conditions reproducible.

Command output from the check

  • The first executed run completes the pattern write before pressing Off and checks memory, storage, and reload, showing that both edits survive.

Command output from the check

  • The second executed run presses Off before releasing the pattern write and checks both queued payloads and reload, showing that the second save restores the stale grouped pattern.

View artifacts

Summary

This PR adds web-only switch scanning with shared button highlights, grouped sections, dialog scopes, key assignment, and stored preferences.

The action picker cannot select actions through scanning: repeated Next presses reach only Close. Key assignment also lacks cleanup for a missed key release after focus loss. These switch-access problems should be fixed before merging. Three additional, non-blocking settings problems were reproduced: rejected choices appear saved, invalid manual fallbacks prevent settings saves, and overlapping edits lose an earlier change.

Acknowledged scope: enaboapps explicitly leaves live typing outside this change because keys go to the text field. enaboapps leaves native switch input unchanged because React Native has no global key events; scanning and its settings card are web-only. Physical switch-interface and screen-reader checks remain deferred, as stated by enaboapps.

T-Rex validation blocked

The key-assignment browser check stopped at initial onboarding: the page showed Step 1 of 2 rather than Settings, and the Settings locator timed out. The missed-release behavior was not exercised.

  • Switch users can scan and activate controls across Remote.
  • Switch settings let people choose scan movement and assign keys.

Reviews (1) · Last reviewed commit: "feat(scanning): add switch scanning of t..." · Reviewed by Greptile

Comment on lines +35 to +37
<ScanSection exclusive radius={0} style={{ flex: 1 }}>
<ActionPickerContent {...props} />
</ScanSection>

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 Action choices cannot be scanned

The new exclusive ScanSection confines scanning to ActionPicker, but its action rows are plain Pressable controls that never register with useScannable. A switch user can open the picker from an empty layout cell, but scanning only reaches Close, not any action. This blocks adding an action through switch input and should be fixed before merging. Register the action rows with the same highlight and activation behavior as the shared buttons.

Knowledge Base Used: Layout editing, dragging, and surface rendering

Artifacts

▶ Recording of the check

  • Rendered the base picker, pressed Next eight times and Select, then clicked an action; base has no scan target but pointer assignment works.

Base ActionPicker after eight Next switch presses

  • Captured the rendered base picker after switch navigation; the controller reports no highlighted item.

▶ Recording of the check

  • Rendered head and exercised Next, Select, and pointer assignment; switches reach only Close while clicking an action assigns it.

Head ActionPicker after eight Next switch presses

  • Captured the rendered head picker after repeated switch navigation; the controller and DOM highlight identify Close as the sole target.

Command output from the check

  • Recorded actual controller snapshots, callback events, and assertions with command, working directory, and exit code; head confirms Close-only scanning.

Command output from the check

  • Captured the actual Metro build command and output; the executable web harness bundled successfully with exit code zero.

Evidence from the check

  • Contains the executed wrapper that records command, directory, exit code, and browser output; the validation is reproducible.

Evidence from the check

  • Contains base extraction, Metro build, HTTP serving, Chromium interaction, assertions, and recording capture; it executes both sides without production edits.

Evidence from the check

  • Mounts the real picker and keyboard-switch integration with three catalog actions and observed callbacks; no scanning registration or activation handlers are mocked.

Evidence from the check

  • Extracted the picker from the specified base revision and redirected its ActionButton import to the extracted base control; this preserves the before behavior.

Evidence from the check

  • Extracted the base ActionButton and adjusted local dependency paths; the before comparison does not accidentally inherit head Close scanning.

Evidence from the check

  • Contains the Metro-generated JavaScript served to Chromium for both runs; this is the exact compiled execution source.

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/layouts/ActionPicker.tsx
Line: 35-37

Comment:
**Action choices cannot be scanned**

The new exclusive `ScanSection` confines scanning to `ActionPicker`, but its action rows are plain `Pressable` controls that never register with `useScannable`. A switch user can open the picker from an empty layout cell, but scanning only reaches Close, not any action. This blocks adding an action through switch input and should be fixed before merging. Register the action rows with the same highlight and activation behavior as the shared buttons.

**Knowledge Base Used:** [Layout editing, dragging, and surface rendering](https://app.greptile.com/owen-mcgirr/-/custom-context/knowledge-base/switchifyapp/switchify-remote/-/docs/layout-editor-and-rendering.md)

---

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

Comment on lines +45 to +50
const onKeyUp = (event: KeyboardEvent) => {
swallow(event);
if (pressed !== null && event.code === pressed) close();
};
window.addEventListener('keydown', onKeyDown, true);
window.addEventListener('keyup', onKeyUp, true);

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 Key assignment can trap input

If the browser loses focus while the assigned key is held and the release happens elsewhere, captureNextKey never receives the matching keyup. Capture stays active after the user returns, swallows every later key including Escape, and keeps SwitchInput disabled until that same key is pressed and released again. Cancel capture on window blur or when the page becomes hidden, and keep its cleanup handle until capture actually closes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/scanning/keyCapture.ts
Line: 45-50

Comment:
**Key assignment can trap input**

If the browser loses focus while the assigned key is held and the release happens elsewhere, `captureNextKey` never receives the matching `keyup`. Capture stays active after the user returns, swallows every later key including Escape, and keeps `SwitchInput` disabled until that same key is pressed and released again. Cancel capture on window blur or when the page becomes hidden, and keep its cleanup handle until capture actually closes.

---

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

Comment on lines +87 to +88
<SelectorField label="Press" options={PRESS_ACTIONS.map((action) => ({ key: action, label: ACTION_LABELS[action] }))} selectedKey={binding.pressAction} onSelect={(pressAction) => updateBinding(binding.id, { pressAction, name: ACTION_LABELS[pressAction] }).then(() => undefined)} />
<SelectorField label="Hold" options={HOLD_CHOICES.map(({ key, label }) => ({ key, label }))} selectedKey={holdKey(binding.holdActions)} onSelect={(key) => updateBinding(binding.id, { holdActions: HOLD_CHOICES.find((choice) => choice.key === key)?.actions ?? [] }).then(() => 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 Rejected settings look saved

Changing the only Select binding to Next fails validation, but the Press selector discards the refused save with .then(() => undefined). SelectorField consequently closes, displays Next, and announces it as selected while the saved binding remains Select. The unchanged selectedKey does not reset that display. This is a non-blocking settings feedback problem: propagate the failed save so the field keeps its saved value instead of presenting an action the switch will not perform.

Artifacts

Evidence from the check

  • This authored Playwright script exercises the real settings controls and asserts UI, announcement, and persisted-state behavior, making the reproduction executable.

Evidence from the check

  • This authored shell wrapper runs the browser script and records the command, working directory, exit code, and captured output, making the execution trace reproducible.

▶ Recording of the check

  • Chromium enables scanning and leaves one Select switch, then opens its selected option, showing the valid starting configuration.

Settings with the sole switch set to Select

  • The browser captures the settings after dismissing the initial selector, recording the configuration before the rejected edit.

▶ Recording of the check

  • Chromium chooses Next and reopens the selector after validation rejects the change, capturing the misleading Next display despite unchanged persisted Select.

Settings after the rejected edit and selector reopening

  • The browser captures the final settings state after reopening and dismissing the selector, recording the rejected edit’s lingering UI state.

Command output from the check

  • The successful execution records DOM values, announcement text, stored preferences, and fresh-render behavior, confirming that Next is presented while Select remains persisted.

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/scanning/ScanningSettings.tsx
Line: 87-88

Comment:
**Rejected settings look saved**

Changing the only Select binding to Next fails validation, but the Press selector discards the refused save with `.then(() => undefined)`. `SelectorField` consequently closes, displays Next, and announces it as selected while the saved binding remains Select. The unchanged `selectedKey` does not reset that display. This is a non-blocking settings feedback problem: propagate the failed save so the field keeps its saved value instead of presenting an action the switch will not perform.

---

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

Comment on lines +54 to +59
return {
enabled: value.enabled === true,
automatic: options.automatic,
intervalMs: options.intervalMs,
pattern: options.pattern,
switches: validateSwitchSettings(switches, options.automatic) === null ? switches : DEFAULT_SCANNING.switches,

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 Manual fallback stays invalid

When stored manual settings have missing or unusable switches, normalizeScanning falls back to DEFAULT_SCANNING.switches but keeps automatic: false. Those defaults have Select and Next but no Previous, so the returned settings still fail validateSwitchSettings. Every settings save then fails, including Off. This is a non-blocking recovery problem for damaged or outdated preferences: use manual-compatible fallback switches, or restore automatic movement with the fallback, and test that the resulting combination passes validation.

Knowledge Base Used: Persistence and settings

Artifacts

Evidence from the check

  • This authored Jest test exercises real normalization, validation, preference reload and Settings handlers with valid and unusable manual inputs, establishing the F4 reproduction.

Evidence from the check

  • This executed runner invokes both focused Jest cases and captures each command, working directory, exit code and observed output, making the comparison reproducible.

Command output from the check

  • The valid-manual control run validates successfully and persists On and Off with one write each, showing that the tested persistence path works.

Command output from the check

  • The reproduction run returns invalid manual defaults and rejects On, Off and pattern saves without writing storage, confirming F4.

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/scanning/preferences.ts
Line: 54-59

Comment:
**Manual fallback stays invalid**

When stored manual settings have missing or unusable switches, `normalizeScanning` falls back to `DEFAULT_SCANNING.switches` but keeps `automatic: false`. Those defaults have Select and Next but no Previous, so the returned settings still fail `validateSwitchSettings`. Every settings save then fails, including Off. This is a non-blocking recovery problem for damaged or outdated preferences: use manual-compatible fallback switches, or restore automatic movement with the fallback, and test that the resulting combination passes validation.

**Knowledge Base Used:** [Persistence and settings](https://app.greptile.com/owen-mcgirr/-/custom-context/knowledge-base/switchifyapp/switchify-remote/-/docs/persistence-and-settings.md)

---

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

Comment on lines +37 to +44
const save = async (next: ScanningPreferences): Promise<boolean> => {
const problem = validateSwitchSettings(next.switches, next.automatic);
if (problem) { setError(problem); return false; }
setError(null);
await preferencesStore.update({ scanning: next });
return true;
};
const updateBinding = (id: string, patch: Partial<SwitchBinding>) => save({ ...scanning, switches: { ...scanning.switches, bindings: scanning.switches.bindings.map((binding) => binding.id === id ? { ...binding, ...patch } : binding) } });

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 Second edits undo earlier changes

save replaces all of scanning using the value from the current render. If a user changes two settings while the first storage write is still pending, both saves use the old value. The second queued write then undoes the first change; selecting Every control and then Off persisted the old grouped pattern. This is a non-blocking persistence concern that silently loses a preference edit. Apply each change to the latest settings inside the write queue, or prevent further edits until the save finishes.

Knowledge Base Used: Persistence and settings

Artifacts

Evidence from the check

  • The authored test renders the real settings component, gates AsyncStorage completion, and checks persistence and reload, reproducing the lost edit without replacing application logic.

Evidence from the check

  • The executed shell script runs the focused Jest test twice and captures each command, working directory, exit code, and observed output, making both conditions reproducible.

Command output from the check

  • The first executed run completes the pattern write before pressing Off and checks memory, storage, and reload, showing that both edits survive.

Command output from the check

  • The second executed run presses Off before releasing the pattern write and checks both queued payloads and reload, showing that the second save restores the stale grouped pattern.

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/scanning/ScanningSettings.tsx
Line: 37-44

Comment:
**Second edits undo earlier changes**

`save` replaces all of `scanning` using the value from the current render. If a user changes two settings while the first storage write is still pending, both saves use the old value. The second queued write then undoes the first change; selecting Every control and then Off persisted the old grouped pattern. This is a non-blocking persistence concern that silently loses a preference edit. Apply each change to the latest settings inside the write queue, or prevent further edits until the save finishes.

**Knowledge Base Used:** [Persistence and settings](https://app.greptile.com/owen-mcgirr/-/custom-context/knowledge-base/switchifyapp/switchify-remote/-/docs/persistence-and-settings.md)

---

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

This branch was successfully deployed

1 active deployment
Preview — ab36a129 Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add switch scanning of the Remote interface

2 participants