From 112878b0ee5c21d36da204fb0018225f8de280bb Mon Sep 17 00:00:00 2001 From: jagrat7 Date: Sat, 3 Oct 2026 02:21:30 -0400 Subject: [PATCH] feat(web): add an Escape to stop setting for running turns --- apps/web/src/components/chat/ChatComposer.tsx | 12 ++++-- .../settings/KeybindingsSettings.tsx | 16 +++++++ .../components/settings/SettingsPanels.tsx | 42 +++++++++++++++++++ .../src/components/settings/settingsSearch.ts | 6 +++ docs/user/keybindings.md | 3 ++ packages/contracts/src/settings.ts | 2 + 6 files changed, 78 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 8cd297925f31..7f753ffd68e7 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -4345,9 +4345,15 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const { trigger } = resolveActiveComposerTrigger(); const menuIsActive = composerMenuOpenRef.current || trigger !== null; if (key === "Escape") { - if (!menuIsActive || event.isComposing || event.keyCode === 229) return false; - dismissComposerTrigger(trigger); - composerMenuOpenRef.current = false; + if (event.isComposing || event.keyCode === 229) return false; + // Like Enter, an open menu takes Escape first; otherwise Escape to stop can stop the turn. + if (menuIsActive) { + dismissComposerTrigger(trigger); + composerMenuOpenRef.current = false; + return true; + } + if (phase !== "running" || !settings.escapeToStop) return false; + if (!event.repeat) onInterrupt(); return true; } if (menuIsActive && (submissionIntent === null || submissionIntent === "foreground")) { diff --git a/apps/web/src/components/settings/KeybindingsSettings.tsx b/apps/web/src/components/settings/KeybindingsSettings.tsx index bf14e709dda1..7b5bc23cbc17 100644 --- a/apps/web/src/components/settings/KeybindingsSettings.tsx +++ b/apps/web/src/components/settings/KeybindingsSettings.tsx @@ -39,6 +39,7 @@ import { formatShortcutLabel } from "../../keybindings"; import { cn } from "../../lib/utils"; import { serverEnvironment } from "../../state/server"; import { useSettingsScope } from "./SettingsScopeContext"; +import { useScopedSettings } from "./useScopedSettings"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; @@ -303,6 +304,13 @@ function KeybindingConflictWarning({ labels }: { labels: ReadonlyArray } ); } +/** Flags a bare Escape binding while the Escape to stop setting is on. */ +export function EscapeToStopConflictWarning({ labels }: { labels: ReadonlyArray }) { + if (labels.length === 0) return null; + const description = `Conflicts with ${labels.join(", ")}.`; + return {description}; +} + function WhenVariableSelect({ value, variables, @@ -1021,6 +1029,7 @@ function KeybindingHoverRowMenu(props: { function KeybindingSettingsRow(props: KeybindingRowProps) { const { row, isSaving, anchorId, allRows, variables, onSave, onReset, onRemove } = props; const editor = useKeybindingRowEditor({ row, allRows, onSave }); + const escapeToStop = useScopedSettings((settings) => settings.escapeToStop); return ( + settings.escapeToStop); return ( +