"use client"; import { useCallback, useRef, useState, useEffect, useImperativeHandle } from "react"; import type React from "react"; import { useResizableInput } from "@/hooks/use-resizable-input"; import { useChatInputState } from "./use-chat-input-state"; import type { TipTapInputHandle } from "./tiptap-input"; import type { ContextItem } from "@/lib/types/context"; import type { ContextFile } from "@/lib/state/context-files-store"; import type { Message } from "@/lib/types/http"; import type { DiffComment } from "@/lib/diff/types"; import type { MessageAttachment, ChatInputContainerHandle } from "./chat-input-container"; type UseChatInputContainerParams = { ref: React.ForwardedRef; sessionId: string | null; isSending: boolean; isStarting: boolean; isMoving: boolean; isFailed: boolean; needsRecovery: boolean; executorUnavailable: boolean; isAgentBusy: boolean; hasAgentCommands: boolean; placeholder: string | undefined; contextItems: ContextItem[]; pendingClarification: Message | null | undefined; onClarificationResolved: (() => void) | undefined; pendingCommentsByFile: Record | undefined; hasContextComments: boolean; showRequestChangesTooltip: boolean; onRequestChangesTooltipDismiss: (() => void) | undefined; onSubmit: ( message: string, reviewComments?: DiffComment[], attachments?: MessageAttachment[], inlineMentions?: ContextFile[], ) => void; }; function useInputHandle( ref: React.ForwardedRef, inputRef: React.RefObject, ) { useImperativeHandle( ref, () => ({ focusInput: () => inputRef.current?.focus(), getTextareaElement: () => inputRef.current?.getTextareaElement() ?? null, getValue: () => inputRef.current?.getValue() ?? "", getSelectionStart: () => inputRef.current?.getSelectionStart() ?? 0, insertText: (text: string, from: number, to: number) => { inputRef.current?.insertText(text, from, to); }, clear: () => inputRef.current?.clear(), }), [inputRef], ); } function useSyncTipTapRef( tiptapRef: React.RefObject, inputRef: React.RefObject, ) { useEffect(() => { tiptapRef.current = inputRef.current; }); } function getInputPlaceholder( placeholder: string | undefined, isAgentBusy: boolean, hasAgentCommands: boolean, isStarting: boolean, ): string { if (isStarting) return "Preparing workspace..."; if (placeholder) return placeholder; if (isAgentBusy) return "Queue more instructions..."; if (hasAgentCommands) return "Ask to make changes, @mention files, run /commands"; return "Ask to make changes, @mention files"; } function computeDerivedState(params: { isStarting: boolean; isMoving: boolean; isSending: boolean; isFailed: boolean; needsRecovery: boolean; executorUnavailable: boolean; pendingClarification: Message | null | undefined; onClarificationResolved: (() => void) | undefined; pendingCommentsByFile: Record | undefined; allItemsLength: number; isInputFocused: boolean; placeholder: string | undefined; isAgentBusy: boolean; hasAgentCommands: boolean; }) { const isDisabled = params.isMoving || params.isSending || params.isFailed || params.needsRecovery || params.executorUnavailable; const submitDisabled = params.isStarting || isDisabled; const submitDisabledReason = params.isStarting ? "The agent is still being set up." : undefined; const hasClarification = !!(params.pendingClarification && params.onClarificationResolved); const hasPendingComments = !!( params.pendingCommentsByFile && Object.keys(params.pendingCommentsByFile).length > 0 ); const hasContextZone = params.allItemsLength > 0; const showFocusHint = !params.isInputFocused && !hasClarification && !hasPendingComments; const inputPlaceholder = getInputPlaceholder( params.placeholder, params.isAgentBusy, params.hasAgentCommands, params.isStarting, ); return { isDisabled, submitDisabled, submitDisabledReason, hasClarification, hasPendingComments, hasContextZone, showFocusHint, inputPlaceholder, }; } export function useChatInputContainer(params: UseChatInputContainerParams) { const { ref, sessionId, isSending, isStarting, isMoving, isFailed, needsRecovery, executorUnavailable, isAgentBusy, hasAgentCommands, placeholder, contextItems, pendingClarification, onClarificationResolved, pendingCommentsByFile, showRequestChangesTooltip, onRequestChangesTooltipDismiss, onSubmit, } = params; const [isInputFocused, setIsInputFocused] = useState(false); const [showNewSessionDialog, setShowNewSessionDialog] = useState(false); const [contextPopoverOpen, setContextPopoverOpen] = useState(false); const tiptapRef = useRef(null); const getContentElement = useCallback(() => tiptapRef.current?.getTextareaElement() ?? null, []); const { height, resetHeight, autoExpand, containerRef, resizeHandleProps } = useResizableInput( sessionId ?? undefined, getContentElement, ); const fileInputRef = useRef(null); const { value, inputRef, addFiles, handleChange, handleSubmit, allItems } = useChatInputState({ sessionId, isSending, contextItems, pendingCommentsByFile, hasContextComments: params.hasContextComments, showRequestChangesTooltip, onRequestChangesTooltipDismiss, onSubmit, }); useSyncTipTapRef(tiptapRef, inputRef); useInputHandle(ref, inputRef); // Auto-expand the input container as the user types more lines const handleChangeWithAutoExpand = useCallback( (val: string) => { handleChange(val); requestAnimationFrame(autoExpand); }, [handleChange, autoExpand], ); useEffect(() => { if (showRequestChangesTooltip && inputRef.current) inputRef.current.focus(); }, [showRequestChangesTooltip, inputRef]); const handleAgentCommand = useCallback( (commandName: string) => { onSubmit(`/${commandName}`); }, [onSubmit], ); const handleSubmitWithReset = useCallback( () => handleSubmit(resetHeight), [handleSubmit, resetHeight], ); const derived = computeDerivedState({ isStarting, isMoving, isSending, isFailed, needsRecovery, executorUnavailable, pendingClarification, onClarificationResolved, pendingCommentsByFile, allItemsLength: allItems.length, isInputFocused, placeholder, isAgentBusy, hasAgentCommands, }); return { isInputFocused, setIsInputFocused, showNewSessionDialog, setShowNewSessionDialog, contextPopoverOpen, setContextPopoverOpen, height, containerRef, resizeHandleProps, value, inputRef, addFiles, fileInputRef, handleChange: handleChangeWithAutoExpand, handleSubmitWithReset, handleAgentCommand, allItems, ...derived, }; } export type { TipTapInputHandle };