Skip to content

⌨️ feat: Keyboard Submit, Focus and Escape for Ask User Cards - #17231

Merged
berry-13 merged 8 commits into
devfrom
berry-13/enhancement-add-shortcut-controls-to-hitl-forms
Oct 18, 2026
Merged

berry-13 merged 8 commits into
devfrom
berry-13/enhancement-add-shortcut-controls-to-hitl-forms

Conversation

@berry-13

@berry-13 berry-13 commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

The ask_user_question chat cards had almost no keyboard support. The single-question card's answer box ignored Enter, so the only way to send an answer was to click Submit. In the batched card, focus landed on the step's fieldset after each question, so every step needed a Tab before it could be answered. Neither card had a way to cancel from the keyboard.

This PR carries @dev-elie's work from #16832 with their commits intact, and adds Escape. The single-question answer box now follows the composer's Enter table: Enter submits with Enter-to-send on, Ctrl/Cmd+Enter submits with it off, Shift+Enter never submits, and an Enter that confirms an IME composition is ignored. The batched card takes focus on arrival only when focus is idle, moves it to a real control on each step, and lets Enter on the picked option confirm it. Escape in a chat card skips the question or batch, but only before anything has been entered. Escape with focus inside the composer popover moves the question to the chat, matching what Escape already does from the composer, and puts focus back in the composer. A held Enter never both picks and submits, Shift/Alt/held Enter in the popover's answer field can no longer submit the chat form underneath it, and a submit shortcut rebound off Enter (for example Ctrl+J) works in both answer fields.

Fixes #16816
Supersedes #16832

How it works

Escape never throws away input. With a typed answer, a checked option or a partial batch, Escape in a chat card does nothing, because Skip resumes the run with a decline that can't be undone. In the popover, Escape is a move rather than a decline, so it always applies. A batch keeps the composer disabled while its popover is open, so focus goes back to the composer in an effect after the popover closes, not in the key handler.

composer popover   Escape (composer or popover focus)  -> move to chat, focus composer
chat card          Escape, nothing entered              -> Skip
                   Escape, answer/pick in progress      -> nothing
                   Escape during IME, Shift+Escape      -> nothing
single answer box  Enter / Ctrl+Enter / Cmd+Enter       -> composer rules (Enter-to-send)
batched card       Enter in field, Enter on picked      -> next step, or submit

Type of change

  • Feature
  • Bug fix

Testing

  1. Start LibreChat with Agents enabled and give an agent the Ask User tool.
  2. Ask one question, press Escape in the popover: the question moves to the chat and the composer has focus. In the chat card, Escape with an empty answer skips; with text typed it does nothing.
  3. Ask a batch: focus lands on the first control, Enter advances, Escape in the popover moves it to the chat, Escape in the chat card skips only before any answer.

Tested environments/configuration:

Automated tests:

  • New e2e/specs/mock/scenarios/ask-keyboard.spec.ts: a batch answered and submitted from the keyboard alone, Escape in the popover moving the batch to the chat with focus back in the composer, and Escape in the chat card skipping only while nothing is answered. All pass in the three projects.
  • New AskUserQuestionPopover.test.tsx, plus Escape, held-Enter, implicit-submit and rebound-shortcut cases in AskUserQuestion.test.tsx, AskUserQuestions.test.tsx and shortcuts.spec.ts. Each fails with its source change reverted.
  • npx jest src/components/Chat/Messages/Content src/components/Chat/Input src/components/Chat/ask src/hooks/Input src/utils/shortcuts: 159 suites, 3099 tests pass.
  • npx tsc --noEmit -p client/tsconfig.json and ESLint on the changed files are clean.

Screenshots / recordings

No visual change. The cards look the same; only keyboard behavior changes.

Risk / compatibility

  • Plain Enter in the single-question answer box now submits when Enter-to-send is on, matching the composer. Before, it always added a newline.
  • The batched card moves focus when a batch arrives, but only from an idle page or the empty composer, and once per batch.
  • AskUserQuestions takes two optional props: focusOnArrival (default false) and onEscape (defaults to Skip).
  • The single-question card now reads Enter-to-send and the shortcut bindings from the chat surface host instead of the app store.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • Required documentation PR: N/A

Copilot AI balanced review requested due to automatic review settings October 18, 2026 16:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T16:56:43.700006Z 79c7051 New commits
🔒 Security Review ✅ Completed 2026-10-18T16:05:53.966043Z 545e487 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.

@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: 545e487041

ℹ️ 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".

/** Batches whose arrival already placed focus. A batch remounts when it moves
* between the composer popover and the chat card; only its first arrival may
* take focus, or every move would pull the user back onto its options. */
const arrivalFocused = new Set<string>();

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 Badge Move arrival-focus tracking into feature-owned Jotai state

This module-level mutable set is new UI state outside the feature's Jotai state, so it survives provider, conversation, and session lifetimes and is never cleared; every focused batch remains registered for the tab lifetime, and a later reuse of an action ID will incorrectly suppress arrival focus. Keep the focus-once flag alongside the existing per-action Jotai form state so its ownership and lifecycle are scoped to the feature.

AGENTS.md reference: AGENTS.md:L162-L167

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f779636: the focus-once flag now lives in the batch's Jotai form atom (askQuestionsFormFamily) and resets with the form on submit.

Comment thread client/src/components/Chat/Messages/Content/AskUserQuestion.tsx Outdated
className="popover border-border-light bg-surface-secondary rounded-theme-popover flex max-h-[60vh] flex-col border p-2 shadow-lg [view-transition-name:ask-question]"
onKeyDown={handlePopoverKeyDown}
onKeyDown={(e) => {
if (isCancelKey(e.nativeEvent)) {

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 Ignore Escape while the popover is composing text

When focus is inside a single-question popover control and an IME composition is active, Escape is meant to cancel that composition, but this branch collapses the popover because it does not check e.nativeEvent.isComposing. Both chat-card Escape handlers already apply that guard; add it here as well so an IME cancellation does not unexpectedly move the question to the chat.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f779636: popover Escape now requires !isComposing, with a test that fails without it.

Comment on lines +302 to +305
event.key !== 'Enter' ||
event.shiftKey ||
event.altKey ||
question.multiSelect === 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 Badge Ignore repeated Enter before confirming a selected option

When an unselected single-select option is focused on the last step (including a one-question batch), holding Enter first triggers the button's default click and selects it; the next auto-repeat keydown then sees aria-pressed="true" here and immediately submits the batch. The same held key therefore both chooses and irreversibly confirms an answer, so repeated keydown events should be ignored before calling confirmStep() while a fresh second Enter should remain supported.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f779636: the picked-option confirm ignores event.repeat, so a held Enter only picks; a fresh second Enter still submits (unit test plus e2e ask-batch-completes-from-keyboard).

Comment on lines +343 to +344
event.shiftKey ||
event.altKey ||

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 Badge Prevent modified Enter from submitting the enclosing form

When this batch is rendered in the composer popover, its single-line answer input is inside ChatForm's <form>. Returning here for Shift+Enter or Alt+Enter without preventing the native default permits implicit form submission, so during a paused run—especially when the action slot contains only a non-submit Stop control—the form can route a preserved composer draft instead of leaving the modifier chord inert. Prevent the default Enter action for these modifier cases before returning; only the unmodified/Ctrl/Cmd paths should continue to confirmStep().

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f779636: Shift, Alt and repeated Enter now preventDefault before returning, so the ChatForm cannot submit implicitly; covered by a test that fails without it.

@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: f7796360dd

ℹ️ 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".

* steer/queue actions.
*/
const handleAnswerKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
const action = resolveComposerKeyDown(e.nativeEvent, {

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 Honor non-Enter submit bindings in the answer box

When submitMessage is rebound to a non-Enter chord such as Ctrl+J, pressing that chord in this answer textarea does nothing: resolveComposerKeyDown immediately returns none for every non-Enter key, while the window shortcut handler deliberately yields submitMessage whenever a non-main editing field has focus (useKeyboardShortcuts.ts:1131-1135). Handle a matching non-Enter submitOverride locally so the answer box actually follows the user's configured submit binding.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 3bc4589: a submit chord rebound off Enter now submits from the single-question answer box and confirms the batched answer field (shared isReboundSubmitChord), with tests that fail without it.

dev-elie and others added 8 commits October 18, 2026 18:51
The interactive ask_user_question card's answer textarea had no key
handling, so the answer could only be submitted by clicking. Route its
keydown through resolveComposerKeyDown so it follows the composer's
Enter-to-send preference, a rebound submit chord, and IME composition.
The run is paused rather than generating, so it resolves as idle and
Ctrl/Cmd+Enter submits instead of reaching the during-run actions.
- Focus the first question's controls when a batch arrives, only when
  focus is idle (body, or the empty composer that sent the turn), and
  focus into the next step on advance instead of the fieldset: the picked
  or first option, else the answer field.
- Enter on the already-picked single-select option confirms the step;
  Enter on an unpicked option stays its click, so a pick never submits
  by itself (WCAG 3.2.2). Multi-select rows only toggle.
- Ctrl/Cmd+Enter confirm from the answer field like Enter; Shift+Enter
  never does.
Review findings on the batched ask_user_question card, and what this
commit does about each.

1. Held or double-tapped Enter could pick answers and submit the batch.
   After Enter confirmed a step, focus moved to the next step's first
   option. A keyboard Enter on a focused button is its click, so the
   key repeat of a held Enter (or a quick double-tap) picked that option
   and advanced again. On the last step the next repeat landed on the
   now-pressed option, confirmed it, and submitted the whole batch with
   answers the user never chose.

   Fix: after a step change, focus goes to the step's picked option,
   else its answer field. An unpicked option is only a focus target when
   a batch first arrives. Repeats now reach a text field, which can page
   on but never picks an option or submits on the user's behalf.

2. Focus-on-arrival ran on every mount, not only on arrival. Its effect
   has empty deps, so moving the batch between the composer popover and
   the chat card, or remounting a message that still holds an
   unanswered batch (reopening a conversation, a Share view), pulled
   focus onto an option of that batch. The next Enter or Space would
   then answer it.

   Fix: `AskUserQuestions` takes a `focusOnArrival` prop, set only by
   the surface showing the live pause (the popover, or the chat card
   while it is live and visible), so other mounts never take focus.
   Focus is placed at most once per batch, tracked by action id, so a
   move between surfaces is not treated as a new arrival.

Tests: a held Enter from an answer field picks nothing and never
submits; a surface that isn't the live pause leaves focus alone; a
remount of the same batch doesn't take focus again. All three fail
without this change. The Chat/Messages/Content, Chat/Input, Chat/ask
and hooks/Input suites pass, apart from TokenUsage/index.spec.tsx,
which fails the same way without this change. tsc, ESLint and Prettier
are clean on the changed files.
Escape in the chat card skips the question or batch, but only before
anything is entered, so a stray Escape cannot discard an answer and
resume the run. Escape with focus inside the composer popover moves the
question to the chat, as it already did from the composer, and returns
focus to the composer once the popover closes. Also drops the em dashes
from the batched card's comments.
Ignore auto-repeated Enter before confirming a picked option, so a held
Enter cannot both pick and submit the last question. In the answer
field, Shift, Alt and repeated Enter now prevent the enclosing chat
form's implicit submit instead of returning early. Escape that cancels
an IME composition no longer moves the popover to the chat, and the
arrival-focus flag lives in the batch's Jotai form state.
The window shortcut handler yields submitMessage in every editing field
except the composer, so a chord such as Ctrl+J did nothing in the ask
answer fields. Both the single-question answer box and the batched
answer field now act on it, through a shared isReboundSubmitChord.
@berry-13
berry-13 force-pushed the berry-13/enhancement-add-shortcut-controls-to-hitl-forms branch from 3bc4589 to 79c7051 Compare October 18, 2026 16:53
@berry-13
berry-13 merged commit f51ea67 into dev Oct 18, 2026
35 checks passed
@berry-13
berry-13 deleted the berry-13/enhancement-add-shortcut-controls-to-hitl-forms branch October 18, 2026 18:17
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.

3 participants