diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx index 482f25c74..d0b386153 100644 --- a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx @@ -2,7 +2,7 @@ import { BotIcon, PlusSquare } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { Button } from "@/components/ui/button"; @@ -11,7 +11,11 @@ import { ArtifactTrigger } from "@/components/workspace/artifacts"; import { ChatBox, useThreadChat } from "@/components/workspace/chats"; import { ExportTrigger } from "@/components/workspace/export-trigger"; import { InputBox } from "@/components/workspace/input-box"; -import { MessageList } from "@/components/workspace/messages"; +import { + MessageList, + MESSAGE_LIST_DEFAULT_PADDING_BOTTOM, + MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM, +} from "@/components/workspace/messages"; import { ThreadContext } from "@/components/workspace/messages/context"; import { ThreadTitle } from "@/components/workspace/thread-title"; import { TodoList } from "@/components/workspace/todo-list"; @@ -28,6 +32,7 @@ import { cn } from "@/lib/utils"; export default function AgentChatPage() { const { t } = useI18n(); + const [showFollowups, setShowFollowups] = useState(false); const router = useRouter(); const { agent_name } = useParams<{ @@ -81,6 +86,11 @@ export default function AgentChatPage() { await thread.stop(); }, [thread]); + const messageListPaddingBottom = showFollowups + ? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM + + MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM + : undefined; + return ( @@ -128,6 +138,7 @@ export default function AgentChatPage() { className={cn("size-full", !isNewThread && "pt-10")} threadId={threadId} thread={thread} + paddingBottom={messageListPaddingBottom} /> @@ -173,6 +184,7 @@ export default function AgentChatPage() { } disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"} onContextChange={(context) => setSettings("context", context)} + onFollowupsVisibilityChange={setShowFollowups} onSubmit={handleSubmit} onStop={handleStop} /> diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 0cff87d0d..3bcafcf5d 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { type PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { ArtifactTrigger } from "@/components/workspace/artifacts"; @@ -11,7 +11,11 @@ import { } from "@/components/workspace/chats"; import { ExportTrigger } from "@/components/workspace/export-trigger"; import { InputBox } from "@/components/workspace/input-box"; -import { MessageList } from "@/components/workspace/messages"; +import { + MessageList, + MESSAGE_LIST_DEFAULT_PADDING_BOTTOM, + MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM, +} from "@/components/workspace/messages"; import { ThreadContext } from "@/components/workspace/messages/context"; import { ThreadTitle } from "@/components/workspace/thread-title"; import { TodoList } from "@/components/workspace/todo-list"; @@ -27,6 +31,7 @@ import { cn } from "@/lib/utils"; export default function ChatPage() { const { t } = useI18n(); + const [showFollowups, setShowFollowups] = useState(false); const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat(); const [settings, setSettings] = useThreadSettings(threadId); useSpecificChatMode(); @@ -70,6 +75,11 @@ export default function ChatPage() { await thread.stop(); }, [thread]); + const messageListPaddingBottom = showFollowups + ? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM + + MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM + : undefined; + return ( @@ -97,6 +107,7 @@ export default function ChatPage() { className={cn("size-full", !isNewThread && "pt-10")} threadId={threadId} thread={thread} + paddingBottom={messageListPaddingBottom} />
@@ -141,6 +152,7 @@ export default function ChatPage() { isUploading } onContextChange={(context) => setSettings("context", context)} + onFollowupsVisibilityChange={setShowFollowups} onSubmit={handleSubmit} onStop={handleStop} /> diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index d1682bb73..19e93b3f3 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -109,6 +109,7 @@ export function InputBox({ threadId, initialValue, onContextChange, + onFollowupsVisibilityChange, onSubmit, onStop, ...props @@ -136,6 +137,7 @@ export function InputBox({ reasoning_effort?: "minimal" | "low" | "medium" | "high"; }, ) => void; + onFollowupsVisibilityChange?: (visible: boolean) => void; onSubmit?: (message: PromptInputMessage) => void; onStop?: () => void; }) { @@ -186,6 +188,8 @@ export function InputBox({ return models.find((m) => m.name === context.model_name) ?? models[0]; }, [context.model_name, models]); + const resolvedModelName = selectedModel?.name; + const supportThinking = useMemo( () => selectedModel?.supports_thinking ?? false, [selectedModel], @@ -253,9 +257,33 @@ export function InputBox({ setFollowups([]); setFollowupsHidden(false); setFollowupsLoading(false); + + // Guard against submitting before the initial model auto-selection + // effect has flushed thread settings to storage/state. + if (resolvedModelName && context.model_name !== resolvedModelName) { + onContextChange?.({ + ...context, + model_name: resolvedModelName, + mode: getResolvedMode( + context.mode, + selectedModel?.supports_thinking ?? false, + ), + }); + setTimeout(() => onSubmit?.(message), 0); + return; + } + onSubmit?.(message); }, - [onSubmit, onStop, status], + [ + context, + onContextChange, + onSubmit, + onStop, + resolvedModelName, + selectedModel?.supports_thinking, + status, + ], ); const requestFormSubmit = useCallback(() => { @@ -309,6 +337,26 @@ export function InputBox({ setTimeout(() => requestFormSubmit(), 0); }, [pendingSuggestion, requestFormSubmit, textInput]); + const showFollowups = + !disabled && + !isNewThread && + !followupsHidden && + (followupsLoading || followups.length > 0); + + const followupsVisibilityChangeRef = useRef(onFollowupsVisibilityChange); + + useEffect(() => { + followupsVisibilityChangeRef.current = onFollowupsVisibilityChange; + }, [onFollowupsVisibilityChange]); + + useEffect(() => { + followupsVisibilityChangeRef.current?.(showFollowups); + }, [showFollowups]); + + useEffect(() => { + return () => followupsVisibilityChangeRef.current?.(false); + }, []); + useEffect(() => { const streaming = status === "streaming"; const wasStreaming = wasStreamingRef.current; @@ -769,40 +817,37 @@ export function InputBox({ )} - {!disabled && - !isNewThread && - !followupsHidden && - (followupsLoading || followups.length > 0) && ( -
-
- {followupsLoading ? ( -
- {t.inputBox.followupLoading} -
- ) : ( - - {followups.map((s) => ( - handleFollowupClick(s)} - /> - ))} - - - )} -
+ {showFollowups && ( +
+
+ {followupsLoading ? ( +
+ {t.inputBox.followupLoading} +
+ ) : ( + + {followups.map((s) => ( + handleFollowupClick(s)} + /> + ))} + + + )}
- )} +
+ )} diff --git a/frontend/src/components/workspace/messages/message-list.tsx b/frontend/src/components/workspace/messages/message-list.tsx index 8d5e0f6b9..354a27f04 100644 --- a/frontend/src/components/workspace/messages/message-list.tsx +++ b/frontend/src/components/workspace/messages/message-list.tsx @@ -29,11 +29,14 @@ import { MessageListItem } from "./message-list-item"; import { MessageListSkeleton } from "./skeleton"; import { SubtaskCard } from "./subtask-card"; +export const MESSAGE_LIST_DEFAULT_PADDING_BOTTOM = 160; +export const MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM = 80; + export function MessageList({ className, threadId, thread, - paddingBottom = 160, + paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM, }: { className?: string; threadId: string;