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
23 changes: 23 additions & 0 deletions apps/mesh/src/api/routes/decopilot/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,26 @@ Example output: Database Connection Setup

Example input: "What tools are available?"
Example output: Available Tools Overview`;

export const DESCRIPTION_GENERATOR_PROMPT = `Your task: Summarize what the AI assistant did or said in 8-15 words.

Rules:
- Output ONLY the summary, nothing else
- No quotes, no punctuation at the end
- No explanations, no "Summary:" prefix
- Use past tense for completed actions
- If the assistant asked the user a question, include the question topic
- Be specific and descriptive, not generic
- Just the raw summary text

Example input: "I've created the database migration files and updated the schema to include the new columns you requested."
Example output: Created database migration files and updated schema with new columns

Example input: "Here's a poem I wrote about autumn. What feeling or mood does this poem evoke for you?"
Example output: Wrote an autumn poem and asking what mood it evokes

Example input: "I encountered an error: ECONNREFUSED when trying to connect to the Slack API."
Example output: Failed to connect to Slack API due to connection refused error

Example input: "I've summarized 17 Slack threads and organized them into your Notion workspace."
Example output: Summarized 17 Slack threads into Notion workspace`;
67 changes: 67 additions & 0 deletions apps/mesh/src/api/routes/decopilot/description-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Decopilot Description Generator
*
* Generates short thread descriptions in the background using LLM.
* Mirrors the title-generator pattern.
*/

import type { LanguageModelV2 } from "@ai-sdk/provider";
import { generateText } from "ai";

import { DESCRIPTION_GENERATOR_PROMPT } from "./constants";

const DESCRIPTION_TIMEOUT_MS = 3000;

export async function generateDescriptionInBackground(config: {
model: LanguageModelV2;
assistantText: string;
}): Promise<string | null> {
const { model, assistantText } = config;

if (!assistantText || assistantText.trim().length < 10) {
return null;
}

const abortController = new AbortController();

const timeoutId = setTimeout(() => {
abortController.abort();
}, DESCRIPTION_TIMEOUT_MS);

try {
const result = await generateText({
model,
system: DESCRIPTION_GENERATOR_PROMPT,
messages: [{ role: "user", content: assistantText.slice(0, 2000) }],
maxOutputTokens: 80,
temperature: 0.2,
abortSignal: abortController.signal,
});

const rawDescription = result.text.trim();
const firstLine = rawDescription.split("\n")[0] ?? rawDescription;
const description = firstLine
.replace(/^["']|["']$/g, "")
.replace(/^(Summary:|summary:|Description:|description:)\s*/i, "")
.replace(/[.!?]$/, "")
.slice(0, 100)
.trim();

return description || null;
} catch (error) {
const err = error as Error;
if (err.name === "AbortError") {
console.warn(
"[decopilot:description] Description generation aborted (timeout)",
);
} else {
console.error(
"[decopilot:description] Failed to generate description:",
err.message,
);
}
return null;
} finally {
clearTimeout(timeoutId);
}
}
31 changes: 31 additions & 0 deletions apps/mesh/src/api/routes/decopilot/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from "./model-permissions";
import { createModelProviderFromClient } from "./model-provider";
import { StreamRequestSchema } from "./schemas";
import { generateDescriptionInBackground } from "./description-generator";
import { resolveThreadStatus } from "./status";
import { genTitle } from "./title-generator";
import type { ChatMessage } from "./types";
Expand Down Expand Up @@ -402,6 +403,36 @@ app.post("/:org/decopilot/stream", async (c) => {
error,
);
});

// Generate description from assistant response (fire-and-forget)
const assistantText = (responseMessage?.parts ?? [])
.filter(
(p): p is { type: "text"; text: string } =>
"type" in p && p.type === "text" && "text" in p,
)
.map((p) => p.text)
.join(" ")
.trim();

if (assistantText) {
generateDescriptionInBackground({
model: modelProvider.fastModel ?? modelProvider.thinkingModel,
assistantText,
})
.then(async (description) => {
if (description) {
await ctx.storage.threads.update(mem.thread.id, {
description,
});
}
})
.catch((error) => {
console.error(
"[decopilot:stream] Error generating description",
error,
);
});
}
},
}),
);
Expand Down
13 changes: 9 additions & 4 deletions apps/mesh/src/mcp-clients/virtual-mcp/passthrough-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ function createLazyClient(

function getRealClient(): Promise<Client> {
if (!realClientPromise) {
realClientPromise = clientFromConnection(connection, ctx, superUser).then(
(client) => {
realClientPromise = clientFromConnection(connection, ctx, superUser)
.then((client) => {
// Apply streaming support for HTTP connections so callStreamableTool
// can stream responses via direct fetch instead of MCP transport
if (
Expand All @@ -99,8 +99,13 @@ function createLazyClient(
);
}
return client;
},
);
})
.catch((err) => {
// Clear the cached promise so the next call can retry instead of
// permanently returning the same rejected promise.
realClientPromise = null;
throw err;
});
}
return realClientPromise;
}
Expand Down
16 changes: 5 additions & 11 deletions apps/mesh/src/web/components/chat/side-panel-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getWellKnownDecopilotVirtualMCP,
useProjectContext,
} from "@decocms/mesh-sdk";
import { ClockRewind, Plus, Users03, X } from "@untitledui/icons";
import { CheckDone01, Plus, Users03, X } from "@untitledui/icons";
import { Suspense, useState, useTransition } from "react";
import { ErrorBoundary } from "../error-boundary";
import { Chat, useChat } from "./index";
Expand All @@ -22,7 +22,6 @@ function ChatPanelContent() {
isChatEmpty,
activeThreadId,
createThread,
switchToThread,
threads,
} = useChat();
const activeThread = threads.find((thread) => thread.id === activeThreadId);
Expand Down Expand Up @@ -115,9 +114,9 @@ function ChatPanelContent() {
type="button"
onClick={() => setShowThreadsOverlay(true)}
className="flex size-6 items-center justify-center rounded-full p-1 hover:bg-transparent group cursor-pointer"
title="Chat history"
title="Tasks"
>
<ClockRewind
<CheckDone01
size={16}
className="text-muted-foreground group-hover:text-foreground transition-colors"
/>
Expand Down Expand Up @@ -169,7 +168,7 @@ function ChatPanelContent() {
</Chat.Footer>
</div>

{/* Threads view */}
{/* Tasks view */}
<div
className={cn(
"absolute inset-0 flex flex-col transition-all duration-300 ease-in-out",
Expand All @@ -178,12 +177,7 @@ function ChatPanelContent() {
: "opacity-0 translate-x-4 pointer-events-none",
)}
>
<ThreadsView
threads={threads}
activeThreadId={activeThreadId}
onThreadSelect={switchToThread}
onClose={() => setShowThreadsOverlay(false)}
/>
<ThreadsView onClose={() => setShowThreadsOverlay(false)} />
</div>
</Chat>
);
Expand Down
Loading