- {#each messages as message, idx (message.id)}
+ {#each messages as message, idx (idx)}
{
@@ -54,7 +51,7 @@
begin="indefinite"
end="indefinite"
dur="3.2s"
- repeatCount="indefinite"
+ repeatCount={"indefinite"}
fill="freeze"
calcMode="spline"
keyTimes="0; .33; .66; .9; 1"
From 62997076cde4f4a51344656c12e210f47c65c68e Mon Sep 17 00:00:00 2001
From: "Thomas G. Lopes" <26071571+TGlide@users.noreply.github.com>
Date: Wed, 8 Oct 2025 19:58:56 +0100
Subject: [PATCH 8/8] stable message ids
---
src/lib/components/chat/ChatWindow.svelte | 2 +-
src/lib/components/chat/MessageAvatar.svelte | 6 +++-
src/lib/utils/messageUpdates.ts | 5 ++++
src/lib/utils/tree/addChildren.ts | 4 +--
src/lib/utils/tree/addSibling.ts | 2 +-
src/lib/utils/tree/tree.d.ts | 2 +-
src/routes/conversation/[id]/+page.svelte | 29 ++++++++++++++++----
src/routes/conversation/[id]/+server.ts | 25 ++++++++++++++++-
8 files changed, 63 insertions(+), 12 deletions(-)
diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte
index 3157d55eac0..85b9416ab8c 100644
--- a/src/lib/components/chat/ChatWindow.svelte
+++ b/src/lib/components/chat/ChatWindow.svelte
@@ -346,7 +346,7 @@
{#if messages.length > 0}
- {#each messages as message, idx (idx)}
+ {#each messages as message, idx (message.id)}
();
let svgEl = $state();
+ let begun = $state(false);
$effect(() => {
if (!blobAnim) return;
if (animating) {
// Resume animations and start once
+ if (!begun) {
+ blobAnim.beginElement();
+ begun = true;
+ }
svgEl?.unpauseAnimations?.();
- blobAnim.beginElement();
}
return () => {
diff --git a/src/lib/utils/messageUpdates.ts b/src/lib/utils/messageUpdates.ts
index c847dfbc5ef..4a37310cb01 100644
--- a/src/lib/utils/messageUpdates.ts
+++ b/src/lib/utils/messageUpdates.ts
@@ -14,6 +14,10 @@ type MessageUpdateRequestOptions = {
isRetry: boolean;
isContinue: boolean;
files?: MessageFile[];
+ createdMessageIds?: {
+ userMessageId?: string;
+ assistantMessageId?: string;
+ };
};
export async function fetchMessageUpdates(
conversationId: string,
@@ -30,6 +34,7 @@ export async function fetchMessageUpdates(
id: opts.messageId,
is_retry: opts.isRetry,
is_continue: opts.isContinue,
+ created_message_ids: opts.createdMessageIds,
});
opts.files?.forEach((file) => {
diff --git a/src/lib/utils/tree/addChildren.ts b/src/lib/utils/tree/addChildren.ts
index 82b160409bb..888902e2b07 100644
--- a/src/lib/utils/tree/addChildren.ts
+++ b/src/lib/utils/tree/addChildren.ts
@@ -4,7 +4,7 @@ import type { Tree, TreeId, NewNode, TreeNode } from "./tree";
export function addChildren(conv: Tree, message: NewNode, parentId?: TreeId): TreeId {
// if this is the first message we just push it
if (conv.messages.length === 0) {
- const messageId = v4();
+ const messageId = "id" in message && message.id ? message.id : v4();
conv.rootMessageId = messageId;
conv.messages.push({
...message,
@@ -18,7 +18,7 @@ export function addChildren(conv: Tree, message: NewNode, parentId?: Tr
throw new Error("You need to specify a parentId if this is not the first message");
}
- const messageId = v4();
+ const messageId = "id" in message && message.id ? message.id : v4();
if (!conv.rootMessageId) {
// if there is no parentId we just push the message
if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) {
diff --git a/src/lib/utils/tree/addSibling.ts b/src/lib/utils/tree/addSibling.ts
index 42658b2a0ab..c56889f1995 100644
--- a/src/lib/utils/tree/addSibling.ts
+++ b/src/lib/utils/tree/addSibling.ts
@@ -19,7 +19,7 @@ export function addSibling(conv: Tree, message: NewNode, siblingId: Tre
throw new Error("The sibling message is the root message, therefore we can't add a sibling");
}
- const messageId = v4();
+ const messageId = "id" in message && message.id ? message.id : v4();
conv.messages.push({
...message,
diff --git a/src/lib/utils/tree/tree.d.ts b/src/lib/utils/tree/tree.d.ts
index 9bbb6a10f89..e1a32578987 100644
--- a/src/lib/utils/tree/tree.d.ts
+++ b/src/lib/utils/tree/tree.d.ts
@@ -11,4 +11,4 @@ export type TreeNode = T & {
children?: TreeId[];
};
-export type NewNode = Omit, "id">;
+export type NewNode = Omit, "id"> | TreeNode;
diff --git a/src/routes/conversation/[id]/+page.svelte b/src/routes/conversation/[id]/+page.svelte
index a124fb40173..a6d78963cdf 100644
--- a/src/routes/conversation/[id]/+page.svelte
+++ b/src/routes/conversation/[id]/+page.svelte
@@ -19,7 +19,7 @@
import { addChildren } from "$lib/utils/tree/addChildren";
import { addSibling } from "$lib/utils/tree/addSibling";
import { fetchMessageUpdates } from "$lib/utils/messageUpdates";
- import type { v4 } from "uuid";
+ import { v4 } from "uuid";
import { useSettingsStore } from "$lib/stores/settings.js";
import { browser } from "$app/environment";
import {
@@ -154,6 +154,7 @@
);
let messageToWriteToId: Message["id"] | undefined = undefined;
+ let createdMessageIds: { userMessageId?: string; assistantMessageId?: string } = {};
// used for building the prompt, subtree of the conversation that goes from the latest message to the root
if (isContinue && messageId) {
@@ -177,12 +178,19 @@
if (messageToRetry?.from === "user" && prompt) {
// add a sibling to this message from the user, with the alternative prompt
// add a children to that sibling, where we can write to
- const newUserMessageId = addSibling(
+ const newUserMessageId = v4();
+ const newAssistantMessageId = v4();
+ createdMessageIds = {
+ userMessageId: newUserMessageId,
+ assistantMessageId: newAssistantMessageId,
+ };
+ addSibling(
{
messages,
rootMessageId: data.rootMessageId,
},
{
+ id: newUserMessageId,
from: "user",
content: prompt,
files: messageToRetry.files,
@@ -194,30 +202,39 @@
messages,
rootMessageId: data.rootMessageId,
},
- { from: "assistant", content: "" },
+ { id: newAssistantMessageId, from: "assistant", content: "" },
newUserMessageId
);
} else if (messageToRetry?.from === "assistant") {
// we're retrying an assistant message, to generate a new answer
// just add a sibling to the assistant answer where we can write to
+ const newAssistantMessageId = v4();
+ createdMessageIds = { assistantMessageId: newAssistantMessageId };
messageToWriteToId = addSibling(
{
messages,
rootMessageId: data.rootMessageId,
},
- { from: "assistant", content: "" },
+ { id: newAssistantMessageId, from: "assistant", content: "" },
messageId
);
}
} else {
// just a normal linear conversation, so we add the user message
// and the blank assistant message back to back
- const newUserMessageId = addChildren(
+ const newUserMessageId = v4();
+ const newAssistantMessageId = v4();
+ createdMessageIds = {
+ userMessageId: newUserMessageId,
+ assistantMessageId: newAssistantMessageId,
+ };
+ addChildren(
{
messages,
rootMessageId: data.rootMessageId,
},
{
+ id: newUserMessageId,
from: "user",
content: prompt ?? "",
files: base64Files,
@@ -235,6 +252,7 @@
rootMessageId: data.rootMessageId,
},
{
+ id: newAssistantMessageId,
from: "assistant",
content: "",
},
@@ -259,6 +277,7 @@
isRetry,
isContinue,
files: isRetry ? userMessage?.files : base64Files,
+ createdMessageIds,
},
messageUpdatesAbortController.signal
).catch((err) => {
diff --git a/src/routes/conversation/[id]/+server.ts b/src/routes/conversation/[id]/+server.ts
index 49ae8bf6984..5480d6cfa01 100644
--- a/src/routes/conversation/[id]/+server.ts
+++ b/src/routes/conversation/[id]/+server.ts
@@ -149,6 +149,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
id: messageId,
is_retry: isRetry,
is_continue: isContinue,
+ created_message_ids: createdMessageIds,
} = z
.object({
id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue
@@ -170,6 +171,14 @@ export async function POST({ request, locals, params, getClientAddress }) {
})
)
),
+ created_message_ids: z
+ .optional(
+ z.object({
+ userMessageId: z.string().uuid().optional(),
+ assistantMessageId: z.string().uuid().optional(),
+ })
+ )
+ .optional(),
})
.parse(JSON.parse(json));
@@ -246,6 +255,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
const newUserMessageId = addSibling(
conv,
{
+ ...(createdMessageIds?.userMessageId && { id: createdMessageIds.userMessageId }),
from: "user",
content: newPrompt,
files: uploadedFiles,
@@ -257,6 +267,9 @@ export async function POST({ request, locals, params, getClientAddress }) {
messageToWriteToId = addChildren(
conv,
{
+ ...(createdMessageIds?.assistantMessageId && {
+ id: createdMessageIds.assistantMessageId,
+ }),
from: "assistant",
content: "",
createdAt: new Date(),
@@ -270,7 +283,15 @@ export async function POST({ request, locals, params, getClientAddress }) {
// just add a sibling to the assistant answer where we can write to
messageToWriteToId = addSibling(
conv,
- { from: "assistant", content: "", createdAt: new Date(), updatedAt: new Date() },
+ {
+ ...(createdMessageIds?.assistantMessageId && {
+ id: createdMessageIds.assistantMessageId,
+ }),
+ from: "assistant",
+ content: "",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
messageId
);
messagesForPrompt = buildSubtree(conv, messageId);
@@ -282,6 +303,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
const newUserMessageId = addChildren(
conv,
{
+ ...(createdMessageIds?.userMessageId && { id: createdMessageIds.userMessageId }),
from: "user",
content: newPrompt ?? "",
files: uploadedFiles,
@@ -294,6 +316,7 @@ export async function POST({ request, locals, params, getClientAddress }) {
messageToWriteToId = addChildren(
conv,
{
+ ...(createdMessageIds?.assistantMessageId && { id: createdMessageIds.assistantMessageId }),
from: "assistant",
content: "",
createdAt: new Date(),