Skip to content

feat(app): show a sandbox-loading state on Work instead of an empty session list - #299

Merged
quickbeard merged 5 commits into
mainfrom
work-loading-state
Aug 22, 2026
Merged

quickbeard merged 5 commits into
mainfrom
work-loading-state

Conversation

@quickbeard

@quickbeard quickbeard commented Aug 22, 2026 •

Copy link
Copy Markdown
Owner

What

While a sandbox is coming up, the Work session pane showed a search box over "Nothing here yet / Create a session to get started" — the only action offered could not work, since the server that would run the session did not exist yet, and the search had nothing to search.

It now shows a spinner over "Loading sandbox…", centred in the pane, with the search box hidden, until the sandbox is ready.

The sidebar is deliberately untouched — it keeps saying "Your sandbox is not running yet. It appears here once it is ready."

How

packages/app/src/pages/home.tsx + a new i18n key (upstream files, one row in DIVERGENCES.md):

  • NewHome gains useWorkspaceSide() + useSandboxes() and a waitingForSandbox() memo, which drives two independent Shows: the session list's empty fallback, and whether HomeSessionSearch renders at all.
  • HomeSandboxLoading is a fork-only view — Spinner at size-8 over the workspace.loading.sandbox label, centred. It is not HomeSessionSkeleton: that skeleton stands in for rows about to arrive, which is true of a server's session load and false here, where nothing arrives for minutes. HomeSessionSkeleton and the Code-side branch that uses it are untouched.
  • It carries its wrapper's own min-h-[calc(100cqh-72px)] lg:min-h-[calc(100cqh-96px)] rather than min-h-full — the wrapper has a min-height and an auto height, so a percentage resolves against the content and centres nothing. (Found by rendering it, not by reading it.)

The predicate lives in a new fork-only packages/app/src/workspace/waiting.ts — a boolean over the sandbox list: any sandbox still booting, or any job in flight (a sandbox mid-create is not in the list yet, and one mid-reset reads "stopped" the whole time, so the job list is the only thing that says either wait is happening). side.ts routes its own "starting" branch through the same predicate so the sidebar and the session pane cannot drift apart; its behaviour is unchanged.

waiting.test.ts covers it: boots, jobs whatever the sandboxes say, and the three states that are not waits — Code, stopped, failed. It is its own module because side.ts imports the tabs context, which pulls @solidjs/router into any importer and cannot be loaded from a unit test.

Verification

  • bun run test:unit in packages/app: 695 pass / 0 fail (5 new).
  • bun turbo typecheck: 33/33. bun run build: clean. bun run lint: byte-identical totals with and without the change.
  • Rendered and looked at: the built app CSS driving the real markup — including the container-query height chain the centring depends on — in headless Chrome. That is what caught the min-h-full bug and, before it, a spinner so dim it was invisible.

Rebased onto current main.

@quickbeard quickbeard changed the title feat(app): show what the Work side is waiting on while a sandbox starts feat(app): show Code's loading state on Work while a sandbox starts Aug 22, 2026
Quickbeard and others added 4 commits August 22, 2026 18:02
Work answered a wait that can run for minutes with one static sentence —
"Your sandbox is not running yet. It appears here once it is ready." — so a
download, an unpack, a boot and a reset all looked identical, and identical
to a side that was simply empty.

Both halves of the screen now show the loading treatment the rest of the app
uses. The sidebar's empty state gets placeholder rows sized to HomeProjectRow
under its status line; the session list swaps its "Nothing here yet / create a
session" fallback for the same HomeSessionSkeleton it already shows while a
server's sessions load. Code is untouched: the wait is undefined there, so
upstream's empty state still renders.

The status line says what is happening rather than that something is:
sandboxCreatePhase (the Add sandbox dialog's own phase logic, reused so the
two can never disagree) splits the image download from the unpack behind it,
so it counts MB while bytes arrive and then names the job. A destroy keeps its
line but loses the placeholders — nothing arrives at the end of it — and the
offer to add a sandbox is hidden while one is already being created, since
that sandbox is not in the list until it finishes.

The derivation moves to a fork-only workspace/empty-state.ts: side.ts imports
the tabs context, which pulls in the router and cannot be loaded from a unit
test, and this logic is worth pinning down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scope back to the one place the wait is misleading: the session list, which
offered "create a session to get started" while the sandbox that would run it
was still coming up. It now renders the page's own HomeSessionSkeleton with
the same common.loading label the branch above it uses, so Work's wait and a
Code server's session load are the same element and the same words.

The sidebar keeps its original message and gains nothing, so side.ts and both
i18n dictionaries are back to what they were — side.ts only routes its
starting branch through the shared predicate. What is left of the derivation
is workspace/waiting.ts: a boolean, since no caller needs the job detail now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The state rendered, but nothing in it moved: the label sat alone and the four
placeholder rows are a flat fill barely above the background, so a Work side
waiting minutes on a sandbox read as a screen that had given up.

HomeSessionSkeleton now leads its label with the Spinner this page already
imports, and its rows pulse the way the sidebar's own SessionSkeleton does.
The spinner is one step brighter than the muted label beside it — at the
label's own colour it disappears into the background, which is what the first
attempt did. Both changes are in the shared component, so the Code-side
loading state this was asked to match keeps matching it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three changes to the waiting state, all Work-only:

- The search box is hidden while the sandbox comes up. It sat above an empty
  pane over sessions that do not exist yet, so it could only ever return
  nothing.
- The wait gets its own view instead of borrowing HomeSessionSkeleton: a
  size-8 spinner over its label, centred in the pane. The skeleton stands in
  for rows about to arrive, which is true of a server's session load and false
  here, where nothing arrives for minutes — and the centred view leaves
  nothing else on screen to read as available.
- The label is "Loading sandbox..." (new workspace.loading.sandbox key), not
  the bare "Loading", which said nothing about what was being waited on.

HomeSessionSkeleton goes back to upstream: the spinner and pulse added to it
last commit were serving the Work case, and Work no longer uses it.

The centred view carries its wrapper's own container-query height rather than
min-h-full — the wrapper has a min-height and an auto height, so a percentage
resolves against the content and centres nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard quickbeard changed the title feat(app): show Code's loading state on Work while a sandbox starts feat(app): show a sandbox-loading state on Work instead of an empty session list Aug 22, 2026
@quickbeard
quickbeard merged commit 7207736 into main Aug 22, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the work-loading-state branch August 22, 2026 11:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant