fix(craft): animate the welcome composer into the conversation (#15726)

This commit is contained in:
Wenxi authored and GitHub committed 2026-10-08 21:08:51 +00:00
1 parent 485c3f28ab
commit 9f489a1110
3 files changed
+54 -30

No files matched your search

@@ -51,6 +51,9 @@ over; the entire agentic loop runs inside the sandbox's `opencode serve`
process, and the backend's job is to start it, stream its events, persist
them, and know when to stop.
The composer moves from the welcome position to the conversation footer with
a shared layout animation. Reduced-motion users get an immediate transition.
The user can interrupt a running turn at any time. Partial output stays
visible and stays saved; sending a new message does not erase what was
already produced. When the agent is about to do something that needs
@@ -2,6 +2,7 @@
import { useRef, useState } from "react";
import { useTranslations } from "next-intl";
import { motion, useReducedMotion } from "motion/react";
import { BuildFile } from "@/app/craft/contexts/UploadFilesContext";
import { useVideoBackgroundToggleClick } from "@/app/craft/components/video-background/useVideoBackgroundToggleClick";
import Text from "@/refresh-components/texts/Text";
@@ -39,6 +40,7 @@ export default function BuildWelcome({
sandboxInitializing = false,
}: BuildWelcomeProps) {
const t = useTranslations("craft.welcome");
const reduceMotion: boolean | null = useReducedMotion();
const inputBarRef = useRef<CraftInputBarHandle>(null);
const [selectedModel, setSelectedModel] = useState<BuildLlmSelection | null>(
null
@@ -103,7 +105,11 @@ export default function BuildWelcome({
</div>
<div className="row-start-2 w-full flex flex-col items-center">
<div className="w-full max-w-(--app-page-main-content-width)">
<motion.div
layoutId="craft-composer"
transition={{ duration: reduceMotion ? 0 : 0.25, ease: "easeOut" }}
className="w-full max-w-(--app-page-main-content-width)"
>
<CraftInputBar
ref={inputBarRef}
onSubmit={(message, files) =>
@@ -114,7 +120,7 @@ export default function BuildWelcome({
sandboxInitializing={sandboxInitializing}
disabled={!hasAnyProvider}
/>
</div>
</motion.div>
</div>
<div className="row-start-3 min-h-0 w-full flex flex-col items-center">
+43 -28
View File
@@ -61,7 +61,12 @@ import { Button, Tooltip } from "@opal/components";
import { useBuildContext } from "@/app/craft/contexts/BuildContext";
import useScreenSize from "@/hooks/useScreenSize";
import { cn } from "@opal/utils";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
import {
motion,
AnimatePresence,
LayoutGroup,
useReducedMotion,
} from "motion/react";
interface BuildChatPanelProps {
/** Session ID from URL - used to prevent welcome flash while loading */
@@ -651,7 +656,7 @@ export default function BuildChatPanel({
]);
return (
<div className="h-full w-full">
<LayoutGroup id="craft-chat">
{/* Content wrapper - shrinks when output panel opens. Wrapped in a
dropzone so files can be dropped anywhere in the chat area. */}
{/* noPaste: the input bar already uploads pasted files itself. */}
@@ -834,37 +839,47 @@ export default function BuildChatPanel({
)}
{/* The composer stays in view for subagents (layout consistency)
but is disabled — replying to subagents is not supported. */}
<CraftInputBar
ref={inputBarRef}
onSubmit={handleSubmit}
isRunning={displayIsRunning}
isInterrupting={isInterrupting}
onInterrupt={
hasInterruptibleTurn && !scheduledRunInFlight
? handleInterrupt
: undefined
}
disabled={
isViewingSubagent || scheduledRunInFlight || !hasProvider
}
placeholder={
isViewingSubagent
? t("input.subagentPlaceholder")
: scheduledRunInFlight
? t("input.scheduledRunPlaceholder")
: t("input.continuePlaceholder")
}
queuedMessages={queuedMessages}
onQueueMessage={handleQueueMessage}
onRemoveQueuedMessage={handleRemoveQueuedMessage}
contextUsage={contextUsage}
/>
<motion.div
layoutId="craft-composer"
transition={{
duration: reduceMotion ? 0 : 0.25,
ease: "easeOut",
}}
>
<CraftInputBar
ref={inputBarRef}
onSubmit={handleSubmit}
isRunning={displayIsRunning}
isInterrupting={isInterrupting}
onInterrupt={
hasInterruptibleTurn && !scheduledRunInFlight
? handleInterrupt
: undefined
}
disabled={
isViewingSubagent ||
scheduledRunInFlight ||
!hasProvider
}
placeholder={
isViewingSubagent
? t("input.subagentPlaceholder")
: scheduledRunInFlight
? t("input.scheduledRunPlaceholder")
: t("input.continuePlaceholder")
}
queuedMessages={queuedMessages}
onQueueMessage={handleQueueMessage}
onRemoveQueuedMessage={handleRemoveQueuedMessage}
contextUsage={contextUsage}
/>
</motion.div>
</div>
</div>
)}
</div>
)}
</Dropzone>
</div>
</LayoutGroup>
);
}