diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 52085daa459321a900664bd6cb7c591a48355695..6c8b8b74ebd8c6d1916f8d1653ce51dbcf3360b2 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,19 @@ export default function ChatView({ threadId }: ChatViewProps) { const toggleInteractionMode = useCallback(() => { handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan"); }, [handleInteractionModeChange, interactionMode]); + 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: { @@ -1890,12 +1906,22 @@ export default function ChatView({ threadId }: ChatViewProps) { if (!composerForm) return; composerFormHeightRef.current = composerForm.getBoundingClientRect().height; + setIsComposerFooterCompact( + shouldUseCompactComposerFooter(composerForm.clientWidth, { + hasWideActions: composerFooterHasWideActions, + }), + ); if (typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver((entries) => { const [entry] = entries; if (!entry) return; + const nextCompact = shouldUseCompactComposerFooter(entry.contentRect.width, { + hasWideActions: composerFooterHasWideActions, + }); + setIsComposerFooterCompact((previous) => (previous === nextCompact ? previous : nextCompact)); + const nextHeight = entry.contentRect.height; const previousHeight = composerFormHeightRef.current; composerFormHeightRef.current = nextHeight; @@ -1909,7 +1935,7 @@ export default function ChatView({ threadId }: ChatViewProps) { return () => { observer.disconnect(); }; - }, [activeThread?.id, scheduleStickToBottom]); + }, [activeThread?.id, composerFooterHasWideActions, scheduleStickToBottom]); useEffect(() => { if (!shouldAutoScrollRef.current) return; scheduleStickToBottom(); @@ -3679,10 +3705,23 @@ export default function ChatView({ threadId }: ChatViewProps) { /> ) : ( -
-
+
+
{/* Provider/model picker */} - {selectedProvider === "codex" && selectedEffort != null ? ( + {isComposerFooterCompact ? ( + + void handleRuntimeModeChange( + runtimeMode === "full-access" ? "approval-required" : "full-access", + ) + } + /> + ) : ( <> - - - - ) : null} - - {/* Divider */} - - - {/* Interaction mode toggle */} - + {selectedProvider === "codex" && selectedEffort != null ? ( + <> + + + + ) : null} - {/* Divider */} - + - {/* Runtime mode toggle */} - + - {/* Plan sidebar toggle */} - {(activePlan || activeProposedPlan || planSidebarOpen) ? ( - <> + + + {(activePlan || activeProposedPlan || planSidebarOpen) ? ( + <> + + + + ) : null} - ) : null} + )}
{/* Right side: send / stop button */} @@ -5534,6 +5578,8 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: { model: ModelSlug; lockedProvider: ProviderKind | null; modelOptionsByProvider: Record>; + serviceTierSetting: AppServiceTier; + compact?: boolean; disabled?: boolean; onProviderModelChange: (provider: ProviderKind, model: ModelSlug) => void; }) { @@ -5559,12 +5605,20 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: {