keyboard: the one-Esc law — a surfaced layer claims at the document level (keyboard 4 of 4)
Owner ruling (keyboard round 2026-10-06): Esc (or any key) does ONE action, on the topmost active layer. The plan-open-while-dictating double-act was the symptom: the plan popup listened at WINDOW level with no claim, and same-node window listeners all fire — the global chain (registered first) cancelled the dictation while the plan's own listener closed the plan. One Esc, two actions. The law (now the house pattern, documented at the chain): - field-level handlers claim at their element (focus in a field: the field's action) — unchanged, already correct - every SURFACED layer claims at the DOCUMENT level with preventDefault + stopPropagation — document fires before the window chain, so the chain stays silent under any surface - the window chain is the bottom-up fallback: dictation (a live capture, NOT a surface — it survives the Esc that closes a plan or overlay), then abort (escAbortTarget layers its own targets) Converted to the claim: plan popup, session overlay, chain table, settings dialog, setup dialog, the delete/abort dialogs (window-level stopPropagation was never enough — same-node listeners all fire), and the stats popover. Popovers (popover-dismiss), menus, model picker, and the sidebar context menu already claimed correctly. 1427 tests, tsc, lint, build green.
This commit is contained in:
@@ -82,10 +82,18 @@ export function KeyboardShortcuts() {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
// Popovers stopPropagation on their own Esc, so reaching here
|
||||
// means none was open. Dictation first, then abort the turn
|
||||
// (a server-side turn has no client request to abort — the
|
||||
// abort seam in Chat decides what's honestly stoppable).
|
||||
// THE ONE-ESC LAW (owner, keyboard round 2026-10-06): one
|
||||
// Esc, one action, on the topmost active layer. Field-level
|
||||
// handlers claim at their element (focus in a field: the
|
||||
// field's action); every SURFACED layer (popovers, plan,
|
||||
// overlays, dialogs) claims at the DOCUMENT level, which
|
||||
// fires before this window listener — so reaching here means
|
||||
// NOTHING is surfaced and no field is focused. This chain is
|
||||
// the fallback, bottom-up: dictation (a live capture, not a
|
||||
// surface — it survives Esc that closes a plan or overlay),
|
||||
// then abort the turn (a server-side turn has no client
|
||||
// request to abort — the abort seam in Chat decides what's
|
||||
// honestly stoppable, layered by escAbortTarget).
|
||||
if (isDictationActive()) {
|
||||
e.preventDefault();
|
||||
window.dispatchEvent(new CustomEvent("lh:kbd-cancel-dictation"));
|
||||
|
||||
Reference in New Issue
Block a user