Repository navigation
UI Consistency: 2 issues found
Two layout consistency issues in the new inline markdown image handling:
apps/web/src/components/ChatMarkdown.tsx(L1007) —CHAT_MARKDOWN_WORKSPACE_IMAGE_LAYOUT_CLASS_NAMEaddsalign-bottom(and keepsmy-1) only for the workspace-image branch. TheDirectbranch renders withCHAT_MARKDOWN_IMAGE_SIZE_CLASS_NAMEalone and so keepsvertical-align: baselineand no inline margin, since.chat-markdown imginindex.cssonly setsdisplay: inline-block. Now that workspace images are inline, a row mixing a remote badge with a workspace image is offset by the strut descender and the workspace image's inline margins grow the line box. Suggested fix: share one alignment/spacing contract across both image branches.apps/web/src/components/ChatMarkdown.tsx(L999–L1004) —authoredImageSizeStyleis ratio-aware only when bothwidthandheightare authored. With a single authored axis the element keeps a definite width whilemax-h-[30rem]clamps height independently, so tall images are letterboxed byobject-containinto visible bands inside the workspace image border (previouslyw-auto/h-autoscaled both axes proportionally), and the loading placeholder keepsaspect-video, producing a large jump to the loaded box.
Both were posted as inline review comments.
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: apps/web/src/components/ChatMarkdown.tsx, apps/web/src/components/files/FileMarkdownPreview.tsx, apps/web/src/components/files/FilePreviewPanel.tsx, apps/web/src/markdown-links.ts, plus the workspace-image tests. Mobile files and packages/client-runtime were read as context only.
Verified as consistent / not flagged:
data-markdown-copyis now emitted on all three workspace image states (loadedimg, loading placeholder, fallback chip) and on the no-thread/blocked fallback;serializeRenderedMarkdownFragmentreads that attribute before theIMGtag case, so copy output is the authored source in every state. This resolves the earlier copy findings.- The loading placeholder no longer claims
w-full; it usesw-64 max-w-fullmerged with the shared bounds class, so it no longer forces a line break next to inline images. - The fully-sized (both axes) path derives
aspect-ratioplus a proportionalmax-width, keeping placeholder and loaded image at identical bounds; covered by a new test. rehypePreserveWindowsImageSrccarries the raw drive path via an allow-listeddataLocalSrcattribute rather than rewritingsrc, and the renderer normalizes separators before classification — no styling or sanitizer-scope regression.FileMarkdownPreview(web) is a single-consumer wrapper that moves the previous call-siteclassNameverbatim; no primitive reconstruction or accessibility change.- No global CSS was moved, added, or deleted, and no theme variants or token reads changed.
🔍 Agent activity — 15 tool calls
💭 I'm checking the current PR head at commit 2727cdc against ChatMarkdown.tsx to see which prior issues were fixed. The skeleton mismatch flagged earlier now looks resolved since it uses proper width bounding classes, so I'm reviewing the rest of the diff for anything new. I need to pull the actual contents of ChatMarkdown.tsx, FileMarkdownPreview.tsx, FilePreviewPanel.tsx, and markdown-links.ts to verify.
1. browse_code — apps/web/src/components/ChatMarkdown.tsx
2. browse_code — apps/web/src/components/files/FileMarkdownPreview.tsx
💭 I'm checking the styling logic — inline width styles should override the
w-autoclass since inline styles take precedence, but I'm noticing the placeholder span has conflicting max-width classes that twMerge would need to properly dedupe. When only width is authored without an aspect-ratio, the placeholder keepsaspect-videoproportions while the loaded image uses its natural aspect ratio, causing a slight layout jump. I'm also checking whetherChatMarkdownImageFallbackstill receives astyleprop for sized images that fail to load, then moving to trace how the non-workspace path handles the image renderer anddataLocalSrclogic.
3. browse_code — apps/web/src/components/ChatMarkdown.tsx
4. browse_code — apps/web/src/components/ChatMarkdown.tsx
💭 I'm realizing sanitize will likely strip the raw
C:\...src since it isn't a recognized protocol, which is fine since the renderer relies on dataLocalSrc instead—but I need to check what happens if the workspace image path itself uses that format. Also checking the Direct branch: spreading{...props}would carry overdata-local-srconto the img element, though a Windows drive path probably wouldn't classify as Direct anyway. I should diff the commit range since the last review to see exactly what changed here.
5. git_tools — 9b743a1..2727cdc
💭 I'm checking whether the branch was force-pushed and reviewing the current state, particularly how the CSS handles image display for chat markdown—looking at whether the change from block to inline-block affects the layering rule that keeps workspace images aligned with their placeholders. Now I'm working through consistency cases between placeholder and loaded image sizing: when no width/height is authored, when only width is set, and when only height is set, checking how the aspect-ratio and max-width/height classes interact to determine how much layout shift occurs in each scenario. I'm verifying the max-height calculation for a very tall image with explicit dimensions—confirming the effective width gets constrained by the min of percentage, 30rem, and the computed aspect-based limit, landing correctly at the 30rem cap. Checking whether the inline maxWidth override on the Direct branch conflicts with the class-based max-width rule, since they resolve to the same value. I'm also noticing that spreading props before setting style means any authored style attribute from raw HTML would get overwritten—though since the sanitizer schema likely strips style attributes anyway, this probably isn't an issue in practice. Now I'm checking how the signed URL fragment gets appended for markdown images, and it looks safe even if the URL already has query params. Since mobile code is out of scope, I'm turning to FileMarkdownPreview.tsx in the web app, which duplicates similar logic, and I want to verify whether resolvePathLinkTarget is actually the correct helper to use here by checking its signature, then move on to reviewing RenderedMarkdownSurface in FilePreviewPanel. Previously the container class was applied directly to ChatMarkdown, but now it's been relocated into FileMarkdownPreview, so I need to check whether RenderedMarkdownSurface forwards any other props that might matter.
6. browse_code — apps/web/src/components/files/FilePreviewPanel.tsx
7. git_tools — export function resolvePathLinkTarget
💭 I'm noting that
resolvePathLinkTarget's colon-stripping is a minor edge case, not a UI concern, and moving on to check that the markdown-clipboard serializer correctly handlesdata-markdown-copyon the image fallback and loading placeholder spans—it looks fine since it just returns the attribute without descending into children. Now I'm examining a potential alignment regression: the fallback chip's outer span switched toinline-blockwithalign-bottom, while the inner span isinline-flex. Previously the whole chip was a single inline-flex with baseline alignment, but nowalign-bottomon the outer wrapper could shift how the chip sits relative to surrounding text within a paragraph, since bottom-alignment to the line box differs from default baseline alignment—though it's at least applied consistently across all three states. I'm also noticing that direct (non-workspace) images get no layout class and fall back to the global.chat-markdown imgrule withdisplay:inline-blockand default baseline alignment, while workspace images now usealign-bottom. That means in a paragraph mixing remote and local images, they could end up vertically misaligned by roughly a baseline descender gap—worth flagging since the stated goal was to keep workspace images aligned with authored text, but this only applies the fix inconsistently between image sources.
8. git_tools — chat-markdown img
💭 I'm noticing a discrepancy - the grep result shows a different class name definition than what I saw when viewing the file directly at that commit, so I need to figure out which version is actually accurate. Let me pin the rev explicitly and re-run the search to resolve this.
9. git_tools — inline-block
10. browse_code — apps/web/src/index.css
💭 That earlier grep must have searched the wrong revision. I need to check the
.chat-markdown imgrule specifically at the PR head instead.
11. git_tools — chat-markdown img
💭 I'm confirming the CSS shows workspace images use align-bottom while direct images default to baseline, which could create a slight vertical misalignment in mixed image rows like badge lists — though it's a fairly minor, modest issue rather than a clear-cut violation. Actually the more relevant check is whether the placeholder span, fallback span, and loaded img all share consistent alignment classes — they do, all using align-bottom, so that's internally consistent. I'm now tracing through the sizing classes for the loading placeholder and fallback states, checking how the width/height/aspect-ratio utilities merge with the bounds constraints to make sure nothing conflicts when an image has no authored dimensions versus when it does. I'm confirming the tailwind-merge behavior keeps the more specific max-width bound and that the test assertions around width classes still pass, then noting the pre-existing divergence where direct images skip the spacing/border/rounded styling that workspace images get — that's an existing inconsistency, not something introduced here. Stepping back, I'm considering the PR's overall scope: the web FileMarkdownPreview component, imageBaseDir handling, copy-markdown support, authored image sizes, and inline layout changes. I notice FileMarkdownPreview hardcodes layout classes like max-width and padding inside the shared component rather than at the call site, though since it's single-consumer that's acceptable. I also spot that the imageBaseDir derivation logic (finding the last path separator) is duplicated between the web and mobile versions of FileMarkdownPreview. Since mobile is out of scope for this review, I'll set that aside as an optional suggestion — maybe worth extracting into a shared helper alongside classifyMarkdownImageSource — but I want to focus on actual behavior regressions instead. One thing worth checking closely is how authoredImageSizeStyle reads width and height from props, since those props exclude certain node-related fields that could matter. Sanitization allows img width/height attributes, and non-numeric values like percentages correctly get skipped since Number() returns NaN. When numeric, style.width gets set to the parsed pixel value, so that seems fine. Now I'm considering the edge case where only one of width or height is specified without the maxWidth override, and how the existing max-width/max-height classes interact with a definite width paired with height:auto. For a replaced element like an image with definite width but no explicit height, if the aspect-ratio-derived height would exceed a max-height constraint, the browser clamps the height while keeping the authored width — which with object-fit:contain doesn't distort the image but does create letterboxing bands, since the actual rendered image shrinks further inside that box. I'm now checking whether this is a pre-existing issue or something the PR introduces. Before this change, the Direct branch spread the raw props (including the width attribute) but also applied a
w-autoclass, and since CSS classes override presentational HTML attributes, the authored width was effectively ignored on direct images previously. This PR changes that by honoring the authored width through an inline style, which means the single-axis clamping/distortion scenario is a new behavior introduced here, not a preexisting bug. Realistically,<img width="200">with just one dimension is fairly common in markdown READMEs, though the tall-image edge case that would trigger max-height clamping is rare. I'm weighing whether this deserves a comment or fix — perhaps when only width is authored, the max-height constraint shouldn't apply at all, lettingheight: autohandle it naturally instead of risking letterboxing. But the guidance here is clear: if a cleanup can't prove semantic equivalence, I should leave it unchanged rather than guess, and not fail on aesthetic preferences. Taking a fresh case — a loading placeholder with<img src="workspace.png" width="200">— the placeholder renders at an assumed aspect ratio while the loaded image uses its natural ratio, causing a layout jump, but that's a minor placeholder-sizing quirk rather than something worth blocking on. CheckingsrcFragmenthandling for signed URLs with existing query strings, that concatenation looks safe. I also tracedcopyMarkdown's use ofnormalizeMarkdownLinkDestinationfor a Windows-style local path and confirmed it preserves backslashes correctly when copied. For images that carry explicitwidth/heightHTML attributes, copying to markdown loses those dimensions—minor but worth noting. Now I want to check whether markdown-clipboard's IMG handler checks for adata-markdown-copyattribute before its default IMG case.
12. git_tools — markdown-copy|case "IMG"
💭 The data-markdown-copy check for IMG looks correct, and the Direct branch also matches authored source since src is the actual URL there — though empty alt text produces an empty copy, which seems like a pre-existing quirk rather than a new issue. Now I want to dig deeper into whether align-bottom on an image creates any real UI inconsistency. Turning to the placeholder chip, align-bottom shifts its text down relative to surrounding text by roughly the descender depth, since the chip's padded box bottom aligns to the line box bottom rather than baseline. This differs from how the loaded image aligns, since align-bottom on an image is a standard fix for the descender gap, but the chip has different dimensions so perfect alignment across states isn't really achievable anyway — it's a minor shift that seems self-consistent with the surrounding elements rather than a real bug. The more substantive issue is that Direct images don't get align-bottom while workspace images do, so in a mixed badge row the two would sit at slightly different vertical positions due to the descender gap — before this PR that wasn't visible since workspace images were block-level, but now that they're inline it's a real regression introduced by these changes. The simplest fix would be applying align-bottom consistently to the shared image size class, or removing it from the workspace layout class. This seems worth flagging as an inline comment since it's a concrete, change-induced inconsistency, unlike the chip alignment nitpick. Confirmed: since margin boxes count toward line box height for inline-level replaced/inline-block elements, the workspace image's
my-1combined withalign-bottomreally will make mixed rows misalign and produce uneven line heights compared to direct images with neither property. This is a legitimate finding worth flagging at the layout class definition around line 1007. I want to be careful not to overstate it as a regression since standalone image paragraphs still get roughly correct spacing — the issue is specifically about mixed rows. I should also check whether the loading placeholder span withaspect-videohas similar inline-block sizing concerns worth investigating. Now the src stays as the raw Windows path, and sanitization will likely strip it since thec:protocol isn't allowed, whereas before with afile:///URL the classification path was different. I need to check how classifyMarkdownImageSource handlesfile://URLs in markdownImages.ts to understand the fallback behavior.
13. browse_code — packages/client-runtime/src/markdownImages.ts
💭 I'm tracing how a Windows local path gets normalized: backslashes get converted to forward slashes, then the drive pattern matches and produces a classified path like
C:/repo/img.png, matching what the old file URL parsing approach would have yielded. That confirms the logic is fine—this is purely a path-handling concern, not a UI consistency issue. Now I want to double-check whetheralign-bottomis actually inconsistent with the stylesheet, since I recall seeing avertical-align: middlerule elsewhere that might apply to a different selector like list marker images, so I should look at that section more closely.
14. browse_code — apps/web/src/index.css
💭 This confirms a real inconsistency worth flagging: workspace images get bottom-aligned with margin while remote/direct markdown images stay baseline-aligned in the same paragraph, which directly undermines the PR's stated goal of consistent authored paragraph alignment—especially in common cases like mixed badge rows. The fix would be sharing one vertical-alignment rule across both image kinds rather than applying it only to workspace images, though I want to double-check whether the alignment choice itself is correct by reasoning through how an empty placeholder span's baseline compares to an image's baseline under CSS inline-block rules. Actually, the placeholder and img already share the same natural baseline since both have no in-flow content, so align-bottom isn't needed for that pairing — it was likely added to fix alignment with the fallback chip, which has real inner content and thus a different baseline. But align-bottom aligns to the line box bottom rather than to sibling elements, so applying it selectively creates a new mismatch: a workspace image would sit slightly lower than a baseline-aligned remote image by roughly the font's descender depth, producing a visible misalignment in rows mixing both image types. I also want to flag that
my-1on the inline-block workspace image adds extra margin that expands the line box height compared to direct images without margin, worsening the same row-alignment inconsistency — I'll mention it as a secondary point in the same comment. I'll place the comment at the layout-class definition around line 1007, noting workspace images now render inline alongside direct images but only getalign-bottomandmy-1, causing uneven alignment and line height in mixed rows, and suggest a shared alignment/spacing contract across both branches. Let me confirm this line is actually part of the diff hunk before finalizing where to attach the comment. I'm also weighing whether to flag the letterboxing issue withauthoredImageSizeStyleandobject-containcombined withmax-h-[30rem]— since direct images lack a border, empty bands would be invisible, but workspace images have a visible border, so the same letterboxing would show as visible empty space inside the frame. The prior fix addressed the two-axis version by computing maxWidth, but this single-axis case shares the same root cause and wasn't caught. I think this is worth flagging as a follow-up: when only one axis is authored, the other axis's cap still clamps independently, and dropping the max-height constraint in that scenario would be the fix. I'm weighing whether to raise a third variant of this issue since two similar cases were already flagged and fixed. There's also a real placeholder mismatch for the single-axis case—an authored width withaspect-videoproduces a placeholder shape that doesn't match the eventual loaded image dimensions—which seems worth flagging as a secondary, lower-priority comment. I'll settle on two comments: a primary one on the align-bottom/margin inconsistency in the inline row, and a secondary one noting thatauthoredImageSizeStyleonly accounts for ratio when both axes are authored, letting a single authored axis clamp independently and create letterboxed bands plus a placeholder-to-loaded size jump—suggesting the opposite cap be skipped in that case too. So with object-contain, the image renders at 120x480 centered, leaving visible transparent bands on the sides within the border. Before the authored width was added, the auto-width/auto-height replaced element would preserve its natural ratio when clamped by max-height, so it rendered cleanly at 120x480 with no bands — confirming this is a real regression introduced by the new inline width style. Checking the placeholder for that same image reveals a mismatch too: it's sized 300x169 via aspect-video, but the loaded image ends up 300x480, causing a 311px layout jump — unlike the two-axis case that was explicitly tested for parity. This confirms the ratio-aware path only handles cases where both width and height are authored together. The cleanest fix would be to only apply the authored style when both axes are known, falling back to undefined for single-axis cases, restoring the old behavior and keeping the placeholder in sync — though I recognize this sacrifices the ability to honor a single explicitwidthattribute on things like a logo image. I'll go with posting both comments, with the layout regression as the primary one — this exact issue pattern has been flagged three times before across earlier commits and the author kept iterating on it, so the remaining single-axis case seems like a genuine leftover rather than nitpicking. Let me double-check the correctness of the primary claim before finalizing, since I don't want to raise a false positive. I'm confirming that direct markdown images use baseline alignment while workspace images use bottom alignment, which does create a visible pixel offset when both appear on the same line — the align-bottom image sits noticeably lower relative to the line box than the baseline-aligned one. Also the extra vertical margin on workspace images grows the line box compared to remote images, so despite achieving inline layout, the alignment between the two image types remains inconsistent, which conflicts with the PR's stated goal of clean inline row alignment. I'll write a comment flagging this: theDirectbranch only applies the size class, keeping baseline alignment and no margin, while this branch addsalign-bottomandmy-1, causing workspace images to sit lower and add extra spacing in mixed image rows. I should suggest moving the alignment/spacing onto the shared image class so both branches render consistently, though a clean inline suggestion isn't possible since it touches the shared class definition. For the second comment, I want to flag that only the both-axes path is ratio-aware — with a single authored axis like a fixed width, the element keeps a definite width while the height clamp acts independently, causing a tall image to get letterboxed by object-contain rather than scaling proportionally like the old w-auto/h-auto behavior did. I'll anchor this at line 1002 with start_line 997, noting the loading placeholder still uses aspect-video in that scenario too. I'm sketching a possible inline fix: bail out and return undefined unless both width and height are defined, then compute aspectRatio, set height to auto, and cap maxWidth using min(100%, 30rem, and a ratio-scaled value). But that would mean dropping support for honoring a single authored width entirely, which is a behavior change worth flagging rather than just slipping in. Finishing computing the aspect ratio and max-width so the whole replacement block is self-contained, spanning lines 999-1004 as one added function. That confirms the suggestion is valid, so I'll go ahead and post the review now.
15. modify_pr — Two layout findings on the new inline image handling in ChatMarkdown.tsx. Everything else (copy parity across all three image states, ratio-aware sizing for fully sized images, bounded placeholder width) looks consistent.
Agent Credits: 142 credits