ui: Improve performance when streaming (#25225)

* ui: Improve performance when streaming

* ui: build sibling info map in branching utils

Moves the node map and sibling map construction from the
.by block into buildSiblingInfoMap() in branching.ts.

The map is built once per structural change and only read
afterwards, so it does not need SvelteMap reactivity. Keeping
the construction in plain TypeScript fixes the
svelte/prefer-svelte-reactivity lint error and groups the
branching logic where it already lives.

---------

Co-authored-by: Pascal <admin@serveurperso.com>
This commit is contained in:
Nick Towle
2026-07-03 10:03:51 -07:00
committed by GitHub
parent f113e02d5a
commit d4cff114c0
3 changed files with 50 additions and 107 deletions

View File

@@ -20,9 +20,9 @@
agenticInjectSteeringMessage agenticInjectSteeringMessage
} from '$lib/stores/agentic.svelte'; } from '$lib/stores/agentic.svelte';
import { import {
buildSiblingInfoMap,
copyToClipboard, copyToClipboard,
formatMessageForClipboard, formatMessageForClipboard,
getMessageSiblings,
hasAgenticContent hasAgenticContent
} from '$lib/utils'; } from '$lib/utils';
@@ -169,6 +169,8 @@
}); });
}); });
let siblingInfoByMessageId = $derived(buildSiblingInfoMap(allConversationMessages));
let displayMessages = $derived.by(() => { let displayMessages = $derived.by(() => {
if (!messages.length) { if (!messages.length) {
return []; return [];
@@ -223,18 +225,18 @@
} }
} }
const siblingInfo = getMessageSiblings(allConversationMessages, msg.id); const siblingInfo = siblingInfoByMessageId.get(msg.id) ?? {
message: msg,
siblingIds: [msg.id],
currentIndex: 0,
totalSiblings: 1
};
result.push({ result.push({
message: msg, message: msg,
toolMessages, toolMessages,
isLastAssistantMessage: false, isLastAssistantMessage: false,
siblingInfo: siblingInfo || { siblingInfo
message: msg,
siblingIds: [msg.id],
currentIndex: 0,
totalSiblings: 1
}
}); });
} }

View File

@@ -92,18 +92,14 @@ export function filterByLeafNodeId(
* Finds the leaf node (message with no children) for a given message branch. * Finds the leaf node (message with no children) for a given message branch.
* Traverses down the tree following the last child until reaching a leaf. * Traverses down the tree following the last child until reaching a leaf.
* *
* @param messages - All messages in the conversation * @param nodeMap - Map of messages keyed by ID
* @param messageId - Starting message ID to find leaf for * @param messageId - Starting message ID to find leaf for
* @returns The leaf node ID, or the original messageId if no children * @returns The leaf node ID, or the original messageId if no children
*/ */
export function findLeafNode(messages: readonly DatabaseMessage[], messageId: string): string { function findLeafNodeInMap(
const nodeMap = new Map<string, DatabaseMessage>(); nodeMap: ReadonlyMap<string, DatabaseMessage>,
messageId: string
// Build node map for quick lookups ): string {
for (const msg of messages) {
nodeMap.set(msg.id, msg);
}
let currentNode: DatabaseMessage | undefined = nodeMap.get(messageId); let currentNode: DatabaseMessage | undefined = nodeMap.get(messageId);
while (currentNode && currentNode.children.length > 0) { while (currentNode && currentNode.children.length > 0) {
// Follow the last child (most recent branch) // Follow the last child (most recent branch)
@@ -114,6 +110,22 @@ export function findLeafNode(messages: readonly DatabaseMessage[], messageId: st
return currentNode?.id ?? messageId; return currentNode?.id ?? messageId;
} }
/**
* Convenience wrapper around {@link findLeafNodeInMap} for callers that only have
* a flat message array.
*
* Finds the leaf node (message with no children) for a given message branch.
* Traverses down the tree following the last child until reaching a leaf.
*
* @param messages - All messages in the conversation
* @param messageId - Starting message ID to find leaf for
* @returns The leaf node ID, or the original messageId if no children
*/
export function findLeafNode(messages: readonly DatabaseMessage[], messageId: string): string {
const nodeMap = new Map(messages.map((msg) => [msg.id, msg] as const));
return findLeafNodeInMap(nodeMap, messageId);
}
/** /**
* Finds all descendant messages (children, grandchildren, etc.) of a given message. * Finds all descendant messages (children, grandchildren, etc.) of a given message.
* This is used for cascading deletion to remove all messages in a branch. * This is used for cascading deletion to remove all messages in a branch.
@@ -156,21 +168,14 @@ export function findDescendantMessages(
* Gets sibling information for a message, including all sibling IDs and current position. * Gets sibling information for a message, including all sibling IDs and current position.
* Siblings are messages that share the same parent. * Siblings are messages that share the same parent.
* *
* @param messages - All messages in the conversation * @param nodeMap - Map of messages keyed by ID
* @param messageId - The message to get sibling info for * @param messageId - The message to get sibling info for
* @returns Sibling information including leaf node IDs for navigation * @returns Sibling information including leaf node IDs for navigation
*/ */
export function getMessageSiblings( export function getMessageSiblings(
messages: readonly DatabaseMessage[], nodeMap: ReadonlyMap<string, DatabaseMessage>,
messageId: string messageId: string
): ChatMessageSiblingInfo | null { ): ChatMessageSiblingInfo | null {
const nodeMap = new Map<string, DatabaseMessage>();
// Build node map for quick lookups
for (const msg of messages) {
nodeMap.set(msg.id, msg);
}
const message = nodeMap.get(messageId); const message = nodeMap.get(messageId);
if (!message) { if (!message) {
return null; return null;
@@ -203,7 +208,9 @@ export function getMessageSiblings(
// Convert sibling message IDs to their corresponding leaf node IDs // Convert sibling message IDs to their corresponding leaf node IDs
// This allows navigation between different conversation branches // This allows navigation between different conversation branches
const siblingLeafIds = siblingIds.map((siblingId: string) => findLeafNode(messages, siblingId)); const siblingLeafIds = siblingIds.map((siblingId: string) =>
findLeafNodeInMap(nodeMap, siblingId)
);
// Find current message's position among siblings // Find current message's position among siblings
const currentIndex = siblingIds.indexOf(messageId); const currentIndex = siblingIds.indexOf(messageId);
@@ -217,85 +224,22 @@ export function getMessageSiblings(
} }
/** /**
* Creates a display-ready list of messages with sibling information for UI rendering. * Builds sibling information for every message in a conversation.
* This is the main function used by chat components to render conversation branches. * A single node map is shared across all lookups for O(1) access.
* *
* @param messages - All messages in the conversation * @param messages - All messages in the conversation
* @param leafNodeId - Current leaf node being viewed * @returns Map of message ID to its sibling information
* @returns Array of messages with sibling navigation info
*/ */
export function getMessageDisplayList( export function buildSiblingInfoMap(
messages: readonly DatabaseMessage[], messages: readonly DatabaseMessage[]
leafNodeId: string ): Map<string, ChatMessageSiblingInfo> {
): ChatMessageSiblingInfo[] { const nodeMap = new Map(messages.map((msg) => [msg.id, msg] as const));
// Get the current conversation path const siblingMap = new Map<string, ChatMessageSiblingInfo>();
const currentPath = filterByLeafNodeId(messages, leafNodeId, true); for (const msg of messages) {
const result: ChatMessageSiblingInfo[] = []; const info = getMessageSiblings(nodeMap, msg.id);
if (info) {
// Add sibling info for each message in the current path siblingMap.set(msg.id, info);
for (const message of currentPath) {
if (message.type === 'root') {
continue; // Skip root messages in display
}
const siblingInfo = getMessageSiblings(messages, message.id);
if (siblingInfo) {
result.push(siblingInfo);
} }
} }
return siblingMap;
return result;
}
/**
* Checks if a message has multiple siblings (indicating branching at that point).
*
* @param messages - All messages in the conversation
* @param messageId - The message to check
* @returns True if the message has siblings
*/
export function hasMessageSiblings(
messages: readonly DatabaseMessage[],
messageId: string
): boolean {
const siblingInfo = getMessageSiblings(messages, messageId);
return siblingInfo ? siblingInfo.totalSiblings > 1 : false;
}
/**
* Gets the next sibling message ID for navigation.
*
* @param messages - All messages in the conversation
* @param messageId - Current message ID
* @returns Next sibling's leaf node ID, or null if at the end
*/
export function getNextSibling(
messages: readonly DatabaseMessage[],
messageId: string
): string | null {
const siblingInfo = getMessageSiblings(messages, messageId);
if (!siblingInfo || siblingInfo.currentIndex >= siblingInfo.totalSiblings - 1) {
return null;
}
return siblingInfo.siblingIds[siblingInfo.currentIndex + 1];
}
/**
* Gets the previous sibling message ID for navigation.
*
* @param messages - All messages in the conversation
* @param messageId - Current message ID
* @returns Previous sibling's leaf node ID, or null if at the beginning
*/
export function getPreviousSibling(
messages: readonly DatabaseMessage[],
messageId: string
): string | null {
const siblingInfo = getMessageSiblings(messages, messageId);
if (!siblingInfo || siblingInfo.currentIndex <= 0) {
return null;
}
return siblingInfo.siblingIds[siblingInfo.currentIndex - 1];
} }

View File

@@ -26,10 +26,7 @@ export {
findLeafNode, findLeafNode,
findDescendantMessages, findDescendantMessages,
getMessageSiblings, getMessageSiblings,
getMessageDisplayList, buildSiblingInfoMap
hasMessageSiblings,
getNextSibling,
getPreviousSibling
} from './branching'; } from './branching';
// Code // Code