This commit is contained in:
seb
2026-10-12 18:09:57 +02:00
parent 713c31a21e
commit c35126bb11
2 changed files with 32 additions and 9 deletions

View File

@@ -1119,7 +1119,7 @@
* description: Id of the conversation that started this one as a subagent, or null for a top-level conversation. Deleting a conversation also deletes every conversation below it in this chain.
* first_message:
* type: [string, 'null']
* description: Only returned by the list endpoint. Plain text of the conversation's first user message (max 200 chars, ellipsis if cut), or null if there is none yet.
* description: Plain text of the conversation's first user message (XML stripped, max 100 chars, ellipsis if cut), or null if there is none yet.
* ModelConversationCreateInput:
* type: object
* required: [provider, model]

View File

@@ -4,7 +4,7 @@ import { RETIRED_MODELS, resolveDefaultModel } from '../../tools/llm-key.js';
import { emit } from '../sse.js';
import { getOrCreateManager, removeManager, cleanUserMessageContent } from '../../services/conversationManager.js';
const FIRST_MESSAGE_MAX_CHARS = 200;
const FIRST_MESSAGE_MAX_CHARS = 100;
// #region Helpers
@@ -24,9 +24,26 @@ function parsePositiveInt(value, field) {
return { id };
}
/**
* Removes XML elements together with their content (e.g. `<USERENV><ROUTE>…</ROUTE></USERENV>`)
* and any leftover self-closing tags.
* @param {string} text
* @returns {string}
*/
function stripXmlElements(text) {
const elementPattern = /<([A-Za-z][\w:.-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1\s*>/g;
let previous;
let stripped = text;
do {
previous = stripped;
stripped = stripped.replace(elementPattern, '');
} while (stripped !== previous);
return stripped.replace(/<[A-Za-z][\w:.-]*(?:\s[^>]*)?\/>/g, '');
}
/**
* Turns the stored first user message payload (`{"content": ...}`) into short plain text
* for conversation lists.
* for conversation lists: JSON content only, XML elements stripped, outer whitespace trimmed.
* @param {string|null} storedMessage
* @returns {string|null}
*/
@@ -38,10 +55,19 @@ function buildFirstMessagePreview(storedMessage) {
} catch {
rawContent = storedMessage;
}
const text = cleanUserMessageContent(rawContent).trim();
const text = stripXmlElements(cleanUserMessageContent(rawContent)).trim();
return text.length > FIRST_MESSAGE_MAX_CHARS ? `${text.slice(0, FIRST_MESSAGE_MAX_CHARS)}…` : text;
}
/**
* Replaces the store's raw `first_user_message` with the plain-text `first_message` preview.
* @param {{ first_user_message: string|null }} row
* @returns {object}
*/
function buildConversationResponse({ first_user_message: storedFirstMessage, ...conversation }) {
return { ...conversation, first_message: buildFirstMessagePreview(storedFirstMessage) };
}
function modelConversationErrorStatus(error) {
if (error.message === 'No company selected') return 400;
if (error.message === 'Not authorized') return 403;
@@ -113,10 +139,7 @@ export function register(app, basePath = '') {
try {
const rows = await db.list(req.user.id);
const conversations = rows.map(({ first_user_message: storedFirstMessage, ...conversation }) => ({
...conversation,
first_message: buildFirstMessagePreview(storedFirstMessage),
}));
const conversations = rows.map(buildConversationResponse);
res.status(200).json({ conversations });
} catch (error) {
const status = modelConversationErrorStatus(error);
@@ -269,7 +292,7 @@ export function register(app, basePath = '') {
}
try {
const conversation = await db.get(req.user.id, parsedId.id);
const conversation = buildConversationResponse(await db.get(req.user.id, parsedId.id));
res.status(200).json({ conversation });
} catch (error) {
const status = modelConversationErrorStatus(error);