feat(ui): migrate all shortcut surfaces to the centralized registry
This commit is contained in:
@@ -15,3 +15,10 @@ export function shouldStopDropdownImeEscape(
|
||||
&& event.key === 'Escape'
|
||||
&& (event.isComposing || event.keyCode === 229);
|
||||
}
|
||||
|
||||
export function isEditableEventTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
if (target.isContentEditable) return true;
|
||||
const tagName = target.tagName;
|
||||
return tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,97 +1,130 @@
|
||||
import React from 'react';
|
||||
import { focusChatInput } from '@/components/chat/composer/editor/dom';
|
||||
import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop';
|
||||
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||
import { ShortcutDispatcher, getEffectiveShortcutCombo, shortcutRegistry } from '@/lib/shortcuts';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useSelectionStore } from '@/sync/selection-store';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useKeybinds } from './useKeybind';
|
||||
import { isEditableEventTarget } from './keyboard-shortcut-dom';
|
||||
|
||||
export const useMiniChatKeyboardShortcuts = () => {
|
||||
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
|
||||
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
|
||||
const dispatcherRef = React.useRef<ShortcutDispatcher | null>(null);
|
||||
|
||||
if (!dispatcherRef.current) {
|
||||
dispatcherRef.current = new ShortcutDispatcher({
|
||||
registry: shortcutRegistry,
|
||||
getBinding: (actionId) => getEffectiveShortcutCombo(
|
||||
actionId,
|
||||
useUIStore.getState().shortcutOverrides,
|
||||
),
|
||||
});
|
||||
}
|
||||
const dispatcher = dispatcherRef.current;
|
||||
|
||||
const cycleFavoriteModel = (delta: number): boolean | void => {
|
||||
const { favoriteModels, addRecentModel } = useUIStore.getState();
|
||||
if (favoriteModels.length === 0) return false;
|
||||
|
||||
const {
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
setProvider,
|
||||
setModel,
|
||||
} = useConfigStore.getState();
|
||||
const currentIndex = favoriteModels.findIndex(
|
||||
(favorite) => favorite.providerID === currentProviderId && favorite.modelID === currentModelId,
|
||||
);
|
||||
const next = favoriteModels[(currentIndex + delta + favoriteModels.length) % favoriteModels.length];
|
||||
setProvider(next.providerID);
|
||||
setModel(next.modelID);
|
||||
addRecentModel(next.providerID, next.modelID);
|
||||
};
|
||||
|
||||
useKeybinds({
|
||||
focus_input: () => {
|
||||
focusChatInput();
|
||||
},
|
||||
new_mini_chat: () => {
|
||||
if (!canUseElectronDesktopIPC()) return false;
|
||||
void invokeDesktop('desktop_open_draft_mini_chat_window', {
|
||||
directory: '',
|
||||
projectId: null,
|
||||
})?.catch((error) => {
|
||||
console.warn('[mini-chat-shortcuts] failed to open draft mini chat window', error);
|
||||
});
|
||||
},
|
||||
new_chat: () => {
|
||||
const sessionState = useSessionUIStore.getState();
|
||||
openNewSessionDraft(sessionState.currentSessionId && sessionState.currentSessionDirectory
|
||||
? { directoryOverride: sessionState.currentSessionDirectory }
|
||||
: undefined);
|
||||
focusChatInput();
|
||||
},
|
||||
open_model_selector: () => {
|
||||
const { isModelSelectorOpen, setModelSelectorOpen } = useUIStore.getState();
|
||||
setModelSelectorOpen(!isModelSelectorOpen);
|
||||
},
|
||||
cycle_thinking_variant: () => {
|
||||
const configState = useConfigStore.getState();
|
||||
if (configState.getCurrentModelVariants().length === 0) return false;
|
||||
|
||||
configState.cycleCurrentVariant();
|
||||
const sessionId = useSessionUIStore.getState().currentSessionId;
|
||||
const {
|
||||
currentVariant,
|
||||
currentAgentName,
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
} = useConfigStore.getState();
|
||||
if (sessionId && currentAgentName && currentProviderId && currentModelId) {
|
||||
useSelectionStore.getState().saveAgentModelVariantForSession(
|
||||
sessionId,
|
||||
currentAgentName,
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
currentVariant,
|
||||
);
|
||||
}
|
||||
},
|
||||
cycle_favorite_model_forward: () => cycleFavoriteModel(1),
|
||||
cycle_favorite_model_backward: () => cycleFavoriteModel(-1),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const combo = (actionId: string) => getEffectiveShortcutCombo(actionId, shortcutOverrides);
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (eventMatchesShortcut(event, combo('focus_input'))) {
|
||||
event.preventDefault();
|
||||
focusChatInput();
|
||||
const handleActivePrefixKeyDownCapture = (event: KeyboardEvent) => {
|
||||
if (!dispatcher.hasActivePrefix()) return;
|
||||
// An unmodified completion key typed into an editable target is only a
|
||||
// deliberate sequence when the prefix was armed from that same target;
|
||||
// otherwise it is regular typing and must not be swallowed.
|
||||
if (
|
||||
!event.ctrlKey && !event.metaKey && !event.altKey
|
||||
&& isEditableEventTarget(event.target)
|
||||
&& dispatcher.getActivePrefixTarget() !== event.target
|
||||
) {
|
||||
dispatcher.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (canUseElectronDesktopIPC() && eventMatchesShortcut(event, combo('new_mini_chat'))) {
|
||||
if (dispatcher.dispatchActivePrefix(event)) {
|
||||
event.preventDefault();
|
||||
void invokeDesktop('desktop_open_draft_mini_chat_window', {
|
||||
directory: '',
|
||||
projectId: null,
|
||||
})?.catch((error) => {
|
||||
console.warn('[mini-chat-shortcuts] failed to open draft mini chat window', error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventMatchesShortcut(event, combo('new_chat'))) {
|
||||
event.preventDefault();
|
||||
const sessionState = useSessionUIStore.getState();
|
||||
openNewSessionDraft(sessionState.currentSessionId && sessionState.currentSessionDirectory
|
||||
? { directoryOverride: sessionState.currentSessionDirectory }
|
||||
: undefined);
|
||||
focusChatInput();
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventMatchesShortcut(event, combo('open_model_selector'))) {
|
||||
event.preventDefault();
|
||||
const { isModelSelectorOpen, setModelSelectorOpen } = useUIStore.getState();
|
||||
setModelSelectorOpen(!isModelSelectorOpen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventMatchesShortcut(event, combo('cycle_thinking_variant'))) {
|
||||
const configState = useConfigStore.getState();
|
||||
const variants = configState.getCurrentModelVariants();
|
||||
if (variants.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
configState.cycleCurrentVariant();
|
||||
|
||||
const nextVariant = useConfigStore.getState().currentVariant;
|
||||
const sessionId = useSessionUIStore.getState().currentSessionId;
|
||||
const agentName = useConfigStore.getState().currentAgentName;
|
||||
const providerId = useConfigStore.getState().currentProviderId;
|
||||
const modelId = useConfigStore.getState().currentModelId;
|
||||
|
||||
if (sessionId && agentName && providerId && modelId) {
|
||||
useSelectionStore.getState().saveAgentModelVariantForSession(sessionId, agentName, providerId, modelId, nextVariant);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const cyclesForward = eventMatchesShortcut(event, combo('cycle_favorite_model_forward'));
|
||||
const cyclesBackward = eventMatchesShortcut(event, combo('cycle_favorite_model_backward'));
|
||||
if (cyclesForward || cyclesBackward) {
|
||||
const { favoriteModels, addRecentModel } = useUIStore.getState();
|
||||
if (favoriteModels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const { currentProviderId, currentModelId, setProvider, setModel } = useConfigStore.getState();
|
||||
const currentIndex = favoriteModels.findIndex((favorite) => favorite.providerID === currentProviderId && favorite.modelID === currentModelId);
|
||||
const delta = cyclesForward ? 1 : -1;
|
||||
const next = favoriteModels[(currentIndex + delta + favoriteModels.length) % favoriteModels.length];
|
||||
|
||||
setProvider(next.providerID);
|
||||
setModel(next.modelID);
|
||||
addRecentModel(next.providerID, next.modelID);
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (dispatcher.consumeCapturedPrefixEvent(event)) return;
|
||||
if (dispatcher.dispatch(event)) event.preventDefault();
|
||||
};
|
||||
const handleBlur = () => dispatcher.handleBlur();
|
||||
|
||||
window.addEventListener('keydown', handleActivePrefixKeyDownCapture, true);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [openNewSessionDraft, shortcutOverrides]);
|
||||
window.addEventListener('blur', handleBlur);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleActivePrefixKeyDownCapture, true);
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
};
|
||||
}, [dispatcher]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user