feat: release chat and mindspace UI updates

This commit is contained in:
john
2026-06-27 21:57:21 +08:00
parent c924b2bb77
commit 8264e71f9e
24 changed files with 2042 additions and 209 deletions
+184 -6
View File
@@ -11,11 +11,78 @@
* conversation content, so it can be dropped/truncated safely at any time.
*/
const DERIVED_TITLE_MAX_LEN = 40;
function isGeneratedSessionName(name) {
return /^20\d{6}(?:[_-]\d+)?$/.test(String(name ?? '').trim());
}
function isDefaultSessionName(name) {
const trimmed = String(name ?? '').trim();
return !trimmed || trimmed === 'New Chat' || trimmed === '新对话' || isGeneratedSessionName(trimmed);
}
/** Extract a plain-text snippet from a stored normalized message. */
function messageSnippet(message) {
const fromContent = (message?.content ?? [])
.filter((item) => item?.type === 'text' && typeof item.text === 'string')
.map((item) => item.text)
.join('')
.trim();
if (fromContent) return fromContent;
const displayText = message?.metadata?.displayText;
return typeof displayText === 'string' ? displayText.trim() : '';
}
/** Build a one-line title from the first user message of a stored conversation. */
function deriveTitleFromMessages(messages) {
if (!Array.isArray(messages)) return '';
const firstUser = messages.find((m) => m?.role === 'user' && messageSnippet(m));
const text = (firstUser ? messageSnippet(firstUser) : '')
.replace(/\s+/g, ' ')
.trim();
if (!text) return '';
return text.length > DERIVED_TITLE_MAX_LEN
? `${text.slice(0, DERIVED_TITLE_MAX_LEN)}`
: text;
}
function resolveDisplayTitle(session, messages) {
if (session?.user_set_name) {
const explicit = String(session.name ?? '').trim();
if (explicit) return explicit;
}
const recipeTitle = String(session?.recipe?.title ?? '').trim();
if (recipeTitle) return recipeTitle;
const sessionName = String(session?.name ?? '').trim();
if (sessionName && !isDefaultSessionName(sessionName)) return sessionName;
return deriveTitleFromMessages(messages);
}
export function createSessionSnapshotService(pool) {
function isEnabled() {
return process.env.SESSION_SNAPSHOT_CACHE_ENABLED !== '0';
}
async function persistDisplayTitle(sessionId, title) {
const normalized = String(title ?? '').trim();
if (!normalized || !pool) return;
try {
await pool.query(
`UPDATE h5_session_snapshots
SET display_title = ?
WHERE agent_session_id = ?
AND (display_title IS NULL OR display_title = '')`,
[normalized, sessionId],
);
} catch (err) {
console.warn('[snapshot] persistDisplayTitle failed:', err instanceof Error ? err.message : err);
}
}
/**
* Persist a snapshot for a session.
* @param {string} sessionId
@@ -29,6 +96,7 @@ export function createSessionSnapshotService(pool) {
const now = Date.now();
const sessionMeta = {
name: session.name ?? '',
display_title: resolveDisplayTitle(session, messages),
working_dir: session.working_dir ?? '',
created_at_str: session.created_at ?? '',
updated_at_str: session.updated_at ?? '',
@@ -37,13 +105,14 @@ export function createSessionSnapshotService(pool) {
};
await pool.query(
`INSERT INTO h5_session_snapshots
(agent_session_id, user_id, name, working_dir,
(agent_session_id, user_id, name, display_title, working_dir,
created_at_str, updated_at_str, user_set_name, recipe_json,
synced_msg_count, source_updated_at,
messages_json, synced_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
display_title = VALUES(display_title),
working_dir = VALUES(working_dir),
created_at_str = VALUES(created_at_str),
updated_at_str = VALUES(updated_at_str),
@@ -57,6 +126,7 @@ export function createSessionSnapshotService(pool) {
sessionId,
userId,
sessionMeta.name,
sessionMeta.display_title,
sessionMeta.working_dir,
sessionMeta.created_at_str,
sessionMeta.updated_at_str,
@@ -82,7 +152,7 @@ export function createSessionSnapshotService(pool) {
if (!isEnabled() || !pool) return null;
try {
const [rows] = await pool.query(
`SELECT agent_session_id, user_id, name, working_dir,
`SELECT agent_session_id, user_id, name, display_title, working_dir,
created_at_str, updated_at_str, user_set_name, recipe_json,
synced_msg_count, source_updated_at, messages_json, synced_at
FROM h5_session_snapshots
@@ -92,10 +162,26 @@ export function createSessionSnapshotService(pool) {
);
if (!rows.length) return null;
const row = rows[0];
const parsedMessages = JSON.parse(row.messages_json);
const storedDisplayTitle = String(row.display_title ?? '').trim();
const rawName = String(row.name ?? '').trim();
const fallbackDisplayTitle =
storedDisplayTitle ||
resolveDisplayTitle(
{
name: row.name,
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
user_set_name: row.user_set_name === 1,
},
parsedMessages,
);
if (!storedDisplayTitle && fallbackDisplayTitle) {
void persistDisplayTitle(sessionId, fallbackDisplayTitle);
}
return {
session: {
id: sessionId,
name: row.name,
name: fallbackDisplayTitle || rawName,
working_dir: row.working_dir,
message_count: row.synced_msg_count,
created_at: row.created_at_str || undefined,
@@ -104,7 +190,7 @@ export function createSessionSnapshotService(pool) {
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
conversation: null,
},
messages: JSON.parse(row.messages_json),
messages: parsedMessages,
meta: {
synced_msg_count: row.synced_msg_count,
source_updated_at: row.source_updated_at,
@@ -153,5 +239,97 @@ export function createSessionSnapshotService(pool) {
}
}
return { save, get, remove, refresh, isEnabled };
/**
* Derive display titles from the first user message for the given session ids.
* Used to give unnamed/default sessions a meaningful label in the history list.
* Returns a Map<sessionId, title>; sessions without a usable snapshot are omitted.
*/
async function getDerivedTitles(sessionIds) {
if (!isEnabled() || !pool) return new Map();
const ids = [...new Set((sessionIds ?? []).filter(Boolean))];
if (ids.length === 0) return new Map();
const titles = new Map();
try {
const placeholders = ids.map(() => '?').join(', ');
const [rows] = await pool.query(
`SELECT agent_session_id, display_title, name, recipe_json, user_set_name, messages_json
FROM h5_session_snapshots
WHERE agent_session_id IN (${placeholders})`,
ids,
);
for (const row of rows) {
try {
const parsedMessages = JSON.parse(row.messages_json);
const title =
String(row.display_title ?? '').trim() ||
resolveDisplayTitle(
{
name: row.name,
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
user_set_name: row.user_set_name === 1,
},
parsedMessages,
);
if (title) {
titles.set(row.agent_session_id, title);
if (!String(row.display_title ?? '').trim()) {
void persistDisplayTitle(row.agent_session_id, title);
}
}
} catch {
// Skip rows with unparseable message payloads.
}
}
} catch (err) {
console.warn('[snapshot] getDerivedTitles failed:', err instanceof Error ? err.message : err);
}
return titles;
}
async function getHistorySummaries(sessionIds) {
if (!isEnabled() || !pool) return new Map();
const ids = [...new Set((sessionIds ?? []).filter(Boolean))];
if (ids.length === 0) return new Map();
const summaries = new Map();
try {
const placeholders = ids.map(() => '?').join(', ');
const [rows] = await pool.query(
`SELECT agent_session_id, display_title, name, recipe_json, user_set_name,
synced_msg_count, messages_json
FROM h5_session_snapshots
WHERE agent_session_id IN (${placeholders})`,
ids,
);
for (const row of rows) {
try {
const parsedMessages = JSON.parse(row.messages_json);
const title =
String(row.display_title ?? '').trim() ||
resolveDisplayTitle(
{
name: row.name,
recipe: row.recipe_json ? JSON.parse(row.recipe_json) : null,
user_set_name: row.user_set_name === 1,
},
parsedMessages,
);
const messageCount = Number(row.synced_msg_count ?? 0);
summaries.set(row.agent_session_id, {
title,
messageCount,
});
if (title && !String(row.display_title ?? '').trim()) {
void persistDisplayTitle(row.agent_session_id, title);
}
} catch {
// Skip rows with unparseable payloads.
}
}
} catch (err) {
console.warn('[snapshot] getHistorySummaries failed:', err instanceof Error ? err.message : err);
}
return summaries;
}
return { save, get, remove, refresh, getDerivedTitles, getHistorySummaries, isEnabled };
}