From d310a0777c4c48ec772bdc9a510005d5e91b09c7 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 19 May 2026 20:12:00 +0400 Subject: [PATCH 001/170] refac --- src/lib/components/chat/Chat.svelte | 27 ++--------- src/lib/utils/index.ts | 75 +++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 751c49bed9..44dabafeb2 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -58,6 +58,7 @@ copyToClipboard, getMessageContentParts, createMessagesList, + sanitizeHistory, getPromptVariables, processDetails, removeAllDetails, @@ -1379,29 +1380,9 @@ ? chatContent.history : convertMessagesToHistory(chatContent.messages); - // Sanitize history: repair orphaned references from failed regenerations (#24424) - for (const message of Object.values(history.messages)) { - if (message.childrenIds) { - message.childrenIds = message.childrenIds.filter( - (childId) => history.messages[childId] - ); - } - } - if (history.currentId && !history.messages[history.currentId]) { - const messageIds = Object.keys(history.messages); - let lastMessageId = null; - for (const messageId of messageIds) { - const message = history.messages[messageId]; - if ( - (message.childrenIds ?? []).length === 0 && - (!lastMessageId || - (message.timestamp ?? 0) > (history.messages[lastMessageId].timestamp ?? 0)) - ) { - lastMessageId = messageId; - } - } - history.currentId = lastMessageId ?? messageIds[0] ?? null; - } + // Sanitize history: repair orphaned references and structurally-malformed + // nodes from failed regenerations (#24424, #24157, #20474) + sanitizeHistory(history); chatTitle.set(chatContent.title); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 91cfa2b688..b4009f198a 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -225,6 +225,81 @@ export const convertMessagesToHistory = (messages) => { return history; }; +// Repair structurally-malformed history nodes from failed regenerations. +// A lost assistant placeholder may exist under its map key with only +// completion fields (content/done/error), missing id/role/parentId. +// Reconstruct graph fields so already-corrupted chats recover on open. +export const sanitizeHistory = (history) => { + if (!history?.messages || typeof history.messages !== 'object') return; + + // Purge entries that aren't usable objects + for (const [id, message] of Object.entries(history.messages)) { + if (!message || typeof message !== 'object') { + delete history.messages[id]; + } + } + + // Ensure every surviving node has its canonical id and a childrenIds array + for (const [id, message] of Object.entries(history.messages)) { + if (message.id !== id) message.id = id; + if (!Array.isArray(message.childrenIds)) message.childrenIds = []; + } + + // Build reverse lookup: parent, indexed by child id + const parentByChildId = {}; + for (const [id, message] of Object.entries(history.messages)) { + for (const childId of message.childrenIds) { + parentByChildId[childId] = id; + } + } + + // Reconstruct missing parentId and role + for (const [id, message] of Object.entries(history.messages)) { + // Well-formed: has role and explicit parentId (null is valid for root) + if (message.role && message.parentId !== undefined) continue; + + if (message.parentId === undefined) { + message.parentId = parentByChildId[id] ?? null; + } + + if (!message.role) { + const parent = message.parentId ? history.messages[message.parentId] : null; + message.role = + parent?.role === 'user' + ? 'assistant' + : parent?.role === 'assistant' + ? 'user' + : message.model || message.usage || message.done !== undefined + ? 'assistant' + : 'user'; + } + } + + // Prune childrenIds referencing deleted/missing nodes + for (const message of Object.values(history.messages)) { + message.childrenIds = message.childrenIds.filter( + (childId) => history.messages[childId] + ); + } + + // Recover currentId if it points to a missing or incomplete node + const currentMessage = history.messages?.[history.currentId]; + if (!currentMessage?.id || !currentMessage?.role) { + let latestLeafId = null; + let latestTimestamp = -1; + for (const [id, message] of Object.entries(history.messages)) { + if ( + message.childrenIds.length === 0 && + (message.timestamp ?? 0) > latestTimestamp + ) { + latestLeafId = id; + latestTimestamp = message.timestamp ?? 0; + } + } + history.currentId = latestLeafId ?? Object.keys(history.messages)[0] ?? null; + } +}; + export const getGravatarURL = (email) => { // Trim leading and trailing whitespace from // an email address and force all characters From ef975649b26d3e7cd589c49be2fc77cee80fad8f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 19 May 2026 20:19:23 +0400 Subject: [PATCH 002/170] refac --- .../admin/Functions/FunctionEditor.svelte | 12 ++++- .../workspace/Skills/SkillEditor.svelte | 54 ++++--------------- .../workspace/Tools/ToolkitEditor.svelte | 12 ++++- 3 files changed, 33 insertions(+), 45 deletions(-) diff --git a/src/lib/components/admin/Functions/FunctionEditor.svelte b/src/lib/components/admin/Functions/FunctionEditor.svelte index 6c84dd35d4..eca65e929f 100644 --- a/src/lib/components/admin/Functions/FunctionEditor.svelte +++ b/src/lib/components/admin/Functions/FunctionEditor.svelte @@ -4,7 +4,7 @@ const i18n = getContext('i18n'); - import { nameToId } from '$lib/utils'; + import { extractFrontmatter, formatSkillName, nameToId } from '$lib/utils'; import CodeEditor from '$lib/components/common/CodeEditor.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import Badge from '$lib/components/common/Badge.svelte'; @@ -375,6 +375,16 @@ class Pipe: {boilerplate} onChange={(e) => { _content = e; + if (!edit) { + const fm = extractFrontmatter(e); + if (fm.title && !name) { + name = formatSkillName(fm.title); + id = nameToId(fm.title); + } + if (fm.description && !meta.description) { + meta = { ...meta, description: fm.description }; + } + } }} onSave={async () => { if (formElement) { diff --git a/src/lib/components/workspace/Skills/SkillEditor.svelte b/src/lib/components/workspace/Skills/SkillEditor.svelte index 7537ec45e7..79c0158229 100644 --- a/src/lib/components/workspace/Skills/SkillEditor.svelte +++ b/src/lib/components/workspace/Skills/SkillEditor.svelte @@ -30,47 +30,21 @@ let accessGrants = []; let showAccessControlModal = false; - let hasManualEdit = false; - let hasManualName = false; - let hasManualDescription = false; - let isFrontmatterDetected = false; + $: if (!edit && !clone && name) { + id = slugify(name); + } - // Auto-detect frontmatter and fill name/description in create mode - $: if (!edit && content) { + const handleContentInput = () => { + if (edit) return; const fm = parseFrontmatter(content); - if (fm.name) { - isFrontmatterDetected = true; - if (!hasManualName) { - name = formatSkillName(fm.name); - } - if (!hasManualEdit) { - id = fm.name; - } - } else { - isFrontmatterDetected = false; + if (fm.name && !name) { + name = formatSkillName(fm.name); + id = fm.name; } - if (fm.description && !hasManualDescription) { + if (fm.description && !description) { description = fm.description; } - } else if (!edit && !content) { - isFrontmatterDetected = false; - } - - $: if (!edit && !hasManualEdit && !isFrontmatterDetected) { - id = name !== '' ? slugify(name) : ''; - } - - function handleIdInput(e: Event) { - hasManualEdit = true; - } - - function handleNameInput(e: Event) { - hasManualName = true; - } - - function handleDescriptionInput(e: Event) { - hasManualDescription = true; - } + }; const submitHandler = async () => { if (disabled) { @@ -100,10 +74,6 @@ description = skill.description || ''; content = skill.content || ''; accessGrants = skill?.access_grants === undefined ? [] : skill?.access_grants; - - if (name) hasManualName = true; - if (description) hasManualDescription = true; - if (id) hasManualEdit = true; } }); @@ -156,7 +126,6 @@ placeholder={$i18n.t('Skill Name')} aria-label={$i18n.t('Skill Name')} bind:value={name} - on:input={handleNameInput} required {disabled} /> @@ -202,7 +171,6 @@ placeholder={$i18n.t('Skill ID')} aria-label={$i18n.t('Skill ID')} bind:value={id} - on:input={handleIdInput} required disabled={edit} /> @@ -220,7 +188,6 @@ placeholder={$i18n.t('Skill Description')} aria-label={$i18n.t('Skill Description')} bind:value={description} - on:input={handleDescriptionInput} {disabled} /> @@ -240,6 +207,7 @@