Skip to content

Default Caps Lock rules to all keyboards; show the options inline - #1366

Open
malpern wants to merge 2 commits into
masterfrom
fix/caps-all-keyboards-default
Open

malpern wants to merge 2 commits into
masterfrom
fix/caps-all-keyboards-default

Conversation

@malpern

@malpern malpern commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Problem

On a Mac with a Caps Lock selection saved before all-keyboards mode, Caps Lock rules paused after every restart:

  1. The upgrade fallback in selectedKeyboardOnly() treated any saved selection as one-keyboard mode.
  2. A selection is bound to its boot session, so it expired at every restart.
  3. Automatic selection requires exactly one keyboard-class device. A keyboard plus a mouse with a keyboard interface, or the HID test fixture, is enough to refuse.

The Status tab then read "Caps Lock Needs Attention", and Fix it opened the setup wizard, which knows nothing about Caps Lock. Clicking it did nothing useful. The control that actually helped was hidden in a collapsed "Caps Lock options" group.

Observed on the mini with a cl80, an X2A and the KeyPath physical HID fixture attached. The log repeated Caps keyboard not chosen automatically (selection); starting with Caps Lock rules paused on every session start.

Changes

  • One-keyboard scope is opt-in only. selectedKeyboardOnly() reads selectedKeyboardOnlyKey and nothing else. A leftover selection no longer pins the Mac. KeyPath has no installed base to migrate.
  • Fix it opens Settings > General when paused Caps Lock rules are the only problem. The status icon does the same. Every other case still opens the wizard.
  • No disclosure group. The on/off switch sits on the Caps Lock rules row. With several keyboards, each gets a checkbox, followed by "Use on one keyboard only…". The picker and "Use on All Keyboards" appear inline once that mode is chosen. With a single keyboard, nothing extra shows.
  • Session startup waits for the Caps Lock fleet write. The parent retired a worker still in starting after 40 polls of 100 ms. All-keyboards startup runs a read, write and readback hidutil per keyboard interface. With three keyboard-class devices on the mini that took about 5.1 s, so every start was killed mid-write and KeyPath never came up. The wait is now a 20 s deadline (sessionStartupTimeout). Failure or exit still ends it at once. A stack sample confirmed the worker was in SessionCapsMappingFleet.acquire, not hung. Write-up: docs/bugs/2026-11-07-session-startup-retired-during-caps-fleet-write.md.
  • The user guide (guides/driverless-caps-lock.md) and docs/architecture/all-keyboards-caps.md match the new behaviour.

Testing

  • TEST_FILTER='SessionCapsMappingFleetTests|SessionCaps|StatusSettings|SettingsView' ./Scripts/run-tests-safe.sh: 174 executed, 171 passed, 3 skipped, 0 failures.
  • TEST_FILTER='SessionRuntime|ServiceLifecycle|SessionCaps|StatusSettings|SettingsView': 226 executed, 223 passed, 3 skipped, 0 failures.
  • Deployed to the mini with quick-deploy.sh. Before the startup fix, every session start was retired mid-write. After it, the session reached ready in 5.1 s, the cl80 interfaces carry the Caps Lock mapping, and the log no longer reports paused Caps Lock rules. The new Settings layout was not checked on screen.
  • testSingleKeyboardScopeIsOptInOnly replaces the test that asserted the old fallback.
  • SwiftFormat 0.61.1 lint is clean, and check-accessibility.py passes.
  • Remote review gate selected: the local review-gate.sh could not authenticate on the mini because its OAuth session had expired.

Not covered: the separate bug where the driverless session's event tap swallows Ctrl+arrow Mission Control shortcuts.

🤖 Generated with Claude Code

A Caps Lock selection saved before all-keyboards mode kept that Mac in
one-keyboard mode. Selections expire at every restart, and automatic
selection refuses when more than one keyboard-class device is attached
(a mouse with a keyboard interface is enough), so Caps Lock rules were
paused after every reboot. The Status tab then said "Caps Lock Needs
Attention", and its Fix it button opened the setup wizard, which cannot
fix this.

- One-keyboard scope is now opt-in only (selectedKeyboardOnlyKey). A
  leftover selection no longer implies it. KeyPath has no installed base
  to migrate.
- Fix it, and the status icon, open Settings > General when paused Caps
  Lock rules are the only problem.
- The Caps Lock settings no longer hide behind a "Caps Lock options"
  disclosure group. The on/off switch sits on the row, keyboards get a
  checkbox each when there are several, and the one-keyboard picker
  appears inline when chosen.
- The user guide and the all-keyboards architecture doc describe the
  new default and layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-08T05:36:20.049868Z bb1de52 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 8, 2026

Copy link
Copy Markdown

Review: PR 1366

No blocking issues. Notes:

  1. Behavior change on upgrade (intentional, documented). selectedKeyboardOnly now returns only the explicit flag. A Mac with a leftover selectionKey and no flag silently moves to all-keyboards. That fits the stated rationale, but the stale selectionKey data stays in defaults. If nothing else reads it, consider clearing it when the flag is false.
  2. capsPausedIsOnlyIssue has no test. The SettingsView change adds routing logic with several conjunctive conditions (driverless, paused message, healthy services, TCP, permissions, no critical or error wizard issues). A small unit test covering the true case and one case per blocking condition would protect the "Fix it" routing from regressing.
  3. tcpConfigured != false treats nil (unknown) as OK. That is probably intended, but a nil can send the user to Settings instead of the wizard when TCP is actually broken. Worth confirming.
  4. Unused code. showsPicker is still declared next to the new keyboardScope. Check whether anything still uses it, since the picker is now gated by selectedOnly || choosingOneKeyboard.
  5. UI/docs.
    • With a single keyboard, keyboardScope renders nothing, so "Use on one keyboard only" is unreachable there. This is consistent with the comment.
    • The guide says a one-keyboard choice lasts until restart. That is correct, but it is a surprising limit for an opt-in mode, and the UI doesn't say so at the point of choice.
    • The docs date (2026-11-07) is fine.

Otherwise the toggle is now visible and labelled, and the accessibility identifiers are preserved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb1de527f1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.accessibilityIdentifier("settings-caps-cancel-keyboard-choice")
}
}
} else if keyboards.count > 1 {

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 Badge Keep the remaining keyboard's exclusion control accessible

If a user unchecks one keyboard while two are connected and then disconnects the other, this condition hides the remaining keyboard's checkbox even though its exclusion is still persisted. SessionRuntimeWorker continues filtering that device out using excludedKeyboardIDs(), and toggling the main Caps Lock switch does not clear those exclusions. Consequently, Caps Lock rules remain off on the only connected keyboard, with no way to re-enable it in Settings without connecting another keyboard. Show the scope controls when a connected keyboard is excluded, even if only one remains.

Useful? React with 👍 / 👎.

The parent retired a worker still in `starting` after 40 polls of 100 ms.
In all-keyboards mode startup includes a read, write and readback hidutil
run per keyboard interface. With three keyboard-class devices attached
that took about 5.1 s, so every start was killed mid-write and KeyPath
never came up. The wait is now a 20 s deadline. Failure or exit still
ends it immediately.

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

claude Bot commented Nov 8, 2026

Copy link
Copy Markdown

Review: a few actionable notes

  1. Behavior change without migration (SessionCapsRuntimeSupport.selectedKeyboardOnly): Users with a saved SessionCapsSelection who relied on the one-keyboard scope are silently switched to all-keyboards on upgrade. The docs justify this ("no installed base"), but public 1.0.1 users exist per AGENTS.md. The change is probably desirable, but the guide no longer mentions it. Consider one release-note line.

  2. Startup wait (ServiceLifecycleCoordinator+SessionRuntime): The 20 s deadline is better than a fixed count. Two things to check: (a) the loop's per-iteration sleep and the final-state handling after the deadline expires must still retire the worker and return false. The diff doesn't show them, so confirm they weren't tied to the old 0..<40 bound. (b) sessionStartupTimeout is a plain static with no test. A test that injects a short timeout would cover the 'worker stuck in starting' path.

  3. capsPausedIsOnlyIssue: It checks wizardIssues for critical/error severity only. A warning-level wizard issue (for example a permission nudge) is ignored, so 'Fix it' skips the wizard even though the wizard would have something to show. Confirm that's intended. This logic has no unit test, and it is the only thing deciding where 'Fix it' routes.

  4. UI: Toggle(...).labelsHidden() plus .accessibilityLabel is fine. The guide says the one-keyboard choice 'lasts until you restart your Mac'. That is a notable limitation to surface in the UI as well, and it is the root of the original reboot-pause bug.

No correctness blockers found.

This branch has not been deployed

No deployments
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.

1 participant