Conversation
…w toolbar Adds a schema-driven form editor for deco.cx page sections, integrated as a side panel in the Preview tab. The URL bar now shows a pages dropdown (from the decofile) for quick navigation. Saves go through a new vm-file API proxy that writes block JSON files to the sandbox. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsSuggested: Minor ( React with an emoji to override the release type:
Current version:
|
There was a problem hiding this comment.
7 issues found across 22 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/web/components/sections-editor/fields/enum-field.tsx">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/fields/enum-field.tsx:28">
P2: Map the selected string back to the original enum option before calling `onChange` so numbers, booleans, and other non-string enum values preserve their types.</violation>
</file>
<file name="apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx:44">
P2: `selectedIdx` is derived from `value` only on first render, so external `value` updates can desynchronize the selected option from the actual field value.</violation>
</file>
<file name="apps/mesh/src/web/components/sections-editor/fields/array-field.tsx">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/fields/array-field.tsx:19">
P2: New array items default to an empty string for all non-object types, producing schema-invalid values for number/boolean/integer arrays.</violation>
<violation number="2" location="apps/mesh/src/web/components/sections-editor/fields/array-field.tsx:46">
P2: Using the array index as the row key can cause item state to shift to the wrong row after removals.</violation>
</file>
<file name="apps/mesh/src/web/components/sections-editor/resolve-schema.ts">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/resolve-schema.ts:79">
P2: `allOf` flattening drops `required` constraints from member schemas, so required fields can be incorrectly treated as optional in the resolved schema.</violation>
</file>
<file name="apps/mesh/src/web/components/sections-editor/page-list.tsx">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/page-list.tsx:23">
P2: Guard `decodeURIComponent` to avoid runtime crashes on malformed page keys.</violation>
</file>
<file name="apps/mesh/src/web/components/vm/preview/preview.tsx">
<violation number="1" location="apps/mesh/src/web/components/vm/preview/preview.tsx:571">
P2: Dropdown page items use `onMouseDown` instead of `onClick`, so keyboard activation (Enter/Space) cannot navigate to a page.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| {schema.description && ( | ||
| <p className="text-xs text-muted-foreground">{schema.description}</p> | ||
| )} | ||
| <Select value={strValue} onValueChange={(v) => onChange(v)}> |
There was a problem hiding this comment.
P2: Map the selected string back to the original enum option before calling onChange so numbers, booleans, and other non-string enum values preserve their types.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/fields/enum-field.tsx, line 28:
<comment>Map the selected string back to the original enum option before calling `onChange` so numbers, booleans, and other non-string enum values preserve their types.</comment>
<file context>
@@ -0,0 +1,42 @@
+ {schema.description && (
+ <p className="text-xs text-muted-foreground">{schema.description}</p>
+ )}
+ <Select value={strValue} onValueChange={(v) => onChange(v)}>
+ <SelectTrigger>
+ <SelectValue placeholder="Select..." />
</file context>
| <p className="text-xs text-muted-foreground">{schema.description}</p> | ||
| )} | ||
| {items.map((item, i) => ( | ||
| <div key={`${path}.${i}`} className="border rounded-md p-3 relative"> |
There was a problem hiding this comment.
P2: Using the array index as the row key can cause item state to shift to the wrong row after removals.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/fields/array-field.tsx, line 46:
<comment>Using the array index as the row key can cause item state to shift to the wrong row after removals.</comment>
<file context>
@@ -0,0 +1,79 @@
+ <p className="text-xs text-muted-foreground">{schema.description}</p>
+ )}
+ {items.map((item, i) => (
+ <div key={`${path}.${i}`} className="border rounded-md p-3 relative">
+ <Button
+ type="button"
</file context>
|
|
||
| const addItem = () => { | ||
| const defaultVal = | ||
| itemSchema?.type === "object" ? {} : (itemSchema?.default ?? ""); |
There was a problem hiding this comment.
P2: New array items default to an empty string for all non-object types, producing schema-invalid values for number/boolean/integer arrays.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/fields/array-field.tsx, line 19:
<comment>New array items default to an empty string for all non-object types, producing schema-invalid values for number/boolean/integer arrays.</comment>
<file context>
@@ -0,0 +1,79 @@
+
+ const addItem = () => {
+ const defaultVal =
+ itemSchema?.type === "object" ? {} : (itemSchema?.default ?? "");
+ onChange([...items, defaultVal]);
+ };
</file context>
| } | ||
|
|
||
| // Merge allOf | ||
| if (Array.isArray(schema.allOf)) { |
There was a problem hiding this comment.
P2: allOf flattening drops required constraints from member schemas, so required fields can be incorrectly treated as optional in the resolved schema.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/resolve-schema.ts, line 79:
<comment>`allOf` flattening drops `required` constraints from member schemas, so required fields can be incorrectly treated as optional in the resolved schema.</comment>
<file context>
@@ -0,0 +1,198 @@
+ }
+
+ // Merge allOf
+ if (Array.isArray(schema.allOf)) {
+ let merged: RawSchema = {};
+ for (const entry of schema.allOf) {
</file context>
| name = name.slice(0, lastDash); | ||
| } | ||
| } | ||
| return decodeURIComponent(name); |
There was a problem hiding this comment.
P2: Guard decodeURIComponent to avoid runtime crashes on malformed page keys.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/page-list.tsx, line 23:
<comment>Guard `decodeURIComponent` to avoid runtime crashes on malformed page keys.</comment>
<file context>
@@ -0,0 +1,80 @@
+ name = name.slice(0, lastDash);
+ }
+ }
+ return decodeURIComponent(name);
+}
+
</file context>
| onMouseDown={(e) => { | ||
| e.preventDefault(); | ||
| setPagesOpen(false); | ||
| // Navigate the iframe | ||
| const iframe = previewIframeRef.current; | ||
| if (iframe && previewUrl) { | ||
| iframe.src = new URL(page.path, previewUrl).href; | ||
| } |
There was a problem hiding this comment.
P2: Dropdown page items use onMouseDown instead of onClick, so keyboard activation (Enter/Space) cannot navigate to a page.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/vm/preview/preview.tsx, line 571:
<comment>Dropdown page items use `onMouseDown` instead of `onClick`, so keyboard activation (Enter/Space) cannot navigate to a page.</comment>
<file context>
@@ -493,10 +542,54 @@ export function PreviewContent() {
+ key={page.key}
+ type="button"
+ className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left text-sm hover:bg-accent hover:text-accent-foreground"
+ onMouseDown={(e) => {
+ e.preventDefault();
+ setPagesOpen(false);
</file context>
| onMouseDown={(e) => { | |
| e.preventDefault(); | |
| setPagesOpen(false); | |
| // Navigate the iframe | |
| const iframe = previewIframeRef.current; | |
| if (iframe && previewUrl) { | |
| iframe.src = new URL(page.path, previewUrl).href; | |
| } | |
| onClick={() => { | |
| setPagesOpen(false); | |
| // Navigate the iframe | |
| const iframe = previewIframeRef.current; | |
| if (iframe && previewUrl) { | |
| iframe.src = new URL(page.path, previewUrl).href; | |
| } | |
| }} |
…editable page fields - Rewrite resolve-schema.ts to mirror admin-mcp buildProperty/collectProps - Rewrite section-list.tsx with parseSections() for lazy/hidden/saved-block/multivariate detection - Add drag-and-drop reordering with @dnd-kit/sortable - Add editable page name/path inputs with auto-save - Drill-down layout: section list → section form with back navigation - Unwrap lazy/hidden/multivariate wrappers for form editing and re-wrap on save - Detect lazy-wrapped saved blocks (e.g. Lazy → Header) - Use TextInput icon for CMS toggle button - Attempt to preserve focus during iframe reload Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
5 issues found across 7 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx:56">
P1: This change breaks the legacy `anyOf` rendering path: `AnyOfField` now returns `null` unless `anyOfRefs` is present, so non-`anyOfRefs` `anyOf` fields disappear from the editor.</violation>
</file>
<file name="apps/mesh/src/web/components/sections-editor/section-list.tsx">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/section-list.tsx:221">
P2: The section item was changed from a `<button>` to a non-focusable `<div>`, which breaks keyboard selection. Make the row focusable and semantic (or use a real button) so Enter/Space selection works.</violation>
</file>
<file name="apps/mesh/src/web/components/sections-editor/sections-editor.tsx">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/sections-editor.tsx:210">
P1: When `currentPath` switches pages, section/form state is not cleared, so stale data can be edited and saved against sections from the newly active page.</violation>
<violation number="2" location="apps/mesh/src/web/components/sections-editor/sections-editor.tsx:331">
P2: Debounced page header autosave can lose edits when `name` and `path` are changed close together; only the last changed field is persisted.</violation>
</file>
<file name="apps/mesh/src/web/components/sections-editor/resolve-schema.ts">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/resolve-schema.ts:156">
P2: Selecting `resolved.type[0]` for union `type` arrays can misclassify nullable fields as `null`, causing those fields to disappear from the form when unset.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic
| } | ||
|
|
||
| // No variants available | ||
| return null; |
There was a problem hiding this comment.
P1: This change breaks the legacy anyOf rendering path: AnyOfField now returns null unless anyOfRefs is present, so non-anyOfRefs anyOf fields disappear from the editor.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/fields/any-of-field.tsx, line 56:
<comment>This change breaks the legacy `anyOf` rendering path: `AnyOfField` now returns `null` unless `anyOfRefs` is present, so non-`anyOfRefs` `anyOf` fields disappear from the editor.</comment>
<file context>
@@ -17,104 +15,43 @@ export function AnyOfField({
- </div>
- );
+ // No variants available
+ return null;
}
</file context>
| const pages = extractPages(decofile); | ||
| const norm = (s: string) => s.replace(/\/+$/, "") || "/"; | ||
| const activePage = pages.find((p) => norm(p.path) === norm(currentPath)); | ||
| const activePageKey = activePage?.key ?? null; |
There was a problem hiding this comment.
P1: When currentPath switches pages, section/form state is not cleared, so stale data can be edited and saved against sections from the newly active page.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/sections-editor.tsx, line 210:
<comment>When `currentPath` switches pages, section/form state is not cleared, so stale data can be edited and saved against sections from the newly active page.</comment>
<file context>
@@ -67,187 +205,226 @@ export function SectionsEditor({
const pages = extractPages(decofile);
+ const norm = (s: string) => s.replace(/\/+$/, "") || "/";
+ const activePage = pages.find((p) => norm(p.path) === norm(currentPath));
+ const activePageKey = activePage?.key ?? null;
- const selectedPage =
</file context>
| <div | ||
| ref={setNodeRef} | ||
| style={style} | ||
| onClick={onSelect} |
There was a problem hiding this comment.
P2: The section item was changed from a <button> to a non-focusable <div>, which breaks keyboard selection. Make the row focusable and semantic (or use a real button) so Enter/Space selection works.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/section-list.tsx, line 221:
<comment>The section item was changed from a `<button>` to a non-focusable `<div>`, which breaks keyboard selection. Make the row focusable and semantic (or use a real button) so Enter/Space selection works.</comment>
<file context>
@@ -1,28 +1,339 @@
+ <div
+ ref={setNodeRef}
+ style={style}
+ onClick={onSelect}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") onSelect();
</file context>
| onClick={onSelect} | |
| role="button" | |
| tabIndex={0} | |
| onClick={onSelect} |
| pageDebounceRef.current = setTimeout(() => { | ||
| const fullPageData = { | ||
| ...(decofile[activePageKey] as Record<string, unknown>), | ||
| [field]: value, |
There was a problem hiding this comment.
P2: Debounced page header autosave can lose edits when name and path are changed close together; only the last changed field is persisted.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/sections-editor.tsx, line 331:
<comment>Debounced page header autosave can lose edits when `name` and `path` are changed close together; only the last changed field is persisted.</comment>
<file context>
@@ -67,187 +205,226 @@ export function SectionsEditor({
+ pageDebounceRef.current = setTimeout(() => {
+ const fullPageData = {
+ ...(decofile[activePageKey] as Record<string, unknown>),
+ [field]: value,
};
- const updatedSections = [...sections];
</file context>
| type = Array.isArray(resolved.type) | ||
| ? String(resolved.type[0]) | ||
| : String(resolved.type); |
There was a problem hiding this comment.
P2: Selecting resolved.type[0] for union type arrays can misclassify nullable fields as null, causing those fields to disappear from the form when unset.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/resolve-schema.ts, line 156:
<comment>Selecting `resolved.type[0]` for union `type` arrays can misclassify nullable fields as `null`, causing those fields to disappear from the form when unset.</comment>
<file context>
@@ -1,198 +1,363 @@
+ // Determine type
+ let type: string | undefined;
+ if (resolved.type) {
+ type = Array.isArray(resolved.type)
+ ? String(resolved.type[0])
+ : String(resolved.type);
</file context>
| type = Array.isArray(resolved.type) | |
| ? String(resolved.type[0]) | |
| : String(resolved.type); | |
| type = Array.isArray(resolved.type) | |
| ? String( | |
| resolved.type.find((t) => t !== "null") ?? resolved.type[0], | |
| ) | |
| : String(resolved.type); |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/web/components/sections-editor/section-list.tsx">
<violation number="1" location="apps/mesh/src/web/components/sections-editor/section-list.tsx:226">
P2: `touch-none` is now applied to the full row, which can block native touch scrolling on section items.</violation>
<violation number="2" location="apps/mesh/src/web/components/sections-editor/section-list.tsx:236">
P2: The added `listeners` spread overrides the existing `onKeyDown`, so keyboard selection (Enter/Space) no longer runs.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| if (e.key === "Enter" || e.key === " ") onSelect(); | ||
| }} | ||
| className={cn( | ||
| "group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 touch-none transition-colors active:cursor-grabbing", |
There was a problem hiding this comment.
P2: touch-none is now applied to the full row, which can block native touch scrolling on section items.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/section-list.tsx, line 226:
<comment>`touch-none` is now applied to the full row, which can block native touch scrolling on section items.</comment>
<file context>
@@ -223,7 +223,7 @@ function SortableSectionItem({
}}
className={cn(
- "group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 transition-colors",
+ "group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 touch-none transition-colors active:cursor-grabbing",
selected
? "bg-accent text-accent-foreground"
</file context>
| "group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 touch-none transition-colors active:cursor-grabbing", | |
| "group flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-2.5 transition-colors active:cursor-grabbing", |
Tip: Review your code locally with the cubic CLI to iterate faster.
| : "text-foreground/80 hover:bg-accent hover:text-accent-foreground", | ||
| )} | ||
| {...attributes} | ||
| {...listeners} |
There was a problem hiding this comment.
P2: The added listeners spread overrides the existing onKeyDown, so keyboard selection (Enter/Space) no longer runs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/sections-editor/section-list.tsx, line 236:
<comment>The added `listeners` spread overrides the existing `onKeyDown`, so keyboard selection (Enter/Space) no longer runs.</comment>
<file context>
@@ -232,17 +232,10 @@ function SortableSectionItem({
: "text-foreground/80 hover:bg-accent hover:text-accent-foreground",
)}
+ {...attributes}
+ {...listeners}
>
- {/* Drag handle */}
</file context>
| {...listeners} | |
| {...listeners} | |
| onKeyDown={(e) => { | |
| listeners?.onKeyDown?.(e); | |
| if (e.key === "Enter" || e.key === " ") onSelect(); | |
| }} |
…w toolbar
Adds a schema-driven form editor for deco.cx page sections, integrated as a side panel in the Preview tab. The URL bar now shows a pages dropdown (from the decofile) for quick navigation. Saves go through a new vm-file API proxy that writes block JSON files to the sandbox.
What is this contribution about?
Screenshots/Demonstration
How to Test
Migration Notes
Review Checklist
Summary by cubic
Adds a schema-driven Sections Editor side panel in Preview and a pages dropdown in the URL bar for quick navigation. You can edit deco.cx page sections and named blocks with debounced auto-save to the sandbox via a new vm-file API proxy.
/.decofile, drill-down list → form, drag-and-drop reorder (entire row is draggable), and edit page name/path with autosave.$ref, mergesallOf/anyOf/oneOf, const-enum extraction); supports block-ref variants and text/number/boolean/array/object/image/color; unwraps lazy/hidden/multivariate and saved (incl. lazy-wrapped) blocks for editing and rewraps on save./.decofilenavigates the preview iframe; “Sections Editor” toggle added; preview reloads after saves and tries to preserve focus./api/:org/vm-file/{write,read}forwards authenticated requests and writes.deco/blocks/<key>.json; added query keysdecofileandlive-meta.Written for commit b1b2c0d. Summary will update on new commits. Review in cubic