44 ref =" dynamicScrollerRef"
55 class =" message-list-container relative flex-1 scrollbar-hide overflow-y-auto w-full h-full pr-12 lg:pr-12 transition-opacity duration-300"
66 :class =" { 'opacity-0': !visible }"
7- :items =" items "
7+ :items =" virtualItems "
88 list-class =" w-full pt-4"
99 :min-item-size =" 48"
1010 :buffer =" 200"
4444 </DynamicScrollerItem >
4545 </template >
4646 <template #after >
47+ <template :active =" false " v-for =" streamItem in streamingTailItems " :key =" streamItem .id " >
48+ <div
49+ v-if =" streamItem.message?.role === 'assistant'"
50+ @mouseenter =" minimap.handleHover(streamItem.id)"
51+ @mouseleave =" minimap.handleHover(null)"
52+ >
53+ <MessageItemAssistant
54+ :message =" streamItem.message as AssistantMessage"
55+ :is-capturing-image =" capture.isCapturing.value"
56+ @copy-image =" handleCopyImage"
57+ @variant-changed =" wrapScrollToMessage"
58+ @trace =" handleTrace"
59+ />
60+ </div >
61+ </template >
4762 <div ref =" scrollAnchor" class =" h-8" />
4863 </template >
4964 </DynamicScroller >
@@ -175,6 +190,36 @@ const minimapMessages = computed(() => {
175190 return chatStore .variantAwareMessages
176191})
177192
193+ const isStreamingMessage = (message : Message ) => {
194+ if (message .role !== ' assistant' ) return false
195+ if (message .status === ' pending' ) return true
196+
197+ const blocks = (message as AssistantMessage ).content
198+ if (! Array .isArray (blocks )) return false
199+ return blocks .some ((block ) =>
200+ [' loading' , ' reading' , ' optimizing' , ' pending' ].includes (block .status )
201+ )
202+ }
203+
204+ const streamingTailItems = computed (() => {
205+ const tail: MessageListItem [] = []
206+ for (let index = props .items .length - 1 ; index >= 0 ; index -- ) {
207+ const item = props .items [index ]
208+ const message = item .message
209+ if (! message || ! isStreamingMessage (message )) break
210+ tail .unshift (item )
211+ }
212+ return tail
213+ })
214+
215+ const streamingTailIds = computed (() => new Set (streamingTailItems .value .map ((item ) => item .id )))
216+
217+ const virtualItems = computed (() => {
218+ const streamingIds = streamingTailIds .value
219+ if (! streamingIds .size ) return props .items
220+ return props .items .filter ((item ) => ! streamingIds .has (item .id ))
221+ })
222+
178223// === Constants ===
179224const HIGHLIGHT_CLASS = ' selection-highlight'
180225const HIGHLIGHT_ACTIVE_CLASS = ' selection-highlight-active'
@@ -554,7 +599,7 @@ const refreshVirtualScroller = async (messageId?: string) => {
554599 await new Promise ((resolve ) => requestAnimationFrame (resolve ))
555600
556601 const scroller = dynamicScrollerRef .value
557- if (messageId && scroller ?.scrollToItem ) {
602+ if (messageId && ! streamingTailIds . value . has ( messageId ) && scroller ?.scrollToItem ) {
558603 const index = props .items .findIndex ((item ) => item .id === messageId )
559604 if (index !== - 1 ) {
560605 scroller .scrollToItem (index )
@@ -566,6 +611,10 @@ const refreshVirtualScroller = async (messageId?: string) => {
566611
567612const wrapScrollToMessage = async (messageId : string ) => {
568613 void chatStore .ensureMessagesLoadedByIds ([messageId ])
614+ if (streamingTailIds .value .has (messageId )) {
615+ scrollToBottom (true )
616+ return
617+ }
569618 scrollToMessage (messageId , () => props .items )
570619 await refreshVirtualScroller (messageId )
571620}
0 commit comments