Skip to content

fix(clients): harden Codex file citations

MacroscopeApp / Macroscope - UI Consistency failed Aug 28, 2026 in 4m 34s

UI Consistency: 2 rendering regressions found

Two concrete rendering regressions from enabling remark-directive on all chat markdown:

  • apps/web/src/components/ChatMarkdown.tsx (remarkCodexFileCitations visit): the early return on link nodes stops traversal, so directive nodes inside link text (and linkReference children) are never restored. They fall through to remark-rehype's unknown-node handler, which emits an empty <div> inside the <a> and drops the literal text (e.g. [Error:ENOENT explained](https://example.com)). Suggested fix: thread an insideLink flag, as remarkNormalizeLinksAndTagInlineCode already does, and restore the directive source inside links.
  • apps/web/src/components/ChatMarkdown.tsx (restoreDirectiveSource): flow directives (leafDirective, containerDirective) are restored as text nodes, which land in flow position as bare root text. They lose the <p> wrapper and therefore .chat-markdown p { margin: 0.65rem 0 } spacing, and with lineBreaks enabled their soft breaks no longer render as <br>. Suggested fix: restore flow directives as a paragraph containing the raw source text.

Both findings were posted as inline review comments with suggested fixes.

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: only apps/web/src/** files were targeted, per the check's include patterns; packages/client-runtime and apps/mobile changes were read for context only.

Checks performed:

  • Verified the prior run's finding (unhandled directives rendering as empty <div>) is now largely addressed by restoreDirectiveSource; the two comments above cover the remaining paths it does not reach.
  • Traced citation links through remarkNormalizeLinksAndTagInlineCode, normalizeMarkdownLinkHrefKey, and resolveMarkdownFileLinkMeta/resolveMarkdownFileLinkTarget: the #L<n> fragment produced by resolveCodexFileCitationLink is correctly converted to a line position by appendLineColumnFromHash, and the %25/%23/%3F escaping round-trips through safeDecode. The a renderer's fallback to resolveMarkdownFileLinkMeta means citation hrefs absent from markdownFileLinkMetaByHref (which is built by scanning raw source for literal link syntax) still resolve to file chips.
  • Environment routing: no new ChatMarkdown call sites were added in the web app; file-chip actions continue to resolve environmentId from threadRef?.environmentId ?? explicitEnvironmentId, with shell actions gated by canUseMarkdownFileShellActions. No active-environment fallback is introduced by this patch.
  • No new raw controls, primitive re-implementations, or global CSS changes are introduced in scope.

🔍 Agent activity — 14 tool calls

💭 I'm checking whether the prior finding about remarkDirective treating directive-like text as nodes was addressed. The diff now includes a restoreDirectiveSource step that converts non-codex directives back to plain text, which looks like it resolves the issue, so I want to verify the current implementation closely.

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

2. browse_code — packages/client-runtime/src/codexFileCitations.ts

💭 I'm checking whether converting a containerDirective block into a raw text node actually works correctly when it gets passed through remark-rehype, since that node type is normally block-level and I want to make sure the source-restoring approach doesn't break the tree structure. Also thinking about plugin ordering — since remarkDirective, remarkGfm, and remarkBreaks all just register micromark extensions, their order in the plugin list shouldn't matter, but I want to double check that restoreDirectiveSource correctly slices raw text for both container and text directives without mangling escaped characters. The bigger concern I'm circling is that restoring a container directive's entire source as one literal text block destroys any nested markdown formatting inside it — bold text or real links inside a :::note block would render as raw literal text instead of proper HTML, which is a real rendering regression compared to before this change, though I'm unsure how common :::note blocks actually are in agent output. I'm also realizing an early return on link nodes prevents recursion into their children, which means a citation directive nested inside link text never gets converted and instead reaches remark-rehype as an unhandled directive node, likely rendering as an empty div. So the fallback handler would drop the ":note" text and insert an empty div nested inside the link, which is invalid nesting on top of losing content. I'm also checking other node types like table cells and image alt text, which do get visited properly since the skip only applies to links. The remaining question is how likely it is for directive syntax to accidentally appear inside link labels—like whether something like "10:30" in a link text could be misparsed as a directive, which depends on whether directive names can start with digits. I'm also checking whether the "Meeting at 10:30" test case would actually distinguish between approaches, and it seems like it'd pass either way. The real question is where the restore path might fail — likely with directives nested inside link children that don't get visited, or with how text nodes render at the block level through mdast-to-hast. Beyond that, I'm noticing this loses the paragraph wrapper: a directive like ::note on its own line used to render inside a <p> tag, but now it renders as bare text, which changes spacing and styling — a small but real visual regression that also affects container directives. I'm also confirming that remarkBreaks doesn't alter position offsets, so that's not a concern, and double-checking that the vfile value react-markdown passes matches what's used for source slicing. There's still the open question of how text directives with labels inside paragraphs get restored. Now I'm considering whether the link-skip issue is actually a realistic bug — directives probably don't appear inside link text in generated content, though something like a timestamp inside link text could theoretically be misparsed as a directive name. I'm tracing through the directive name-parsing grammar to see whether a colon followed by digits would actually be accepted as a valid name start. I'm now considering whether directives nested inside link children get skipped by the early link-check return, meaning they'd survive as raw directive nodes and get mishandled by remark-rehype's unknown-node fallback, producing stray elements inside anchor tags. I need to check whether the directive check actually runs before the link check in the traversal order, since that would determine if this is a real bug or already handled. Actually, thinking about it more, link text like "Error:ENOENT explained" could plausibly parse ":ENOENT" as a directive, meaning the guard would strip it inside a link — that's a genuine text-dropping regression worth flagging as a real bug, not just a contrived edge case. This nested div breaking out of paragraph/anchor context in HTML is a genuine hydration and layout risk. Checking linkReference nodes too — unresolved references still get descended into properly since their type isn't "link", so that path seems safe. I'm now considering whether block-level restoration might be losing the paragraph wrapper somewhere. Since a bare ::note line previously rendered inside a paragraph with prose styling, converting it to a raw text node at root means it loses the container's typography and margin classes entirely — react-markdown just renders it as unstyled string content. That's a real regression for both single-line notes and full :::note blocks. I'm now thinking the fix should force the restored node into a paragraph structure instead, wrapping the raw slice as text inside a proper paragraph node, so it keeps flow content validity and gets normal spacing. But for container directives with nested markdown like bold text inside, restoring as a literal paragraph means the inner **content** syntax shows up as plain asterisks instead of rendering bold, since before the PR it was truly parsed as one continuous paragraph with the directive markers stripped but inline formatting intact. I'm now deciding to post one inline comment on the link subtree skip issue since it drops text and produces invalid HTML, and considering a second lower-priority comment about the container directive restoration losing paragraph semantics. I need to verify the exact line numbers in the diff so the inline comment lands correctly on the right side of the new file's added hunk. I'm tracing the citation href through to the plugin ordering — since remarkCodexFileCitations runs before remarkNormalizeLinksAndTagInlineCode, absolute file paths should correctly convert into file chips, and any leftover attributes on the resulting link node seem harmless since the hast conversion only reads url/title/children. I'm also confirming that returning early after the conversion prevents visiting the node's children. Now I'm thinking about whether these new citation-derived links introduce new environment-routing behavior for file chips, since ChatMarkdown already has an existing mechanism for resolving which environment handles file-open actions from explicit markdown links. Since the call sites for ChatMarkdown remain unchanged, this looks like it's just reusing the pre-existing chip resolution logic rather than introducing a new pathway worth flagging. I want to check how relative paths in citations, like ones without a leading directory, resolve against the cwd prop that ChatMarkdown expects, and whether appended line-range fragments like #L42 are handled correctly when converted into file chips.

3. git_tools — chat-markdown-file-link

4. git_tools — remarkNormalizeLinksAndTagInlineCode

💭 I'm noticing that remarkNormalizeLinksAndTagInlineCode already uses an insideLink flag pattern that the new function should mirror. I want to check lines 469-560 and the file link chip rendering to verify how #L42 line references and environment routing are handled.

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

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

💭 There's a mismatch between the grep result line and what I see at that line number in the file—the grep may have run against a different revision than the one I'm viewing, so I need to reconcile which commit each tool actually used.

7. git_tools — FILE_LINK|openFile|resolveFilePath|environmentId

💭 Environment routing already looks handled by existing infrastructure, so I'll move on to checking how markdownFileLinkMetaByHref processes #L42 line fragments.

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

9. git_tools — export function resolvePathLinkTarget

💭 I'm noticing that markdownFileLinkMetaByHref only gets populated from actual markdown link hrefs in the raw source text, but the citation directive isn't a markdown link—so its href wouldn't be in that map, yet the test expects the chip to render anyway. I need to check how the anchor rendering handles this case.

10. git_tools — function resolveMarkdownFileLinkMeta|function normalizeMarkdownLinkHrefKey|function extractMarkdownLinkHrefs

💭 I want to check the anchor renderer to see how it looks up meta info, and whether it falls back gracefully when the href isn't in the map.

11. git_tools — markdownFileLinkMetaByHref.get

💭 There's a fallback resolver so citation links still work. Now I need to check whether the href fragment like #L5 for line numbers is properly parsed into a line target, or whether it might leak into the file path used when opening the chip.

12. browse_code — apps/web/src/markdown-links.ts

💭 I'm tracing how special characters like #, ?, and % get escaped when building the destination path, then confirming they decode back correctly when normalizing the link href key—so round-tripping through encode/decode preserves the original path. Now I'm checking react-markdown's URL transform behavior to confirm it falls back to default sanitization when a custom rewrite doesn't apply. I'm focusing on the actual bug: directives nested inside link children get dropped instead of restored, leaving orphaned text and a stray div wrapped inside the anchor. To verify this, I'm looking at mdast-util-to-hast's default unknown-node handler, which either preserves raw text values or wraps unrecognized children in a generic div element—confirming that unhandled directive nodes end up rendered as divs rather than proper inline content, and that remark-rehype passes them through this same fallback path. I'm confirming that mdast-util-directive itself only provides markdown serialization utilities, not hast conversion handlers, and remark-directive's own docs state that unhandled directives render oddly unless explicitly processed—matching the original bug report. I'm also checking whether the traversal that restores directive content actually reaches nested cases like link children, table cells, and inline formatting nodes like emphasis, and now I'm tracing through the root handler in mdast-util-to-hast to see how it processes children when wrapping text nodes at the block level. Looking at how the root handler wraps children with newlines but doesn't paragraph-wrap plain text, I realize a directive like ::note rendered as a bare text node would lose its paragraph styling and margin spacing that the chat CSS relies on—this differs from prior behavior where that line was a proper paragraph. I'm also worried about a bigger regression: when a container directive spans multiple lines with nested markdown, the entire block gets flattened into one literal text node, so inner formatting like bold text no longer renders—checking how common this pattern is in real usage. Thinking through realistic cases, I land on Docusaurus-style admonitions (:::warning ... :::) since assistants often paste this exact syntax when editing docs, and before the change it would render as a styled paragraph with proper bold formatting, but after the change it becomes an unstyled literal block with the markdown syntax showing through. That's a legitimate visual regression worth flagging, likely bundled with the earlier comment about the directive source restoration. I also want to flag two more spots: one where directives nested inside link text never get restored, dropping content silently, which needs an insideLink-style flag similar to the existing inline-code tagging logic; and another where leaf/container directives restored as plain text at the flow level lose their paragraph wrapper and inner formatting, changing spacing and typography compared to before—so I'm considering whether rebuilding those as proper paragraph nodes is the right fix, especially for multi-line container directive content.

13. git_tools — .chat-markdown p

💭 I've confirmed that paragraph elements get vertical margin spacing while bare text nodes lose it, so directive lines render jammed against neighboring content. The markdown clipboard payload re-emits fine from DOM since it works off text nodes directly, but I'm now checking whether restoreDirectiveSource deleting children causes issues for text-type nodes. There's also an interaction with remarkBreaks: since line breaks are converted to <br> at parse time but the restored source text now contains raw newlines instead, multi-line directive content loses its line breaks after rendering, since HTML collapses literal \n characters. This reinforces the original finding. I want to check whether the restore path correctly handles nested content like citations inside container directives, though that's likely an edge case I can set aside, and also whether file.value matches the positions being sliced. I'm now settling on flagging this as a failure since there's a clear regression risk. The main issue: the traversal short-circuits early on link nodes, so directive nodes nested inside link text never get restored, and remark-rehype's fallback renders them as an invalid empty <div> inside the anchor while dropping the literal text — something like a link containing directive syntax as its label. The fix should thread an insideLink flag through the visitor, similar to the existing inline-code normalization approach, so directives inside links get restored to their literal source rather than producing broken nested nodes. Writing the suggested diff... I'm confirming the line range for the inline suggestion spans the visit function definition through its invocation, both being added lines so they're commentable. Then I turn to a second comment about flow directives being restored as plain text, anchored around lines 412-414, where the fix needs to distinguish text handling properly. I'm confirming the two comment ranges don't overlap—420-446 and 409-418 are disjoint, so I can safely suggest both fixes independently. Now I'm drafting the inline suggestion diff for the second comment, matching the file's indentation. Checking the line length for that isFlowDirective condition, I count it fits comfortably under the 100-character limit as a single line, so I'll keep it unbroken for prettier consistency. Writing traversal logic... That handles nested links inside resolved reference links too. These are distinct residual issues from the prior comment, so I'll post the review now with a summary body and mark it as failure.

14. modify_pr — Two residual gaps in the new remarkCodexFileCitations directive handling in apps/web/src/components/ChatMarkdown.tsx. Both are rendering regressions introduced by enabling remark-directive on all chat markdown; details inline.


Agent Credits: 144 credits

Token usage: 22 uncached input · 703,058 cache-read input · 95,778 cache-write input · 16,652 output
Agent Credits may also include non-token charges from external tools such as web research.