Skip to content

test(mobile): stub the hooks the intrinsic image renderer uses

MacroscopeApp / Macroscope - UI Consistency failed Aug 23, 2026 in 4m 33s

UI Consistency: 1 issue found

apps/web/src/components/ChatMarkdown.tsx — authoredImageSizeStyle only preserves the authored aspect ratio when both width and height are present. With a single authored axis (the common <img src="…" width="300"> README form, which survives rehype-raw + the sanitize schema), the element gets a definite width while max-h-[30rem] clamps the height independently, so ratio-preserving auto-sizing is lost and object-contain letterboxes tall images inside the workspace image's bordered frame (and the skeleton takes the same over-wide box). Prior to this change h-auto w-auto plus the two caps shrank the box proportionally.

Suggested smallest fix: express a lone authored axis as a max constraint (e.g. maxWidth: min(300px, 100%, 30rem) with the dimension left auto) so the intrinsic ratio still drives the box while the 30rem caps clamp it.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Scope reviewed (include patterns apps/web/src/**/*.{ts,tsx,css}): components/ChatMarkdown.tsx, components/ChatMarkdown.workspace-images.test.tsx, components/files/FileMarkdownPreview.tsx, components/files/FilePreviewPanel.tsx, markdown-links.ts, markdown-links.test.ts. Mobile and packages/client-runtime changes were read for context only.

Checked and found acceptable:

  • inline-block!/align-bottom layout class is now shared by the loaded image, the loading skeleton, and the unavailable chip; the skeleton's earlier w-full (flagged in a prior run) is now w-64 with the shared max-h-[30rem] max-w-[min(100%,30rem)] bounds, so inline rows no longer reflow from stacked full-width boxes. The ! still correctly outranks the unlayered .chat-markdown img { display: inline-block } rule in index.css.
  • data-markdown-copy is now emitted on all three image states (loaded, loading, unavailable), and serializeRenderedMarkdownFragment reads that attribute before the IMG/element fallbacks, so copy output is the authored source in every state, including the no-threadRef fallback flagged in a prior run.
  • rehypePreserveWindowsImageSrc + the img: ["dataLocalSrc"] sanitize allowance carry the raw drive path to the renderer without minting a file:/// src; Windows drive paths never classify as Direct, so the data attribute cannot leak onto a rendered DOM node via the {...props} spread.
  • imageBaseDir is threaded into the components memo dependency list, and FileMarkdownPreview keeps the previous call-site className while centralizing base-dir resolution for the single consumer in FilePreviewPanel.
  • Accessibility attributes (role="status", aria-label="Loading image", alt) are preserved across all image states.

🔍 Agent activity — 13 tool calls

💭 I'm checking the current state of the PR head at commit 84eb288, since prior runs flagged issues at earlier commits, and want to look at the current image rendering code in ChatMarkdown.tsx.

1. git_tools — CHAT_MARKDOWN_IMAGE_BOUNDS_CLASS_NAME|CHAT_MARKDOWN_WORKSPACE_IMAGE_LAYOUT_CLASS_NAME|authoredImageSizeStyle

2. browse_code — apps/web/src/components/files/FileMarkdownPreview.tsx

💭 The earlier review at 9b743a1 flagged the skeleton's w-full width; now it's been changed to w-64 max-w-full, which looks like it addresses that feedback directly. Now I should scan the rest of the image-related code in ChatMarkdown.tsx for any remaining issues.

3. browse_code — apps/web/src/components/ChatMarkdown.tsx

4. git_tools — 9b743a1..84eb288

💭 I'm checking how the image sizing logic handles cases where only width or only height is authored, making sure inline styles and clamp classes interact correctly without conflicts, and now verifying the case where both dimensions are set together. I'm also confirming the placeholder's aspect ratio and sizing classes properly defer to inline styles when a size is authored, and tracing through whether the data-markdown-copy attribute correctly gets omitted when copyMarkdown is undefined. Now I'm checking the sanitize schema to make sure dataLocalSrc maps correctly to the data-local-src HTML attribute in hast's camelCase property convention, and verifying the img renderer actually receives and spreads that data attribute through react-markdown's node properties. I want to trace whether Windows drive paths ever hit the "Direct" classification branch by checking markdownImages.ts, and also confirm that serializeRenderedMarkdownFragment properly handles the data-markdown-copy attribute on img elements the same way it does for file-link chip spans. Beyond that I'm noting that imageBaseDir is correctly included in the memo dependency array, and reconsidering the ChatMarkdownImageFallback layout — the outer span switched from inline-flex with gap to inline-block! with an inner inline-flex span, which should be fine as long as the important-flagged block! rule still properly overrides the unlayered chat-markdown img display rule as originally intended. Not worth flagging strongly. Let me check index.css for the .chat-markdown img rule, and also look at markdown-clipboard.ts and the img renderer region.

5. browse_code — apps/web/src/components/ChatMarkdown.tsx

6. git_tools — data-markdown-copy|markdownCopy

💭 I should look at markdown-clipboard.ts around lines 190-230 to see how elements get serialized, especially IMG handling and whether a span with data-markdown-copy is treated as inline or block.

7. browse_code — apps/web/src/markdown-clipboard.ts

💭 With copy behavior handled, I'm narrowing the check scope to the changed files under apps/web/src—ChatMarkdown, its workspace-images test, FileMarkdownPreview, FilePreviewPanel, and the markdown-links files—and specifically looking at FilePreviewPanel's changes to see if the styling on ChatMarkdown regressed anything. I notice the mx-auto max-w-4xl px-6 py-5 classes moved from the call site into FileMarkdownPreview, so I want to check that call site for any other props that might have been dropped.

8. browse_code — apps/web/src/components/files/FilePreviewPanel.tsx

💭 I'm looking at the change where the rehype plugin now stores the Windows drive path in dataLocalSrc but leaves src untouched, instead of normalizing it to a file:/// URL like before. I need to check whether the sanitizer's protocol allowlist will now strip that raw src attribute since it no longer has an approved protocol. I think it will get dropped—sanitize checks for a colon before any slash/query/hash, and treats what precedes as the "protocol," so "C" isn't recognized as valid and the attribute gets removed. That seems intentional since dataLocalSrc now carries the value forward for the renderer to use instead. I still want to double check how classifyMarkdownImageSource treats forward-slash Windows paths, and look at the isWindowsDrivePathHref check used for anchor hrefs to make sure both paths are handled consistently. I'm also considering whether dataLocalSrc survives sanitize's schema matching—since it's a data-prefixed property, it should pass through fine, and hast-util-to-jsx-runtime should convert it correctly into a data-local-src prop for react-markdown to render.

9. browse_code — packages/client-runtime/src/markdownImages.ts

10. browse_code — apps/web/src/components/ChatMarkdown.tsx

💭 Windows drive paths never trigger the Direct branch, so that's confirmed safe. Now I'm checking the sizing mismatch between the placeholder's fixed dimensions and the loaded image's flexible bounds, since without authored dimensions this could cause a visible layout shift. I also notice tailwind-merge would collapse the duplicate max-width classes, keeping only the more specific bound — dead but harmless since w-64 already fits within it. Now I'm looking at whether switching the fallback chip's outer span from inline-flex to inline-block affects vertical alignment of its contents.

11. git_tools — chat-markdown img|chat-markdown p img|.chat-markdown

💭 I should search directly for where images are set to display inline-block within the chat markdown styles.

12. git_tools — img {|img,

💭 I'm reasoning through why the global CSS rule for images being inline-block is redundant given Tailwind's layering, but it's still necessary for spans since they default to inline and need a box to apply width, height, or aspect-ratio. Checking the direct image branch, I confirm the authored size style overrides any spread props' style attribute, but since the sanitize schema doesn't allow a style attribute anyway, this isn't actually a conflict. I'm now examining whether the explicit width/height attributes on the img tag could cause any other regression. Working through the workspace image placeholder logic, I trace how width-only, height-only, and both-dimensions-authored cases interact with the fixed aspect-ratio and max-width classes — width-only clamps sensibly under the max-width constraint, but height-only creates a conflict since an explicit height alongside the aspect-video class means the aspect ratio gets overridden by the specified height. I should verify whether cn actually uses tailwind-merge here. Then I notice a more significant issue: the loading placeholder now sits at a fixed w-64 while a loaded workspace image without authored dimensions renders at intrinsic size up to 30rem, though the prior w-full concern was addressed. Checking data-markdown-copy behavior, it correctly takes precedence in serializeNode over the IMG case, returning the authored markdown string inline within a paragraph, so copy-as-markdown should work fine — but I still need to check how copy-as-HTML handles this case. Looking further, the new FileMarkdownPreview on web hardcodes its className rather than accepting an override prop, but since there's only one consumer, that's acceptable under a minimal-contract approach. I also notice web and mobile compute the image base directory differently (resolvePathLinkTarget vs resolveWorkspaceFilePath), which is duplicated logic, though mobile is out of scope here. The loading placeholder's accessibility attributes are retained correctly, and I'm now checking whether the fallback chip's outer span styling is properly set to inline-block. Now I'm noticing that switching the outer chip span from inline-flex to inline-block with align-bottom changes its vertical alignment relative to surrounding text — previously it sat on the text baseline via default vertical-align, but now its bottom edge aligns to the line box bottom, which could push the line box taller and shift adjacent text upward since the chip exceeds the text height. That's likely a deliberate mimic of the align-bottom trick used for images to avoid the descender gap, though it's imperfect for a text-containing chip and is probably too minor to flag as a real bug. I want to check something more concrete now: whether appending a fragment identifier to an already-signed URL that may include a query string could break the URL, since simply tacking on a fragment after a query string needs careful handling. I trace through how the fragment gets extracted from the classified source after stripping search and hash — it looks correct for cases like a filename with a version query and a trailing fragment, since the extraction function properly isolates everything after the #. There's an edge case where a literal # appears in a filename itself, which would get stripped during classification, but that seems like pre-existing behavior rather than a new bug. Now I want to reconsider what the actual meaningful finding might be here — it's likely related to how the image size class name and computed style interact in the "Direct" image branch, particularly whether the max-height constraint conflicts with the new width-preserving ratio style. I confirm the max-width calculation correctly caps wide images at the height limit and shrinks tall images proportionally, so the class-based max-height constraint never actually triggers since the computed max-width already prevents the derived height from exceeding it — this resolves the earlier concern. But then I start considering a variant case: what if only the width is specified for a very tall image, where height is auto and the max-height class alone would need to do the clamping work. Actually, Tailwind's w-auto/h-auto classes override the width/height attributes since presentational hints have lowest priority, so authored dimensions were effectively ignored for Direct images before but are now honored — meaning a single-axis case like an 800x2000 image with only width="800" set would render at max-width with height scaling proportionally, no letterboxing. But I'm now second-guessing whether the single-axis case is worth flagging, since without knowing the natural ratio there's no way to compute a matching max-width, and forcing maxHeight:none would lose the height cap. I need to think through whether there's a cleaner fix here that avoids distortion while still respecting both constraints. So this is the same class of bug the author partially patched — the single-axis width-only case still breaks. It'll only actually surface in READMEs when someone sets a width whose implied height exceeds 30rem, which is a fairly narrow but real scenario. This does feel like a genuine, concrete regression tied directly to the changed lines—authoring width alone now defeats the ratio-preserving height clamp, since there's no way to infer intrinsic ratio from one axis. The real fix would be treating a single authored dimension as a max-bound rather than a fixed one, though that requires knowing which axis is safe to leave flexible. I'm double-checking the test expectations match, confirming the placeholder and fallback chip behavior are acceptable, and scanning the rest of the props for anything else that might be inconsistent, like the newly added imageBaseDir prop. I notice cwd still governs link resolution while imageBaseDir now governs image resolution—so a relative link and a relative image in the same nested markdown file could resolve against different base directories. That's a real inconsistency the PR surfaces, though it's arguably pre-existing link behavior rather than something introduced fresh, so I'm weighing whether it's worth flagging given the PR's scope is images. I'm also double-checking the CSS math for the tall-image case: with an inline width:400px overriding the w-auto class, the computed width is definite at 400px while height stays auto, and max-h-[30rem] caps it at 480px—so I want to trace through how the browser applies replaced-element sizing rules here before deciding if this is a genuine regression worth commenting on. Checking the CSS 2.1 constraint table for the h > max-h violation case: it resolves to width = max(w·max-h/h, min-w) and height = max-h, but this table technically applies when width/height are both derived from intrinsic dimensions, which I need to double check against our case. That table only applies to replaced elements when both width and height compute to auto. With a specified width and auto height, max-height clamps to 480, producing a 400x480 box that letterboxes the content with object-fit: contain — confirming this is a real regression, since previously the width attribute was overridden by w-auto, keeping both dimensions auto and routing through the ratio table correctly. It's a moderate, somewhat edge-case severity issue. This same family of bug has already been flagged across three prior iterations, and while the author fixed the two-axis case, the one-axis case is the same underlying issue with a small fix — treating the authored dimension as a max instead of definite, or setting the opposite cap to none. I'm weighing whether using min(Wpx, 100%, 30rem) for width with maxHeight set to none would work, but that risks a tall image blowing past the visual budget the caps are meant to enforce, so I'm considering keeping the ratio-based approach instead. Given the guidance that a clear consistency or regression risk warrants flagging, and this is a realistic scenario — mobile screenshots with authored width in README previews are common — I think this is worth surfacing rather than dismissing as nitpicking. Mobile screenshots are exactly this canonical case, which makes it worth flagging. I confirm object-contain and the border are both active on workspace images, so the empty bands would indeed be visibly boxed. The smallest fix seems to be: when only one axis is authored, avoid letting the opposite cap distort the box — perhaps by deriving maxWidth from the authored width while keeping width auto, or clearing maxHeight for the single-axis case. I'm weighing whether expressing the authored dimension as a max constraint (min of authored width, 100%, and a 30rem cap) with auto on the other axis would preserve the intrinsic ratio properly, though this sacrifices upscaling beyond intrinsic size. I keep searching for an approach that also preserves upscaling but haven't landed on one yet. Now I'm shifting toward drafting the actual review comment, describing the concrete broken behavior and suggesting the max-constraint approach as a minimal fix while leaving the final decision to the author. I need to figure out where to anchor this in ChatMarkdown.tsx — the relevant code sits around lines 990-1005 in the new file, so I'm checking whether to comment on a single line or the whole range, making sure the line numbers actually match the diff hunk on the PR's head side. I'm reconsidering whether this is worth flagging at all, weighing the risk of over-flagging against the instruction to only report concrete violations from changed behavior — but this does seem like a genuine layout regression tied directly to a case the PR enables, not just an aesthetic nitpick. I want to confirm the file preview path actually renders raw <img width> HTML, checking that rehypeRaw is included in the plugin list so raw HTML gets parsed, and that sanitization doesn't strip out the width attribute. Still recalling the attribute list... Confirmed width/height survive sanitization since the PR test with sized image works. Since markdown images never carry width/height attributes, this issue only affects raw HTML <img> tags — and single-axis width="300" without height is a very common pattern in READMEs, since that's GitHub's standard way to size images. I'm drafting a review comment explaining that with only one axis authored, the element gets a definite width while the height cap still clamps it, breaking ratio-preserving sizing — a tall screenshot would get letterboxed with visible bands inside the frame, whereas the previous h-auto w-auto approach scaled proportionally. I'm suggesting expressing a single authored axis as a max constraint instead. I'm sketching a possible fix: adjusting the width/height style logic so that when only one dimension is set, it applies as a max constraint rather than a fixed size, while making sure the undefined-check still works correctly — trying to keep it to a single clean diff hunk rather than something risky and multi-part. I'll skip proposing an actual diff since the fix is ambiguous, and instead just describe the issue in a comment. I'm pinpointing the exact lines (997 and 1004) to anchor the review comment, and checking that I'm not repeating feedback already given in earlier commits before posting the inline comment and marking the review as a failure.

13. modify_pr — One finding on the authored image sizing helper in apps/web/src/components/ChatMarkdown.tsx. The both-axes case is now ratio-correct, but the single-axis case (the common README form) still loses the intrinsic ratio.


Agent Credits: 125 credits