Skip to content

fix(renderer): restore html preview context#1407

Merged
zerob13 merged 1 commit intodevfrom
fix/issue-1406
Mar 30, 2026
Merged

fix(renderer): restore html preview context#1407
zerob13 merged 1 commit intodevfrom
fix/issue-1406

Conversation

@zerob13
Copy link
Copy Markdown
Collaborator

@zerob13 zerob13 commented Mar 30, 2026

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed artifact clearing logic that was inadvertently removing currently selected artifacts during context changes.
  • Improvements

    • Enhanced code preview rendering system with improved session and message context tracking across the application.
    • Preview components now properly maintain and propagate identification metadata throughout the preview pipeline.
    • Better integration of message and thread context in artifact preview displays.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

This change adds optional messageId and threadId props to MarkdownRenderer that flow through the component hierarchy starting from MessageBlockContent and WorkspacePreviewPane. It also refines artifact clearing logic in WorkspacePanel to preserve currently selected artifacts, and updates corresponding test coverage.

Changes

Cohort / File(s) Summary
Prop propagation to MarkdownRenderer
src/renderer/src/components/markdown/MarkdownRenderer.vue, src/renderer/src/components/message/MessageBlockContent.vue, src/renderer/src/components/sidepanel/viewer/WorkspacePreviewPane.vue, src/renderer/src/components/sidepanel/WorkspaceViewer.vue
Added optional messageId and threadId props with fallback ID generation via nanoid(). Props flow from MessageBlockContent and WorkspacePreviewPaneMarkdownRenderer, with sessionId now passed through WorkspaceViewerWorkspacePreviewPane.
Artifact selection and clearing logic
src/renderer/src/components/sidepanel/WorkspacePanel.vue
Modified watcher to clear artifacts only when absent from computed artifactItems AND not currently active in artifactStore; preserves selected artifacts during initialization.
MarkdownRenderer tests
test/renderer/components/MarkdownRenderer.test.ts, test/renderer/components/message/MessageBlockContent.test.ts
Added new test suite verifying showArtifact receives expected message/thread IDs (from props or fallback), and updated MarkdownRenderer stub to expose messageId/threadId via test attributes.
Workspace component tests
test/renderer/components/WorkspacePanel.test.ts, test/renderer/components/WorkspaceViewer.test.ts, test/renderer/components/WorkspacePreviewPane.test.ts
Updated test setups to initialize artifact store state, pass sessionId through component hierarchy, and verify new prop bindings are correctly propagated and exposed on DOM elements.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • deepinfect

Poem

🐰 Props flow like carrots down the tree so tall,
Message IDs whisper through components all,
Artifacts preserved with logic so keen,
Fallback IDs dance in the realm between,
Tests now verify each thread and each call!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(renderer): restore html preview context' accurately describes the main change: restoring HTML preview context by threading messageId and threadId through the MarkdownRenderer and related components.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1406

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/renderer/src/components/sidepanel/WorkspacePanel.vue (1)

258-281: ⚠️ Potential issue | 🟠 Major

Watch the current-artifact fields too.

Line 272 derives matchesCurrentArtifact from artifactStore.current*, but the watcher only subscribes to artifactItems and selectedArtifactContext. If selectedArtifactContext is present first and syncArtifact() fills currentMessageId/currentThreadId later, this callback still clears once and never re-evaluates when the store catches up. Please add the three current-artifact fields to the watch sources, or watch a computed matchesCurrentArtifact instead.

🔧 Suggested change
 watch(
-  [artifactItems, () => sessionState.value.selectedArtifactContext] as const,
-  ([items, context]) => {
+  [
+    artifactItems,
+    () => sessionState.value.selectedArtifactContext,
+    () => artifactStore.currentArtifact?.id,
+    () => artifactStore.currentMessageId,
+    () => artifactStore.currentThreadId
+  ] as const,
+  ([items, context, currentArtifactId, currentMessageId, currentThreadId]) => {
     if (!context) {
       return
     }
 
     const existsInArtifactItems = items.some(
@@
 
     const matchesCurrentArtifact =
-      artifactStore.currentArtifact?.id === context.artifactId &&
-      artifactStore.currentMessageId === context.messageId &&
-      artifactStore.currentThreadId === context.threadId
+      currentArtifactId === context.artifactId &&
+      currentMessageId === context.messageId &&
+      currentThreadId === context.threadId
 
     if (!existsInArtifactItems && !matchesCurrentArtifact) {
       sidepanelStore.clearArtifact(props.sessionId)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/sidepanel/WorkspacePanel.vue` around lines 258 -
281, The watcher currently listens to artifactItems and () =>
sessionState.value.selectedArtifactContext but not the artifactStore.current*
fields, causing a race where matchesCurrentArtifact (derived from
artifactStore.currentArtifact/currentMessageId/currentThreadId) may change later
without retriggering the watcher; update the watch sources to include
artifactStore.currentArtifact, artifactStore.currentMessageId, and
artifactStore.currentThreadId (or replace with a single computed
matchesCurrentArtifact) so the callback re-evaluates when the store updates,
keeping the existing logic that calls
sidepanelStore.clearArtifact(props.sessionId) when neither existsInArtifactItems
nor matchesCurrentArtifact are true.
src/renderer/src/components/sidepanel/WorkspaceViewer.vue (1)

103-109: ⚠️ Potential issue | 🟠 Major

Forward the originating artifact messageId here too.

Line 105 passes the thread id, but the owning message id is still dropped at this boundary. WorkspacePreviewPane now falls back to artifact.id, and MarkdownRenderer uses that value as the exact key for getSearchResults() and as part of nested preview context. For artifacts opened from WorkspacePanel, artifact.id is only the artifact identifier, so citations and child previews lose their original message context. Please pass sessionState.value.selectedArtifactContext?.messageId for artifact previews and keep the path fallback only for raw workspace files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/sidepanel/WorkspaceViewer.vue` around lines 103 -
109, The WorkspacePreviewPane call drops the originating artifact message id,
causing MarkdownRenderer/getSearchResults() to lose message context; update the
WorkspacePreviewPane invocation to forward the originating message id (e.g. pass
sessionState.value.selectedArtifactContext?.messageId) as a prop (named
something like originatingMessageId or messageId) alongside the existing props
(previewArtifact/previewFilePreview) and ensure
WorkspacePreviewPane/MarkdownRenderer use that prop and only fall back to
artifact.id for raw workspace files; reference WorkspacePreviewPane,
previewArtifact, previewFilePreview,
sessionState.value.selectedArtifactContext?.messageId, MarkdownRenderer, and
getSearchResults() when making the change.
src/renderer/src/components/markdown/MarkdownRenderer.vue (1)

66-134: ⚠️ Potential issue | 🟠 Major

Pass a custom-id to setCustomComponents() to scope component handlers per renderer instance.

Line 66 calls setCustomComponents() without a custom-id, which registers handlers in the global/default mapping. In views with multiple MarkdownRenderer instances, each subsequent renderer overwrites the handlers used by previous ones, causing reference previews and code previews to be associated with the wrong message context. Pass a unique custom-id (derived from effectiveMessageId and/or effectiveThreadId) as the second parameter to setCustomComponents() to isolate each renderer's handlers:

setCustomComponents(
  {
    reference: (_props) => ...,
    mermaid: (_props) => ...,
    code_block: (_props) => ...
  },
  `markdown-${effectiveMessageId.value}-${effectiveThreadId.value}`
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/markdown/MarkdownRenderer.vue` around lines 66 -
134, The handlers passed to setCustomComponents are being registered globally
causing cross-instance collisions; update the setCustomComponents call to pass a
per-renderer custom id (e.g. derived from effectiveMessageId.value and/or
effectiveThreadId.value) as the second argument so each MarkdownRenderer
instance scopes its handlers (keep using the same component factories reference,
mermaid, code_block, but call setCustomComponents(components, uniqueId) where
uniqueId uses effectiveMessageId/effectiveThreadId).
🧹 Nitpick comments (2)
test/renderer/components/WorkspacePanel.test.ts (1)

491-519: This regression test misses the ordering that still matters.

Here the matching artifactStore.current* fields are populated before mount. The remaining failure mode is the opposite order: selectedArtifactContext exists first, then the artifact store catches up later. Adding that sequence here would guard the async syncArtifact() path as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/renderer/components/WorkspacePanel.test.ts` around lines 491 - 519, The
test should also cover the ordering where sessionState.selectedArtifactContext
is set before the artifact store values arrive so the async syncArtifact() path
is exercised; modify the 'keeps the current temporary artifact selection...'
spec to set sessionState.selectedArtifactContext first, then mount
WorkspacePanel, then (after mount) assign artifactStore.currentArtifact,
artifactStore.currentMessageId and artifactStore.currentThreadId, call await
flushPromises(), and finally assert clearArtifactMock was not called; target the
same symbols (sessionState.selectedArtifactContext,
artifactStore.currentArtifact, artifactStore.currentMessageId,
artifactStore.currentThreadId, WorkspacePanel and syncArtifact behavior) so the
opposite-order timing is covered.
test/renderer/components/message/MessageBlockContent.test.ts (1)

53-73: Consider adding the loading prop to the stub for completeness.

The stub includes content, messageId, and threadId, but the actual MarkdownRenderer component and its usage in MessageBlockContent.vue also passes a :loading prop. While the current tests don't require it, adding it to the stub would make the mock more representative of the real component.

♻️ Optional: Add loading prop to stub
 vi.mock('@/components/markdown/MarkdownRenderer.vue', () => ({
   default: defineComponent({
     name: 'MarkdownRenderer',
     props: {
       content: {
         type: String,
         default: ''
       },
+      loading: {
+        type: Boolean,
+        default: false
+      },
       messageId: {
         type: String,
         default: undefined
       },
       threadId: {
         type: String,
         default: undefined
       }
     },
     template:
       '<div class="markdown-stub" :data-message-id="messageId" :data-thread-id="threadId">{{ content }}</div>'
   })
 }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/renderer/components/message/MessageBlockContent.test.ts` around lines 53
- 73, The MarkdownRenderer stub in the test mock is missing the loading prop
used by MessageBlockContent.vue; update the mock definition (the default
defineComponent named 'MarkdownRenderer' in the test) to declare a loading prop
(type: Boolean, default: false) and expose it in the template (e.g., as a data
attribute like :data-loading="loading") so the stub mirrors the real component's
API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/renderer/src/components/markdown/MarkdownRenderer.vue`:
- Around line 66-134: The handlers passed to setCustomComponents are being
registered globally causing cross-instance collisions; update the
setCustomComponents call to pass a per-renderer custom id (e.g. derived from
effectiveMessageId.value and/or effectiveThreadId.value) as the second argument
so each MarkdownRenderer instance scopes its handlers (keep using the same
component factories reference, mermaid, code_block, but call
setCustomComponents(components, uniqueId) where uniqueId uses
effectiveMessageId/effectiveThreadId).

In `@src/renderer/src/components/sidepanel/WorkspacePanel.vue`:
- Around line 258-281: The watcher currently listens to artifactItems and () =>
sessionState.value.selectedArtifactContext but not the artifactStore.current*
fields, causing a race where matchesCurrentArtifact (derived from
artifactStore.currentArtifact/currentMessageId/currentThreadId) may change later
without retriggering the watcher; update the watch sources to include
artifactStore.currentArtifact, artifactStore.currentMessageId, and
artifactStore.currentThreadId (or replace with a single computed
matchesCurrentArtifact) so the callback re-evaluates when the store updates,
keeping the existing logic that calls
sidepanelStore.clearArtifact(props.sessionId) when neither existsInArtifactItems
nor matchesCurrentArtifact are true.

In `@src/renderer/src/components/sidepanel/WorkspaceViewer.vue`:
- Around line 103-109: The WorkspacePreviewPane call drops the originating
artifact message id, causing MarkdownRenderer/getSearchResults() to lose message
context; update the WorkspacePreviewPane invocation to forward the originating
message id (e.g. pass sessionState.value.selectedArtifactContext?.messageId) as
a prop (named something like originatingMessageId or messageId) alongside the
existing props (previewArtifact/previewFilePreview) and ensure
WorkspacePreviewPane/MarkdownRenderer use that prop and only fall back to
artifact.id for raw workspace files; reference WorkspacePreviewPane,
previewArtifact, previewFilePreview,
sessionState.value.selectedArtifactContext?.messageId, MarkdownRenderer, and
getSearchResults() when making the change.

---

Nitpick comments:
In `@test/renderer/components/message/MessageBlockContent.test.ts`:
- Around line 53-73: The MarkdownRenderer stub in the test mock is missing the
loading prop used by MessageBlockContent.vue; update the mock definition (the
default defineComponent named 'MarkdownRenderer' in the test) to declare a
loading prop (type: Boolean, default: false) and expose it in the template
(e.g., as a data attribute like :data-loading="loading") so the stub mirrors the
real component's API.

In `@test/renderer/components/WorkspacePanel.test.ts`:
- Around line 491-519: The test should also cover the ordering where
sessionState.selectedArtifactContext is set before the artifact store values
arrive so the async syncArtifact() path is exercised; modify the 'keeps the
current temporary artifact selection...' spec to set
sessionState.selectedArtifactContext first, then mount WorkspacePanel, then
(after mount) assign artifactStore.currentArtifact,
artifactStore.currentMessageId and artifactStore.currentThreadId, call await
flushPromises(), and finally assert clearArtifactMock was not called; target the
same symbols (sessionState.selectedArtifactContext,
artifactStore.currentArtifact, artifactStore.currentMessageId,
artifactStore.currentThreadId, WorkspacePanel and syncArtifact behavior) so the
opposite-order timing is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1a5f987f-a9a0-4952-be9e-f34cd6aae31d

📥 Commits

Reviewing files that changed from the base of the PR and between f9af684 and f8b0432.

📒 Files selected for processing (10)
  • src/renderer/src/components/markdown/MarkdownRenderer.vue
  • src/renderer/src/components/message/MessageBlockContent.vue
  • src/renderer/src/components/sidepanel/WorkspacePanel.vue
  • src/renderer/src/components/sidepanel/WorkspaceViewer.vue
  • src/renderer/src/components/sidepanel/viewer/WorkspacePreviewPane.vue
  • test/renderer/components/MarkdownRenderer.test.ts
  • test/renderer/components/WorkspacePanel.test.ts
  • test/renderer/components/WorkspacePreviewPane.test.ts
  • test/renderer/components/WorkspaceViewer.test.ts
  • test/renderer/components/message/MessageBlockContent.test.ts

@zerob13 zerob13 merged commit 457b13f into dev Mar 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant