130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
import React from 'react';
|
|
import { focusChatInput } from '@/components/chat/composer/editor/dom';
|
|
import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop';
|
|
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 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;
|
|
|
|
const nextVariantOverride = configState.cycleCurrentVariant();
|
|
const sessionId = useSessionUIStore.getState().currentSessionId;
|
|
const {
|
|
currentAgentName,
|
|
currentProviderId,
|
|
currentModelId,
|
|
} = useConfigStore.getState();
|
|
if (sessionId && currentAgentName && currentProviderId && currentModelId) {
|
|
useSelectionStore.getState().saveAgentModelVariantForSession(
|
|
sessionId,
|
|
currentAgentName,
|
|
currentProviderId,
|
|
currentModelId,
|
|
nextVariantOverride,
|
|
);
|
|
}
|
|
},
|
|
cycle_favorite_model_forward: () => cycleFavoriteModel(1),
|
|
cycle_favorite_model_backward: () => cycleFavoriteModel(-1),
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
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 (dispatcher.dispatchActivePrefix(event)) {
|
|
event.preventDefault();
|
|
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);
|
|
window.addEventListener('blur', handleBlur);
|
|
return () => {
|
|
window.removeEventListener('keydown', handleActivePrefixKeyDownCapture, true);
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
window.removeEventListener('blur', handleBlur);
|
|
};
|
|
}, [dispatcher]);
|
|
};
|