/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import { useCallback, useLayoutEffect, useRef } from 'react'; import type { TextBuffer } from '../../../components/shared/text-buffer.js'; import type { UseInputHistoryStoreReturn } from '../../../hooks/useInputHistoryStore.js'; import type { HistoryItemWithoutId } from '../../../types.js'; import { ToolCallStatus } from '../../../types.js'; import { isSlashCommand } from '../../../utils/commandUtils.js'; import type { AppAction } from '../../../reducers/appReducer.js'; export interface UseInputHandlingParams { buffer: TextBuffer; inputHistoryStore: Pick; submitQuery: (query: string) => Promise; pendingHistoryItems: HistoryItemWithoutId[]; lastSubmittedPromptRef: React.MutableRefObject; /** Whether the user needs to re-authenticate before continuing. */ needsRelogin: boolean; /** Dispatch app actions (e.g., open auth dialog). */ appDispatch: React.Dispatch; } export interface UseInputHandlingResult { handleUserCancel: (shouldRestorePrompt?: boolean) => void; handleFinalSubmit: (submittedValue: string) => void; cancelHandlerRef: React.MutableRefObject< ((shouldRestorePrompt?: boolean) => void) | null >; } /** * Checks if any tool is currently executing in pending history items. */ export function isToolExecuting( pendingHistoryItems: HistoryItemWithoutId[], ): boolean { return pendingHistoryItems.some((item) => { if (item.type === 'tool_group') { return item.tools.some( (tool) => tool.status === ToolCallStatus.Executing, ); } return false; }); } function restoreOrClearBuffer( buffer: TextBuffer, lastSubmittedPromptRef: React.MutableRefObject, shouldRestorePrompt?: boolean, ): void { if (shouldRestorePrompt === true) { const lastUserMessage = lastSubmittedPromptRef.current; if (lastUserMessage != null) { buffer.setText(lastUserMessage); } } else { buffer.setText(''); } } function captureDeferredPrompt( trimmedValue: string, inputHistoryStore: Pick, lastSubmittedPromptRef: React.MutableRefObject, ): void { if (lastSubmittedPromptRef.current !== trimmedValue) { inputHistoryStore.addInput(trimmedValue); } lastSubmittedPromptRef.current = trimmedValue; } function useFinalSubmitHandler({ inputHistoryStore, submitQuery, lastSubmittedPromptRef, needsRelogin, appDispatch, }: UseInputHandlingParams): (submittedValue: string) => void { return useCallback( (submittedValue: string) => { const trimmedValue = submittedValue.trim(); if (trimmedValue.length === 0) return; const isCommand = isSlashCommand(trimmedValue); if (!isCommand && needsRelogin) { appDispatch({ type: 'OPEN_DIALOG', payload: 'auth' }); captureDeferredPrompt( trimmedValue, inputHistoryStore, lastSubmittedPromptRef, ); return; } lastSubmittedPromptRef.current = trimmedValue; inputHistoryStore.addInput(trimmedValue); void submitQuery(trimmedValue); }, [ submitQuery, inputHistoryStore, lastSubmittedPromptRef, needsRelogin, appDispatch, ], ); } export function useInputHandling( params: UseInputHandlingParams, ): UseInputHandlingResult { const { buffer, pendingHistoryItems, lastSubmittedPromptRef } = params; const cancelHandlerRef = useRef< ((shouldRestorePrompt?: boolean) => void) | null >(null); const handleFinalSubmit = useFinalSubmitHandler(params); const handleUserCancel = useCallback( (shouldRestorePrompt?: boolean) => { restoreOrClearBuffer(buffer, lastSubmittedPromptRef, shouldRestorePrompt); }, [buffer, lastSubmittedPromptRef], ); const cancelHandler = useCallback( (shouldRestorePrompt?: boolean) => { if (isToolExecuting(pendingHistoryItems)) { buffer.setText(''); return; } restoreOrClearBuffer(buffer, lastSubmittedPromptRef, shouldRestorePrompt); }, [buffer, pendingHistoryItems, lastSubmittedPromptRef], ); useLayoutEffect(() => { cancelHandlerRef.current = cancelHandler; }, [cancelHandlerRef, cancelHandler]); return { handleUserCancel, handleFinalSubmit, cancelHandlerRef, }; }