source dump of claude code
at main 129 lines 4.8 kB view raw
1/** 2 * Session title generation via Haiku. 3 * 4 * Standalone module with minimal dependencies so it can be imported from 5 * print.ts (SDK control request handler) without pulling in the React/chalk/ 6 * git dependency chain that teleport.tsx carries. 7 * 8 * This is the single source of truth for AI-generated session titles across 9 * all surfaces. Previously there were separate Haiku title generators: 10 * - teleport.tsx generateTitleAndBranch (6-word title + branch for CCR) 11 * - rename/generateSessionName.ts (kebab-case name for /rename) 12 * Each remains for backwards compat; new callers should use this module. 13 */ 14 15import { z } from 'zod/v4' 16import { getIsNonInteractiveSession } from '../bootstrap/state.js' 17import { logEvent } from '../services/analytics/index.js' 18import { queryHaiku } from '../services/api/claude.js' 19import type { Message } from '../types/message.js' 20import { logForDebugging } from './debug.js' 21import { safeParseJSON } from './json.js' 22import { lazySchema } from './lazySchema.js' 23import { extractTextContent } from './messages.js' 24import { asSystemPrompt } from './systemPromptType.js' 25 26const MAX_CONVERSATION_TEXT = 1000 27 28/** 29 * Flatten a message array into a single text string for Haiku title input. 30 * Skips meta/non-human messages. Tail-slices to the last 1000 chars so 31 * recent context wins when the conversation is long. 32 */ 33export function extractConversationText(messages: Message[]): string { 34 const parts: string[] = [] 35 for (const msg of messages) { 36 if (msg.type !== 'user' && msg.type !== 'assistant') continue 37 if ('isMeta' in msg && msg.isMeta) continue 38 if ('origin' in msg && msg.origin && msg.origin.kind !== 'human') continue 39 const content = msg.message.content 40 if (typeof content === 'string') { 41 parts.push(content) 42 } else if (Array.isArray(content)) { 43 for (const block of content) { 44 if ('type' in block && block.type === 'text' && 'text' in block) { 45 parts.push(block.text as string) 46 } 47 } 48 } 49 } 50 const text = parts.join('\n') 51 return text.length > MAX_CONVERSATION_TEXT 52 ? text.slice(-MAX_CONVERSATION_TEXT) 53 : text 54} 55 56const SESSION_TITLE_PROMPT = `Generate a concise, sentence-case title (3-7 words) that captures the main topic or goal of this coding session. The title should be clear enough that the user recognizes the session in a list. Use sentence case: capitalize only the first word and proper nouns. 57 58Return JSON with a single "title" field. 59 60Good examples: 61{"title": "Fix login button on mobile"} 62{"title": "Add OAuth authentication"} 63{"title": "Debug failing CI tests"} 64{"title": "Refactor API client error handling"} 65 66Bad (too vague): {"title": "Code changes"} 67Bad (too long): {"title": "Investigate and fix the issue where the login button does not respond on mobile devices"} 68Bad (wrong case): {"title": "Fix Login Button On Mobile"}` 69 70const titleSchema = lazySchema(() => z.object({ title: z.string() })) 71 72/** 73 * Generate a sentence-case session title from a description or first message. 74 * Returns null on error or if Haiku returns an unparseable response. 75 * 76 * @param description - The user's first message or a description of the session 77 * @param signal - Abort signal for cancellation 78 */ 79export async function generateSessionTitle( 80 description: string, 81 signal: AbortSignal, 82): Promise<string | null> { 83 const trimmed = description.trim() 84 if (!trimmed) return null 85 86 try { 87 const result = await queryHaiku({ 88 systemPrompt: asSystemPrompt([SESSION_TITLE_PROMPT]), 89 userPrompt: trimmed, 90 outputFormat: { 91 type: 'json_schema', 92 schema: { 93 type: 'object', 94 properties: { 95 title: { type: 'string' }, 96 }, 97 required: ['title'], 98 additionalProperties: false, 99 }, 100 }, 101 signal, 102 options: { 103 querySource: 'generate_session_title', 104 agents: [], 105 // Reflect the actual session mode — this module is called from 106 // both the SDK print path (non-interactive) and the CCR remote 107 // session path via useRemoteSession (interactive). 108 isNonInteractiveSession: getIsNonInteractiveSession(), 109 hasAppendSystemPrompt: false, 110 mcpTools: [], 111 }, 112 }) 113 114 const text = extractTextContent(result.message.content) 115 116 const parsed = titleSchema().safeParse(safeParseJSON(text)) 117 const title = parsed.success ? parsed.data.title.trim() || null : null 118 119 logEvent('tengu_session_title_generated', { success: title !== null }) 120 121 return title 122 } catch (error) { 123 logForDebugging(`generateSessionTitle failed: ${error}`, { 124 level: 'error', 125 }) 126 logEvent('tengu_session_title_generated', { success: false }) 127 return null 128 } 129}