feat: show sandbox setup status (#1455)

* feat: show sandbox setup status

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: avoid sandbox setup status for queued follow-ups

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-08 14:35:21 -07:00 • committed by GitHub
parent 9350361f0d
commit b0cfa2cfad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 29 additions and 6 deletions

View file

@ -120,6 +120,8 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
const hasMessages = displayMessages.length > 0 const hasMessages = displayMessages.length > 0
const hasActiveRun = thread.status === "running" const hasActiveRun = thread.status === "running"
const isStreaming = hasActiveRun || pendingPrompts.length > 0 const isStreaming = hasActiveRun || pendingPrompts.length > 0
const settingUpSandbox =
hasActiveRun && thread.messages.length === 0 && pendingPrompts.length > 0
return ( return (
<div className="flex min-w-0 flex-1 flex-col"> <div className="flex min-w-0 flex-1 flex-col">
@ -129,6 +131,7 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
<MessageView <MessageView
messages={displayMessages} messages={displayMessages}
isStreaming={isStreaming} isStreaming={isStreaming}
settingUpSandbox={settingUpSandbox}
contentWidthClass="max-w-3xl" contentWidthClass="max-w-3xl"
/> />
<div className="shrink-0 px-4 pb-4"> <div className="shrink-0 px-4 pb-4">

View file

@ -297,6 +297,7 @@ export type MessageViewScrollControl = {
interface MessageViewProps extends ApprovalCallbacks { interface MessageViewProps extends ApprovalCallbacks {
messages: Message[]; messages: Message[];
isStreaming: boolean; isStreaming: boolean;
settingUpSandbox?: boolean;
project?: Project | null; project?: Project | null;
contentWidthClass?: string; contentWidthClass?: string;
/** Horizontal padding on centered content (scroll track stays edge-to-edge). */ /** Horizontal padding on centered content (scroll track stays edge-to-edge). */
@ -654,12 +655,19 @@ const MessageBubble = memo(function MessageBubble({
); );
}); });
function ThinkingSpinner({ isStreaming }: { isStreaming: boolean }) { function ThinkingSpinner({
isStreaming,
settingUpSandbox,
}: {
isStreaming: boolean;
settingUpSandbox: boolean;
}) {
const [textIdx, setTextIdx] = useState(0); const [textIdx, setTextIdx] = useState(0);
const [done, setDone] = useState<{ past: string; elapsed: string } | null>(null); const [done, setDone] = useState<{ past: string; elapsed: string } | null>(null);
const startTimeRef = useRef(0); const startTimeRef = useRef(0);
const textIdxRef = useRef(textIdx); const textIdxRef = useRef(textIdx);
const wasStreamingRef = useRef(false); const wasStreamingRef = useRef(false);
const settingUpSandboxRef = useRef(settingUpSandbox);
textIdxRef.current = textIdx; textIdxRef.current = textIdx;
useEffect(() => { useEffect(() => {
@ -670,18 +678,24 @@ function ThinkingSpinner({ isStreaming }: { isStreaming: boolean }) {
setDone(null); setDone(null);
} else if (wasStreamingRef.current) { } else if (wasStreamingRef.current) {
setDone({ setDone({
past: BUSY_TEXTS[textIdxRef.current].past, past: settingUpSandboxRef.current
? "Set up sandbox"
: BUSY_TEXTS[textIdxRef.current].past,
elapsed: formatElapsed(Date.now() - startTimeRef.current), elapsed: formatElapsed(Date.now() - startTimeRef.current),
}); });
} }
}, [isStreaming]); }, [isStreaming]);
useEffect(() => { useEffect(() => {
if (!isStreaming) return; settingUpSandboxRef.current = settingUpSandbox;
}, [settingUpSandbox]);
useEffect(() => {
if (!isStreaming || settingUpSandbox) return;
const BUSY_TEXT_ROTATE_INTERVAL_MS = 12000; const BUSY_TEXT_ROTATE_INTERVAL_MS = 12000;
const id = setInterval(() => setTextIdx((i) => (i + 1) % BUSY_TEXTS.length), BUSY_TEXT_ROTATE_INTERVAL_MS); const id = setInterval(() => setTextIdx((i) => (i + 1) % BUSY_TEXTS.length), BUSY_TEXT_ROTATE_INTERVAL_MS);
return () => clearInterval(id); return () => clearInterval(id);
}, [isStreaming]); }, [isStreaming, settingUpSandbox]);
if (!isStreaming && !done) return null; if (!isStreaming && !done) return null;
@ -696,7 +710,9 @@ function ThinkingSpinner({ isStreaming }: { isStreaming: boolean }) {
return ( return (
<div className="my-2 flex items-center gap-2"> <div className="my-2 flex items-center gap-2">
<span className="shimmer-text text-xs">{BUSY_TEXTS[textIdx].present}</span> <span className="shimmer-text text-xs">
{settingUpSandbox ? "Setting up sandbox..." : BUSY_TEXTS[textIdx].present}
</span>
</div> </div>
); );
} }
@ -706,6 +722,7 @@ const BOTTOM_LOCK_THRESHOLD_PX = 24;
export const MessageView = memo(function MessageView({ export const MessageView = memo(function MessageView({
messages, messages,
isStreaming, isStreaming,
settingUpSandbox = false,
project, project,
contentWidthClass = "max-w-[42rem]", contentWidthClass = "max-w-[42rem]",
contentPaddingClass = "px-6", contentPaddingClass = "px-6",
@ -885,7 +902,10 @@ export const MessageView = memo(function MessageView({
onOpenDiff={onOpenDiff} onOpenDiff={onOpenDiff}
/> />
))} ))}
<ThinkingSpinner isStreaming={isStreaming} /> <ThinkingSpinner
isStreaming={isStreaming}
settingUpSandbox={settingUpSandbox}
/>
</div> </div>
</div> </div>