Repository navigation
UI Consistency: 1 finding (test gate)
The previously flagged regression is resolved: MarkdownLinkContext now suppresses onImageExpand for images nested inside links, so [](url) no longer renders a dead role="button" / tabIndex / cursor-zoom-in affordance, and the file-link-chip branch drops link children entirely so it is unaffected.
Remaining finding:
apps/web/src/components/ChatMarkdown.tsx— the new expand contract (context-based suppression inside links,onImageExpandforwarding toChatMarkdownWorkspaceImage, Enter/Space keyboard activation) has no focused test. The suppression only holds while theimgrenderer renders inside thearenderer's provider subtree, so a later refactor can silently reintroduce the linked-image affordance. Suggested smallest fix: tworenderToStaticMarkupassertions in the existingChatMarkdown.workspace-images.test.tsxsuite.
No blocking consistency or visual regression found, so this is reported as neutral rather than a failure.
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.
Checks performed:
- Verified
useandtype KeyboardEvent as ReactKeyboardEventare imported (ChatMarkdown.tsxlines 33-49) and that React 19.2.6 (apps/web/package.json) supports the<Context value>provider form, which matches existing usage inMessagesTimeline.tsx,sidebar.tsx, andsettingsLayout.tsx. - Confirmed both link render paths (
linkChildrendirectly and viaMarkdownExternalLinkContent) wrap children in the provider, and that thefileLinkMetabranch discards children, so no image escapes suppression. - Confirmed
onImageExpandwas added to themarkdownComponentsuseMemodependency list (line 2335), so the renderer map refreshes when the callback changes;ChatMarkdownismemo'd with the default comparator, so the new prop propagates. - Accessibility/interaction: no global focus-outline reset exists in
index.css(*only setsoutline-ring/50), so the new tabbable image keeps a visible UA focus ring; Space activation callspreventDefaultso page scroll is not triggered.role="button"on the<img>itself is treated as an intentional image target rather than a reconstructedButton, consistent with the check's exemption, even though sibling attachment thumbnails inMessagesTimeline.tsxandComposerPreviewAnnotationCards.tsxuse a wrapping<button type="button">; the markdown inline flow makes a wrapper impractical here. - Environment routing:
ChatMarkdownWorkspaceImagecontinues to resolve asset URLs fromprops.threadRef.environmentId, so no active-environment fallback is introduced. - Type import:
import { type ExpandedImagePreview }is safe becauseapps/web/tsconfig.jsonsetsverbatimModuleSyntax: false. - Test survey found no existing coverage for
onImageExpand, expand affordance classes, or the image-inside-link case inChatMarkdown.test.tsx,ChatMarkdown.workspace-images.test.tsx,ExpandedImageDialog.test.tsx, orMessagesTimeline.test.tsx.
🔍 Agent activity — 17 tool calls
💭 I'm checking the PR diff between the two commits and see a new
MarkdownLinkContextaddition that looks like the fix for the earlier linked-images comment. I want to verify it uses valid React 19 context syntax and confirmuseis properly imported. Tracing through the render flow: since the link's children are React elements rendered lazily inside the provider's subtree, the nestedimgcomponent readinguse(MarkdownLinkContext)should correctly pick uptruewhen wrapped by a link. I also notice theimgrenderer changed from an inline arrow function to a named function component, which shouldn't affect context reads but is worth double-checking for behavioral differences. I also want to verify theclosest("a")guard is now redundant but not causing issues, and confirmonImageExpandactually exists on the MessagesTimeline context before I finalize this. Let me pull up the file directly to check imports and confirm these details.
1. git_tools — ^import React, {| use,| useState,|type KeyboardEvent as ReactKeyboardEvent
2. browse_code — apps/web/src/components/ChatMarkdown.tsx
💭 Let me check the code around the img renderer and the
arenderer to see how they're handled.
3. browse_code — apps/web/src/components/ChatMarkdown.tsx
4. browse_code — apps/web/src/components/ChatMarkdown.tsx
💭 The case where fileLinkMeta exists is already handled since it returns a chip and drops children. But I realize raw HTML anchor tags in markdown also get parsed through rehypeRaw and sanitize, routing through the same
acomponent override, so I need to check that path too. I should also verify the React version supports using Context directly as a provider component, and confirm definingMarkdownImageinline inside the render function doesn't cause unnecessary remounts due to a new function identity on every render. Since thecomponentsobject mapping to react-markdown appears memoized based on dependency arrays, the identity issue should be avoided. I'm also considering whether the context actually propagates properly across react-markdown's rendering boundary — children passed to theacomponent are pre-created elements, so wrapping them in the provider should let the image component correctly read the context value. I'm also noting that usingrole="button"on an image element changes its accessible semantics from image to button, which seems to be an existing pattern from this PR rather than a new issue introduced now. Checking whether an image target is explicitly exempted from the raw-element guideline, whether keyboard handling for space/enter correctly prevents scroll, whether there's any class duplication, and whether prop spread ordering in the Direct branch could let markdown props override the expandable behavior — all seem fine on inspection. I also want to verify that spreading expandableMarkdownImageProps before onError in the workspace image component doesn't cause conflicts, and think about whether memoization on ChatMarkdownWorkspaceImage could break if onImageExpand isn't a stable function reference from the parent — likely a minor perf issue rather than a bug. The closest("a") guard looks like dead code now that context handles this, but it's probably still harmless for other rendering paths, so I'm now checking if there's any other path where an image could slip through unhandled. Checking the fileLinkChip path confirms it returns a chip and drops children, and the context default of false means images outside links still get onImageExpand correctly, so that's fine. The workspace image's asset URL explicitly uses environmentId from threadRef, so no environment routing issue there — now I want to trace how MessagesTimeline threads this through. Rather than expanding this to every ChatMarkdown call site since the PR is scoped to fixing agent image preview specifically for assistant rows, I should check ExpandedImagePreview's definition alongside how onImageExpand gets passed in, and confirm whether user message rows already have their own expansion handling to make sure there's no inconsistency worth flagging.
5. git_tools — onImageExpand
💭 I'm noticing the codebase already has an established pattern elsewhere—wrapping images in real buttons with proper aria-labels and cursor styling—whereas the new ChatMarkdown code reinvents this with a div using role="button", tabIndex, and manual keyboard handling instead of reusing the existing convention. I'm checking whether that reconstruction loses focus-visible ring styling compared to the sibling implementations, and also verifying that both assistant and user message rows render markdown images consistently.
6. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx
7. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx
💭 I'm trying to reconcile the line numbers from the grep output with what I saw when viewing the file directly, since the attachment button markup with the zoom-in cursor and aria-label seems to be located in a slightly different spot than expected. The main thing I'm weighing now is whether using
role="button"andtabIndexon an<img>element counts as a real finding, given that the codebase already has a proper<button>wrapper pattern elsewhere for image previews — the rules likely exempt this case, but I still want to check if the image element is missing a focus-visible ring style. The remaining edge cases—context usage inside MarkdownImage, HTML img/a nesting, and the memoized image component—all look covered. Now I want to check the focus styles in index.css to confirm whether outlines are reset globally.
8. git_tools — outline
9. git_tools — cursor-zoom-in
💭 The default outline styling just tints the ring color rather than removing focus indication, so that's fine as-is. I'm weighing whether putting role="button" on the raw
<img>instead of wrapping it in a real button element should be flagged, given this is functioning as an image-based interactive target rather than a typical semantic case. I'm checking through the remaining event handler typing to confirm currentTarget.closest works correctly given React's synthetic event generics, and considering the accessibility tradeoff of role="button" on an image stripping its image semantics versus the minor benefit of an aria-label fallback. These seem like minor concerns rather than blocking issues, so I'm moving on to check whether the workspace has other related considerations. Now I want to verify that ChatMarkdown's memo wrapper won't silently drop the new onImageExpand prop if there's a custom comparator excluding it, so I'm looking at the export and any explicit prop list used for memoization.
10. browse_code — apps/web/src/components/ChatMarkdown.tsx
11. git_tools — export default memo|memo(ChatMarkdown
💭 I should verify the React version supports the new Context.value syntax from React 19, and check how the codebase already uses patterns like
use(TimelineRowCtx)in MessagesTimeline to confirm the provider syntax being used elsewhere.
12. git_tools — Ctx value=|Context value=|Ctx value>|Context value>
13. git_tools — "react"|"react-dom"
💭 I'm checking whether the fix actually resolves the regression—confirming the markdown link context defaults to false and only applies in the non-file-link branch, so images inside file link chips aren't affected. I still need to verify how
expandableMarkdownImagePropshandles its closest-anchor lookup. Redundant safety check, not a real issue. Click-to-expand on images nested within other clickable ancestors like collapsibles is fine since stopPropagation handles it, and images inside tables, summaries, or code blocks aren't a concern. The role="button" with alt text works fine for screen readers since they'd rely on aria-label anyway, and the prop spread order in the workspace image component doesn't create conflicts. The key fix holds: images inside a link no longer get the zoom cursor or expand props since imageExpand is undefined in that context, and the memoized workspace image component correctly receives that undefined value when nested in a link. Checking whether the MarkdownLinkContext provider setup inside the link renderer could cause ordering issues with how react-markdown renders the image component, but that seems fine since the provider wraps children properly. The Tooltip's render prop cloning behavior with the link element also looks safe since children still render within it. There's an edge case with ChatMarkdownExternalLinkContent where it might render plainText instead of children when there's a favicon host and text content, so an image inside an external link with text could behave unexpectedly, but that's a pre-existing edge case rather than something new. I'm also noting that other markdown call sites that don't pass onImageExpand simply won't get expansion behavior, which isn't a regression. I'm now considering whether there's anything else worth flagging in this review. Adding tabIndex and role to every markdown image does increase the number of tab stops in long conversations, but this is consistent with how sibling attachment components already behave as buttons, so it's not really a new concern. The keyboard handler for the Space key firing repeatedly on hold is harmless. I'm also checking whether the copy handler or data-markdown-copy attribute is affected by these accessibility additions, and it doesn't seem to be. A more genuine concern is component identity: since the image renderer is defined as a named function inside a memoized components object, any change to the memo's dependencies creates a new component type, which would cause React to unmount and remount every image — losing load state and causing flicker. That's worth flagging as a real regression compared to the previous stable component reference. I'm also wondering about the type-only import of ExpandedImagePreview from the component module — withimport { type X }syntax, depending on verbatimModuleSyntax settings this could still emit a side-effect import and risk a circular dependency if that module ever imports ChatMarkdown back. I want to check how other files in the repo, like MessagesTimeline, handle importing that same type to see if this pattern is already established elsewhere.
14. git_tools — ExpandedImagePreview
15. git_tools — verbatimModuleSyntax|importsNotUsedAsValues
💭 Since apps/web has verbatimModuleSyntax off, the type import gets elided, which is fine. I'm double-checking whether ExpandedImagePreview from MessagesTimeline is a type or component alongside buildExpandedImagePreview—it's used as a type annotation there, so that module likely exports both the component and the type correctly. I'm now considering whether the ChatMarkdown onImageExpand wiring only applies to assistant rows, leaving tool-call markdown or plan cards out of scope, but that seems fine since it's outside this concern. I don't think event bubbling or preventDefault on the image click causes any issue with the timeline's click handling, since assistant rows don't rely on that bubbling for anything. The only minor nag is that the role="button" image relies on the UA default focus outline rather than the app's custom focus-visible ring styling used elsewhere, but that's an acceptable, minor inconsistency rather than a real violation. I also realize that giving the image role="button" overrides its native img role, so screen readers will announce it as a generic button rather than an image, though the aria-label still conveys the alt text, so it's a reasonable tradeoff. Since this PR introduces new keyboard handling and prop forwarding behavior, I want to check whether there are existing tests covering onImageExpand in ChatMarkdown, since that kind of behavioral change should have focused test coverage.
16. browse_code — In apps/web/src/components, are there any tests (e.g. ChatMarkdown.test.tsx or similar) covering the new onImageExpand prop of ChatMarkdown, markdown image click-to-expand, or the MarkdownLinkContext behavior for images nested inside links? List test files and relevant test names.
💭 Since this change adds keyboard handling and a context-based suppression contract to a shared renderer, and there's already an existing test suite that renders markdown to static markup, adding a focused test here seems feasible and worthwhile given the guidance to test behavior changes in primitive composition. Weighing whether this rises to a concrete, actionable violation: it's specific enough — add a case asserting the expand affordance appears for a bare image but not a linked one. Given the check's explicit rule about requiring tests for prop-forwarding/keyboard-handling changes, and that this is the only finding I have, I think one comment here is warranted rather than reporting all clear. I also want to sanity-check the memoized image renderer, since it now invokes a hook internally and depends on
text, which changes on every streaming token — meaningmarkdownComponentsgets recreated each chunk and React treats the image component as a new type, unmounting and remounting it every time. That remounting behavior already existed pre-PR for all renderers, so it's not a new regression introduced here. Then I trace through the conditional class logic for the zoom cursor and confirm it's applied correctly. Weighing severity now — is the missing regression test a real, reportable violation, or would flagging it be overly pedantic given the guidance to avoid failing on optional preferences? I decide the safer path is to double check the keyboard handler itself once more for an actual behavioral bug before deciding whether to report anything at all. I'm re-examining the Enter/Space key handler to see if the expand callback fires correctly. I trace through the case where the image is inside a link, confirming that focus and expansion still behave sensibly there, and checking whether the surrounding CSS could be quietly disabling focus outlines in the chat area. I don't find a global outline suppression, so focus visibility seems fine, and touch targets and drag behavior aren't relevant concerns here either. I check text selection and copy handling for the Direct image branch, noting it lacks the copy-markdown attribute like the other path does, but this is a preexisting gap rather than something introduced by this change. Overall I don't see a functional regression, so the real question becomes whether to flag the missing focused test given the review guidelines explicitly call for tests when prop forwarding and keyboard handling change, both of which apply here. I'm weighing whether to report this as a genuine finding versus staying quiet, since the reporting guidance says to only flag concrete violations tied to changed behavior and not fail for optional preferences, but a missing test for new prop forwarding and keyboard handling does seem like a real coverage gap worth surfacing as a suggestion rather than a hard failure. I decide the missing test is a genuine risk since the link-suppression behavior is invisible in the diff and easy to silently regress, and the codebase already has a suitable test suite where it'd be a quick addition. I'll post one inline comment on the relevant line asking for a focused test, though I'm still weighing whether the overall state should be failure given the check's default. I confirm line 2245 is the added line where the image component and expand hook are defined, so that's where the comment should attach. I'll keep the comment focused on the gap and smallest fix, skip mentioning the redundantclosest("a")guard since it's harmless, and post it.
17. modify_pr — Reviewed the follow-up: the linked-image affordance I flagged earlier is now correctly suppressed via MarkdownLinkContext, so [](url) no longer renders a tab stop / role="button" / cursor-zoom-in on the nested image. One remaining gate: the new interaction contract is untested.
Agent Credits: 172 credits
Token usage: 36 uncached input · 579,870 cache-read input · 130,846 cache-write input · 21,038 output
Agent Credits may also include non-token charges from external tools such as web research.