feat(chat): bare Enter sends on fine-pointer devices, Shift+Enter newline (ADR-0031) #71

Merged
dominik.polakovics merged 1 commit from afk/70 into main 2026-07-09 14:33:11 +02:00

Bare Enter now submits both web composers — the chat reply textarea (RunChat.tsx) and the new-run composer (NewRun.tsx) — on fine-pointer devices, per the maintainer grill session of 2026-07-09. Touch devices keep return-as-newline and the on-screen Send button.

What changed

  • Shared gate web/src/lib/composerKeys.ts (isComposerSend): one pure predicate both composers call, so they cannot drift. Encodes the whole contract:
    • Bare Enter sends iff matchMedia('(hover: hover) and (pointer: fine)') matches — evaluated per keydown (never snapshotted), so docking/undocking a keyboard on hybrids behaves; absence of matchMedia reads as not-fine-pointer.
    • Shift+Enter / Alt+Enter are never intercepted — browser-default newline everywhere.
    • Cmd/Ctrl+Enter keeps sending in all environments, as today.
    • New IME guard: isComposing / legacy keyCode === 229 Enter never sends (previously absent in web/src; required once bare Enter can send).
  • All agent states: bare Enter sends in idle/needs_input/working alike, inheriting ADR-0029's always-send contract.
  • Popover precedence unchanged: while the slash-command popover is open, Enter/Tab accept the highlighted command; the send gate runs only when it's closed.
  • Empty box: bare Enter never sends and is preventDefault-ed (no stray leading newline). In NewRun specifically, bare Enter on an empty box must not launch a plain spawn — while Cmd/Ctrl+Enter (and the Start button) keep that explicit empty-spawn path.
  • Tooltip: NewRun Start button title → "Start run (Enter)".
  • No enterkeyhint change, no user-facing toggle, no Go/API/provider change — pure SPA diff plus ADR.

Tests

558/558 green (37 files); lint, prettier, and tsc --noEmit && vite build all clean.

  • Rewrote the old "sends on Cmd/Ctrl+Enter but never on bare Enter" test to the new contract (fine-pointer bare-Enter sends; Shift+Enter not prevented and never sends; Ctrl+Enter sends).
  • Extended the ADR-0029 working-state test with bare Enter while working.
  • New: touch profile and no-matchMedia profiles (bare Enter inert, Ctrl+Enter sends), isComposing guard, empty-box preventDefault, popover-accept precedence under fine-pointer.
  • New: full keyboard coverage for the previously untested NewRun composer, including pinning Cmd/Ctrl+Enter's empty plain-spawn and the new tooltip.

ADR

docs/adr/0031-composer-bare-enter-sends-on-fine-pointer.md records the pinned decisions and rejected options (enterkeyhint, settings toggle, mount-time snapshot, UA sniffing, skipping the IME guard, per-composer duplication). ADR-0022 (incl. its now-overturned rejected option) and ADR-0029 are annotated in place; everything else in ADR-0029 (always-Send, no queue, header Interrupt) remains in force.

Closes #70

🤖 Generated with Claude Code

https://claude.ai/code/session_0191AABRn5vPgRUSkck4oTgg

Bare Enter now submits both web composers — the chat reply textarea (`RunChat.tsx`) and the new-run composer (`NewRun.tsx`) — on fine-pointer devices, per the maintainer grill session of 2026-07-09. Touch devices keep return-as-newline and the on-screen Send button. ## What changed - **Shared gate** `web/src/lib/composerKeys.ts` (`isComposerSend`): one pure predicate both composers call, so they cannot drift. Encodes the whole contract: - Bare Enter sends iff `matchMedia('(hover: hover) and (pointer: fine)')` matches — evaluated per keydown (never snapshotted), so docking/undocking a keyboard on hybrids behaves; absence of `matchMedia` reads as not-fine-pointer. - Shift+Enter / Alt+Enter are never intercepted — browser-default newline everywhere. - Cmd/Ctrl+Enter keeps sending in all environments, as today. - **New IME guard**: `isComposing` / legacy `keyCode === 229` Enter never sends (previously absent in `web/src`; required once bare Enter can send). - **All agent states**: bare Enter sends in idle/needs_input/working alike, inheriting ADR-0029's always-send contract. - **Popover precedence unchanged**: while the slash-command popover is open, Enter/Tab accept the highlighted command; the send gate runs only when it's closed. - **Empty box**: bare Enter never sends and is `preventDefault`-ed (no stray leading newline). In `NewRun` specifically, bare Enter on an empty box must not launch a plain spawn — while Cmd/Ctrl+Enter (and the Start button) keep that explicit empty-spawn path. - **Tooltip**: `NewRun` Start button `title` → "Start run (Enter)". - **No** `enterkeyhint` change, no user-facing toggle, no Go/API/provider change — pure SPA diff plus ADR. ## Tests 558/558 green (37 files); lint, prettier, and `tsc --noEmit && vite build` all clean. - Rewrote the old "sends on Cmd/Ctrl+Enter but never on bare Enter" test to the new contract (fine-pointer bare-Enter sends; Shift+Enter not prevented and never sends; Ctrl+Enter sends). - Extended the ADR-0029 working-state test with bare Enter while `working`. - New: touch profile and no-`matchMedia` profiles (bare Enter inert, Ctrl+Enter sends), `isComposing` guard, empty-box `preventDefault`, popover-accept precedence under fine-pointer. - New: full keyboard coverage for the previously untested `NewRun` composer, including pinning Cmd/Ctrl+Enter's empty plain-spawn and the new tooltip. ## ADR `docs/adr/0031-composer-bare-enter-sends-on-fine-pointer.md` records the pinned decisions and rejected options (`enterkeyhint`, settings toggle, mount-time snapshot, UA sniffing, skipping the IME guard, per-composer duplication). ADR-0022 (incl. its now-overturned rejected option) and ADR-0029 are annotated in place; everything else in ADR-0029 (always-Send, no queue, header Interrupt) remains in force. Closes #70 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0191AABRn5vPgRUSkck4oTgg
feat(chat): bare Enter sends on fine-pointer devices, Shift+Enter newline (ADR-0031)
All checks were successful
ci / native (pull_request) Successful in 5m10s
eff2f33648
Bare Enter now submits both web composers (chat reply, new-run spawn) on
devices with a mouse/trackpad — gated per keydown on
matchMedia('(hover: hover) and (pointer: fine)') so hybrids that dock or
undock a keyboard stay correct. Touch devices keep return-as-newline and
the on-screen Send button, preserving the phone-first rationale of
ADR-0005/0022. Shift/Alt+Enter fall through as newlines everywhere;
Cmd/Ctrl+Enter keeps sending in all environments and all agent states
(ADR-0029's always-send contract now extends to bare Enter).

The gate lives in one shared predicate, web/src/lib/composerKeys.ts
(isComposerSend), so the two composers cannot drift. It adds the
previously missing IME guard (isComposing / legacy keyCode 229) — safe
to omit under Ctrl+Enter, required under bare Enter. Bare Enter on an
empty box never sends and is preventDefault-ed (no stray leading
newline); in NewRun it also must not launch a plain spawn, while
Cmd/Ctrl+Enter keeps that explicit empty-spawn path. Slash-command
popover precedence is unchanged. NewRun's Start tooltip becomes
"Start run (Enter)".

ADR-0031 records the decision and partially supersedes ADR-0022/0029
(their bare-Enter-stays-a-newline clauses, including ADR-0022's
rejected-option entry); both files are annotated in place.

Closes #70

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191AABRn5vPgRUSkck4oTgg
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!71
No description provided.