Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6b54065
feat(01-01): define SITE_BINDING and create site-editor plugin skeleton
vibegui Feb 14, 2026
a0e32de
feat(01-02): add preview panel component and tunnel URL hook
vibegui Feb 14, 2026
ffe3137
feat(01-01): implement ServerPlugin, ClientPlugin, and register in Mesh
vibegui Feb 14, 2026
94ec844
feat(01-03): implement five page CRUD server tools
vibegui Feb 14, 2026
4dae337
feat(01-03): implement pages list and page editor UI components
vibegui Feb 14, 2026
dec8639
feat(02-01): scanner pipeline -- types, extract, discover, schema
vibegui Feb 14, 2026
1fe0826
feat(02-01): block server tools and client API helpers
vibegui Feb 14, 2026
b0ea78d
feat(02-02): add RJSF templates, widgets, and PropEditor component
vibegui Feb 14, 2026
ffc6d93
feat(02-02): add block list UI, block detail view, and routing
vibegui Feb 14, 2026
8668b2f
feat(03-01): typed postMessage protocol and BlockInstance type
vibegui Feb 14, 2026
f48f5c7
feat(03-01): page composer layout, viewport toggle, and enhanced prev…
vibegui Feb 14, 2026
bad2c47
feat(03-02): section list sidebar with @dnd-kit sortable and block pi…
vibegui Feb 14, 2026
7662d8f
feat(03-02): wire composer with section sidebar, block picker, and Dn…
vibegui Feb 14, 2026
2e08aa5
feat(03-03): snapshot-based undo/redo hook with useReducer
vibegui Feb 14, 2026
3280303
feat(03-03): integrate undo/redo into page composer with keyboard sho…
vibegui Feb 14, 2026
4c8b010
feat(04-01): add LoaderDefinition types and discoverLoaders scanner
vibegui Feb 14, 2026
d06bedc
feat(04-01): add loader server tools, client API, and query keys
vibegui Feb 14, 2026
6bf04b0
feat(04-02): loaders list and detail views with router wiring
vibegui Feb 14, 2026
b71a254
feat(04-02): loader picker modal and prop binding in page composer
vibegui Feb 14, 2026
abad437
feat(05-03): starter template with React 19, Vite 7, Tailwind 4, and …
vibegui Feb 14, 2026
dc82ec1
feat(05-01): extend SITE_BINDING with branch tools and create branch API
vibegui Feb 14, 2026
898142e
feat(05-03): .deco scaffolding with pre-configured pages, blocks, and…
vibegui Feb 14, 2026
a9ef28d
feat(05-01): add branch switcher and publish bar UI components
vibegui Feb 14, 2026
9a74804
feat(05-02): extend SITE_BINDING with history tools and create server…
vibegui Feb 14, 2026
d1d3823
feat(05-02): add page history panel with diff view and revert integra…
vibegui Feb 14, 2026
e36e343
feat: site-editor routing, filesystem path resolution, and preview co…
vibegui Feb 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"marked": "^15.0.6",
"mesh-plugin-object-storage": "workspace:*",
"mesh-plugin-private-registry": "workspace:*",
"mesh-plugin-site-editor": "workspace:*",
"mesh-plugin-user-sandbox": "workspace:*",
"mesh-plugin-reports": "workspace:*",
"mesh-plugin-workflows": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions apps/mesh/src/server-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import type { ServerPlugin } from "@decocms/bindings/server-plugin";
import { serverPlugin as privateRegistryPlugin } from "mesh-plugin-private-registry/server";
import { serverPlugin as userSandboxPlugin } from "mesh-plugin-user-sandbox/server";
import { serverPlugin as siteEditorPlugin } from "mesh-plugin-site-editor/server";
import { serverPlugin as workflowsPlugin } from "mesh-plugin-workflows/server";

/**
Expand All @@ -21,4 +22,5 @@ export const serverPlugins: ServerPlugin[] = [
userSandboxPlugin,
privateRegistryPlugin,
workflowsPlugin,
siteEditorPlugin,
];
4 changes: 4 additions & 0 deletions apps/mesh/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as OrganizationTools from "./organization";
import * as ProjectTools from "./projects";
import * as TagTools from "./tags";
import * as ThreadTools from "./thread";
import * as FilesystemTools from "./filesystem";
import * as UserTools from "./user";
import { ToolName } from "./registry";

Expand Down Expand Up @@ -128,6 +129,9 @@ const CORE_TOOLS = [
ProjectTools.PROJECT_DELETE,
ProjectTools.PROJECT_PLUGIN_CONFIG_GET,
ProjectTools.PROJECT_PLUGIN_CONFIG_UPDATE,

// Filesystem tools
FilesystemTools.FILESYSTEM_PICK_DIRECTORY,
] as const satisfies { name: ToolName }[];

// Plugin tools - collected at startup, gated by org settings at runtime
Expand Down
13 changes: 12 additions & 1 deletion apps/mesh/src/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export type ToolCategory =
| "Event Bus"
| "Code Execution"
| "Tags"
| "Projects";
| "Projects"
| "Filesystem";

/**
* All tool names - keep in sync with ALL_TOOLS in index.ts
Expand Down Expand Up @@ -120,6 +121,8 @@ const ALL_TOOL_NAMES = [
"PROJECT_DELETE",
"PROJECT_PLUGIN_CONFIG_GET",
"PROJECT_PLUGIN_CONFIG_UPDATE",
// Filesystem tools
"FILESYSTEM_PICK_DIRECTORY",
] as const;

/**
Expand Down Expand Up @@ -535,6 +538,12 @@ export const MANAGEMENT_TOOLS: ToolMetadata[] = [
description: "Update project plugin configuration",
category: "Projects",
},
// Filesystem tools
{
name: "FILESYSTEM_PICK_DIRECTORY",
description: "Open native folder picker dialog",
category: "Filesystem",
},
];

/**
Expand Down Expand Up @@ -612,6 +621,7 @@ const TOOL_LABELS: Record<ToolName, string> = {
PROJECT_DELETE: "Delete project",
PROJECT_PLUGIN_CONFIG_GET: "View plugin config",
PROJECT_PLUGIN_CONFIG_UPDATE: "Update plugin config",
FILESYSTEM_PICK_DIRECTORY: "Pick directory",
};

// ============================================================================
Expand All @@ -635,6 +645,7 @@ export function getToolsByCategory() {
"Code Execution": [],
Tags: [],
Projects: [],
Filesystem: [],
};

for (const tool of MANAGEMENT_TOOLS) {
Expand Down
177 changes: 146 additions & 31 deletions apps/mesh/src/web/components/binding-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
*
* A reusable connection selector that filters connections by binding.
* Shows connection icons and supports inline installation from registry.
* When running locally, offers a "Choose local folder" option that creates
* a @modelcontextprotocol/server-filesystem STDIO connection.
*/

import { useConnections } from "@decocms/mesh-sdk";
import {
useConnections,
useConnectionActions,
useProjectContext,
useMCPClient,
SELF_MCP_ALIAS_ID,
} from "@decocms/mesh-sdk";
import { useBindingConnections } from "@/web/hooks/use-binding";
import { useInstallFromRegistry } from "@/web/hooks/use-install-from-registry";
import { Loading01, Plus } from "@untitledui/icons";
import { Folder, Loading01, Plus } from "@untitledui/icons";
import { Button } from "@deco/ui/components/button.tsx";
import {
Select,
Expand Down Expand Up @@ -59,11 +67,19 @@ export function BindingSelector({
disabled = false,
}: BindingSelectorProps) {
const [isLocalInstalling, setIsLocalInstalling] = useState(false);
const [isBrowsing, setIsBrowsing] = useState(false);
const { installByBinding, isInstalling: isGlobalInstalling } =
useInstallFromRegistry();

const isInstalling = isLocalInstalling || isGlobalInstalling;

const { org } = useProjectContext();
const { create } = useConnectionActions();
const selfClient = useMCPClient({
connectionId: SELF_MCP_ALIAS_ID,
orgId: org.id,
});

const allConnections = useConnections();

// Filter connections based on binding type
Expand Down Expand Up @@ -149,8 +165,82 @@ export function BindingSelector({
onAddNew?.();
};

// Get selected connection for display
const selectedConnection = connections.find((c) => c.id === value);
// Detect if binding requires object storage tools (LIST_OBJECTS, GET_PRESIGNED_URL, etc.)
const isObjectStorageBinding = (() => {
if (!binding) return false;
if (typeof binding === "string") {
return binding === "OBJECT_STORAGE";
}
if (Array.isArray(binding)) {
return binding.some(
(b) =>
b.name === "LIST_OBJECTS" ||
b.name === "GET_PRESIGNED_URL" ||
b.name === "PUT_PRESIGNED_URL",
);
}
return false;
})();

// Virtual connection ID for dev-assets (local object storage routed through mesh)
const devAssetsConnectionId = `${org.id}_dev-assets`;

const handleChooseLocalFolder = async () => {
setIsBrowsing(true);
try {
// Open native OS folder picker via the mesh server
const pickResult = (await selfClient.callTool({
name: "FILESYSTEM_PICK_DIRECTORY",
arguments: {},
})) as { structuredContent?: { path: string | null } };

const folderPath = pickResult.structuredContent?.path ?? null;
if (!folderPath) return;

const folderName =
folderPath.split("/").filter(Boolean).pop() ?? "folder";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Folder name extraction only splits on /, so Windows paths with backslashes produce incorrect titles. Split on both separators to derive the final folder name reliably.

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/binding-selector.tsx, line 201:

<comment>Folder name extraction only splits on `/`, so Windows paths with backslashes produce incorrect titles. Split on both separators to derive the final folder name reliably.</comment>

<file context>
@@ -149,8 +165,82 @@ export function BindingSelector({
+      if (!folderPath) return;
+
+      const folderName =
+        folderPath.split("/").filter(Boolean).pop() ?? "folder";
+
+      // For object storage bindings, use the local-object-storage bridge
</file context>
Suggested change
folderPath.split("/").filter(Boolean).pop() ?? "folder";
folderPath.split(/[/\\]/).filter(Boolean).pop() ?? "folder";
Fix with Cubic


// For object storage bindings, use the local-object-storage bridge
// For file/site bindings, use the standard filesystem MCP
const connectionConfig = isObjectStorageBinding
? {
title: `Local Files: ${folderName}`,
connection_type: "STDIO" as const,
connection_headers: {
command: "node",
args: [
"--experimental-strip-types",
"../../packages/mcp-local-object-storage/src/index.ts",
folderPath,
],
},
}
: {
title: `Local: ${folderName}`,
connection_type: "STDIO" as const,
connection_headers: {
command: "npx",
args: [
"-y",
"@modelcontextprotocol/server-filesystem",
folderPath,
],
},
};

const newConnection = await create.mutateAsync(
connectionConfig as Parameters<typeof create.mutateAsync>[0],
);

onValueChange(newConnection.id);
} catch {
// Error is handled by the mutation toast
} finally {
setIsBrowsing(false);
}
};

const busy = isInstalling || isBrowsing;

return (
<Select
Expand All @@ -159,30 +249,24 @@ export function BindingSelector({
disabled={disabled}
>
<SelectTrigger size="sm" className={cn("w-[200px]", className)}>
<SelectValue placeholder={placeholder}>
{selectedConnection ? (
<div className="flex items-center gap-2">
{selectedConnection.icon ? (
<img
src={selectedConnection.icon}
alt={selectedConnection.title}
className="w-4 h-4 rounded shrink-0"
/>
) : (
<div className="w-4 h-4 rounded bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center text-xs font-semibold text-primary shrink-0">
{selectedConnection.title.slice(0, 1).toUpperCase()}
</div>
)}
<span className="truncate">{selectedConnection.title}</span>
</div>
) : (
placeholder
)}
</SelectValue>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No connection</SelectItem>
{connections.length === 0 ? (
{isObjectStorageBinding && (
<SelectItem value={devAssetsConnectionId}>
<div className="flex flex-col">
<div className="flex items-center gap-2">
<Folder size={16} />
<span>Local Storage</span>
</div>
<span className="text-xs text-muted-foreground ml-6">
Store in your browser
</span>
</div>
</SelectItem>
)}
{connections.length === 0 && !isObjectStorageBinding ? (
<div className="px-2 py-4 text-center text-sm text-muted-foreground">
No compatible connections found
</div>
Expand All @@ -206,16 +290,47 @@ export function BindingSelector({
</SelectItem>
))
)}
{(onAddNew || canInstallInline) && (
<div className="border-t border-border">
<div className="border-t border-border flex flex-col">
{/* Choose local folder - always available when running locally */}
<Button
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleChooseLocalFolder();
}}
disabled={busy || disabled}
variant="ghost"
className="w-full justify-start gap-2 px-2 py-2 h-auto hover:bg-muted rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed"
type="button"
>
{isBrowsing ? (
<>
<Loading01 size={16} className="animate-spin" />
<span>Selecting folder...</span>
</>
) : (
<div className="flex flex-col items-start">
<div className="flex items-center gap-2">
<Folder size={16} />
<span>Local Files</span>
</div>
<span className="text-xs text-muted-foreground ml-6">
Store in your files
</span>
</div>
)}
</Button>
{/* Add connection / install from registry */}
{(onAddNew || canInstallInline) && (
<Button
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleCreateConnection();
}}
disabled={isInstalling || disabled}
disabled={busy || disabled}
variant="ghost"
className="w-full justify-start gap-2 px-2 py-2 h-auto hover:bg-muted rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed"
type="button"
Expand All @@ -228,12 +343,12 @@ export function BindingSelector({
) : (
<>
<Plus size={16} />
<span>Add Connection</span>
<span>Custom Connection</span>
</>
)}
</Button>
</div>
)}
)}
</div>
</SelectContent>
</Select>
);
Expand Down
Loading
Loading