Skip to content

Commit 8c0b5bb

Browse files
committed
fix(file-viewer): resolve in-app workspace image URLs in the rich editor
The removed MarkdownPreview rewrote /workspace/{id}/files/{fileId} image src to the serving endpoint /api/files/view/{fileId}; without it, in-app image URLs 404 in the rich editor (Cursor finding). Re-add the rewrite as a display-only transform on the rendered <img src> — the node's stored src attribute keeps the original path so markdown round-trips unchanged. Absolute/non-workspace URLs pass through. Unit tested.
1 parent ebf4986 commit 8c0b5bb

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { resolveDisplaySrc } from './image'
6+
7+
describe('resolveDisplaySrc', () => {
8+
it('rewrites an in-app workspace file path to its serving endpoint (display only)', () => {
9+
expect(resolveDisplaySrc('/workspace/W1/files/F123')).toBe('/api/files/view/F123')
10+
expect(resolveDisplaySrc('/workspace/any-ws-id/files/abc-def')).toBe('/api/files/view/abc-def')
11+
})
12+
13+
it('leaves absolute and non-workspace URLs untouched', () => {
14+
expect(resolveDisplaySrc('https://cdn.example.com/a.png')).toBe('https://cdn.example.com/a.png')
15+
expect(resolveDisplaySrc('http://localhost/workspace/W1/files/F1')).toBe(
16+
'http://localhost/workspace/W1/files/F1'
17+
)
18+
expect(resolveDisplaySrc('/other/path/files/x')).toBe('/other/path/files/x')
19+
expect(resolveDisplaySrc('relative/image.png')).toBe('relative/image.png')
20+
})
21+
22+
it('passes through empty/undefined and unparseable values', () => {
23+
expect(resolveDisplaySrc(undefined)).toBeUndefined()
24+
expect(resolveDisplaySrc('')).toBe('')
25+
expect(resolveDisplaySrc('/workspace/W1/files/')).toBe('/workspace/W1/files/')
26+
})
27+
})

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ function escapeAttr(value: string): string {
2626
.replace(/>/g, '&gt;')
2727
}
2828

29+
/**
30+
* Rewrite an in-app workspace file path (`/workspace/{id}/files/{fileId}`) to its serving endpoint
31+
* (`/api/files/view/{fileId}`) for display only — the stored `src` attribute keeps the original path
32+
* so markdown round-trips unchanged. Absolute and non-workspace URLs pass through untouched.
33+
*/
34+
export function resolveDisplaySrc(src: string | undefined): string | undefined {
35+
if (!src) return src
36+
try {
37+
const parsed = new URL(src, 'http://placeholder')
38+
if (parsed.origin !== 'http://placeholder') return src
39+
const [, seg1, , seg3, fileId] = parsed.pathname.split('/')
40+
if (seg1 === 'workspace' && seg3 === 'files' && fileId) return `/api/files/view/${fileId}`
41+
} catch {
42+
// not a parseable URL — render as-is
43+
}
44+
return src
45+
}
46+
2947
/**
3048
* Serialize an image to markdown when it has no explicit size, and to an HTML `<img>` tag when
3149
* it does — standard markdown has no width syntax, so a resized image must round-trip as HTML to
@@ -211,7 +229,7 @@ function ResizableImageView({ node, updateAttributes, selected, editor }: ReactN
211229
const image = (
212230
<img
213231
ref={imageRef}
214-
src={attrs.src}
232+
src={resolveDisplaySrc(attrs.src)}
215233
alt={attrs.alt ?? ''}
216234
title={attrs.title ?? undefined}
217235
// When editable, the image itself is the drag handle — grab anywhere on it to reorder. (The node

0 commit comments

Comments
 (0)