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 [expandedIndex, setExpandedIndex] = useState(null); + const sectionRefs = useRef>(new Map()); + + 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; + }>; + + // Build a mapping from resolveType -> sidebar index for inspect matching. + // The DOM's data-manifest-key is the component path (e.g. "site/sections/Hero.tsx"), + // which matches our unwrapped resolveType. + const resolveTypeToIndex = new Map(); + for (let i = 0; i < sections.length; i++) { + const sec = sections[i]; + if (!sec) continue; + const { resolveType } = unwrapSection(sec, decofile); + resolveTypeToIndex.set(resolveType, i); + } + + // Listen for inspect clicks from the preview iframe + // oxlint-disable-next-line ban-use-effect/ban-use-effect — custom event listener for cross-component communication + useEffect(() => { + const handler = (e: Event) => { + const { manifestKey } = (e as CustomEvent<{ manifestKey: string }>) + .detail; + const idx = resolveTypeToIndex.get(manifestKey); + if (idx == null) return; + setExpandedIndex(idx); + requestAnimationFrame(() => { + sectionRefs.current + .get(idx) + ?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); + }; + window.addEventListener(SECTION_CLICK_EVENT, handler); + return () => window.removeEventListener(SECTION_CLICK_EVENT, handler); + }); + + return ( +
+
+ + / + {pageName} +
+ +
+ {!resolver ? ( +
+

+ Could not load site schema. +

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

No sections found.

+
+ ) : ( +
+ {sections.map((section, idx) => ( + + setExpandedIndex(expandedIndex === idx ? null : idx) + } + ref={(el) => { + if (el) sectionRefs.current.set(idx, el); + else sectionRefs.current.delete(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 sendToIframe(iframe: HTMLIFrameElement | null, script: string) { + if (!iframe?.contentWindow) return; + iframe.contentWindow.postMessage( + { type: "editor::inject", args: { script } }, + "*", + ); +} + +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 [inspectMode, setInspectMode] = useState(false); + + const previewSrc = `${envUrl}${previewPath}`; + + const handlePathSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const normalized = pathInput.startsWith("/") ? pathInput : `/${pathInput}`; + setPreviewPath(normalized); + }; + + const toggleInspect = () => { + const next = !inspectMode; + setInspectMode(next); + sendToIframe( + iframeRef.current, + next ? INSPECT_ENABLE_SCRIPT : INSPECT_DISABLE_SCRIPT, + ); + }; + + // Re-inject inspect overlays after iframe navigates or refreshes + const handleIframeLoad = () => { + if (inspectMode) { + sendToIframe(iframeRef.current, INSPECT_ENABLE_SCRIPT); + } + }; + + // Listen for section-click messages from the iframe and relay as a custom event + // oxlint-disable-next-line ban-use-effect/ban-use-effect — window message listener for cross-origin iframe communication + useEffect(() => { + const handler = (e: MessageEvent) => { + if (e.data?.type !== "studio::section-click") return; + const manifestKey = e.data.manifestKey as string; + window.dispatchEvent( + new CustomEvent(SECTION_CLICK_EVENT, { detail: { manifestKey } }), + ); + }; + window.addEventListener("message", handler); + return () => window.removeEventListener("message", handler); + }, []); + + 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" + /> +
+
+ + + +
+ +
+