diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte
index 673f3b1069..41b9d0fc95 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte
@@ -17,8 +17,10 @@
import { AgenticSectionType } from '$lib/enums';
import { AGENTIC_TAGS, AGENTIC_REGEX } from '$lib/constants/agentic';
import { formatJsonPretty } from '$lib/utils/formatters';
+ import type { DatabaseMessage } from '$lib/types/database';
interface Props {
+ message?: DatabaseMessage;
content: string;
isStreaming?: boolean;
}
@@ -31,7 +33,7 @@
toolResult?: string;
}
- let { content, isStreaming = false }: Props = $props();
+ let { content, message, isStreaming = false }: Props = $props();
const sections = $derived(parseAgenticContent(content));
@@ -184,7 +186,7 @@
{#each sections as section, index (index)}
{#if section.type === AgenticSectionType.TEXT}
diff --git a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte
index fb98f461b7..2c46330b52 100644
--- a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte
+++ b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte
@@ -14,6 +14,7 @@
import { rehypeRestoreTableHtml } from '$lib/markdown/table-html-restorer';
import { rehypeEnhanceLinks } from '$lib/markdown/enhance-links';
import { rehypeEnhanceCodeBlocks } from '$lib/markdown/enhance-code-blocks';
+ import { rehypeResolveAttachmentImages } from '$lib/markdown/resolve-attachment-images';
import { remarkLiteralHtml } from '$lib/markdown/literal-html';
import { copyCodeToClipboard, preprocessLaTeX } from '$lib/utils';
import '$styles/katex-custom.scss';
@@ -21,9 +22,11 @@
import githubLightCss from 'highlight.js/styles/github.css?inline';
import { mode } from 'mode-watcher';
import CodePreviewDialog from './CodePreviewDialog.svelte';
+ import type { DatabaseMessage } from '$lib/types/database';
import { getImageErrorFallbackHtml } from '$lib/utils/image-error-fallback';
interface Props {
+ message?: DatabaseMessage;
content: string;
class?: string;
disableMath?: boolean;
@@ -34,7 +37,7 @@
html: string;
}
- let { content, class: className = '', disableMath = false }: Props = $props();
+ let { content, message, class: className = '', disableMath = false }: Props = $props();
let containerRef = $state
();
let renderedBlocks = $state([]);
@@ -49,6 +52,8 @@
const themeStyleId = `highlight-theme-${(window.idxThemeStyle = (window.idxThemeStyle ?? 0) + 1)}`;
let processor = $derived(() => {
+ // Force reactivity on message changes
+ void message;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let proc: any = remark().use(remarkGfm); // GitHub Flavored Markdown
@@ -70,6 +75,7 @@
.use(rehypeRestoreTableHtml) // Restore limited HTML (e.g.,
, ) inside Markdown tables
.use(rehypeEnhanceLinks) // Add target="_blank" to links
.use(rehypeEnhanceCodeBlocks) // Wrap code blocks with header and actions
+ .use(rehypeResolveAttachmentImages, { message })
.use(rehypeStringify, { allowDangerousHtml: true }); // Convert to HTML string
});
diff --git a/tools/server/webui/src/lib/markdown/resolve-attachment-images.ts b/tools/server/webui/src/lib/markdown/resolve-attachment-images.ts
new file mode 100644
index 0000000000..0e5845e4f6
--- /dev/null
+++ b/tools/server/webui/src/lib/markdown/resolve-attachment-images.ts
@@ -0,0 +1,32 @@
+import type { Root as HastRoot } from 'hast';
+import { visit } from 'unist-util-visit';
+import type { DatabaseMessage, DatabaseMessageExtraImageFile } from '$lib/types/database';
+
+/**
+ * Rehype plugin to resolve attachment image sources.
+ * Converts attachment names (e.g., "mcp-attachment-xxx.png") to base64 data URLs.
+ */
+export function rehypeResolveAttachmentImages(options: { message?: DatabaseMessage }) {
+ return (tree: HastRoot) => {
+ visit(tree, 'element', (node) => {
+ if (node.tagName === 'img' && node.properties?.src) {
+ const src = String(node.properties.src);
+
+ // Skip data URLs and external URLs
+ if (src.startsWith('data:') || src.startsWith('http')) {
+ return;
+ }
+
+ // Find matching attachment
+ const attachment = options.message?.extra?.find(
+ (a): a is DatabaseMessageExtraImageFile => a.type === 'IMAGE' && a.name === src
+ );
+
+ // Replace with base64 URL if found
+ if (attachment?.base64Url) {
+ node.properties.src = attachment.base64Url;
+ }
+ }
+ });
+ };
+}
diff --git a/tools/server/webui/src/lib/services/chat.service.ts b/tools/server/webui/src/lib/services/chat.service.ts
index 4036443fd2..cf435d47b0 100644
--- a/tools/server/webui/src/lib/services/chat.service.ts
+++ b/tools/server/webui/src/lib/services/chat.service.ts
@@ -619,10 +619,14 @@ export class ChatService {
});
}
- const imageFiles = message.extra.filter(
- (extra: DatabaseMessageExtra): extra is DatabaseMessageExtraImageFile =>
- extra.type === AttachmentType.IMAGE
- );
+ // Only include images for user messages (assistant images are for display only)
+ const imageFiles =
+ message.role === 'user'
+ ? message.extra.filter(
+ (extra: DatabaseMessageExtra): extra is DatabaseMessageExtraImageFile =>
+ extra.type === AttachmentType.IMAGE
+ )
+ : [];
for (const image of imageFiles) {
contentParts.push({