<template>
<div ref="containerEl" class="message-list" @scroll="onScroll">
<div class="message-list-inner" :class="{ 'is-hidden': !contentVisible }">
<div class="messages-group">
<div
v-for="msg in chat.messages"
:key="msg.id"
:class="{ 'msg-enter': msg.animate }"
>
<component :is="resolveComponent(msg)" :msg="msg" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, nextTick, onMounted } from 'vue'
import { useChatStore } from '@/stores/chat.js'
import UserMessage from '@/components/messages/UserMessage.vue'
import AssistantMessage from '@/components/messages/AssistantMessage.vue'
import SummaryCard from '@/components/messages/SummaryCard.vue'
import CompressionNotice from '@/components/messages/CompressionNotice.vue'
import ErrorMessage from '@/components/messages/ErrorMessage.vue'
const chat = useChatStore()
const containerEl = ref(null)
const contentVisible = ref(false)
let userScrolledUp = false
function resolveComponent(msg) {
if (msg.role === 'user') return UserMessage
if (msg.type === 'summary') return SummaryCard
if (msg.type === 'compression_notice') return CompressionNotice
if (msg.type === 'error') return ErrorMessage
return AssistantMessage
}
function scrollToBottom(smooth = false) {
const el = containerEl.value
if (!el) return
el.scrollTo({ top: el.scrollHeight, behavior: smooth ? 'smooth' : 'instant' })
}
function onScroll() {
const el = containerEl.value
if (!el) return
const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
userScrolledUp = distFromBottom > 100
}
// Auto-scroll when messages change — skip during history load
watch(
() => chat.messages.length,
() => {
if (!userScrolledUp && !chat.loading) nextTick(() => scrollToBottom())
}
)
// Auto-scroll during streaming text
watch(
() => chat.streamingMsg?.text,
() => {
if (!userScrolledUp) nextTick(() => scrollToBottom())
}
)
// Auto-scroll when tool cards are added or complete
watch(
() => chat.streamingMsg?.tools?.length,
() => {
if (!userScrolledUp) nextTick(() => scrollToBottom())
}
)
// Double rAF ensures browser has fully computed layout before acting
function afterLayout(fn) {
nextTick(() => requestAnimationFrame(() => requestAnimationFrame(fn)))
}
// When session changes: hide content immediately, reset scroll state
watch(
() => chat.currentId,
() => {
userScrolledUp = false
contentVisible.value = false
}
)
// When session finishes loading: scroll to bottom while invisible, then reveal
watch(
() => chat.loading,
(loading) => {
if (!loading) {
contentVisible.value = false
afterLayout(() => {
scrollToBottom()
contentVisible.value = true
})
}
}
)
// Handle case where loading is already false when component mounts
onMounted(() => {
if (!chat.loading && chat.messages.length) {
afterLayout(() => {
scrollToBottom()
contentVisible.value = true
})
} else if (!chat.loading) {
contentVisible.value = true
}
})
</script>
<style scoped>
.messages-group {
display: flex;
flex-direction: column;
gap: 32px;
}
</style>