feat(site-editor): Localhost in the drafts list for Blocks v8 sites

Draft
TL
4d ago
+2375-747

Part of the v8 Studio stack (base: #7796). The last PR of the v8 Fast Preview + Localhost track, on top of #7874. PO decisions of 2026-10-11.

R5: a "Localhost" item in the drafts list for Blocks v8 sites, replacing Advanced → Local. Choosing it switches the editor to the developer's own deco serve. Schema and content come over the content protocol on localhost (not git or storage), and the preview is the app deco serve reports (describe.preview).

The tree of this branch is identical to feat/blocks-v8-hosted (b01649227, a normal merge of this branch, no force-push): git diff --stat origin/split/49-v8-localhost-item origin/feat/blocks-v8-hosted is empty, and both trees hash to b3469a0e2.

PO decision (2026-10-11): no site check. Localhost connects to whatever deco serve answers on the endpoint, even one running another site's folder. Studio doesn't encourage that, but no code blocks it (b7e4cf807 removed the check).

What

  • Draft selector (v8 only): a permanent Localhost row above "New draft" (localhost-row.tsx). It is selected while a deco serve connection is stored, and the trigger then reads "Localhost". Choosing any draft clears it, and so does "Stop using Localhost".

  • Choosing it: the endpoint that answered last time is tried first, else localhost:4545. If nothing is running yet, the editor's existing probe backoff (1 s up to 10 s) connects as soon as deco serve starts, and reconnects on its own after a restart.

  • Remembered per user and project: the key is studio:deco-serve-connection:<userId>:<vmcpId>. The old per-project key only existed on this unreleased stack, so nothing is migrated.

  • States, in plain words with one next step each:

    StateWhat the row shows
    Available"Edit the site running on your computer"
    ConnectingSpinner and "Connecting…"
    Connected✓ and "Connected to localhost:4545"
    Not running"Run deco serve in your site's folder", the command with a copy button, and "We'll connect as soon as it starts"
    Chrome blocks accessHow to allow it
    Something answered that isn't usableOut-of-date deco serve, another protocol version, another program on the port, or an error, each with its next step

    The row's state comes from the editor's own content backend, so the two never disagree.

  • Preview pane: while there is no app to show (still connecting, or deco serve found no app), it shows a guide instead of booting a sandbox or showing the hosted draft.

  • Advanced: v8 has no Local tab; v7 keeps main's tab (#7874). local-url-submit.ts and the v8 Local form are deleted.

Known limits

  • There is no separate "Reconnecting" state. A connected site keeps working through a short restart (3 failed checks, about 7 s). After that the row shows the not-running guide, which already says it reconnects on its own.

Tests

  • localhost-row.test.tsx:
    • localhostStatus maps backend × permission to each state.
    • A render test for each state: available (choosing calls onSelect), connecting, connected (Stop calls onTurnOff), not running (command plus Copy), needs permission, and a problem (out of date).
    • The per-user and per-project storage key.
  • tsc --noEmit for apps/web, oxlint, biome format and knip are clean. 193 test files under sections-editor, thread, main-panel-tabs, hooks, routes, i18n, lib and chat/pills pass, one process per file, including the site-editor copy (jargon) test.
  • Not done: a full BranchPicker render test (it needs releases, members and branches mocked) and the manual run on the local spike (starting and stopping deco serve).

Stack

v8 Fast Preview track: previous: #7874 · next: #7876 (copy the site id; top of the stack)

Review fixes (94691da59)

  • The old tunnel on v8: useLocalPreviewUrl returns no tunnel for a v8 project. A tunnel saved earlier no longer replaces the preview or the editor. v7 is unchanged.
  • Choosing Localhost: it connects to the endpoint this user last used for this project, else the default port. Nothing writes the global /site-editor key from here any more.
  • v7 draft selector: the v8-only hooks moved into V8LocalhostRow, so the v7 picker runs none of them.
  • Preview tab: a gate clause that could never be false is gone.
  • New tests: useDecoServeConnection (per user and project, kept endpoint, v7 reads none, nothing written before the session loads), and useLocalPreviewUrl on v7 vs v8.

Fixes after the PO's run (d5ff0e044..f46bfc4f3, plus 7fabb893d in #7872)

  1. The local preview recovers by itself (d2ae03678, f46bfc4f3). Before, if deco serve answered before the app (vite dev) was listening, or the app restarted, the frame stayed on Chrome's error page until a save or a reload. Now the preview checks the app from the browser with a no-cors HEAD and backoff (1 s up to 5 s while down, every 10 s while up). Until the app answers, it shows a "Waiting for your app" guide: "Your app at localhost:3000 isn't answering yet…" and "The preview loads on its own as soon as your app answers." Then it loads the app. If the app goes away (2 failed checks in a row), the guide comes back, and the frame loads again on its own when the app returns. /site-editor uses the same tab, so it gets this too.
  2. Not-running replaces an old problem (f6f9855b3). The local probe used to keep its last answer through failed retries, so the row went on showing an old problem after deco serve stopped. Now the latest probe wins: one failed probe replaces an earlier problem straight away. A connected site gets a short grace period. A failed describe poll on a connected site probes again, so a stop is noticed without waiting for a save.
  3. v8 copy and badge colours:
    • On v8, the Preview server field no longer says "Required for Fast Preview" (7fabb893d, in #7872), because v8 has no Fast Preview switch. It says "Your live site's address. Studio shows your drafts on it before you publish." v7 keeps main's text.
    • The header badge "Local server" is no longer red while it connects or waits for deco serve (2496e1069). It stays neutral with a pulsing dot, and turns red only when something answered but can't be used: out of date, another program, or an error.
  4. No storage key without both ids (d5ff0e044). useLocalStorage writes its default on every read, so …:<userId>: keys holding null were written before the project id was known. The Localhost connection now uses a scoped store with these rules:
    • It never writes on read.
    • It doesn't read or write at all until both the user and the project are known.
    • Turning Localhost off removes the key.

Tests added:

  • use-local-app-reachable.test.tsx: the backoff and grace rules; it waits, then reports the app up on its own; a new URL starts over.

  • preview-tab.test.tsx: the guide while the app isn't answering or isn't checked yet; the frame once it answers; the guide when it goes away, then the frame when it's back; nothing is checked without Localhost.

  • use-content-backend.test.ts (localServeBackend):

    • Any site's deco serve connects (no site check).
    • A connected site rides out 2 failures and becomes not-running at 3.
    • An error from the server beats an earlier answer.
    • Not-running, then connected again.
  • deco-serve-chip.test.ts: the badge is pending while connecting or waiting, and red only for real problems.

  • use-deco-serve-connection.test.tsx: nothing written before the session loads or without a project id; a read stores nothing; a choice is stored only under :<userId>:<vmcpId>.

  • preview-settings-rows.test.tsx (#7872): the v8 hint doesn't mention Fast Preview, and v7 keeps it.

  • Every changed test file passes (one process per file). tsc --noEmit for apps/web, oxlint and biome format are clean.

  • The manual run on the spike was not done. The stack plus a test browser peaks at about 2.9 GB, over the 2 GB per-agent memory limit.

  • d7a3e8957: on a v8 site with no sandbox, the editor no longer spins on "Starting your preview" before the first edit. Nothing starts there, and there is no draft until the first edit, so it shows the published site with no spinner, then the draft as before. hostedDraftSession (v8 marker plus the GitHub backend) is the input; v7 and sandbox sessions are unchanged. Tests: preview-display.test.ts (hosted draft: no spinner before an edit, the draft after, v7 unchanged, Localhost/suspended still win) and content-backend.test.ts (hostedDraftSession).

  • 8517d30de: /site-editor and a project's Localhost Preview render ONE empty state, LocalServeEmptyState (local-serve-empty-state.tsx), with the same copy from one place (decoServe.waiting.*): "Waiting for your computer", the same intro, the same two steps, the Site editor guide / deco serve options / Troubleshooting links, and the moving dot. /site-editor no longer has the Preview-tab/--preview paragraph, the older-site note, the Quickstart link, or the separate "Looking for deco serve…" screen. Problem states (out of date, another program, error) and the Chrome-blocked state look the same in both places. The "Look for deco serve on this computer" button stays, in the same spot, only while Chrome still needs a click before it asks for local-network permission. Choosing Localhost in a project already counts as that click. Tests: site-editor-guide.test.tsx checks that both places render the same markup for "nothing answering" and for "another program on the port". preview-tab.test.tsx was updated. origin/feat/blocks-v8-hosted was updated by a normal merge (23410da81), and its tree matches split/49 (f81747205).

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig