{"content":"# Automatic Chat Naming\n\nGenerate descriptive chat titles from the first message using a fast LLM. Runs as a background workflow step after the main response to avoid delaying the experience.\n\n## The Naming Step\n\nCreate a workflow step that generates a chat title using a fast model:\n\n```ts\n// src/workflows/chat/steps/name-chat.ts\nimport { generateText } from \"ai\";\nimport { db } from \"@/lib/db/client\";\nimport { chats } from \"@/lib/chat/schema\";\nimport { eq } from \"drizzle-orm\";\nimport { logger } from \"@/lib/logging/logger\";\nimport type { ChatAgentUIMessage } from \"../types\";\n\nconst DEFAULT_TITLE = \"New chat\";\n\nconst namingSystemPrompt = `You are a chat naming assistant. Given a user's message, generate a short, descriptive title for the conversation.\n\nRules:\n- Keep it under 50 characters\n- Be concise and descriptive\n- Don't use quotes or special formatting\n- Focus on the main topic or intent\n- Use title case\n\nRespond with ONLY the title, nothing else.`;\n\nfunction extractUserMessageText(message: ChatAgentUIMessage): string | null {\n  for (const part of message.parts) {\n    if (part.type === \"text\" && \"text\" in part) {\n      return part.text;\n    }\n  }\n  return null;\n}\n\n/**\n * Generates a name for a chat if it still has the default title.\n * Uses a fast model to create a descriptive title based on the user's message.\n */\nexport async function nameChatStep(\n  chatId: string,\n  userMessage: ChatAgentUIMessage,\n): Promise<void> {\n  \"use step\";\n\n  const userMessageText = extractUserMessageText(userMessage);\n  if (!userMessageText) {\n    return;\n  }\n\n  const chat = await db.query.chats.findFirst({\n    where: eq(chats.id, chatId),\n    columns: { title: true },\n  });\n\n  if (!chat || chat.title !== DEFAULT_TITLE) {\n    logger.debug(\n      { chatId, title: chat?.title },\n      \"Chat already named, skipping\",\n    );\n    return;\n  }\n\n  try {\n    const { text } = await generateText({\n      model: \"google/gemini-2.5-flash\",\n      system: namingSystemPrompt,\n      prompt: userMessageText,\n    });\n\n    const newTitle = text.trim().slice(0, 100);\n\n    if (newTitle) {\n      await db\n        .update(chats)\n        .set({ title: newTitle, updatedAt: new Date() })\n        .where(eq(chats.id, chatId));\n\n      logger.info({ chatId, newTitle }, \"Chat renamed\");\n    }\n  } catch (error) {\n    logger.error({ error, chatId }, \"Failed to generate chat name\");\n  }\n}\n```\n\nKey design decisions:\n\n- **Fast model**: Uses `gemini-2.5-flash` for quick, cheap title generation\n- **Idempotent**: Only renames chats with the default \"New chat\" title\n- **Non-blocking**: Errors are logged but don't break the workflow\n- **Truncated**: Limits title to 100 characters as a safety measure\n\n## Adding to the Workflow\n\nImport and call the naming step at the end of your chat workflow, after the main response is complete:\n\n```ts\n// src/workflows/chat/index.ts\nimport { nameChatStep } from \"./steps/name-chat\";\n\nexport async function chatWorkflow({\n  chatId,\n  userMessage,\n}: {\n  chatId: string;\n  userMessage: ChatAgentUIMessage;\n}) {\n  \"use workflow\";\n\n  // ... existing workflow steps ...\n\n  await removeRunId(messageId);\n\n  // Name the chat after the response is complete\n  await nameChatStep(chatId, userMessage);\n}\n```\n\nThe naming step runs after `removeRunId`, meaning:\n\n1. The AI response stream is already finished\n2. The user sees the response without waiting for naming\n3. The chat title updates in the background\n\n## How It Works\n\n1. User sends their first message to a new chat\n2. Workflow processes the message and streams the AI response\n3. After the response completes, `nameChatStep` runs\n4. The step checks if the chat still has the default title\n5. If so, it generates a new title from the user's message\n6. The chat title is updated in the database\n\nThe next time the user views their chat list, they'll see the descriptive title instead of \"New chat\"."}