Download src/server/services/titleService.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 8.53 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/services/titleService.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/services/titleService.ts
-
curl -L -o titleService.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/services/titleService.ts
8.53 kB
| /** | |
| * Title Service — AI-powered session title generation | |
| * | |
| * Two-stage approach matching the CLI: | |
| * 1. deriveTitle() — instant placeholder from first user message | |
| * 2. generateTitle() — async Haiku call for a polished 3-7 word title | |
| */ | |
| import { ProviderService } from './providerService.js' | |
| import { SettingsService } from './settingsService.js' | |
| import { sessionService } from './sessionService.js' | |
| import { hahaOpenAIOAuthService } from './hahaOpenAIOAuthService.js' | |
| import { isOpenAIOfficialProviderId } from './openaiOfficialProvider.js' | |
| import { OPENAI_CODEX_API_ENDPOINT } from '../../services/openaiAuth/client.js' | |
| import { resolveOpenAICodexModel } from '../../services/openaiAuth/models.js' | |
| import { anthropicToOpenaiResponses } from '../proxy/transform/anthropicToOpenaiResponses.js' | |
| import { openaiResponsesStreamToAnthropicResponse } from '../proxy/streaming/openaiResponsesStreamToAnthropicResponse.js' | |
| import { cleanSessionTitleSource, hasSessionTitleMarkup } from '../../utils/sessionTitleText.js' | |
| const TITLE_MAX_LEN = 50 | |
| const TITLE_MAX_OUTPUT_TOKENS = 100 | |
| const TITLE_SYSTEM_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"}` | |
| /** | |
| * Quick placeholder title derived from user message text. | |
| * Returns first sentence, collapsed to single line, max 50 chars. | |
| */ | |
| export function deriveTitle(raw: string): string | undefined { | |
| const clean = cleanSessionTitleSource(raw) | |
| const firstSentence = /^(.*?[.!?。!?])\s/.exec(clean)?.[1] ?? clean | |
| const flat = firstSentence.replace(/\s+/g, ' ').trim() | |
| if (!flat) return undefined | |
| return flat.length > TITLE_MAX_LEN | |
| ? flat.slice(0, TITLE_MAX_LEN - 1) + '\u2026' | |
| : flat | |
| } | |
| /** | |
| * Generate an AI title using the session's provider Haiku model when possible. | |
| * Fire-and-forget — returns null on any failure. | |
| */ | |
| export async function generateTitle( | |
| conversationText: string, | |
| providerId?: string | null, | |
| ): Promise<string | null> { | |
| const trimmed = cleanSessionTitleSource(conversationText) | |
| if (!trimmed) return null | |
| try { | |
| const providerService = new ProviderService() | |
| if (providerId === null) return null | |
| let resolvedProvider = providerId | |
| ? await providerService.getProvider(providerId) | |
| : null | |
| if (!resolvedProvider) { | |
| const { activeId, providers } = await providerService.listProviders() | |
| resolvedProvider = activeId | |
| ? isOpenAIOfficialProviderId(activeId) | |
| ? await providerService.getProvider(activeId) | |
| : providers.find((provider) => provider.id === activeId) ?? null | |
| : null | |
| } | |
| if (resolvedProvider && isOpenAIOfficialProviderId(resolvedProvider.id)) { | |
| return await generateOpenAIOfficialTitle( | |
| trimmed, | |
| resolvedProvider.models.haiku || resolvedProvider.models.main, | |
| ) | |
| } | |
| if (!resolvedProvider?.baseUrl || !resolvedProvider?.apiKey) return null | |
| const model = resolvedProvider.models.haiku || resolvedProvider.models.main | |
| const url = `${resolvedProvider.baseUrl.replace(/\/+$/, '')}/v1/messages` | |
| const shouldDisableThinking = await shouldDisableThinkingForTitle() | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'x-api-key': resolvedProvider.apiKey, | |
| 'anthropic-version': '2023-06-01', | |
| }, | |
| body: JSON.stringify({ | |
| model, | |
| max_tokens: 100, | |
| system: TITLE_SYSTEM_PROMPT, | |
| messages: [{ role: 'user', content: trimmed.slice(0, 2000) }], | |
| ...(shouldDisableThinking && { thinking: { type: 'disabled' } }), | |
| }), | |
| signal: AbortSignal.timeout(15_000), | |
| }) | |
| if (!response.ok) return null | |
| const body = (await response.json()) as { | |
| content?: Array<{ type: string; text?: string }> | |
| } | |
| const text = body.content?.find((b) => b.type === 'text')?.text | |
| if (!text) return null | |
| return parseGeneratedTitleText(text) | |
| } catch { | |
| return null | |
| } | |
| } | |
| async function generateOpenAIOfficialTitle( | |
| trimmed: string, | |
| model: string, | |
| ): Promise<string | null> { | |
| const tokens = await hahaOpenAIOAuthService.ensureFreshTokens() | |
| if (!tokens?.accessToken) return null | |
| const mappedModel = resolveOpenAICodexModel(model) | |
| const requestBody = anthropicToOpenaiResponses({ | |
| model: mappedModel, | |
| max_tokens: TITLE_MAX_OUTPUT_TOKENS, | |
| system: TITLE_SYSTEM_PROMPT, | |
| messages: [{ role: 'user', content: trimmed.slice(0, 2000) }], | |
| stream: true, | |
| thinking: { type: 'disabled' }, | |
| }) | |
| requestBody.stream = true | |
| requestBody.max_output_tokens = TITLE_MAX_OUTPUT_TOKENS | |
| const headers = new Headers() | |
| headers.set('Content-Type', 'application/json') | |
| headers.set('Authorization', `Bearer ${tokens.accessToken}`) | |
| if (tokens.accountId) { | |
| headers.set('ChatGPT-Account-Id', tokens.accountId) | |
| } | |
| const response = await fetch(OPENAI_CODEX_API_ENDPOINT, { | |
| method: 'POST', | |
| headers, | |
| body: JSON.stringify(requestBody), | |
| signal: AbortSignal.timeout(15_000), | |
| }) | |
| if (!response.ok || !response.body) return null | |
| const body = await openaiResponsesStreamToAnthropicResponse( | |
| response.body, | |
| mappedModel, | |
| ) | |
| const text = body.content.find((b) => b.type === 'text')?.text | |
| if (!text) return null | |
| return parseGeneratedTitleText(text) | |
| } | |
| export function parseGeneratedTitleText(text: string): string | null { | |
| const trimmed = text.trim() | |
| if (!trimmed) return null | |
| const parsed = parseTitleFromStructuredText(trimmed) | |
| if (parsed) return normalizeTitle(parsed) | |
| if (looksLikeStructuredTitleFragment(trimmed)) return null | |
| return normalizeTitle(trimmed) | |
| } | |
| function parseTitleFromStructuredText(text: string): string | null { | |
| const candidates = new Set<string>([text]) | |
| const fenced = text.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim() | |
| if (fenced) candidates.add(fenced) | |
| const firstBrace = text.indexOf('{') | |
| const lastBrace = text.lastIndexOf('}') | |
| if (firstBrace >= 0 && lastBrace > firstBrace) { | |
| candidates.add(text.slice(firstBrace, lastBrace + 1)) | |
| } | |
| for (const candidate of [...candidates]) { | |
| const unescaped = candidate.replace(/\\"/g, '"').replace(/\\n/g, '\n') | |
| if (unescaped !== candidate) candidates.add(unescaped) | |
| } | |
| for (const candidate of candidates) { | |
| const title = parseTitleJson(candidate) | |
| if (title) return title | |
| } | |
| return null | |
| } | |
| function parseTitleJson(candidate: string): string | null { | |
| try { | |
| const parsed = JSON.parse(candidate) | |
| if (typeof parsed === 'string') { | |
| return parseTitleFromStructuredText(parsed) | |
| } | |
| if (parsed && typeof parsed === 'object' && typeof (parsed as { title?: unknown }).title === 'string') { | |
| return (parsed as { title: string }).title | |
| } | |
| } catch { | |
| return null | |
| } | |
| return null | |
| } | |
| function normalizeTitle(title: string): string | null { | |
| const clean = cleanSessionTitleSource(title) | |
| if ( | |
| !clean || | |
| clean.length > 60 || | |
| looksLikeStructuredTitleFragment(clean) || | |
| hasSessionTitleMarkup(clean) | |
| ) return null | |
| return clean | |
| } | |
| function looksLikeStructuredTitleFragment(text: string): boolean { | |
| return ( | |
| text.includes('```') || | |
| text.includes('{') || | |
| text.includes('}') || | |
| /\\?"title\\?"\s*:/.test(text) | |
| ) | |
| } | |
| async function shouldDisableThinkingForTitle(): Promise<boolean> { | |
| const settings = await new SettingsService().getUserSettings() | |
| return settings.alwaysThinkingEnabled === false | |
| } | |
| /** | |
| * Persist an AI-generated title to the session's JSONL file. | |
| * Returns false when a user custom title exists, because custom titles are | |
| * intentional and must not be replaced by automatic title refreshes. | |
| */ | |
| export async function saveAiTitle(sessionId: string, title: string): Promise<boolean> { | |
| if (await sessionService.getCustomTitle(sessionId)) { | |
| return false | |
| } | |
| await sessionService.appendAiTitle(sessionId, title) | |
| return true | |
| } | |