Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
4.8 kB · 129 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130/** * Session title generation via Haiku. * * Standalone module with minimal dependencies so it can be imported from * print.ts (SDK control request handler) without pulling in the React/chalk/ * git dependency chain that teleport.tsx carries. * * This is the single source of truth for AI-generated session titles across * all surfaces. Previously there were separate Haiku title generators: * - teleport.tsx generateTitleAndBranch (6-word title + branch for CCR) * - rename/generateSessionName.ts (kebab-case name for /rename) * Each remains for backwards compat; new callers should use this module. */
import { z } from 'zod/v4'import { getIsNonInteractiveSession } from '../bootstrap/state.js'import { logEvent } from '../services/analytics/index.js'import { queryHaiku } from '../services/api/claude.js'import type { Message } from '../types/message.js'import { logForDebugging } from './debug.js'import { safeParseJSON } from './json.js'import { lazySchema } from './lazySchema.js'import { extractTextContent } from './messages.js'import { asSystemPrompt } from './systemPromptType.js'
const MAX_CONVERSATION_TEXT = 1000
/** * Flatten a message array into a single text string for Haiku title input. * Skips meta/non-human messages. Tail-slices to the last 1000 chars so * recent context wins when the conversation is long. */export function extractConversationText(messages: Message[]): string { const parts: string[] = [] for (const msg of messages) { if (msg.type !== 'user' && msg.type !== 'assistant') continue if ('isMeta' in msg && msg.isMeta) continue if ('origin' in msg && msg.origin && msg.origin.kind !== 'human') continue const content = msg.message.content if (typeof content === 'string') { parts.push(content) } else if (Array.isArray(content)) { for (const block of content) { if ('type' in block && block.type === 'text' && 'text' in block) { parts.push(block.text as string) } } } } const text = parts.join('\n') return text.length > MAX_CONVERSATION_TEXT ? text.slice(-MAX_CONVERSATION_TEXT) : text}
const 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.
Return JSON with a single "title" field.
Good examples:{"title": "Fix login button on mobile"}{"title": "Add OAuth authentication"}{"title": "Debug failing CI tests"}{"title": "Refactor API client error handling"}
Bad (too vague): {"title": "Code changes"}Bad (too long): {"title": "Investigate and fix the issue where the login button does not respond on mobile devices"}Bad (wrong case): {"title": "Fix Login Button On Mobile"}`
const titleSchema = lazySchema(() => z.object({ title: z.string() }))
/** * Generate a sentence-case session title from a description or first message. * Returns null on error or if Haiku returns an unparseable response. * * @param description - The user's first message or a description of the session * @param signal - Abort signal for cancellation */export async function generateSessionTitle( description: string, signal: AbortSignal,): Promise<string | null> { const trimmed = description.trim() if (!trimmed) return null
try { const result = await queryHaiku({ systemPrompt: asSystemPrompt([SESSION_TITLE_PROMPT]), userPrompt: trimmed, outputFormat: { type: 'json_schema', schema: { type: 'object', properties: { title: { type: 'string' }, }, required: ['title'], additionalProperties: false, }, }, signal, options: { querySource: 'generate_session_title', agents: [], // Reflect the actual session mode — this module is called from // both the SDK print path (non-interactive) and the CCR remote // session path via useRemoteSession (interactive). isNonInteractiveSession: getIsNonInteractiveSession(), hasAppendSystemPrompt: false, mcpTools: [], }, })
const text = extractTextContent(result.message.content)
const parsed = titleSchema().safeParse(safeParseJSON(text)) const title = parsed.success ? parsed.data.title.trim() || null : null
logEvent('tengu_session_title_generated', { success: title !== null })
return title } catch (error) { logForDebugging(`generateSessionTitle failed: ${error}`, { level: 'error', }) logEvent('tengu_session_title_generated', { success: false }) return null }}