Load a conversation's history on the client

The conversation pages rendered their messages on the server, so a page
the back button restored from the back-forward cache showed an old
thread. The server now renders everything else (rail, header, composer)
and the history (active session, latest messages, active task) loads
from api/admin/bots/conversation-history once the page is on screen,
with a loading line under the header, and again on a pageshow restore.
Messages sent or pushed while it loads are merged with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-25 03:55:38 +01:00
co-authored by Claude Opus 5.5
parent cdb93ac1bc
commit d542b4e9ad
15 changed files with 485 additions and 63 deletions
@@ -0,0 +1,44 @@
import { describe, expect, test } from "bun:test";
import type { BotMessageWithAttachments, ConversationHistory } from "@/src/types";
import mergeLoadedHistory from "./merge-loaded-history";
function message({ id, session_id = 1 }: { id: number; session_id?: number }) {
return { id, session_id, author: "bot", text: `#${id}` } as BotMessageWithAttachments;
}
function history({ ids, session_id = 1 }: { ids: number[]; session_id?: number }) {
return {
session: { id: session_id },
messages: ids.map((id) => message({ id, session_id })),
has_more: false,
task: null,
} as ConversationHistory;
}
const ids = (messages: BotMessageWithAttachments[]) => messages.map((m) => m.id);
describe("mergeLoadedHistory", () => {
test("an empty thread takes the history", () => {
expect(ids(mergeLoadedHistory({ shown: [], history: history({ ids: [1, 2] }) }))).toEqual([1, 2]);
});
test("keeps what arrived after the history, once", () => {
const shown = [message({ id: 2 }), message({ id: 3 })];
expect(ids(mergeLoadedHistory({ shown, history: history({ ids: [1, 2] }) }))).toEqual([1, 2, 3]);
});
test("an older copy of the thread gives way", () => {
const shown = [message({ id: 1 }), message({ id: 2 })];
expect(ids(mergeLoadedHistory({ shown, history: history({ ids: [2, 4, 5] }) }))).toEqual([2, 4, 5]);
});
test("another session's messages give way", () => {
const shown = [message({ id: 9, session_id: 1 })];
expect(
ids(mergeLoadedHistory({ shown, history: history({ ids: [10], session_id: 2 }) })),
).toEqual([10]);
});
});
@@ -0,0 +1,29 @@
import type { BotMessageWithAttachments, ConversationHistory } from "@/src/types";
type Params = {
/**
* What the thread shows now.
*/
shown: BotMessageWithAttachments[];
history: ConversationHistory;
};
/**
* The thread once a history lands: its messages, then any the thread
* already has from the same session that came after them (sent or pushed
* while the history was on its way). Everything else the thread showed
* (an older copy of the thread, another session's) gives way to it.
*/
export default function mergeLoadedHistory({ shown, history }: Params) {
const loaded = history.messages;
const last_id = Number(loaded[loaded.length - 1]?.id) || 0;
const later = shown.filter(
(message) =>
message.session_id == history.session.id &&
Number(message.id) > last_id &&
!loaded.some((m) => m.id == message.id),
);
return [...loaded, ...later];
}