import { shouldHideDuplicateOptimisticAssistant } from '../../agent-response/visibility' import type { StudioMessage } from '../../protocol/studio-agent-types' import type { StudioCommandPanelSnapshot } from './store' export interface StudioCommandRowView { message: StudioMessage | null isStreamingTarget: boolean streamedText: string showCaret: boolean } const emptyVisibleMessageIds: string[] = [] let visibleMessageIdsCache = { messages: null as StudioMessage[] | null, isBusy: false, ids: emptyVisibleMessageIds, } export function selectVisibleMessageIds(snapshot: StudioCommandPanelSnapshot): string[] { if ( visibleMessageIdsCache.messages === snapshot.messages && visibleMessageIdsCache.isBusy === snapshot.isBusy ) { return visibleMessageIdsCache.ids } const ids = snapshot.messages .filter((message, index, messages) => { if (message.role === 'user') { return true } if (shouldHideDuplicateOptimisticAssistant(messages, index)) { return false } return shouldRenderAssistantMessage(message, { isLast: index === messages.length - 1, isBusy: snapshot.isBusy, }) }) .map((message) => message.renderId ?? message.id) visibleMessageIdsCache = { messages: snapshot.messages, isBusy: snapshot.isBusy, ids, } return ids } export function selectRowView( snapshot: StudioCommandPanelSnapshot, messageId: string, ): StudioCommandRowView { const message = snapshot.messages.find((entry) => (entry.renderId ?? entry.id) === messageId) ?? null if (!message) { return { message: null, isStreamingTarget: false, streamedText: '', showCaret: false, } } const lastMessage = snapshot.messages.at(-1) ?? null const streamIntoLastAssistant = Boolean( lastMessage && lastMessage.role === 'assistant' && (snapshot.isBusy || snapshot.latestAssistantText || snapshot.animatedAssistantText) ) const isStreamingTarget = streamIntoLastAssistant && lastMessage?.id === message.id return { message, isStreamingTarget, streamedText: isStreamingTarget ? snapshot.animatedAssistantText : '', showCaret: Boolean( isStreamingTarget && (snapshot.isBusy || snapshot.latestAssistantText !== snapshot.animatedAssistantText) ), } } function shouldRenderAssistantMessage( message: Extract, options: { isLast: boolean; isBusy: boolean }, ): boolean { const hasRenderableText = message.parts.some((part) => ( (part.type === 'text' || part.type === 'reasoning') && part.text.trim() )) const hasToolParts = message.parts.some((part) => part.type === 'tool') if (hasRenderableText || hasToolParts) { return true } return options.isLast && options.isBusy }