fix(web): render markdown images from workspace files - #4631

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews
Closed

fix(web): render markdown images from workspace files#4631
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What Changed

ChatMarkdown had no img renderer, so a markdown image whose source is a local path fell through to a bare <img src="docs/shots/portrait.png">. Local sources now resolve through the same signed workspace-asset URL that the file preview panel already uses.

  • apps/web/src/components/ChatMarkdown.tsx — new img component. A source that resolves to a workspace file (relative to the thread cwd, absolute, or file://) is rendered from assets.createUrl({_tag: "workspace-file", …}). Remote http(s) sources pass through untouched.
  • Sources the environment will not sign — outside the workspace root, deleted, not a previewable image type — degrade to the existing file chip rather than a broken image.
  • Loaded images are click-to-open in the file preview, wrapped in a <button aria-label="Preview …"> following the pattern already used for annotation crops in MessagesTimeline.
  • apps/web/src/index.css.chat-markdown-image sizing (max 100% wide, 32rem tall, contained, bordered) and a loading placeholder. There was no image styling at all before.

Also lifted the duplicated file-chip label construction out of the a renderer into markdownFileLinkLabel so the image fallback builds the same label.

Why

The web origin only serves the client bundle. An unknown path falls through to the SPA handler in apps/server/src/http.ts, which answers with index.html — so every local-path image decoded as HTML and rendered as the broken-image icon. Only http(s) sources could ever have worked.

This matters because agents write these. Ask one to summarize UI work and it produces ![portrait pip](docs/shots/portrait.png) pointing at screenshots it just captured in the workspace; every one of them rendered broken.

The asset endpoint is the right lever rather than a new file-serving route: it already signs short-lived, claim-scoped URLs, already restricts to files inside the thread's workspace root, and already gates on previewable image types. The fix reuses that boundary instead of widening it, which is why an out-of-workspace path degrades to a chip instead of loading.

UI Changes

Before — a message with four workspace screenshots, all broken:

markdown images before

After — same message, same files:

markdown images after

Verification

New apps/web/src/components/ChatMarkdown.test.tsx covers the four cases: relative path, absolute path, refused asset (falls back to the chip), and remote URL (untouched). Confirmed the tests fail against the unmodified component — the baseline output is literally <img src="docs/shots/docked.png">.

cd apps/web && tsgo --noEmit
vp test run --project unit src/components/ChatMarkdown.test.tsx \
src/components/chat/MessagesTimeline.test.tsx src/markdown-list-indentation.test.tsx
vp lint apps/web/src/components/ChatMarkdown.tsx apps/web/src/components/ChatMarkdown.test.tsx
vp fmt --check apps/web/src/components/ChatMarkdown.tsx apps/web/src/index.css

Integrated web pass through test-t3-app: isolated dev environment, real project, the message above sent and rendered in the controlled browser. All four images load from /api/assets/…; a missing file renders the chip; a remote URL is passed through unchanged.

Not addressed here, to keep this focused: mobile's NativeMarkdownImage passes the raw href to <Image source={{uri}}> and has the same defect, but it needs async resolution on a different render path.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Note

Low Risk
Scoped to chat markdown rendering and reuses existing signed-asset boundaries; adds tests and a small link-parser guard.

Overview
Chat markdown workspace image paths no longer render as broken <img> tags pointing at filesystem paths. A new img renderer resolves relative, absolute, and file:// sources through useAssetUrlState (signed workspace-file URLs), leaves http(s) URLs unchanged, and shows a file chip when signing fails or the path is not a previewable image type.

MarkdownWorkspaceImage handles loading placeholders, onError fallback, and optional click-to-open in the file preview panel. File-chip labels are shared via markdownFileLinkLabel. resolveMarkdownFileLinkMeta now ignores protocol-relative (//…) URLs so they are not treated as workspace files. CSS adds .chat-markdown-image and a loading placeholder. ChatMarkdown.test.tsx covers the four main cases.

Reviewed by Cursor Bugbot for commit 9296530. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Render workspace image files inline in chat markdown

  • Adds a MarkdownWorkspaceImage component in ChatMarkdown.tsx that resolves local workspace image paths to signed asset URLs and renders them as <img> elements with lazy loading.
  • Non-previewable local image paths fall back to a file chip (MarkdownFileLink); remote http(s) and protocol-relative URLs are left unchanged.
  • Adds a guard in markdown-links.ts to prevent protocol-relative URLs (//...) from being treated as workspace file paths.
  • Adds CSS in index.css for .chat-markdown-image and .chat-markdown-image-placeholder to constrain size and apply consistent visual styling.
  • Adds cwd to the markdownComponentsuseMemo dependency array so image/link resolution updates when the working directory changes.

Macroscope summarized 9296530.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 27, 2026
Comment threadapps/web/src/components/ChatMarkdown.tsx
Comment threadapps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeappBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new user-facing functionality (rendering workspace images in markdown via signed asset URLs) rather than fixing existing behavior. The author is a first-time contributor to this repository. New features warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aae71ecb-40d5-4931-a584-788e65e6b786

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from f455c9c to dcc1ab0CompareJuly 28, 2026 02:08
colonelpanic8and others added 2 commits July 27, 2026 19:47
Markdown images whose source is a local path rendered as a broken image.
The web origin only serves the client bundle, so `<img src="docs/shot.png">`
hit the SPA fallback and decoded as HTML.
Resolve local sources — relative to the thread cwd, absolute, or file:// —
through the same signed workspace-asset URL the file preview panel already
uses. Remote sources still pass through untouched, and a source the
environment will not sign degrades to the existing file chip instead of a
broken image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from dcc1ab0 to 5f85c61CompareJuly 28, 2026 02:51

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f85c61. Configure here.

Comment threadapps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 10, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 11, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 18, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Workspace image rendering already shipped in #6433.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(web): render markdown images from workspace files - #4631

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews
Closed

fix(web): render markdown images from workspace files#4631
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What Changed

ChatMarkdown had no img renderer, so a markdown image whose source is a local path fell through to a bare <img src="docs/shots/portrait.png">. Local sources now resolve through the same signed workspace-asset URL that the file preview panel already uses.

  • apps/web/src/components/ChatMarkdown.tsx — new img component. A source that resolves to a workspace file (relative to the thread cwd, absolute, or file://) is rendered from assets.createUrl({_tag: "workspace-file", …}). Remote http(s) sources pass through untouched.
  • Sources the environment will not sign — outside the workspace root, deleted, not a previewable image type — degrade to the existing file chip rather than a broken image.
  • Loaded images are click-to-open in the file preview, wrapped in a <button aria-label="Preview …"> following the pattern already used for annotation crops in MessagesTimeline.
  • apps/web/src/index.css.chat-markdown-image sizing (max 100% wide, 32rem tall, contained, bordered) and a loading placeholder. There was no image styling at all before.

Also lifted the duplicated file-chip label construction out of the a renderer into markdownFileLinkLabel so the image fallback builds the same label.

Why

The web origin only serves the client bundle. An unknown path falls through to the SPA handler in apps/server/src/http.ts, which answers with index.html — so every local-path image decoded as HTML and rendered as the broken-image icon. Only http(s) sources could ever have worked.

This matters because agents write these. Ask one to summarize UI work and it produces ![portrait pip](docs/shots/portrait.png) pointing at screenshots it just captured in the workspace; every one of them rendered broken.

The asset endpoint is the right lever rather than a new file-serving route: it already signs short-lived, claim-scoped URLs, already restricts to files inside the thread's workspace root, and already gates on previewable image types. The fix reuses that boundary instead of widening it, which is why an out-of-workspace path degrades to a chip instead of loading.

UI Changes

Before — a message with four workspace screenshots, all broken:

markdown images before

After — same message, same files:

markdown images after

Verification

New apps/web/src/components/ChatMarkdown.test.tsx covers the four cases: relative path, absolute path, refused asset (falls back to the chip), and remote URL (untouched). Confirmed the tests fail against the unmodified component — the baseline output is literally <img src="docs/shots/docked.png">.

cd apps/web && tsgo --noEmit
vp test run --project unit src/components/ChatMarkdown.test.tsx \
src/components/chat/MessagesTimeline.test.tsx src/markdown-list-indentation.test.tsx
vp lint apps/web/src/components/ChatMarkdown.tsx apps/web/src/components/ChatMarkdown.test.tsx
vp fmt --check apps/web/src/components/ChatMarkdown.tsx apps/web/src/index.css

Integrated web pass through test-t3-app: isolated dev environment, real project, the message above sent and rendered in the controlled browser. All four images load from /api/assets/…; a missing file renders the chip; a remote URL is passed through unchanged.

Not addressed here, to keep this focused: mobile's NativeMarkdownImage passes the raw href to <Image source={{uri}}> and has the same defect, but it needs async resolution on a different render path.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Note

Low Risk
Scoped to chat markdown rendering and reuses existing signed-asset boundaries; adds tests and a small link-parser guard.

Overview
Chat markdown workspace image paths no longer render as broken <img> tags pointing at filesystem paths. A new img renderer resolves relative, absolute, and file:// sources through useAssetUrlState (signed workspace-file URLs), leaves http(s) URLs unchanged, and shows a file chip when signing fails or the path is not a previewable image type.

MarkdownWorkspaceImage handles loading placeholders, onError fallback, and optional click-to-open in the file preview panel. File-chip labels are shared via markdownFileLinkLabel. resolveMarkdownFileLinkMeta now ignores protocol-relative (//…) URLs so they are not treated as workspace files. CSS adds .chat-markdown-image and a loading placeholder. ChatMarkdown.test.tsx covers the four main cases.

Reviewed by Cursor Bugbot for commit 9296530. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Render workspace image files inline in chat markdown

  • Adds a MarkdownWorkspaceImage component in ChatMarkdown.tsx that resolves local workspace image paths to signed asset URLs and renders them as <img> elements with lazy loading.
  • Non-previewable local image paths fall back to a file chip (MarkdownFileLink); remote http(s) and protocol-relative URLs are left unchanged.
  • Adds a guard in markdown-links.ts to prevent protocol-relative URLs (//...) from being treated as workspace file paths.
  • Adds CSS in index.css for .chat-markdown-image and .chat-markdown-image-placeholder to constrain size and apply consistent visual styling.
  • Adds cwd to the markdownComponentsuseMemo dependency array so image/link resolution updates when the working directory changes.

Macroscope summarized 9296530.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 27, 2026
Comment threadapps/web/src/components/ChatMarkdown.tsx
Comment threadapps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeappBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new user-facing functionality (rendering workspace images in markdown via signed asset URLs) rather than fixing existing behavior. The author is a first-time contributor to this repository. New features warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aae71ecb-40d5-4931-a584-788e65e6b786

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from f455c9c to dcc1ab0CompareJuly 28, 2026 02:08
colonelpanic8and others added 2 commits July 27, 2026 19:47
Markdown images whose source is a local path rendered as a broken image.
The web origin only serves the client bundle, so `<img src="docs/shot.png">`
hit the SPA fallback and decoded as HTML.
Resolve local sources — relative to the thread cwd, absolute, or file:// —
through the same signed workspace-asset URL the file preview panel already
uses. Remote sources still pass through untouched, and a source the
environment will not sign degrades to the existing file chip instead of a
broken image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from dcc1ab0 to 5f85c61CompareJuly 28, 2026 02:51

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f85c61. Configure here.

Comment threadapps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 10, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 11, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 18, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Workspace image rendering already shipped in #6433.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): render markdown images from workspace files - #4631

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews
Closed

fix(web): render markdown images from workspace files#4631
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What Changed

ChatMarkdown had no img renderer, so a markdown image whose source is a local path fell through to a bare <img src="docs/shots/portrait.png">. Local sources now resolve through the same signed workspace-asset URL that the file preview panel already uses.

  • apps/web/src/components/ChatMarkdown.tsx — new img component. A source that resolves to a workspace file (relative to the thread cwd, absolute, or file://) is rendered from assets.createUrl({_tag: "workspace-file", …}). Remote http(s) sources pass through untouched.
  • Sources the environment will not sign — outside the workspace root, deleted, not a previewable image type — degrade to the existing file chip rather than a broken image.
  • Loaded images are click-to-open in the file preview, wrapped in a <button aria-label="Preview …"> following the pattern already used for annotation crops in MessagesTimeline.
  • apps/web/src/index.css.chat-markdown-image sizing (max 100% wide, 32rem tall, contained, bordered) and a loading placeholder. There was no image styling at all before.

Also lifted the duplicated file-chip label construction out of the a renderer into markdownFileLinkLabel so the image fallback builds the same label.

Why

The web origin only serves the client bundle. An unknown path falls through to the SPA handler in apps/server/src/http.ts, which answers with index.html — so every local-path image decoded as HTML and rendered as the broken-image icon. Only http(s) sources could ever have worked.

This matters because agents write these. Ask one to summarize UI work and it produces ![portrait pip](docs/shots/portrait.png) pointing at screenshots it just captured in the workspace; every one of them rendered broken.

The asset endpoint is the right lever rather than a new file-serving route: it already signs short-lived, claim-scoped URLs, already restricts to files inside the thread's workspace root, and already gates on previewable image types. The fix reuses that boundary instead of widening it, which is why an out-of-workspace path degrades to a chip instead of loading.

UI Changes

Before — a message with four workspace screenshots, all broken:

markdown images before

After — same message, same files:

markdown images after

Verification

New apps/web/src/components/ChatMarkdown.test.tsx covers the four cases: relative path, absolute path, refused asset (falls back to the chip), and remote URL (untouched). Confirmed the tests fail against the unmodified component — the baseline output is literally <img src="docs/shots/docked.png">.

cd apps/web && tsgo --noEmit
vp test run --project unit src/components/ChatMarkdown.test.tsx \
src/components/chat/MessagesTimeline.test.tsx src/markdown-list-indentation.test.tsx
vp lint apps/web/src/components/ChatMarkdown.tsx apps/web/src/components/ChatMarkdown.test.tsx
vp fmt --check apps/web/src/components/ChatMarkdown.tsx apps/web/src/index.css

Integrated web pass through test-t3-app: isolated dev environment, real project, the message above sent and rendered in the controlled browser. All four images load from /api/assets/…; a missing file renders the chip; a remote URL is passed through unchanged.

Not addressed here, to keep this focused: mobile's NativeMarkdownImage passes the raw href to <Image source={{uri}}> and has the same defect, but it needs async resolution on a different render path.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Note

Low Risk
Scoped to chat markdown rendering and reuses existing signed-asset boundaries; adds tests and a small link-parser guard.

Overview
Chat markdown workspace image paths no longer render as broken <img> tags pointing at filesystem paths. A new img renderer resolves relative, absolute, and file:// sources through useAssetUrlState (signed workspace-file URLs), leaves http(s) URLs unchanged, and shows a file chip when signing fails or the path is not a previewable image type.

MarkdownWorkspaceImage handles loading placeholders, onError fallback, and optional click-to-open in the file preview panel. File-chip labels are shared via markdownFileLinkLabel. resolveMarkdownFileLinkMeta now ignores protocol-relative (//…) URLs so they are not treated as workspace files. CSS adds .chat-markdown-image and a loading placeholder. ChatMarkdown.test.tsx covers the four main cases.

Reviewed by Cursor Bugbot for commit 9296530. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Render workspace image files inline in chat markdown

  • Adds a MarkdownWorkspaceImage component in ChatMarkdown.tsx that resolves local workspace image paths to signed asset URLs and renders them as <img> elements with lazy loading.
  • Non-previewable local image paths fall back to a file chip (MarkdownFileLink); remote http(s) and protocol-relative URLs are left unchanged.
  • Adds a guard in markdown-links.ts to prevent protocol-relative URLs (//...) from being treated as workspace file paths.
  • Adds CSS in index.css for .chat-markdown-image and .chat-markdown-image-placeholder to constrain size and apply consistent visual styling.
  • Adds cwd to the markdownComponentsuseMemo dependency array so image/link resolution updates when the working directory changes.

Macroscope summarized 9296530.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 27, 2026
Comment threadapps/web/src/components/ChatMarkdown.tsx
Comment threadapps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeappBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new user-facing functionality (rendering workspace images in markdown via signed asset URLs) rather than fixing existing behavior. The author is a first-time contributor to this repository. New features warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aae71ecb-40d5-4931-a584-788e65e6b786

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from f455c9c to dcc1ab0CompareJuly 28, 2026 02:08
colonelpanic8and others added 2 commits July 27, 2026 19:47
Markdown images whose source is a local path rendered as a broken image.
The web origin only serves the client bundle, so `<img src="docs/shot.png">`
hit the SPA fallback and decoded as HTML.
Resolve local sources — relative to the thread cwd, absolute, or file:// —
through the same signed workspace-asset URL the file preview panel already
uses. Remote sources still pass through untouched, and a source the
environment will not sign degrades to the existing file chip instead of a
broken image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from dcc1ab0 to 5f85c61CompareJuly 28, 2026 02:51

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f85c61. Configure here.

Comment threadapps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 10, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 11, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 18, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Workspace image rendering already shipped in #6433.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): render markdown images from workspace files - #4631

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews
Closed

fix(web): render markdown images from workspace files#4631
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What Changed

ChatMarkdown had no img renderer, so a markdown image whose source is a local path fell through to a bare <img src="docs/shots/portrait.png">. Local sources now resolve through the same signed workspace-asset URL that the file preview panel already uses.

  • apps/web/src/components/ChatMarkdown.tsx — new img component. A source that resolves to a workspace file (relative to the thread cwd, absolute, or file://) is rendered from assets.createUrl({_tag: "workspace-file", …}). Remote http(s) sources pass through untouched.
  • Sources the environment will not sign — outside the workspace root, deleted, not a previewable image type — degrade to the existing file chip rather than a broken image.
  • Loaded images are click-to-open in the file preview, wrapped in a <button aria-label="Preview …"> following the pattern already used for annotation crops in MessagesTimeline.
  • apps/web/src/index.css.chat-markdown-image sizing (max 100% wide, 32rem tall, contained, bordered) and a loading placeholder. There was no image styling at all before.

Also lifted the duplicated file-chip label construction out of the a renderer into markdownFileLinkLabel so the image fallback builds the same label.

Why

The web origin only serves the client bundle. An unknown path falls through to the SPA handler in apps/server/src/http.ts, which answers with index.html — so every local-path image decoded as HTML and rendered as the broken-image icon. Only http(s) sources could ever have worked.

This matters because agents write these. Ask one to summarize UI work and it produces ![portrait pip](docs/shots/portrait.png) pointing at screenshots it just captured in the workspace; every one of them rendered broken.

The asset endpoint is the right lever rather than a new file-serving route: it already signs short-lived, claim-scoped URLs, already restricts to files inside the thread's workspace root, and already gates on previewable image types. The fix reuses that boundary instead of widening it, which is why an out-of-workspace path degrades to a chip instead of loading.

UI Changes

Before — a message with four workspace screenshots, all broken:

markdown images before

After — same message, same files:

markdown images after

Verification

New apps/web/src/components/ChatMarkdown.test.tsx covers the four cases: relative path, absolute path, refused asset (falls back to the chip), and remote URL (untouched). Confirmed the tests fail against the unmodified component — the baseline output is literally <img src="docs/shots/docked.png">.

cd apps/web && tsgo --noEmit
vp test run --project unit src/components/ChatMarkdown.test.tsx \
src/components/chat/MessagesTimeline.test.tsx src/markdown-list-indentation.test.tsx
vp lint apps/web/src/components/ChatMarkdown.tsx apps/web/src/components/ChatMarkdown.test.tsx
vp fmt --check apps/web/src/components/ChatMarkdown.tsx apps/web/src/index.css

Integrated web pass through test-t3-app: isolated dev environment, real project, the message above sent and rendered in the controlled browser. All four images load from /api/assets/…; a missing file renders the chip; a remote URL is passed through unchanged.

Not addressed here, to keep this focused: mobile's NativeMarkdownImage passes the raw href to <Image source={{uri}}> and has the same defect, but it needs async resolution on a different render path.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Note

Low Risk
Scoped to chat markdown rendering and reuses existing signed-asset boundaries; adds tests and a small link-parser guard.

Overview
Chat markdown workspace image paths no longer render as broken <img> tags pointing at filesystem paths. A new img renderer resolves relative, absolute, and file:// sources through useAssetUrlState (signed workspace-file URLs), leaves http(s) URLs unchanged, and shows a file chip when signing fails or the path is not a previewable image type.

MarkdownWorkspaceImage handles loading placeholders, onError fallback, and optional click-to-open in the file preview panel. File-chip labels are shared via markdownFileLinkLabel. resolveMarkdownFileLinkMeta now ignores protocol-relative (//…) URLs so they are not treated as workspace files. CSS adds .chat-markdown-image and a loading placeholder. ChatMarkdown.test.tsx covers the four main cases.

Reviewed by Cursor Bugbot for commit 9296530. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Render workspace image files inline in chat markdown

  • Adds a MarkdownWorkspaceImage component in ChatMarkdown.tsx that resolves local workspace image paths to signed asset URLs and renders them as <img> elements with lazy loading.
  • Non-previewable local image paths fall back to a file chip (MarkdownFileLink); remote http(s) and protocol-relative URLs are left unchanged.
  • Adds a guard in markdown-links.ts to prevent protocol-relative URLs (//...) from being treated as workspace file paths.
  • Adds CSS in index.css for .chat-markdown-image and .chat-markdown-image-placeholder to constrain size and apply consistent visual styling.
  • Adds cwd to the markdownComponentsuseMemo dependency array so image/link resolution updates when the working directory changes.

Macroscope summarized 9296530.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 27, 2026
Comment threadapps/web/src/components/ChatMarkdown.tsx
Comment threadapps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeappBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new user-facing functionality (rendering workspace images in markdown via signed asset URLs) rather than fixing existing behavior. The author is a first-time contributor to this repository. New features warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aae71ecb-40d5-4931-a584-788e65e6b786

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from f455c9c to dcc1ab0CompareJuly 28, 2026 02:08
colonelpanic8and others added 2 commits July 27, 2026 19:47
Markdown images whose source is a local path rendered as a broken image.
The web origin only serves the client bundle, so `<img src="docs/shot.png">`
hit the SPA fallback and decoded as HTML.
Resolve local sources — relative to the thread cwd, absolute, or file:// —
through the same signed workspace-asset URL the file preview panel already
uses. Remote sources still pass through untouched, and a source the
environment will not sign degrades to the existing file chip instead of a
broken image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from dcc1ab0 to 5f85c61CompareJuly 28, 2026 02:51

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f85c61. Configure here.

Comment threadapps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 10, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 11, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 18, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Workspace image rendering already shipped in #6433.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(web): render markdown images from workspace files - #4631

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews
Closed

fix(web): render markdown images from workspace files#4631
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What Changed

ChatMarkdown had no img renderer, so a markdown image whose source is a local path fell through to a bare <img src="docs/shots/portrait.png">. Local sources now resolve through the same signed workspace-asset URL that the file preview panel already uses.

  • apps/web/src/components/ChatMarkdown.tsx — new img component. A source that resolves to a workspace file (relative to the thread cwd, absolute, or file://) is rendered from assets.createUrl({_tag: "workspace-file", …}). Remote http(s) sources pass through untouched.
  • Sources the environment will not sign — outside the workspace root, deleted, not a previewable image type — degrade to the existing file chip rather than a broken image.
  • Loaded images are click-to-open in the file preview, wrapped in a <button aria-label="Preview …"> following the pattern already used for annotation crops in MessagesTimeline.
  • apps/web/src/index.css.chat-markdown-image sizing (max 100% wide, 32rem tall, contained, bordered) and a loading placeholder. There was no image styling at all before.

Also lifted the duplicated file-chip label construction out of the a renderer into markdownFileLinkLabel so the image fallback builds the same label.

Why

The web origin only serves the client bundle. An unknown path falls through to the SPA handler in apps/server/src/http.ts, which answers with index.html — so every local-path image decoded as HTML and rendered as the broken-image icon. Only http(s) sources could ever have worked.

This matters because agents write these. Ask one to summarize UI work and it produces ![portrait pip](docs/shots/portrait.png) pointing at screenshots it just captured in the workspace; every one of them rendered broken.

The asset endpoint is the right lever rather than a new file-serving route: it already signs short-lived, claim-scoped URLs, already restricts to files inside the thread's workspace root, and already gates on previewable image types. The fix reuses that boundary instead of widening it, which is why an out-of-workspace path degrades to a chip instead of loading.

UI Changes

Before — a message with four workspace screenshots, all broken:

markdown images before

After — same message, same files:

markdown images after

Verification

New apps/web/src/components/ChatMarkdown.test.tsx covers the four cases: relative path, absolute path, refused asset (falls back to the chip), and remote URL (untouched). Confirmed the tests fail against the unmodified component — the baseline output is literally <img src="docs/shots/docked.png">.

cd apps/web && tsgo --noEmit
vp test run --project unit src/components/ChatMarkdown.test.tsx \
src/components/chat/MessagesTimeline.test.tsx src/markdown-list-indentation.test.tsx
vp lint apps/web/src/components/ChatMarkdown.tsx apps/web/src/components/ChatMarkdown.test.tsx
vp fmt --check apps/web/src/components/ChatMarkdown.tsx apps/web/src/index.css

Integrated web pass through test-t3-app: isolated dev environment, real project, the message above sent and rendered in the controlled browser. All four images load from /api/assets/…; a missing file renders the chip; a remote URL is passed through unchanged.

Not addressed here, to keep this focused: mobile's NativeMarkdownImage passes the raw href to <Image source={{uri}}> and has the same defect, but it needs async resolution on a different render path.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Note

Low Risk
Scoped to chat markdown rendering and reuses existing signed-asset boundaries; adds tests and a small link-parser guard.

Overview
Chat markdown workspace image paths no longer render as broken <img> tags pointing at filesystem paths. A new img renderer resolves relative, absolute, and file:// sources through useAssetUrlState (signed workspace-file URLs), leaves http(s) URLs unchanged, and shows a file chip when signing fails or the path is not a previewable image type.

MarkdownWorkspaceImage handles loading placeholders, onError fallback, and optional click-to-open in the file preview panel. File-chip labels are shared via markdownFileLinkLabel. resolveMarkdownFileLinkMeta now ignores protocol-relative (//…) URLs so they are not treated as workspace files. CSS adds .chat-markdown-image and a loading placeholder. ChatMarkdown.test.tsx covers the four main cases.

Reviewed by Cursor Bugbot for commit 9296530. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Render workspace image files inline in chat markdown

  • Adds a MarkdownWorkspaceImage component in ChatMarkdown.tsx that resolves local workspace image paths to signed asset URLs and renders them as <img> elements with lazy loading.
  • Non-previewable local image paths fall back to a file chip (MarkdownFileLink); remote http(s) and protocol-relative URLs are left unchanged.
  • Adds a guard in markdown-links.ts to prevent protocol-relative URLs (//...) from being treated as workspace file paths.
  • Adds CSS in index.css for .chat-markdown-image and .chat-markdown-image-placeholder to constrain size and apply consistent visual styling.
  • Adds cwd to the markdownComponentsuseMemo dependency array so image/link resolution updates when the working directory changes.

Macroscope summarized 9296530.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 27, 2026
Comment threadapps/web/src/components/ChatMarkdown.tsx
Comment threadapps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeappBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new user-facing functionality (rendering workspace images in markdown via signed asset URLs) rather than fixing existing behavior. The author is a first-time contributor to this repository. New features warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aae71ecb-40d5-4931-a584-788e65e6b786

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from f455c9c to dcc1ab0CompareJuly 28, 2026 02:08
colonelpanic8and others added 2 commits July 27, 2026 19:47
Markdown images whose source is a local path rendered as a broken image.
The web origin only serves the client bundle, so `<img src="docs/shot.png">`
hit the SPA fallback and decoded as HTML.
Resolve local sources — relative to the thread cwd, absolute, or file:// —
through the same signed workspace-asset URL the file preview panel already
uses. Remote sources still pass through untouched, and a source the
environment will not sign degrades to the existing file chip instead of a
broken image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from dcc1ab0 to 5f85c61CompareJuly 28, 2026 02:51

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f85c61. Configure here.

Comment threadapps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 10, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 11, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 18, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Workspace image rendering already shipped in #6433.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): render markdown images from workspace files - #4631

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews
Closed

fix(web): render markdown images from workspace files#4631
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What Changed

ChatMarkdown had no img renderer, so a markdown image whose source is a local path fell through to a bare <img src="docs/shots/portrait.png">. Local sources now resolve through the same signed workspace-asset URL that the file preview panel already uses.

  • apps/web/src/components/ChatMarkdown.tsx — new img component. A source that resolves to a workspace file (relative to the thread cwd, absolute, or file://) is rendered from assets.createUrl({_tag: "workspace-file", …}). Remote http(s) sources pass through untouched.
  • Sources the environment will not sign — outside the workspace root, deleted, not a previewable image type — degrade to the existing file chip rather than a broken image.
  • Loaded images are click-to-open in the file preview, wrapped in a <button aria-label="Preview …"> following the pattern already used for annotation crops in MessagesTimeline.
  • apps/web/src/index.css.chat-markdown-image sizing (max 100% wide, 32rem tall, contained, bordered) and a loading placeholder. There was no image styling at all before.

Also lifted the duplicated file-chip label construction out of the a renderer into markdownFileLinkLabel so the image fallback builds the same label.

Why

The web origin only serves the client bundle. An unknown path falls through to the SPA handler in apps/server/src/http.ts, which answers with index.html — so every local-path image decoded as HTML and rendered as the broken-image icon. Only http(s) sources could ever have worked.

This matters because agents write these. Ask one to summarize UI work and it produces ![portrait pip](docs/shots/portrait.png) pointing at screenshots it just captured in the workspace; every one of them rendered broken.

The asset endpoint is the right lever rather than a new file-serving route: it already signs short-lived, claim-scoped URLs, already restricts to files inside the thread's workspace root, and already gates on previewable image types. The fix reuses that boundary instead of widening it, which is why an out-of-workspace path degrades to a chip instead of loading.

UI Changes

Before — a message with four workspace screenshots, all broken:

markdown images before

After — same message, same files:

markdown images after

Verification

New apps/web/src/components/ChatMarkdown.test.tsx covers the four cases: relative path, absolute path, refused asset (falls back to the chip), and remote URL (untouched). Confirmed the tests fail against the unmodified component — the baseline output is literally <img src="docs/shots/docked.png">.

cd apps/web && tsgo --noEmit
vp test run --project unit src/components/ChatMarkdown.test.tsx \
src/components/chat/MessagesTimeline.test.tsx src/markdown-list-indentation.test.tsx
vp lint apps/web/src/components/ChatMarkdown.tsx apps/web/src/components/ChatMarkdown.test.tsx
vp fmt --check apps/web/src/components/ChatMarkdown.tsx apps/web/src/index.css

Integrated web pass through test-t3-app: isolated dev environment, real project, the message above sent and rendered in the controlled browser. All four images load from /api/assets/…; a missing file renders the chip; a remote URL is passed through unchanged.

Not addressed here, to keep this focused: mobile's NativeMarkdownImage passes the raw href to <Image source={{uri}}> and has the same defect, but it needs async resolution on a different render path.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Note

Low Risk
Scoped to chat markdown rendering and reuses existing signed-asset boundaries; adds tests and a small link-parser guard.

Overview
Chat markdown workspace image paths no longer render as broken <img> tags pointing at filesystem paths. A new img renderer resolves relative, absolute, and file:// sources through useAssetUrlState (signed workspace-file URLs), leaves http(s) URLs unchanged, and shows a file chip when signing fails or the path is not a previewable image type.

MarkdownWorkspaceImage handles loading placeholders, onError fallback, and optional click-to-open in the file preview panel. File-chip labels are shared via markdownFileLinkLabel. resolveMarkdownFileLinkMeta now ignores protocol-relative (//…) URLs so they are not treated as workspace files. CSS adds .chat-markdown-image and a loading placeholder. ChatMarkdown.test.tsx covers the four main cases.

Reviewed by Cursor Bugbot for commit 9296530. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Render workspace image files inline in chat markdown

  • Adds a MarkdownWorkspaceImage component in ChatMarkdown.tsx that resolves local workspace image paths to signed asset URLs and renders them as <img> elements with lazy loading.
  • Non-previewable local image paths fall back to a file chip (MarkdownFileLink); remote http(s) and protocol-relative URLs are left unchanged.
  • Adds a guard in markdown-links.ts to prevent protocol-relative URLs (//...) from being treated as workspace file paths.
  • Adds CSS in index.css for .chat-markdown-image and .chat-markdown-image-placeholder to constrain size and apply consistent visual styling.
  • Adds cwd to the markdownComponentsuseMemo dependency array so image/link resolution updates when the working directory changes.

Macroscope summarized 9296530.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 27, 2026
Comment threadapps/web/src/components/ChatMarkdown.tsx
Comment threadapps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeappBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new user-facing functionality (rendering workspace images in markdown via signed asset URLs) rather than fixing existing behavior. The author is a first-time contributor to this repository. New features warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aae71ecb-40d5-4931-a584-788e65e6b786

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from f455c9c to dcc1ab0CompareJuly 28, 2026 02:08
colonelpanic8and others added 2 commits July 27, 2026 19:47
Markdown images whose source is a local path rendered as a broken image.
The web origin only serves the client bundle, so `<img src="docs/shot.png">`
hit the SPA fallback and decoded as HTML.
Resolve local sources — relative to the thread cwd, absolute, or file:// —
through the same signed workspace-asset URL the file preview panel already
uses. Remote sources still pass through untouched, and a source the
environment will not sign degrades to the existing file chip instead of a
broken image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from dcc1ab0 to 5f85c61CompareJuly 28, 2026 02:51

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f85c61. Configure here.

Comment threadapps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 10, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 11, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 18, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Workspace image rendering already shipped in #6433.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(web): render markdown images from workspace files - #4631

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews
Closed

fix(web): render markdown images from workspace files#4631
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What Changed

ChatMarkdown had no img renderer, so a markdown image whose source is a local path fell through to a bare <img src="docs/shots/portrait.png">. Local sources now resolve through the same signed workspace-asset URL that the file preview panel already uses.

  • apps/web/src/components/ChatMarkdown.tsx — new img component. A source that resolves to a workspace file (relative to the thread cwd, absolute, or file://) is rendered from assets.createUrl({_tag: "workspace-file", …}). Remote http(s) sources pass through untouched.
  • Sources the environment will not sign — outside the workspace root, deleted, not a previewable image type — degrade to the existing file chip rather than a broken image.
  • Loaded images are click-to-open in the file preview, wrapped in a <button aria-label="Preview …"> following the pattern already used for annotation crops in MessagesTimeline.
  • apps/web/src/index.css.chat-markdown-image sizing (max 100% wide, 32rem tall, contained, bordered) and a loading placeholder. There was no image styling at all before.

Also lifted the duplicated file-chip label construction out of the a renderer into markdownFileLinkLabel so the image fallback builds the same label.

Why

The web origin only serves the client bundle. An unknown path falls through to the SPA handler in apps/server/src/http.ts, which answers with index.html — so every local-path image decoded as HTML and rendered as the broken-image icon. Only http(s) sources could ever have worked.

This matters because agents write these. Ask one to summarize UI work and it produces ![portrait pip](docs/shots/portrait.png) pointing at screenshots it just captured in the workspace; every one of them rendered broken.

The asset endpoint is the right lever rather than a new file-serving route: it already signs short-lived, claim-scoped URLs, already restricts to files inside the thread's workspace root, and already gates on previewable image types. The fix reuses that boundary instead of widening it, which is why an out-of-workspace path degrades to a chip instead of loading.

UI Changes

Before — a message with four workspace screenshots, all broken:

markdown images before

After — same message, same files:

markdown images after

Verification

New apps/web/src/components/ChatMarkdown.test.tsx covers the four cases: relative path, absolute path, refused asset (falls back to the chip), and remote URL (untouched). Confirmed the tests fail against the unmodified component — the baseline output is literally <img src="docs/shots/docked.png">.

cd apps/web && tsgo --noEmit
vp test run --project unit src/components/ChatMarkdown.test.tsx \
src/components/chat/MessagesTimeline.test.tsx src/markdown-list-indentation.test.tsx
vp lint apps/web/src/components/ChatMarkdown.tsx apps/web/src/components/ChatMarkdown.test.tsx
vp fmt --check apps/web/src/components/ChatMarkdown.tsx apps/web/src/index.css

Integrated web pass through test-t3-app: isolated dev environment, real project, the message above sent and rendered in the controlled browser. All four images load from /api/assets/…; a missing file renders the chip; a remote URL is passed through unchanged.

Not addressed here, to keep this focused: mobile's NativeMarkdownImage passes the raw href to <Image source={{uri}}> and has the same defect, but it needs async resolution on a different render path.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Note

Low Risk
Scoped to chat markdown rendering and reuses existing signed-asset boundaries; adds tests and a small link-parser guard.

Overview
Chat markdown workspace image paths no longer render as broken <img> tags pointing at filesystem paths. A new img renderer resolves relative, absolute, and file:// sources through useAssetUrlState (signed workspace-file URLs), leaves http(s) URLs unchanged, and shows a file chip when signing fails or the path is not a previewable image type.

MarkdownWorkspaceImage handles loading placeholders, onError fallback, and optional click-to-open in the file preview panel. File-chip labels are shared via markdownFileLinkLabel. resolveMarkdownFileLinkMeta now ignores protocol-relative (//…) URLs so they are not treated as workspace files. CSS adds .chat-markdown-image and a loading placeholder. ChatMarkdown.test.tsx covers the four main cases.

Reviewed by Cursor Bugbot for commit 9296530. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Render workspace image files inline in chat markdown

  • Adds a MarkdownWorkspaceImage component in ChatMarkdown.tsx that resolves local workspace image paths to signed asset URLs and renders them as <img> elements with lazy loading.
  • Non-previewable local image paths fall back to a file chip (MarkdownFileLink); remote http(s) and protocol-relative URLs are left unchanged.
  • Adds a guard in markdown-links.ts to prevent protocol-relative URLs (//...) from being treated as workspace file paths.
  • Adds CSS in index.css for .chat-markdown-image and .chat-markdown-image-placeholder to constrain size and apply consistent visual styling.
  • Adds cwd to the markdownComponentsuseMemo dependency array so image/link resolution updates when the working directory changes.

Macroscope summarized 9296530.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 27, 2026
Comment threadapps/web/src/components/ChatMarkdown.tsx
Comment threadapps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeappBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new user-facing functionality (rendering workspace images in markdown via signed asset URLs) rather than fixing existing behavior. The author is a first-time contributor to this repository. New features warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aae71ecb-40d5-4931-a584-788e65e6b786

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from f455c9c to dcc1ab0CompareJuly 28, 2026 02:08
colonelpanic8and others added 2 commits July 27, 2026 19:47
Markdown images whose source is a local path rendered as a broken image.
The web origin only serves the client bundle, so `<img src="docs/shot.png">`
hit the SPA fallback and decoded as HTML.
Resolve local sources — relative to the thread cwd, absolute, or file:// —
through the same signed workspace-asset URL the file preview panel already
uses. Remote sources still pass through untouched, and a source the
environment will not sign degrades to the existing file chip instead of a
broken image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from dcc1ab0 to 5f85c61CompareJuly 28, 2026 02:51

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f85c61. Configure here.

Comment threadapps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 10, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 11, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 18, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Workspace image rendering already shipped in #6433.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(web): render markdown images from workspace files - #4631

Closed
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews
Closed

fix(web): render markdown images from workspace files#4631
colonelpanic8 wants to merge 3 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-broken-image-previews

Conversation

@colonelpanic8

@colonelpanic8colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What Changed

ChatMarkdown had no img renderer, so a markdown image whose source is a local path fell through to a bare <img src="docs/shots/portrait.png">. Local sources now resolve through the same signed workspace-asset URL that the file preview panel already uses.

  • apps/web/src/components/ChatMarkdown.tsx — new img component. A source that resolves to a workspace file (relative to the thread cwd, absolute, or file://) is rendered from assets.createUrl({_tag: "workspace-file", …}). Remote http(s) sources pass through untouched.
  • Sources the environment will not sign — outside the workspace root, deleted, not a previewable image type — degrade to the existing file chip rather than a broken image.
  • Loaded images are click-to-open in the file preview, wrapped in a <button aria-label="Preview …"> following the pattern already used for annotation crops in MessagesTimeline.
  • apps/web/src/index.css.chat-markdown-image sizing (max 100% wide, 32rem tall, contained, bordered) and a loading placeholder. There was no image styling at all before.

Also lifted the duplicated file-chip label construction out of the a renderer into markdownFileLinkLabel so the image fallback builds the same label.

Why

The web origin only serves the client bundle. An unknown path falls through to the SPA handler in apps/server/src/http.ts, which answers with index.html — so every local-path image decoded as HTML and rendered as the broken-image icon. Only http(s) sources could ever have worked.

This matters because agents write these. Ask one to summarize UI work and it produces ![portrait pip](docs/shots/portrait.png) pointing at screenshots it just captured in the workspace; every one of them rendered broken.

The asset endpoint is the right lever rather than a new file-serving route: it already signs short-lived, claim-scoped URLs, already restricts to files inside the thread's workspace root, and already gates on previewable image types. The fix reuses that boundary instead of widening it, which is why an out-of-workspace path degrades to a chip instead of loading.

UI Changes

Before — a message with four workspace screenshots, all broken:

markdown images before

After — same message, same files:

markdown images after

Verification

New apps/web/src/components/ChatMarkdown.test.tsx covers the four cases: relative path, absolute path, refused asset (falls back to the chip), and remote URL (untouched). Confirmed the tests fail against the unmodified component — the baseline output is literally <img src="docs/shots/docked.png">.

cd apps/web && tsgo --noEmit
vp test run --project unit src/components/ChatMarkdown.test.tsx \
src/components/chat/MessagesTimeline.test.tsx src/markdown-list-indentation.test.tsx
vp lint apps/web/src/components/ChatMarkdown.tsx apps/web/src/components/ChatMarkdown.test.tsx
vp fmt --check apps/web/src/components/ChatMarkdown.tsx apps/web/src/index.css

Integrated web pass through test-t3-app: isolated dev environment, real project, the message above sent and rendered in the controlled browser. All four images load from /api/assets/…; a missing file renders the chip; a remote URL is passed through unchanged.

Not addressed here, to keep this focused: mobile's NativeMarkdownImage passes the raw href to <Image source={{uri}}> and has the same defect, but it needs async resolution on a different render path.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes — n/a, no motion

Note

Low Risk
Scoped to chat markdown rendering and reuses existing signed-asset boundaries; adds tests and a small link-parser guard.

Overview
Chat markdown workspace image paths no longer render as broken <img> tags pointing at filesystem paths. A new img renderer resolves relative, absolute, and file:// sources through useAssetUrlState (signed workspace-file URLs), leaves http(s) URLs unchanged, and shows a file chip when signing fails or the path is not a previewable image type.

MarkdownWorkspaceImage handles loading placeholders, onError fallback, and optional click-to-open in the file preview panel. File-chip labels are shared via markdownFileLinkLabel. resolveMarkdownFileLinkMeta now ignores protocol-relative (//…) URLs so they are not treated as workspace files. CSS adds .chat-markdown-image and a loading placeholder. ChatMarkdown.test.tsx covers the four main cases.

Reviewed by Cursor Bugbot for commit 9296530. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Render workspace image files inline in chat markdown

  • Adds a MarkdownWorkspaceImage component in ChatMarkdown.tsx that resolves local workspace image paths to signed asset URLs and renders them as <img> elements with lazy loading.
  • Non-previewable local image paths fall back to a file chip (MarkdownFileLink); remote http(s) and protocol-relative URLs are left unchanged.
  • Adds a guard in markdown-links.ts to prevent protocol-relative URLs (//...) from being treated as workspace file paths.
  • Adds CSS in index.css for .chat-markdown-image and .chat-markdown-image-placeholder to constrain size and apply consistent visual styling.
  • Adds cwd to the markdownComponentsuseMemo dependency array so image/link resolution updates when the working directory changes.

Macroscope summarized 9296530.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 27, 2026
Comment threadapps/web/src/components/ChatMarkdown.tsx
Comment threadapps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeappBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces new user-facing functionality (rendering workspace images in markdown via signed asset URLs) rather than fixing existing behavior. The author is a first-time contributor to this repository. New features warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai

coderabbitaiBot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aae71ecb-40d5-4931-a584-788e65e6b786

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
…les)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from f455c9c to dcc1ab0CompareJuly 28, 2026 02:08
colonelpanic8and others added 2 commits July 27, 2026 19:47
Markdown images whose source is a local path rendered as a broken image.
The web origin only serves the client bundle, so `<img src="docs/shot.png">`
hit the SPA fallback and decoded as HTML.
Resolve local sources — relative to the thread cwd, absolute, or file:// —
through the same signed workspace-asset URL the file preview panel already
uses. Remote sources still pass through untouched, and a source the
environment will not sign degrades to the existing file chip instead of a
broken image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8force-pushed the t3code/fix-broken-image-previews branch from dcc1ab0 to 5f85c61CompareJuly 28, 2026 02:51

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f85c61. Configure here.

Comment threadapps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
… files)
# Conflicts:
#	apps/web/src/components/ChatMarkdown.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 10, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 11, 2026
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Aug 18, 2026
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

Closing this PR after an automated pass over open pull requests. Workspace image rendering already shipped in #6433.

@t3dotggt3dotgg closed this Aug 23, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@colonelpanic8@t3dotgg