diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 3362622b7e..5fb47b1a9a 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -214,6 +214,7 @@ import { useComposerDraftStore, useComposerThreadDraft, } from "../composerDraftStore"; +import { shouldUseCompactComposerFooter } from "./composerFooterLayout"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { clamp } from "effect/Number"; import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; @@ -639,6 +640,7 @@ export default function ChatView({ threadId }: ChatViewProps) { useState>({}); const [expandedWorkGroups, setExpandedWorkGroups] = useState>({}); const [planSidebarOpen, setPlanSidebarOpen] = useState(false); + const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false); // 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. @@ -935,6 +937,7 @@ export default function ChatView({ threadId }: ChatViewProps) { isComposerApprovalState || pendingUserInputs.length > 0 || (showPlanFollowUpPrompt && activeProposedPlan !== null); + const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null; useEffect(() => { if (!activePendingProgress) { return; @@ -1672,6 +1675,24 @@ export default function ChatView({ threadId }: ChatViewProps) { const toggleInteractionMode = useCallback(() => { handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan"); }, [handleInteractionModeChange, interactionMode]); + const toggleRuntimeMode = useCallback(() => { + void handleRuntimeModeChange( + runtimeMode === "full-access" ? "approval-required" : "full-access", + ); + }, [handleRuntimeModeChange, runtimeMode]); + const togglePlanSidebar = useCallback(() => { + setPlanSidebarOpen((open) => { + if (open) { + const turnKey = activePlan?.turnId ?? activeProposedPlan?.turnId ?? null; + if (turnKey) { + planSidebarDismissedForTurnRef.current = turnKey; + } + } else { + planSidebarDismissedForTurnRef.current = null; + } + return !open; + }); + }, [activePlan?.turnId, activeProposedPlan?.turnId]); const persistThreadSettingsForNextTurn = useCallback( async (input: { @@ -1888,14 +1909,25 @@ export default function ChatView({ threadId }: ChatViewProps) { useLayoutEffect(() => { const composerForm = composerFormRef.current; if (!composerForm) return; + const measureComposerFormWidth = () => composerForm.clientWidth; composerFormHeightRef.current = composerForm.getBoundingClientRect().height; + setIsComposerFooterCompact( + shouldUseCompactComposerFooter(measureComposerFormWidth(), { + hasWideActions: composerFooterHasWideActions, + }), + ); if (typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver((entries) => { const [entry] = entries; if (!entry) return; + const nextCompact = shouldUseCompactComposerFooter(measureComposerFormWidth(), { + hasWideActions: composerFooterHasWideActions, + }); + setIsComposerFooterCompact((previous) => (previous === nextCompact ? previous : nextCompact)); + const nextHeight = entry.contentRect.height; const previousHeight = composerFormHeightRef.current; composerFormHeightRef.current = nextHeight; @@ -1909,7 +1941,7 @@ export default function ChatView({ threadId }: ChatViewProps) { return () => { observer.disconnect(); }; - }, [activeThread?.id, scheduleStickToBottom]); + }, [activeThread?.id, composerFooterHasWideActions, scheduleStickToBottom]); useEffect(() => { if (!shouldAutoScrollRef.current) return; scheduleStickToBottom(); @@ -3468,7 +3500,7 @@ export default function ChatView({ threadId }: ChatViewProps) { onDismiss={() => setThreadError(activeThread.id, null)} /> {/* Main content area with optional plan sidebar */} -
+
{/* Chat column */}
@@ -3679,10 +3711,24 @@ export default function ChatView({ threadId }: ChatViewProps) { />
) : ( -
-
+
+
{/* Provider/model picker */} - {selectedProvider === "codex" && selectedEffort != null ? ( + {isComposerFooterCompact ? ( + + ) : ( <> + {selectedProvider === "codex" && selectedEffort != null ? ( + <> + + + + ) : null} + - - - ) : null} - {/* Divider */} - + - {/* Interaction mode toggle */} - - - {/* Divider */} - - - {/* Runtime mode toggle */} - - - {/* Plan sidebar toggle */} - {(activePlan || activeProposedPlan || planSidebarOpen) ? ( - <> + + + {(activePlan || activeProposedPlan || planSidebarOpen) ? ( + <> + + + + ) : null} - ) : null} + )}
{/* Right side: send / stop button */} -
+
{isPreparingWorktree ? ( Preparing worktree... ) : null} @@ -3953,6 +4000,15 @@ export default function ChatView({ threadId }: ChatViewProps) {
+ {isGitRepo && ( + + )} +
{/* end chat column */} {/* Plan sidebar */} @@ -3974,15 +4030,6 @@ export default function ChatView({ threadId }: ChatViewProps) { ) : null}
{/* end horizontal flex container */} - {isGitRepo && ( - - )} - {(() => { if (!terminalState.terminalOpen || !activeProject) { return null; @@ -5534,6 +5581,7 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: { model: ModelSlug; lockedProvider: ProviderKind | null; modelOptionsByProvider: Record>; + compact?: boolean; disabled?: boolean; onProviderModelChange: (provider: ProviderKind, model: ModelSlug) => void; }) { @@ -5559,12 +5607,20 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: {