fix(chat): show Codex generated images in the thread - #6441

Open
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat
Open

fix(chat): show Codex generated images in the thread#6441
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat

Conversation

@sambhavthakkar

@sambhavthakkarsambhavthakkar commented Aug 13, 2026

Copy link
Copy Markdown

What Changed

Codex imagegen already writes a file and emits savedPath. T3 dropped that path, labeled the row Image view, and only served workspace files, so generated images never showed in chat.

  • Keep savedPath on the Codex item and label it Generated image
  • Serve $CODEX_HOME/generated_images through signed asset URLs (not the base64 result over the websocket)
  • Render the file inline in web and mobile chat (markdown ![]() and the work row)

Why

The file is already on disk and the protocol already names it. Chat should show the picture, including remote clients.

UI Changes

  • Before: work row is a generic Image view / file path; markdown ![]() does not load
  • After: the generated file renders inline in the thread and can be expanded

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes

Note

Medium Risk
Adds host-path asset access outside the workspace with root allowlisting—security-sensitive but scoped to Codex generated_images and exact basename tokens; broad touch across server assets, ingestion, and both clients.

Overview
Codex-generated images now render inline in web and mobile chat (markdown images and work-log rows) instead of showing only a path or a generic “Image view” label.

On the server, Codex imageGeneration / imageView items keep full savedPath (no truncation for image_view), map to Generated image where appropriate, and workspace asset URLs can fall back to host-image-exact tokens for files under configured Codex generated_images roots when the path is outside the project workspace. WebSocket asset issuance passes those roots from server settings.

Clients add custom markdown image rendering (renderImage on native markdown; ChatWorkspaceImage / MarkdownWorkspaceImage) with signed workspace URLs, loading/error UI, and tap-to-preview. Work-log activities expose imagePath from lifecycle payloads for inline previews. Mobile asset hooks gain useAssetUrlState for non-binary loading/failure handling; list sizing skips fixed height when a row has an image preview.

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

Note

Show Codex-generated images inline in chat threads on web and mobile

  • Adds end-to-end support for rendering image_view work log entries as inline image previews in both the web MessagesTimeline and mobile ThreadFeed, with click-to-expand dialogs and loading/failure states.
  • Introduces a new host-image-exact asset claim in AssetAccess.ts so the server can securely serve generated images that live outside the workspace root, validated against configured generatedImagesRoots.
  • Extends CodexAdapter.ts to populate detail on imageGeneration/imageView items with the saved file path, and preserves full (untruncated) paths through the ingestion pipeline for image_view events.
  • Adds a renderImage prop to NativeMarkdownBlock and SelectableMarkdownText so custom image renderers can be injected into the markdown tree on mobile.
  • Risk: image_view detail strings are no longer truncated anywhere in the pipeline; very long paths are passed through as-is.

Macroscope summarized a1816fc.

@coderabbitai

coderabbitaiBot commented Aug 13, 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: 140d9003-d0d1-4b7e-a057-eccf5aa815a3

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

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@sambhavthakkar
sambhavthakkarforce-pushed the fix/codex-generated-images-in-chat branch from 0f61eab to 3052999CompareAugust 13, 2026 07:15

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts
Comment threadapps/server/src/ws.ts
function* (settings: ServerSettingsValue) {
const path = yield* Path.Path;
const roots: string[] = [];
const instances = Object.values(settings.providerInstances)

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.

🔴 Criticalsrc/ws.ts:142

A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. resolveCodexGeneratedImagesRoots returns generated_images roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's modelSelection.instanceId (and its session provider instance) instead of returning all configured roots.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 142:
A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. `resolveCodexGeneratedImagesRoots` returns `generated_images` roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's `modelSelection.instanceId` (and its session provider instance) instead of returning all configured roots.

...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.detail
? { detail: toolLifecycleDetail(event.payload.itemType, event.payload.detail) }

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.

🟡 MediumLayers/ProviderRuntimeIngestion.ts:805

image_view activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. toolLifecycleDetail disables truncation for every image_view, even though OpenCode maps any tool name containing “image” to that type and detailFromToolPart supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 805:
`image_view` activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. `toolLifecycleDetail` disables truncation for every `image_view`, even though OpenCode maps any tool name containing “image” to that type and `detailFromToolPart` supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

if (trimmed.includes("\n") || /\s/.test(trimmed)) {
return undefined;
}
if (trimmed.includes("/") || trimmed.includes("\\")) {

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.

🟡 MediumLayers/CodexAdapter.ts:283

imageGenerationResultPath returns short base64 image data containing / as detail, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to revisedPrompt. Validate that result is an actual path rather than relying on / alone.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 283:
`imageGenerationResultPath` returns short base64 image data containing `/` as `detail`, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to `revisedPrompt`. Validate that `result` is an actual path rather than relying on `/` alone.

Comment on lines +291 to +295
return (
trimText(item.savedPath ?? undefined) ??
imageGenerationResultPath(item.result) ??
trimText(item.revisedPrompt ?? undefined)
);

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.

🟡 MediumLayers/CodexAdapter.ts:291

Pathless imageGeneration items return revisedPrompt as detail, so extractImageViewPath sends prompt text such as a ceramic mug to the workspace-asset loader as imagePath, producing a broken image preview. Only actual image paths should populate this detail channel; remove the revisedPrompt fallback.

Suggested change
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)??
trimText(item.revisedPrompt??undefined)
);
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)
);
Also found in 1 other location(s)

apps/mobile/src/lib/threadActivity.ts:966

extractImageViewPath treats every non-empty payload.detail as an image path. For an imageGeneration item without savedPath (the protocol makes it optional), CodexAdapter.itemDetail can populate detail with revisedPrompt; this then becomes imagePath, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept detail when it is known to be a path (for example, an actual imageView item), not for pathless image-generation results.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 291-295:
Pathless `imageGeneration` items return `revisedPrompt` as `detail`, so `extractImageViewPath` sends prompt text such as `a ceramic mug` to the workspace-asset loader as `imagePath`, producing a broken image preview. Only actual image paths should populate this detail channel; remove the `revisedPrompt` fallback.
Also found in 1 other location(s):
- apps/mobile/src/lib/threadActivity.ts:966 -- `extractImageViewPath` treats every non-empty `payload.detail` as an image path. For an `imageGeneration` item without `savedPath` (the protocol makes it optional), `CodexAdapter.itemDetail` can populate `detail` with `revisedPrompt`; this then becomes `imagePath`, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept `detail` when it is known to be a path (for example, an actual `imageView` item), not for pathless image-generation results.

/>
);
}
if (src && /^https?:\/\//i.test(src)) {

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.

🟡 Mediumcomponents/ChatMarkdown.tsx:1564

Protocol-relative images such as ![logo](//cdn.example.com/logo.png) fail to load and, when a thread exists, are sent to ChatWorkspaceImage as a workspace path. The img renderer only identifies http:// and https:// as remote, so resolveMarkdownFileLinkMeta classifies //cdn.example.com/logo.png as a file link; treat // URLs as remote as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1564:
Protocol-relative images such as `![logo](//cdn.example.com/logo.png)` fail to load and, when a thread exists, are sent to `ChatWorkspaceImage` as a workspace path. The `img` renderer only identifies `http://` and `https://` as remote, so `resolveMarkdownFileLinkMeta` classifies `//cdn.example.com/logo.png` as a file link; treat `//` URLs as remote as well.

renderFeedEntry(info, {
environmentId: props.environmentId,
threadId: props.threadId,
workspaceRoot: props.workspaceRoot,

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.

🟠 Highthreads/ThreadFeed.tsx:1969

Visible image rows keep using the previous workspaceRoot when threadCwd changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. workspaceRoot is captured by renderItem but is absent from listAppearanceData/extraData, and LegendList does not repaint visible rows when only that closure changes; include it in extraData (or otherwise invalidate/remount the rows).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1969:
Visible image rows keep using the previous `workspaceRoot` when `threadCwd` changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. `workspaceRoot` is captured by `renderItem` but is absent from `listAppearanceData`/`extraData`, and LegendList does not repaint visible rows when only that closure changes; include it in `extraData` (or otherwise invalidate/remount the rows).

Comment on lines +15 to +42
const [failed, setFailed] = useState(false);
const [expanded, setExpanded] = useState(false);
const name = props.alt.trim().length > 0 ? props.alt : "Image";

if (failed) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
</div>
);
}

return (
<>
<button
type="button"
className={cn(
"my-2 block max-w-full cursor-zoom-in overflow-hidden rounded-lg border border-border/80 bg-background/70",
props.className,
)}
aria-label={`Preview ${name}`}
onClick={() => setExpanded(true)}
>
<img
src={props.src}
alt={name}
className="block h-auto max-h-[360px] max-w-full object-contain"
onError={() => setFailed(true)}

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.

🟡 Mediumchat/ChatWorkspaceImage.tsx:15

After ChatRemoteImage's image load fails, changing props.src still renders the old error state, so corrected remote URLs remain permanently hidden. Because failed is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when props.src changes).

- const [failed, setFailed] = useState(false);+ const [failed, setFailed] = useState<string | null>(null);- if (failed) {+ if (failed === props.src) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
@@
- onError={() => setFailed(true)}+ onError={() => setFailed(props.src)}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatWorkspaceImage.tsx around lines 15-42:
After `ChatRemoteImage`'s image load fails, changing `props.src` still renders the old error state, so corrected remote URLs remain permanently hidden. Because `failed` is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when `props.src` changes).

</div>
</div>
{imagePath && threadRef ? (
<div className="ms-7 mt-1" onClick={stopRowToggle} onPointerDown={stopRowToggle}>

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.

🟡 Mediumchat/MessagesTimeline.tsx:2398

When ChatWorkspaceImage is focused, pressing Enter or Space bubbles to the row's onKeyDown, which calls preventDefault() and toggles expanded instead of activating the image button. Add an onKeyDown handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 2398:
When `ChatWorkspaceImage` is focused, pressing Enter or Space bubbles to the row's `onKeyDown`, which calls `preventDefault()` and toggles `expanded` instead of activating the image button. Add an `onKeyDown` handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

@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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3052999. Configure here.

Comment threadapps/web/src/session-logic.ts
@macroscopeapp

macroscopeappBot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This PR introduces new capability for displaying Codex-generated images with cross-cutting changes to mobile, web, and server-side authorization logic. A Critical finding identifies a potential thread isolation bypass in asset URL minting that requires human review.

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

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

Labels

size:XL500-999 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.

1 participant

@sambhavthakkar
, '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(chat): show Codex generated images in the thread - #6441

Open
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat
Open

fix(chat): show Codex generated images in the thread#6441
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat

Conversation

@sambhavthakkar

@sambhavthakkarsambhavthakkar commented Aug 13, 2026

Copy link
Copy Markdown

What Changed

Codex imagegen already writes a file and emits savedPath. T3 dropped that path, labeled the row Image view, and only served workspace files, so generated images never showed in chat.

  • Keep savedPath on the Codex item and label it Generated image
  • Serve $CODEX_HOME/generated_images through signed asset URLs (not the base64 result over the websocket)
  • Render the file inline in web and mobile chat (markdown ![]() and the work row)

Why

The file is already on disk and the protocol already names it. Chat should show the picture, including remote clients.

UI Changes

  • Before: work row is a generic Image view / file path; markdown ![]() does not load
  • After: the generated file renders inline in the thread and can be expanded

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes

Note

Medium Risk
Adds host-path asset access outside the workspace with root allowlisting—security-sensitive but scoped to Codex generated_images and exact basename tokens; broad touch across server assets, ingestion, and both clients.

Overview
Codex-generated images now render inline in web and mobile chat (markdown images and work-log rows) instead of showing only a path or a generic “Image view” label.

On the server, Codex imageGeneration / imageView items keep full savedPath (no truncation for image_view), map to Generated image where appropriate, and workspace asset URLs can fall back to host-image-exact tokens for files under configured Codex generated_images roots when the path is outside the project workspace. WebSocket asset issuance passes those roots from server settings.

Clients add custom markdown image rendering (renderImage on native markdown; ChatWorkspaceImage / MarkdownWorkspaceImage) with signed workspace URLs, loading/error UI, and tap-to-preview. Work-log activities expose imagePath from lifecycle payloads for inline previews. Mobile asset hooks gain useAssetUrlState for non-binary loading/failure handling; list sizing skips fixed height when a row has an image preview.

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

Note

Show Codex-generated images inline in chat threads on web and mobile

  • Adds end-to-end support for rendering image_view work log entries as inline image previews in both the web MessagesTimeline and mobile ThreadFeed, with click-to-expand dialogs and loading/failure states.
  • Introduces a new host-image-exact asset claim in AssetAccess.ts so the server can securely serve generated images that live outside the workspace root, validated against configured generatedImagesRoots.
  • Extends CodexAdapter.ts to populate detail on imageGeneration/imageView items with the saved file path, and preserves full (untruncated) paths through the ingestion pipeline for image_view events.
  • Adds a renderImage prop to NativeMarkdownBlock and SelectableMarkdownText so custom image renderers can be injected into the markdown tree on mobile.
  • Risk: image_view detail strings are no longer truncated anywhere in the pipeline; very long paths are passed through as-is.

Macroscope summarized a1816fc.

@coderabbitai

coderabbitaiBot commented Aug 13, 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: 140d9003-d0d1-4b7e-a057-eccf5aa815a3

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

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@sambhavthakkar
sambhavthakkarforce-pushed the fix/codex-generated-images-in-chat branch from 0f61eab to 3052999CompareAugust 13, 2026 07:15

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts
Comment threadapps/server/src/ws.ts
function* (settings: ServerSettingsValue) {
const path = yield* Path.Path;
const roots: string[] = [];
const instances = Object.values(settings.providerInstances)

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.

🔴 Criticalsrc/ws.ts:142

A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. resolveCodexGeneratedImagesRoots returns generated_images roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's modelSelection.instanceId (and its session provider instance) instead of returning all configured roots.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 142:
A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. `resolveCodexGeneratedImagesRoots` returns `generated_images` roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's `modelSelection.instanceId` (and its session provider instance) instead of returning all configured roots.

...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.detail
? { detail: toolLifecycleDetail(event.payload.itemType, event.payload.detail) }

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.

🟡 MediumLayers/ProviderRuntimeIngestion.ts:805

image_view activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. toolLifecycleDetail disables truncation for every image_view, even though OpenCode maps any tool name containing “image” to that type and detailFromToolPart supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 805:
`image_view` activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. `toolLifecycleDetail` disables truncation for every `image_view`, even though OpenCode maps any tool name containing “image” to that type and `detailFromToolPart` supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

if (trimmed.includes("\n") || /\s/.test(trimmed)) {
return undefined;
}
if (trimmed.includes("/") || trimmed.includes("\\")) {

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.

🟡 MediumLayers/CodexAdapter.ts:283

imageGenerationResultPath returns short base64 image data containing / as detail, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to revisedPrompt. Validate that result is an actual path rather than relying on / alone.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 283:
`imageGenerationResultPath` returns short base64 image data containing `/` as `detail`, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to `revisedPrompt`. Validate that `result` is an actual path rather than relying on `/` alone.

Comment on lines +291 to +295
return (
trimText(item.savedPath ?? undefined) ??
imageGenerationResultPath(item.result) ??
trimText(item.revisedPrompt ?? undefined)
);

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.

🟡 MediumLayers/CodexAdapter.ts:291

Pathless imageGeneration items return revisedPrompt as detail, so extractImageViewPath sends prompt text such as a ceramic mug to the workspace-asset loader as imagePath, producing a broken image preview. Only actual image paths should populate this detail channel; remove the revisedPrompt fallback.

Suggested change
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)??
trimText(item.revisedPrompt??undefined)
);
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)
);
Also found in 1 other location(s)

apps/mobile/src/lib/threadActivity.ts:966

extractImageViewPath treats every non-empty payload.detail as an image path. For an imageGeneration item without savedPath (the protocol makes it optional), CodexAdapter.itemDetail can populate detail with revisedPrompt; this then becomes imagePath, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept detail when it is known to be a path (for example, an actual imageView item), not for pathless image-generation results.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 291-295:
Pathless `imageGeneration` items return `revisedPrompt` as `detail`, so `extractImageViewPath` sends prompt text such as `a ceramic mug` to the workspace-asset loader as `imagePath`, producing a broken image preview. Only actual image paths should populate this detail channel; remove the `revisedPrompt` fallback.
Also found in 1 other location(s):
- apps/mobile/src/lib/threadActivity.ts:966 -- `extractImageViewPath` treats every non-empty `payload.detail` as an image path. For an `imageGeneration` item without `savedPath` (the protocol makes it optional), `CodexAdapter.itemDetail` can populate `detail` with `revisedPrompt`; this then becomes `imagePath`, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept `detail` when it is known to be a path (for example, an actual `imageView` item), not for pathless image-generation results.

/>
);
}
if (src && /^https?:\/\//i.test(src)) {

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.

🟡 Mediumcomponents/ChatMarkdown.tsx:1564

Protocol-relative images such as ![logo](//cdn.example.com/logo.png) fail to load and, when a thread exists, are sent to ChatWorkspaceImage as a workspace path. The img renderer only identifies http:// and https:// as remote, so resolveMarkdownFileLinkMeta classifies //cdn.example.com/logo.png as a file link; treat // URLs as remote as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1564:
Protocol-relative images such as `![logo](//cdn.example.com/logo.png)` fail to load and, when a thread exists, are sent to `ChatWorkspaceImage` as a workspace path. The `img` renderer only identifies `http://` and `https://` as remote, so `resolveMarkdownFileLinkMeta` classifies `//cdn.example.com/logo.png` as a file link; treat `//` URLs as remote as well.

renderFeedEntry(info, {
environmentId: props.environmentId,
threadId: props.threadId,
workspaceRoot: props.workspaceRoot,

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.

🟠 Highthreads/ThreadFeed.tsx:1969

Visible image rows keep using the previous workspaceRoot when threadCwd changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. workspaceRoot is captured by renderItem but is absent from listAppearanceData/extraData, and LegendList does not repaint visible rows when only that closure changes; include it in extraData (or otherwise invalidate/remount the rows).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1969:
Visible image rows keep using the previous `workspaceRoot` when `threadCwd` changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. `workspaceRoot` is captured by `renderItem` but is absent from `listAppearanceData`/`extraData`, and LegendList does not repaint visible rows when only that closure changes; include it in `extraData` (or otherwise invalidate/remount the rows).

Comment on lines +15 to +42
const [failed, setFailed] = useState(false);
const [expanded, setExpanded] = useState(false);
const name = props.alt.trim().length > 0 ? props.alt : "Image";

if (failed) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
</div>
);
}

return (
<>
<button
type="button"
className={cn(
"my-2 block max-w-full cursor-zoom-in overflow-hidden rounded-lg border border-border/80 bg-background/70",
props.className,
)}
aria-label={`Preview ${name}`}
onClick={() => setExpanded(true)}
>
<img
src={props.src}
alt={name}
className="block h-auto max-h-[360px] max-w-full object-contain"
onError={() => setFailed(true)}

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.

🟡 Mediumchat/ChatWorkspaceImage.tsx:15

After ChatRemoteImage's image load fails, changing props.src still renders the old error state, so corrected remote URLs remain permanently hidden. Because failed is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when props.src changes).

- const [failed, setFailed] = useState(false);+ const [failed, setFailed] = useState<string | null>(null);- if (failed) {+ if (failed === props.src) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
@@
- onError={() => setFailed(true)}+ onError={() => setFailed(props.src)}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatWorkspaceImage.tsx around lines 15-42:
After `ChatRemoteImage`'s image load fails, changing `props.src` still renders the old error state, so corrected remote URLs remain permanently hidden. Because `failed` is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when `props.src` changes).

</div>
</div>
{imagePath && threadRef ? (
<div className="ms-7 mt-1" onClick={stopRowToggle} onPointerDown={stopRowToggle}>

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.

🟡 Mediumchat/MessagesTimeline.tsx:2398

When ChatWorkspaceImage is focused, pressing Enter or Space bubbles to the row's onKeyDown, which calls preventDefault() and toggles expanded instead of activating the image button. Add an onKeyDown handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 2398:
When `ChatWorkspaceImage` is focused, pressing Enter or Space bubbles to the row's `onKeyDown`, which calls `preventDefault()` and toggles `expanded` instead of activating the image button. Add an `onKeyDown` handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

@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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3052999. Configure here.

Comment threadapps/web/src/session-logic.ts
@macroscopeapp

macroscopeappBot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This PR introduces new capability for displaying Codex-generated images with cross-cutting changes to mobile, web, and server-side authorization logic. A Critical finding identifies a potential thread isolation bypass in asset URL minting that requires human review.

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

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

Labels

size:XL500-999 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.

1 participant

@sambhavthakkar
, '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(chat): show Codex generated images in the thread - #6441

Open
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat
Open

fix(chat): show Codex generated images in the thread#6441
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat

Conversation

@sambhavthakkar

@sambhavthakkarsambhavthakkar commented Aug 13, 2026

Copy link
Copy Markdown

What Changed

Codex imagegen already writes a file and emits savedPath. T3 dropped that path, labeled the row Image view, and only served workspace files, so generated images never showed in chat.

  • Keep savedPath on the Codex item and label it Generated image
  • Serve $CODEX_HOME/generated_images through signed asset URLs (not the base64 result over the websocket)
  • Render the file inline in web and mobile chat (markdown ![]() and the work row)

Why

The file is already on disk and the protocol already names it. Chat should show the picture, including remote clients.

UI Changes

  • Before: work row is a generic Image view / file path; markdown ![]() does not load
  • After: the generated file renders inline in the thread and can be expanded

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes

Note

Medium Risk
Adds host-path asset access outside the workspace with root allowlisting—security-sensitive but scoped to Codex generated_images and exact basename tokens; broad touch across server assets, ingestion, and both clients.

Overview
Codex-generated images now render inline in web and mobile chat (markdown images and work-log rows) instead of showing only a path or a generic “Image view” label.

On the server, Codex imageGeneration / imageView items keep full savedPath (no truncation for image_view), map to Generated image where appropriate, and workspace asset URLs can fall back to host-image-exact tokens for files under configured Codex generated_images roots when the path is outside the project workspace. WebSocket asset issuance passes those roots from server settings.

Clients add custom markdown image rendering (renderImage on native markdown; ChatWorkspaceImage / MarkdownWorkspaceImage) with signed workspace URLs, loading/error UI, and tap-to-preview. Work-log activities expose imagePath from lifecycle payloads for inline previews. Mobile asset hooks gain useAssetUrlState for non-binary loading/failure handling; list sizing skips fixed height when a row has an image preview.

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

Note

Show Codex-generated images inline in chat threads on web and mobile

  • Adds end-to-end support for rendering image_view work log entries as inline image previews in both the web MessagesTimeline and mobile ThreadFeed, with click-to-expand dialogs and loading/failure states.
  • Introduces a new host-image-exact asset claim in AssetAccess.ts so the server can securely serve generated images that live outside the workspace root, validated against configured generatedImagesRoots.
  • Extends CodexAdapter.ts to populate detail on imageGeneration/imageView items with the saved file path, and preserves full (untruncated) paths through the ingestion pipeline for image_view events.
  • Adds a renderImage prop to NativeMarkdownBlock and SelectableMarkdownText so custom image renderers can be injected into the markdown tree on mobile.
  • Risk: image_view detail strings are no longer truncated anywhere in the pipeline; very long paths are passed through as-is.

Macroscope summarized a1816fc.

@coderabbitai

coderabbitaiBot commented Aug 13, 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: 140d9003-d0d1-4b7e-a057-eccf5aa815a3

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

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@sambhavthakkar
sambhavthakkarforce-pushed the fix/codex-generated-images-in-chat branch from 0f61eab to 3052999CompareAugust 13, 2026 07:15

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts
Comment threadapps/server/src/ws.ts
function* (settings: ServerSettingsValue) {
const path = yield* Path.Path;
const roots: string[] = [];
const instances = Object.values(settings.providerInstances)

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.

🔴 Criticalsrc/ws.ts:142

A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. resolveCodexGeneratedImagesRoots returns generated_images roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's modelSelection.instanceId (and its session provider instance) instead of returning all configured roots.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 142:
A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. `resolveCodexGeneratedImagesRoots` returns `generated_images` roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's `modelSelection.instanceId` (and its session provider instance) instead of returning all configured roots.

...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.detail
? { detail: toolLifecycleDetail(event.payload.itemType, event.payload.detail) }

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.

🟡 MediumLayers/ProviderRuntimeIngestion.ts:805

image_view activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. toolLifecycleDetail disables truncation for every image_view, even though OpenCode maps any tool name containing “image” to that type and detailFromToolPart supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 805:
`image_view` activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. `toolLifecycleDetail` disables truncation for every `image_view`, even though OpenCode maps any tool name containing “image” to that type and `detailFromToolPart` supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

if (trimmed.includes("\n") || /\s/.test(trimmed)) {
return undefined;
}
if (trimmed.includes("/") || trimmed.includes("\\")) {

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.

🟡 MediumLayers/CodexAdapter.ts:283

imageGenerationResultPath returns short base64 image data containing / as detail, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to revisedPrompt. Validate that result is an actual path rather than relying on / alone.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 283:
`imageGenerationResultPath` returns short base64 image data containing `/` as `detail`, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to `revisedPrompt`. Validate that `result` is an actual path rather than relying on `/` alone.

Comment on lines +291 to +295
return (
trimText(item.savedPath ?? undefined) ??
imageGenerationResultPath(item.result) ??
trimText(item.revisedPrompt ?? undefined)
);

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.

🟡 MediumLayers/CodexAdapter.ts:291

Pathless imageGeneration items return revisedPrompt as detail, so extractImageViewPath sends prompt text such as a ceramic mug to the workspace-asset loader as imagePath, producing a broken image preview. Only actual image paths should populate this detail channel; remove the revisedPrompt fallback.

Suggested change
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)??
trimText(item.revisedPrompt??undefined)
);
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)
);
Also found in 1 other location(s)

apps/mobile/src/lib/threadActivity.ts:966

extractImageViewPath treats every non-empty payload.detail as an image path. For an imageGeneration item without savedPath (the protocol makes it optional), CodexAdapter.itemDetail can populate detail with revisedPrompt; this then becomes imagePath, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept detail when it is known to be a path (for example, an actual imageView item), not for pathless image-generation results.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 291-295:
Pathless `imageGeneration` items return `revisedPrompt` as `detail`, so `extractImageViewPath` sends prompt text such as `a ceramic mug` to the workspace-asset loader as `imagePath`, producing a broken image preview. Only actual image paths should populate this detail channel; remove the `revisedPrompt` fallback.
Also found in 1 other location(s):
- apps/mobile/src/lib/threadActivity.ts:966 -- `extractImageViewPath` treats every non-empty `payload.detail` as an image path. For an `imageGeneration` item without `savedPath` (the protocol makes it optional), `CodexAdapter.itemDetail` can populate `detail` with `revisedPrompt`; this then becomes `imagePath`, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept `detail` when it is known to be a path (for example, an actual `imageView` item), not for pathless image-generation results.

/>
);
}
if (src && /^https?:\/\//i.test(src)) {

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.

🟡 Mediumcomponents/ChatMarkdown.tsx:1564

Protocol-relative images such as ![logo](//cdn.example.com/logo.png) fail to load and, when a thread exists, are sent to ChatWorkspaceImage as a workspace path. The img renderer only identifies http:// and https:// as remote, so resolveMarkdownFileLinkMeta classifies //cdn.example.com/logo.png as a file link; treat // URLs as remote as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1564:
Protocol-relative images such as `![logo](//cdn.example.com/logo.png)` fail to load and, when a thread exists, are sent to `ChatWorkspaceImage` as a workspace path. The `img` renderer only identifies `http://` and `https://` as remote, so `resolveMarkdownFileLinkMeta` classifies `//cdn.example.com/logo.png` as a file link; treat `//` URLs as remote as well.

renderFeedEntry(info, {
environmentId: props.environmentId,
threadId: props.threadId,
workspaceRoot: props.workspaceRoot,

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.

🟠 Highthreads/ThreadFeed.tsx:1969

Visible image rows keep using the previous workspaceRoot when threadCwd changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. workspaceRoot is captured by renderItem but is absent from listAppearanceData/extraData, and LegendList does not repaint visible rows when only that closure changes; include it in extraData (or otherwise invalidate/remount the rows).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1969:
Visible image rows keep using the previous `workspaceRoot` when `threadCwd` changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. `workspaceRoot` is captured by `renderItem` but is absent from `listAppearanceData`/`extraData`, and LegendList does not repaint visible rows when only that closure changes; include it in `extraData` (or otherwise invalidate/remount the rows).

Comment on lines +15 to +42
const [failed, setFailed] = useState(false);
const [expanded, setExpanded] = useState(false);
const name = props.alt.trim().length > 0 ? props.alt : "Image";

if (failed) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
</div>
);
}

return (
<>
<button
type="button"
className={cn(
"my-2 block max-w-full cursor-zoom-in overflow-hidden rounded-lg border border-border/80 bg-background/70",
props.className,
)}
aria-label={`Preview ${name}`}
onClick={() => setExpanded(true)}
>
<img
src={props.src}
alt={name}
className="block h-auto max-h-[360px] max-w-full object-contain"
onError={() => setFailed(true)}

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.

🟡 Mediumchat/ChatWorkspaceImage.tsx:15

After ChatRemoteImage's image load fails, changing props.src still renders the old error state, so corrected remote URLs remain permanently hidden. Because failed is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when props.src changes).

- const [failed, setFailed] = useState(false);+ const [failed, setFailed] = useState<string | null>(null);- if (failed) {+ if (failed === props.src) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
@@
- onError={() => setFailed(true)}+ onError={() => setFailed(props.src)}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatWorkspaceImage.tsx around lines 15-42:
After `ChatRemoteImage`'s image load fails, changing `props.src` still renders the old error state, so corrected remote URLs remain permanently hidden. Because `failed` is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when `props.src` changes).

</div>
</div>
{imagePath && threadRef ? (
<div className="ms-7 mt-1" onClick={stopRowToggle} onPointerDown={stopRowToggle}>

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.

🟡 Mediumchat/MessagesTimeline.tsx:2398

When ChatWorkspaceImage is focused, pressing Enter or Space bubbles to the row's onKeyDown, which calls preventDefault() and toggles expanded instead of activating the image button. Add an onKeyDown handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 2398:
When `ChatWorkspaceImage` is focused, pressing Enter or Space bubbles to the row's `onKeyDown`, which calls `preventDefault()` and toggles `expanded` instead of activating the image button. Add an `onKeyDown` handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

@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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3052999. Configure here.

Comment threadapps/web/src/session-logic.ts
@macroscopeapp

macroscopeappBot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This PR introduces new capability for displaying Codex-generated images with cross-cutting changes to mobile, web, and server-side authorization logic. A Critical finding identifies a potential thread isolation bypass in asset URL minting that requires human review.

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

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

Labels

size:XL500-999 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.

1 participant

@sambhavthakkar
, '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(chat): show Codex generated images in the thread - #6441

Open
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat
Open

fix(chat): show Codex generated images in the thread#6441
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat

Conversation

@sambhavthakkar

@sambhavthakkarsambhavthakkar commented Aug 13, 2026

Copy link
Copy Markdown

What Changed

Codex imagegen already writes a file and emits savedPath. T3 dropped that path, labeled the row Image view, and only served workspace files, so generated images never showed in chat.

  • Keep savedPath on the Codex item and label it Generated image
  • Serve $CODEX_HOME/generated_images through signed asset URLs (not the base64 result over the websocket)
  • Render the file inline in web and mobile chat (markdown ![]() and the work row)

Why

The file is already on disk and the protocol already names it. Chat should show the picture, including remote clients.

UI Changes

  • Before: work row is a generic Image view / file path; markdown ![]() does not load
  • After: the generated file renders inline in the thread and can be expanded

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes

Note

Medium Risk
Adds host-path asset access outside the workspace with root allowlisting—security-sensitive but scoped to Codex generated_images and exact basename tokens; broad touch across server assets, ingestion, and both clients.

Overview
Codex-generated images now render inline in web and mobile chat (markdown images and work-log rows) instead of showing only a path or a generic “Image view” label.

On the server, Codex imageGeneration / imageView items keep full savedPath (no truncation for image_view), map to Generated image where appropriate, and workspace asset URLs can fall back to host-image-exact tokens for files under configured Codex generated_images roots when the path is outside the project workspace. WebSocket asset issuance passes those roots from server settings.

Clients add custom markdown image rendering (renderImage on native markdown; ChatWorkspaceImage / MarkdownWorkspaceImage) with signed workspace URLs, loading/error UI, and tap-to-preview. Work-log activities expose imagePath from lifecycle payloads for inline previews. Mobile asset hooks gain useAssetUrlState for non-binary loading/failure handling; list sizing skips fixed height when a row has an image preview.

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

Note

Show Codex-generated images inline in chat threads on web and mobile

  • Adds end-to-end support for rendering image_view work log entries as inline image previews in both the web MessagesTimeline and mobile ThreadFeed, with click-to-expand dialogs and loading/failure states.
  • Introduces a new host-image-exact asset claim in AssetAccess.ts so the server can securely serve generated images that live outside the workspace root, validated against configured generatedImagesRoots.
  • Extends CodexAdapter.ts to populate detail on imageGeneration/imageView items with the saved file path, and preserves full (untruncated) paths through the ingestion pipeline for image_view events.
  • Adds a renderImage prop to NativeMarkdownBlock and SelectableMarkdownText so custom image renderers can be injected into the markdown tree on mobile.
  • Risk: image_view detail strings are no longer truncated anywhere in the pipeline; very long paths are passed through as-is.

Macroscope summarized a1816fc.

@coderabbitai

coderabbitaiBot commented Aug 13, 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: 140d9003-d0d1-4b7e-a057-eccf5aa815a3

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

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@sambhavthakkar
sambhavthakkarforce-pushed the fix/codex-generated-images-in-chat branch from 0f61eab to 3052999CompareAugust 13, 2026 07:15

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts
Comment threadapps/server/src/ws.ts
function* (settings: ServerSettingsValue) {
const path = yield* Path.Path;
const roots: string[] = [];
const instances = Object.values(settings.providerInstances)

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.

🔴 Criticalsrc/ws.ts:142

A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. resolveCodexGeneratedImagesRoots returns generated_images roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's modelSelection.instanceId (and its session provider instance) instead of returning all configured roots.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 142:
A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. `resolveCodexGeneratedImagesRoots` returns `generated_images` roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's `modelSelection.instanceId` (and its session provider instance) instead of returning all configured roots.

...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.detail
? { detail: toolLifecycleDetail(event.payload.itemType, event.payload.detail) }

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.

🟡 MediumLayers/ProviderRuntimeIngestion.ts:805

image_view activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. toolLifecycleDetail disables truncation for every image_view, even though OpenCode maps any tool name containing “image” to that type and detailFromToolPart supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 805:
`image_view` activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. `toolLifecycleDetail` disables truncation for every `image_view`, even though OpenCode maps any tool name containing “image” to that type and `detailFromToolPart` supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

if (trimmed.includes("\n") || /\s/.test(trimmed)) {
return undefined;
}
if (trimmed.includes("/") || trimmed.includes("\\")) {

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.

🟡 MediumLayers/CodexAdapter.ts:283

imageGenerationResultPath returns short base64 image data containing / as detail, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to revisedPrompt. Validate that result is an actual path rather than relying on / alone.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 283:
`imageGenerationResultPath` returns short base64 image data containing `/` as `detail`, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to `revisedPrompt`. Validate that `result` is an actual path rather than relying on `/` alone.

Comment on lines +291 to +295
return (
trimText(item.savedPath ?? undefined) ??
imageGenerationResultPath(item.result) ??
trimText(item.revisedPrompt ?? undefined)
);

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.

🟡 MediumLayers/CodexAdapter.ts:291

Pathless imageGeneration items return revisedPrompt as detail, so extractImageViewPath sends prompt text such as a ceramic mug to the workspace-asset loader as imagePath, producing a broken image preview. Only actual image paths should populate this detail channel; remove the revisedPrompt fallback.

Suggested change
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)??
trimText(item.revisedPrompt??undefined)
);
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)
);
Also found in 1 other location(s)

apps/mobile/src/lib/threadActivity.ts:966

extractImageViewPath treats every non-empty payload.detail as an image path. For an imageGeneration item without savedPath (the protocol makes it optional), CodexAdapter.itemDetail can populate detail with revisedPrompt; this then becomes imagePath, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept detail when it is known to be a path (for example, an actual imageView item), not for pathless image-generation results.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 291-295:
Pathless `imageGeneration` items return `revisedPrompt` as `detail`, so `extractImageViewPath` sends prompt text such as `a ceramic mug` to the workspace-asset loader as `imagePath`, producing a broken image preview. Only actual image paths should populate this detail channel; remove the `revisedPrompt` fallback.
Also found in 1 other location(s):
- apps/mobile/src/lib/threadActivity.ts:966 -- `extractImageViewPath` treats every non-empty `payload.detail` as an image path. For an `imageGeneration` item without `savedPath` (the protocol makes it optional), `CodexAdapter.itemDetail` can populate `detail` with `revisedPrompt`; this then becomes `imagePath`, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept `detail` when it is known to be a path (for example, an actual `imageView` item), not for pathless image-generation results.

/>
);
}
if (src && /^https?:\/\//i.test(src)) {

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.

🟡 Mediumcomponents/ChatMarkdown.tsx:1564

Protocol-relative images such as ![logo](//cdn.example.com/logo.png) fail to load and, when a thread exists, are sent to ChatWorkspaceImage as a workspace path. The img renderer only identifies http:// and https:// as remote, so resolveMarkdownFileLinkMeta classifies //cdn.example.com/logo.png as a file link; treat // URLs as remote as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1564:
Protocol-relative images such as `![logo](//cdn.example.com/logo.png)` fail to load and, when a thread exists, are sent to `ChatWorkspaceImage` as a workspace path. The `img` renderer only identifies `http://` and `https://` as remote, so `resolveMarkdownFileLinkMeta` classifies `//cdn.example.com/logo.png` as a file link; treat `//` URLs as remote as well.

renderFeedEntry(info, {
environmentId: props.environmentId,
threadId: props.threadId,
workspaceRoot: props.workspaceRoot,

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.

🟠 Highthreads/ThreadFeed.tsx:1969

Visible image rows keep using the previous workspaceRoot when threadCwd changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. workspaceRoot is captured by renderItem but is absent from listAppearanceData/extraData, and LegendList does not repaint visible rows when only that closure changes; include it in extraData (or otherwise invalidate/remount the rows).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1969:
Visible image rows keep using the previous `workspaceRoot` when `threadCwd` changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. `workspaceRoot` is captured by `renderItem` but is absent from `listAppearanceData`/`extraData`, and LegendList does not repaint visible rows when only that closure changes; include it in `extraData` (or otherwise invalidate/remount the rows).

Comment on lines +15 to +42
const [failed, setFailed] = useState(false);
const [expanded, setExpanded] = useState(false);
const name = props.alt.trim().length > 0 ? props.alt : "Image";

if (failed) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
</div>
);
}

return (
<>
<button
type="button"
className={cn(
"my-2 block max-w-full cursor-zoom-in overflow-hidden rounded-lg border border-border/80 bg-background/70",
props.className,
)}
aria-label={`Preview ${name}`}
onClick={() => setExpanded(true)}
>
<img
src={props.src}
alt={name}
className="block h-auto max-h-[360px] max-w-full object-contain"
onError={() => setFailed(true)}

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.

🟡 Mediumchat/ChatWorkspaceImage.tsx:15

After ChatRemoteImage's image load fails, changing props.src still renders the old error state, so corrected remote URLs remain permanently hidden. Because failed is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when props.src changes).

- const [failed, setFailed] = useState(false);+ const [failed, setFailed] = useState<string | null>(null);- if (failed) {+ if (failed === props.src) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
@@
- onError={() => setFailed(true)}+ onError={() => setFailed(props.src)}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatWorkspaceImage.tsx around lines 15-42:
After `ChatRemoteImage`'s image load fails, changing `props.src` still renders the old error state, so corrected remote URLs remain permanently hidden. Because `failed` is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when `props.src` changes).

</div>
</div>
{imagePath && threadRef ? (
<div className="ms-7 mt-1" onClick={stopRowToggle} onPointerDown={stopRowToggle}>

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.

🟡 Mediumchat/MessagesTimeline.tsx:2398

When ChatWorkspaceImage is focused, pressing Enter or Space bubbles to the row's onKeyDown, which calls preventDefault() and toggles expanded instead of activating the image button. Add an onKeyDown handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 2398:
When `ChatWorkspaceImage` is focused, pressing Enter or Space bubbles to the row's `onKeyDown`, which calls `preventDefault()` and toggles `expanded` instead of activating the image button. Add an `onKeyDown` handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

@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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3052999. Configure here.

Comment threadapps/web/src/session-logic.ts
@macroscopeapp

macroscopeappBot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This PR introduces new capability for displaying Codex-generated images with cross-cutting changes to mobile, web, and server-side authorization logic. A Critical finding identifies a potential thread isolation bypass in asset URL minting that requires human review.

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

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

Labels

size:XL500-999 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.

1 participant

@sambhavthakkar
, '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(chat): show Codex generated images in the thread - #6441

Open
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat
Open

fix(chat): show Codex generated images in the thread#6441
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat

Conversation

@sambhavthakkar

@sambhavthakkarsambhavthakkar commented Aug 13, 2026

Copy link
Copy Markdown

What Changed

Codex imagegen already writes a file and emits savedPath. T3 dropped that path, labeled the row Image view, and only served workspace files, so generated images never showed in chat.

  • Keep savedPath on the Codex item and label it Generated image
  • Serve $CODEX_HOME/generated_images through signed asset URLs (not the base64 result over the websocket)
  • Render the file inline in web and mobile chat (markdown ![]() and the work row)

Why

The file is already on disk and the protocol already names it. Chat should show the picture, including remote clients.

UI Changes

  • Before: work row is a generic Image view / file path; markdown ![]() does not load
  • After: the generated file renders inline in the thread and can be expanded

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes

Note

Medium Risk
Adds host-path asset access outside the workspace with root allowlisting—security-sensitive but scoped to Codex generated_images and exact basename tokens; broad touch across server assets, ingestion, and both clients.

Overview
Codex-generated images now render inline in web and mobile chat (markdown images and work-log rows) instead of showing only a path or a generic “Image view” label.

On the server, Codex imageGeneration / imageView items keep full savedPath (no truncation for image_view), map to Generated image where appropriate, and workspace asset URLs can fall back to host-image-exact tokens for files under configured Codex generated_images roots when the path is outside the project workspace. WebSocket asset issuance passes those roots from server settings.

Clients add custom markdown image rendering (renderImage on native markdown; ChatWorkspaceImage / MarkdownWorkspaceImage) with signed workspace URLs, loading/error UI, and tap-to-preview. Work-log activities expose imagePath from lifecycle payloads for inline previews. Mobile asset hooks gain useAssetUrlState for non-binary loading/failure handling; list sizing skips fixed height when a row has an image preview.

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

Note

Show Codex-generated images inline in chat threads on web and mobile

  • Adds end-to-end support for rendering image_view work log entries as inline image previews in both the web MessagesTimeline and mobile ThreadFeed, with click-to-expand dialogs and loading/failure states.
  • Introduces a new host-image-exact asset claim in AssetAccess.ts so the server can securely serve generated images that live outside the workspace root, validated against configured generatedImagesRoots.
  • Extends CodexAdapter.ts to populate detail on imageGeneration/imageView items with the saved file path, and preserves full (untruncated) paths through the ingestion pipeline for image_view events.
  • Adds a renderImage prop to NativeMarkdownBlock and SelectableMarkdownText so custom image renderers can be injected into the markdown tree on mobile.
  • Risk: image_view detail strings are no longer truncated anywhere in the pipeline; very long paths are passed through as-is.

Macroscope summarized a1816fc.

@coderabbitai

coderabbitaiBot commented Aug 13, 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: 140d9003-d0d1-4b7e-a057-eccf5aa815a3

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

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@sambhavthakkar
sambhavthakkarforce-pushed the fix/codex-generated-images-in-chat branch from 0f61eab to 3052999CompareAugust 13, 2026 07:15

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts
Comment threadapps/server/src/ws.ts
function* (settings: ServerSettingsValue) {
const path = yield* Path.Path;
const roots: string[] = [];
const instances = Object.values(settings.providerInstances)

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.

🔴 Criticalsrc/ws.ts:142

A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. resolveCodexGeneratedImagesRoots returns generated_images roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's modelSelection.instanceId (and its session provider instance) instead of returning all configured roots.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 142:
A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. `resolveCodexGeneratedImagesRoots` returns `generated_images` roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's `modelSelection.instanceId` (and its session provider instance) instead of returning all configured roots.

...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.detail
? { detail: toolLifecycleDetail(event.payload.itemType, event.payload.detail) }

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.

🟡 MediumLayers/ProviderRuntimeIngestion.ts:805

image_view activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. toolLifecycleDetail disables truncation for every image_view, even though OpenCode maps any tool name containing “image” to that type and detailFromToolPart supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 805:
`image_view` activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. `toolLifecycleDetail` disables truncation for every `image_view`, even though OpenCode maps any tool name containing “image” to that type and `detailFromToolPart` supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

if (trimmed.includes("\n") || /\s/.test(trimmed)) {
return undefined;
}
if (trimmed.includes("/") || trimmed.includes("\\")) {

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.

🟡 MediumLayers/CodexAdapter.ts:283

imageGenerationResultPath returns short base64 image data containing / as detail, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to revisedPrompt. Validate that result is an actual path rather than relying on / alone.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 283:
`imageGenerationResultPath` returns short base64 image data containing `/` as `detail`, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to `revisedPrompt`. Validate that `result` is an actual path rather than relying on `/` alone.

Comment on lines +291 to +295
return (
trimText(item.savedPath ?? undefined) ??
imageGenerationResultPath(item.result) ??
trimText(item.revisedPrompt ?? undefined)
);

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.

🟡 MediumLayers/CodexAdapter.ts:291

Pathless imageGeneration items return revisedPrompt as detail, so extractImageViewPath sends prompt text such as a ceramic mug to the workspace-asset loader as imagePath, producing a broken image preview. Only actual image paths should populate this detail channel; remove the revisedPrompt fallback.

Suggested change
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)??
trimText(item.revisedPrompt??undefined)
);
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)
);
Also found in 1 other location(s)

apps/mobile/src/lib/threadActivity.ts:966

extractImageViewPath treats every non-empty payload.detail as an image path. For an imageGeneration item without savedPath (the protocol makes it optional), CodexAdapter.itemDetail can populate detail with revisedPrompt; this then becomes imagePath, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept detail when it is known to be a path (for example, an actual imageView item), not for pathless image-generation results.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 291-295:
Pathless `imageGeneration` items return `revisedPrompt` as `detail`, so `extractImageViewPath` sends prompt text such as `a ceramic mug` to the workspace-asset loader as `imagePath`, producing a broken image preview. Only actual image paths should populate this detail channel; remove the `revisedPrompt` fallback.
Also found in 1 other location(s):
- apps/mobile/src/lib/threadActivity.ts:966 -- `extractImageViewPath` treats every non-empty `payload.detail` as an image path. For an `imageGeneration` item without `savedPath` (the protocol makes it optional), `CodexAdapter.itemDetail` can populate `detail` with `revisedPrompt`; this then becomes `imagePath`, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept `detail` when it is known to be a path (for example, an actual `imageView` item), not for pathless image-generation results.

/>
);
}
if (src && /^https?:\/\//i.test(src)) {

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.

🟡 Mediumcomponents/ChatMarkdown.tsx:1564

Protocol-relative images such as ![logo](//cdn.example.com/logo.png) fail to load and, when a thread exists, are sent to ChatWorkspaceImage as a workspace path. The img renderer only identifies http:// and https:// as remote, so resolveMarkdownFileLinkMeta classifies //cdn.example.com/logo.png as a file link; treat // URLs as remote as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1564:
Protocol-relative images such as `![logo](//cdn.example.com/logo.png)` fail to load and, when a thread exists, are sent to `ChatWorkspaceImage` as a workspace path. The `img` renderer only identifies `http://` and `https://` as remote, so `resolveMarkdownFileLinkMeta` classifies `//cdn.example.com/logo.png` as a file link; treat `//` URLs as remote as well.

renderFeedEntry(info, {
environmentId: props.environmentId,
threadId: props.threadId,
workspaceRoot: props.workspaceRoot,

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.

🟠 Highthreads/ThreadFeed.tsx:1969

Visible image rows keep using the previous workspaceRoot when threadCwd changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. workspaceRoot is captured by renderItem but is absent from listAppearanceData/extraData, and LegendList does not repaint visible rows when only that closure changes; include it in extraData (or otherwise invalidate/remount the rows).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1969:
Visible image rows keep using the previous `workspaceRoot` when `threadCwd` changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. `workspaceRoot` is captured by `renderItem` but is absent from `listAppearanceData`/`extraData`, and LegendList does not repaint visible rows when only that closure changes; include it in `extraData` (or otherwise invalidate/remount the rows).

Comment on lines +15 to +42
const [failed, setFailed] = useState(false);
const [expanded, setExpanded] = useState(false);
const name = props.alt.trim().length > 0 ? props.alt : "Image";

if (failed) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
</div>
);
}

return (
<>
<button
type="button"
className={cn(
"my-2 block max-w-full cursor-zoom-in overflow-hidden rounded-lg border border-border/80 bg-background/70",
props.className,
)}
aria-label={`Preview ${name}`}
onClick={() => setExpanded(true)}
>
<img
src={props.src}
alt={name}
className="block h-auto max-h-[360px] max-w-full object-contain"
onError={() => setFailed(true)}

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.

🟡 Mediumchat/ChatWorkspaceImage.tsx:15

After ChatRemoteImage's image load fails, changing props.src still renders the old error state, so corrected remote URLs remain permanently hidden. Because failed is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when props.src changes).

- const [failed, setFailed] = useState(false);+ const [failed, setFailed] = useState<string | null>(null);- if (failed) {+ if (failed === props.src) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
@@
- onError={() => setFailed(true)}+ onError={() => setFailed(props.src)}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatWorkspaceImage.tsx around lines 15-42:
After `ChatRemoteImage`'s image load fails, changing `props.src` still renders the old error state, so corrected remote URLs remain permanently hidden. Because `failed` is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when `props.src` changes).

</div>
</div>
{imagePath && threadRef ? (
<div className="ms-7 mt-1" onClick={stopRowToggle} onPointerDown={stopRowToggle}>

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.

🟡 Mediumchat/MessagesTimeline.tsx:2398

When ChatWorkspaceImage is focused, pressing Enter or Space bubbles to the row's onKeyDown, which calls preventDefault() and toggles expanded instead of activating the image button. Add an onKeyDown handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 2398:
When `ChatWorkspaceImage` is focused, pressing Enter or Space bubbles to the row's `onKeyDown`, which calls `preventDefault()` and toggles `expanded` instead of activating the image button. Add an `onKeyDown` handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

@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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3052999. Configure here.

Comment threadapps/web/src/session-logic.ts
@macroscopeapp

macroscopeappBot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This PR introduces new capability for displaying Codex-generated images with cross-cutting changes to mobile, web, and server-side authorization logic. A Critical finding identifies a potential thread isolation bypass in asset URL minting that requires human review.

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

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

Labels

size:XL500-999 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.

1 participant

@sambhavthakkar
, '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(chat): show Codex generated images in the thread - #6441

Open
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat
Open

fix(chat): show Codex generated images in the thread#6441
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat

Conversation

@sambhavthakkar

@sambhavthakkarsambhavthakkar commented Aug 13, 2026

Copy link
Copy Markdown

What Changed

Codex imagegen already writes a file and emits savedPath. T3 dropped that path, labeled the row Image view, and only served workspace files, so generated images never showed in chat.

  • Keep savedPath on the Codex item and label it Generated image
  • Serve $CODEX_HOME/generated_images through signed asset URLs (not the base64 result over the websocket)
  • Render the file inline in web and mobile chat (markdown ![]() and the work row)

Why

The file is already on disk and the protocol already names it. Chat should show the picture, including remote clients.

UI Changes

  • Before: work row is a generic Image view / file path; markdown ![]() does not load
  • After: the generated file renders inline in the thread and can be expanded

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes

Note

Medium Risk
Adds host-path asset access outside the workspace with root allowlisting—security-sensitive but scoped to Codex generated_images and exact basename tokens; broad touch across server assets, ingestion, and both clients.

Overview
Codex-generated images now render inline in web and mobile chat (markdown images and work-log rows) instead of showing only a path or a generic “Image view” label.

On the server, Codex imageGeneration / imageView items keep full savedPath (no truncation for image_view), map to Generated image where appropriate, and workspace asset URLs can fall back to host-image-exact tokens for files under configured Codex generated_images roots when the path is outside the project workspace. WebSocket asset issuance passes those roots from server settings.

Clients add custom markdown image rendering (renderImage on native markdown; ChatWorkspaceImage / MarkdownWorkspaceImage) with signed workspace URLs, loading/error UI, and tap-to-preview. Work-log activities expose imagePath from lifecycle payloads for inline previews. Mobile asset hooks gain useAssetUrlState for non-binary loading/failure handling; list sizing skips fixed height when a row has an image preview.

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

Note

Show Codex-generated images inline in chat threads on web and mobile

  • Adds end-to-end support for rendering image_view work log entries as inline image previews in both the web MessagesTimeline and mobile ThreadFeed, with click-to-expand dialogs and loading/failure states.
  • Introduces a new host-image-exact asset claim in AssetAccess.ts so the server can securely serve generated images that live outside the workspace root, validated against configured generatedImagesRoots.
  • Extends CodexAdapter.ts to populate detail on imageGeneration/imageView items with the saved file path, and preserves full (untruncated) paths through the ingestion pipeline for image_view events.
  • Adds a renderImage prop to NativeMarkdownBlock and SelectableMarkdownText so custom image renderers can be injected into the markdown tree on mobile.
  • Risk: image_view detail strings are no longer truncated anywhere in the pipeline; very long paths are passed through as-is.

Macroscope summarized a1816fc.

@coderabbitai

coderabbitaiBot commented Aug 13, 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: 140d9003-d0d1-4b7e-a057-eccf5aa815a3

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

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@sambhavthakkar
sambhavthakkarforce-pushed the fix/codex-generated-images-in-chat branch from 0f61eab to 3052999CompareAugust 13, 2026 07:15

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts
Comment threadapps/server/src/ws.ts
function* (settings: ServerSettingsValue) {
const path = yield* Path.Path;
const roots: string[] = [];
const instances = Object.values(settings.providerInstances)

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.

🔴 Criticalsrc/ws.ts:142

A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. resolveCodexGeneratedImagesRoots returns generated_images roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's modelSelection.instanceId (and its session provider instance) instead of returning all configured roots.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 142:
A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. `resolveCodexGeneratedImagesRoots` returns `generated_images` roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's `modelSelection.instanceId` (and its session provider instance) instead of returning all configured roots.

...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.detail
? { detail: toolLifecycleDetail(event.payload.itemType, event.payload.detail) }

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.

🟡 MediumLayers/ProviderRuntimeIngestion.ts:805

image_view activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. toolLifecycleDetail disables truncation for every image_view, even though OpenCode maps any tool name containing “image” to that type and detailFromToolPart supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 805:
`image_view` activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. `toolLifecycleDetail` disables truncation for every `image_view`, even though OpenCode maps any tool name containing “image” to that type and `detailFromToolPart` supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

if (trimmed.includes("\n") || /\s/.test(trimmed)) {
return undefined;
}
if (trimmed.includes("/") || trimmed.includes("\\")) {

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.

🟡 MediumLayers/CodexAdapter.ts:283

imageGenerationResultPath returns short base64 image data containing / as detail, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to revisedPrompt. Validate that result is an actual path rather than relying on / alone.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 283:
`imageGenerationResultPath` returns short base64 image data containing `/` as `detail`, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to `revisedPrompt`. Validate that `result` is an actual path rather than relying on `/` alone.

Comment on lines +291 to +295
return (
trimText(item.savedPath ?? undefined) ??
imageGenerationResultPath(item.result) ??
trimText(item.revisedPrompt ?? undefined)
);

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.

🟡 MediumLayers/CodexAdapter.ts:291

Pathless imageGeneration items return revisedPrompt as detail, so extractImageViewPath sends prompt text such as a ceramic mug to the workspace-asset loader as imagePath, producing a broken image preview. Only actual image paths should populate this detail channel; remove the revisedPrompt fallback.

Suggested change
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)??
trimText(item.revisedPrompt??undefined)
);
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)
);
Also found in 1 other location(s)

apps/mobile/src/lib/threadActivity.ts:966

extractImageViewPath treats every non-empty payload.detail as an image path. For an imageGeneration item without savedPath (the protocol makes it optional), CodexAdapter.itemDetail can populate detail with revisedPrompt; this then becomes imagePath, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept detail when it is known to be a path (for example, an actual imageView item), not for pathless image-generation results.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 291-295:
Pathless `imageGeneration` items return `revisedPrompt` as `detail`, so `extractImageViewPath` sends prompt text such as `a ceramic mug` to the workspace-asset loader as `imagePath`, producing a broken image preview. Only actual image paths should populate this detail channel; remove the `revisedPrompt` fallback.
Also found in 1 other location(s):
- apps/mobile/src/lib/threadActivity.ts:966 -- `extractImageViewPath` treats every non-empty `payload.detail` as an image path. For an `imageGeneration` item without `savedPath` (the protocol makes it optional), `CodexAdapter.itemDetail` can populate `detail` with `revisedPrompt`; this then becomes `imagePath`, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept `detail` when it is known to be a path (for example, an actual `imageView` item), not for pathless image-generation results.

/>
);
}
if (src && /^https?:\/\//i.test(src)) {

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.

🟡 Mediumcomponents/ChatMarkdown.tsx:1564

Protocol-relative images such as ![logo](//cdn.example.com/logo.png) fail to load and, when a thread exists, are sent to ChatWorkspaceImage as a workspace path. The img renderer only identifies http:// and https:// as remote, so resolveMarkdownFileLinkMeta classifies //cdn.example.com/logo.png as a file link; treat // URLs as remote as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1564:
Protocol-relative images such as `![logo](//cdn.example.com/logo.png)` fail to load and, when a thread exists, are sent to `ChatWorkspaceImage` as a workspace path. The `img` renderer only identifies `http://` and `https://` as remote, so `resolveMarkdownFileLinkMeta` classifies `//cdn.example.com/logo.png` as a file link; treat `//` URLs as remote as well.

renderFeedEntry(info, {
environmentId: props.environmentId,
threadId: props.threadId,
workspaceRoot: props.workspaceRoot,

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.

🟠 Highthreads/ThreadFeed.tsx:1969

Visible image rows keep using the previous workspaceRoot when threadCwd changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. workspaceRoot is captured by renderItem but is absent from listAppearanceData/extraData, and LegendList does not repaint visible rows when only that closure changes; include it in extraData (or otherwise invalidate/remount the rows).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1969:
Visible image rows keep using the previous `workspaceRoot` when `threadCwd` changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. `workspaceRoot` is captured by `renderItem` but is absent from `listAppearanceData`/`extraData`, and LegendList does not repaint visible rows when only that closure changes; include it in `extraData` (or otherwise invalidate/remount the rows).

Comment on lines +15 to +42
const [failed, setFailed] = useState(false);
const [expanded, setExpanded] = useState(false);
const name = props.alt.trim().length > 0 ? props.alt : "Image";

if (failed) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
</div>
);
}

return (
<>
<button
type="button"
className={cn(
"my-2 block max-w-full cursor-zoom-in overflow-hidden rounded-lg border border-border/80 bg-background/70",
props.className,
)}
aria-label={`Preview ${name}`}
onClick={() => setExpanded(true)}
>
<img
src={props.src}
alt={name}
className="block h-auto max-h-[360px] max-w-full object-contain"
onError={() => setFailed(true)}

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.

🟡 Mediumchat/ChatWorkspaceImage.tsx:15

After ChatRemoteImage's image load fails, changing props.src still renders the old error state, so corrected remote URLs remain permanently hidden. Because failed is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when props.src changes).

- const [failed, setFailed] = useState(false);+ const [failed, setFailed] = useState<string | null>(null);- if (failed) {+ if (failed === props.src) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
@@
- onError={() => setFailed(true)}+ onError={() => setFailed(props.src)}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatWorkspaceImage.tsx around lines 15-42:
After `ChatRemoteImage`'s image load fails, changing `props.src` still renders the old error state, so corrected remote URLs remain permanently hidden. Because `failed` is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when `props.src` changes).

</div>
</div>
{imagePath && threadRef ? (
<div className="ms-7 mt-1" onClick={stopRowToggle} onPointerDown={stopRowToggle}>

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.

🟡 Mediumchat/MessagesTimeline.tsx:2398

When ChatWorkspaceImage is focused, pressing Enter or Space bubbles to the row's onKeyDown, which calls preventDefault() and toggles expanded instead of activating the image button. Add an onKeyDown handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 2398:
When `ChatWorkspaceImage` is focused, pressing Enter or Space bubbles to the row's `onKeyDown`, which calls `preventDefault()` and toggles `expanded` instead of activating the image button. Add an `onKeyDown` handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

@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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3052999. Configure here.

Comment threadapps/web/src/session-logic.ts
@macroscopeapp

macroscopeappBot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This PR introduces new capability for displaying Codex-generated images with cross-cutting changes to mobile, web, and server-side authorization logic. A Critical finding identifies a potential thread isolation bypass in asset URL minting that requires human review.

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

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

Labels

size:XL500-999 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.

1 participant

@sambhavthakkar
, '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(chat): show Codex generated images in the thread - #6441

Open
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat
Open

fix(chat): show Codex generated images in the thread#6441
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat

Conversation

@sambhavthakkar

@sambhavthakkarsambhavthakkar commented Aug 13, 2026

Copy link
Copy Markdown

What Changed

Codex imagegen already writes a file and emits savedPath. T3 dropped that path, labeled the row Image view, and only served workspace files, so generated images never showed in chat.

  • Keep savedPath on the Codex item and label it Generated image
  • Serve $CODEX_HOME/generated_images through signed asset URLs (not the base64 result over the websocket)
  • Render the file inline in web and mobile chat (markdown ![]() and the work row)

Why

The file is already on disk and the protocol already names it. Chat should show the picture, including remote clients.

UI Changes

  • Before: work row is a generic Image view / file path; markdown ![]() does not load
  • After: the generated file renders inline in the thread and can be expanded

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes

Note

Medium Risk
Adds host-path asset access outside the workspace with root allowlisting—security-sensitive but scoped to Codex generated_images and exact basename tokens; broad touch across server assets, ingestion, and both clients.

Overview
Codex-generated images now render inline in web and mobile chat (markdown images and work-log rows) instead of showing only a path or a generic “Image view” label.

On the server, Codex imageGeneration / imageView items keep full savedPath (no truncation for image_view), map to Generated image where appropriate, and workspace asset URLs can fall back to host-image-exact tokens for files under configured Codex generated_images roots when the path is outside the project workspace. WebSocket asset issuance passes those roots from server settings.

Clients add custom markdown image rendering (renderImage on native markdown; ChatWorkspaceImage / MarkdownWorkspaceImage) with signed workspace URLs, loading/error UI, and tap-to-preview. Work-log activities expose imagePath from lifecycle payloads for inline previews. Mobile asset hooks gain useAssetUrlState for non-binary loading/failure handling; list sizing skips fixed height when a row has an image preview.

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

Note

Show Codex-generated images inline in chat threads on web and mobile

  • Adds end-to-end support for rendering image_view work log entries as inline image previews in both the web MessagesTimeline and mobile ThreadFeed, with click-to-expand dialogs and loading/failure states.
  • Introduces a new host-image-exact asset claim in AssetAccess.ts so the server can securely serve generated images that live outside the workspace root, validated against configured generatedImagesRoots.
  • Extends CodexAdapter.ts to populate detail on imageGeneration/imageView items with the saved file path, and preserves full (untruncated) paths through the ingestion pipeline for image_view events.
  • Adds a renderImage prop to NativeMarkdownBlock and SelectableMarkdownText so custom image renderers can be injected into the markdown tree on mobile.
  • Risk: image_view detail strings are no longer truncated anywhere in the pipeline; very long paths are passed through as-is.

Macroscope summarized a1816fc.

@coderabbitai

coderabbitaiBot commented Aug 13, 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: 140d9003-d0d1-4b7e-a057-eccf5aa815a3

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

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@sambhavthakkar
sambhavthakkarforce-pushed the fix/codex-generated-images-in-chat branch from 0f61eab to 3052999CompareAugust 13, 2026 07:15

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts
Comment threadapps/server/src/ws.ts
function* (settings: ServerSettingsValue) {
const path = yield* Path.Path;
const roots: string[] = [];
const instances = Object.values(settings.providerInstances)

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.

🔴 Criticalsrc/ws.ts:142

A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. resolveCodexGeneratedImagesRoots returns generated_images roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's modelSelection.instanceId (and its session provider instance) instead of returning all configured roots.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 142:
A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. `resolveCodexGeneratedImagesRoots` returns `generated_images` roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's `modelSelection.instanceId` (and its session provider instance) instead of returning all configured roots.

...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.detail
? { detail: toolLifecycleDetail(event.payload.itemType, event.payload.detail) }

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.

🟡 MediumLayers/ProviderRuntimeIngestion.ts:805

image_view activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. toolLifecycleDetail disables truncation for every image_view, even though OpenCode maps any tool name containing “image” to that type and detailFromToolPart supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 805:
`image_view` activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. `toolLifecycleDetail` disables truncation for every `image_view`, even though OpenCode maps any tool name containing “image” to that type and `detailFromToolPart` supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

if (trimmed.includes("\n") || /\s/.test(trimmed)) {
return undefined;
}
if (trimmed.includes("/") || trimmed.includes("\\")) {

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.

🟡 MediumLayers/CodexAdapter.ts:283

imageGenerationResultPath returns short base64 image data containing / as detail, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to revisedPrompt. Validate that result is an actual path rather than relying on / alone.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 283:
`imageGenerationResultPath` returns short base64 image data containing `/` as `detail`, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to `revisedPrompt`. Validate that `result` is an actual path rather than relying on `/` alone.

Comment on lines +291 to +295
return (
trimText(item.savedPath ?? undefined) ??
imageGenerationResultPath(item.result) ??
trimText(item.revisedPrompt ?? undefined)
);

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.

🟡 MediumLayers/CodexAdapter.ts:291

Pathless imageGeneration items return revisedPrompt as detail, so extractImageViewPath sends prompt text such as a ceramic mug to the workspace-asset loader as imagePath, producing a broken image preview. Only actual image paths should populate this detail channel; remove the revisedPrompt fallback.

Suggested change
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)??
trimText(item.revisedPrompt??undefined)
);
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)
);
Also found in 1 other location(s)

apps/mobile/src/lib/threadActivity.ts:966

extractImageViewPath treats every non-empty payload.detail as an image path. For an imageGeneration item without savedPath (the protocol makes it optional), CodexAdapter.itemDetail can populate detail with revisedPrompt; this then becomes imagePath, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept detail when it is known to be a path (for example, an actual imageView item), not for pathless image-generation results.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 291-295:
Pathless `imageGeneration` items return `revisedPrompt` as `detail`, so `extractImageViewPath` sends prompt text such as `a ceramic mug` to the workspace-asset loader as `imagePath`, producing a broken image preview. Only actual image paths should populate this detail channel; remove the `revisedPrompt` fallback.
Also found in 1 other location(s):
- apps/mobile/src/lib/threadActivity.ts:966 -- `extractImageViewPath` treats every non-empty `payload.detail` as an image path. For an `imageGeneration` item without `savedPath` (the protocol makes it optional), `CodexAdapter.itemDetail` can populate `detail` with `revisedPrompt`; this then becomes `imagePath`, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept `detail` when it is known to be a path (for example, an actual `imageView` item), not for pathless image-generation results.

/>
);
}
if (src && /^https?:\/\//i.test(src)) {

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.

🟡 Mediumcomponents/ChatMarkdown.tsx:1564

Protocol-relative images such as ![logo](//cdn.example.com/logo.png) fail to load and, when a thread exists, are sent to ChatWorkspaceImage as a workspace path. The img renderer only identifies http:// and https:// as remote, so resolveMarkdownFileLinkMeta classifies //cdn.example.com/logo.png as a file link; treat // URLs as remote as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1564:
Protocol-relative images such as `![logo](//cdn.example.com/logo.png)` fail to load and, when a thread exists, are sent to `ChatWorkspaceImage` as a workspace path. The `img` renderer only identifies `http://` and `https://` as remote, so `resolveMarkdownFileLinkMeta` classifies `//cdn.example.com/logo.png` as a file link; treat `//` URLs as remote as well.

renderFeedEntry(info, {
environmentId: props.environmentId,
threadId: props.threadId,
workspaceRoot: props.workspaceRoot,

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.

🟠 Highthreads/ThreadFeed.tsx:1969

Visible image rows keep using the previous workspaceRoot when threadCwd changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. workspaceRoot is captured by renderItem but is absent from listAppearanceData/extraData, and LegendList does not repaint visible rows when only that closure changes; include it in extraData (or otherwise invalidate/remount the rows).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1969:
Visible image rows keep using the previous `workspaceRoot` when `threadCwd` changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. `workspaceRoot` is captured by `renderItem` but is absent from `listAppearanceData`/`extraData`, and LegendList does not repaint visible rows when only that closure changes; include it in `extraData` (or otherwise invalidate/remount the rows).

Comment on lines +15 to +42
const [failed, setFailed] = useState(false);
const [expanded, setExpanded] = useState(false);
const name = props.alt.trim().length > 0 ? props.alt : "Image";

if (failed) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
</div>
);
}

return (
<>
<button
type="button"
className={cn(
"my-2 block max-w-full cursor-zoom-in overflow-hidden rounded-lg border border-border/80 bg-background/70",
props.className,
)}
aria-label={`Preview ${name}`}
onClick={() => setExpanded(true)}
>
<img
src={props.src}
alt={name}
className="block h-auto max-h-[360px] max-w-full object-contain"
onError={() => setFailed(true)}

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.

🟡 Mediumchat/ChatWorkspaceImage.tsx:15

After ChatRemoteImage's image load fails, changing props.src still renders the old error state, so corrected remote URLs remain permanently hidden. Because failed is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when props.src changes).

- const [failed, setFailed] = useState(false);+ const [failed, setFailed] = useState<string | null>(null);- if (failed) {+ if (failed === props.src) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
@@
- onError={() => setFailed(true)}+ onError={() => setFailed(props.src)}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatWorkspaceImage.tsx around lines 15-42:
After `ChatRemoteImage`'s image load fails, changing `props.src` still renders the old error state, so corrected remote URLs remain permanently hidden. Because `failed` is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when `props.src` changes).

</div>
</div>
{imagePath && threadRef ? (
<div className="ms-7 mt-1" onClick={stopRowToggle} onPointerDown={stopRowToggle}>

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.

🟡 Mediumchat/MessagesTimeline.tsx:2398

When ChatWorkspaceImage is focused, pressing Enter or Space bubbles to the row's onKeyDown, which calls preventDefault() and toggles expanded instead of activating the image button. Add an onKeyDown handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 2398:
When `ChatWorkspaceImage` is focused, pressing Enter or Space bubbles to the row's `onKeyDown`, which calls `preventDefault()` and toggles `expanded` instead of activating the image button. Add an `onKeyDown` handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

@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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3052999. Configure here.

Comment threadapps/web/src/session-logic.ts
@macroscopeapp

macroscopeappBot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This PR introduces new capability for displaying Codex-generated images with cross-cutting changes to mobile, web, and server-side authorization logic. A Critical finding identifies a potential thread isolation bypass in asset URL minting that requires human review.

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

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

Labels

size:XL500-999 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.

1 participant

@sambhavthakkar
, '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(chat): show Codex generated images in the thread - #6441

Open
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat
Open

fix(chat): show Codex generated images in the thread#6441
sambhavthakkar wants to merge 4 commits into
pingdotgg:mainfrom
sambhavthakkar:fix/codex-generated-images-in-chat

Conversation

@sambhavthakkar

@sambhavthakkarsambhavthakkar commented Aug 13, 2026

Copy link
Copy Markdown

What Changed

Codex imagegen already writes a file and emits savedPath. T3 dropped that path, labeled the row Image view, and only served workspace files, so generated images never showed in chat.

  • Keep savedPath on the Codex item and label it Generated image
  • Serve $CODEX_HOME/generated_images through signed asset URLs (not the base64 result over the websocket)
  • Render the file inline in web and mobile chat (markdown ![]() and the work row)

Why

The file is already on disk and the protocol already names it. Chat should show the picture, including remote clients.

UI Changes

  • Before: work row is a generic Image view / file path; markdown ![]() does not load
  • After: the generated file renders inline in the thread and can be expanded

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes

Note

Medium Risk
Adds host-path asset access outside the workspace with root allowlisting—security-sensitive but scoped to Codex generated_images and exact basename tokens; broad touch across server assets, ingestion, and both clients.

Overview
Codex-generated images now render inline in web and mobile chat (markdown images and work-log rows) instead of showing only a path or a generic “Image view” label.

On the server, Codex imageGeneration / imageView items keep full savedPath (no truncation for image_view), map to Generated image where appropriate, and workspace asset URLs can fall back to host-image-exact tokens for files under configured Codex generated_images roots when the path is outside the project workspace. WebSocket asset issuance passes those roots from server settings.

Clients add custom markdown image rendering (renderImage on native markdown; ChatWorkspaceImage / MarkdownWorkspaceImage) with signed workspace URLs, loading/error UI, and tap-to-preview. Work-log activities expose imagePath from lifecycle payloads for inline previews. Mobile asset hooks gain useAssetUrlState for non-binary loading/failure handling; list sizing skips fixed height when a row has an image preview.

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

Note

Show Codex-generated images inline in chat threads on web and mobile

  • Adds end-to-end support for rendering image_view work log entries as inline image previews in both the web MessagesTimeline and mobile ThreadFeed, with click-to-expand dialogs and loading/failure states.
  • Introduces a new host-image-exact asset claim in AssetAccess.ts so the server can securely serve generated images that live outside the workspace root, validated against configured generatedImagesRoots.
  • Extends CodexAdapter.ts to populate detail on imageGeneration/imageView items with the saved file path, and preserves full (untruncated) paths through the ingestion pipeline for image_view events.
  • Adds a renderImage prop to NativeMarkdownBlock and SelectableMarkdownText so custom image renderers can be injected into the markdown tree on mobile.
  • Risk: image_view detail strings are no longer truncated anywhere in the pipeline; very long paths are passed through as-is.

Macroscope summarized a1816fc.

@coderabbitai

coderabbitaiBot commented Aug 13, 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: 140d9003-d0d1-4b7e-a057-eccf5aa815a3

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

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@sambhavthakkar
sambhavthakkarforce-pushed the fix/codex-generated-images-in-chat branch from 0f61eab to 3052999CompareAugust 13, 2026 07:15

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts

@macroscopeappmacroscopeappBot 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.

One finding on Effect error modeling in apps/server/src/assets/AssetAccess.ts. Everything else in the changed Effect code (namespace imports from effect/*, Effect.catchTags for the single WorkspacePathOutsideRootError tag, Path.Path acquired from the environment in resolveCodexGeneratedImagesRoots, no new Layer.succeed/ManagedRuntime/runPromise in service code) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment threadapps/server/src/assets/AssetAccess.ts
Comment threadapps/server/src/ws.ts
function* (settings: ServerSettingsValue) {
const path = yield* Path.Path;
const roots: string[] = [];
const instances = Object.values(settings.providerInstances)

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.

🔴 Criticalsrc/ws.ts:142

A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. resolveCodexGeneratedImagesRoots returns generated_images roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's modelSelection.instanceId (and its session provider instance) instead of returning all configured roots.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/ws.ts around line 142:
A client authorized for one thread can mint a signed URL for a known image from another configured Codex instance or thread. `resolveCodexGeneratedImagesRoots` returns `generated_images` roots for every Codex instance, while asset issuance validates only the requested thread, so an absolute path under any returned root bypasses thread/workspace isolation. Restrict resolution to the thread's `modelSelection.instanceId` (and its session provider instance) instead of returning all configured roots.

...(event.payload.status ? { status: event.payload.status } : {}),
...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),
...(event.payload.detail
? { detail: toolLifecycleDetail(event.payload.itemType, event.payload.detail) }

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.

🟡 MediumLayers/ProviderRuntimeIngestion.ts:805

image_view activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. toolLifecycleDetail disables truncation for every image_view, even though OpenCode maps any tool name containing “image” to that type and detailFromToolPart supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 805:
`image_view` activity details are persisted and sent to clients without the 180-character cap, so OpenCode image-tool outputs—including large or base64 text—can create oversized activity records and websocket payloads. `toolLifecycleDetail` disables truncation for every `image_view`, even though OpenCode maps any tool name containing “image” to that type and `detailFromToolPart` supplies the full output or error text; preserve untruncated details only after verifying they are file paths (or only for the Codex generated-image case).

if (trimmed.includes("\n") || /\s/.test(trimmed)) {
return undefined;
}
if (trimmed.includes("/") || trimmed.includes("\\")) {

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.

🟡 MediumLayers/CodexAdapter.ts:283

imageGenerationResultPath returns short base64 image data containing / as detail, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to revisedPrompt. Validate that result is an actual path rather than relying on / alone.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around line 283:
`imageGenerationResultPath` returns short base64 image data containing `/` as `detail`, so image rows treat the payload as a filesystem path and render a failed preview instead of falling back to `revisedPrompt`. Validate that `result` is an actual path rather than relying on `/` alone.

Comment on lines +291 to +295
return (
trimText(item.savedPath ?? undefined) ??
imageGenerationResultPath(item.result) ??
trimText(item.revisedPrompt ?? undefined)
);

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.

🟡 MediumLayers/CodexAdapter.ts:291

Pathless imageGeneration items return revisedPrompt as detail, so extractImageViewPath sends prompt text such as a ceramic mug to the workspace-asset loader as imagePath, producing a broken image preview. Only actual image paths should populate this detail channel; remove the revisedPrompt fallback.

Suggested change
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)??
trimText(item.revisedPrompt??undefined)
);
return(
trimText(item.savedPath??undefined)??
imageGenerationResultPath(item.result)
);
Also found in 1 other location(s)

apps/mobile/src/lib/threadActivity.ts:966

extractImageViewPath treats every non-empty payload.detail as an image path. For an imageGeneration item without savedPath (the protocol makes it optional), CodexAdapter.itemDetail can populate detail with revisedPrompt; this then becomes imagePath, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept detail when it is known to be a path (for example, an actual imageView item), not for pathless image-generation results.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CodexAdapter.ts around lines 291-295:
Pathless `imageGeneration` items return `revisedPrompt` as `detail`, so `extractImageViewPath` sends prompt text such as `a ceramic mug` to the workspace-asset loader as `imagePath`, producing a broken image preview. Only actual image paths should populate this detail channel; remove the `revisedPrompt` fallback.
Also found in 1 other location(s):
- apps/mobile/src/lib/threadActivity.ts:966 -- `extractImageViewPath` treats every non-empty `payload.detail` as an image path. For an `imageGeneration` item without `savedPath` (the protocol makes it optional), `CodexAdapter.itemDetail` can populate `detail` with `revisedPrompt`; this then becomes `imagePath`, so mobile attempts to request and render the prompt text as a workspace asset and shows a broken image preview. The fallback should only accept `detail` when it is known to be a path (for example, an actual `imageView` item), not for pathless image-generation results.

/>
);
}
if (src && /^https?:\/\//i.test(src)) {

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.

🟡 Mediumcomponents/ChatMarkdown.tsx:1564

Protocol-relative images such as ![logo](//cdn.example.com/logo.png) fail to load and, when a thread exists, are sent to ChatWorkspaceImage as a workspace path. The img renderer only identifies http:// and https:// as remote, so resolveMarkdownFileLinkMeta classifies //cdn.example.com/logo.png as a file link; treat // URLs as remote as well.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ChatMarkdown.tsx around line 1564:
Protocol-relative images such as `![logo](//cdn.example.com/logo.png)` fail to load and, when a thread exists, are sent to `ChatWorkspaceImage` as a workspace path. The `img` renderer only identifies `http://` and `https://` as remote, so `resolveMarkdownFileLinkMeta` classifies `//cdn.example.com/logo.png` as a file link; treat `//` URLs as remote as well.

renderFeedEntry(info, {
environmentId: props.environmentId,
threadId: props.threadId,
workspaceRoot: props.workspaceRoot,

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.

🟠 Highthreads/ThreadFeed.tsx:1969

Visible image rows keep using the previous workspaceRoot when threadCwd changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. workspaceRoot is captured by renderItem but is absent from listAppearanceData/extraData, and LegendList does not repaint visible rows when only that closure changes; include it in extraData (or otherwise invalidate/remount the rows).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1969:
Visible image rows keep using the previous `workspaceRoot` when `threadCwd` changes without a feed-data change, so images remain missing or resolve against the wrong root until another list invalidation occurs. `workspaceRoot` is captured by `renderItem` but is absent from `listAppearanceData`/`extraData`, and LegendList does not repaint visible rows when only that closure changes; include it in `extraData` (or otherwise invalidate/remount the rows).

Comment on lines +15 to +42
const [failed, setFailed] = useState(false);
const [expanded, setExpanded] = useState(false);
const name = props.alt.trim().length > 0 ? props.alt : "Image";

if (failed) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
</div>
);
}

return (
<>
<button
type="button"
className={cn(
"my-2 block max-w-full cursor-zoom-in overflow-hidden rounded-lg border border-border/80 bg-background/70",
props.className,
)}
aria-label={`Preview ${name}`}
onClick={() => setExpanded(true)}
>
<img
src={props.src}
alt={name}
className="block h-auto max-h-[360px] max-w-full object-contain"
onError={() => setFailed(true)}

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.

🟡 Mediumchat/ChatWorkspaceImage.tsx:15

After ChatRemoteImage's image load fails, changing props.src still renders the old error state, so corrected remote URLs remain permanently hidden. Because failed is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when props.src changes).

- const [failed, setFailed] = useState(false);+ const [failed, setFailed] = useState<string | null>(null);- if (failed) {+ if (failed === props.src) {
return (
<div className="my-2 rounded-lg border border-border/80 bg-background/70 px-3 py-2 text-secondary-label text-xs">
Unable to load image{props.alt ? ` “${props.alt}”` : ""}.
@@
- onError={() => setFailed(true)}+ onError={() => setFailed(props.src)}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/ChatWorkspaceImage.tsx around lines 15-42:
After `ChatRemoteImage`'s image load fails, changing `props.src` still renders the old error state, so corrected remote URLs remain permanently hidden. Because `failed` is a component-wide boolean, it is never cleared for the new URL; track the failed URL instead (or reset the state when `props.src` changes).

</div>
</div>
{imagePath && threadRef ? (
<div className="ms-7 mt-1" onClick={stopRowToggle} onPointerDown={stopRowToggle}>

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.

🟡 Mediumchat/MessagesTimeline.tsx:2398

When ChatWorkspaceImage is focused, pressing Enter or Space bubbles to the row's onKeyDown, which calls preventDefault() and toggles expanded instead of activating the image button. Add an onKeyDown handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 2398:
When `ChatWorkspaceImage` is focused, pressing Enter or Space bubbles to the row's `onKeyDown`, which calls `preventDefault()` and toggles `expanded` instead of activating the image button. Add an `onKeyDown` handler to this wrapper that stops propagation so keyboard users can open the generated-image dialog.

@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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3052999. Configure here.

Comment threadapps/web/src/session-logic.ts
@macroscopeapp

macroscopeappBot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This PR introduces new capability for displaying Codex-generated images with cross-cutting changes to mobile, web, and server-side authorization logic. A Critical finding identifies a potential thread isolation bypass in asset URL minting that requires human review.

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

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

Labels

size:XL500-999 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.

1 participant

@sambhavthakkar