From 68e296278c2eabe4585f7a938a928785e1c8fa3e Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 3 Mar 2026 23:10:46 +0000 Subject: [PATCH] feat(chat): add auto-title generation after first exchange (M8.5) After the first AI response, sends a background request to generate a 3-5 word title using Claude Haiku. Silently replaces the default title derived from the first user message. Co-Authored-By: Claude Opus 4.6 --- packages/app/src/composables/useAI.ts | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/app/src/composables/useAI.ts b/packages/app/src/composables/useAI.ts index 441e762b..4b047833 100644 --- a/packages/app/src/composables/useAI.ts +++ b/packages/app/src/composables/useAI.ts @@ -281,6 +281,51 @@ function formatWebSearchContext(results: { title: string; url: string; content?: let currentAbort: AbortController | null = null +/** Background title generation after first exchange */ +async function generateAutoTitle(conversationId: string) { + const chatStore = useChatStore() + const conv = chatStore.conversations.get(conversationId) + if (!conv) return + + // Only auto-title after first exchange (1 user + 1 assistant message) + if (conv.messages.length !== 2) return + const userMsg = conv.messages[0] + if (userMsg.role !== 'user') return + + // Skip if title was manually set (not auto-generated from first message) + const autoTitle = userMsg.content.slice(0, 60) + (userMsg.content.length > 60 ? '...' : '') + if (conv.title !== autoTitle) return + + try { + const headers: Record = { 'Content-Type': 'application/json' } + const vaultKey = await getApiKey('claude') + if (vaultKey) headers['x-api-key'] = vaultKey + + const res = await fetch(CLAUDE_PATH, { + method: 'POST', + headers, + body: JSON.stringify({ + model: 'claude-haiku-4.5', + system: 'You generate very short conversation titles. Respond with ONLY a 3-5 word title, no quotes, no punctuation at the end.', + messages: [{ role: 'user', content: `Title this conversation: "${userMsg.content.slice(0, 200)}"` }], + max_tokens: 20, + stream: false, + }), + }) + + if (!res.ok) return + + const data = await res.json() + const title = data?.content?.[0]?.text?.trim() + if (title && title.length > 0 && title.length < 60) { + conv.title = title + conv.updatedAt = Date.now() + } + } catch { + // Silent fail — title stays as default + } +} + export function useAI() { const chatStore = useChatStore() @@ -353,6 +398,9 @@ export function useAI() { currentAbort = null chatStore.isStreaming = false } + + // Auto-title: generate a short title after first exchange + generateAutoTitle(cid) } /**