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

View file

@ -297,6 +297,7 @@ export type MessageViewScrollControl = {
interface MessageViewProps extends ApprovalCallbacks {
messages: Message[];
isStreaming: boolean;
settingUpSandbox?: boolean;
project?: Project | null;
contentWidthClass?: string;
/** 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 [done, setDone] = useState<{ past: string; elapsed: string } | null>(null);
const startTimeRef = useRef(0);
const textIdxRef = useRef(textIdx);
const wasStreamingRef = useRef(false);
const settingUpSandboxRef = useRef(settingUpSandbox);
textIdxRef.current = textIdx;
useEffect(() => {
@ -670,18 +678,24 @@ function ThinkingSpinner({ isStreaming }: { isStreaming: boolean }) {
setDone(null);
} else if (wasStreamingRef.current) {
setDone({
past: BUSY_TEXTS[textIdxRef.current].past,
past: settingUpSandboxRef.current
? "Set up sandbox"
: BUSY_TEXTS[textIdxRef.current].past,
elapsed: formatElapsed(Date.now() - startTimeRef.current),
});
}
}, [isStreaming]);
useEffect(() => {
if (!isStreaming) return;
settingUpSandboxRef.current = settingUpSandbox;
}, [settingUpSandbox]);
useEffect(() => {
if (!isStreaming || settingUpSandbox) return;
const BUSY_TEXT_ROTATE_INTERVAL_MS = 12000;
const id = setInterval(() => setTextIdx((i) => (i + 1) % BUSY_TEXTS.length), BUSY_TEXT_ROTATE_INTERVAL_MS);
return () => clearInterval(id);
}, [isStreaming]);
}, [isStreaming, settingUpSandbox]);
if (!isStreaming && !done) return null;
@ -696,7 +710,9 @@ function ThinkingSpinner({ isStreaming }: { isStreaming: boolean }) {
return (
<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>
);
}
@ -706,6 +722,7 @@ const BOTTOM_LOCK_THRESHOLD_PX = 24;
export const MessageView = memo(function MessageView({
messages,
isStreaming,
settingUpSandbox = false,
project,
contentWidthClass = "max-w-[42rem]",
contentPaddingClass = "px-6",
@ -885,7 +902,10 @@ export const MessageView = memo(function MessageView({
onOpenDiff={onOpenDiff}
/>
))}
<ThinkingSpinner isStreaming={isStreaming} />
<ThinkingSpinner
isStreaming={isStreaming}
settingUpSandbox={settingUpSandbox}
/>
</div>
</div>