Cloud sandbox setup gets its own progress card

Shown while a cloud task starts in the VS Code extension
Starting a cloud task in the VS Code extension now shows a progress card while the sandbox comes up. It names the repo and branch, and ticks off each setup step as the sandbox reports it. Underneath, it's one new chat message type.
The old line looked like an assistant reply
The old setup line looked like an assistant reply

“It looked like an assistant reply, and the shimmer was misleading because nothing was thinking yet.”
Here's what it replaced. Starting a cloud task used to post a plain chat line, with the Thinking shimmer under it. The author's point was that this read like an assistant reply, and nothing was thinking yet. The sandbox takes about half a minute to start, so that state stayed on screen a while.
One row, rewritten as each phase lands
One chat row, rewritten in place as each phase lands
The card is one chat row that the coordinator keeps rewriting. It starts with the phase starting, so the first step spins. When the sandbox reports it's cloning the repo, the same row comes back with the first step checked. Then once more when the agent starts. Each update keeps the row's timestamp, so the webview replaces it in place.
Resume uses the card, then it folds to a header
Resuming reuses the card, and it folds to a header once work starts


Resuming a suspended sandbox uses the same card, with a single step, wake up sandbox. Once anything follows the row, the agent starting or an error, the card collapses to its header. The conversation keeps a compact marker with the repo and the branch.
A new say type carries a JSON payload
A new message type carries a JSON payload
Here's the path that message takes. The cloud session coordinator now emits a JSON payload with the phase, repo and branch, instead of prose. The shared message types gain a cloud provisioning say value. The proto enum gets value 39, with both conversion entries. And ChatRow hands the message to the new row component.
The coordinator re-emits the row per phase
The coordinator emits the same row again for each phase
sdk-cloud-session-coordinator.ts
function cloudProvisioningRow(ts: number, phase: …, repoContext: …): ClineMessage {
const info: ClineCloudProvisioningInfo = {
phase,
repo: repoContext.repoUrl?.replace(/^https:\/\/github\.com\//, ""),
branch: repoContext.branch,
}
return { ts, type: "say", say: "cloud_provisioning", text: JSON.stringify(info), partial: false }
}
if (phase === shownPhase || (phase !== "cloning_repo" && phase !== "agent_starting") || isStale()) return
shownPhase = phase
// Same ts: the progress row is updated in place, not appended.
this.options.messages.appendAndEmit([provisioningRow(phase)], provisioningEvent)
A small helper builds the row. It strips the GitHub prefix from the repo URL and packs the phase, repo and branch together. It returns a say row of the new type, with that as JSON text. When the sandbox reports a phase, the callback skips a repeat, and any phase other than cloning or agent start, then emits the row with the same timestamp.
The row picks its steps from the phase
The row takes its steps from the phase
components/cloud/CloudProvisioningRow.tsx
const START_STEPS: Step[] = [
{ phase: "starting", label: "Start sandbox" },
{ phase: "cloning_repo", label: "Clone repository" },
{ phase: "agent_starting", label: "Start agent" },
]
const RESUME_STEPS: Step[] = [{ phase: "resuming", label: "Wake up sandbox" }]
const steps = info.phase === "resuming" ? RESUME_STEPS : START_STEPS
const current = steps.findIndex((step) => step.phase === info.phase)
components/chat/ChatRow.tsx
case "cloud_provisioning":
return <CloudProvisioningRow
active={lastModifiedMessage?.ts === message.ts}
message={message} />
The row renders a 1 px spacer. “Virtuoso cannot handle zero-height items.”
In the webview, the row parses that payload and picks a step list. Starting a sandbox gets three steps, and resuming gets one. The current phase decides which step spins. ChatRow marks the row active only while it's the last modified message, and an inactive row shows just its header. Unparseable text renders a one pixel spacer.
Review caught Thinking showing beside the card
Review caught “Thinking…” rendering next to the card
“During this handoff, forceShow is still true and the provisioning message has partial: false, so optimisticShow below overrides this false and renders ‘Thinking…’ alongside the progress card.”
useThinkingLoaderRow.ts, after the fix
+ const cloudProvisioning = lastRawMessage?.type === "say" && lastRawMessage.say === "cloud_provisioning"
return useDebouncedLoaderVisibility(
- optimisticShow || isWaitingForResponse || handoffToReasoningPending,
+ !cloudProvisioning && (optimisticShow || isWaitingForResponse || handoffToReasoningPending),
One review comment reshaped the fix. The reviewer noticed that on a new cloud send, the optimistic loader is still forced on until the full state arrives through a 50 millisecond debounce. So Thinking would render right next to the card. The author moved the provisioning check onto the combined condition, and kept the hook call unconditional.
A full-hook test pins the loader hidden
A full-hook test keeps the loader hidden while the sandbox provisions
useThinkingLoaderRow.test.tsx · “stays hidden while a cloud sandbox provisions, even before the optimistic send is acknowledged”
const provisioning = say(1, "cloud_provisioning", false, JSON.stringify({ phase: "starting" }))
const idle: TurnState = { phase: "idle", seq: 1 }
const { result, rerender } = renderLoader({ ...inputsFor([provisioning], idle), forceShow: true })
expect(result.current).toBe(false)
rerender({ ...inputsFor([provisioning, say(2, "api_req_started", false)], idle), forceShow: true })
expect(result.current).toBe(true)
expected true to be false
The coordinator test expects three JSON phases on one timestamp
The new test runs the whole hook. With a provisioning row as the tail, a stale idle turn state, and the optimistic flag forced on, the loader stays hidden. Once an API request starts, it comes back. Against the code before the fix, it fails with expected true to be false.
A live run, measured from send
In a live run, the card collapsed at about 35 seconds
Cloud task on gitscription/test in a VS Code dev host, measured from send. Every card state also rendered in Storybook, dark and light themes.
Then a live run. On a real cloud task, measured from send, the card sat about 30 seconds on start sandbox, and about 5 seconds on start agent. It collapsed at about 35 seconds, and the first agent output arrived at about a minute. Every card state was also rendered in Storybook, in dark and light themes.
Where to look next
Checked in Storybook and in one live run, signed in through a local auth shim that isn't committed.
Cloud sandbox setup gets its own progress card
Starting a cloud sandbox for gitscription/test (main)…

ClineCloudProvisioningInfo["phase"]
START_STEPS in CloudProvisioningRow
the coordinator's phase filter
One limit. The end-to-end check is a single live run, signed in through a local auth shim that isn't committed. It replaces the prose line that read like a reply. If you add a sandbox phase, three places change together: the phase type, the step list in the row, and the coordinator's phase filter. Cloud sandbox setup now has its own progress card.











