diff --git a/src/components/agent-chat/AgentChatView.tsx b/src/components/agent-chat/AgentChatView.tsx index 926ee031..c1b36c3c 100644 --- a/src/components/agent-chat/AgentChatView.tsx +++ b/src/components/agent-chat/AgentChatView.tsx @@ -614,6 +614,7 @@ export default function AgentChatView({ tabId, paneId, paneContent, hidden }: Ag {/* Composer */} void } interface ChatComposerProps { + paneId?: string onSend: (text: string) => void onInterrupt: () => void disabled?: boolean @@ -15,10 +17,28 @@ interface ChatComposerProps { autoFocus?: boolean } -const ChatComposer = forwardRef(function ChatComposer({ onSend, onInterrupt, disabled, isRunning, placeholder, autoFocus }, ref) { - const [text, setText] = useState('') +const ChatComposer = forwardRef(function ChatComposer({ paneId, onSend, onInterrupt, disabled, isRunning, placeholder, autoFocus }, ref) { + const [text, setText] = useState(() => (paneId ? getDraft(paneId) : '')) const textareaRef = useRef(null) + const resizeTextarea = useCallback((el: HTMLTextAreaElement) => { + el.style.height = 'auto' + el.style.height = `${Math.min(el.scrollHeight, 200)}px` + }, []) + + // Resync text state and textarea height if paneId changes (component reused for a different pane) + const prevPaneIdRef = useRef(paneId) + useEffect(() => { + if (paneId !== prevPaneIdRef.current) { + prevPaneIdRef.current = paneId + setText(paneId ? getDraft(paneId) : '') + // Schedule resize after React paints the new text + requestAnimationFrame(() => { + if (textareaRef.current) resizeTextarea(textareaRef.current) + }) + } + }, [paneId, resizeTextarea]) + useImperativeHandle(ref, () => ({ focus: () => textareaRef.current?.focus(), }), []) @@ -40,16 +60,23 @@ const ChatComposer = forwardRef(function } }, []) + // Sync draft store on every text change + const handleTextChange = useCallback((value: string) => { + setText(value) + if (paneId) setDraft(paneId, value) + }, [paneId]) + const handleSend = useCallback(() => { const trimmed = text.trim() if (!trimmed) return onSend(trimmed) setText('') + if (paneId) clearDraft(paneId) // Reset height if (textareaRef.current) { textareaRef.current.style.height = 'auto' } - }, [text, onSend]) + }, [text, onSend, paneId]) const handleKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -63,10 +90,16 @@ const ChatComposer = forwardRef(function }, [handleSend, isRunning, onInterrupt]) const handleInput = useCallback(() => { - const el = textareaRef.current - if (!el) return - el.style.height = 'auto' - el.style.height = `${Math.min(el.scrollHeight, 200)}px` + if (textareaRef.current) resizeTextarea(textareaRef.current) + }, [resizeTextarea]) + + // Restore textarea height when mounting with a saved draft + useEffect(() => { + if (text && textareaRef.current) { + resizeTextarea(textareaRef.current) + } + // Only run on mount — text is intentionally excluded + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( @@ -75,7 +108,7 @@ const ChatComposer = forwardRef(function