fix(desktop): keep esc inside compose instead of exiting the stream (#294)
While recording or reviewing a compose, Escape both cancelled the compose and exited the stream. The stream's window-level navigation handler skips keys when an input is focused (isTypingTarget), which is why the text path was unaffected, but recording/reviewing have no focused input so Escape leaked through to the exit handler. Fix it at the compose layer rather than teaching the navigation pipe about compose: the compose key handler now consumes (preventDefault + stopPropagation) any key it handles and listens in the capture phase, so it reliably wins over the stream's bubble-phase navigation/action handlers regardless of listener registration order. Pure pause states (hold-space) are untouched, so Escape still exits the stream there. This leaves the typing/task/configuring paths equivalent (their onCancel is the same cancel() the handler invokes) while ⌘+Enter / ⌘+M still fall through to the text editor. Claude-Session: https://claude.ai/code/session_01DapjFX1MhYPZeJv4s56K5L Co-authored-by: Claude <noreply@anthropic.com>
This commit was merged in pull request #294.
This commit is contained in:
@@ -595,6 +595,16 @@ export function ComposeOverlay({
|
|||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
const currentStep = stepRef.current;
|
const currentStep = stepRef.current;
|
||||||
|
|
||||||
|
// Consume a key compose handles so it never reaches the stream's
|
||||||
|
// window-level navigation/action handlers. Without this, e.g. Escape
|
||||||
|
// while recording or reviewing would both cancel compose and exit the
|
||||||
|
// stream. Listening in the capture phase (see below) guarantees compose
|
||||||
|
// sees the key before those handlers regardless of registration order.
|
||||||
|
const consume = () => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
currentStep === 'typing' ||
|
currentStep === 'typing' ||
|
||||||
currentStep === 'task' ||
|
currentStep === 'task' ||
|
||||||
@@ -602,7 +612,7 @@ export function ComposeOverlay({
|
|||||||
currentStep === 'picking'
|
currentStep === 'picking'
|
||||||
) {
|
) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
e.preventDefault();
|
consume();
|
||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -620,19 +630,19 @@ export function ComposeOverlay({
|
|||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case 'idle': {
|
case 'idle': {
|
||||||
if (e.key === '`' && !e.repeat) {
|
if (e.key === '`' && !e.repeat) {
|
||||||
e.preventDefault();
|
consume();
|
||||||
handleRecordIntent();
|
handleRecordIntent();
|
||||||
} else if (e.key === 's' || e.key === 'S') {
|
} else if (e.key === 's' || e.key === 'S') {
|
||||||
e.preventDefault();
|
consume();
|
||||||
if (!guardIdle()) break;
|
if (!guardIdle()) break;
|
||||||
if (!requireDesktop('Screen recording')) break;
|
if (!requireDesktop('Screen recording')) break;
|
||||||
setRecordingSource('screen');
|
setRecordingSource('screen');
|
||||||
setStepSync('picking');
|
setStepSync('picking');
|
||||||
} else if (e.key === 't' || e.key === 'T') {
|
} else if (e.key === 't' || e.key === 'T') {
|
||||||
e.preventDefault();
|
consume();
|
||||||
handleTextIntent();
|
handleTextIntent();
|
||||||
} else if (e.key === 'd' || e.key === 'D') {
|
} else if (e.key === 'd' || e.key === 'D') {
|
||||||
e.preventDefault();
|
consume();
|
||||||
handleTaskIntent();
|
handleTaskIntent();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -641,17 +651,17 @@ export function ComposeOverlay({
|
|||||||
case 'recording': {
|
case 'recording': {
|
||||||
if (e.key === '`' && !e.repeat) {
|
if (e.key === '`' && !e.repeat) {
|
||||||
// Second tap stops media recording (toggle mode)
|
// Second tap stops media recording (toggle mode)
|
||||||
e.preventDefault();
|
consume();
|
||||||
handleStopIntent();
|
handleStopIntent();
|
||||||
} else if (
|
} else if (
|
||||||
(e.key === 's' || e.key === 'S') &&
|
(e.key === 's' || e.key === 'S') &&
|
||||||
recordingSourceRef.current === 'screen'
|
recordingSourceRef.current === 'screen'
|
||||||
) {
|
) {
|
||||||
// S stops screen recording when main window is focused
|
// S stops screen recording when main window is focused
|
||||||
e.preventDefault();
|
consume();
|
||||||
handleStopIntent();
|
handleStopIntent();
|
||||||
} else if (e.key === 'q' || e.key === 'Q' || e.key === 'Escape') {
|
} else if (e.key === 'q' || e.key === 'Q' || e.key === 'Escape') {
|
||||||
e.preventDefault();
|
consume();
|
||||||
handleCancelIntent();
|
handleCancelIntent();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -659,10 +669,10 @@ export function ComposeOverlay({
|
|||||||
|
|
||||||
case 'reviewing': {
|
case 'reviewing': {
|
||||||
if (e.key === 'q' || e.key === 'Q' || e.key === 'Escape') {
|
if (e.key === 'q' || e.key === 'Q' || e.key === 'Escape') {
|
||||||
e.preventDefault();
|
consume();
|
||||||
handleCancelIntent();
|
handleCancelIntent();
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
consume();
|
||||||
handleSendIntent();
|
handleSendIntent();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -689,10 +699,13 @@ export function ComposeOverlay({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
// Capture phase so compose can consume keys before the stream's
|
||||||
|
// window-level navigation/action handlers (which listen in the bubble
|
||||||
|
// phase) see them.
|
||||||
|
window.addEventListener('keydown', handleKeyDown, true);
|
||||||
window.addEventListener('keyup', handleKeyUp);
|
window.addEventListener('keyup', handleKeyUp);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown, true);
|
||||||
window.removeEventListener('keyup', handleKeyUp);
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
Reference in New Issue
Block a user