Skip to content

Commit f8b0432

Browse files
committed
fix(renderer): restore html preview context
1 parent f9af684 commit f8b0432

File tree

10 files changed

+310
-16
lines changed

10 files changed

+310
-16
lines changed

src/renderer/src/components/markdown/MarkdownRenderer.vue

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,22 @@ import { useUiSettingsStore } from '@/stores/uiSettingsStore'
2828
const props = defineProps<{
2929
content: string
3030
debug?: boolean
31+
messageId?: string
32+
threadId?: string
3133
}>()
3234
const themeStore = useThemeStore()
3335
const uiSettingsStore = useUiSettingsStore()
3436
// 组件映射表
3537
const artifactStore = useArtifactStore()
3638
// 生成唯一的 message ID 和 thread ID,用于 MarkdownRenderer
37-
const messageId = `artifact-msg-${nanoid()}`
38-
const threadId = `artifact-thread-${nanoid()}`
39+
const fallbackMessageId = `artifact-msg-${nanoid()}`
40+
const fallbackThreadId = `artifact-thread-${nanoid()}`
3941
const referenceStore = useReferenceStore()
4042
const newAgentPresenter = usePresenter('newAgentPresenter')
4143
const referenceNode = ref<HTMLElement | null>(null)
4244
const debouncedContent = ref(props.content)
45+
const effectiveMessageId = computed(() => props.messageId ?? fallbackMessageId)
46+
const effectiveThreadId = computed(() => props.threadId ?? fallbackThreadId)
4347
const codeBlockMonacoOption = computed(() => ({
4448
fontFamily: uiSettingsStore.formattedCodeFontFamily
4549
}))
@@ -63,11 +67,11 @@ setCustomComponents({
6367
reference: (_props) =>
6468
h(ReferenceNode, {
6569
..._props,
66-
messageId,
67-
threadId,
70+
messageId: effectiveMessageId.value,
71+
threadId: effectiveThreadId.value,
6872
onClick() {
6973
// TODO: remove this temporary fallback after search result loading is fully unified.
70-
newAgentPresenter.getSearchResults(_props.messageId ?? '').then((results) => {
74+
newAgentPresenter.getSearchResults(effectiveMessageId.value).then((results) => {
7175
const index = parseInt(_props.node.id)
7276
if (index < results.length) {
7377
window.open(results[index - 1].url, '_blank', 'noopener,noreferrer')
@@ -77,7 +81,7 @@ setCustomComponents({
7781
onMouseEnter() {
7882
console.log('Mouse entered')
7983
referenceStore.hideReference()
80-
newAgentPresenter.getSearchResults(_props.messageId ?? '').then((results) => {
84+
newAgentPresenter.getSearchResults(effectiveMessageId.value).then((results) => {
8185
const index = parseInt(_props.node.id)
8286
if (index - 1 < results.length && referenceNode.value) {
8387
referenceStore.showReference(
@@ -120,8 +124,8 @@ setCustomComponents({
120124
content: v.node.code,
121125
status: 'loaded'
122126
},
123-
messageId,
124-
threadId,
127+
effectiveMessageId.value,
128+
effectiveThreadId.value,
125129
{ force: true }
126130
)
127131
}

src/renderer/src/components/message/MessageBlockContent.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
<template>
33
<template v-for="(part, index) in processedContent" :key="index">
44
<!-- 使用结构化渲染器替代 v-html -->
5-
<MarkdownRenderer v-if="part.type === 'text'" :content="part.content" :loading="part.loading" />
5+
<MarkdownRenderer
6+
v-if="part.type === 'text'"
7+
:content="part.content"
8+
:loading="part.loading"
9+
:message-id="messageId"
10+
:thread-id="threadId"
11+
/>
612

713
<ArtifactThinking v-else-if="part.type === 'thinking' && part.loading" />
814
<div v-else-if="part.type === 'artifact' && part.artifact" class="my-1">

src/renderer/src/components/sidepanel/WorkspacePanel.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,14 +262,19 @@ watch(
262262
return
263263
}
264264
265-
const exists = items.some(
265+
const existsInArtifactItems = items.some(
266266
(item) =>
267267
item.threadId === context.threadId &&
268268
item.messageId === context.messageId &&
269269
item.artifactId === context.artifactId
270270
)
271271
272-
if (!exists) {
272+
const matchesCurrentArtifact =
273+
artifactStore.currentArtifact?.id === context.artifactId &&
274+
artifactStore.currentMessageId === context.messageId &&
275+
artifactStore.currentThreadId === context.threadId
276+
277+
if (!existsInArtifactItems && !matchesCurrentArtifact) {
273278
sidepanelStore.clearArtifact(props.sessionId)
274279
}
275280
},

src/renderer/src/components/sidepanel/WorkspaceViewer.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102

103103
<WorkspacePreviewPane
104104
v-else-if="paneKind === 'preview' && previewKind"
105+
:session-id="props.sessionId"
105106
:preview-kind="previewKind"
106107
:artifact="previewArtifact"
107108
:file-preview="previewFilePreview"

src/renderer/src/components/sidepanel/viewer/WorkspacePreviewPane.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
data-testid="workspace-preview-markdown"
66
>
77
<div class="min-h-full px-4 py-4">
8-
<MarkdownRenderer :content="resolvedContent" />
8+
<MarkdownRenderer
9+
:content="resolvedContent"
10+
:message-id="previewSourceId"
11+
:thread-id="props.sessionId"
12+
/>
913
</div>
1014
</div>
1115

@@ -91,6 +95,7 @@ import MermaidArtifact from '@/components/artifacts/MermaidArtifact.vue'
9195
import ReactArtifact from '@/components/artifacts/ReactArtifact.vue'
9296
9397
const props = defineProps<{
98+
sessionId?: string
9499
previewKind: WorkspacePreviewKind
95100
artifact?: ArtifactState | null
96101
filePreview?: WorkspaceFilePreview | null
@@ -137,6 +142,7 @@ const fileBlock = computed(() => {
137142
138143
const resolvedBlock = computed(() => artifactBlock.value ?? fileBlock.value)
139144
const resolvedContent = computed(() => props.artifact?.content ?? props.filePreview?.content ?? '')
145+
const previewSourceId = computed(() => props.artifact?.id ?? props.filePreview?.path)
140146
const resolvedTitle = computed(
141147
() => props.artifact?.title ?? props.filePreview?.name ?? t('artifacts.preview')
142148
)
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { flushPromises, mount } from '@vue/test-utils'
2+
import { defineComponent, h } from 'vue'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
const { showArtifactMock, getSearchResultsMock, hideReferenceMock, showReferenceMock, nanoidMock } =
6+
vi.hoisted(() => ({
7+
showArtifactMock: vi.fn(),
8+
getSearchResultsMock: vi.fn().mockResolvedValue([]),
9+
hideReferenceMock: vi.fn(),
10+
showReferenceMock: vi.fn(),
11+
nanoidMock: vi.fn()
12+
}))
13+
14+
const setup = async (props: Record<string, unknown> = {}) => {
15+
vi.resetModules()
16+
17+
let customComponents: Record<string, (...args: any[]) => any> = {}
18+
19+
vi.doMock('nanoid', () => ({
20+
nanoid: nanoidMock
21+
}))
22+
23+
vi.doMock('@/stores/artifact', () => ({
24+
useArtifactStore: () => ({
25+
showArtifact: showArtifactMock
26+
})
27+
}))
28+
29+
vi.doMock('@/stores/reference', () => ({
30+
useReferenceStore: () => ({
31+
hideReference: hideReferenceMock,
32+
showReference: showReferenceMock
33+
})
34+
}))
35+
36+
vi.doMock('@/stores/theme', () => ({
37+
useThemeStore: () => ({
38+
isDark: false
39+
})
40+
}))
41+
42+
vi.doMock('@/stores/uiSettingsStore', () => ({
43+
useUiSettingsStore: () => ({
44+
formattedCodeFontFamily: 'monospace'
45+
})
46+
}))
47+
48+
vi.doMock('@/composables/usePresenter', () => ({
49+
usePresenter: () => ({
50+
getSearchResults: getSearchResultsMock
51+
})
52+
}))
53+
54+
vi.doMock('markstream-vue', () => {
55+
const previewPayload = {
56+
id: 'preview-artifact',
57+
artifactType: 'text/html',
58+
artifactTitle: 'HTML Preview',
59+
language: 'html',
60+
node: {
61+
code: '<h1>Hello</h1>'
62+
}
63+
}
64+
65+
const NodeRenderer = defineComponent({
66+
name: 'NodeRenderer',
67+
setup() {
68+
return () =>
69+
customComponents.code_block?.({
70+
node: {
71+
language: 'html',
72+
code: '<h1>Hello</h1>',
73+
raw: '<h1>Hello</h1>'
74+
}
75+
}) ?? h('div')
76+
}
77+
})
78+
79+
const CodeBlockNode = defineComponent({
80+
name: 'CodeBlockNode',
81+
emits: ['previewCode'],
82+
mounted() {
83+
this.$emit('previewCode', previewPayload)
84+
},
85+
render() {
86+
return h('div', { 'data-testid': 'code-block-node' })
87+
}
88+
})
89+
90+
const ReferenceNode = defineComponent({
91+
name: 'ReferenceNode',
92+
render() {
93+
return h('div')
94+
}
95+
})
96+
97+
const MermaidBlockNode = defineComponent({
98+
name: 'MermaidBlockNode',
99+
render() {
100+
return h('div')
101+
}
102+
})
103+
104+
return {
105+
default: NodeRenderer,
106+
NodeRenderer,
107+
CodeBlockNode,
108+
ReferenceNode,
109+
MermaidBlockNode,
110+
setCustomComponents: (components: Record<string, (...args: any[]) => any>) => {
111+
customComponents = components
112+
}
113+
}
114+
})
115+
116+
const MarkdownRenderer = (await import('@/components/markdown/MarkdownRenderer.vue')).default
117+
const wrapper = mount(MarkdownRenderer, {
118+
props: {
119+
content: '```html\n<h1>Hello</h1>\n```',
120+
...props
121+
}
122+
})
123+
124+
await flushPromises()
125+
126+
return { wrapper }
127+
}
128+
129+
describe('MarkdownRenderer', () => {
130+
beforeEach(() => {
131+
showArtifactMock.mockReset()
132+
getSearchResultsMock.mockReset()
133+
getSearchResultsMock.mockResolvedValue([])
134+
hideReferenceMock.mockReset()
135+
showReferenceMock.mockReset()
136+
nanoidMock.mockReset()
137+
nanoidMock.mockReturnValueOnce('fallback-message').mockReturnValueOnce('fallback-thread')
138+
})
139+
140+
it('uses the provided message and thread ids for HTML preview artifacts', async () => {
141+
await setup({
142+
messageId: 'message-1',
143+
threadId: 'thread-1'
144+
})
145+
146+
expect(showArtifactMock).toHaveBeenCalledWith(
147+
{
148+
id: 'preview-artifact',
149+
type: 'text/html',
150+
title: 'HTML Preview',
151+
language: 'html',
152+
content: '<h1>Hello</h1>',
153+
status: 'loaded'
154+
},
155+
'message-1',
156+
'thread-1',
157+
{ force: true }
158+
)
159+
})
160+
161+
it('falls back to local ids when no message or thread ids are provided', async () => {
162+
await setup()
163+
164+
expect(showArtifactMock).toHaveBeenCalledWith(
165+
{
166+
id: 'preview-artifact',
167+
type: 'text/html',
168+
title: 'HTML Preview',
169+
language: 'html',
170+
content: '<h1>Hello</h1>',
171+
status: 'loaded'
172+
},
173+
'artifact-msg-fallback-message',
174+
'artifact-thread-fallback-thread',
175+
{ force: true }
176+
)
177+
})
178+
})

test/renderer/components/WorkspacePanel.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ describe('WorkspacePanel', () => {
212212
sessionState.sections.files = true
213213
sessionState.sections.git = true
214214
sessionState.sections.artifacts = true
215+
artifactStore.currentArtifact = null
216+
artifactStore.currentMessageId = null
217+
artifactStore.currentThreadId = null
215218

216219
showArtifactMock.mockReset()
217220
toggleSectionMock.mockReset()
@@ -484,4 +487,34 @@ describe('WorkspacePanel', () => {
484487

485488
wrapper.unmount()
486489
})
490+
491+
it('keeps the current temporary artifact selection when it is not part of artifact items', async () => {
492+
sessionState.selectedArtifactContext = {
493+
threadId: 's1',
494+
messageId: 'C:/repo/README.md',
495+
artifactId: 'temp-html-preview'
496+
}
497+
artifactStore.currentArtifact = {
498+
id: 'temp-html-preview',
499+
type: 'text/html',
500+
title: 'HTML Preview',
501+
content: '<h1>Hello</h1>',
502+
status: 'loaded'
503+
}
504+
artifactStore.currentMessageId = 'C:/repo/README.md'
505+
artifactStore.currentThreadId = 's1'
506+
507+
const wrapper = mount(WorkspacePanel, {
508+
props: {
509+
sessionId: 's1',
510+
workspacePath: 'C:/repo'
511+
}
512+
})
513+
514+
await flushPromises()
515+
516+
expect(clearArtifactMock).not.toHaveBeenCalled()
517+
518+
wrapper.unmount()
519+
})
487520
})

0 commit comments

Comments
 (0)