# Double-click Shortcuts 0.6.3

Base: stable `main` / `d93ddcf89c31dd685ee27c045169d79a7d5088da` (Watch Glance 0.6.2).
Branch: `pi/double-click-shortcuts-0.6.3`.
Version: `0.6.3-dev` (feature candidate; no tag, no `main` merge).

Two top-level shortcuts:

```text
Double A -> Speech Mode selector (OFF / MANUAL / AUTO_REPLY / AUTO_SUMMARY)
Double B -> existing Agent Session Picker (openAgentSessionPicker)
```

They exist only on the normal top-level Shell (`Watch`, `Agent`, `Monitor`, `Help`).
They are disabled while any modal owns input (`Voice`, `Settings`, `Navigator`,
native `UserInput`, `Error`) or while an approval / message / speech overlay is
visible. Existing behavior is preserved: `A click` next page, `B click` previous
page, `Hold A` Voice, `Hold B` Settings, Settings `Hold A` Back / `Hold B` Close,
Approval A/B, Voice A Send / B Cancel, Navigator A/B and native input A/B.

## State machine

No new button dependency. The existing `ButtonGesture` gains the single/double
disambiguation and a new `Gesture::DOUBLE_CLICK`.

```text
DEBOUNCE_MS   = 30   (unchanged)
LONG_MS       = 600  (unchanged)
DOUBLE_GAP_MS = 300  (first version, physical test parameter)
```

The first short release is not reported immediately. A pending `CLICK` is armed
with an absolute deadline of `now + DOUBLE_GAP_MS`. A second confirmed short press
and release of the same button inside that window reports exactly one
`DOUBLE_CLICK` and never the leading `CLICK`. Otherwise the pending `CLICK` is
reported once the deadline passes and the line is idle.

Boundary rule: the deadline is inclusive and only expires while the raw line is
idle, because a second raw-down inside the gap is a possible press whose 30 ms
debounce is longer than the remaining gap.

```text
299 ms raw-down gap -> DOUBLE_CLICK
301 ms              -> first CLICK, then the late press as its own sequence
exactly 300 ms idle -> CLICK (deadline is inclusive)
```

`LONG` always wins. A first short press followed by a second press held past
`LONG_MS` discards the pending single/double and reports `LONG`; no `CLICK` and no
`DOUBLE_CLICK` are produced, and `LONG_RELEASE` keeps its existing meaning.

A pending `CLICK` is bound to the epoch that armed it. If the UI epoch moves
(approval generation, modal ownership, target/session navigation), the pending
click is discarded and is never replayed against the new UI. The owning UI may also
call `invalidate_pending()` explicitly when it takes ownership mid-gap.

`allow_double` is passed to `update()` alongside the existing `allow_long`. It is
true only when `InputModal::NONE`, no approval and no overlay is visible
(`shell_double_press_allowed`). Every other owner keeps the original immediate
`CLICK` latency: no 300 ms wait, no `DOUBLE_CLICK`.

## Double B

Double B calls the existing `openAgentSessionPicker()` — the only Session entry.
It is behaviorally identical to `Settings -> Agent -> Session`: same Project
Picker, same Session list, same target CAS, same authoritative snapshot. No second
quick picker exists.

If the negotiated `catalog`/`target-select` capability is unavailable, or the
Gateway is offline, the shell shows a short `ERROR` (`OFFLINE` /
`SESSION PICKER UNAVAILABLE`) instead of crashing or opening a partial picker.

## Double A

Double A enters the Mode page directly, skipping the Settings chain:

```text
SPEECH MODE
AUTO_SUMMARY
A NEXT   B APPLY
```

`A NEXT` cycles `OFF -> MANUAL -> AUTO_REPLY -> AUTO_SUMMARY`. `B APPLY` uses the
existing `speech_settings_set` command; the Gateway persists the per-device/target
preference. On acknowledgement the shell returns to the original page with a short
`SAVED / <mode>` notice. Offline, the shortcut shows a bounded `ERROR` and never
leaves the user in an unescapable state.

## Speech Settings and Playback stay separate

Speech Output remains **FROZEN**. This candidate restores only the Mode
*preference*. It does not restore playback, `speech_start`, `speech_chunk`,
`speech_end`, Read Screen, TTS, AUTO_REPLY playback or AUTO_SUMMARY playback.

The capability is split minimally:

```text
speech-settings = supported   (firmware 0.6.3 advertises this)
speech-playback = NOT supported
```

* Firmware `hello` advertises `speech-settings` and does not advertise
  `speech_playback`.
* Gateway `SpeechManager.settings_capable()` accepts `speech-settings` or
  `speech_playback`; only `speech_settings` frames use it. `capable()` still
  requires `speech_playback` for every other speech command, so a
  settings-only device can never receive playback.
* Firmware executes only the `speech_settings` preference frame; every other
  `speech_*` frame is still dropped.

Saving a mode therefore cannot re-enable the frozen playback path.

## Tests

`firmware/tests/button_gesture_test.cpp` (raw state machine) covers single A/B,
double A/B, the 299/301/exact-gap boundaries, long A/B, first-short + second-long
(`LONG` only), long release with no trailing click, A then B and B then A, contact
bounce, stale epoch and explicit invalidation, and `allow_double=false`. It asserts
`DOUBLE` produces zero leading `CLICK` and `LONG` produces zero `CLICK`/`DOUBLE`.

`firmware/tests/run_shell_input_test.py` compiles the production
`handle_gesture` / `poll_shell_button` / `open_speech_mode` /
`open_double_session_picker` / `speech_mode_saved` at the raw GPIO level and
checks: Double B reuses the single picker entry (no second picker, no picker when
the capability is missing or offline), Double A reaches the Mode page and APPLY
saves, modals/approval/overlay disable doubles while keeping immediate single
clicks, single A/B still navigate, and A then B is never a double.

`server/tests/test_speech.py` checks that a `speech-settings`-only device receives
`speech_settings`, can save a mode, and never receives `speech_start`/`chunk`/`end`.

## Gates

```text
git diff --check                       PASS
./scripts/test-wearable-shell.sh       PASS
./scripts/test-network-resilience.sh   PASS (88 Python tests)
./scripts/test-server.sh               PASS (224 Python tests, plus Node/firmware/smoke)
./scripts/build.sh m5stack-stopwatch   PASS (device_lab.bin 0x2b5380)
```

## Not in this round

Touch, speech playback repair, alarm, WSS cadence optimization, power-save
changes, Watch redesign and Agent page redesign.