Skip to content

Merge branch 'main' into feat/reveal-file-chip-in-file-manager

MacroscopeApp / Macroscope - UI Consistency failed Aug 25, 2026 in 2m 4s

UI Consistency: 1 issue found

apps/web/src/components/ChatMarkdown.tsx — the new <button> fallback for chips with no primary action is rendered with selectable and the same chip classes as the <a> branch, but UA stylesheets apply -webkit-user-select: none to button. The chip label therefore cannot be selected or dragged from, unlike the anchor chip and the surrounding markdown prose that .chat-markdown's selection-based copy handler operates on. Smallest fix: add select-text to the button's cn(...) call (inline suggestion posted).

Previously reported issues are resolved in this head commit: the index.css chip rules are now element-agnostic (.chat-markdown .chat-markdown-file-link) so the button branch keeps --contrast-foreground and the focus ring; editorLabels.ts reuses getLocalFileManagerName; openInEditorMenuLabel no longer names a file manager for an onOpen that does not reveal; canOpenInPanel uses Boolean(workspaceRelativePath); and shouldUseMarkdownFileBrowserPrimaryAction removes the inert-anchor path.

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.

Files examined in scope: apps/web/src/components/ChatMarkdown.tsx, apps/web/src/components/ChatMarkdown.test.tsx, apps/web/src/components/chat/OpenInPicker.tsx, apps/web/src/components/preview/fileExplorerLabel.ts(.test), apps/web/src/editorLabels.ts(.test), apps/web/src/index.css, apps/web/src/remoteOpen.ts, and the pullRequest/* call sites that now thread environmentId.

Checks performed:

  • Cascade analysis of .chat-markdown .chat-markdown-file-link (unlayered, after @layer components closes) versus the Tailwind utilities in CHAT_INLINE_CHIP_CLASS_NAME; the widened selector now covers both the anchor and button branches, so the chip color and focus ring are preserved. The newly added focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/70 utilities are redundant with that unlayered rule but harmless, so they are not reported.
  • Clipboard pipeline: markdown-clipboard.ts reads data-markdown-copy before the BUTTON skip, and sanitizedHtmlFrom exempts .chat-markdown-file-link from the button removal selector, so markdown copy of the button chip is intact.
  • Primary-action fall-through: with onOpen optional, every hasPrimaryAction === true combination now resolves to editor, browser, or panel, so no anchor renders inert.
  • Environment routing: environmentId = threadRef?.environmentId ?? explicitEnvironmentId ?? null, with capability gating, RPC dispatch, editor label, and reveal label all read from that environment's serverConfig; call sites without an explicit scope suppress the shell actions instead of falling back to the active environment. All PullRequestMarkdown/PullRequestMarkdownEditor/TimelineBody call sites pass an explicit environmentId.
  • Label parity for the OpenInPicker refactor: every EDITORS[].label matches the removed literals, and file-manager resolves through getLocalFileManagerName (Finder/Explorer/Files), so no wording changed.

🔍 Agent activity — 30 tool calls

💭 I'm checking whether the prior findings were addressed in the current PR diff. The anchor-scoped CSS rule fix, the editorLabels.ts change to use getLocalFileManagerName, and the canOpenInPanel Boolean check all appear resolved — now I'm verifying the html/htm inert case as well. The primary action logic now correctly falls back to the browser when neither editor nor panel can open the file, and the "Open in Finder" label mismatch is fixed too, so most prior findings look addressed. Now I need to go through the current head files directly to spot any new issues.

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

2. browse_code — apps/web/src/editorLabels.ts

💭 With prior findings addressed, I'm now checking for new issues, particularly around the newly added "environment routing in shared renderers" rule in the check config. I'm verifying that ChatMarkdown correctly resolves the environment ID by falling back through the thread reference, then an explicit ID, then null. Now I'm tracing where serverConfig and revealInFileManagerLabel come from in ChatMarkdown, since it seems tied to something called serverEnvironment earlier in the code.

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

💭 Environment routing checks out. I'm considering whether the raw button element is intentional here since it's being used as a chip rather than recreating a primitive, and noting the file link class includes cursor and focus-visible styling. I want to check if there's a matching focus-visible rule for the chat markdown file link in the CSS file.

4. browse_code — apps/web/src/index.css

💭 I'm noticing the focus-visible ring added to the markdown file link class might duplicate the global :focus-visible box-shadow rule already in index.css, since Tailwind's ring utility also uses box-shadow under the hood. Since the unlayered global CSS wins in specificity, the added ring classes are likely dead code, though visually the two produce the same effect so it's not a real regression—just a minor redundancy that's weak to flag. I'm also checking whether MARKDOWN_FILE_LINK_CLASS_NAME's chip is used anywhere outside the .chat-markdown context, since that could change how the cascade interacts. Turning to the button branch, I'm considering keyboard activation via handleContextMenu — clientX/clientY default to 0, but they fall back to the bounding rect, so that's handled correctly. My concern now is whether the chip's context menu depends on readLocalApi() being available, since in a non-desktop/web context this could render as a fully inert button despite still showing hover states and aria-haspopup="menu", which would be a real UX regression worth checking. That aria-label override seems reasonable since the tooltip shows the full path anyway, and the tooltip trigger setup looks fine. Now I want to check readLocalApi to see how it's implemented.

5. git_tools — readLocalApi

6. git_tools — chat-markdown-file-link

💭 There's a discrepancy between what git_grep shows at the default revision versus what the view tool showed at the specific commit sha — I need to figure out why the grep result seems to reflect an older version of the file than expected. Let me try grepping again with an explicit revision to sort out the mismatch.

7. git_tools — chat-markdown-file-link

💭 I should use explicit revs going forward since the previous grep without one used a stale ref. I want to check whether MARKDOWN_FILE_CHIP_CLASS_NAME is used elsewhere besides building MARKDOWN_FILE_LINK_CLASS_NAME, and also whether the clipboard sanitization logic in markdown-clipboard.ts handles preserving button elements as interactive content. The chip re-emits as markdown for copying via a data-markdown-copy attribute, so I need to verify whether markdown-clipboard only queries a[data-markdown-copy] — if so, that would break copying for the button-based chip, which would be a real regression worth confirming.

8. git_tools — data-markdown-copy

💭 I'm noticing the generic element handling reads the copy attribute before checking skipped elements, so plain text copy works for the button—but I still need to check the html branch involving preserved interactive class names further down in the file.

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

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

💭 I confirm the button class exemption already works correctly since sanitizedHtmlFrom checks classList after matching the selector. Now I'm turning to look at how serializeNode handles text extraction. Shifting to the UI consistency changes: I need to verify OpenInPicker.tsx's refactor to use editorLabelForPlatform preserves the original labels — checking getLocalFileManagerName against the old hardcoded Finder/Explorer/Files logic, and comparing editor labels like "VS Code" and "IntelliJ IDEA" against the EDITORS definitions to make sure nothing changed unintentionally.

11. browse_code — packages/contracts/src/editor.ts

12. git_tools — getLocalFileManagerName

💭 All labels match, so the OpenInPicker refactor is label-equivalent, and its platform usage still matches the prior isMacPlatform behavior without semantic change. I'm also checking that preferredEditorMenuLabel doesn't involve any platform logic at all. The revealInFileManagerLabel correctly uses serverConfig's environment ID, and usePreferredEditor is properly scoped per environment through useOpenInPreferredEditor. revealMarkdownFileInFileManager also correctly threads cwd and environmentId into its file matching. I'm now looking at whether the CSS selector fix for the file link color rule (changing from a.chat-markdown-file-link to .chat-markdown-file-link) leaves other anchor hover rules unaffected, and noticing that the newly added Tailwind focus-visible ring classes might duplicate an existing global focus style rule for the same class. I'm comparing how the Tailwind ring utility computes its box-shadow versus the global CSS rule's box-shadow, and realizing the unlayered global rule takes precedence and would override any shadow contribution from Tailwind's ring composition—meaning the newly added focus-visible utilities are effectively dead code within .chat-markdown. I want to verify whether MarkdownFileLink is ever rendered outside that container, since that would change whether this redundancy actually matters. Since only real regressions matter, I should look harder rather than flagging this minor duplication. I'm checking the button's accessibility semantics — its class name, placement inside a paragraph, and whether the click handler behaves correctly when no local API is available, like in a web build context. I want to check localApi.ts to see how readLocalApi is implemented.

13. browse_code — apps/web/src/localApi.ts

14. git_tools — CHAT_FILE_TAG_CHIP_CLASS_NAME =

💭 Since readLocalApi always returns the browser local API, contextMenu.show works fine there too through the in-app menu rendering. I want to double check whether CHAT_INLINE_CHIP_CLASS_NAME accounts for button-specific style resets, since browsers set default button font and text-align that Tailwind preflight would need to override. Regarding button cursor defaults, Tailwind v4's preflight no longer forces pointer cursor, but since MARKDOWN_FILE_LINK_CLASS_NAME already includes cursor-pointer explicitly, that's covered. Now I need to look at the actual definition of CHAT_INLINE_CHIP_CLASS_NAME.

15. git_tools — CHAT_INLINE_CHIP_CLASS_NAME =

💭 I'm double-checking the font sizing on the chip class, and confirming buttons inherit font-family properly under Tailwind's preflight reset since that affects consistent styling for both anchor and button elements. Then I trace through the markdown copy button serialization path to confirm data-markdown-copy handling works correctly via serializeChildren, and verify isSkippedElement is only relevant to the plain-text path, not the HTML render path. Now I want to check the broader diff — specifically how environmentId is threaded through PullRequestSummaryTab.tsx and PullRequestReviewAnnotation.tsx into comment editing, and trace where it originates in the review thread.

16. git_tools — environmentId

💭 I'm checking other ChatMarkdown call sites that render file links without threadRef or environmentId — with the new rule, missing environmentId means canUseShellActions resolves to false, correctly suppressing environment actions at those sites. I'm now considering a subtlety around the useOpenIn hook that could affect this behavior. I also need to check onOpenInBrowser, another environment-scoped action, since it likely calls a preview-open function tied to environmentId — I want to trace where MarkdownFileLink is instantiated and how that handler gets computed there.

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

💭 I'm noticing that passing revealLabel when onReveal is undefined is harmless since the menu requires both to be set. But there's a subtler issue: onReveal is a fresh inline closure created on every render, so the equality check in areMarkdownFileLinkPropsEqual comparing previous.onReveal to next.onReveal by reference will likely always fail, potentially defeating the memoization. Since fileLinkChip is invoked freshly each time the markdown 'a' renderer runs, and onOpen was previously stable while onReveal now isn't, this seems like a real memoization regression -- MarkdownFileLink will always re-render because the comparator the author explicitly added now always returns false, which matters especially since this markdown renderer is used for streaming chat with frequent re-renders per token. That said, I'm second-guessing whether this belongs under "UI consistency" versus being a pure performance concern, though the fact that a comparator specifically exists to prevent unnecessary re-renders suggests the author cares about this exact scenario. I also want to check whether markdownComponents itself gets recreated on every streamed token anyway (since its dependencies likely include text-derived maps), which would mean all chips re-render regardless during streaming and the memo mainly protects against unrelated re-renders like theme changes -- making this a real but more moderate finding. One quick fix would be wrapping onReveal in a stable per-path callback or comparing by targetPath instead. But I want to double-check whether there's a stronger finding here, so I'm revisiting the primary-action click handling logic to trace through the branches for opening in editor versus browser versus file preview, looking for the inert-chip regressions found in prior passes. Tracing through the case where hasPrimaryAction is true but onOpen is undefined and canOpenInPanel is false, I find that canOpenInBrowser must be true, which makes useBrowserPrimaryAction evaluate to true as well — so the browser path handles it correctly and there's no inert state here after all. Checking further, canOpenInBrowser also requires threadRef, similar to canOpenInPanel's dependency on threadRef plus workspaceRelativePath, so a thread markdown pointing to an external HTML file with panel actions suppressed correctly falls back to opening in the browser. Now I need to look at what happens when hasPrimaryAction is false. Actually, there's a subtler issue: while useRemoteOpenResolution is unresolved, shell actions get suppressed, so a PR panel chip could render as a button first, then swap to an anchor once resolution completes. That element-type swap causing a remount is a minor but real layout concern worth flagging. I also want to check the CSS specificity shift from a.chat-markdown-file-link to .chat-markdown-file-link—dropping from (0,2,1) to (0,2,0) could let a later same-specificity rule override styling, though the inline code color rule seems to apply the same color regardless. Since Tailwind v4's utilities live in a cascade layer, unlayered custom rules like --contrast-foreground always beat them regardless of specificity, so that part of the fix holds and now correctly applies to the button too. I still need to verify whether index.css wraps these markdown rules in @layer as well.

18. git_tools — ^@layer|^}

💭 I'm tracing the @layer components block to find where it closes, confirming that the rule around line 2274 sits outside any layer—so it should override Tailwind's layered utilities without needing extra specificity tricks. Given that, the redundant Tailwind focus-visible classes on the button become dead code since the global unlayered rule already wins the cascade. It's minor and harmless, but I'm weighing whether it counts as a real ownership inconsistency worth flagging, since it makes the styling look button-owned when it's actually controlled globally—so I want to double check where the chat-markdown root class actually gets applied to confirm the selector really matches.

19. git_tools — "chat-markdown"|chat-markdown +

💭 Since it's always inside .chat-markdown, those added focus-visible utilities don't actually do anything visually, so that's minor. I want to find something more substantial, so I'm rereading the ChatMarkdown diff around the button and Tooltip, particularly how Base UI's render prop merges props onto the button element via TooltipTrigger. I check whether MARKDOWN_FILE_CHIP_CLASS_NAME is used elsewhere besides building the file link class string—it's only used once, which is fine as a constant. I also notice the button gets the same hover/cursor "link" styling as an actual link even though it just opens a context menu, but this seems consistent with the flagged pattern from before, and the aria-haspopup="menu" usage with a native context menu seems acceptable. Then I broaden my search to other new UI surfaces in the diff, checking the OpenInPicker label refactor and whether omitting "label" from OpenInOption for base options causes any type issues—it looks fine. Looking at resolveOptions(platform, availableEditors), I trace how the file-manager label switched from using isMacPlatform(platform) directly to editorLabelForPlatform(option.value, platform), and confirm non-file-manager options ignore platform entirely, which is consistent. I question whether platform here represents the actual OS string like "darwin" versus something isMacPlatform would match against a mac regex—if it's "darwin" the check might fail and produce the wrong label on macOS—but I realize this is pre-existing behavior unrelated to the current change, so I set it aside. I circle back to the memo/onReveal identity concern to decide if it's worth reporting, then start examining whether revealInFileManagerLabel being computed from serverConfig for a given environmentId interacts correctly with when onReveal is actually provided. The reveal action only shows when both onReveal and revealInFileManagerLabel are present, which checks out. I then trace openInEditorMenuLabel back through usePreferredEditor(availableEditors), confirming the editor list is scoped to the same environment, and verify the preference stored globally still validates against that environment's editor list so labels stay consistent with the executing environment. This satisfies the new rule that capability gating, action dispatch, and labels must all read from the same environment's server config—onOpen calls openInPreferredEditor(environmentId, availableEditors) using that same environment, so it checks out. I move on to check whether all call sites of PullRequestMarkdownEditor/PullRequestMarkdown have been updated now that they require an environmentId prop, planning to verify this against the type checker.

20. git_tools — <PullRequestMarkdown

21. git_tools — PullRequestMarkdown(Editor)?

💭 Now I want to check other places in the repo where ChatMarkdown is used, to see if there's a UX regression where an environment exists but isn't passed, causing shell actions to silently get dropped in surfaces like review comments or settings output.

22. git_tools — ChatMarkdown$

💭 I'm checking other ChatMarkdown call sites like settings or agent skill preview that lack threadRef or environmentId — these would have shell actions suppressed, which seems correct per the rule. But I'm wondering if surfaces that previously relied on the active environment via useActiveEnvironmentId() for "Open in editor" now lose that functionality since they don't pass threadRef or environmentId explicitly. Let me search more broadly for ChatMarkdown usages, including single-line variants, since my earlier grep pattern may have missed some cases due to escaping issues.

23. git_tools — <ChatMarkdown

💭 I'm wondering when ctx.threadRef would actually be null in the chat timeline — maybe for draft messages before a thread exists — which would suppress "Open in editor" that previously worked via the active environment. That's a real behavior change, though it does align with the suppress-rather-than-guess rule, even though the active environment often matches the thread's environment anyway. I don't think there's a strong new finding here, but I want to check whether the anchor-vs-button rendering split matters for keyboard focus or accessibility — both seem fine since both remain focusable with visible focus rings, and neither needs aria-haspopup. I'm now checking whether the button's aria-label overriding its content causes any screen reader discrepancy. Turning to the remoteOpen.ts changes: the unresolved case still resolves to LOCAL_EXEC just like before, so useRemoteOpenState's behavior for existing consumers looks preserved — I want to verify the earlier null-environmentId branch matches this new unresolved condition.

24. browse_code — apps/web/src/remoteOpen.ts

💭 I notice isResolved stays true even when serverConfig hasn't loaded, which could mean shell actions get suppressed correctly for a remote-unavailable case, but this reveals an inconsistency in how canUseMarkdownFileShellActions treats the resolved state. Overall I don't think there's a significant UI-consistency violation here. Looking at the two candidates I flagged, the redundant Tailwind focus-visible utilities seem like minor dead code that the guidance explicitly says not to fail on since it's an aesthetic/verbosity concern rather than something overriding a primitive's focus ring. I want to double-check whether the global rule is actually unlayered by verifying where the @layer components block opens and closes relative to the rule in question. Confirming that: rules nested inside @layer components would be indented so their closing braces wouldn't be at column zero, whereas the top-level closing braces I found do sit at column zero — that confirms the rule in question really is unlayered as I suspected. The second issue, an unstable closure defeating a memoization comparator in a hot chat-streaming render path, is a genuine regression introduced by this diff and worth flagging as an inline note since it silently disables an existing memo check for every markdown element rendered. Actually digging into whether that memo is ever truly stable — the file-chip creator generates a fresh closure on every call, so the comparator will always see a new reference and fail. But then I notice onOpenInBrowser was already an inline arrow function before this PR, meaning the memoization was already broken for any chip with browser-preview support, regardless of this change. That undercuts how novel a regression this really is. So this change just widens an existing gap rather than introducing something new — it extends the same defeated-memo pattern to all chips instead of just previewable ones, which worsens performance slightly but isn't a visual or consistency bug worth flagging strongly. Given the prior six issues were already fixed, I'm leaning toward reporting all clear here. Though the instructions ask for a plain "All clear" response, the system also requires JSON output, so I'll reflect that conclusion inside the JSON structure instead. Before finalizing, I want to double check the button markup nested inside markdown paragraphs and confirm what the "selectable" prop on the chip content actually controls — likely enabling text selection via a CSS class. I recall that in Chrome, you actually can't select text inside a <button> by dragging with the mouse — mousedown on a button doesn't initiate a text selection the way it does on other elements, even though I'm not certain this comes from an explicit UA-stylesheet user-select: none rule. The sanitized HTML selector for markdown-clipboard includes button with an exemption for .chat-markdown-file-link, so HTML copy should preserve it, and text serialization checks for data-markdown-copy before skipping BUTTON elements, so copying works there too. The remaining question is what FileTagChipContent's selectable prop actually does — if it applies select-text explicitly, that should override any default non-selectability on the button. I want to check how that prop is implemented.

25. browse_code — apps/web/src/components/chat/FileTagChip.tsx

💭 I'm trying to recall whether browsers' default stylesheets apply user-select: none to buttons, since that would affect whether text inside a button is selectable by default. Now I'm recalling more specifically that Blink's UA stylesheet applies -webkit-user-select: none to button-like elements, including <button> itself and various input types acting as buttons — which explains why button text isn't selectable by default in Chrome and Safari. This matters for the branch chip in the Electron app: since it renders as a button, its label text becomes unselectable in Chromium, unlike the anchor chip. That's a problem for the markdown surface's custom copy handling, since a selection spanning a non-selectable button might exclude or mangle that text when the range is cloned for the clipboard payload. Since the selectable prop is a deliberate design choice, I want to check whether the chat chip's label class differs from the composer's — specifically whether the composer variant includes select-none while the chat variant doesn't. Confirming that difference makes it clear the button element is undermining an intentional distinction: chat chips are meant to be selectable text, composer chips aren't. Since Tailwind's select-text utility lives in a layer that could be overridden by the browser's default button styles, I need to verify the CSS specificity actually works as intended before proposing it as the fix.

26. git_tools — select-text|user-select

27. git_tools — select-text

💭 I'm not finding decisive evidence about buttons specifically, so I'm checking Tailwind's Preflight stylesheet to see how it resets button appearance and styling by default. I recall that Chromium's built-in html.css stylesheet applies -webkit-user-select: none to buttons and input elements of type button/submit/reset, which matches the observable behavior that you can't select a button's label text by dragging in Chrome — that's consistent whether it comes from user-select or mousedown handling. But since the chip is intentionally selectable (so dragging a selection across a paragraph preserves the file reference and the custom copy handler relies on DOM selection), switching to <button> would make the chip unselectable by drag. Chrome excludes user-select:none content from selection-based copy, though the custom onCopy handler using range.cloneContents() might still capture the button at the DOM level and pick up the data-markdown-copy attribute, so plain-text copy could still work even though visual selection would skip over the chip. I'm now checking whether adding select-text to the button's classes is the right fix, since that's the concrete, scoped finding matching the "preserve accessibility and interaction semantics" rule. But I want to reduce the risk of a false positive, so I'm searching the codebase for existing buttons using select-text, and also examining the sanitizedHtmlFrom logic that explicitly exempts .chat-markdown-file-link buttons from removal to see if that exemption predates this PR or was introduced by it. I realize markdown-clipboard.ts isn't part of this PR's diff, so its handling of file-link exemptions pre-existed — but before this PR, .chat-markdown-file-link was always an anchor tag, which never matched the removal selector anyway, so the exemption logic was effectively dormant until now. I'm down to one moderate-confidence finding around button text selection, and a couple of weaker candidates. Since the task explicitly asks to only report concrete violations and say "All clear" if nothing holds up, I want to firm up my confidence on that button issue before deciding. I should check whether some stylesheet like index.css sets user-select globally on buttons, so I'll search that file for "user-select".

28. git_tools — user-select

💭 There's no global user-select rule in index.css, so it likely depends on the UA default—though many Electron apps disable text selection app-wide. I should check other CSS files or the app root for a user-select override.

29. git_tools — select-none|user-select

💭 Recalling the Chromium html.css UA stylesheet, I'm confident that the button element rule includes align-items: flex-start and text-align: center along with the appearance and cursor defaults, matching what I remembered for input buttons too. The other flex/cursor utility classes on the chip already override those UA defaults, so the real actionable finding is that the fallback <button> chip inherits user-select: none from the UA stylesheet, breaking selectability that the anchor chip variant deliberately preserves — which matters since ChatMarkdown's copy pipeline relies on selecting prose containing chips. The fix is adding select-text to the button's classes, though I want to double check whether a selection dragged across a paragraph with a user-select: none button behaves correctly in Chrome. Even though the DOM range still includes the button node and copy serialization stays correct, the visible UX regression is that users can't drag-select from within the chip and its text won't highlight, unlike neighboring chips in the same paragraph — a real but modest inconsistency worth flagging as an inline comment. Also, since selectable text is explicitly requested elsewhere on non-file markdown in that same PR panel surface, a chip that blocks selection while selectable is passed seems like an unintended gap rather than a deliberate choice. I'll flag this as an inline suggestion adding a select-text class to the button's className, making sure className itself stays last in the merge order. Now I need to pin down the exact line to comment on — the button's className block sits around lines 1574-1578 in the head file, all part of the added diff hunk, so any line in that range should work for the review comment. Adding select-text class to fix... I'll skip mentioning the redundant focus-visible utilities to keep the review focused, and now draft the comment body explaining that the button's UA stylesheet blocks text selection unlike the anchor branch, framing the fix as a suggestion.

30. modify_pr — One finding on the new no-primary-action chip branch in ChatMarkdown.tsx: the <button> fallback loses the anchor chip's text selectability. Everything else in scope (the index.css selector widened to cover both elements, editorLabels reusing getLocalFileManagerName, the label/RPC environment scoping, and the OpenInPicker label extraction) is consistent.


Agent Credits: 154 credits