diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index 458a8ed1..933e723e 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -53,6 +53,7 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ const [lastResponse, setLastResponse] = useState(null); const [currentStatus, setCurrentStatus] = useState<'thinking' | 'planning' | 'generating'>('thinking'); const statusIntervalRef = useRef(); + const [isLastInteracted, setIsLastInteracted] = useState(isInitialState); // Notify parent of message changes useEffect(() => { @@ -85,6 +86,7 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ content: prompt }]); setResponseError(null); + setIsLastInteracted(true); } const handleApplyChange = useCallback(( @@ -313,6 +315,8 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ loading={loadingResponse} disabled={loadingResponse} initialFocus={isInitialState} + shouldAutoFocus={isLastInteracted} + onFocus={() => setIsLastInteracted(true)} /> diff --git a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx index f58d6011..42c46dc2 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/components/chat.tsx @@ -50,6 +50,7 @@ export function Chat({ const [lastAgenticRequest, setLastAgenticRequest] = useState(null); const [lastAgenticResponse, setLastAgenticResponse] = useState(null); const [optimisticMessages, setOptimisticMessages] = useState[]>(chat.messages); + const [isLastInteracted, setIsLastInteracted] = useState(false); const getCopyContent = useCallback(() => { return JSON.stringify({ @@ -90,6 +91,7 @@ export function Chat({ }]; setMessages(updatedMessages); setFetchResponseError(null); + setIsLastInteracted(true); } // reset state when workflow changes @@ -293,6 +295,8 @@ export function Chat({ handleUserMessage={handleUserMessage} messages={messages.filter(msg => msg.content !== undefined) as any} loading={loadingAssistantResponse} + shouldAutoFocus={isLastInteracted} + onFocus={() => setIsLastInteracted(true)} /> ; diff --git a/apps/rowboat/components/common/compose-box-copilot.tsx b/apps/rowboat/components/common/compose-box-copilot.tsx index 42645812..a1988416 100644 --- a/apps/rowboat/components/common/compose-box-copilot.tsx +++ b/apps/rowboat/components/common/compose-box-copilot.tsx @@ -20,6 +20,8 @@ interface ComposeBoxCopilotProps { loading: boolean; disabled?: boolean; initialFocus?: boolean; + shouldAutoFocus?: boolean; + onFocus?: () => void; } export function ComposeBoxCopilot({ @@ -28,18 +30,29 @@ export function ComposeBoxCopilot({ loading, disabled = false, initialFocus = false, + shouldAutoFocus = false, + onFocus, }: ComposeBoxCopilotProps) { const [input, setInput] = useState(''); const [isFocused, setIsFocused] = useState(false); const textareaRef = useRef(null); + const previousMessagesLength = useRef(messages.length); - // Add effect to handle initial focus + // Handle initial focus useEffect(() => { if (initialFocus && textareaRef.current) { textareaRef.current.focus(); } }, [initialFocus]); + // Handle auto-focus when new messages arrive + useEffect(() => { + if (shouldAutoFocus && messages.length > previousMessagesLength.current && textareaRef.current) { + textareaRef.current.focus(); + } + previousMessagesLength.current = messages.length; + }, [messages.length, shouldAutoFocus]); + function handleInput() { const prompt = input.trim(); if (!prompt) { @@ -56,6 +69,11 @@ export function ComposeBoxCopilot({ } } + const handleFocus = () => { + setIsFocused(true); + onFocus?.(); + }; + return (
{/* Keyboard shortcut hint */} @@ -74,7 +92,7 @@ export function ComposeBoxCopilot({ value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleInputKeyDown} - onFocus={() => setIsFocused(true)} + onFocus={handleFocus} onBlur={() => setIsFocused(false)} disabled={disabled || loading} placeholder="Type a message..." diff --git a/apps/rowboat/components/common/compose-box-playground.tsx b/apps/rowboat/components/common/compose-box-playground.tsx index 7547e43a..c54e875b 100644 --- a/apps/rowboat/components/common/compose-box-playground.tsx +++ b/apps/rowboat/components/common/compose-box-playground.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { Textarea } from '@/components/ui/textarea'; import { Button, Spinner } from "@heroui/react"; @@ -7,6 +7,8 @@ interface ComposeBoxPlaygroundProps { messages: any[]; loading: boolean; disabled?: boolean; + shouldAutoFocus?: boolean; + onFocus?: () => void; } export function ComposeBoxPlayground({ @@ -14,10 +16,21 @@ export function ComposeBoxPlayground({ messages, loading, disabled = false, + shouldAutoFocus = false, + onFocus, }: ComposeBoxPlaygroundProps) { const [input, setInput] = useState(''); const [isFocused, setIsFocused] = useState(false); const textareaRef = useRef(null); + const previousMessagesLength = useRef(messages.length); + + // Handle auto-focus when new messages arrive + useEffect(() => { + if (shouldAutoFocus && messages.length > previousMessagesLength.current && textareaRef.current) { + textareaRef.current.focus(); + } + previousMessagesLength.current = messages.length; + }, [messages.length, shouldAutoFocus]); function handleInput() { const prompt = input.trim(); @@ -35,6 +48,11 @@ export function ComposeBoxPlayground({ } }; + const handleFocus = () => { + setIsFocused(true); + onFocus?.(); + }; + return (
{/* Keyboard shortcut hint */} @@ -53,7 +71,7 @@ export function ComposeBoxPlayground({ value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleInputKeyDown} - onFocus={() => setIsFocused(true)} + onFocus={handleFocus} onBlur={() => setIsFocused(false)} disabled={disabled || loading} placeholder="Type a message..."