diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce77d95a1..4142ca058 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/src/components/ChatPanel.vue b/src/components/ChatPanel.vue
index 5eab64a38..c30225287 100644
--- a/src/components/ChatPanel.vue
+++ b/src/components/ChatPanel.vue
@@ -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() {
-
+
diff --git a/src/components/chat/ChatMessage.vue b/src/components/chat/ChatMessage.vue
index 8750c13dd..5674599d3 100644
--- a/src/components/chat/ChatMessage.vue
+++ b/src/components/chat/ChatMessage.vue
@@ -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, { toolCallId: string }>
@@ -121,9 +125,12 @@ function partKey(part: UIMessagePart, index: number): stri
class="rounded-xl rounded-tl-md bg-hover px-3 py-2 text-xs leading-relaxed text-surface"
>
diff --git a/tests/e2e/chat/panel.spec.ts b/tests/e2e/chat/panel.spec.ts
index 62587e8cd..c10b09e90 100644
--- a/tests/e2e/chat/panel.spec.ts
+++ b/tests/e2e/chat/panel.spec.ts
@@ -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')