From c35126bb114efac479f5348e4a0a1353786a56d8 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 12 Oct 2026 18:09:57 +0200 Subject: [PATCH] u --- src/api/openapi.js | 2 +- src/api/routes/modelconversation.js | 39 +++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/api/openapi.js b/src/api/openapi.js index e113bd0..e9c0c3a 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -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] diff --git a/src/api/routes/modelconversation.js b/src/api/routes/modelconversation.js index 21eac7c..f5fe294 100644 --- a/src/api/routes/modelconversation.js +++ b/src/api/routes/modelconversation.js @@ -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. `…`) + * 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);