feat: keyboard waves A/B — global shortcut registry, question row machine, Esc-cancel dictation

Wave A (owner key map): registry in src/core/keyboard.ts + KeyboardShortcuts
provider; Cmd+D dictation, Cmd+K model picker, Shift+Tab thinking cycle,
Cmd+B sidebar, Cmd+Shift+F sessions search, Alt+arrows session cycle,
Alt+Enter open, Cmd+G graph, Cmd+N new, F2 rename, ? cheat sheet. Tab
fully suppressed; Esc chain popover > dictation > abort (local stream).
Esc now CANCELS dictation (discard) — flush stays on Cmd+D/Stop. Owner
bug reports fixed: focus outline removed, Alt+Enter setState-in-render,
Cmd+F graph node search (surface-scoped lh:kbd-find).

Wave B: question dock row machine — arrows walk options + dont-know/Other
with wrap + ring highlight, Space picks (mirrors click semantics), Enter
submits when canSubmit; typing surfaces and in-dock keys keep native
behavior. aria-checked={isSel} restored on option buttons.

docs/keyboard.md records the full map.
This commit is contained in:
amosblomqvist
2026-09-17 21:18:56 +03:00
parent 682edde844
commit 8b158972db
11 changed files with 232 additions and 38 deletions
+5 -5
View File
@@ -899,7 +899,7 @@ export function Chat({
turns={turns}
onReserve={setDockReserve}
dictationSlot={<DictationBar dictation={dictation} />}
dictationStop={dictation.stop}
dictationCancel={dictation.cancel}
composer={
<Composer
// Stop applies only to a LOCAL stream: a server-side turn has no
@@ -910,7 +910,7 @@ export function Chat({
onSend={onSend}
onStop={() => stop()}
canSend={canSend}
onEscape={dictation.active ? dictation.stop : undefined}
onEscape={dictation.active ? dictation.cancel : undefined}
verdict={
planVerdict
? {
@@ -978,7 +978,7 @@ function DockOverlay({
plan,
onOpenPlan,
dictationSlot,
dictationStop,
dictationCancel,
}: {
pending: ReturnType<typeof findPendingToolCall>;
/** Teach-phase pending question: arrives collapsed behind the reveal button (W5). */
@@ -1006,7 +1006,7 @@ function DockOverlay({
dictationSlot?: React.ReactNode;
/** Esc in a pending question's field stops dictation (the strip is the
* stats bar's; the stop belongs to the page-owned hook, threaded down). */
dictationStop?: () => void;
dictationCancel?: () => void;
}) {
const { isAtBottom, scrollToBottom, scrollRef } = useStickToBottomContext();
// Teach-phase mount (session reload with a pending check): the dock starts
@@ -1257,7 +1257,7 @@ function DockOverlay({
phase={phase}
onExpand={expandAndReveal}
onSubmit={onSubmitAnswer}
onEscape={dictationStop}
onEscape={dictationCancel}
/>
) : (
composer