Skip to content

Render generated images inline in chat#3984

Open
jakeleventhal wants to merge 10 commits into
pingdotgg:mainfrom
jakeleventhal:agent/render-generated-images-inline
Open

Render generated images inline in chat#3984
jakeleventhal wants to merge 10 commits into
pingdotgg:mainfrom
jakeleventhal:agent/render-generated-images-inline

Conversation

@jakeleventhal

@jakeleventhal jakeleventhal commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Screenshot 2026-07-15 at 12 12 51 AM Screenshot 2026-07-15 at 12 12 54 AM
  • render generated image references inline in assistant messages at 25% of the message width
  • correlate each image reference with the generating tool activity from the same thread turn
  • reuse the existing exact workspace-file asset URL flow and expanded-image lightbox

Why

Generated images currently appear as code-formatted paths such as images/1.jpg, so users must leave the conversation to inspect them. These files live in provider session storage rather than the project workspace or managed attachment store.

The new thread-artifact request carries the thread, turn, and relative image path needed to authorize the matching projected tool output. After that authorization, the server delegates signing and serving to the existing exact workspace-file asset implementation. Turn correlation prevents an older message from resolving a newer artifact with the same relative path.

Validation

  • pnpm exec vp check
  • pnpm exec vp run typecheck
  • pnpm exec vp test apps/server/src/assets/AssetAccess.test.ts apps/web/src/components/chat/MessagesTimeline.test.tsx (19 tests passed)
  • manually verified the historical generated-image thread: the image loaded inline at 25% width and opened in the existing lightbox

Note

Render generated images inline in chat as expandable thumbnails

  • Adds a thread-artifact variant to AssetResource and a new assetsCreateUrl RPC handler that resolves artifact paths from thread activity history within the assistant message time window.
  • Adds normalizeGeneratedImageReference (client) and normalizeThreadArtifactReference (server) to validate and normalize inline code image references.
  • Extends ChatMarkdown to detect inline code containing valid image references and render them as lazy-loaded thumbnails via a new MarkdownThreadArtifactImage component; clicking opens the expanded image view.
  • AssistantTimelineRow passes turn/message context to ChatMarkdown to scope artifact resolution.
  • Risk: if artifact path resolution finds zero or multiple matches in the message window, the image silently falls back to plain <code> rendering.

Macroscope summarized fdf4006.


Note

Medium Risk
Adds authorization logic that maps user-supplied relative paths to filesystem locations via projected activities; ambiguous or out-of-window matches fail closed, but incorrect correlation could still expose wrong files within session storage.

Overview
Assistant messages can show generated image paths (e.g. `images/1.jpg`) as inline previews instead of plain code, with click-to-expand in the existing lightbox.

A new thread-artifact AssetResource carries threadId, turnId, messageId, and a relative image path. assetsCreateUrl resolves it by loading thread detail, matching the path to tool activity output via ThreadArtifactResolver (message time window, unique suffix match, fail-closed on ambiguity), then issuing a signed URL through the existing workspace-file flow against the resolved session directory. issueAssetUrl no longer accepts thread-artifact directly.

ChatMarkdown detects un-fenced inline code that normalizes to a safe image reference and, when turn/message context is passed from MessagesTimeline, fetches a URL and renders MarkdownThreadArtifactImage (~25% width). Client and server both reject absolute paths, traversal, and non-image extensions.

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

@coderabbitai

coderabbitai Bot commented Jul 15, 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

Run ID: 425e7615-46c8-49bd-b7a2-3331dcad6ab2

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 15, 2026
@jakeleventhal jakeleventhal marked this pull request as ready for review July 15, 2026 04:18
Comment on lines +896 to +898
function generatedImageReference(value: string): string | null {
const reference = value.trim();
const segments = reference.split("/");

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.

🟡 Medium components/ChatMarkdown.tsx:896

generatedImageReference splits the reference only on /, so a Windows-style path like images\1.jpg produces a single segment and is rejected even though the server accepts and normalizes that same artifact path. This causes inline generated-image rendering to fail for Windows-style references. Consider normalizing \ to / before splitting and validating, matching the server's behavior.

Suggested change
function generatedImageReference(value: string): string | null {
const reference = value.trim();
const segments = reference.split("/");
function generatedImageReference(value: string): string | null {
const reference = value.trim().replace(/\\/g, "/");
const segments = reference.split("/");
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around lines 896-898:

`generatedImageReference` splits the reference only on `/`, so a Windows-style path like `images\1.jpg` produces a single segment and is rejected even though the server accepts and normalizes that same artifact path. This causes inline generated-image rendering to fail for Windows-style references. Consider normalizing `\` to `/` before splitting and validating, matching the server's behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 391fa4da8: normalize generated-image path separators before validation.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Comment thread apps/web/src/components/ChatMarkdown.tsx
Comment thread apps/server/src/ws.ts Outdated
Comment thread apps/server/src/ws.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces a new user-facing feature (inline image rendering) with new components and server-side logic. Additionally, there are multiple unresolved review comments identifying bugs including Windows path handling issues, broken fenced code block rendering, and incorrect artifact correlation within turns.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a679e683b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/server/src/ws.ts Outdated
Comment on lines +160 to +161
const rawOutput = asUnknownRecord(data?.rawOutput);
const artifactPath = rawOutput?.path;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Read Codex savedPath when resolving artifacts

When the matching activity comes from Codex app-server image generation, CodexAdapter stores the raw item payload as payload.data (see apps/server/src/provider/Layers/CodexAdapter.ts:476-484), and the generated file is exposed as data.item.savedPath for imageGeneration (or data.item.path for imageView), not as data.rawOutput.path. With this lookup limited to rawOutput.path, a valid assistant code span such as images/1.jpg never matches Codex's generated-image activity, so assets.createUrl returns not found and the primary Codex generated images stay rendered as plain code.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 2c613968b: resolve Codex imageGeneration.savedPath and imageView.path values.

Comment on lines +1557 to +1560
code({ node, className: codeClassName, children, ...props }) {
const artifactReference = codeClassName
? null
: generatedImageReference(plainHastText(node) ?? "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve fenced code block rendering

With this code component registered, fenced-code children passed to the pre renderer are React elements of this function rather than the literal "code" element that extractCodeBlock requires (onlyChild.type !== "code"). For any fenced code block, the pre renderer falls back to raw <pre> and skips the existing MarkdownCodeBlock/Shiki path, removing code-block chrome and highlighting; if a fence contains an image-looking path it can also render an artifact preview inside the block.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bd0756a80: add regression coverage for enhanced fenced-code rendering.

Comment on lines +897 to +898
const reference = value.trim();
const segments = reference.split("/");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize backslashes before matching artifacts

For Windows-style relative artifact references such as images\\1.jpg, this client matcher splits only on /, so it returns null before the server-side backslash normalization can run and the image remains plain code. The server accepts backslashes in normalizeThreadArtifactReference, so normalize them here too to keep generated images working when providers format relative paths with Windows separators.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0dcfdaf6d: add focused Windows-path normalization coverage.

Comment thread apps/server/src/ws.ts Outdated
Comment on lines +156 to +157
for (const activity of [...thread.activities].toReversed()) {
if (activity.turnId !== turnId) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Correlate artifacts within a turn

When a single turn generates the same relative artifact name more than once (for example two image-generation calls that both report images/1.jpg from different session directories), this reverse scan always picks the last activity in that turn for every assistant message carrying that turnId. Earlier messages in the turn then resolve to a later artifact, so the UI can show the wrong generated image; the lookup needs a message/activity ordering discriminator rather than turnId alone.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in c6a0a2a70: correlate artifact lookup to the assistant message's activity window.

Comment thread apps/web/src/components/ChatMarkdown.tsx Outdated

@cursor cursor Bot 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 fdf4006. Configure here.

for (const activity of thread.activities) {
if (activity.turnId !== turnId) continue;
const createdAt = Date.parse(activity.createdAt);
if (Number.isNaN(createdAt) || createdAt <= window.after || createdAt > window.before) continue;

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.

Activity window excludes mid-stream tools

Medium Severity

The artifactMessageWindow function uses an assistant message's createdAt as the upper bound for matching activities. For streaming messages, this createdAt is from the first chunk. Activities, like image generation, that occur later in the same streaming message have a createdAt after this bound, causing findThreadArtifactPath to incorrectly exclude them and preventing inline previews from resolving.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fdf4006. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant