59559d4b24
Thinking disappeared on failure and never reached session storage. Keep the sanitized chain on disk and on errors, and regroup the sidebar around reports, charts, favorites, and dated history titles. Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
4.3 KiB
TypeScript
111 lines
4.3 KiB
TypeScript
import type { ConsultationDomain } from "./consultation-domain-registry.ts";
|
||
import { consultationDomainDefinition } from "./consultation-domain-registry.ts";
|
||
import type { ConsultationEntrypoint } from "./consultation-entrypoint.ts";
|
||
import {
|
||
isGeneralDailyFortuneQuestion,
|
||
isRectificationHandoffQuestion,
|
||
} from "./consultation-entrypoint.ts";
|
||
|
||
export type ReplyTheme = ConsultationDomain;
|
||
|
||
import type { ConsultationReplyMetadata } from "./consultation-reply-metadata.ts";
|
||
|
||
const GENERIC_SESSION_TITLES = new Set([
|
||
"新对话",
|
||
"深入看今日",
|
||
"从今日问起",
|
||
"查看今日运势",
|
||
"生时校正",
|
||
"再次校正",
|
||
"一般占星咨询",
|
||
"深入看今日主题咨询",
|
||
]);
|
||
|
||
function readTitle(value: string): string | undefined {
|
||
const title = value.replace(/\s+/g, " ").trim();
|
||
if (!title || /[\d\p{P}\p{S}]/u.test(title)) return undefined;
|
||
if (/\p{Script=Han}/u.test(title)) {
|
||
const length = Array.from(title.replace(/\s/g, "")).length;
|
||
return length >= 6 && length <= 14 ? title : undefined;
|
||
}
|
||
const words = title.split(" ").filter(Boolean);
|
||
return words.length >= 3 && words.length <= 7 && title.length <= 64 ? title : undefined;
|
||
}
|
||
|
||
// AYANAM_SUGGESTIONS blocks are no longer produced anywhere, but stored answers from
|
||
// before the follow-up chips were removed still carry them, so they stay strippable
|
||
// rather than leaking a raw HTML comment into a replayed transcript.
|
||
function stripAgentReplyMetadata(value: string) {
|
||
let title: string | undefined;
|
||
const text = value
|
||
.replace(/<!--AYANAM_SUGGESTIONS:\[[\s\S]*?\]-->/g, "")
|
||
.replace(/<!--AYANAM_TITLE:([\s\S]*?)-->/g, (_, rawTitle: string) => {
|
||
title = readTitle(rawTitle);
|
||
return "";
|
||
})
|
||
.replace(/<!--AYANAM_[\s\S]*$/, "")
|
||
.trim();
|
||
|
||
return { text, title };
|
||
}
|
||
|
||
export function parseAgentReply(value: string, metadata?: ConsultationReplyMetadata) {
|
||
const parsed = stripAgentReplyMetadata(value);
|
||
return { text: parsed.text, title: metadata?.title ?? parsed.title };
|
||
}
|
||
|
||
export type SessionTitleOptions = {
|
||
readonly entrypoint?: ConsultationEntrypoint | null;
|
||
readonly theme?: ConsultationDomain;
|
||
readonly at?: Date;
|
||
readonly existingTitles?: readonly string[];
|
||
};
|
||
|
||
export function isGenericSessionTitle(title: string): boolean {
|
||
const text = title.replace(/\s+/g, " ").trim();
|
||
if (!text) return true;
|
||
if (GENERIC_SESSION_TITLES.has(text)) return true;
|
||
return /^(?:深入看今日|从今日问起|查看今日运势|生时校正|再次校正)/.test(text);
|
||
}
|
||
|
||
function clipTitle(value: string, maxChars = 14): string {
|
||
const characters = Array.from(value);
|
||
return characters.length > maxChars ? `${characters.slice(0, maxChars).join("")}…` : value;
|
||
}
|
||
|
||
function datedSessionTitle(at: Date, suffix: string): string {
|
||
return `${at.getMonth() + 1}月${at.getDate()}日 · ${suffix}`;
|
||
}
|
||
|
||
function uniquifySessionTitle(title: string, existingTitles: readonly string[], at: Date): string {
|
||
if (!existingTitles.includes(title)) return title;
|
||
const hours = String(at.getHours()).padStart(2, "0");
|
||
const minutes = String(at.getMinutes()).padStart(2, "0");
|
||
return `${title} ${hours}:${minutes}`;
|
||
}
|
||
|
||
export function resolveSessionTitle(
|
||
question: string,
|
||
modelTitle?: string,
|
||
options: SessionTitleOptions = {},
|
||
): string {
|
||
const at = options.at ?? new Date();
|
||
const existingTitles = options.existingTitles ?? [];
|
||
if (modelTitle && !isGenericSessionTitle(modelTitle)) {
|
||
return uniquifySessionTitle(clipTitle(modelTitle), existingTitles, at);
|
||
}
|
||
if (options.entrypoint === "daily_starlanguage" || isGeneralDailyFortuneQuestion(question)) {
|
||
return uniquifySessionTitle(datedSessionTitle(at, "今日节奏"), existingTitles, at);
|
||
}
|
||
if (options.entrypoint === "birth_time_rectification" || isRectificationHandoffQuestion(question)) {
|
||
return uniquifySessionTitle(datedSessionTitle(at, "生时校正"), existingTitles, at);
|
||
}
|
||
const normalized = question.replace(/\s+/g, " ").trim().replace(/[??!!。.,,;;::]+$/u, "");
|
||
if (!normalized) return "新对话";
|
||
if (options.theme && options.theme !== "general") {
|
||
const label = consultationDomainDefinition(options.theme).label;
|
||
return uniquifySessionTitle(`${label} · ${clipTitle(normalized, 10)}`, existingTitles, at);
|
||
}
|
||
return uniquifySessionTitle(clipTitle(normalized), existingTitles, at);
|
||
}
|