feat(cms-mode): host the CMS block editor in the side panel - #6145
Closed
tlgimenes wants to merge 21 commits into
Closed
feat(cms-mode): host the CMS block editor in the side panel#6145tlgimenes wants to merge 21 commits into
tlgimenes wants to merge 21 commits into
Conversation
Fast Preview projects rendered an inert chat panel — a "coming soon" notice — while the block editor was squeezed into a 31% pane nested inside the Preview tab. The side panel now hosts the editor and the preview canvas takes the whole main panel. Renames Fast Preview to CMS mode throughout, and consolidates the gate: `resolveCmsMode` in packages/shared is now the single reader, consumed by the web app and by both API gates (`decofile.ts`, `sandbox-proxy.ts`) that previously read `metadata.fastPreview` directly. `preview.tsx` also inlined its own copy; it now calls the helper. The persisted key is deliberately still written as `fastPreview` — `resolveCmsMode` reads `cmsMode` first and falls back to it. Flipping the write before every reader ships would 404 the CMS for any newly-toggled project, since the decofile API gates on it. - SidePanelKind widens to "chat" | "cms", including the six sites that hardcoded the literal. Three of those failed silently rather than at compile time (router zod schema, thread-layout memory, chat navigation) and would have dropped the CMS panel on navigation. - The nested blocks pane and its Edit content toggle are retired in CMS mode only. Emptying the pane was not enough: `defaultSize` applies at mount, so it left a 210px dead column. - The CMS tour anchor moves from the preview toolbar to the new toggle. - Sandbox projects are unchanged. Verified against a live CMS-mode project in the native app: toggle, panel mount, section list, `?sidepanel=cms` surviving a full reload, and both the nested pane and old toggle gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CMS mode has no chat to start, so the relocated new-chat button in the panel header was offering a dead action. The sidebar still exposes it when expanded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tlgimenes
force-pushed
the
claude/sharded-mapping-starfish
branch
from
August 17, 2026 12:20
f2865f9 to
a31cbe2
Compare
The console rendered in CMS mode. Three places decided its visibility and none agreed: the provider derived `visible` from a preference and a per-VM localStorage override, `preview.tsx` hid the ⋯ menu item when CMS mode was on, and MainPanelWithDrawer gated the drawer on `hasClonableSource` — true for a CMS project, which has a repo. So a default-on preference (or a stale per-VM override) kept the drawer mounted, while the only control that could dismiss it was hidden. Move the condition to where the state lives: the provider takes `available` and forces `visible` false when there is no daemon to attach to, so an override cannot resurrect it. Consumers read `available` instead of re-deriving the gate — which removes the CMS branch from preview.tsx rather than adding a fourth one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… collapse Terminal visibility was two stacked persisted booleans: "does the drawer exist" (a preference plus a per-VM override, behind the ⋯ menu) and "is it expanded" (PreviewDrawerHost's own open/height, per VM). The first layer only existed to hide a surface the second layer could already collapse — and hiding it removed the very control that dismissed it. Now the drawer mounts whenever the project can have one and sits collapsed to its toolbar until expanded. Collapsed it renders the toolbar only: no xterm instances, and no new connections, since the lifecycle and events contexts are already mounted at the shell. Removes TerminalVisibilityProvider, the terminalVisibleByDefault preference and its Settings row, the per-VM localStorage override with its parser and tests, the ⋯ Show/Hide item, and four i18n keys in both dictionaries. Net -223 lines, and the class of bug this replaces — a hidden control stranding a visible surface — is now unrepresentable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… tab Two more surfaces assumed a sandbox. The Content tab bailed to SandboxStateRenderer because `previewState.kind !== "iframe"`, then treated every phase before `running` as warming — so CMS mode, whose phase never leaves "idle", showed "cloning your repo" forever. The tab itself was gated the same way: `devServerReady` fed the decofile fetch, so `hasEditableDecoContent` never saw data. `resolveBlocksTabState` already documents the rule — "the lifecycle phase stays idle forever and must not gate the panel; data readiness alone decides" — it just was not applied here. Both now bypass the lifecycle in CMS mode and read the decofile over HTTP. Also splits the Preview/Code pair `getSourceSystemTabs` shipped as one unit. Code browses the sandbox filesystem, which CMS mode does not have, so CMS mode gets Preview + Content and vibecoding keeps Preview + Code. Verified on a live CMS project: Content loads 677 pages, 10 sections, 25 loaders; the tab bar reads Preview · Content with no Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tour gated eligibility on `lifecycle.phase === "running"`, which CMS mode never reaches — so the walkthrough built to teach the CMS only ever ran on sandbox projects. Readiness now also accepts CMS mode, where the surface is up immediately. The launch path already polls for the lead anchor and filters steps to visible ones, so the sandbox-less case degrades on its own: four steps resolve in CMS mode (Preview, page dropdown, CMS toggle, branches) and the visual-editor step drops out, since it needs an origin we can inject into. Verified on a live CMS project: previewReady true with phase "idle", four visible steps, tour marks itself seen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec and plan were working notes for this change, not documentation the repo should carry — and they had already drifted from what shipped. Their content stays in this branch's history. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CMS mode was a project-level either/or: `resolveCmsMode(metadata).active` short-circuited the whole sandbox proxy to `runner: null` for every branch, so a CMS project could never boot a pod and vibecoding was unreachable. Split the gate in two. `resolveCmsMode` stays the project capability; the new `resolveCmsModeForBranch` narrows it with "does this branch have a sandbox?" and is what every runtime surface now reads. Provisioning a sandbox moves that one branch onto the daemon while its siblings stay sandbox-less. That keeps exactly one writer per branch. The CMS hooks already had both substrates wired — decofile API vs `/sandbox/write`, preview server vs dev server — so following the branch means a CMS edit on a pod-backed draft lands in the working tree next to the agent's edits, instead of committing to a head the pod can no longer see. No divergence to warn about. The switch itself: the chat composer on a sandbox-less draft now offers "Start coding" (SANDBOX_START) instead of a locked input, and a CMS project with a pod shows both the CMS and Chat toggles rather than one or the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oding button The gate change had no black-box coverage. Adds cms-mode-branch-gate.spec.ts, which asserts over HTTP that a CMS project's `git/status` is GitHub-backed until a sandbox is recorded for that branch, that recording one takes THAT branch off the GitHub path, and that its sibling branch is unaffected — the per-branch half of the contract, which a project-level gate would fail. Promotes the CMS-project fixture (and the GitHub stub admin helpers) out of decofile-api.spec.ts into fixtures/cms-project.ts so both suites share one definition instead of the second copying it. Two fixes found while verifying: - `StartCodingState` disabled itself on `status !== "idle"`, but the live provider never yields "idle" — `computeDrawerStatus` maps a branch with no preview URL to "starting", which is every sandbox-less branch. The button was permanently disabled. Replaced with `isStarting`, the start mutation's own pending flag, now exposed on the lifecycle context. - The per-branch signal now also counts a pending start and the seeded preview URL, so the branch leaves the sandbox-less path at the CLICK rather than when the metadata refetch lands. In that window the pod is already cloning the branch head, so a CMS write routed to the head could miss the clone and be lost silently; routed to the not-yet-reachable sandbox it fails visibly. Verified: 12/12 of both e2e specs on a clean Postgres. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e you edit The switch had no handle. `sidePanelToggles` gated the Chat half on `!cmsModeActive`, so on a sandbox-less CMS draft — exactly the drafts that need to switch — it never rendered. The "Start coding" prompt sat in a side panel with no button to open it, reachable only by hand-editing `?sidepanel=`. A CMS project now shows BOTH halves in every branch state, as a `CMS | Code` pair. Only the Code half's behaviour varies: on a sandbox-less draft it provisions the dev environment and opens the agent, so the modes are one click apart in both directions. Non-CMS projects keep the single Chat toggle. The rule moves into `resolveSidePanelToggles` with a test asserting that provisioning a sandbox never adds or removes a toggle — the invariant this bug broke, and the second toggle-visibility regression on this branch. Adds the one advisory the per-branch model still warrants. Once a draft has a dev environment the CMS panel keeps working but its writes stop being commits on the branch head and become files in the pod's working tree, next to the agent's uncommitted work; a note in the panel says so. A sandbox-less draft gets nothing — the default needs no announcement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The preview toolbar's Edit action and its nested pane were gated per branch, so a CMS draft that gained a sandbox got BOTH editors: the side panel's BlocksPanel and a second copy of the same component inside the Preview tab, each with its own selection state. The toolbar button also sat in the middle of the preview while the real switch lives on the left. Both now gate on the PROJECT (`resolveCmsMode(...).active`), not the branch. The side panel owns content editing in CMS and vibecoding alike, so provisioning a sandbox no longer resurrects the inline pane. Projects with no CMS keep it as their only way in — and, with the pair mutually exclusive, `TOUR_ANCHORS.edit` again resolves to exactly one element per project type. The downgrade rule moves into `resolveEffectiveEditingMode` with tests, so "blocks never opens inline on a CMS project, in either mode" is asserted rather than trusted to a condition in JSX. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The divider that sets the Edit action off from the view controls rendered unconditionally, so a CMS project — which has no Edit action, its block editor being the left-hand toggle — was left with a stray vertical rule leading the URL group. The mobile layout's empty left zone stays: both side zones are `flex-1` so the page selector centres on the bar, and dropping the zone would shift it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g-starfish # Conflicts: # apps/web/src/i18n/en/agent-shell-layout.ts # apps/web/src/i18n/pt-br/agent-shell-layout.ts # apps/web/src/layouts/agent-shell-layout/toggle-buttons.tsx # apps/web/src/layouts/agent-shell-layout/workspace-panel-group.tsx
Two tab buttons read as two features rather than two modes of one draft, and neither had anywhere to say that vibecoding must first be provisioned. They collapse into a single SplitButton: body → collapse or reopen the side panel, on the mode it already shows caret → choose the mode The division follows how often each is used. Collapsing is a many-times-a- session action and takes the wide target — which is also what the tab did, so nothing is relearned. Choosing a mode happens about once per draft and sits behind a caret, the only thing a caret has ever meant. On a sandbox-less draft the vibecoding entry reads "Start vibecoding · builds a dev environment" and provisions on select, so the cost is stated before it is paid. `mode` is threaded in rather than read off `sidePanel`, which goes null on close: the body must reopen the occupant the user left, so WorkspacePanelGroup remembers it. It lives there and not in the button because the button unmounts as it relocates between the chat and main headers. `SplitButtonMenuItem` gains `description` and `selected` — a check in a reserved gutter and a secondary line — plus a `dataTour` passthrough so the CMS tour can still anchor on the control. All three are additive and optional. `resolveSidePanelToggles` drops from two half-flags to one `mode`, keeping the test that provisioning a sandbox never adds or removes a control. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`PuzzlePiece01` said "plugin", not "content". `Grid01` — four separate rounded squares in a 2x2 — reads as a page of blocks, which is what the CMS edits, and it is the mark the mode switch was designed against. Swapped in both places the concept appears: the mode split button, and the preview toolbar's Edit action, which is how projects WITHOUT a CMS reach the same inline block editor. Leaving one behind would have split the visual language across two doors into the same surface. Vibecoding keeps `Code01`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The menu named the two modes but only the button carried their icons, so the list read as text while the control it belongs to did not. Each entry now leads with its mark — the four-square grid for CMS, the chevrons for vibecoding. The optical fix lives in SplitButton, not the caller: a `description` stacks the row, so the check and the icon get a half-step down to align with the label's line instead of the block's centre. Callers pass a plain sized icon and the component places it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ghost left the two halves floating with nothing joining them, so the control read as a label beside a stray caret. Outline gives them a shared border, and ButtonGroup drops the seam between — the divider the primary already draws is then the only line inside, which is what makes a split button read as one control rather than two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lapsed Collapsing the panel moves the switch into the main header, where the cluster's 2px gap put an outlined control flush against the borderless Preview/Content tabs — one run of buttons, when the switch is a different kind of control from the views beside it. Scoped to that position rather than baked into the button: in the chat header it leads the row and needs nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…panel Picking CMS on a draft that had started vibecoding only moved the side panel: the preview kept serving the dev server, the Code tab and console stayed, and the mode read as one-way even though the control offered both directions. The gate now takes the selected mode alongside the substrate. A branch with no sandbox is CMS regardless — there is nothing else to be — and a branch with one follows `?mode=`, so switching back restores the preview origin, the tabs and the console together. Mode is its own URL param rather than something read off `?sidepanel=`, which forgets it whenever the panel closes. It now governs the workspace, so it has to outlive the panel that set it, and survive a reload. The deliberate cost: a CMS write commits to the branch head while the pod still holds an uncommitted tree it cannot see, and the agent's next commit resolves that one way or the other. Chosen over the alternative of pinning the preview to the pod. The advisory is what makes it honest — the CMS panel warns whenever the pod has uncommitted work, naming the count. It can see that work because the API keeps gating on the substrate, so `/git/status` stays daemon-backed even while the UI is in CMS mode. `countLocalWork` joins `hasPublishableLocalWork` behind one shared path list, so the banner's count and its trigger cannot disagree; it dedupes paths that sit in several git buckets at once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`?mode` was new URL state that the per-thread layout memory did not know about, so leaving a vibecoding draft and coming back reopened it in CMS — silently, the same shape as the six `SidePanelKind` literal sites that dropped the panel kind. It matters more here: mode decides the preview's origin, the view tabs and the console, so the wrong one restores the wrong workspace. `ThreadLayout` now carries it, saved on the way out and restored on the way in. `sanitizeThreadLayout` drops anything that is not a known mode — sessionStorage is tamperable and an unknown value must read as "no memory", not reach the gate. Tests cover the round trip and the absent case, since nothing about this failure is visible: no error, just the other mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two things, and the second one is why the first became interesting.
1. CMS mode gets a real home. Fast Preview projects rendered an inert chat panel — a "coming soon" notice — while the block editor was squeezed into a 31% pane nested inside the Preview tab. Three columns, and the widest one did nothing. The side panel now hosts the CMS editor and the preview canvas takes the whole main panel. Fast Preview is renamed CMS mode throughout.
2. The gate becomes per-branch, which makes the two modes composable instead of exclusive.
resolveCmsMode(metadata).activeshort-circuited the entire sandbox proxy torunner: nullfor every branch of a CMS project — so a CMS project could never boot a pod, and vibecoding was unreachable. The gate now splits:resolveCmsModeis the project capability, and the newresolveCmsModeForBranchnarrows it with "does this branch have a sandbox?". Provisioning one moves that branch onto the daemon while its siblings stay sandbox-less.Sandbox-only projects are unchanged.
Why per-branch is the whole design
The two editors write to different layers of the same branch:
Gating on the project gives a branch two writers: the CMS committing to a head that the pod — holding uncommitted work — can no longer see. The agent's next commit then clobbers or conflicts.
Gating on the branch gives it exactly one. Every CMS hook already had both substrates wired (
use-save-block,use-delete-block,use-decofile,use-section-preview-baseeach branch oncmsModeActive), so following the branch means a CMS edit on a pod-backed draft lands in the working tree next to the agent's edits. There is no divergence left to warn about — the fix is deleting a wrong condition, not adding a reconciliation UI.The switch
SANDBOX_START) instead of a locked input.Notable
SidePanelKindwidens to"chat" | "cms"across all six sites that hardcoded the literal. Three failed silently — the router zod schema rejected?sidepanel=cms, and thread-layout memory + chat navigation dropped it — so the panel would have vanished on navigation with no error.defaultSizeapplies at mount, so gating only its content left a 210px dead column. It has to not render.cmsModeActivealso counts a pending start and the seeded preview URL. In that window the pod is already cloning the head, so a CMS write routed there could miss the clone and vanish silently; routed to the not-yet-reachable sandbox it fails visibly.hasVmForBranchis kind-agnostic on purpose, andbranchHasSandboxfalls back to the thread row — a thread-scoped branch records its pod there, and missing it would hand a pod-backed branch back to the head-committing path.Deliberate: the write path still writes the legacy key
resolveCmsModereadscmsModefirst and falls back tofastPreview, but writers still writefastPreview. Flipping the write before every reader ships would 404 the CMS for any newly-toggled project, and the metadata object is.loose(), so a prematurecmsModewrite type-checks and fails only against a real server. An e2e provescmsModealone opens the gate.Testing
fmt·check·lint·knipclean.cms-mode-branch-gate.spec.ts— asserts over HTTP thatgit/statusis GitHub-backed until a sandbox is recorded for the branch, that recording one takes that branch off the GitHub path, and that its sibling is unaffected. A project-level gate fails the third assertion.decofile-api.spec.tsintofixtures/cms-project.ts, so both suites share one definition. (This was listed as a follow-up on an earlier revision of this PR; it's done.)resolveCmsModeandresolveCmsModeForBranch.Verified live in the native app: toggle renders as CMS, panel mounts with the real section list,
?sidepanel=cmssurvives a full reload, nested pane and oldEdit contenttoggle both gone.Known gaps
header-actions.tsxcarries ~100 lines of dead CMS handling.VirtualMcpHeaderInfohas routed CMS projects toCmsHeaderActionssince feat(fast-preview): smart primary action button for CMS mode #6054, so those branches were already unreachable before this PR. Left alone rather than widening the diff.sandbox-events-context.tsxstill reads the project-level gate for its blocks-save reload heuristic. Cosmetic only (the decofile refetch inside is separately gated on the dev server), and the legacy semantics there predate the sandbox-less rework.🤖 Generated with Claude Code
Summary by cubic
Hosts the CMS block editor in the side panel and renames Fast Preview to CMS mode. The CMS/Vibecoding switch now governs the entire workspace and gates per branch, so one draft can start vibecoding without affecting siblings.
packages/sharedasresolveCmsMode(capability) andresolveCmsModeForBranch(per-branch runtime). API routes (decofile,sandbox-proxy) and web read the same rule; writers still persist the legacyfastPreviewkey.?mode=cms|vibecodingand is remembered per thread; CMS projects default the side panel to CMS.SidePanelKindto"chat" | "cms"and sanitizes untrusted values withparseSidePanelKindacross router search, layout memory, task switch, and chat navigation.resolveEffectiveEditingMode; CMS editing lives in the side panel in both modes. Section previews usebuildCmsDraftUrl; preview display usescmsModeActive/cmsModeReady.countLocalWork).cms-mode-branch-gate.spec.ts; shared CMS fixtures; unit tests for gates, editing‑mode downgrades, preview display; side‑panel toggles; layout memory and task‑switch restore; sandbox git‑API counts.Migration
@/sdk/fast-previewwith@/sdk/cms-mode; renamebuildFastPreviewDraftUrltobuildCmsDraftUrl; updateresolveSectionPreviewBaseto acceptcmsModeActive."cms"in side‑panel state; useparseSidePanelKindfor untrusted input. Persist the selected mode via themodequery param (cms|vibecoding) and restore it per thread.resolveCmsModeForBranchfor runtime gating andresolveCmsModefor capability checks (API and web).TerminalVisibilityProviderusage; do not assume the Code tab or console exists on sandbox‑less branches.Written for commit b2d8dd3. Summary will update on new commits.