Skip to content

Commit a0c0bfb

Browse files
committed
🤖 fix: change cancel edit keybind from Ctrl+Q to Escape
Ctrl+Q conflicts with system quit shortcut on Linux/Windows. New behavior: - Non-vim mode: Escape cancels edit - Vim mode: Escape goes to normal mode, second Escape cancels edit UI hints update dynamically based on vim mode: - Shows 'Esc to cancel' in non-vim mode - Shows 'Esc×2 to cancel' in vim mode _Generated with mux_
1 parent 76d8779 commit a0c0bfb

File tree

5 files changed

+47
-13
lines changed

5 files changed

+47
-13
lines changed

scripts/bump_tag.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ if [[ -z "$CURRENT_VERSION" || "$CURRENT_VERSION" == "null" ]]; then
1818
fi
1919

2020
# Parse semver components
21-
IFS='.' read -r MAJOR MINOR_V PATCH <<< "$CURRENT_VERSION"
21+
IFS='.' read -r MAJOR MINOR_V PATCH <<<"$CURRENT_VERSION"
2222

2323
# Calculate new version
2424
if [[ "$MINOR" == "true" ]]; then
@@ -30,7 +30,7 @@ fi
3030
echo "Bumping version: $CURRENT_VERSION -> $NEW_VERSION"
3131

3232
# Update package.json
33-
jq --arg v "$NEW_VERSION" '.version = $v' package.json > package.json.tmp
33+
jq --arg v "$NEW_VERSION" '.version = $v' package.json >package.json.tmp
3434
mv package.json.tmp package.json
3535

3636
# Commit and tag

src/browser/components/ChatInput/index.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,14 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
826826
}
827827
};
828828

829+
// Handler for Escape in vim normal mode - cancels edit if editing
830+
const handleEscapeInNormalMode = () => {
831+
if (variant === "workspace" && editingMessage && props.onCancelEdit) {
832+
props.onCancelEdit();
833+
inputRef.current?.blur();
834+
}
835+
};
836+
829837
const handleKeyDown = (e: React.KeyboardEvent) => {
830838
// Handle cancel for creation variant
831839
if (variant === "creation" && matchesKeybind(e, KEYBINDS.CANCEL) && props.onCancel) {
@@ -870,9 +878,11 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
870878
return;
871879
}
872880

873-
// Handle cancel edit (Ctrl+Q) - workspace only
881+
// Handle cancel edit (Escape) - workspace only
882+
// In vim mode, escape first goes to normal mode; escapeInNormalMode callback handles cancel
883+
// In non-vim mode, escape directly cancels edit
874884
if (matchesKeybind(e, KEYBINDS.CANCEL_EDIT)) {
875-
if (variant === "workspace" && editingMessage && props.onCancelEdit) {
885+
if (variant === "workspace" && editingMessage && props.onCancelEdit && !vimEnabled) {
876886
e.preventDefault();
877887
props.onCancelEdit();
878888
const isFocused = document.activeElement === inputRef.current;
@@ -897,7 +907,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
897907
}
898908

899909
// Note: ESC handled by VimTextArea (for mode transitions) and CommandSuggestions (for dismissal)
900-
// Edit canceling is Ctrl+Q, stream interruption is Ctrl+C (vim) or Esc (normal)
910+
// Edit canceling is Esc (non-vim) or Esc twice (vim), stream interruption is Ctrl+C (vim) or Esc (normal)
901911

902912
// Don't handle keys if command suggestions are visible
903913
if (
@@ -924,7 +934,10 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
924934

925935
// Workspace variant placeholders
926936
if (editingMessage) {
927-
return `Edit your message... (${formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel, ${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send)`;
937+
const cancelHint = vimEnabled
938+
? "Esc×2 to cancel"
939+
: `${formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel`;
940+
return `Edit your message... (${cancelHint}, ${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send)`;
928941
}
929942
if (isCompacting) {
930943
const interruptKeybind = vimEnabled
@@ -1040,6 +1053,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10401053
onPaste={handlePaste}
10411054
onDragOver={handleDragOver}
10421055
onDrop={handleDrop}
1056+
onEscapeInNormalMode={handleEscapeInNormalMode}
10431057
suppressKeys={showCommandSuggestions ? COMMAND_SUGGESTION_KEYS : undefined}
10441058
placeholder={placeholder}
10451059
disabled={!editingMessage && (disabled || isSending)}
@@ -1074,7 +1088,8 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10741088
{/* Editing indicator - workspace only */}
10751089
{variant === "workspace" && editingMessage && (
10761090
<div className="text-edit-mode text-[11px] font-medium">
1077-
Editing message ({formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel)
1091+
Editing message ({vimEnabled ? "Esc×2" : formatKeybind(KEYBINDS.CANCEL_EDIT)} to
1092+
cancel)
10781093
</div>
10791094
)}
10801095

src/browser/components/VimTextArea.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,27 @@ export interface VimTextAreaProps
3232
isEditing?: boolean;
3333
suppressKeys?: string[]; // keys for which Vim should not interfere (e.g. ["Tab","ArrowUp","ArrowDown","Escape"]) when popovers are open
3434
trailingAction?: React.ReactNode;
35+
/** Called when Escape is pressed in normal mode (vim) - useful for cancel edit */
36+
onEscapeInNormalMode?: () => void;
3537
}
3638

3739
type VimMode = vim.VimMode;
3840

3941
export const VimTextArea = React.forwardRef<HTMLTextAreaElement, VimTextAreaProps>(
40-
({ value, onChange, mode, isEditing, suppressKeys, onKeyDown, trailingAction, ...rest }, ref) => {
42+
(
43+
{
44+
value,
45+
onChange,
46+
mode,
47+
isEditing,
48+
suppressKeys,
49+
onKeyDown,
50+
trailingAction,
51+
onEscapeInNormalMode,
52+
...rest
53+
},
54+
ref
55+
) => {
4156
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
4257
// Expose DOM ref to parent
4358
useEffect(() => {
@@ -129,7 +144,7 @@ export const VimTextArea = React.forwardRef<HTMLTextAreaElement, VimTextAreaProp
129144

130145
e.preventDefault();
131146

132-
// Handle side effects (undo/redo)
147+
// Handle side effects (undo/redo/escapeInNormalMode)
133148
if (result.action === "undo") {
134149
document.execCommand("undo");
135150
return;
@@ -138,6 +153,10 @@ export const VimTextArea = React.forwardRef<HTMLTextAreaElement, VimTextAreaProp
138153
document.execCommand("redo");
139154
return;
140155
}
156+
if (result.action === "escapeInNormalMode") {
157+
onEscapeInNormalMode?.();
158+
return;
159+
}
141160

142161
// Apply new state to React
143162
const newState = result.newState;

src/browser/utils/ui/keybinds.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export const KEYBINDS = {
198198
CANCEL: { key: "Escape" },
199199

200200
/** Cancel editing message (exit edit mode) */
201-
CANCEL_EDIT: { key: "q", ctrl: true, macCtrlBehavior: "control" },
201+
CANCEL_EDIT: { key: "Escape" },
202202

203203
/** Interrupt active stream (destructive - stops AI generation) */
204204
// Vim mode: Ctrl+C (familiar from terminal interrupt)

src/browser/utils/vim.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface VimState {
1919
pendingOp: null | { op: "d" | "y" | "c"; at: number; args?: string[] };
2020
}
2121

22-
export type VimAction = "undo" | "redo";
22+
export type VimAction = "undo" | "redo" | "escapeInNormalMode";
2323

2424
export type VimKeyResult =
2525
| { handled: false } // Browser should handle this key
@@ -457,9 +457,9 @@ function handleNormalModeKey(state: VimState, key: string, modifiers: KeyModifie
457457
const opResult = tryHandleOperator(state, key, now);
458458
if (opResult) return opResult;
459459

460-
// Stay in normal mode for ESC
460+
// Escape in normal mode - signal to parent (e.g., to cancel edit mode)
461461
if (key === "Escape" || (key === "[" && modifiers.ctrl)) {
462-
return { handled: true, newState: state };
462+
return { handled: true, newState: state, action: "escapeInNormalMode" };
463463
}
464464

465465
// Swallow all other single-character keys in normal mode (don't type letters)

0 commit comments

Comments
 (0)