Repository navigation
fix(mentions): keep @mentions tagged when punctuation or quotes follow the name - #2713
Conversation
…w the name Typing a name in full and then punctuation or a quote lost the mention. There were two causes. The composer dropped a selected or pending mention when a quote, an apostrophe or another non-word character followed it. It ended a name only at a short list of characters, while the timeline ends one at any character outside a name, so `@Honey’s` or `"@honey"` sent no tag. The boundary now ends a name at the next character that is not a letter, mark, digit or underscore. It runs on every document change, so it also covers characters that arrive without a keydown, such as macOS smart-quote substitution. Only Space selected the exact match for a name typed in full, so `@Honey,` or `@Honey'` stayed plain text. Closing punctuation (`. , ! ? ; : )` and the quotes `' ’ " ”`) now selects the provider-verified exact match the way Space does, and the typed character follows the mention in place of the space. It holds a pending mention the way Space does. The same rules apply: when a longer known name continues with the character (`Pat O'Brien` for `Pat O`, `Wes’s Honey` for `Wes`), the character is typed into the query instead. Teams and evidence checks that block Space block these keys too. Option may type curly quotes on a Mac. Ctrl and Cmd remain shortcuts, and the character stays literal in code. While editing a message, it follows the profile link a typed name becomes. Document the rule in mentions README §5 and the help doc. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: mtoohey <[email protected]>
Follow-up to review of 22e868d. Restore pendingMentionQuery to main. Treating a trailing quote as prose only skipped a pending chip in the composer that would fail anyway, but the workflow editor uses the same check to keep its suggestion list open, so the list closed after `@Pat O'` and reopened on the next letter. State the mentions README §5 rule precisely. Any longer known name that continues the query blocks every closing character, as it blocks Space, whatever character comes next in that name: `,` after `Wes` stays in the query while `Wes Smith` is known. Closing punctuation SHOULD select, and the §5 heading and the Abstract name it beside Space. Reword the mentionBoundary comment. The timeline does not count a combining mark as part of a name; only the composer does. Set up the smart-quote test through the path where macOS substitutes: a picked chip, Backspace, a native `'` keystroke, then a DOM-only change to `’`. Add the loading-race case: `'` holds `@Pat O` while a member name and the directory are loading, `Brien` is typed, and the chip extends to `Pat O'Brien` when the directory reports it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: mtoohey <[email protected]>
Follow-up to review of caf9e0c. Only a mention now lets Option type its terminator. 22e868d let Shift or Option type any provider's terminator except Space, so Option+`:` after an exact shortcode converted it to the emoji, which main never did. Option still closes a mention with any closing punctuation, since Mac layouts other than US type these characters with Option. Emoji and channel acceptance now matches main for every modifier. Bring docs/agents.md and docs/plugin-architecture.md in line with mentions README §5 and point to it. Both still described Space-only selection. agents.md also said a longer name blocks selection only when it continues with a space. Any known longer name that continues the query blocks selection, whatever separator follows. The modified-keys sentence in plugin-architecture.md now names its exceptions. Add a test that Option+`:` and Option+Shift+`:` after `:-1` leave the colon to the editor and that a plain closing colon still converts. It fails before this change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: mtoohey <[email protected]>
a155810 to
2612d5c
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, with independent Rocket, Mantis and Groot lanes.
Reviewed 2612d5cb29cd6853155651bae8cebedc92c7f032 against f9ce9dfc4ae8ea69ad82c6dcd270e570077ce6de. No actionable correctness or security findings in this revision. The punctuation path reuses the existing exact-name/team/evidence checks, pending-resolution lifecycle and recipient admission; edits continue to add identity links without new notification recipients.
Optional documentation cleanup: the pending-chip descriptions in docs/agents.md around lines 794 and 894 still name only Space; “Space or closing punctuation” would match the updated README §5.
Source-only review: tests were inspected, not run; no app, native/WebKit/IME or end-to-end behavior was exercised. The PR’s five stated known gaps remain explicit limitations. Public PR text and one demo thumbnail were inspected; the full video was not visually reviewed. Its thumbnail labels the after build a155810, so the recording does not establish validation of this reviewed head. This COMMENT is not approval or runtime acceptance.
kazemek21
left a comment
There was a problem hiding this comment.
🤖 I reviewed the PR and went through the Nexus review-tracker notes. I didn't see any approval-blocking issues in this change.
Posted by Codex (gpt-6.1-sol) on behalf of kylez
Integrate the mainline sidebar geometry correction while retaining the dialog focus changes. Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@feedback.communities.buzz.xyz> * origin/main: fix(sidebar): give Drafts & sent the page rows' icon box (#2946) fix(sidebar): drop the draft pencil from channel and DM rows (#2943) feat(channels): create #general for the owner of an empty community (#2904) feat(agents): move local teams to the relay and show sync state (#2846) Preserve legacy emoji avatars during agent import (#2849) feat(communities): skip join consent covered by a Builderlab account (#2912) fix(mentions): keep @mentions tagged when punctuation or quotes follow the name (#2713) fix(sidebar): align Drafts & sent row with channel rows (#2733) test(navigation): split the plugin-settings return-path journey (#2800) fix(messages): let Escape close Forward and return focus to ⋯ (#2807) fix(messages): say older messages can load once Buzz reconnects, instead of an inert Load older button (#2810) fix(navigation): say when the open page's plugin is turned off, and open it in Manage plugins (#2817) fix(settings): reopen plugin details on Back and Forward (#2825) fix(agents): show agent card pictures without a backdrop (#2828) feat(agents): team instructions history and agent/team config tools (#2848) test(plugin-manager): use an immutable git socket fixture (#2665) fix(attention): show the attention toggle when an agent has nothing yet (#2907) Signed-off-by: Taylor Ho <[email protected]>
… colon ends a name Follow-up to e21a524, fixing the CI failure on PR 2581 (run 42589272929, JavaScript 1/6 and both Flake check jobs). The two `nostr:npub` cases of "settles a pending mention of the %s of the owner's agent from another community into it once their agents here load" failed at `findByText("Loading recipients…")` in every run. They typed `@nostr:npub1…` key by key. Since #2713 (cae3922, in this branch's base after the last rebase), `:` is a mention terminator: typed straight after a query it selects the exact name, or holds the query as a pending mention while the chooser is undecided (mentions README section 5, `mentionTerminator`). With the directory still searching `nostr`, the colon committed `@nostr` as a pending chip, and `:npub1…` was typed as prose after it (the CI DOM shows a chip with `data-source="@nostr"` and the key as trailing text). No key query ever reached the chooser, so nothing was held. The hex and npub cases have no colon and passed. This is the documented product rule, not a defect in the exact-key hold (bce7123, 52794e1): a `nostr:` key reaches the chooser pasted, as a 63-character key does in practice, and the hold then applies to the whole query. The tests now enter the key as a user would: `enterKey` types `@` and the hex key or npub, and types `@` then pastes a `nostr:` key. The assertions are unchanged. The "Maximum update depth exceeded" warning that CI printed for these two cases is not a render loop. Recording commits with a React DevTools hook showed one commit per keystroke; the warning comes from React's nested-passive-update counter, which `setPasteTooLong(false)` in the composer's draft effect (92d4c36, on main) increments on every keystroke whose passive flush runs while the next keystroke's update is already pending, so that 60-odd keys typed after a pending chip cross its limit of 50. The paste removes the long typed run, and the warning no longer appears. The effect itself is main's code and is left alone. Fail-before: with the colon typed, both cases fail as in CI; with the paste, all 6 variants pass. Validation (load average about 5 to 9 on 16 cores): - `vitest run src/bundled/mentions MessageComposer.test.tsx SoloDraft.test.tsx person-match.test.ts relay/mentions.test.ts`: 11 files, 1063 tests passed, with no "Maximum update depth" line on stderr. - The two `nostr:npub` cases passed in 3 more runs each, with no warning on stderr. - `tsc --noEmit` passed. `biome check` on the test file and `git diff --check` passed. - Not run: the browser journeys (mention-rules, completion-work, typeahead) and a check in the app. Signed-off-by: Matt Toohey <[email protected]> Co-Authored-By: Claude Fable 5.1 <[email protected]>
… colon ends a name Follow-up to e21a524, fixing the CI failure on PR 2581 (run 42589272929, JavaScript 1/6 and both Flake check jobs). The two `nostr:npub` cases of "settles a pending mention of the %s of the owner's agent from another community into it once their agents here load" failed at `findByText("Loading recipients…")` in every run. They typed `@nostr:npub1…` key by key. Since #2713 (cae3922, in this branch's base after the last rebase), `:` is a mention terminator: typed straight after a query it selects the exact name, or holds the query as a pending mention while the chooser is undecided (mentions README section 5, `mentionTerminator`). With the directory still searching `nostr`, the colon committed `@nostr` as a pending chip, and `:npub1…` was typed as prose after it (the CI DOM shows a chip with `data-source="@nostr"` and the key as trailing text). No key query ever reached the chooser, so nothing was held. The hex and npub cases have no colon and passed. This is the documented product rule, not a defect in the exact-key hold (bce7123, 52794e1): a `nostr:` key reaches the chooser pasted, as a 63-character key does in practice, and the hold then applies to the whole query. The tests now enter the key as a user would: `enterKey` types `@` and the hex key or npub, and types `@` then pastes a `nostr:` key. The assertions are unchanged. The "Maximum update depth exceeded" warning that CI printed for these two cases is not a render loop. Recording commits with a React DevTools hook showed one commit per keystroke; the warning comes from React's nested-passive-update counter, which `setPasteTooLong(false)` in the composer's draft effect (92d4c36, on main) increments on every keystroke whose passive flush runs while the next keystroke's update is already pending, so that 60-odd keys typed after a pending chip cross its limit of 50. The paste removes the long typed run, and the warning no longer appears. The effect itself is main's code and is left alone. Fail-before: with the colon typed, both cases fail as in CI; with the paste, all 6 variants pass. Validation (load average about 5 to 9 on 16 cores): - `vitest run src/bundled/mentions MessageComposer.test.tsx SoloDraft.test.tsx person-match.test.ts relay/mentions.test.ts`: 11 files, 1063 tests passed, with no "Maximum update depth" line on stderr. - The two `nostr:npub` cases passed in 3 more runs each, with no warning on stderr. - `tsc --noEmit` passed. `biome check` on the test file and `git diff --check` passed. - Not run: the browser journeys (mention-rules, completion-work, typeahead) and a check in the app. Signed-off-by: Matt Toohey <[email protected]> Co-Authored-By: Claude Fable 5.1 <[email protected]>
… colon ends a name Follow-up to e21a524, fixing the CI failure on PR 2581 (run 42589272929, JavaScript 1/6 and both Flake check jobs). The two `nostr:npub` cases of "settles a pending mention of the %s of the owner's agent from another community into it once their agents here load" failed at `findByText("Loading recipients…")` in every run. They typed `@nostr:npub1…` key by key. Since #2713 (cae3922, in this branch's base after the last rebase), `:` is a mention terminator: typed straight after a query it selects the exact name, or holds the query as a pending mention while the chooser is undecided (mentions README section 5, `mentionTerminator`). With the directory still searching `nostr`, the colon committed `@nostr` as a pending chip, and `:npub1…` was typed as prose after it (the CI DOM shows a chip with `data-source="@nostr"` and the key as trailing text). No key query ever reached the chooser, so nothing was held. The hex and npub cases have no colon and passed. This is the documented product rule, not a defect in the exact-key hold (bce7123, 52794e1): a `nostr:` key reaches the chooser pasted, as a 63-character key does in practice, and the hold then applies to the whole query. The tests now enter the key as a user would: `enterKey` types `@` and the hex key or npub, and types `@` then pastes a `nostr:` key. The assertions are unchanged. The "Maximum update depth exceeded" warning that CI printed for these two cases is not a render loop. Recording commits with a React DevTools hook showed one commit per keystroke; the warning comes from React's nested-passive-update counter, which `setPasteTooLong(false)` in the composer's draft effect (92d4c36, on main) increments on every keystroke whose passive flush runs while the next keystroke's update is already pending, so that 60-odd keys typed after a pending chip cross its limit of 50. The paste removes the long typed run, and the warning no longer appears. The effect itself is main's code and is left alone. Fail-before: with the colon typed, both cases fail as in CI; with the paste, all 6 variants pass. Validation (load average about 5 to 9 on 16 cores): - `vitest run src/bundled/mentions MessageComposer.test.tsx SoloDraft.test.tsx person-match.test.ts relay/mentions.test.ts`: 11 files, 1063 tests passed, with no "Maximum update depth" line on stderr. - The two `nostr:npub` cases passed in 3 more runs each, with no warning on stderr. - `tsc --noEmit` passed. `biome check` on the test file and `git diff --check` passed. - Not run: the browser journeys (mention-rules, completion-work, typeahead) and a check in the app. Signed-off-by: Matt Toohey <[email protected]> Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
…#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 <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Cursor <[email protected]>
… colon ends a name Follow-up to e21a524, fixing the CI failure on PR 2581 (run 42589272929, JavaScript 1/6 and both Flake check jobs). The two `nostr:npub` cases of "settles a pending mention of the %s of the owner's agent from another community into it once their agents here load" failed at `findByText("Loading recipients…")` in every run. They typed `@nostr:npub1…` key by key. Since #2713 (cae3922, in this branch's base after the last rebase), `:` is a mention terminator: typed straight after a query it selects the exact name, or holds the query as a pending mention while the chooser is undecided (mentions README section 5, `mentionTerminator`). With the directory still searching `nostr`, the colon committed `@nostr` as a pending chip, and `:npub1…` was typed as prose after it (the CI DOM shows a chip with `data-source="@nostr"` and the key as trailing text). No key query ever reached the chooser, so nothing was held. The hex and npub cases have no colon and passed. This is the documented product rule, not a defect in the exact-key hold (bce7123, 52794e1): a `nostr:` key reaches the chooser pasted, as a 63-character key does in practice, and the hold then applies to the whole query. The tests now enter the key as a user would: `enterKey` types `@` and the hex key or npub, and types `@` then pastes a `nostr:` key. The assertions are unchanged. The "Maximum update depth exceeded" warning that CI printed for these two cases is not a render loop. Recording commits with a React DevTools hook showed one commit per keystroke; the warning comes from React's nested-passive-update counter, which `setPasteTooLong(false)` in the composer's draft effect (92d4c36, on main) increments on every keystroke whose passive flush runs while the next keystroke's update is already pending, so that 60-odd keys typed after a pending chip cross its limit of 50. The paste removes the long typed run, and the warning no longer appears. The effect itself is main's code and is left alone. Fail-before: with the colon typed, both cases fail as in CI; with the paste, all 6 variants pass. Validation (load average about 5 to 9 on 16 cores): - `vitest run src/bundled/mentions MessageComposer.test.tsx SoloDraft.test.tsx person-match.test.ts relay/mentions.test.ts`: 11 files, 1063 tests passed, with no "Maximum update depth" line on stderr. - The two `nostr:npub` cases passed in 3 more runs each, with no warning on stderr. - `tsc --noEmit` passed. `biome check` on the test file and `git diff --check` passed. - Not run: the browser journeys (mention-rules, completion-work, typeahead) and a check in the app. Signed-off-by: Matt Toohey <[email protected]> Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
… colon ends a name Follow-up to e21a524, fixing the CI failure on PR 2581 (run 42589272929, JavaScript 1/6 and both Flake check jobs). The two `nostr:npub` cases of "settles a pending mention of the %s of the owner's agent from another community into it once their agents here load" failed at `findByText("Loading recipients…")` in every run. They typed `@nostr:npub1…` key by key. Since #2713 (cae3922, in this branch's base after the last rebase), `:` is a mention terminator: typed straight after a query it selects the exact name, or holds the query as a pending mention while the chooser is undecided (mentions README section 5, `mentionTerminator`). With the directory still searching `nostr`, the colon committed `@nostr` as a pending chip, and `:npub1…` was typed as prose after it (the CI DOM shows a chip with `data-source="@nostr"` and the key as trailing text). No key query ever reached the chooser, so nothing was held. The hex and npub cases have no colon and passed. This is the documented product rule, not a defect in the exact-key hold (bce7123, 52794e1): a `nostr:` key reaches the chooser pasted, as a 63-character key does in practice, and the hold then applies to the whole query. The tests now enter the key as a user would: `enterKey` types `@` and the hex key or npub, and types `@` then pastes a `nostr:` key. The assertions are unchanged. The "Maximum update depth exceeded" warning that CI printed for these two cases is not a render loop. Recording commits with a React DevTools hook showed one commit per keystroke; the warning comes from React's nested-passive-update counter, which `setPasteTooLong(false)` in the composer's draft effect (92d4c36, on main) increments on every keystroke whose passive flush runs while the next keystroke's update is already pending, so that 60-odd keys typed after a pending chip cross its limit of 50. The paste removes the long typed run, and the warning no longer appears. The effect itself is main's code and is left alone. Fail-before: with the colon typed, both cases fail as in CI; with the paste, all 6 variants pass. Validation (load average about 5 to 9 on 16 cores): - `vitest run src/bundled/mentions MessageComposer.test.tsx SoloDraft.test.tsx person-match.test.ts relay/mentions.test.ts`: 11 files, 1063 tests passed, with no "Maximum update depth" line on stderr. - The two `nostr:npub` cases passed in 3 more runs each, with no warning on stderr. - `tsc --noEmit` passed. `biome check` on the test file and `git diff --check` passed. - Not run: the browser journeys (mention-rules, completion-work, typeahead) and a check in the app. Signed-off-by: Matt Toohey <[email protected]> Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
… colon ends a name Follow-up to e21a524, fixing the CI failure on PR 2581 (run 42589272929, JavaScript 1/6 and both Flake check jobs). The two `nostr:npub` cases of "settles a pending mention of the %s of the owner's agent from another community into it once their agents here load" failed at `findByText("Loading recipients…")` in every run. They typed `@nostr:npub1…` key by key. Since #2713 (cae3922, in this branch's base after the last rebase), `:` is a mention terminator: typed straight after a query it selects the exact name, or holds the query as a pending mention while the chooser is undecided (mentions README section 5, `mentionTerminator`). With the directory still searching `nostr`, the colon committed `@nostr` as a pending chip, and `:npub1…` was typed as prose after it (the CI DOM shows a chip with `data-source="@nostr"` and the key as trailing text). No key query ever reached the chooser, so nothing was held. The hex and npub cases have no colon and passed. This is the documented product rule, not a defect in the exact-key hold (bce7123, 52794e1): a `nostr:` key reaches the chooser pasted, as a 63-character key does in practice, and the hold then applies to the whole query. The tests now enter the key as a user would: `enterKey` types `@` and the hex key or npub, and types `@` then pastes a `nostr:` key. The assertions are unchanged. The "Maximum update depth exceeded" warning that CI printed for these two cases is not a render loop. Recording commits with a React DevTools hook showed one commit per keystroke; the warning comes from React's nested-passive-update counter, which `setPasteTooLong(false)` in the composer's draft effect (92d4c36, on main) increments on every keystroke whose passive flush runs while the next keystroke's update is already pending, so that 60-odd keys typed after a pending chip cross its limit of 50. The paste removes the long typed run, and the warning no longer appears. The effect itself is main's code and is left alone. Fail-before: with the colon typed, both cases fail as in CI; with the paste, all 6 variants pass. Validation (load average about 5 to 9 on 16 cores): - `vitest run src/bundled/mentions MessageComposer.test.tsx SoloDraft.test.tsx person-match.test.ts relay/mentions.test.ts`: 11 files, 1063 tests passed, with no "Maximum update depth" line on stderr. - The two `nostr:npub` cases passed in 3 more runs each, with no warning on stderr. - `tsc --noEmit` passed. `biome check` on the test file and `git diff --check` passed. - Not run: the browser journeys (mention-rules, completion-work, typeahead) and a check in the app. Signed-off-by: Matt Toohey <[email protected]> Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
… colon ends a name Follow-up to e21a524, fixing the CI failure on PR 2581 (run 42589272929, JavaScript 1/6 and both Flake check jobs). The two `nostr:npub` cases of "settles a pending mention of the %s of the owner's agent from another community into it once their agents here load" failed at `findByText("Loading recipients…")` in every run. They typed `@nostr:npub1…` key by key. Since #2713 (cae3922, in this branch's base after the last rebase), `:` is a mention terminator: typed straight after a query it selects the exact name, or holds the query as a pending mention while the chooser is undecided (mentions README section 5, `mentionTerminator`). With the directory still searching `nostr`, the colon committed `@nostr` as a pending chip, and `:npub1…` was typed as prose after it (the CI DOM shows a chip with `data-source="@nostr"` and the key as trailing text). No key query ever reached the chooser, so nothing was held. The hex and npub cases have no colon and passed. This is the documented product rule, not a defect in the exact-key hold (bce7123, 52794e1): a `nostr:` key reaches the chooser pasted, as a 63-character key does in practice, and the hold then applies to the whole query. The tests now enter the key as a user would: `enterKey` types `@` and the hex key or npub, and types `@` then pastes a `nostr:` key. The assertions are unchanged. The "Maximum update depth exceeded" warning that CI printed for these two cases is not a render loop. Recording commits with a React DevTools hook showed one commit per keystroke; the warning comes from React's nested-passive-update counter, which `setPasteTooLong(false)` in the composer's draft effect (92d4c36, on main) increments on every keystroke whose passive flush runs while the next keystroke's update is already pending, so that 60-odd keys typed after a pending chip cross its limit of 50. The paste removes the long typed run, and the warning no longer appears. The effect itself is main's code and is left alone. Fail-before: with the colon typed, both cases fail as in CI; with the paste, all 6 variants pass. Validation (load average about 5 to 9 on 16 cores): - `vitest run src/bundled/mentions MessageComposer.test.tsx SoloDraft.test.tsx person-match.test.ts relay/mentions.test.ts`: 11 files, 1063 tests passed, with no "Maximum update depth" line on stderr. - The two `nostr:npub` cases passed in 3 more runs each, with no warning on stderr. - `tsc --noEmit` passed. `biome check` on the test file and `git diff --check` passed. - Not run: the browser journeys (mention-rules, completion-work, typeahead) and a check in the app. Signed-off-by: Matt Toohey <[email protected]> Co-Authored-By: Claude Fable 5.1 <[email protected]> Signed-off-by: Matt Toohey <[email protected]>
Summary
Typing
@SomeAgent's,@SomeAgent"or@SomeAgent,sent the message without aptag, so the agent was never woken and nothing warned the user. Two independent composer causes were responsible; both are fixed here.SomeAgent,matched no one, so nothing was tagged.mentionBoundaryaccepted, . ! ? : ; )as the character following a name but not'’"”, so a quote right after a chip turned it back into plain text and dropped the recipient.Nothing on the relay or Rust side reads
@namesfrom text; agents wake only onptags, so both fixes are in the frontend composer.Changes
mention-draft.ts): a mention now ends at the first character that isn't a letter, combining mark, digit or_, matching how the timeline renders mentions. The check runs on every edit, so it also covers macOS smart-quote substitution.codePointAtavoids treating half a surrogate pair as a boundary.ComposerCompletions.tsx,MentionCompletion.tsx,MessageComposer.tsx,EditableInput.tsx,composer-dom.ts):, . ! ? ; : )and' ’ " ”typed straight after an exact, unique name select it the way Space does, and the character stays after the mention (@Name,rather than@Name ,). They also hold a pending mention while names are still loading.Pat OwhilePat O'Brienis known;WeswhileWes Smithis known) blocks every one of these keys; team and evidence rules that block Space block them too; Ctrl/Cmd stay shortcuts; the typed key stays literal inside code spans. Option/Alt is allowed only for mentions so Mac curly quotes work, and emoji completion is unchanged frommain.docs/agents.md,docs/plugin-architecture.mdanddocs/buzz-help.md.Before / after
Left:
main(fe3dd19). Right: this branch. The same keystrokes go to both panes in lockstep. Each pane shows a chip badge under the composer and a log of sent events with theirp-tag count (green = agent tagged, red = not tagged). Onmainthe agent is tagged in 1 of 6 sends (only the Space control); with the fix, 6 of 6.staged-image-e75d4781-d5df-471d-8a3e-653eeabedb9a.mp4
main@Other Honey, Space,please look's@Other Honey's turn’s@Other Honey+ Option+Shift+],s turn,@Other Honey, please look"@Other Honey" said hi''s turnRecorded in headless Chromium against
tests/fixtures/mentions.html?identity-names; the native app, WebKit and IME were not recorded.Test plan
mention-draft.test.ts,EditableInput.test.tsx,MessageComposer.test.tsx,src/bundled/mentionsPat O/Pat O'Brien,Wes/Wes Smith), loading-race case where'holds a pending mention and the directory later reportsPat O'Brien:after an emoji shortcode no longer converts (matchesmain)tsc --noEmitand Biome cleanKnown gaps (unchanged)
@("@Name) still doesn't open the mention list; that's part of the portable mention contract (README §2 /queryfixtures) and needs sign-off.@Wes,still doesn't select whileWes Smithis known, the same as Space.') rather than curly; both are treated the same.profileMentionPartscallmentionBoundary.🤖 Generated with Claude Code