Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 39 additions & 0 deletions apps/mesh/migrations/029-add-thread-virtual-mcp-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Migration: Add virtual_mcp_id to threads table
*
* Associates threads with the virtual MCP (agent) that was used when the thread was created.
* This allows threads to display the correct agent icon and filter threads by agent.
*/

import { Kysely } from "kysely";

export async function up(db: Kysely<unknown>): Promise<void> {
// Add virtual_mcp_id column to threads table
await db.schema
.alterTable("threads")
.addColumn("virtual_mcp_id", "text")
.execute();

// Create index for efficient filtering by virtual_mcp_id
await db.schema
.createIndex("idx_threads_virtual_mcp_id")
.on("threads")
.columns(["virtual_mcp_id"])
.execute();

// Create composite index for filtering by organization + virtual_mcp_id
await db.schema
.createIndex("idx_threads_org_virtual_mcp_id")
.on("threads")
.columns(["organization_id", "virtual_mcp_id"])
.execute();
}

export async function down(db: Kysely<unknown>): Promise<void> {
// Drop indexes first
await db.schema.dropIndex("idx_threads_org_virtual_mcp_id").execute();
await db.schema.dropIndex("idx_threads_virtual_mcp_id").execute();

// Drop column
await db.schema.alterTable("threads").dropColumn("virtual_mcp_id").execute();
}
2 changes: 2 additions & 0 deletions apps/mesh/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as migration025addmonitoringvirtualmcpid from "./025-add-monitoring-vir
import * as migration026restrictchildconnectiondelete from "./026-restrict-child-connection-delete.ts";
import * as migration027updatemanagementmcpurl from "./027-update-management-mcp-url.ts";
import * as migration028updatemanagementmcptoself from "./028-update-management-mcp-to-self.ts";
import * as migration029addthreadvirtualmcpid from "./029-add-thread-virtual-mcp-id.ts";

const migrations = {
"001-initial-schema": migration001initialschema,
Expand Down Expand Up @@ -59,6 +60,7 @@ const migrations = {
migration026restrictchildconnectiondelete,
"027-update-management-mcp-url": migration027updatemanagementmcpurl,
"028-update-management-mcp-to-self": migration028updatemanagementmcptoself,
"029-add-thread-virtual-mcp-id": migration029addthreadvirtualmcpid,
} satisfies Record<string, Migration>;

export default migrations;
2 changes: 2 additions & 0 deletions apps/mesh/src/api/routes/decopilot/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export async function processConversation(
messages: UIMessage<Metadata>[];
systemPrompts: string[];
removeFileParts?: boolean;
virtualMcpId?: string | null;
},
): Promise<ProcessedConversation> {
const userId = ensureUser(ctx);
Expand All @@ -47,6 +48,7 @@ export async function processConversation(
threadId: config.threadId,
userId,
defaultWindowSize: config.windowSize,
virtualMcpId: config.virtualMcpId,
});

// Load thread history
Expand Down
9 changes: 8 additions & 1 deletion apps/mesh/src/api/routes/decopilot/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export async function createMemory(
storage: ThreadStoragePort,
config: MemoryConfig,
): Promise<Memory> {
const { threadId, organizationId, userId, defaultWindowSize } = config;
const { threadId, organizationId, userId, defaultWindowSize, virtualMcpId } =
config;

let thread: Thread;

Expand All @@ -64,6 +65,7 @@ export async function createMemory(
thread = await storage.create({
id: generatePrefixedId("thrd"),
organizationId,
virtualMcpId: virtualMcpId ?? undefined,
createdBy: userId,
});
} else {
Expand All @@ -76,10 +78,15 @@ export async function createMemory(
thread = await storage.create({
id: existing ? generatePrefixedId("thrd") : threadId,
organizationId,
virtualMcpId: virtualMcpId ?? undefined,
createdBy: userId,
});
} else {
// If existing thread doesn't have virtualMcpId and we're providing one, update it
thread = existing;
if (virtualMcpId && !thread.virtualMcpId) {
thread = await storage.update(thread.id, { virtualMcpId });
}
}
}

Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/api/routes/decopilot/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ app.post("/:org/decopilot/stream", async (c) => {
messages,
systemPrompts: [DECOPILOT_BASE_PROMPT],
removeFileParts: !modelHasVision,
virtualMcpId: agent.id,
});

const shouldGenerateTitle = prunedMessages.length === 1;
Expand Down
3 changes: 3 additions & 0 deletions apps/mesh/src/api/routes/decopilot/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export interface MemoryConfig {

/** Default window size for pruning */
defaultWindowSize?: number;

/** Virtual MCP (Agent) ID if routed through an agent */
virtualMcpId?: string | null;
}

// ============================================================================
Expand Down
18 changes: 16 additions & 2 deletions apps/mesh/src/storage/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class SqlThreadStorage implements ThreadStoragePort {
organization_id: data.organizationId,
title: data.title,
description: data.description ?? null,
virtual_mcp_id: data.virtualMcpId ?? null,
created_at: now,
updated_at: now,
created_by: data.createdBy,
Expand Down Expand Up @@ -84,6 +85,9 @@ export class SqlThreadStorage implements ThreadStoragePort {
if (data.hidden !== undefined) {
updateData.hidden = data.hidden;
}
if (data.virtualMcpId !== undefined) {
updateData.virtual_mcp_id = data.virtualMcpId;
}

await this.db
.updateTable("threads")
Expand Down Expand Up @@ -231,8 +235,17 @@ export class SqlThreadStorage implements ThreadStoragePort {
updated_at: Date | string;
created_by: string;
updated_by: string | null;
hidden: boolean | null;
hidden: boolean | number | null;
virtual_mcp_id?: string | null;
}): Thread {
// Convert hidden from number (0/1) to boolean if needed (SQLite returns numbers)
const hidden =
row.hidden === null || row.hidden === undefined
? null
: typeof row.hidden === "number"
? row.hidden !== 0
: row.hidden;

return {
id: row.id,
organizationId: row.organization_id,
Expand All @@ -248,7 +261,8 @@ export class SqlThreadStorage implements ThreadStoragePort {
: row.updated_at.toISOString(),
createdBy: row.created_by,
updatedBy: row.updated_by,
hidden: row.hidden,
hidden,
virtualMcpId: row.virtual_mcp_id ?? undefined,
};
}

Expand Down
2 changes: 2 additions & 0 deletions apps/mesh/src/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ export interface ThreadTable {
title: string;
description: string | null;
hidden: boolean | null;
virtual_mcp_id: string | null; // Virtual MCP (Agent) ID if routed through an agent
created_at: ColumnType<Date, Date | string, never>;
updated_at: ColumnType<Date, Date | string, Date | string>;
created_by: string; // User ID;
Expand All @@ -614,6 +615,7 @@ export interface Thread {
createdBy: string;
updatedBy: string | null;
hidden: boolean | null;
virtualMcpId?: string | null; // Virtual MCP (Agent) ID if routed through an agent
}

export interface ThreadMessageTable {
Expand Down
1 change: 1 addition & 0 deletions apps/mesh/src/tools/thread/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const COLLECTION_THREADS_CREATE = defineTool({
organizationId: organization.id,
title: input.data.title,
description: input.data.description,
virtualMcpId: input.data.virtualMcpId ?? undefined,
createdBy: userId,
});

Expand Down
30 changes: 29 additions & 1 deletion apps/mesh/src/tools/thread/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,22 @@ export const COLLECTION_THREADS_LIST = defineTool({
outputSchema: ThreadListOutputSchema,

handler: async (input, ctx) => {
await ctx.access.check();
try {
await ctx.access.check();
} catch (error) {
// Debug: log access check error
if (process.env.NODE_ENV === "development") {
// eslint-disable-next-line no-console
console.error("[COLLECTION_THREADS_LIST] Access check failed:", {
error: error instanceof Error ? error.message : String(error),
userId: ctx.auth.user?.id,
hasUser: !!ctx.auth.user,
hasApiKey: !!ctx.auth.apiKey,
});
}
throw error;
}

const userId = ctx.auth.user?.id;
if (!userId) {
throw new Error("User ID required to list threads");
Expand All @@ -52,6 +67,19 @@ export const COLLECTION_THREADS_LIST = defineTool({
{ limit, offset },
);

// Debug: log query results
if (process.env.NODE_ENV === "development") {
// eslint-disable-next-line no-console
console.log("[COLLECTION_THREADS_LIST] Query:", {
organizationId: organization.id,
userId,
threadsFound: threads.length,
total,
offset,
limit,
});
}

const hasMore = offset + limit < total;

return {
Expand Down
9 changes: 9 additions & 0 deletions apps/mesh/src/tools/thread/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export const ThreadEntitySchema = z.object({
.string()
.nullable()
.describe("User ID who last updated the thread"),
virtualMcpId: z
.string()
.nullable()
.optional()
.describe("Virtual MCP (Agent) ID if routed through an agent"),
});

export type ThreadEntity = z.infer<typeof ThreadEntitySchema>;
Expand All @@ -60,6 +65,10 @@ export const ThreadCreateDataSchema = z.object({
id: z.string().optional().describe("Optional custom ID for the thread"),
title: z.string().describe("Thread title"),
description: z.string().nullish().describe("Thread description"),
virtualMcpId: z
.string()
.nullish()
.describe("Virtual MCP (Agent) ID if routed through an agent"),
});

export type ThreadCreateData = z.infer<typeof ThreadCreateDataSchema>;
Expand Down
2 changes: 1 addition & 1 deletion apps/mesh/src/web/components/chat/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ async function callUpdateThreadTool(
return payload.item;
}

const ChatContext = createContext<ChatContextValue | null>(null);
export const ChatContext = createContext<ChatContextValue | null>(null);

/**
* Provider component for chat context
Expand Down
9 changes: 5 additions & 4 deletions apps/mesh/src/web/components/chat/ice-breakers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,6 @@ export function IceBreakers({ className }: IceBreakersProps) {
const { selectedVirtualMcp } = useChat();
// When selectedVirtualMcp is null, it means default virtual MCP (id is null)
const connectionId = selectedVirtualMcp?.id ?? null;
// Use a stable key for ErrorBoundary (null becomes "default")
const errorBoundaryKey = connectionId ?? "default";

return (
<div
Expand All @@ -350,8 +348,11 @@ export function IceBreakers({ className }: IceBreakersProps) {
className,
)}
>
<ErrorBoundary key={errorBoundaryKey} fallback={null}>
<Suspense fallback={<IceBreakersFallback />}>
<ErrorBoundary fallback={null}>
<Suspense
key={connectionId ?? "default"}
fallback={<IceBreakersFallback />}
>
<IceBreakersContent connectionId={connectionId} />
</Suspense>
</ErrorBoundary>
Expand Down
15 changes: 14 additions & 1 deletion apps/mesh/src/web/components/chat/select-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { Skeleton } from "@deco/ui/components/skeleton.tsx";
import { cn } from "@deco/ui/lib/utils.ts";
import {
ChevronDown,
ChevronSelectorVertical,
CurrencyDollar,
File06,
Expand Down Expand Up @@ -336,7 +337,15 @@ function SelectedModelDisplay({
placeholder?: string;
}) {
if (!model) {
return <span className="text-sm text-muted-foreground">{placeholder}</span>;
return (
<div className="flex items-center gap-1.5">
<span className="text-sm text-muted-foreground">{placeholder}</span>
<ChevronDown
size={14}
className="text-muted-foreground opacity-50 shrink-0"
/>
</div>
);
}

return (
Expand All @@ -351,6 +360,10 @@ function SelectedModelDisplay({
<span className="text-sm text-muted-foreground group-hover:text-foreground transition-colors truncate min-w-0 hidden sm:inline-block">
{model.title}
</span>
<ChevronDown
size={14}
className="text-muted-foreground opacity-50 shrink-0"
/>
</div>
);
}
Expand Down
Loading