mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(desktop): render video/audio image-markdown as media, not broken <img>
Generated media often arrives as image markdown (). The chat markdown renderer maps the img element to MarkdownImage, which renders a raw <img>; for a video/audio source the browser cannot paint it and shows a broken-image icon even though the file is valid and plays fine. Images worked only because <img src=...png> is valid markup. Route video/audio sources in MarkdownImage to the existing MediaAttachment, which already picks the correct <video>/<audio> element (streaming protocol + open-externally fallback) by media kind. The intended MEDIA:-tag -> #media: link path is unaffected; this only fixes the bare image-markdown case. Since #57944 the image path is built on hooks (async src resolution, failure and loading states), so the routing check cannot be a plain early return inside it -- it would have to sit after every hook call and would still fire an image resolve for media never rendered as an image. MarkdownImage is therefore a thin hookless router in front of MarkdownImageContent, which is the previous body unchanged. The regression tests join the existing markdown-text.media.test.tsx added by #57944 rather than replacing it. Fixes #40896 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018vU45SEvcxLkVeyyEE9gJ2
This commit is contained in:
parent
3e47efeb25
commit
181511473e
2 changed files with 54 additions and 3 deletions
|
|
@ -1,9 +1,9 @@
|
|||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { $connection } from '@/store/session'
|
||||
|
||||
import { MarkdownTextContent } from './markdown-text'
|
||||
import { MarkdownImage, MarkdownTextContent } from './markdown-text'
|
||||
|
||||
const REMOTE_IMAGE_PATH = '/home/user/project/images/remote-preview.png'
|
||||
const REMOTE_IMAGE_DATA_URL = 'data:image/png;base64,cmVtb3RlLWltYWdl'
|
||||
|
|
@ -50,3 +50,32 @@ describe('MarkdownTextContent remote images', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Regression for #40896: generated media often arrives as image markdown
|
||||
// (``). A raw <img> with a video/audio source paints a
|
||||
// broken-image icon even though the file is valid, so MarkdownImage must route
|
||||
// video/audio sources to the proper <video>/<audio> element.
|
||||
describe('MarkdownImage media routing', () => {
|
||||
afterEach(cleanup)
|
||||
|
||||
it('renders a <video> (not a broken <img>) for a video source', async () => {
|
||||
const { container } = render(<MarkdownImage alt="clip" src="file:///tmp/clip.mp4" />)
|
||||
|
||||
await waitFor(() => expect(container.querySelector('video')).not.toBeNull())
|
||||
expect(container.querySelector('img')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders an <audio> element for an audio source', async () => {
|
||||
const { container } = render(<MarkdownImage alt="note" src="file:///tmp/note.mp3" />)
|
||||
|
||||
await waitFor(() => expect(container.querySelector('audio')).not.toBeNull())
|
||||
expect(container.querySelector('img')).toBeNull()
|
||||
})
|
||||
|
||||
it('still renders an <img> for an image source', () => {
|
||||
const { container } = render(<MarkdownImage alt="pic" src="file:///tmp/pic.png" />)
|
||||
|
||||
expect(container.querySelector('video')).toBeNull()
|
||||
expect(container.querySelector('audio')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -303,7 +303,29 @@ function MarkdownLink({ children, className, href, ...props }: ComponentProps<'a
|
|||
)
|
||||
}
|
||||
|
||||
function MarkdownImage({ className, src, alt, ...props }: ComponentProps<'img'>) {
|
||||
// Generated/inline media often arrives as image markdown — ``.
|
||||
// A raw <img> with a video/audio source renders a broken-image icon (the file is
|
||||
// valid, the browser just can't paint it as an image), so route those sources to
|
||||
// MediaAttachment, which picks the right <video>/<audio> element (streaming
|
||||
// protocol + open-externally fallback) by media kind. Detection is
|
||||
// extension-based via mediaKind(); an extension-less/data/blob video URL still
|
||||
// resolves to 'file' and falls through to the image path as before.
|
||||
//
|
||||
// This is split from the image path because that path is built on hooks: a
|
||||
// conditional return inside it would have to sit after every hook call, which
|
||||
// would still fire an image resolve for media we never render as an image.
|
||||
export function MarkdownImage(props: ComponentProps<'img'>) {
|
||||
const rawSrc = typeof props.src === 'string' ? props.src : ''
|
||||
const kind = rawSrc ? mediaKind(rawSrc) : 'file'
|
||||
|
||||
if (kind === 'video' || kind === 'audio') {
|
||||
return <MediaAttachment path={rawSrc} />
|
||||
}
|
||||
|
||||
return <MarkdownImageContent {...props} />
|
||||
}
|
||||
|
||||
function MarkdownImageContent({ className, src, alt, ...props }: ComponentProps<'img'>) {
|
||||
const rawSrc = typeof src === 'string' ? src : ''
|
||||
const [resolvedSrc, setResolvedSrc] = useState(() => (rawSrc && isInlineMediaSrc(rawSrc) ? rawSrc : ''))
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue