fix(ui): capture double escape abort in composer
This commit is contained in:
@@ -21,7 +21,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
const armAbortPrompt = useSessionUIStore((s) => s.armAbortPrompt);
|
const armAbortPrompt = useSessionUIStore((s) => s.armAbortPrompt);
|
||||||
const clearAbortPrompt = useSessionUIStore((s) => s.clearAbortPrompt);
|
const clearAbortPrompt = useSessionUIStore((s) => s.clearAbortPrompt);
|
||||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||||
const abortCurrentOperation = sessionActions.abortCurrentOperation;;
|
const abortCurrentOperation = sessionActions.abortCurrentOperation;
|
||||||
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette);
|
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette);
|
||||||
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
||||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||||
@@ -127,8 +127,93 @@ export const useKeyboardShortcuts = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEscapeKeyDownCapture = (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Escape') return;
|
||||||
|
|
||||||
|
const target = e.target as Element | null;
|
||||||
|
const isInsideDialog = Boolean(target?.closest('[role="dialog"]'));
|
||||||
|
const isSettingsMounted = Boolean(document.querySelector('[data-settings-view="true"]'));
|
||||||
|
const isInsideTerminal = isTerminalEventTarget(target);
|
||||||
|
const hasDropdownInteraction = isDropdownEventTarget(target) || hasOpenDropdown();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isSettingsDialogOpen,
|
||||||
|
isCommandPaletteOpen,
|
||||||
|
isHelpDialogOpen,
|
||||||
|
isSessionSwitcherOpen,
|
||||||
|
isAboutDialogOpen,
|
||||||
|
isMultiRunLauncherOpen,
|
||||||
|
isImagePreviewOpen,
|
||||||
|
activeMainTab,
|
||||||
|
isPromptNavigatorPanelOpen,
|
||||||
|
} = useUIStore.getState();
|
||||||
|
|
||||||
|
if (isInsideDialog || isInsideTerminal || hasDropdownInteraction) {
|
||||||
|
resetAbortPriming();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPromptNavigatorPanelOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
setPromptNavigatorPanelOpen(false);
|
||||||
|
resetAbortPriming();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSettingsDialogOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
setSettingsDialogOpen(false);
|
||||||
|
resetAbortPriming();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSettingsMounted) {
|
||||||
|
resetAbortPriming();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen || isMultiRunLauncherOpen || isImagePreviewOpen;
|
||||||
|
const isChatActive = activeMainTab === 'chat';
|
||||||
|
|
||||||
|
if (hasOverlay || !isChatActive) {
|
||||||
|
resetAbortPriming();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = currentSessionId;
|
||||||
|
if (sessionPhase === 'idle' || !sessionId) {
|
||||||
|
resetAbortPriming();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const primedUntil = abortPrimedUntilRef.current;
|
||||||
|
|
||||||
|
if (primedUntil && now < primedUntil) {
|
||||||
|
e.preventDefault();
|
||||||
|
resetAbortPriming();
|
||||||
|
void abortCurrentOperation(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const expiresAt = armAbortPrompt(3000) ?? now + 3000;
|
||||||
|
abortPrimedUntilRef.current = expiresAt;
|
||||||
|
|
||||||
|
if (abortPrimedTimeoutRef.current) {
|
||||||
|
clearTimeout(abortPrimedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = Math.max(expiresAt - now, 0);
|
||||||
|
abortPrimedTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (abortPrimedUntilRef.current && Date.now() >= abortPrimedUntilRef.current) {
|
||||||
|
resetAbortPriming();
|
||||||
|
}
|
||||||
|
}, delay || 0);
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (isTerminalEventTarget(e.target)) {
|
if (e.key === 'Escape' || isTerminalEventTarget(e.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,100 +615,15 @@ export const useKeyboardShortcuts = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
const target = e.target as Element | null;
|
|
||||||
const isInsideDialog = Boolean(target?.closest('[role="dialog"]'));
|
|
||||||
const isSettingsMounted = Boolean(document.querySelector('[data-settings-view="true"]'));
|
|
||||||
const isInsideTerminal = isTerminalEventTarget(target);
|
|
||||||
const hasDropdownInteraction = isDropdownEventTarget(target) || hasOpenDropdown();
|
|
||||||
|
|
||||||
const {
|
|
||||||
isSettingsDialogOpen,
|
|
||||||
isCommandPaletteOpen,
|
|
||||||
isHelpDialogOpen,
|
|
||||||
isSessionSwitcherOpen,
|
|
||||||
isAboutDialogOpen,
|
|
||||||
isMultiRunLauncherOpen,
|
|
||||||
isImagePreviewOpen,
|
|
||||||
activeMainTab,
|
|
||||||
isPromptNavigatorPanelOpen,
|
|
||||||
} = useUIStore.getState();
|
|
||||||
|
|
||||||
if (isInsideDialog || isInsideTerminal || hasDropdownInteraction) {
|
|
||||||
resetAbortPriming();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPromptNavigatorPanelOpen) {
|
|
||||||
e.preventDefault();
|
|
||||||
setPromptNavigatorPanelOpen(false);
|
|
||||||
resetAbortPriming();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If settings is open, close it
|
|
||||||
if (isSettingsDialogOpen) {
|
|
||||||
e.preventDefault();
|
|
||||||
setSettingsDialogOpen(false);
|
|
||||||
resetAbortPriming();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSettingsMounted) {
|
|
||||||
resetAbortPriming();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any overlay is open or not on chat tab - don't process abort
|
|
||||||
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen || isMultiRunLauncherOpen || isImagePreviewOpen;
|
|
||||||
const isChatActive = activeMainTab === 'chat';
|
|
||||||
|
|
||||||
if (hasOverlay || !isChatActive) {
|
|
||||||
resetAbortPriming();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Double-ESC abort logic - only when on chat tab with no overlays
|
|
||||||
const sessionId = currentSessionId;
|
|
||||||
const canAbortNow = sessionPhase !== 'idle' && Boolean(sessionId);
|
|
||||||
if (!canAbortNow) {
|
|
||||||
resetAbortPriming();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const primedUntil = abortPrimedUntilRef.current;
|
|
||||||
|
|
||||||
if (primedUntil && now < primedUntil) {
|
|
||||||
e.preventDefault();
|
|
||||||
resetAbortPriming();
|
|
||||||
void abortCurrentOperation(sessionId ?? '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const expiresAt = armAbortPrompt(3000) ?? now + 3000;
|
|
||||||
abortPrimedUntilRef.current = expiresAt;
|
|
||||||
|
|
||||||
if (abortPrimedTimeoutRef.current) {
|
|
||||||
clearTimeout(abortPrimedTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
const delay = Math.max(expiresAt - now, 0);
|
|
||||||
abortPrimedTimeoutRef.current = setTimeout(() => {
|
|
||||||
if (abortPrimedUntilRef.current && Date.now() >= abortPrimedUntilRef.current) {
|
|
||||||
resetAbortPriming();
|
|
||||||
}
|
|
||||||
}, delay || 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleTerminalShortcutCapture, true);
|
window.addEventListener('keydown', handleTerminalShortcutCapture, true);
|
||||||
|
window.addEventListener('keydown', handleEscapeKeyDownCapture, true);
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleTerminalShortcutCapture, true);
|
window.removeEventListener('keydown', handleTerminalShortcutCapture, true);
|
||||||
|
window.removeEventListener('keydown', handleEscapeKeyDownCapture, true);
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
Reference in New Issue
Block a user