Skip to content

fix: match an agent's quote against rendered text - #69

Merged
handlename merged 1 commit into
mainfrom
fix/agent-thread-anchor
Aug 29, 2026
Merged

fix: match an agent's quote against rendered text#69
handlename merged 1 commit into
mainfrom
fix/agent-thread-anchor

Conversation

@handlename

Copy link
Copy Markdown
Owner

Why this change is necessary

A thread the agent opens carries an Anchor Quote, and the page turns that quote into an Anchor so the card can point at a block. The agent quotes the Markdown source, but the page matched that quote against a block's rendered text, so any quote carrying **, a backtick or a ## never matched the very block it was copied from. The thread lost its target, and with the target went the click handler: the card did nothing when clicked, and the block it was written about carried no Comment Indicator at all.

This was not an edge case. Every agent-opened thread in this project's own session transcripts quoted the source with its markup intact — which is exactly what review_reply asks for ("copy it exactly from the document"). The agent was doing as it was told; the comparison was the half that was wrong.

Approach

Reading the Markdown in a quote is the server's half now. The server is what rendered the document, so putting the quote through that same converter is what keeps the two from drifting — a second, approximate parser in the page is precisely the failure this avoids. The page keeps the anchor resolution, because spec-element-N exists only in its own DOM walk and reproducing that numbering in Go is the duplication the anchor rules forbid.

The derived text is computed on every read and never stored, so the quote the agent wrote stays the single source of truth and a sidecar written before this existed resolves its threads too. Markdown only: a diff resolves its quotes against the diff lines in Go, where a Markdown reading of a line of source code would be nothing but a trap.

Review Points

  • The division of labour is the decision worth arguing with: the browser still owns the anchor, the server now owns reading the quote. If that line belongs elsewhere, this is the change to say so on.
  • Two cases were only visible from a real page, and both shape the code: a tag becomes nothing rather than a space, because textContent draws no whitespace from an element and a space costs the match on a code span that ends a sentence; and a table row quoted on its own is handed the delimiter row GFM wants, because a tr is a comment target whose pipes no rendered row contains.
  • A known limit: a pipe inside a code span throws the table row's column count off and the row then does not parse — which leaves the quote where it already was, so the miscount costs nothing new.
  • Out of scope, found while verifying: commentCard() returns the first card for an anchor, so when two threads quote the same block, clicking the second selects the first one's line. That predates this change and is reachable with two human comments on one paragraph.

An agent quotes the Markdown source, but the page matched that quote
against a block's textContent. A quote carrying `**`, a backtick or a
`##` therefore never matched the very block it was copied from: the
thread lost its anchor, and with the anchor went the click handler
that selects the target, so the card did nothing when clicked.

Reading the Markdown in a quote is the server's half now, because the
server is what rendered the document and a second parser in the page
would drift from what is on screen. NormalizeAnchorQuote puts the
quote through the same converter as the document body; the page keeps
the anchor resolution, where the spec-element-N numbering lives, and
compares against the text it is handed.

anchorQuoteText is derived on every read and never stored, so a
sidecar written before it existed resolves its threads too. Markdown
only: a diff resolves its quotes against the diff lines in Go, and a
Markdown reading of a line of source code would be a trap there.

Two cases the browser check caught: a tag becomes nothing rather than
a space, because textContent draws no whitespace from an element; and
a table row quoted on its own is given the delimiter row GFM wants,
since a tr is a comment target whose pipes no rendered row contains.

User request: reproduce the bug first, then fix it. Reproduction had
to be both a headless-browser measurement and a test that go test can
run, so the quote normalization was moved into Go rather than adding
the browser test harness AGENTS.md says this repository does without.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@handlename
handlename merged commit ffca66d into main Aug 29, 2026
2 checks passed
@handlename
handlename deleted the fix/agent-thread-anchor branch August 29, 2026 14:03
@handlename handlename mentioned this pull request Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant