From 25363ab45db38defacde65c2b7546b29c9003141 Mon Sep 17 00:00:00 2001 From: decobot Date: Fri, 10 Apr 2026 14:59:40 -0300 Subject: [PATCH 1/3] feat(pages): add site pages editor with schema viewer and live preview Introduce a Pages view for deco sites with: - Pages list derived client-side from .decofile (no MCP round-trip) - Section schema viewer in the thick sidebar (SchemaResolver parses _meta) - Live iframe preview with editable URL bar in the main content area - Shared data hooks (useDecofile, useSiteMeta) with React Query caching - Search/filter on the pages list - Fix openMainView to properly clear id/toolName on navigation Made-with: Cursor --- .../web/components/chat/side-panel-tasks.tsx | 76 +- .../src/web/layouts/agent-shell-layout.tsx | 10 +- apps/mesh/src/web/layouts/shell-layout.tsx | 12 +- apps/mesh/src/web/lib/schema-resolver.ts | 466 +++++++++++ apps/mesh/src/web/routes/agent-home.tsx | 10 + apps/mesh/src/web/views/pages/index.tsx | 724 ++++++++++++++++++ 6 files changed, 1293 insertions(+), 5 deletions(-) create mode 100644 apps/mesh/src/web/lib/schema-resolver.ts create mode 100644 apps/mesh/src/web/views/pages/index.tsx diff --git a/apps/mesh/src/web/components/chat/side-panel-tasks.tsx b/apps/mesh/src/web/components/chat/side-panel-tasks.tsx index f56eece0e7..153a538260 100644 --- a/apps/mesh/src/web/components/chat/side-panel-tasks.tsx +++ b/apps/mesh/src/web/components/chat/side-panel-tasks.tsx @@ -11,7 +11,13 @@ import { Page } from "@/web/components/page"; import { getIconComponent, parseIconString } from "../agent-icon"; import { usePanelActions } from "@/web/layouts/shell-layout"; -import { Edit05, LayoutLeft, Loading01, Settings02 } from "@untitledui/icons"; +import { + Edit05, + File06, + LayoutLeft, + Loading01, + Settings02, +} from "@untitledui/icons"; import { useVirtualMCPActions, useVirtualMCP } from "@decocms/mesh-sdk"; import type { VirtualMCPEntity } from "@decocms/mesh-sdk/types"; import { Suspense, useEffect, useRef, useState, useTransition } from "react"; @@ -28,6 +34,10 @@ import { } from "@deco/ui/components/tooltip.tsx"; import { IconPicker } from "@/web/components/icon-picker.tsx"; import { useInsetContext } from "@/web/layouts/agent-shell-layout"; +import { + PageSectionsSidebar, + getDecoConnectionId, +} from "@/web/views/pages/index"; // ──────────────────────────────────────── // Shared nav item style — used by New session and view buttons @@ -117,10 +127,14 @@ function ProjectViewsSection({ project }: { project: VirtualMCPEntity }) { icon: string | null; }> | null) ?? []; - if (pinnedViews.length === 0) return null; + // Deco projects have a file_explorer pinned view + const isDecoProject = pinnedViews.some((v) => v.toolName === "file_explorer"); + + if (pinnedViews.length === 0 && !isDecoProject) return null; // Determine which pinned view is currently active const currentMain = virtualMcpCtx?.mainView; + const isPagesActive = currentMain?.type === "pages"; const isExtAppActive = (view: { connectionId: string; toolName: string }) => currentMain?.type === "ext-apps" && currentMain.id === view.connectionId && @@ -128,6 +142,21 @@ function ProjectViewsSection({ project }: { project: VirtualMCPEntity }) { return ( <> + {isDecoProject && ( + + )} {pinnedViews.map((view) => ( + ))} + + )} + + )} + + + ); +} + +// --------------------------------------------------------------------------- +// PageSectionsPanel — rendered inside the thick sidebar +// --------------------------------------------------------------------------- + +/** + * Renders sections list for a page. Used by PageSectionsSidebar. + * Fetches _meta + decofile via shared hooks (cached, no duplication). + */ +export function PageSectionsPanel({ + envUrl, + pageKey, +}: { + envUrl: string; + pageKey: string; +}) { + const { openMainView } = usePanelActions(); + + const { data: meta } = useSiteMeta(envUrl); + const { data: decofile } = useDecofile(envUrl); + + const resolver = meta ? new SchemaResolver(meta) : null; + const pageContent = decofile?.[pageKey] ?? null; + const pageName = + (pageContent?.name as string) ?? (pageContent?.title as string) ?? pageKey; + + const sections = (pageContent?.sections ?? []) as Array<{ + __resolveType: string; + [k: string]: unknown; + }>; + + return ( +
+
+ + / + {pageName} +
+ +
+ {!resolver ? ( +
+

+ Could not load site schema. +

+
+ ) : sections.length === 0 ? ( +
+

No sections found.

+
+ ) : ( +
+ {sections.map((section, idx) => ( + + ))} +
+ )} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// PageSectionsSidebar — bootstraps env then renders sections panel +// --------------------------------------------------------------------------- + +export function PageSectionsSidebar({ + connectionId, + pageKey, +}: { + connectionId: string; + pageKey: string; +}) { + const { org } = useProjectContext(); + const client = useMCPClient({ connectionId, orgId: org.id }); + + const { data: envResult } = useMCPToolCall({ + client, + toolName: "file_explorer", + toolArguments: {}, + staleTime: 5 * 60 * 1000, + }); + + const env = extractStructured(envResult); + if (!env?.userEnv) { + return ( +
+

+ Initializing environment... +

+
+ ); + } + + const envUrl = buildEnvUrl(env.site, env.userEnv, env.userEnvUrl); + + return ; +} + +// --------------------------------------------------------------------------- +// PagePreviewView — iframe + URL bar (main content area) +// --------------------------------------------------------------------------- + +function PagePreviewView({ + envUrl, + pageKey, +}: { + envUrl: string; + pageKey: string; +}) { + const iframeRef = useRef(null); + + const { data: decofile } = useDecofile(envUrl); + const initialPath = (decofile?.[pageKey]?.path as string) ?? "/"; + + const [previewPath, setPreviewPath] = useState(initialPath); + const [pathInput, setPathInput] = useState(initialPath); + const [refreshKey, setRefreshKey] = useState(0); + + const previewSrc = `${envUrl}${previewPath}`; + + const handlePathSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const normalized = pathInput.startsWith("/") ? pathInput : `/${pathInput}`; + setPreviewPath(normalized); + }; + + return ( +
+
+
+
+ setPathInput(e.target.value)} + placeholder="/" + className="h-7 min-w-0 flex-1 border-0 bg-transparent px-1.5 shadow-none focus-visible:ring-0 text-sm" + /> +
+
+ + +
+ +
+