Skip to content

fix(mentions): keep @mentions tagged when punctuation or quotes follow the name - #2713

Merged
matt2e merged 3 commits into
mainfrom
at-mention-agents-breaks-with-punctuation
Nov 7, 2026
Merged

matt2e merged 3 commits into
mainfrom
at-mention-agents-breaks-with-punctuation

Conversation

@matt2e

@matt2e matt2e commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Typing @SomeAgent's, @SomeAgent" or @SomeAgent, sent the message without a p tag, so the agent was never woken and nothing warned the user. Two independent composer causes were responsible; both are fixed here.

  1. Only Space selected a fully typed name. Every other key, including closing punctuation, was typed into the search text. SomeAgent, matched no one, so nothing was tagged.
  2. Quotes weren't in the boundary allowlist. After a mention was picked from the list, mentionBoundary accepted , . ! ? : ; ) 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 @names from text; agents wake only on p tags, so both fixes are in the frontend composer.

Changes

  • Where a name ends (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. codePointAt avoids treating half a surrogate pair as a boundary.
  • Closing punctuation selects (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.
  • Same guards as Space: a longer known name that continues the query (Pat O while Pat O'Brien is known; Wes while Wes Smith is 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 from main.
  • Docs: mentions README §5 now says closing punctuation SHOULD select under the same rule as Space (no contract version bump; §5 changes haven't bumped it before, and no fixture covers which key commits). Pointer sentences in docs/agents.md, docs/plugin-architecture.md and docs/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 their p-tag count (green = agent tagged, red = not tagged). On main the 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
Step Keys main this branch
Control @Other Honey, Space, please look tagged tagged
Typed 's @Other Honey's turn not tagged tagged
Typed ’s @Other Honey + Option+Shift+], s turn not tagged tagged
Typed , @Other Honey, please look not tagged tagged
Typed " @Other Honey" said hi not tagged tagged
Picked, then ' click the row, Backspace, 's turn not tagged tagged

Recorded in headless Chromium against tests/fixtures/mentions.html?identity-names; the native app, WebKit and IME were not recorded.

Test plan

  • New regression tests for both causes (fail with the fix reverted): mention-draft.test.ts, EditableInput.test.tsx, MessageComposer.test.tsx, src/bundled/mentions
  • Longer-name guard (Pat O / Pat O'Brien, Wes / Wes Smith), loading-race case where ' holds a pending mention and the directory later reports Pat O'Brien
  • Option+: after an emoji shortcode no longer converts (matches main)
  • macOS smart-quote substitution after a picked chip keeps the recipient
  • 1,336 tests across the composer, mention and conversation suites; tsc --noEmit and Biome clean
  • Browser repro against the real composer passes on this branch

Known gaps (unchanged)

  • A quote before the @ ("@Name) still doesn't open the mention list; that's part of the portable mention contract (README §2 / query fixtures) and needs sign-off.
  • @Wes, still doesn't select while Wes Smith is known, the same as Space.
  • Keys that arrive without a normal keydown (dead-key layouts such as US-International, IME, the character viewer) don't select a typed name. A picked chip keeps its tag in all of these cases.
  • With smart quotes on, the selecting apostrophe is inserted straight (') rather than curly; both are treated the same.
  • The composer counts combining marks as part of a name; the timeline doesn't. Follow-up: have profileMentionParts call mentionBoundary.

🤖 Generated with Claude Code

@matt2e
matt2e marked this pull request as ready for review November 6, 2026 01:07
@matt2e
matt2e requested a review from a team as a code owner November 6, 2026 01:07
matt2e and others added 3 commits November 6, 2026 16:34
…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]>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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

@matt2e
matt2e merged commit cae3922 into main Nov 7, 2026
62 checks passed
@matt2e
matt2e deleted the at-mention-agents-breaks-with-punctuation branch November 7, 2026 00:19
tellaho added a commit that referenced this pull request Nov 7, 2026
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]>
matt2e added a commit that referenced this pull request Nov 9, 2026
… 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]>
matt2e added a commit that referenced this pull request Nov 9, 2026
… 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]>
matt2e added a commit that referenced this pull request Nov 11, 2026
… 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]>
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 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Cursor <[email protected]>
matt2e added a commit that referenced this pull request Nov 12, 2026
… 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]>
matt2e added a commit that referenced this pull request Nov 12, 2026
… 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]>
matt2e added a commit that referenced this pull request Nov 13, 2026
… 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]>
matt2e added a commit that referenced this pull request Nov 14, 2026
… 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]>
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.

3 participants