<script lang="ts">
import { fadeInView } from '$lib/actions/fade-in-view.svelte';
import { ChatMessage } from '$lib/components/app';
import { setChatActionsContext } from '$lib/contexts';
import { MessageRole } from '$lib/enums';
import { chatStore } from '$lib/stores/chat.svelte';
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
import { config } from '$lib/stores/settings.svelte';
import {
copyToClipboard,
formatMessageForClipboard,
getMessageSiblings,
hasAgenticContent
} from '$lib/utils';
interface Props {
class?: string;
messages?: DatabaseMessage[];
onUserAction?: () => void;
}
let { class: className, messages = [], onUserAction }: Props = $props();
let allConversationMessages = $state<DatabaseMessage[]>([]);
const currentConfig = config();
setChatActionsContext({
copy: async (message: DatabaseMessage) => {
const asPlainText = Boolean(currentConfig.copyTextAttachmentsAsPlainText);
const clipboardContent = formatMessageForClipboard(
message.content,
message.extra,
asPlainText
);
await copyToClipboard(clipboardContent, 'Message copied to clipboard');
},
delete: async (message: DatabaseMessage) => {
await chatStore.deleteMessage(message.id);
refreshAllMessages();
},
navigateToSibling: async (siblingId: string) => {
await conversationsStore.navigateToSibling(siblingId);
},
editWithBranching: async (
message: DatabaseMessage,
newContent: string,
newExtras?: DatabaseMessageExtra[]
) => {
onUserAction?.();
await chatStore.editMessageWithBranching(message.id, newContent, newExtras);
refreshAllMessages();
},
editWithReplacement: async (
message: DatabaseMessage,
newContent: string,
shouldBranch: boolean
) => {
onUserAction?.();
await chatStore.editAssistantMessage(message.id, newContent, shouldBranch);
refreshAllMessages();
},
editUserMessagePreserveResponses: async (
message: DatabaseMessage,
newContent: string,
newExtras?: DatabaseMessageExtra[]
) => {
onUserAction?.();
await chatStore.editUserMessagePreserveResponses(message.id, newContent, newExtras);
refreshAllMessages();
},
regenerateWithBranching: async (message: DatabaseMessage, modelOverride?: string) => {
onUserAction?.();
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
refreshAllMessages();
},
continueAssistantMessage: async (message: DatabaseMessage) => {
onUserAction?.();
await chatStore.continueAssistantMessage(message.id);
refreshAllMessages();
},
forkConversation: async (
message: DatabaseMessage,
options: { name: string; includeAttachments: boolean }
) => {
await conversationsStore.forkConversation(message.id, options);
}
});
function refreshAllMessages() {
const conversation = activeConversation();
if (conversation) {
conversationsStore.getConversationMessages(conversation.id).then((messages) => {
allConversationMessages = messages;
});
} else {
allConversationMessages = [];
}
}
// Single effect that tracks both conversation and message changes
$effect(() => {
const conversation = activeConversation();
if (conversation) {
refreshAllMessages();
}
});
let displayMessages = $derived.by(() => {
if (!messages.length) {
return [];
}
const filteredMessages = currentConfig.showSystemMessage
? messages
: messages.filter((msg) => msg.type !== MessageRole.SYSTEM);
// Build display entries, grouping agentic sessions into single entries.
// An agentic session = assistant(with tool_calls) → tool → assistant → tool → ... → assistant(final)
const result: Array<{
message: DatabaseMessage;
toolMessages: DatabaseMessage[];
isLastAssistantMessage: boolean;
siblingInfo: ChatMessageSiblingInfo;
}> = [];
for (let i = 0; i < filteredMessages.length; i++) {
const msg = filteredMessages[i];
// Skip tool messages - they're grouped with preceding assistant
if (msg.role === MessageRole.TOOL) continue;
const toolMessages: DatabaseMessage[] = [];
if (msg.role === MessageRole.ASSISTANT && hasAgenticContent(msg)) {
let j = i + 1;
while (j < filteredMessages.length) {
const next = filteredMessages[j];
if (next.role === MessageRole.TOOL) {
toolMessages.push(next);
j++;
} else if (next.role === MessageRole.ASSISTANT) {
toolMessages.push(next);
j++;
} else {
break;
}
}
i = j - 1;
} else if (msg.role === MessageRole.ASSISTANT) {
let j = i + 1;
while (j < filteredMessages.length && filteredMessages[j].role === MessageRole.TOOL) {
toolMessages.push(filteredMessages[j]);
j++;
}
}
const siblingInfo = getMessageSiblings(allConversationMessages, msg.id);
result.push({
message: msg,
toolMessages,
isLastAssistantMessage: false,
siblingInfo: siblingInfo || {
message: msg,
siblingIds: [msg.id],
currentIndex: 0,
totalSiblings: 1
}
});
}
// Mark the last assistant message
for (let i = result.length - 1; i >= 0; i--) {
if (result[i].message.role === MessageRole.ASSISTANT) {
result[i].isLastAssistantMessage = true;
break;
}
}
return result;
});
</script>
<div
class="flex h-full flex-col space-y-10 pt-24 {className}"
style="height: auto; min-height: calc(100dvh - 14rem);"
>
{#each displayMessages as { message, toolMessages, isLastAssistantMessage, siblingInfo } (message.id)}
<div use:fadeInView>
<ChatMessage
class="mx-auto w-full max-w-[48rem]"
{message}
{toolMessages}
{isLastAssistantMessage}
{siblingInfo}
/>
</div>
{/each}
</div>