feat(cloud): archive cloud workspaces instead of deleting them, and unarchive them

Merged
SA
1mo ago
+14291-137

What & why

Archiving a cloud workspace used to delete its box immediately, and the desktop had no way back. Now archive stops the box and keeps it for 7 days. Unarchive brings the same box back with its disk and terminal sessions. After 7 days the box is deleted, and unarchive gives the same row a fresh box from its environment. The record (name, timeline, comments, tasks, sharing) is the same row either way.

Screenshots of every step, from the real app: https://app.superset.sh/page/archiving-cloud-workspaces-c8wn33

Server

  • cloudWorkspace.delete stops the box, marks the row archived, and queues a reap job 7 days out (/api/cloud-workspaces/reap, QStash delay, within every plan's limit). The job deletes the box only if the row is still archived from that archive (deletedAt must match), so archive → unarchive → archive can't reap early. If queueing fails, the box is deleted at once. The procedure keeps its delete name because the CLI, SDK and MCP call it.
  • cloudWorkspace.unarchive: if the stopped box still exists, the row goes straight back to ready and the existing wake path resumes it on open. Otherwise the row goes to provisioning under a new box name (nextSandboxNameFor). Vercel answers 409 image_not_ready for a deleted box's name for a few minutes, and the provision job now reads the name from the row.
  • jobs.ts holds the QStash-or-in-process publisher that provision and reap now share.
  • New unarchived activity event.

Desktop

  • Archive happens at once, with no confirm, from the sidebar hover icon, the context menu, the details ⋯ menu and ⌘⇧⌫. Cloud targets of the delete intent are archived by DeleteWorkspaceMount, and local workspaces keep their dialog.
  • Optimistic: moveCloudWorkspaceRow moves the row between the active and archived caches (and patches get) before the server answers, and rolls back with a toast on error.
  • Archived workspace UI: an archived screen on the workspace route (name, "Archived · time", Unarchive), an archive icon after the details title, grayed names with an archive icon and an undo on hover in the list, and Unarchive in the details actions and ⋯ menu. Share and ⋯ stay visible while archived.
  • The provisioning timer counts from when this session first sees the workspace provisioning (provisioningSince), not from createdAt.
  • Translations for all locales.

Migration

0133_cloud_workspace_unarchived_event.sql is a single ALTER TYPE "cloud_workspace_activity_event" ADD VALUE 'unarchived'. It locks only the enum type and touches no table, so it neither rewrites nor scans anything. Nothing in this release uses the value inside the migration transaction. Generated by drizzle-kit on top of main's 0132.

How I tested it

Driven with real clicks over CDP in this branch's dev app, against a local API and real Vercel boxes (dev Neon branch, a personal environment with no secrets):

  • Every archive entry point archives at once, the row leaves the sidebar, and the archived screen shows immediately and stays (sampled for 4 s).
  • Grace resume: I wrote a marker file into the box, archived, and unarchived 13 s later. The server went deleted → ready with no reprovision, the marker read back, and the terminal showed "Session Contents Restored".
  • Reap: with a 60 s window, the box was stopped at +30 s and gone at +60 s. Unarchive afterwards provisioned a fresh box (ready in ≈10 s, timer started at 0:03).
  • Re-ran archive → details → Unarchive (resume) → sidebar archive after rebasing onto #7964.
  • Timeline shows "unarchived the workspace" entries.
  • tsc clean for desktop, api, trpc, db and ui, and mobile via turbo. biome check and sherif are clean, check:i18n passes, and i18n tests plus 914 desktop tests in the touched areas pass.

Not verified: the QStash-delivered reap in production. Locally the job runs on an in-process timer. After deploy: archive a workspace, confirm a QStash message is scheduled for +7 days, and confirm the box is stopped, not deleted.

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • bun run lint and bun run typecheck pass (CI fails on lint warnings too)
  • "Allow edits from maintainers" is checked on fork PRs

Summary by cubic

Archiving a cloud workspace now stops its box and keeps it for 7 days instead of deleting it on the spot, and adds unarchive, which resumes the same box with its disk and terminal sessions. After 7 days the box is deleted, and unarchive provisions a fresh box from the environment.

Server

  • cloudWorkspace.delete stops the box, marks the row archived, and queues a reap job 7 days out via QStash (or in-process on a local API).
  • cloudWorkspace.unarchive resumes the stopped box when it still exists, otherwise provisions a fresh box under a new sandbox name.
  • The reap job only deletes the box if the row is still archived from that same archive, so archive → unarchive → archive can't reap early.
  • Migration 0133 adds the unarchived activity event via ALTER TYPE ... ADD VALUE.

Desktop

  • Archive triggers immediately from the sidebar, context menu, details menu, and ⌘⇧⌫, with no confirmation.
  • Rows move between the active and archived caches optimistically, rolling back with a toast on error.
  • Archived workspaces show an archived screen with Unarchive, grayed list names with an undo on hover, and Unarchive in the details menu.
  • The provisioning timer counts from when the session first sees the workspace provisioning instead of from createdAt.
  • Czech and Turkish translations for the new archive strings were corrected.

Summary by CodeRabbit

  • New Features
    • Cloud workspaces can be archived and unarchived from the workspace list, sidebar, and workspace details.
    • Archived workspaces show their status and archive time, and workspace activity records when they’re unarchived.
    • Unarchiving restores an existing workspace when possible or restarts provisioning. Archived workspaces are scheduled for cleanup after seven days.
    • Archive and unarchive actions are available in supported languages.