Skip to content

fix(web): isolate Codex citation parsing

MacroscopeApp / Macroscope - UI Consistency failed Aug 28, 2026 in 7m 3s

UI Consistency: 1 issue found

apps/web/src/components/ChatMarkdown.tsx — task-list marker offsets now index a different string than their consumer edits.

The new markdownText = replaceCodexFileCitationsWithMarkdownLinks(text) memo is (correctly) used for parsing, and li derives findTaskListMarkerOffset(markdownText, listItemStart). That offset leaves the component through data-task-marker-offset → onTaskListChange, and its only consumer — FilePreviewPanel's RenderedMarkdownSurface — applies it to the untransformed file contents via setMarkdownTaskChecked(currentContents, markerOffset, checked).

Because the citation replacement changes the length of the source (:codex-file-citation{path="src/main.ts" line_range_start="5"} → [main.ts](<src/main.ts#L5>)), every task marker after the first citation is reported at a shifted offset. setMarkdownTaskChecked usually fails its [/] guard and returns the contents unchanged (the checkbox visibly snaps back), and if the shifted offset lands on another [ ] it writes the wrong checkbox to disk. Agent plans saved to the workspace — checklists that contain citation directives — are exactly the markdown this surface renders.

Inline comment posted with a suggested fix (keep the offset-editing surface on the untransformed source, or have the shared transformer expose its replacements so offsets can be mapped back).

No other in-scope UI findings: the web pipeline no longer installs directive grammar into the shared chat parser (all prior directive-grammar regressions I flagged are resolved), file-chip rendering reuses the existing markdown link path unchanged, and environment scoping (threadRef.environmentId ?? environmentId) is untouched.

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.

Verified as no longer applicable

  • remarkDirective / remarkCodexFileCitations were dropped from CHAT_MARKDOWN_REMARK_PLUGINS and ..._WITH_BREAKS; the shared chat grammar is back to its pre-PR rules, which resolves the over-indented list recovery, flow-directive flattening, directive-inside-link, and colon-prose regressions flagged on earlier commits.
  • Citations are converted to ordinary Markdown links before parsing, so they flow through the existing resolveMarkdownFileLinkMeta → MarkdownFileLink chip path with no new environment-scoped action surface. environmentId still resolves from threadRef?.environmentId ?? explicitEnvironmentId.
  • Web copy (resolveAssistantMessageCopyState) now matches the rendered output, closing the earlier web/mobile copy divergence.

Out-of-scope observations (not commented on; files outside this check's include patterns)

  • apps/web/package.json still declares remark-directive: ^4.0.0, but nothing under apps/web/src imports it after the pipeline change — the dependency (and its lockfile entry for the web importer) can be dropped.
  • MarkdownAstNode in ChatMarkdown.tsx retains the value?: string field added for the removed directive plugin; harmless, unused type surface.

Files examined

apps/web/src/components/ChatMarkdown.tsx, apps/web/src/components/ChatMarkdown.test.tsx, apps/web/src/components/chat/MessagesTimeline.tsx, apps/web/src/components/chat/MessagesTimeline.logic.ts, apps/web/src/components/files/FilePreviewPanel.tsx, apps/web/src/components/files/filePreviewMode.ts, apps/web/src/components/pullRequest/PullRequestMarkdown.tsx, apps/web/src/components/chat/ProposedPlanCard.tsx, apps/web/src/markdown-links.ts, plus packages/client-runtime/src/codexFileCitation*.ts for context.


🔍 Agent activity — 23 tool calls

💭 I'm noticing the approach shifted -- instead of adding remarkDirective to ChatMarkdown pipelines, they now pre-transform text via a separate parser that replaces Codex file citations with markdown links, which likely resolves my earlier concerns. I'm checking ChatMarkdown.tsx for regressions, particularly around how the markdown text transformation might alter the original text. I need to verify whether other logic still relies on offsets into the raw text, and whether file link resolution properly passes explicit environment scope rather than triggering actions ambiguously. Mobile is out of scope here, so I'm focusing only on the web path.

1. git_tools — markdownText|\btext\b — rejected: repo.url: required parameter is missing or empty;repo: required parameter is missing or empty

2. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}

💭 Now I want to check the current state of ChatMarkdown.tsx at the head of the branch.

3. git_tools — markdownText|\btext\b

💭 Good, the directive plugin approach was removed in favor of pre-transforming. Now I want to check other regions of ChatMarkdown.tsx to see if there are remaining places using text instead of markdownText, particularly in streaming logic, the copy handler, and file chip rendering, and to confirm unfinished citations stay literal during streaming. I'm also curious whether file chips generated from citation links resolve relative paths against cwd and inherit the active environment routing, since that's existing markdown link behavior that citations would now flow through — so let me look at how ChatMarkdown handles environmentId for file links around lines 1620-1760.

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

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

💭 I'm worried about task marker offsets — since markerOffset is computed against the transformed markdownText rather than the original source text, a citation appearing before a task item could shift offsets and cause consumers of onTaskListChange to apply edits incorrectly.

6. git_tools — onTaskListChange|markerOffset

💭 I'm realizing that the toggle logic operates on the original file contents rather than the rendered markdown text, so if a citation directive shifts character offsets before a task item, the bracket-position check in setMarkdownTaskChecked would fail to match and the toggle could silently do nothing—or worse, mismatch onto an unrelated bracket and corrupt the file. Since this only triggers when the markdown literally contains a codex citation string, though, this feels like a narrow edge case rather than a common regression, so I want to weigh it against more impactful UI issues before flagging it. Streaming behavior seems fine since partial citations stay literal until complete. Copy behavior across ChatMarkdown and the timeline seems mostly consistent, though there's a minor formatting difference between chip copy and message copy that isn't a real bug. The bigger concern is what happens when a citation path doesn't resolve to a recognized file chip — it would fall back to a plain external link, and clicking that could navigate the app away unexpectedly depending on the path format. I want to check what resolveMarkdownFileLinkMeta actually accepts before drawing conclusions.

7. browse_code — In apps/web/src/components (ChatMarkdown and related markdown link helpers), how does resolveMarkdownFileLinkMeta(href, cwd) decide whether a markdown link href is a file link? What href forms does it accept (absolute paths, relative paths, file:// URIs, extensions required?) and what happens when it returns null — how is the link then rendered? Show the implementation.

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

💭 I'm confirming that FilePreviewPanel computes checkbox offsets against the transformed markdown text but applies them to raw file contents, so if a citation directive appears earlier in the file, the wrong marker gets toggled or the edit silently fails. This is a real but narrow data-corruption risk, since it only triggers when a markdown file contains a codex-file-citation directive before a checkbox—plausible for something like a generated progress report, but still low probability overall. Since react-markdown parses the transformed markdownText and the AST positions come from that parsed text, using text instead of markdownText for offset lookups looks like the actual bug introduced by the changed lines, and it falls within the scope of the component under review. But it only manifests when a citation appears in a markdown file that also has task lists, so it's a narrow, low-frequency case. Before flagging it, I want to check a couple of other candidate issues — how inline code and file links are resolved from markdownText, and how the raw HTML parsing path handles offsets, to see which finding is most concrete and worth reporting. There's also a subtler mismatch if the real pipeline includes plugins like list-indentation normalization or raw HTML parsing that run before the citation transform sees the text — an over-indented list item might get treated as an indented code block by the standalone parser, causing a citation inside it to be skipped and rendered as raw directive text instead of being converted. That's a minor edge case worth flagging alongside potential HTML-block handling differences. Another real concern: when a citation resolves to a relative path but there's no cwd available, resolveMarkdownFileLinkTarget returns null, so the link renders as an external target="_blank" anchor pointing at a relative URL — this could open a broken app route in a new tab instead of showing plain text like before, which seems like a genuine UI regression I should verify by checking whether cwd is always supplied.

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

10. browse_code — apps/web/src/components/files/filePreviewMode.ts

💭 There's a guard so a misaligned offset usually just fails silently, but it could coincidentally land on another [ ] marker elsewhere in the document, causing the wrong checkbox to get toggled and saved—a narrow but real data corruption risk, especially once multiple citation-to-link shortenings accumulate offset shifts across occurrences. The real fix is ensuring offsets sent to the host always index the original source, not the transformed text—either skip the citation transform when onTaskListChange is active, or properly map offsets back, though neither is a trivial inline patch. I'll set this aside and move to checking which call sites invoke ChatMarkdown without passing cwd.

11. git_tools — <ChatMarkdown

12. git_tools — ChatMarkdown

💭 I'm noticing the grep results for ChatMarkdown.tsx show different line numbers than what I viewed earlier, which is strange since I used the same tree URL for both. Something seems inconsistent between the two lookups, possibly involving different revisions being referenced.

13. git_tools — ChatMarkdown$

14. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx

💭 I want to check the PullRequestMarkdown call site to see if it passes environmentId when rendering PR body markdown, and whether the citation transform would also apply there—turning file citations in PR descriptions into file chips scoped to the right environment.

15. browse_code — apps/web/src/components/pullRequest/PullRequestMarkdown.tsx

💭 I'm checking MessagesTimeline call sites and noticing that mobile only applies the citation transform for non-user copy text, but web's ChatMarkdown transforms unconditionally—so a user typing citation-like syntax would get transformed too, which seems like a discrepancy worth flagging. The more concrete issue is in ChatMarkdown.tsx: previously the task-list marker offset was computed against the original text, but now both computations use the transformed markdownText, which is self-consistent internally but gets consumed by FilePreviewPanel against the raw file contents. Since the citation replacement text is typically shorter than the original syntax, this shifts the offset, meaning setMarkdownTaskChecked would check the wrong character position and likely silently fail to update the checkbox when citations appear before a task item. I'm weighing how likely markdown files actually contain citation directives alongside task lists — probably uncommon since citations are usually chat-response artifacts rather than saved file content — but it's still a legitimate regression worth flagging. I'm also noting that MarkdownAstNode gained an optional value field in the type definition, which seems like a minor, likely benign addition. Checking whether that new value field is actually used anywhere or if it's leftover dead code, then confirming nothing else in ChatMarkdown reads from props.text besides the one useMemo destructure at line 1631 and its use at 1643. I'm also considering the escaped-citation path in the web test, where a backslash-escaped citation marker might behave differently than expected during streaming.

16. git_tools — MarkdownAstNode|.value

💭 There's a leftover unused field from the removed directive plugin, but it's trivial dead code, not a real UI consistency issue, so I'll skip flagging it. The markdown transform is memoized with a guard, so performance isn't a concern. Now I'm turning to double-check whether the task-list offset issue is actually valid. The onTaskListChange consumer is only in FilePreviewPanel, so this only matters for editing files that literally contain citation directive syntax—unlikely since directives are meant for chat output, not files Codex writes. That path seems low-risk, so I'm shifting attention to the more common streaming scenario in chat: since remark-directive requires a closing brace, a partial citation mid-stream won't match as a directive and will render as literal text momentarily, then snap into a chip once complete—that flashing behavior is likely an accepted tradeoff of the streaming approach. Now I'm considering whether resolveMarkdownFileLinkMeta requires a cwd for relative paths, and what happens in chat surfaces where cwd might not be passed—if it's undefined, a relative citation path would render as a real anchor link pointing to a nonexistent app route instead of staying as plain text like before, which would be a genuine visible regression. I want to check the actual call sites in MessagesTimeline and ProposedPlanCard to see whether cwd is reliably provided.

17. browse_code — apps/web/src/components/chat/ProposedPlanCard.tsx

18. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx

💭 I'm considering that cwd could be undefined, which would make relative citation paths resolve as external anchors instead—though Codex citations usually use absolute paths, so I need to check how the resolver handles path segments with special characters like percent-encoded symbols. Tracing through a specific example: a destination with encoded special characters gets decoded, then split on hash, then checked against a strict character pattern—since asterisks and hashes fail that pattern, it falls through to rendering as a plain external anchor pointing at a broken URL, which is a real edge case worth flagging. More broadly, any citation the parser fails to recognize as a file link ends up as a target="_blank" anchor navigating to a bogus URL at the app's origin, so I'm weighing whether that's actually likely given how Codex typically formats its citations. I think the two strongest findings so far are the offset mismatch between transformed and original text (definite, provable from the code) and this broken-citation-link issue (moderately likely, especially for paths lacking a working directory or containing unusual characters, which the PR's own tests hint at). I'm now checking what happens when the external-link resolver itself returns null for a malformed href. This adds complexity though, and given the guidance to avoid guessing without proof of ownership or semantic equivalence, I should hold off unless I can verify. Let me check where markdownCwd gets defined in MessagesTimeline to see how likely it is to be undefined.

19. git_tools — markdownCwd

💭 markdownCwd could be undefined for threads without a git cwd, which would make relative-path citation links bogus in those cases—but that mirrors existing behavior since agents already emit relative markdown links like this, so it's consistent with pre-existing handling rather than a new problem. I'm also checking whether findTaskListMarkerOffset applies only through FilePreviewPanel, which renders markdown files — and the citation transform only fires when the file itself contains a codex-file-citation marker, which could happen if an agent saves a summary report containing its own citation output. Given the guidance to fail only on clear consistency risks and not on aesthetic nitpicks, I'm leaning toward flagging the offset mismatch as worth noting. Before deciding, I want to make sure this isn't something a prior comment already covered — earlier discussion focused on the remarkDirective approach, which is a different concern. I should also check the existing PR review comments to see if another reviewer already flagged this offset issue, just to avoid redundant noise even though the instruction is really about avoiding my own duplicate comments.

20. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}/comments?per_page=30&page=1

💭 The author consciously switched task-marker offsets to use markdownText, but FilePreviewPanel still applies that offset to the original file contents — a genuine cross-boundary contract break. This is a concrete, provable regression with a clear minimal fix, so it's worth flagging. Actually, I realize plausibility is higher than I thought: agent-generated plans saved via "Save to workspace" are markdown checklists that can contain codex-file-citation directives, and users opening those saved plan files in FilePreviewPanel is exactly the failure scenario. I'll flag this with a suggested fix, likely mapping the marker offset back to the original content rather than the transformed one. I settle on only skipping the citation transform when onTaskListChange is defined, since that dependency is cheap to recompute and preserves file-editing correctness while sacrificing citation linkification in file previews. I confirm findTaskListMarkerOffset's signature and that the data-task-marker-offset attribute is only consumed by that one handler, and I verify ChatMarkdown feeds markdownText directly into ReactMarkdown, so AST offsets stay consistent in that same text space. Checking the leftover value?: string field on MarkdownAstNode, I want to see if it's actually used anywhere or just an unused addition — the .value accesses I found earlier were on ReactNode children, not this type, so I'm looking at whether remarkNormalizeLinksAndTagInlineCode references it before concluding it's dead code that TypeScript wouldn't flag anyway. I'm also noticing apps/web/package.json now adds a remark-directive dependency, but ChatMarkdown no longer imports remarkDirective — that's an unused dependency, though package.json falls outside the file patterns I'm scoped to review, so it's worth a mention in a summary but not a blocking comment.

21. git_tools — remark-directive|remarkDirective

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

💭 I've confirmed remark-directive is an unused leftover dependency in apps/web's package.json — I'll note that in the summary rather than as an inline comment. Now I need to decide where to place the inline comment about the offset issue in ChatMarkdown.tsx, weighing whether line 1986 (the changed line calling findTaskListMarkerOffset) or line 1643 is the better spot. I'm settling on line 1643 since it's part of the diff, proposing a suggestion that guards the markdownText memo so it skips the citation-link replacement when onTaskListChange is present, keeping offsets consistent with the caller's raw text. I'm also checking that onTaskListChange is already destructured earlier in the params, so referencing it here is valid, and weighing whether recomputing the memo on every render (since the callback is inline) is an acceptable tradeoff given the check is cheap. Refining the suggestion to rename the flag to rendersEditableTaskList and updating the comment to explain why offset-based callers must parse the untransformed text, keeping consistent two-space indentation. Now I'm drafting a concise explanation for the review comment itself. Since this represents a genuine correctness bug — offsets computed from transformed markdown text being used to index into the original source, which could corrupt writes if citations appear in editable panels — I'll mark this as a failure rather than a minor note, even weighing the plausibility that such content might be rare in practice. I'm confident enough in the contract violation to post the review as a failure with the fix included.

23. modify_pr — One finding: the task-list marker offsets ChatMarkdown reports are now computed against the citation-transformed source, but the only consumer (FilePreviewPanel) applies them to the original file contents.


Agent Credits: 273 credits

Token usage: 50 uncached input · 1,482,510 cache-read input · 191,785 cache-write input · 26,323 output
Agent Credits may also include non-token charges from external tools such as web research.