Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
759f1b7
feat(home): add filesystem UX with folders and artifact browser
rafavalls Apr 8, 2026
a94362b
feat(home): add quick actions, all-files view, and search
rafavalls Apr 8, 2026
944cc43
feat(home): replace agents list with action items + files section
rafavalls Apr 8, 2026
eae8f74
feat(home): restructure around projects, show all tasks with labels
rafavalls Apr 8, 2026
fb2af87
feat(tasks): unified task panel, sidebar settings hover
rafavalls Apr 8, 2026
e4f9acc
feat(settings): relabel connections as agents inside projects
rafavalls Apr 8, 2026
403b23c
feat: implement Project > Agents > Connections hierarchy
rafavalls Apr 8, 2026
dece983
feat: separate projects from agents via metadata.type flag
rafavalls Apr 8, 2026
0e9f085
feat: separate project and agent views with file browser
rafavalls Apr 8, 2026
ed3316a
feat(home): quick actions create projects with agents inside
rafavalls Apr 8, 2026
3fff996
feat(home): add "New Slides" one-click project creation
rafavalls Apr 8, 2026
87ab3de
feat(layout): reorder panels [Tasks | Chat | Content] with toolbar
rafavalls Apr 8, 2026
c561c1d
feat(layout): remove content border-radius below toolbar, add Files icon
rafavalls Apr 8, 2026
246b6be
feat: thread toolInput from project metadata to MCP app renderer
rafavalls Apr 8, 2026
910062a
feat(settings): fix toolbar icons, add Layout section to project sett…
rafavalls Apr 8, 2026
387440d
fix(layout): Files icon opens project file browser, not the tool UI
rafavalls Apr 8, 2026
8702c45
feat: scope agent selector to project's agents only
rafavalls Apr 8, 2026
6de19af
fix: Files is now the default project view, toolbar matches top bar s…
rafavalls Apr 8, 2026
d6b4eed
feat(tasks): project icons on tasks, filter/group by project or status
rafavalls Apr 8, 2026
7b5394a
fix: swap FolderClosed (blocked icon) for Folder (plain) in toolbar
rafavalls Apr 8, 2026
fc9fae9
fix: task click navigates to project, labels show projects not agents
rafavalls Apr 8, 2026
195ac45
fix: clicking a task restores the project's UI view
rafavalls Apr 8, 2026
90dc4fc
feat: per-task layout persistence (chat + content panel state)
rafavalls Apr 8, 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
135 changes: 135 additions & 0 deletions apps/mesh/scripts/seed-slide-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Seed script: Creates a Slide Maker connection, wraps it in an agent,
* wraps that in a project, and sets the slide_maker tool as the default UI.
*
* Run: bun run --cwd apps/mesh scripts/seed-slide-project.ts
*
* Prerequisites: dev server must be running (bun run dev)
*/

const BASE_URL = "http://localhost:4000";

async function getSession(): Promise<{
token: string;
orgId: string;
orgSlug: string;
}> {
// Get current session
const res = await fetch(`${BASE_URL}/api/auth/get-session`, {
headers: { Accept: "application/json" },
credentials: "include",
});
if (!res.ok) throw new Error("Not logged in. Open the app first and log in.");
const session = (await res.json()) as {
session: { token: string };
user: { id: string };
};

// Get active org
const orgRes = await fetch(
`${BASE_URL}/api/auth/organization/get-full-organization`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session.session.token}`,
},
body: "{}",
},
);

if (!orgRes.ok) {
// Try listing orgs
const listRes = await fetch(
`${BASE_URL}/api/auth/organization/list-organizations`,
{
method: "GET",
headers: {
Authorization: `Bearer ${session.session.token}`,
},
},
);
const orgs = (await listRes.json()) as Array<{
id: string;
slug: string;
}>;
if (orgs.length === 0) throw new Error("No organizations found");
return {
token: session.session.token,
orgId: orgs[0].id,
orgSlug: orgs[0].slug,
};
}

const org = (await orgRes.json()) as { id: string; slug: string };
return {
token: session.session.token,
orgId: org.id,
orgSlug: org.slug,
};
}

async function callTool(
token: string,
orgId: string,
toolName: string,
args: Record<string, unknown>,
) {
const res = await fetch(`${BASE_URL}/api/mcp/self`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
"x-mesh-org-id": orgId,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: crypto.randomUUID(),
method: "tools/call",
params: { name: toolName, arguments: args },
}),
});

if (!res.ok) {
const text = await res.text();
throw new Error(`Tool call failed (${res.status}): ${text}`);
}

const json = (await res.json()) as {
result?: { structuredContent?: unknown };
};
return json.result?.structuredContent ?? json.result;
}

async function main() {
console.log("Getting session...");

// Since we can't easily get the session token from a script,
// let's use the MCP endpoint directly through the internal API.
// The dev server should have the embedded postgres running.

// Actually, let's just use the Kysely DB directly since we're in the same codebase.
console.log(
"\nThis script needs the dev server running. Instead of API calls,",
);
console.log("let's create the data through the UI:\n");
console.log("1. Open Studio in your browser (http://localhost:4000)");
console.log("2. Go to Settings > Connections");
console.log("3. Click 'Create connection' and fill in:");
console.log(" - Title: Slide Maker");
console.log(" - Type: HTTP");
console.log(" - URL: https://slide-maker.decocms.com/api/mcp");
console.log(" - Token: 9c8ed79c-4e23-4ca8-9f22-257afff0aee5");
console.log("4. Save the connection");
console.log("5. Go to Settings > Agents (or create a new agent)");
console.log(" - Name it 'Slide Maker'");
console.log(" - Add the Slide Maker connection to it");
console.log("6. Go home > New Project or click '+' in sidebar");
console.log(" - Name it 'My Slides'");
console.log(" - In project settings, add the Slide Maker agent");
console.log(
"\nThe slide_maker tool UI will be available in the agent's ext-apps view.",
);
}

main().catch(console.error);
24 changes: 19 additions & 5 deletions apps/mesh/src/web/components/chat/select-virtual-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { cn } from "@deco/ui/lib/utils.ts";
import { isDecopilot, type VirtualMCPEntity } from "@decocms/mesh-sdk";
import { useVirtualMCPs } from "@decocms/mesh-sdk";
import { useProjectScope } from "@/web/providers/project-scope";
import { Check, Loading01, SearchMd, Users03 } from "@untitledui/icons";
import {
Suspense,
Expand Down Expand Up @@ -79,6 +80,8 @@ export interface VirtualMCPPopoverContentProps {
selectedVirtualMcpId?: string | null;
onVirtualMcpChange: (virtualMcpId: string | null) => void;
searchInputRef?: RefObject<HTMLInputElement | null>;
/** If provided, only show agents with these IDs (project scoping) */
scopeToIds?: Set<string>;
}

function PopoverContentLoading() {
Expand All @@ -101,6 +104,7 @@ function VirtualMCPPopoverContentInner({
selectedVirtualMcpId,
onVirtualMcpChange,
searchInputRef,
scopeToIds: scopeToIdsProp,
}: VirtualMCPPopoverContentProps) {
const virtualMcps = useVirtualMCPs();
const [searchTerm, setSearchTerm] = useState("");
Expand All @@ -110,17 +114,27 @@ function VirtualMCPPopoverContentInner({
navigateOnCreate: true,
});

// Filter virtual MCPs based on search term and exclude Decopilot
// Auto-scope to project agents if inside a project
const projectScope = useProjectScope();
const scopeToIds = scopeToIdsProp ?? projectScope?.agentIds;

// Filter virtual MCPs: exclude Decopilot, scope to project agents if provided
const filteredVirtualMcps = (() => {
// First filter out Decopilot
const nonDecopilotMcps = virtualMcps.filter(
let mcps = virtualMcps.filter(
(virtualMcp) => !virtualMcp.id || !isDecopilot(virtualMcp.id),
);

if (!searchTerm.trim()) return nonDecopilotMcps;
// If scoped to a project, only show those specific agents
if (scopeToIds) {
mcps = mcps.filter(
(virtualMcp) => virtualMcp.id != null && scopeToIds.has(virtualMcp.id),
);
}

if (!searchTerm.trim()) return mcps;

const search = searchTerm.toLowerCase();
return nonDecopilotMcps.filter((virtualMcp) => {
return mcps.filter((virtualMcp) => {
return (
virtualMcp.title.toLowerCase().includes(search) ||
virtualMcp.description?.toLowerCase().includes(search)
Expand Down
35 changes: 16 additions & 19 deletions apps/mesh/src/web/components/chat/side-panel-chat.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AgentsList } from "@/web/components/home/agents-list.tsx";
import { QuickActions } from "@/web/components/home/quick-actions.tsx";
import { ImportFromDecoDialog } from "@/web/components/import-from-deco-dialog.tsx";
import { IntegrationIcon } from "@/web/components/integration-icon";
import { authClient } from "@/web/lib/auth-client";
Expand Down Expand Up @@ -111,7 +111,7 @@ function HomeEmptyState({
</div>
{/* Agents above input at bottom */}
<div className="w-full flex flex-col gap-4 pb-4">
<AgentsList />
<QuickActions />
<Chat.Input onOpenContextPanel={onOpenContextPanel} />
</div>
{isDecoUser && (
Expand All @@ -127,29 +127,26 @@ function HomeEmptyState({

return (
<>
<div className="flex-1 relative flex flex-col items-center px-10">
<div className="flex-1 flex flex-col items-center justify-center w-full">
<div className="flex flex-col items-center w-full max-w-[672px]">
<div className="text-center mb-10">
<p className="text-3xl font-medium text-foreground">
What's on your mind, {userName}?
</p>
</div>
<div className="w-full">
<Chat.Input onOpenContextPanel={onOpenContextPanel} />
</div>
<div className="flex-1 relative flex flex-col items-center overflow-y-auto px-10 pt-[25vh]">
<div className="flex flex-col items-center w-full max-w-[672px]">
<div className="text-center mb-10">
<p className="text-3xl font-medium text-foreground">
What's on your mind, {userName}?
</p>
</div>
<div className="w-full mt-10 mx-auto">
<AgentsList />
<div className="w-full">
<Chat.Input onOpenContextPanel={onOpenContextPanel} />
</div>
</div>
<div className="w-full mt-10 mx-auto">
<QuickActions />
</div>
{isDecoUser && (
<div className="absolute bottom-6 left-0 right-0 px-10">
<div className="w-full max-w-[500px] mx-auto">
<ImportDecoSiteBanner onClick={() => setImportOpen(true)} />
</div>
<div className="w-full max-w-[500px] mx-auto mt-6">
<ImportDecoSiteBanner onClick={() => setImportOpen(true)} />
</div>
)}
<div className="min-h-6" />
</div>
<ImportFromDecoDialog open={importOpen} onOpenChange={setImportOpen} />
</>
Expand Down
Loading
Loading