Skip to content

fix(mentions): show the mention after a link that ends at CJK punctuation - #2777

Open
cheng-block wants to merge 9 commits into
block:mainfrom
cheng-block:cheng/fix-mention-after-cjk-link
Open

cheng-block wants to merge 9 commits into
block:mainfrom
cheng-block:cheng/fix-mention-after-cjk-link

Conversation

@cheng-block

@cheng-block cheng-block commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Why

A person tagged right after a bare link and CJK punctuation got notified, but the message showed no mention. In https://x.com,@Honey, the text ,@Honey stayed plain.

The timeline finds mentions with profileMentionParts, which skips text inside a URL. That URL ran to the next whitespace, so @Honey counted as part of it. Edit and Copy use the same parts, so they lost the mention too.

What

protectedTextEnd now ends a bare URL where the rendered link ends, so the name is a mention in the timeline, Edit and Copy.

Edit leaves names inside a reference definition alone, as the timeline and Copy already do. So [ref]: https://x.com/a,@Honey keeps its URL when you edit other text.

How

Risk

Only mentions right after a bare URL change, in the timeline, Edit and Copy.

  • mailto:, nostr: and other scheme:// text now also ends at CJK punctuation. Bare URLs now also end at ", as rendered links do.
  • A URL right after such punctuation, " or > is now kept whole. So 链接:https://x.com/?to=@Honey no longer gets a mention in Edit and Copy.
  • Timeline, Edit and Copy still disagree on a URL glued to a CJK letter (看https://x.com/?to=@Honey), as on main.
  • On main, Edit already turns names in other literal Markdown into mentions (<!-- @Honey -->). fix(messages): keep names in literal Markdown as written when editing #2834 fixes that, and covers definitions too.
  • A long run of unmatched [ already scans slowly on main, about 6 s for 100,000. Such a run after a link and CJK punctuation is now scanned too. Bounding it needs its own PR.

Testing

  • profileMentionParts on 100,000-character bodies (spaces, line breaks, ,, quotes, chained links and prose) took at most about 8 ms each, as on main.
  • The new Edit round trip test fails on 56ca07e6e, where every definition URL gains a profile link. It passes now.
  • It also covers a definition under 101 quote levels, past the Markdown depth limit.

Screenshots

Debug build of this PR: the link ends at example.com, and the name after the Chinese comma shows as a BeeBee mention chip.

Message with Chinese text, an example.com link and a Chinese comma, followed by a BeeBee mention chip, with one reply below.

Debug build of this PR: Edit keeps the mention after the link and Chinese comma.

Composer in Editing message mode with the link, a Chinese comma and an `@BeeBee` mention chip.

In-app check

Verified in the macOS app by Cheng.

Build: this PR at 56ca07e6e, merged locally with my other open PRs onto main at c037057a1, run as a debug app bundle (BUZZ_DEV_VIEWER= just desktop-bundle).

Use a test channel with one of your running agents. An agent only wakes on a real mention, so a reply confirms the tag.

  1. Type 看这个 https://example.com, @, pick the agent, then delete the space before @.
  2. Send. Only https://example.com is a link, followed by , and the mention. The agent replies.
  3. Open Edit on that message. The agent is still a mention. Cancel.
  4. Pick the agent, type <https://example.com/a,@AgentName> by hand, and send. Only the first name is a mention.

Step 1 needs the space until #2768 lands.

Not yet checked in the app: the reference definition case in Edit. A unit test covers it.

buzz-review-completed

cheng-block and others added 4 commits November 6, 2026 01:13
…tion

A person mentioned right after a bare link and fullwidth or CJK
punctuation, as in `https://x.com,@Honey`, was notified, but the
timeline showed `@Honey` as plain text. The mention display still ended
a URL only at whitespace, so the name counted as part of the link, while
rendered links end at CJK punctuation since block#2404. Bare URLs there now
end with the pattern messageLinkParts uses, moved to
relay/message-content.ts exactly as block#2772 moves it. `<url>` and `](url)`
keep CJK punctuation as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
The first commit kept a URL whole only right after `<` or `](`. A
Markdown destination after a line break or space, as in
`[a](\nhttps://x.com/a,@honey)` or `[[a]]( https://x.com/a,@Honey)`,
or after a pasted `]\(`, ended at the CJK punctuation instead. Edit then
turned `@Honey` into a mention link inside the URL, which would change
it on save, and Copy did the same for `]\(`. The timeline shows these as
one link, as before. The check now accepts the `]\(` opener and the
whitespace that messageLinkParts accepts, the same lookbehind block#2772 uses
in emoji.ts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
Two problems in the URL check. A link right after the CJK punctuation
that ends a bare link was not seen as a URL, so in
`https://a.test,https://b.test/?to=@Honey` Edit and Copy turned the
`@Honey` inside the second link into a mention link, and
`https://a.test,nostr:npub1…?to=@Honey` showed a mention inside the
nostr text. A URL can now start after any character a bare URL cannot
contain, which also covers a link right after `:`, `“` or `>`. And the
`<` and `](` lookbehind ran at every whitespace boundary and rescanned
the whole run, so 100,000 spaces took about 5 s. It now runs only where
a URL starts, about 6 ms, as on main. The negative cases move into the
existing table.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
block#2772 now splits emoji text with a case-sensitive regex, so its copy of
BARE_URL_REST spells out A-Z in the host label check instead of relying
on the i flag. This takes the same hunk, so relay/message-content.ts
stays byte for byte the same in both PRs. messageLinkParts and the
mention check both use the i flag, so neither changes what it matches.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
@cheng-block
cheng-block marked this pull request as ready for review November 6, 2026 19:52
@cheng-block
cheng-block requested a review from a team as a code owner November 6, 2026 19:52
@tellaho

tellaho commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

🤖 Design review at 56ca07e6edaba478c9f9cd4224b5a81e9ab9cc93. Intent: show and preserve a mention immediately after a bare link and CJK punctuation.

Must-fix: editing unrelated prose can silently change a reference link's destination. With Mic already a recipient, send:

@Mic look at [ref].

[ref]: https://example.test/a,@Mic

Open Edit, change “look” to “see”, and save. The definition's @Mic is now rebound and serialized as [@Mic](nostr:npub1…) inside the URL. The base preserves that URL. Protect reference-definition destinations from mention rebinding and cover this edit round trip; the new bare-prose behavior can stay.

Evidence: changed URL protection, edit serialization. Reproduced with exact base/head functions and Markdown destination parsing.

Unchecked: native app/IME, keyboard/focus and screen-reader integration, themes, widths, enlarged text and many-agent states. No attached screenshots; no app run or package test suite in this review.

cheng-block and others added 4 commits November 9, 2026 09:35
Opening Edit turned a bound name into a profile link. Now that a bare URL
ends at CJK punctuation, `[ref]: https://x.test/a,@Sam` exposed the name,
and saving put the link inside the definition's URL. Edit now skips names
inside reference definitions, which the Markdown parser finds, as the
renderer and Copy already treat definitions as literal.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
…Ranges

Edit now uses the `definitionRanges` helper that block#2772 adds for emoji,
copied byte for byte, instead of walking the Markdown tree itself. The
helper skips the parse when the body has no `]:`, so the separate early
return for bodies without a bound name goes away.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
`definitionRanges` gave up on Markdown nested past the depth limit, so
Edit turned a name inside a definition under 101 quote levels into a
link inside its URL. Take block#2772's fix, which walks the whole tree, byte
for byte, and cover that case in the Edit round trip.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>
@cheng-block

cheng-block commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Must-fix: editing unrelated prose can silently change a reference link's destination. With Mic already a recipient, send:

@Mic look at [ref].

[ref]: https://example.test/a,@Mic

Open Edit, change “look” to “see”, and save. The definition's @Mic is now rebound and serialized as [@Mic](nostr:npub1…) inside the URL. The base preserves that URL. Protect reference-definition destinations from mention rebinding and cover this edit round trip; the new bare-prose behavior can stay.

Fixed in 1fe736c, with follow-ups through 0af467b. Edit now leaves mentions inside reference definition destinations unbound, using the Markdown parser's definition ranges (the same helper #2772 uses). Your example round trip is a regression test: changing "look" to "see" keeps [ref]: https://example.test/a,@Mic as written. The bare prose case still binds the mention.

@tellaho

tellaho commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

🤖 Could you add a screenshot or short video of the change to the PR? Design uses these for faster reviews.

@cheng-block

Copy link
Copy Markdown
Contributor Author

Could you add a screenshot or short video of the change to the PR? Design uses these for faster reviews.

Added under Screenshots in the description. They come from the debug build I tested.

cheng-block added a commit that referenced this pull request Nov 11, 2026
…#2834)

## Why

Edit turns a name inside literal Markdown (HTML, link titles, reference
definitions) into a mention link, so saving changes text you never
touched. `[ref]: https://example.com "Ask @sam"` is saved as `[ref]:
https://example.com "Ask [@sam](nostr:npub1…)"`. The timeline and Copy
show those names as plain text.

`messageEditText` links every name `profileMentionParts` finds, and that
finder skips only code, links and URLs
(`src/features/messages/profile-mentions.ts:99`). The timeline drops a
name that overlaps a range from `literalRanges`
(`src/features/messages/markdown-preparation.ts:39`). Edit had no such
check.

## What

Edit now leaves a name as written when it sits inside literal Markdown,
using the timeline's ranges. Names outside literal Markdown still become
mention links.

## How

- `messageEditText` parses the text it edits (`sourceContent`, which
keeps attachment Markdown) with `scanMarkdown` and takes
`literalRanges(tree, true)`. `literalRanges` is now exported.
- It walks the ranges in order beside the names, like the timeline, so
it checks each range once.
- No second Markdown heuristic. Copy's own walk is left alone.

## Risk

Low. Only the text Edit loads changes. Recipients stay the same: an
edited message keeps the original event's `p` tags, so a name left as
text drops no one.

- Copy and the timeline read a body over 100,000 characters or 100
levels deep as plain text. Edit has no such guard, but it opens only
your own messages, and Buzz sends at most 32 KiB. Another client can
send more.
- Known limit: the timeline repairs a pasted `[Hi
@sam]\(https://example.com)` into a link and shows no mention. Edit and
Copy read it as written and still link that `@Sam`, same as on `main`.
- Out of scope: bare URL ends (#2777), suggestions after CJK punctuation
(#2768), picked name ends (#2713), `protectedTextEnd` time on unmatched
brackets.
- With #2777 merged in, this also closes the Edit limit #2777 lists:
`[ref]: https://example.com/a,@Sam` stays as written.

## Testing

- No browser cases added or removed. The change is text only.
- A one-off script compared names linked by the timeline, Copy and Edit
on 55 bodies. All three agree on 54; the other is the known limit. On
`main`, Edit linked more than Copy on 20.
- The 8 new Edit test texts pass through the composer's load and save
helpers unchanged.
- `messageEditText` runs once when Edit opens (Node and jsdom, median of
five runs). A body that alternates names and code spans took about 72 ms
at 16,000 characters and 270 ms at 32 KiB before the in-order check.
After it, 8 ms and 16 ms.

## Screenshots

Debug build of this PR: Edit shows the real mention as a chip and leaves
`@BeeBee` inside the span title as plain text.

<img width="760" alt="Composer in Editing message mode with Hi `@BeeBee`
on line one and a literal span tag with title \"`@BeeBee`\" below."
src="https://github.com/user-attachments/assets/26434d42-f342-4007-a9b3-382d1627742e"
/>

After saving, the message keeps the real mention and the literal span
exactly as written.

<img width="744" alt="Edited message reading Hello with a BeeBee mention
chip and the literal span tag below."
src="https://github.com/user-attachments/assets/4abdf2a0-6162-482c-84ca-c90f24f445a0"
/>

## In-app check

Verified in the macOS app by Cheng.

Build: this PR at `bcc62911e`, merged locally with my other open PRs
onto `main` at `c037057a1`, run as a debug app bundle. Later commits
only merge `main`.

Use a test channel with a person or agent in it. The steps call them
Name.

1. Type `Hi @`, pick Name, press Shift+Enter twice, then paste `<span
title="@name">hi</span>` with their exact name. Send.
2. The message shows `Hi`, the mention, then `<span
title="@name">hi</span>` as typed.
3. Open Edit. The second line still reads `<span
title="@name">hi</span>`. On `main`, Edit put a profile link inside the
quotes.
4. Change `Hi` to `Hello` and save. The message shows `Hello`, the
mention, and the same `<span>` line.

buzz-review-completed

---------

Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
main's literal Markdown ranges (block#2834) already skip reference
definitions when an edit turns names back into links, so take main's
messageEditText and keep both test sets.

Signed-off-by: Cheng Peng <cheng@block.xyz>
Co-authored-by: Cursor <cursoragent@cursor.com>

This branch has not been deployed

No deployments
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.

2 participants