mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Slash-command and skill prompts can enter the title-generation path as internal XML breadcrumbs. The async title request previously treated that transport metadata as user prose, so a generated ai-title could persist raw command tags and override the normal session title. This routes title sources and generated title output through a shared sanitizer, preserving user-visible command names and arguments while dropping unrelated internal XML metadata. Desktop fallback title rendering now applies the same cleanup so existing transcripts with bad ai-title entries recover on read. Constraint: Session titles can be produced by both desktop server logic and the SDK generate_session_title control request. Rejected: Only clean desktop session display | leaves CLI and remote title generation able to persist bad titles again Confidence: high Scope-risk: moderate Directive: Keep generated-title input and persisted-title readback on the shared sanitizer; do not add a title path that reads command XML directly. Tested: bun test src/utils/__tests__/sessionTitle.test.ts src/utils/__tests__/sessionTitleText.test.ts Tested: bun run check:server Tested: bun run check:desktop Tested: bun run verify (7 passed, 1 failed on existing aggregate agent-utils coverage baseline) Not-tested: live model title-generation smoke
49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
const TITLE_MAX_LEN = 50
|
|
|
|
const PLACEHOLDER_TITLES = new Set([
|
|
'',
|
|
'New Session',
|
|
'Untitled Session',
|
|
])
|
|
|
|
const XML_TAG_BLOCK_PATTERN = /<([a-z][\w-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>\n?/g
|
|
|
|
function decodeXmlText(text: string): string {
|
|
return text
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
.replace(/&/g, '&')
|
|
}
|
|
|
|
function extractXmlTag(text: string, tag: string): string | undefined {
|
|
const match = text.match(new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, 'i'))
|
|
const value = match?.[1]?.trim()
|
|
return value ? decodeXmlText(value) : undefined
|
|
}
|
|
|
|
function cleanSessionTitleSource(raw: string): string {
|
|
const commandName = extractXmlTag(raw, 'command-name')
|
|
const commandArgs = extractXmlTag(raw, 'command-args')
|
|
if (commandName) {
|
|
return [commandName, commandArgs].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim()
|
|
}
|
|
|
|
return raw.replace(XML_TAG_BLOCK_PATTERN, ' ').replace(/\s+/g, ' ').trim()
|
|
}
|
|
|
|
export function deriveSessionTitle(raw: string): string | null {
|
|
const clean = cleanSessionTitleSource(raw)
|
|
const firstSentence = /^(.*?[.!?\u3002\uff01\uff1f])\s/.exec(clean)?.[1] ?? clean
|
|
const flat = firstSentence.replace(/\s+/g, ' ').trim()
|
|
if (!flat) return null
|
|
return flat.length > TITLE_MAX_LEN
|
|
? `${flat.slice(0, TITLE_MAX_LEN - 1)}\u2026`
|
|
: flat
|
|
}
|
|
|
|
export function isPlaceholderSessionTitle(title: string | null | undefined): boolean {
|
|
return PLACEHOLDER_TITLES.has((title ?? '').trim())
|
|
}
|