diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index e628f6ea6..0f36f5ec7 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -142,6 +142,7 @@ import { type TerminalContextSelection, } from "../lib/terminalContext"; import { shouldUseCompactComposerFooter } from "./composerFooterLayout"; +import { useMediaQuery } from "../hooks/useMediaQuery"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; @@ -336,6 +337,9 @@ export default function ChatView({ threadId }: ChatViewProps) { const [expandedWorkGroups, setExpandedWorkGroups] = useState>({}); const [planSidebarOpen, setPlanSidebarOpen] = useState(false); const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false); + const [isComposerFocused, setIsComposerFocused] = useState(false); + const isMobileViewport = useMediaQuery("max-sm"); + const isComposerCollapsedMobile = isMobileViewport && !isComposerFocused; // Tracks whether the user explicitly dismissed the sidebar for the active turn. const planSidebarDismissedForTurnRef = useRef(null); // When set, the thread-change reset effect will open the sidebar instead of closing it. @@ -375,6 +379,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const pendingInteractionAnchorFrameRef = useRef(null); const composerEditorRef = useRef(null); const composerFormRef = useRef(null); + const composerSurfaceRef = useRef(null); const composerFormHeightRef = useRef(0); const composerImagesRef = useRef([]); const composerSelectLockRef = useRef(false); @@ -384,6 +389,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const attachmentPreviewHandoffByMessageIdRef = useRef>({}); const attachmentPreviewHandoffTimeoutByMessageIdRef = useRef>({}); const sendInFlightRef = useRef(false); + const composerBlurFrameRef = useRef(null); const dragDepthRef = useRef(0); const terminalOpenByThreadRef = useRef>({}); const setMessagesScrollContainerRef = useCallback((element: HTMLDivElement | null) => { @@ -1193,6 +1199,24 @@ export default function ChatView({ threadId }: ChatViewProps) { focusComposer(); }); }, [focusComposer]); + const scheduleComposerCollapseCheck = useCallback(() => { + if (composerBlurFrameRef.current !== null) { + window.cancelAnimationFrame(composerBlurFrameRef.current); + } + composerBlurFrameRef.current = window.requestAnimationFrame(() => { + composerBlurFrameRef.current = null; + const composerSurface = composerSurfaceRef.current; + const activeElement = document.activeElement; + if ( + composerSurface && + activeElement instanceof Node && + composerSurface.contains(activeElement) + ) { + return; + } + setIsComposerFocused(false); + }); + }, []); const addTerminalContextToDraft = useCallback( (selection: TerminalContextSelection) => { if (!activeThread) { @@ -1854,6 +1878,14 @@ export default function ChatView({ threadId }: ChatViewProps) { ); }, [composerMenuItems, composerMenuOpen]); + useEffect(() => { + return () => { + if (composerBlurFrameRef.current !== null) { + window.cancelAnimationFrame(composerBlurFrameRef.current); + } + }; + }, []); + useEffect(() => { setIsRevertingCheckpoint(false); }, [activeThread?.id]); @@ -3581,42 +3613,114 @@ export default function ChatView({ threadId }: ChatViewProps) { onDrop={onComposerDrop} >
{ + if (composerBlurFrameRef.current !== null) { + window.cancelAnimationFrame(composerBlurFrameRef.current); + composerBlurFrameRef.current = null; + } + setIsComposerFocused(true); + }} + onBlurCapture={() => { + scheduleComposerCollapseCheck(); + }} + onClick={() => { + if (isComposerCollapsedMobile) { + // First expand the composer, then focus the editor after it renders + setIsComposerFocused(true); + requestAnimationFrame(() => { + composerEditorRef.current?.focusAtEnd(); + }); + } + }} > - {activePendingApproval ? ( -
- -
- ) : pendingUserInputs.length > 0 ? ( -
- -
- ) : showPlanFollowUpPrompt && activeProposedPlan ? ( -
- + {!isComposerCollapsedMobile && + (activePendingApproval ? ( +
+ +
+ ) : pendingUserInputs.length > 0 ? ( +
+ +
+ ) : showPlanFollowUpPrompt && activeProposedPlan ? ( +
+ +
+ ) : null)} + {isComposerCollapsedMobile && ( +
+ + {activePendingProgress + ? activePendingProgress.customAnswer || + "Type your own answer, or leave this blank to use the selected option" + : prompt.trim() || "Ask anything..."} + +
- ) : null} + )}
{composerMenuOpen && !isComposerApprovalState && ( @@ -3633,7 +3737,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
)} - {!isComposerApprovalState && + {!isComposerCollapsedMobile && + !isComposerApprovalState && pendingUserInputs.length === 0 && composerImages.length > 0 && (
@@ -3738,7 +3843,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
{/* Bottom toolbar */} - {activePendingApproval ? ( + {isComposerCollapsedMobile ? null : activePendingApproval ? (
e.preventDefault()} disabled={ activePendingIsResponding || (activePendingProgress.isLastQuestion @@ -3954,6 +4060,7 @@ export default function ChatView({ threadId }: ChatViewProps) { type="submit" size="sm" className="h-9 rounded-full px-4 sm:h-8" + onPointerDown={(e) => e.preventDefault()} disabled={isSendBusy || isConnecting} > {isConnecting || isSendBusy ? "Sending..." : "Refine"} @@ -3964,6 +4071,7 @@ export default function ChatView({ threadId }: ChatViewProps) { type="submit" size="sm" className="h-9 rounded-l-full rounded-r-none px-4 sm:h-8" + onPointerDown={(e) => e.preventDefault()} disabled={isSendBusy || isConnecting} > {isConnecting || isSendBusy ? "Sending..." : "Implement"} @@ -3997,6 +4105,7 @@ export default function ChatView({ threadId }: ChatViewProps) {