keyboard: Enter submits from the custom answer; Enter opens the waiting check (keyboard 1-2 of 4)
Slice 1 — the ask's custom-answer field: Enter submits directly. A single-line input with no newline to protect, and typing has already selected the row — every Enter (shift or not) rides the same submit() seam as the button, dictation finish-first included. No more the Esc-then-Enter dance to file a custom answer. Slice 2 — Enter opens the waiting check (owner, keyboard round 2026-10-06): while a question waits with the dock CLOSED (behind its reveal invitation, or collapsed by scroll) Enter is never dead. Any live dictation finishes FIRST — the flush lands in the reading-thinking field, the live claim, per the nothing-gets-lost law — then the check reveals/expands. Two seams: the reading-thinking field's own Enter (plain or ⌘/Ctrl; Shift+Enter keeps the newline convention — the dump convention survives for deliberate breaks, and Enter stays a newline once the check is OPEN), and a document-level listener for plain-focus Enter — gated to skip typing surfaces and focused buttons, which own their own Enter. 1427 tests, tsc, lint, build green.
This commit is contained in:
+43
-1
@@ -26,7 +26,7 @@ import { Composer } from "@/components/composer";
|
||||
import { useEnterSubmits } from "@/components/enter-submits";
|
||||
import { dockStateKey } from "@/components/dock-state-key";
|
||||
import { QueuedSteersBody, type QueuedSteerLine } from "@/components/queued-steers";
|
||||
import { DictationBar, joinDictation, useDictation } from "@/components/dictation";
|
||||
import { DictationBar, finishDictation, joinDictation, useDictation } from "@/components/dictation";
|
||||
import { JobsContext } from "@/components/jobs-context";
|
||||
import { StatsBar } from "@/components/stats-bar";
|
||||
import { useServerEvent, useServerResync } from "@/components/server-events";
|
||||
@@ -2027,6 +2027,39 @@ function DockOverlay({
|
||||
setExpanded(true);
|
||||
void suppressFoldDuringReveal();
|
||||
};
|
||||
const expandAndRevealRef = useRef(expandAndReveal);
|
||||
expandAndRevealRef.current = expandAndReveal;
|
||||
// ENTER OPENS THE WAITING CHECK (owner, keyboard round 2026-10-06):
|
||||
// while a question waits with the dock CLOSED — behind its reveal
|
||||
// invitation or collapsed by scroll — Enter is never dead. Any live
|
||||
// dictation FINISHES FIRST (the flush lands in the reading-thinking
|
||||
// field, its live claim — the nothing-gets-lost law), then the check
|
||||
// opens. A typing surface or a focused button owns its own Enter (the
|
||||
// reading-thinking field opens from INSIDE when the check is closed;
|
||||
// a focused button activates natively) — this listener claims only
|
||||
// the dead case: plain focus with Enter doing nothing.
|
||||
useEffect(() => {
|
||||
if (!pending || expanded) return;
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Enter" || e.altKey) return;
|
||||
if (e.shiftKey && !e.metaKey && !e.ctrlKey) return;
|
||||
const t = e.target as HTMLElement | null;
|
||||
if (
|
||||
t &&
|
||||
((t.tagName === "INPUT" && (t as HTMLInputElement).type !== "range") ||
|
||||
t.tagName === "TEXTAREA" ||
|
||||
t.isContentEditable ||
|
||||
t.closest("button"))
|
||||
)
|
||||
return;
|
||||
e.preventDefault();
|
||||
void finishDictation().then(() => {
|
||||
expandAndRevealRef.current();
|
||||
});
|
||||
};
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [pending, expanded]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
@@ -2149,6 +2182,15 @@ function DockOverlay({
|
||||
value={readingThinking}
|
||||
onChange={onReadingThinking}
|
||||
onEscape={dictationCancel}
|
||||
onOpen={
|
||||
!expanded
|
||||
? () => {
|
||||
void finishDictation().then(() => {
|
||||
expandAndRevealRef.current();
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
claim={revealPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -667,6 +667,17 @@ function QuestionDockForm({
|
||||
e.currentTarget.blur();
|
||||
setKbdIdx(optionCount);
|
||||
}
|
||||
// Enter submits directly (owner, keyboard round 2026-10-06):
|
||||
// the custom answer is a single-line input — no newline
|
||||
// to protect, and typing has already selected the row, so
|
||||
// the payload is ready. Every Enter (shift or not — nothing
|
||||
// else could ever happen in a one-line field) rides the
|
||||
// same submit seam as the button, dictation finish-first
|
||||
// included.
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
}
|
||||
}}
|
||||
placeholder="Type your own answer…"
|
||||
className="lh-input w-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
|
||||
|
||||
@@ -22,12 +22,18 @@ export function ReadingThinkingField({
|
||||
value,
|
||||
onChange,
|
||||
onEscape,
|
||||
onOpen,
|
||||
claim,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
/** Esc in the field stops dictation (threaded from the page's hook). */
|
||||
onEscape?: () => void;
|
||||
/** The check waits CLOSED — Enter opens it (owner, keyboard round
|
||||
* 2026-10-06). The caller finishes any live dictation FIRST (the
|
||||
* flush lands here — the field is the live claim), then reveals.
|
||||
* Absent = the check is open: Enter keeps the dumping convention. */
|
||||
onOpen?: () => void;
|
||||
/** Primary dictation claim — true while the check is behind its reveal. */
|
||||
claim: boolean;
|
||||
}) {
|
||||
@@ -60,6 +66,17 @@ export function ReadingThinkingField({
|
||||
onEscape?.();
|
||||
return;
|
||||
}
|
||||
// ENTER OPENS THE WAITING CHECK (owner, keyboard round
|
||||
// 2026-10-06): while the check sits closed, plain Enter
|
||||
// (and the universal ⌘/Ctrl+Enter) opens it — the dictation
|
||||
// flush lands HERE first via the caller's finishDictation,
|
||||
// so spoken thoughts ride along. Shift+Enter stays a newline.
|
||||
if (e.key === "Enter" && onOpen !== undefined && (!e.shiftKey || e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpen();
|
||||
return;
|
||||
}
|
||||
// The dock's dumping convention: plain Enter is a newline.
|
||||
// No submit button here — the check's own submit is this
|
||||
// field's carrier — so ⌘↵ just blurs.
|
||||
|
||||
Reference in New Issue
Block a user