perf(chat): release completed streaming parser history (#553)

- Keep only the active assistant response in streaming Markdown mode.\n- Remount completed responses in static mode to release incremental AST caches.\n- Preserve final Markdown, code rendering, and theme behavior.
This commit is contained in:
Danila Poyarkov 2026-08-18 17:43:49 +03:00 committed by GitHub
parent 4e48420ac1
commit 4342fd3262
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 32 additions and 2 deletions

View file

@ -51,6 +51,7 @@
### Performance
- Release obsolete streamed Markdown parser history after each AI response completes, preventing chat memory from multiplying with every streamed chunk. (#544)
- Use cached axis-aligned world positions for hit testing untransformed layer chains and add representative 500/2,000-node interaction profiles. (#527)
- Coalesce writable-document autosaves that overlap an active `.fig` export while preserving a trailing save for newer edits. (#528)
- Defer JSX generation and syntax highlighting until the Code panel is active, keeping large canvas selections responsive. (#500)

View file

@ -73,6 +73,13 @@ const failureMessage = computed(() => {
}
})
const status = computed(() => chat.value?.status ?? 'ready')
function isStreamingMessage(message: UIMessage, index: number): boolean {
return (
message.role === 'assistant' &&
index === messages.value.length - 1 &&
(status.value === 'submitted' || status.value === 'streaming')
)
}
const isThinking = computed(() => {
const s = status.value
if (s !== 'submitted' && s !== 'streaming') return false
@ -254,7 +261,12 @@ function handleClearChat() {
<!-- Messages -->
<div v-else data-test-id="chat-messages" class="flex flex-col gap-3">
<ChatMessage v-for="msg in messages" :key="msg.id" :message="msg" />
<ChatMessage
v-for="(msg, index) in messages"
:key="msg.id"
:message="msg"
:streaming="isStreamingMessage(msg, index)"
/>
<!-- Thinking indicator: shown when AI is working but no visible activity -->
<div v-if="isThinking" data-test-id="chat-typing-indicator" class="flex gap-2">

View file

@ -15,9 +15,13 @@ import ImageAttachment from '@/components/chat/attachment/image/ImageAttachment.
import type { UIDataTypes, UIMessage, UIMessagePart, UITools } from 'ai'
const { message } = defineProps<{ message: UIMessage }>()
const { message, streaming = false } = defineProps<{
message: UIMessage
streaming?: boolean
}>()
const { dialogs } = useI18n()
const isDark = computed(() => resolvedAppTheme.value === 'dark')
const markdownMode = computed(() => (streaming ? 'streaming' : 'static'))
const imageAttachments = imageAttachmentsForMessage(message.id)
type ToolPart = Extract<UIMessagePart<UIDataTypes, UITools>, { toolCallId: string }>
@ -121,9 +125,12 @@ function partKey(part: UIMessagePart<UIDataTypes, UITools>, index: number): stri
class="rounded-xl rounded-tl-md bg-hover px-3 py-2 text-xs leading-relaxed text-surface"
>
<Markdown
:key="markdownMode"
:content="part.text"
:is-dark="isDark"
:mermaid="false"
:mode="markdownMode"
:data-chat-markdown-mode="markdownMode"
class="chat-markdown [&_[data-stream-markdown=code]]:!bg-input"
/>
</div>

View file

@ -261,6 +261,16 @@ test('assistant responds', async () => {
}
})
test('completed Markdown responses release streaming parser history', async () => {
await chatInput().fill('Show a code block')
await chatInput().press('Enter')
const markdown = page.locator('.chat-markdown').last()
await expect(markdown).toBeVisible()
await expect(markdown).toHaveAttribute('data-chat-markdown-mode', 'static')
await expect(markdown.locator('.shiki').first()).toBeVisible()
})
test('assistant code blocks follow the active theme with readable contrast', async () => {
await chatInput().fill('Show a code block')
await chatInput().press('Enter')