Skip to content

feat(web): add an Escape to stop setting for running turns - #3

Open
jagrat7 wants to merge 1 commit into
mainfrom
feat/esc-to-stop-thread
Open

jagrat7 wants to merge 1 commit into
mainfrom
feat/esc-to-stop-thread

Conversation

@jagrat7

@jagrat7 jagrat7 commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Problem

There was no quick keyboard way to stop a running turn. thread.stop exists as a keybinding command but has no default key. Binding it to Escape doesn't work well either: the stop handler listens on the whole window and runs before anything else, so Escape stops the agent even when you meant to close a menu or dialog.

Fix

A new Settings → General → Escape to stop switch sits under Send shortcut. It's off by default.

Escape is handled in the composer's key handler, the same place Enter to send is. In the composer:

  1. If the / or @ suggestion menu is open, Escape closes it. This already worked.
  2. Otherwise, if a turn is running and Escape to stop is on, Escape stops it.

Because it only works while the composer has focus, dialogs, menus, and other inputs keep Escape. Escape during IME composition is left alone, and holding Escape stops the turn only once. thread.stop is unchanged and still has no default key.

While Escape to stop is on, a keybinding on bare Escape shows "Conflicts with Escape to stop." in Settings → Keybindings, and the Escape to stop row shows "Conflicts with ." Keybindings run before the composer, so they would take Escape first.

Added a line to the composer controls section of docs/user/keybindings.md.

Web and desktop share the composer. Mobile has no Escape key, so it's unchanged. The stop path is the same for every provider, and nothing crosses the wire.

Verification

  • apps/web typecheck is clean.
  • vp test run for keybindings.test.ts, KeybindingsSettings.logic.test.ts, settingsSearch.test.ts, and packages/contracts/src/settings.test.ts passes.
  • Not yet exercised in a live client in this version. An earlier version of this PR was tested in the dev web client.

Done by Claude Opus 5.5 (1M context) in Claude Code, running inside T3 Code.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge, though the outstanding warning and modified-Escape behavior and the lost regression coverage merit follow-up.

Fix All in CodexFindings

  1. P2 Warning checks wrong environment ▶
  2. P2 Conditional bindings flagged as conflicts ▶
  3. P2 Dialog conflict warning disappears ▶
  4. P2 Modified Escape also stops turns ▶

Summary

The PR adds an opt-in Escape-to-stop composer setting, conflict warnings in Settings, search and documentation entries, and a persisted settings field. The latest changes simplify conflict detection and remove several setting-specific tests.

Reviews (15) · Last reviewed commit: "feat(web): add an Escape to stop setting..."

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M labels Sep 23, 2026

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

Devin Review

Comment thread apps/web/src/components/ChatView.tsx Outdated
event.stopPropagation();
if (!event.repeat) void onInterrupt();
};
window.addEventListener("keydown", handler);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Custom stop shortcuts fail in editors

With a custom thread.stop chord, focused controls that stop bubbling prevent the shortcut from firing. The former capture listener handled these chords before target handlers, so users cannot stop active turns from those controls.

Learn more

The bubble listener is needed for the new unmodified Escape default because contextual UI gets first refusal. However, the change routes every binding for thread.stop through that listener. Some focused controls stop keydown propagation without regard to the resolved command, such as AssistantCitationCommentEditor, so the event never reaches window in the bubble phase. The previous capture handler received the same custom chord before these target handlers.

Example: A user binds thread.stop to mod+x and focuses the citation comment editor while a turn runs. Pressing mod+x is stopped by the editor and never reaches this listener, while the stop button remains available.

Recommended fix: Defer only the unmodified Escape binding to the bubble phase. Continue handling other resolved thread.stop bindings in the capture listener, or use a capture listener that schedules Escape interruption after propagation and cancels it when the event becomes claimed.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 38af18a. Only Escape-keyed thread.stop bindings wait for the bubble phase now. Other custom chords are handled in the capture-phase shortcut handler again, so controls that stop propagation can't swallow them.

Comment thread packages/shared/src/keybindings.ts Outdated
{ key: "mod+alt+shift+t", command: "themeEditor.toggle" },
{ key: "mod+s", command: "composer.stash", when: "!terminalFocus" },
{ key: "mod+shift+enter", command: "thread.steerQueuedMessage", when: "!terminalFocus" },
{ key: "escape", command: "thread.stop", when: "!terminalFocus" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Existing Escape bindings become shadowed

After upgrade, an existing escape binding with a non-identical condition is shadowed by thread.stop outside terminals. Startup appends the new default, and last-match resolution selects it. The user's existing Escape action no longer runs.

Learn more

Startup backfills a newly added default unless another rule has the same serialized shortcut and exactly the same when string. An existing unconditional Escape binding therefore does not conflict with escape plus !terminalFocus. syncDefaultKeybindingsOnStartup appends the new default after existing rules, while resolveShortcutCommand searches from the end. The new rule consequently wins everywhere outside terminals, even when no turn is running.

Example: A user already has { "key": "escape", "command": "script.cancel-preview.run" }. After upgrading, startup appends { "key": "escape", "command": "thread.stop", "when": "!terminalFocus" }; Escape now resolves to thread.stop throughout the chat and never runs the script.

Recommended fix: During default migration, treat same-key rules with overlapping when conditions as conflicts, or avoid backfilling this default when any existing unmodified Escape rule can match outside terminals. Add a server migration test covering an unconditional existing Escape binding.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 38af18a. Startup backfill now treats an unconditional rule on the same key as a conflict, so an existing unconditional escape rule keeps the default from being appended. At runtime, mergeWithDefaultKeybindings puts retained defaults before custom rules, so the user's rule still wins. Added a server test for this.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reverted in 1b9ce41. An existing unconditional escape rule can only come from a hand-edited keybindings.json: the Settings recorder treats Escape as cancel, and no earlier default used Escape. Appended defaults shadowing looser user rules is how keybinding backfill works in general, not something this PR introduces. If it's worth changing, it should be fixed in the backfill logic for all defaults, separately.

Comment thread packages/shared/src/keybindings.ts Outdated
Comment thread packages/shared/src/keybindings.ts Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
@jagrat7 jagrat7 changed the title feat(web): press Escape to stop a running thread feat(web): better Escape support for stopping threads Sep 24, 2026
@jagrat7 jagrat7 changed the title feat(web): better Escape support for stopping threads feat(web): press Escape in the composer to stop a running turn Sep 24, 2026
@github-actions github-actions Bot added size:S and removed size:M labels Sep 24, 2026
Comment thread apps/web/src/components/chat/ChatComposer.tsx Outdated
@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 112878b.

This comment will update automatically after the next completed run.

@github-actions github-actions Bot added size:M and removed size:S labels Sep 28, 2026
@jagrat7 jagrat7 changed the title feat(web): press Escape in the composer to stop a running turn feat(web): add a Stop shortcut setting to stop a running turn with Escape Sep 28, 2026
// Keybindings run before the composer, so a bare Escape binding can take Escape from Stop shortcut.
const hasEscapeBinding =
settings.stopShortcut === "escape" &&
(environment?.serverConfig?.keybindings ?? []).some(

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 Warning checks wrong environment

If Settings is scoped to an environment other than the primary one, this checks the selected environment’s bindings while chat uses the primary environment’s bindings. The Stop shortcut row can miss a binding that takes Escape from the composer, or warn about one that does not.

Fix in Codex

Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
@jagrat7 jagrat7 changed the title feat(web): add a Stop shortcut setting to stop a running turn with Escape feat(web): add an Escape to stop setting for running turns Sep 28, 2026
Comment on lines +2130 to +2132
(environment?.serverConfig?.keybindings ?? [])
.filter((binding) => shortcutToKeybindingInput(binding.shortcut) === "esc")
.map((binding) => commandLabel(binding.command)),

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 Conditional bindings flagged as conflicts
A bare Escape binding conditioned on terminalFocus cannot take Escape from a focused composer, but this warning still names its command as a conflict. Users may change a binding that does not interfere with Escape to stop. The Keybindings row warning also checks the key without considering the binding’s condition.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

Comment on lines +1043 to +1045
<EscapeToStopConflictWarning
labels={escapeToStop && editor.keyDraft === "esc" ? ["Escape to stop"] : []}
/>

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 Dialog conflict warning disappears
With Escape to stop off, an existing bare Escape binding no longer shows the warning that it can take Escape away from dialogs and menus. A binding loaded from a hand-edited keybindings file can still be handled before those controls receive the key, so users lose notice of a conflict unrelated to the new switch. The same change affects the new-binding row.

Fix in Codex

@jagrat7
jagrat7 force-pushed the feat/esc-to-stop-thread branch from 5fedfc2 to ad6168f Compare October 3, 2026 06:27
@github-actions github-actions Bot added size:L and removed size:M labels Oct 3, 2026
Comment on lines +4355 to +4356
if (phase !== "running" || !settings.escapeToStop) return false;
if (!event.repeat) onInterrupt();

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 Modified Escape also stops turns

When Escape to stop is enabled and a turn is running, pressing Ctrl+Escape in the focused composer can stop the turn if no keybinding consumes it first. The handler checks for Escape but not for modifier keys, so a shortcut other than bare Escape can unexpectedly interrupt the agent.

Suggested change
if (phase !== "running" || !settings.escapeToStop) return false;
if (!event.repeat) onInterrupt();
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return false;
if (phase !== "running" || !settings.escapeToStop) return false;
if (!event.repeat) onInterrupt();

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

@jagrat7
jagrat7 force-pushed the feat/esc-to-stop-thread branch from ad6168f to 112878b Compare October 3, 2026 07:03
@github-actions github-actions Bot added size:M and removed size:L labels Oct 3, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant