fix(web): size the chat image slot from server-reported dimensions#10200

merged3
media-dims/mobile
media-dims/web
openedSep 5, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago

Stack 3/3 (#10201). Depends on #10198.

Problem

After #9938 a workspace image on web still moved the rows below it once: a portrait screenshot grew past its 16:9 slot when it decoded.

Fix

ChatMarkdownAssetImage applies the server's imageDimensions (#10198) through the existing authoredImageSizeStyle, so the slot is the image's final box from the moment the URL resolves. An authored width/height still wins; a caller's cap (the viewed-image row's maxHeight: 16rem) layers on top rather than shadowing the dimensions.

Evidence

Cold thread mount with image responses delayed 4 s, layout-shift observer armed before mount (same harness as #9938):

#9938this PR
Media layout-shift entries1 (0.013 โ€” portrait growing past 16:9)0

The only remaining entry on the page is the composer resizing, unrelated to media.

Verification

  • ChatMarkdown.workspace-images.test.tsx: slot is sized from server dimensions (width: 720px; aspect-ratio: 720 / 1400); authored size overrides server dimensions. 32 passed.
  • apps/web typecheck clean.
  • In-browser: portrait image renders with aspect-ratio: 720 / 1400 on the slot and the settled <img>; a 16rem maxHeight cap yields a 247ร—256 box.

Model: Claude Fable 5 ยท Harness: Claude Code in T3 Code

๐Ÿค– Generated with Claude Code

Note

Size chat asset images from server-reported dimensions

  • ChatMarkdownAssetImage now reads reported width and height from successful asset URL loads and derives inline width, aspect ratio, and a height-derived max-width
  • authoredImageSizeStyle gains an optional height-cap argument (default 30rem); when both dimensions are present the cap is folded into the max-width calculation
  • PlainWorkEntryRow in MessagesTimeline.tsx switches from an inline max-height style to the new maxHeightRem prop, keeping the 16rem value
  • Explicit incoming style props still take precedence; callers without dimensions can still apply a custom rem height cap as a fallback
  • Risk: asset images without server-reported dimensions no longer get a computed aspect ratio โ€” they fall back to the height-cap path only, which may shift layout for previously auto-sized images in ChatMarkdown.tsx

Macroscope summarized d18793c.

apps/web/src/components/ChatMarkdown.tsxline 1549Outdated ยท f3bcde2Open thread
โ€ฆโ€ฆ@@ -1538,6 +1538,17 @@ export const ChatMarkdownAssetImage = memo(function ChatMarkdownAssetImage(props
1546+ ? authoredImageSizeStyle(knownSize.width, knownSize.height)
1547+ : undefined;
1548+ const style =
1549+ knownSizeStyle && props.style
MmacroscopeappBoton line 1549Sep 5, 2026

๐ŸŸก Medium components/ChatMarkdown.tsx:1549

The viewed-image { maxHeight: "16rem" } override leaves width/maxWidth sized for authoredImageSizeStyle's 30rem cap, so portrait images reserve a box with excess horizontal space instead of preserving their aspect ratio. Recompute the width constraint from the effective height cap, or use sizing that preserves the ratio under both limits.

๐Ÿš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/ChatMarkdown.tsx around line 1549:
2
3The viewed-image `{ maxHeight: "16rem" }` override leaves `width`/`maxWidth` sized for `authoredImageSizeStyle`'s `30rem` cap, so portrait images reserve a box with excess horizontal space instead of preserving their aspect ratio. Recompute the width constraint from the effective height cap, or use sizing that preserves the ratio under both limits.

Fixed in b1c674214b. The cap is passed as maxHeightRem and folded into the width bound (max-width: min(100%, 30rem, 16ยทw/h rem)), so a 720ร—1400 image capped at 16rem gets a 132px-wide box rather than a 30rem-wide one. Test added.

Synced from GitHub

Thread transfer impact

โœ… Thread transfer remains within every enforced ceiling.

โ„น๏ธ No successful main baseline artifact is available yet. This run establishes the initial measurement.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wireโ€”13.6 KiBโ€”15.1 KiBโœ…
CodexThread snapshot wireโ€”7.0 KiBโ€”7.3 KiBโœ…
CodexLive turn WebSocket wireโ€”6.6 KiBโ€”7.8 KiBโœ…
CodexLive turn WebSocket decodedโ€”57.1 KiBโ€”66.4 KiBโœ…
CodexLive turn messagesโ€”10โ€”21โœ…
ClaudeTotal thread wireโ€”13.5 KiBโ€”15.1 KiBโœ…
ClaudeThread snapshot wireโ€”7.0 KiBโ€”7.3 KiBโœ…
ClaudeLive turn WebSocket wireโ€”6.5 KiBโ€”7.8 KiBโœ…
ClaudeLive turn WebSocket decodedโ€”57.8 KiBโ€”66.4 KiBโœ…
ClaudeLive turn messagesโ€”8โ€”21โœ…

Baseline: unavailable ยท PR result: d18793c ยท Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 113.8 KiB
  • Claude decoded thread snapshot: 114.5 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable โ€” This is a localized image-layout fix that uses existing server dimensions, preserves authored sizes, and retains explicit height caps. It also changes the default rendering of un-authored workspace images from the prior 16:9-style slot to their reported aspect ratio, making it a product-default behavior change.

No code changes detected at d18793c. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

Added 3 commits

  • fix(web): layer a caller's size cap over server-reported dimensions8135250
  • fix(web): size the chat image slot from server-reported dimensionsa9f98aa
  • fix(web): fold a caller's height cap into the image slot's width boundd18793c