feat(workers): new-workspace creation modal + async provisioning #86

Merged
lz merged 19 commits from feat/new-ws into main 2026-07-22 13:03:26 +02:00
Owner

Summary

Replaces the full-page /new workspace-creation flow with a centered modal (full-screen sheet on mobile), redesigned from scratch, and moves worker provisioning to an async, server-driven flow whose status shows on the workspace card.

  • Modal (NewWorkerModal.svelte): single column with purpose-named drawers — Worker image, Mounts (a reserved, disabled "soon" teaser), Advanced (name override). Opens from the workspaces list; closes on Escape / backdrop / ✕, with focus restored to the trigger.
  • Async provisioning: POST /api/workers inserts the row in a queued phase and returns 201 immediately (modal closes), then a background provisionWorker pulls the image → creates → starts, advancing a provision_phase (queued → pulling → creating → starting, terminal error) that the existing poll renders as coarse text on the card. Boot-time failStuckProvisioning fails rows orphaned by a restart.
  • Reserved seam for #85 (host/remote bind mounts): the Mounts drawer is where that feature lands in a follow-up — no behaviour here, just the home.
  • Cleanup: deletes the /new route, NewWorker.svelte, PullProgress.svelte, the client pull helper, and the pull SSE route (pull now runs server-side inside provisionWorker).
  • App-wide polish: themed scrollbars + dropdown selects (previously unstyled), mono dialog title.

Design + plan: docs/superpowers/specs/2026-07-21-new-workspace-modal-design.md, docs/superpowers/plans/2026-07-21-new-workspace-modal.md.

Correctness fixes from review

A multi-agent review caught (and this branch fixes):

  • Orphan container: clicking Remove during provisioning used to hard-delete the row while the background job created a container/volume with no DB row (invisible, blocking the name slot). provisionWorker now detects the removed-mid-flight race (via finishProvisioning's row count) and on any post-create failure, and tears the container down.
  • Failed/provisioning cards gate Start/Stop/Logs on container_id, so a worker with no container offers only Remove (those buttons previously threw).
  • A stalled image pull aborts after 120s of no progress → recorded card error instead of an eternal "Pulling…".
  • ProvisionPhase is single-sourced in api/types.ts so server/client can't drift silently.

Test plan

  • pnpm typecheck — clean.
  • pnpm test — 845 passing (migration 0014, provisioning helpers incl. every in-flight phase + finishProvisioning return-count, provisionLabel).
  • End-to-end against a real dind stack, driven by playwright-cli: modal create → card pulling/creating/starting → running on a real container; failure path → Failed: <reason> card with only Remove and no orphan container; running card shows the full action row; full-screen mobile sheet; Escape/backdrop/autofocus.

Notes

  • Deferred (follow-ups): failStuckProvisioning reconciling a container that came up in the narrow crash→finish window (recoverable via Remove today); extracting the shared slugify.
## Summary Replaces the full-page `/new` workspace-creation flow with a centered **modal** (full-screen sheet on mobile), redesigned from scratch, and moves worker provisioning to an **async, server-driven** flow whose status shows on the workspace card. - **Modal** (`NewWorkerModal.svelte`): single column with purpose-named drawers — **Worker image**, **Mounts** (a reserved, disabled "soon" teaser), **Advanced** (name override). Opens from the workspaces list; closes on Escape / backdrop / ✕, with focus restored to the trigger. - **Async provisioning**: `POST /api/workers` inserts the row in a `queued` phase and returns `201` immediately (modal closes), then a background `provisionWorker` pulls the image → creates → starts, advancing a `provision_phase` (`queued → pulling → creating → starting`, terminal `error`) that the existing poll renders as coarse text on the card. Boot-time `failStuckProvisioning` fails rows orphaned by a restart. - **Reserved seam for #85** (host/remote bind mounts): the Mounts drawer is where that feature lands in a follow-up — no behaviour here, just the home. - **Cleanup**: deletes the `/new` route, `NewWorker.svelte`, `PullProgress.svelte`, the client pull helper, and the pull SSE route (pull now runs server-side inside `provisionWorker`). - **App-wide polish**: themed scrollbars + dropdown selects (previously unstyled), mono dialog title. Design + plan: `docs/superpowers/specs/2026-07-21-new-workspace-modal-design.md`, `docs/superpowers/plans/2026-07-21-new-workspace-modal.md`. ## Correctness fixes from review A multi-agent review caught (and this branch fixes): - **Orphan container**: clicking Remove during provisioning used to hard-delete the row while the background job created a container/volume with no DB row (invisible, blocking the name slot). `provisionWorker` now detects the removed-mid-flight race (via `finishProvisioning`'s row count) and on any post-create failure, and tears the container down. - Failed/provisioning cards gate Start/Stop/Logs on `container_id`, so a worker with no container offers only Remove (those buttons previously threw). - A stalled image pull aborts after 120s of no progress → recorded card error instead of an eternal "Pulling…". - `ProvisionPhase` is single-sourced in `api/types.ts` so server/client can't drift silently. ## Test plan - `pnpm typecheck` — clean. - `pnpm test` — 845 passing (migration 0014, provisioning helpers incl. every in-flight phase + `finishProvisioning` return-count, `provisionLabel`). - **End-to-end against a real dind stack, driven by playwright-cli**: modal create → card `pulling/creating/starting → running` on a real container; failure path → `Failed: <reason>` card with only Remove and **no orphan container**; running card shows the full action row; full-screen mobile sheet; Escape/backdrop/autofocus. ## Notes - Deferred (follow-ups): `failStuckProvisioning` reconciling a container that came up in the narrow crash→finish window (recoverable via Remove today); extracting the shared `slugify`.
lz added 18 commits 2026-07-21 19:48:48 +02:00
- NewWorkerModal: drop duplicate loadRepos on open ($effect already loads)
- service.ts: reuse row in spawnWorker's DTO return instead of a copy
- provision-label: derive ProvisionPhase from WorkerDTO (fail-fast switch)
- Workers card: generalize cardClass errored; hoist duplicated Remove button
- provisionWorker tears down the container if provisioning fails after create,
  or if finishProvisioning finds the row was removed mid-flight (Remove during
  provisioning) — closes an invisible orphan container/volume leak that blocked
  the name slot on re-spawn.
- finishProvisioning returns its row count to detect the removed-mid-flight race.
- abort a stalled image pull (no progress for 120s) so the card lands on an
  error instead of an eternal 'Pulling…'.
- preserve the real getImage().inspect() error instead of a generic message.
- Workers card: gate Start/Stop/Logs/attach on w.container_id so a failed or
  still-provisioning worker offers only Remove (dead buttons previously threw).
- Previews: use background-color not the background shorthand so the app-wide
  select chevron survives; give the narrow control room for it.
- NewWorkerModal: await tick() before focusing search (input renders a tick after
  connections load); refresh the stale create() catch comment.
refactor(workers): single-source ProvisionPhase; validate image tag; strengthen tests
All checks were successful
ci / nexus (pull_request) Successful in 6m9s
ci / images (pull_request) Successful in 7m3s
8b7cdce20b
- ProvisionPhase now lives once in api/types.ts; schema.ts and provision-label
  import it, so a phase can't drift between server and client without a compile
  error.
- route validates worker_image with isValidImageTag (rejects whitespace tags at
  the 400 boundary).
- tests: failStuckProvisioning covers every in-flight phase; finishProvisioning
  return-count (1 and 0) pinned.
Merge remote-tracking branch 'origin/main' into feat/new-ws
All checks were successful
ci / nexus (pull_request) Successful in 4m59s
ci / images (pull_request) Successful in 6m31s
pr-image-cleanup / delete-pr-images (pull_request) Successful in 8s
aa355cbb2a
# Conflicts:
#	nexus/src/lib/components/NewWorker.svelte
#	nexus/src/lib/components/Workers.svelte
lz merged commit 81205d5b72 into main 2026-07-22 13:03:26 +02:00
lz deleted branch feat/new-ws 2026-07-22 13:03:27 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!86
No description provided.