Skip to content

fix(mentions): resolve fast-typed @name mentions with a pending chip - #1623

Open
matt2e wants to merge 45 commits into
mainfrom
manual-agent-tagging-buggy
Open

matt2e wants to merge 45 commits into
mainfrom
manual-agent-tagging-buggy

Conversation

@matt2e

@matt2e matt2e commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Typing @name and pressing Space before the mention chooser has decided (members, names or agents still loading, or the debounced directory search still waiting on the relay) silently sends @name as plain text: no recipient, no p tag. By spec nothing downstream re-parses text, so the mention is lost.

What the branch does:

  • Pending-mention chip. Space on a name the chooser cannot yet decide inserts a chip with a spinner in the @ slot instead of a literal space. It resolves in the background under the same rule Space uses once the full choice set is known, then becomes an ordinary recipient chip or reverts to plain text (failed or truncated search, namesake, team block, DM outsider, abort). Send waits for in-flight resolutions, bounded at 8 s; if a chip the send waited for reverts to text, the draft stays with a notice (@mira could not be matched to a person or agent. Send again to send it as text.) and the next send goes out as text, so a name the user saw held never leaves silently unaddressed. Wire format is unchanged: a settled chip is a normal recipient token, and a pending chip is never a tag, a reference or a persisted draft. The transparent @ keeps its advance, so settling shifts no layout.
  • Two root-cause fixes to the Space rule. An identity's own longer alias (Example / Example Agent) no longer blocks its exact match, and a member whose profile read completed with no kind 0 is recorded as known-unnamed, so one profile-less member no longer disables fast local selection for the whole channel.

Demo

before-after-realtime.mp4

Both panes play at natural speed against the browser fixture, aligned at the first keystroke: a #Alpha member types @example hi at 50 ms/key, then Enter. Example is a channel member with a profile; another member has no profile. Left: plain text goes out with no p tag. Right: Space selects @Example and the sent row carries the person chip. No artificial latency; here the chooser decides on the keystroke, so no spinner is visible — the pending chip appears when the directory search is still in flight.

Final frame: the Before sent row reads plain "@example hi"; the After sent row shows the @Example person chip followed by "hi"

Testing

  • Tests added across the branch; at cd083895d MessageComposer.test.tsx passes 377/377 and SoloDraft.test.tsx 40/40 (solo-agents.test.tsx 111, EditableInput.test.tsx 242, InlineChip.test.tsx 7 and profile-details.test.ts 13 as of their last change), plus the portable space fixture alias_of_same_identity_does_not_block.
  • biome check on the changed files, tsc --noEmit and the pre-push design checks are green at cd083895d.
  • The demo was recorded through tests/browser/fixture.mjs in headless Chromium; not yet exercised in the WKWebView desktop build, so this stays a draft.

Spec wording: src/bundled/mentions/README.md §5 (pending mention) and §2 (the pending chip's @ opens no query); docs/agents.md states it beside both Space statements.

🤖 Generated with Claude Code

@matt2e
matt2e force-pushed the manual-agent-tagging-buggy branch 2 times, most recently from 8d19b16 to ec15ed9 Compare October 26, 2026 06:42
@matt2e matt2e changed the title feat(mentions): hold an undecided Space as a pending mention and resolve it fix(mentions): resolve fast-typed @name mentions with a pending chip Oct 26, 2026
@matt2e
matt2e marked this pull request as ready for review October 26, 2026 10:38
@matt2e
matt2e requested a review from a team as a code owner October 26, 2026 10:38

@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 Rocket, Mantis and Groot: one P2 integration gap in the new Solo mention flow, detailed inline.

Reviewed head e99f360f8b10dcde30a2f2aeec1e422d6c6d2aeb against base c40dfdf4c81aabe759f3a4329c3e77f854746a59. Source-only: no tests or app execution; CI and native/IME/accessibility behavior were not verified. The attached screenshot redirected to sign-in and the demo video was not inspected, so their visual/privacy contents remain unverified.

Comment thread src/bundled/mentions/MentionCompletion.tsx

@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 follow-up via Wes’s account, integrating Rocket, Mantis and Groot. The initial-load repair addresses the prior finding, but one P2 remains on Retry agents, detailed inline. Source assessment: 8/10; renew the pending roster promise for each loading cycle.

Reviewed head 308aa691fc092740716415dcaf7a0f6080b18642 against base c40dfdf4c81aabe759f3a4329c3e77f854746a59, focusing on the nine-file delta since e99f360f8. Source-only: no tests or app execution; CI, native behavior and rendered accessibility were not verified. The screenshot was inspected; the demo video remains uninspected.

Comment thread src/features/messages/MessageComposer.tsx Outdated

@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 follow-up via Wes’s account, integrating Rocket, Mantis and Groot: no new actionable findings; the prior Retry agents defect is fixed by renewing the pending roster promise for each loading period, with a regression test checking recipient tags. Source assessment: 9/10.

Reviewed head 949f96324b7d4c542101c3db93ad0f83fbcb9335 against base c40dfdf4c81aabe759f3a4329c3e77f854746a59, focusing on the two-file repair since 308aa691. Source-only: no tests or app execution; CI, native behavior and rendered accessibility remain unverified; the screenshot was inspected, but the demo video was not.

@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 follow-up via Wes’s account, integrating Rocket, Mantis and Groot: no new actionable findings in the one-file test repair since 949f9632; production code is unchanged. The helper settles selection before typing, and the added regression checks both editor DOM and draft text. Source assessment: 9/10.

Reviewed head ff4bf9ddacf24ff6f3acc602c77d3ef2017417e9 against base c40dfdf4c81aabe759f3a4329c3e77f854746a59. Source-only: no tests or app execution; fail-before/pass-after and timing claims were not independently reproduced, and CI was still running at the snapshot. The screenshot was inspected; the demo video and native/browser behavior remain unverified.

@kalvinnchau kalvinnchau 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.

🤖 One pending-mention regression found; details inline.

Comment thread src/bundled/mentions/MentionCompletion.tsx
@matt2e
matt2e force-pushed the manual-agent-tagging-buggy branch from ff4bf9d to a2c84b6 Compare October 27, 2026 04:16

@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 follow-up via Wes’s account, integrating Rocket, Mantis and Groot: two P2 defects and one P3 IME boundary defect are detailed inline. Preserve the separator during chip extension, include cross-community namesakes in pending resolution, and recheck the boundary after composition. Source assessment: 8/10 pending those fixes.

Reviewed head a2c84b61e75949e400628c7196a8f6a68bbcb365 against base 64020ded90882f5e7609b76d8778c4f1f27a3dac, including rebase integration and changes since ff4bf9dd. Source-only: no tests or app execution; CI, performance and native/accessibility behavior remain unverified. The public screenshot was inspected; the demo video was not.

Comment thread src/features/messages/MessageComposer.tsx
Comment thread src/bundled/mentions/resolve-space-mention.ts
Comment thread src/features/messages/EditableInput.tsx Outdated

@kalvinnchau kalvinnchau 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.

🤖 One remaining recipient-loss defect in pending mention extension.

Comment thread src/features/messages/MessageComposer.tsx
matt2e added a commit that referenced this pull request Oct 27, 2026
…r the typed words

`typedContinuation` took the longest remainder after the chip that, joined to
the chip's name, normalized to a continued name at a `mentionBoundary`.
`normalizeName` trims, so with the draft ending at `@mira Example ` when the
directory page landed, both `Example` and `Example ` spelled `Mira Example`,
and the trailing-space remainder, found last, won. The editor's extension
deleted that remainder, separator included: the settled draft was
`@Mira Example`, typing `hi` produced `@Mira Examplehi`, and the joined word
dropped the explicit recipient, so the message went out with none. Reported
on PR #1623 (kalvinnchau), reproduced in Chromium and WebKit; the existing
extension tests type `Example hi` before releasing the page and never met
the boundary.

A remainder that ends in whitespace is no longer a candidate: it spells the
same name as the shorter remainder before it, which the loop already took,
so the extension stops on the name's last character and the separator stays
in the text. Punctuation needed no rule: `normalizeName` keeps it, so
`Example,` never spelled the name.

Test: the page is released while the draft ends at `Example `; the chip
extends and settles to `@Mira Example ` with the recipient, `hi` typed after
it reads `@Mira Example hi` with the recipient kept, and the send carries
Mira's tag. It fails at HEAD~ at the settled-value assertion.

Verified: MessageComposer.test.tsx 375/375, `tsc --noEmit` clean, biome clean
on the changed files (one pre-existing `useTemplate` info in the test file).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>

@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 follow-up via Wes’s account, integrating Rocket, Mantis and Groot.

The separator defect is fixed; no new defect found in the two-file repair. These unchanged prior findings still need attention:

Source assessment remains 8/10. Head e188fffa9fccea7aab1d32bfbcc5728765c1beae, base 64020ded90882f5e7609b76d8778c4f1f27a3dac, compared with a2c84b61. Source-only: no tests, app execution or current CI assessment; native/accessibility behavior unverified. Screenshot inspected; demo video uninspected.

@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.

🤖 Carl, an automated reviewer, commenting via Wes’s GitHub account, with two independent Court lanes (Princess Donut, Mordecai) plus my own pass.

Verdict: not ready. Two prior findings remain; one new product question and two optional hardening notes are inline.

Optional, non-blocking: the change adds roughly 65 tests and 2.6k test lines, mostly one per edge. A few journeys (channel Space → pending → p tag on send, including extension and IME under Send; Solo failed then retried inventory; undo/redo/Backspace) plus one table of resolver outcomes would cover the same contracts with less to maintain. The extension machinery (typedContinuation, applyExtend, chained links, held extends) drives most review rounds; it covers a real case, but it is the natural cut if a smaller slice is wanted.

Unverified: prefixPage in useMentionDirectory.ts assumes a longer relay query never matches more than its prefix. If relay search does not guarantee that, Space can select over a page missing a namesake.

Merge criteria: the two open findings fixed with coverage; a decision on the inline send-after-wait question.

Head e188fffa9fccea7aab1d32bfbcc5728765c1beae. Source review only; no local tests, IME or app runs.

Comment thread src/features/messages/MessageComposer.tsx
Comment thread src/features/messages/MessageComposer.tsx Outdated
matt2e added a commit that referenced this pull request Oct 28, 2026
…r returns to prose

Two follow-ups from review of PR #1623.

A send that waited on a pending chip went out as prose when the chip
reverted: `@mira hi`, Enter on a slow relay, the resolver hits its bound or
finds a namesake, and the message leaves with no `p` tag and no notice. The
user saw a chip, waited, and notified no one. `send()` now learns how each
chip it waited for settled: the resolution chain (`resolveChip` ->
`settlePendingMention` -> `settled`) settles with the typed name when the
chip returned to prose, under the name the chip had when it settled (an
extended chip reports `mira Example`). Any such name keeps the draft and
sets the composer's alert: "@mira could not be matched to a person or
agent. Send again to send it as text.", or, where a recipient is required
and none is left, the existing recipient hint in its place. The next send
goes out as text, since nothing is pending by then. A send whose waited
chips all became recipients continues as before.

The settle path stored a provider's resolution verbatim, skipping the shape
check `insertMention` applies, so `{ pubkey, name: undefined }` from a
third-party provider became an `@undefined` chip. `wellFormedRecipient`
(64-hex pubkey, non-empty name) is now one module-level guard used by both,
and settle copies only `{ pubkey, name }`.

Tests: the two bound tests now assert the kept draft and alert, then a
second submit sends the text; the wait table splits into the recipient case
and the prose case with its second send (alert gone after it); a required
recipient gets the hint with Send `aria-disabled`; a name-less resolution
returns the chip to prose. README section 5 and docs/agents.md state the
rule.

Verified: MessageComposer.test.tsx 377/377 (one earlier run under load
average 90 failed `…pending chip to prose mid-word` once; it passes 3/3
alone and in the full rerun), SoloDraft.test.tsx 40/40, `tsc --noEmit`
clean, biome clean on the changed files (one pre-existing `useTemplate`
info in the test file).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>

@kalvinnchau kalvinnchau 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.

🤖 One new diagnostic issue in the pending-mention send alert.

Comment thread src/features/messages/MessageComposer.tsx Outdated
@matt2e
matt2e force-pushed the manual-agent-tagging-buggy branch from cd08389 to c45830a Compare October 28, 2026 12:35
matt2e added a commit that referenced this pull request Oct 28, 2026
…r the typed words

`typedContinuation` took the longest remainder after the chip that, joined to
the chip's name, normalized to a continued name at a `mentionBoundary`.
`normalizeName` trims, so with the draft ending at `@mira Example ` when the
directory page landed, both `Example` and `Example ` spelled `Mira Example`,
and the trailing-space remainder, found last, won. The editor's extension
deleted that remainder, separator included: the settled draft was
`@Mira Example`, typing `hi` produced `@Mira Examplehi`, and the joined word
dropped the explicit recipient, so the message went out with none. Reported
on PR #1623 (kalvinnchau), reproduced in Chromium and WebKit; the existing
extension tests type `Example hi` before releasing the page and never met
the boundary.

A remainder that ends in whitespace is no longer a candidate: it spells the
same name as the shorter remainder before it, which the loop already took,
so the extension stops on the name's last character and the separator stays
in the text. Punctuation needed no rule: `normalizeName` keeps it, so
`Example,` never spelled the name.

Test: the page is released while the draft ends at `Example `; the chip
extends and settles to `@Mira Example ` with the recipient, `hi` typed after
it reads `@Mira Example hi` with the recipient kept, and the send carries
Mira's tag. It fails at HEAD~ at the settled-value assertion.

Verified: MessageComposer.test.tsx 375/375, `tsc --noEmit` clean, biome clean
on the changed files (one pre-existing `useTemplate` info in the test file).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Oct 28, 2026
…r returns to prose

Two follow-ups from review of PR #1623.

A send that waited on a pending chip went out as prose when the chip
reverted: `@mira hi`, Enter on a slow relay, the resolver hits its bound or
finds a namesake, and the message leaves with no `p` tag and no notice. The
user saw a chip, waited, and notified no one. `send()` now learns how each
chip it waited for settled: the resolution chain (`resolveChip` ->
`settlePendingMention` -> `settled`) settles with the typed name when the
chip returned to prose, under the name the chip had when it settled (an
extended chip reports `mira Example`). Any such name keeps the draft and
sets the composer's alert: "@mira could not be matched to a person or
agent. Send again to send it as text.", or, where a recipient is required
and none is left, the existing recipient hint in its place. The next send
goes out as text, since nothing is pending by then. A send whose waited
chips all became recipients continues as before.

The settle path stored a provider's resolution verbatim, skipping the shape
check `insertMention` applies, so `{ pubkey, name: undefined }` from a
third-party provider became an `@undefined` chip. `wellFormedRecipient`
(64-hex pubkey, non-empty name) is now one module-level guard used by both,
and settle copies only `{ pubkey, name }`.

Tests: the two bound tests now assert the kept draft and alert, then a
second submit sends the text; the wait table splits into the recipient case
and the prose case with its second send (alert gone after it); a required
recipient gets the hint with Send `aria-disabled`; a name-less resolution
returns the chip to prose. README section 5 and docs/agents.md state the
rule.

Verified: MessageComposer.test.tsx 377/377 (one earlier run under load
average 90 failed `…pending chip to prose mid-word` once; it passes 3/3
alone and in the full rerun), SoloDraft.test.tsx 40/40, `tsc --noEmit`
clean, biome clean on the changed files (one pre-existing `useTemplate`
info in the test file).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>

@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.

🤖 The new draft-preservation behavior and recipient-shape guard address the prior follow-ups. These existing findings still need attention:

Star Lord’s automated source follow-up via Wes’s account, integrating Rocket, Mantis and Groot. Head c45830a8090f4e1f4b9aa30495df34ebb0cb2113, base 87eefd993e02d06f8fab5f9478e02b822486ac60; compared with e188fffa. No tests, app execution or CI assessment. Screenshot inspected; demo video, native/IME behavior and rendered accessibility remain unverified.

matt2e added a commit that referenced this pull request Oct 28, 2026
…gged

Review of PR #1623 (r4400503808): with 32 recipients already in the draft,
`@honey ` held and Enter pressed, the resolver finds Honey but the
recipient cap refuses it, and the alert said "@honey could not be matched
to a person or agent". The same wording covered a recipient that is not
admitted here and one whose current name would overrun the length limit.
The user is told the name matched no one and retries it, when the fix is
to remove a recipient or shorten the message.

The settle path already makes each check in turn, so it now keeps the
reason with the typed name: `settled` resolves `{ name, reason }`, where
the reason is `unmatched` (no resolution, a malformed one, a timeout, a
namesake or a continuation the words did not complete), `unavailable`
(admission refused), `limit` (32 recipients), or `length` (accepted, but
the editor kept the typed text because the name would overrun the
limit). The send alert gives one sentence per reason, naming its chips:

- "@honey could not be matched to a person or agent."
- "@honey could not be tagged here."
- "@honey could not be tagged: a message can tag at most 32 recipients."
- "@honey could not be tagged: the message would be too long."

followed, as before, by "Send again to send it as text." or the
recipient hint. Draft retention and the second-send behavior are
unchanged. README section 5 and docs/agents.md say the alert names why.

Tests: a resolved recipient refused at send for each of the cap, admission
and length, asserting the reason in the alert, the kept draft and no send.
All three fail at HEAD~, which says the name could not be matched. Two
chips refused for different reasons get a sentence each and "them".

Verified: MessageComposer.test.tsx 411/411 at load average 112, including
the four new cases; the combined run with EditableInput, SoloDraft,
src/bundled/mentions and src/features/conversation passed 1040/1041, the
one failure being the unrelated load-sensitive case noted in the previous
commit; `tsc --noEmit` clean; biome clean on the changed files (one
existing `useTemplate` info in the test file).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>

@kalvinnchau kalvinnchau 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.

🤖 Host-side refusal diagnostics are repaired. One residual wording issue remains below; existing namesake and IME findings are not duplicated.

Comment thread src/features/messages/MessageComposer.tsx Outdated
matt2e added a commit that referenced this pull request Oct 28, 2026
…nd alert

Review of PR #1623 (r4401471378): every null resolution of a pending chip
a send waited for was reported as "@mira could not be matched to a person
or agent". The resolver returns null for more than no match: a namesake,
a team block, a DM outsider, a truncated page, a failed search and the
8 s bound all end the same way, and for most of them a match may well
exist. The alert told the user the name matched no one when the lookup was
ambiguous or incomplete.

The reason is now `unresolved` and says "@mira could not be confirmed as
one person or agent.", true of every null and still a hint that picking
the name from the list is the way to tag it. The other reasons (not
taggable here, the recipient limit, the message length) and the draft
retention are unchanged. README section 5 and docs/agents.md use the same
wording.

Tests: the two bound tests (first resolution and chained extension) assert
the new wording for a timeout, and a new case over the real chooser holds
`@mira`, sends, and answers the search with two people named Mira; the
alert says the name could not be confirmed and the draft is kept. All
three fail with the previous wording.

Verified: MessageComposer.test.tsx 412/412, `tsc --noEmit` clean, biome
clean on the changed files (one existing `useTemplate` info).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e and others added 5 commits October 29, 2026 10:38
…tinues it

`exactMention` counted every choice's longer names as blockers, including
the choice that matches the query exactly. An agent known by both its
library name `Example` and its profile name `Example Agent` therefore
blocked its own Space selection on `@example `, and only `@example agent `
could pick it. One identity is not its own namesake: a choice's longer
names no longer block when that same choice has an alias or label equal
to the query. Other choices' longer names block as before.

Adds the portable `space` fixture `alias_of_same_identity_does_not_block`
(fails on the previous rule) and states the exception in README section 5
and docs/agents.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
The profile directory only ever recorded kind-0 events it received, so a
channel member with no profile stayed "missing" forever. The Mentions
chooser's local-first Space step waits until every member is named, which
meant one profile-less member demoted every mention in that channel to
the directory-gated path, where a fast-typed `@name ` arrives before the
200 ms debounce and relay round trip settle and is sent as prose.

The directory now records the authors a completed read returned no kind 0
for as `unnamed` (bounded, replaced rather than mutated, notified like the
snapshot) and exposes them through an optional `ProfileQueries.unnamed()`.
A profile that arrives later, live or by a later read, removes the entry;
`clear()` forgets it; a failed read records nothing. `useMentionChoices`
subscribes to it and no longer counts known-unnamed members as missing
names. Such a member has no aliases, so it can neither be selected nor
block a name. The chooser's "Some names unavailable" status and Retry are
unchanged.

Tests: the directory records, outranks, ignores failures, and forgets the
evidence; a member mention selects on Space once another member's read
settles without a profile while the directory search is still running,
and keeps waiting while the read is in flight or has failed until a retry
settles it. README section 5 states the rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… pure functions

`useMentionChoices` built its named choice set and applied the Space rule in
closures; `useMentionDirectory` read a directory page inside its debounced
effect. A pending mention (next commits) needs to apply the same rule to the
same page off-screen, after the chooser has unmounted. Extract them without
changing behavior:

- `namedCandidates(session, channelId, invite, roster, selected, people)`:
  the labelled choice set under one naming scope.
- `spaceMention(choices, local, query, evidence)`: README section 5 over
  explicit `{ pending, missingNames, complete }` evidence.
- `fetchMentionDirectoryPage(session, query, signal)`: the shared page cache
  and refuted-prefix bookkeeping around one `people()` read; the hook's
  effect keeps its debounce and React state around it.

Existing chooser and conformance tests cover the moved code unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
A mention Space commits before the chooser has decided needs somewhere to
live while the host resolves it. Add a `pending: { id, name }` attr to the
token node, next to `recipient` and `resource`:

- `projectComposerDocument` emits `draft.pending` (`{ id, name, start, end }`
  per chip, only when any exist) beside `recipients`; `MentionDraft.pending`
  is documented as projection-only and `mentionDraft()` never copies it, so a
  stored draft cannot restore a chip whose resolver died with its editor. The
  restore branch of `readComposerDocument` demotes such a token to its text.
- `insertPendingMention(name, range)` replaces the typed `@name` with the
  atom and a space in one history event (the typed query is the undo target,
  like a selected mention) and returns the chip's id; it refuses a caret away
  from the range, a range that is not the name, and code blocks.
- `settlePendingMention(id, recipient | null)` rewrites the chip into a
  recipient token under the recipient's current name, or back into plain
  text, outside history and while the editor is read-only (it must run while
  sending). A later undo still restores the typed text through it. A current
  name that would overrun the length limit leaves the typed text.
- The token portal paints a pending chip itself, as it does resources, with
  `InlineChip`'s loading face, so nothing depends on the providing plugin.

Backspace, copy and the normalize plugin need no change: the chip is an atom
with no recipient, so it selects then deletes, copies as its text, and is
never demoted as broken provenance. Nothing creates one yet.

Tests: one undo step and redo; refusals; settle into a recipient keeps the
caret and the typed text as undo target; settle to text while read-only;
length overrun; Backspace; persisted snapshot restores as text; copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ention

`ChipFace.loading` existed but nothing drew it. A pending mention chip
(previous commit) needs to read as "still being decided" without moving
the text around it:

- The sigil is wrapped in `.inline-chip-trigger`; while `face.loading` it
  paints `color: transparent` and a `CircleNotchIcon` sits absolutely in its
  box (1em, centred), so the `@` keeps its advance width and baseline in the
  chip's own font and settling into a recipient or back into text shifts
  nothing. The spinner names its own colour because it would otherwise
  inherit the transparent one. `prefers-reduced-motion: reduce` stills it
  in the existing block.
- The `role="img"` span carries `aria-busy` and the name "Resolving mention
  <Name>"; the icon is hidden. A loading face is always inert, even for an
  interactive consumer, since there is nothing truthful to preview yet.
- A "Resolving mention" specimen joins the design-system viewer.

Tests: busy chip renders the spinner inside the trigger with the glyph still
in the flow and no button; settling drops the spinner and busy state; the
stylesheet contract (transparent sigil, reduced-motion `animation: none`)
is checked as text since jsdom does not load chips.css.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e and others added 22 commits October 29, 2026 10:41
…w the displayed match as undecided

Since a682e4e both terminator branches read the publication. A display
that showed exact match X while the publication moved Space to another
recipient still stands, and Space is literal. A display that showed X
while the publication withdrew `spaceId` and set `spacePending` falls
through to the pending branch and holds the chip, where it used to leave
Space literal. That is the intended reading: no other recipient overrules
what the user saw, the decision is deferred to the resolver under the full
rule, which selects X again if it holds. The branch now says so, and a
host test over the real `@` syntax pins it: display X, publication
undecided and Space in one commit, the chip is held with no literal space,
the resolver starts once, and it settles into the recipient.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
`500` appeared in the directory as the batch size, the read limit and the
"page not full" bound without a name tying them together; the reader's
own limit check spells it out separately. `PROFILE_PAGE` names the
directory's use and states why the three agree: a page that fills the
reader's largest read may have left out authors who do have a profile.
No behavior change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…g alone

`settlePendingMention` held a settle on `composing.current || editor.composing`,
but the replay only ever runs from React's `onCompositionEnd`. ProseMirror's
`compositionend` is an edit handler, skipped while the view is not editable,
and nothing else clears its `composing` before the next click. On the path the
previous fix targets, a chip pending under an IME composition with Send
clicked on Mac WebKit, `sending` makes the editor read-only before the IME
commits: `composing.current` clears, `editor.composing` stays set. A
resolution landing after that, within the resolver's 8 s, was parked on
`heldSettles` with no compositionend to come. Its promise dangled,
`entry.settled` never resolved, `send()`'s wait never returned, and `sending`
kept the editor disabled, so the user could not even start another
composition to flush it.

The hold now keys on `composing.current` only: the event that clears it is
the one that replays, and `editor.composing && !composing.current` is exactly
the stale state `settleHeld` already applies through. The other editor APIs
keep both flags; they refuse synchronously rather than park a promise the
host awaits. The send-wait comment now states its bound: the resolver plus
an IME composition already open when Send was clicked, not the resolver
alone.

Test: the existing held-settle send test reordered, compositionstart, Send,
compositionend, then the resolution lands; the send completes with the
recipient's `p` tag. On the previous condition the send never fires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
`settlePendingMention` in the host forked on `outcome instanceof Promise` to
tell an immediate editor settle from one held through a composition. Both
reach `settled` and every consumer, the reconcile effect, `send()` and the
host tests, awaits the resolution asynchronously, so nothing observes the
synchronous branch. `Promise.resolve(outcome).then(...)` is the single path
and `settled` stays the one confirmation point; the three affected test
files pass unchanged.

The `spaceId` branch comment in `ComposerCompletions` could be read as "the
displayed row is accepted". It now says what holds: Space is not redirected
away from an exact match the display showed, so a publication that moves it
to another id leaves Space literal, and only the publication ever selects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…g a mention

`product-ui.spec.mjs` "playground distinguishes same-name people and agents"
failed on every Linux WebKit run of this branch at the third pick, with the
picker still open and no chip inserted; Chromium and main's WebKit were green.

The lab fixture's session has no `directMessages`, so the picker's community
search has always thrown. Before this branch the call threw synchronously
inside the 200 ms debounce timer, an uncaught page error the harness does
not fail on, and nothing reached `.then`: the "Searching community…" status
stayed and the rows never moved. `fetchMentionDirectoryPage` is async, so
the same throw is now a rejection and the picker renders what it was built
to render, the error status and a Retry button above the rows, about 200 ms
after opening. On Linux WebKit the test's row click spans that moment:
mousedown on the Honey row, 48 px of layout shift, mouseup on the Alice row
that moved under the pointer, and the click fires on their common ancestor
and reaches neither button. The CI trace shows the highlight on Honey with
the dialog open and the error shown; a probe holding the mouse down across
the status change reproduces it on Mac WebKit.

Two owners:

- The fixture's session gains `directMessages.people`, answering with the
  playground's four profiles by name prefix, so the search settles (and is
  cached after the first opening) instead of failing in a product catalogue.
- The test waits for the dialog to be visible and the "Searching community…"
  status to be gone before clicking a row. The status is in the dialog's
  first commit whenever a search is pending, so its absence is the settle
  barrier. The product's status line still leaves the flow above the rows
  when a search settles; that is unchanged here.

Verified: product-ui.spec.mjs 18/18 on local WebKit and Chromium
(--repeat-each=6); biome and tsc clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
In a new Solo chat (`SoloDraft` -> `MessageComposer` -> `PersonalComposer`)
the composer always provides `DraftMentionRoster`, as an empty list while
the agent inventory loads. The chooser treated any roster as complete, so
`@Aaron hi` typed before the inventory answered never became a pending
mention: Space was literal, the agent arriving later could not recover it,
and the message could not be sent as "Tag an agent to send". The resolver
had the same gap, since a roster skipped its inventory wait.

The roster's owner now says when it is incomplete:

- `DraftMentionRoster` carries `{ recipients, pending? }`. `pending` is set
  while the owner's sources load and resolves with the first roster
  committed once they settle. `PersonalComposer` (new and saved Solo chats)
  sets it while agent choices are pending and not yet usable, the rule the
  chooser already applies to an invite scope. `NewMessage` passes its
  recipients, never pending.
- `useMentionChoices` counts a pending roster as pending, so the chooser
  shows "Loading recipients…" and publishes `spacePending`.
  `resolveSpaceMention` awaits the roster before applying section 5.
- The host checked admission at settle time against the roster from the
  render that started the resolution, which was empty here, so the settled
  agent went back to prose. It now reads the current roster.

Test: the real `SoloDraft` with the mentions completion and a gated agent
inventory. Typing `@Aaron hi` holds the chip; releasing the inventory
settles it, and Send starts the chat with Aaron and sends his `p` tag.
It fails on the previous code, and separately with either the resolver's
wait or the settle-time roster reverted. The plugin test that mounts the
chooser without a roster is renamed for the invite scope it covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ry loading period

`PersonalComposer` created its roster deferred once per mount and resolved it
the first time the agent inventory stopped loading. A failed library read
stops loading too: the deferred settled with the empty roster, and a Retry
(which does not remount the composer) put the choices back into loading, so
`DraftMentionRoster.pending` advertised that same fulfilled promise. A Space
typed during the retry held the chip, `resolveSpaceMention` awaited the
promise, read the empty roster and returned the mention to prose at once,
and the agent arriving later could not repair it: the new Solo message had
no recipient to send with.

The deferred is now tied to a loading period: created when loading begins,
resolved with the first roster committed after it ends, and dropped, so the
next period starts a new one. A composer mounted with the inventory ready
never creates one.

Test: the inventory read fails, Retry agents is clicked, and `@Aaron hi` is
typed while the retry is gated; releasing it settles the chip and Send
starts the chat with Aaron and sends his `p` tag. On the previous code the
chip reverts to text before the pending assertion. The existing before-load
case shares the mount through a local helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
`EditableInput.test.tsx > uses plain text when pasting rich content into
an existing code block` failed the Vitest flake check twice (runs
40281402855 and 40319121812) with a trailing `\n\n`: the document carried
an empty paragraph after the code block. Locally 34/34 green.

The paragraph is created by the first keystroke, not the fence. `mount`
focuses the editor natively; the editor's `onFocus` runs `editor.focus()`,
which places ProseMirror's caret at `(p, 0)`, and then jsdom's `focus()`
collapses the selection onto the focused element itself, `(editor root, 0)`
(HTMLOrSVGElement-impl.js), where a browser keeps a caret already inside
the host. jsdom reports that in a `selectionchange` task queued with
`setImmediate`, on which ProseMirror's DOMObserver puts its caret back. An
empty draft's `setSelectionRange(0, 0)` changes no selection, so nothing
re-places the caret earlier; a non-empty mount is safe because the move
syncs it synchronously. `user.keyboard` awaits a 1 ms timer before its
first key: locally the immediate wins, on a loaded runner the timer can
fire first, user-event inserts the character at the root, and ProseMirror
parses it as a new paragraph ahead of the empty one. 43 tests in the file
mount an empty editor and type at once; this one lost the race.

`mount` now dispatches the pending `selectionchange` inside its focus
`act`, so the editor has re-placed its caret before any test types. The
regression test makes the losing ordering deterministic: `userEvent.setup({
delay: null })` runs no task between mount and the key, and the first
character must land in the one paragraph. It fails on the previous helper
(`<p>`</p><p><br></p>`) and passes now; the file passes 242/242 and
`scripts/flake-check.mjs vitest --iterations=5` over it is 5/5.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ger name

The pending gate in `MentionCompletion` held any admitted query whose
`spaceId` was unset while evidence was still arriving. `spaceId` is unset
for two reasons, and the gate did not tell them apart: the chooser cannot
decide yet, or a choice it already has settles it. With a loaded member
`Mira Example`, typing `@mira Example ` while the uncached directory read
for `mira` was debouncing or in flight held a chip on the first Space,
which ended the query and left `Example` as prose; the resolver then found
the longer name, returned null, and the whole name went out untagged.
Before the pending mention, that first Space stayed literal and the second
selected the full name.

A known name that continues the query is definitive: choices only arrive,
so nothing that loads later can lift it, and the next word may complete
that name. The longer-name step of `exactMention` is now the exported
predicate `continuedMention(choices, query)` in mention-ranking, with the
same name mapping it had: aliases and the label without its key qualifier
or generated `(agent)` suffix, and nothing for a choice that itself has the
exact name (one identity is not its own namesake, 9f9da13). `exactMention`
calls it before `exactName`, so `spaceMention`, the headless resolver and
the portable fixtures read one rule. The gate declines pending when it
holds over `model.candidates`, the whole known set `spaceId` is decided
over rather than the shown rows: members or roster, offered agents and the
directory rows received so far. `teams.blocksSpace` is unchanged, and the
resolver already returned null here, so it cannot disagree.

Tests: the provider table gains the two-word member with a held read
(`spaceId` and `spacePending` both undefined, before and after the page);
a unit test pins the predicate and the self-alias exception the gate must
not decline on; and the host, over the real `@` syntax with the real
chooser and a held read, keeps the first Space literal with no chip, selects
the member on the second and sends its `p` tag. Both behavioral tests fail
with the gate term removed. No existing test changed. README section 5 and
docs/agents.md state the declined case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…te a longer directory name

Verified at HEAD (d116e3b) with a host test over the real `@` syntax: the
real `MentionCompletion`, every member named, `Mira Example` only in the
community directory, and the `people` read for `mira` held until after the
user has typed. `@mira Example hi` typed at speed held a pending chip on the
first Space (the gate's `continuedMention` had nothing to see: no page yet),
`Example hi` landed as prose after it, and when the page arrived the resolver
found the longer name, returned null, and the chip reverted. Observed: value
`@mira Example hi`, 0 recipients, no chip span, no Invite prompt, and
`send("channel", "@mira Example hi", [], [])`. A slow typist, whose page
arrives before the first Space, already got the mention through the declined
Space and the second Space (pinned by a new host test beside the fix).

The resolver now tells the one null that was only a longer name apart from
every other null. `resolveSpaceMention` returns `SpaceMentionResolution`:
the recipient; null (no match, namesake, team block, DM outsider, truncated
page, failure, abort, timeout); or `{ continued: [{ name, resolve(signal) }] }`
when, over the complete choice set, `continuingNames(choices, query)` (the
longer-name step of `continuedMention`, now returning the names, each once)
is what kept the name from selecting. Each continued name resolves under the
same rule over the same settled page, with no further read: every name that
equals or continues it also continues the query, so the prefix page is its
complete set. A team block and the DM-outsider rule apply to the longer name
as to the first. `CompletionPending.resolve` carries the same shape as
`CompletionResolution`, with `continued` typed as `CompletionPending[]`.

The host (`resolveChip`) runs one resolution per chip through the chain:
on `continued` it reads the draft's projected text after the chip
(`value.pending` gives `{ id, name, end }`) and takes the longest continued
name that the chip's name plus the following words spell, case-folded, up to
a `mentionBoundary`; it asks the editor to extend the chip over that
remainder and resolves the longer name under the same signal and registry
entry, so the reconcile effect and `send()`'s wait see one live resolution.
Words that stop mid-name, complete no name, or changed before the extension
applied settle the chip to text exactly as before; with the caret at the end
of the restored span the chooser reopens over it, and `settled` stays the one
confirmation point. The send-wait comment names the chained bound.

The editor gains `extendPendingMention(id, remainder)`: it checks that the
text node right after the chip begins with `remainder`, then in one
transaction outside history replaces the chip with a pending chip named by
both (source `@mira Example`, so a later settle to text restores what was
typed) and deletes the remainder, while read-only too. The held-settle queue
now holds any pending-chip mutation, so an extension landing under an IME
composition replays at compositionend like a settle, and answers false when
the IME changed the words. Undo after an extension takes the words typed
after the chip first, then the chip's typed query, as for a settle.

Tests: the host test above now passes (chip on `mira`, `Example hi` typed,
page released, chip extends to `Mira Example` with `hi` outside, Enter shows
the Invite / Do nothing prompt, the sent event carries Mira's tag per the
answer) and fails again with `typedContinuation` stubbed out; a partial
remainder and words that complete no name return to prose; the partial case
reopens the chooser over `mira Exam` and selects on the next Space; two
continuing names where the words complete only one; words changed under a
composition before the extension applied; Enter while the first chip is
pending with the page released during the send wait; undo after the
extension. Editor: extend keeps the caret and the undo order, refuses a
changed or missing remainder, works read-only, and is held under a
composition. Resolver: `continued` for a longer name on a settled page, its
names resolving over the same page without another read (and recursing when
one continues another), a DM outsider's longer name resolving to prose, a
roster's longer name without the directory, and plain null for a longer name
on a truncated page. `continuingNames` is pinned in mention-ranking. README
sections 2 and 5 and docs/agents.md state the extension.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ke its first resolution

`resolveChip` awaited a chained `continued[].resolve(signal)` with no timer of
its own. The first resolution is bounded by its provider (`resolveSpaceMention`
races itself against `MENTION_RESOLVE_TIMEOUT_MS`), and the bundled provider
answers a chained resolve at once over the page it already read, so nothing
was lost there. But `CompletionResolution.continued[].resolve` is a host
contract with no stated bound: a provider whose chained resolve does I/O that
never answers left the extended chip spinning and `send()` waiting for it
indefinitely, with the editor disabled under `sending`.

The host now holds each chained link to the same bound: `bounded(resolve,
signal)` races it against `MENTION_RESOLVE_TIMEOUT_MS` and treats expiry as
null, so the chip returns to text as the resolver's own bound would return
it, and the waiting send goes out with the prose. The constant moves to
`features/conversation/completion.ts`, the completion contract's runtime home,
since the bound now belongs to the contract and the host does not import from
`bundled/`; the resolver and `solo-agents.test.tsx` import it from there. The
send-wait comment states what actually holds: the provider bounds its own
resolution, `resolveChip` bounds each extension, plus an open IME composition.

README section 5 and docs/agents.md said the words must "complete exactly one"
longer name while the next clause took the longest; `typedContinuation` takes
the longest name the words spell up to a name boundary, and both now say so.

Tests: a scripted provider whose chained resolve never settles, under fake
timers, keeps the extended chip busy and the send waiting one ms short of the
bound, then returns `@mira Example hi` to prose and sends it with no recipient
(fails without the race: the chip stays `aria-busy`). The host recursion path
is pinned with the real chooser: the directory holds `Mira Example` and `Mira
Example Jr`, `@mira Example hi` extends to `Mira Example`, its resolution
answers `continued` again over the same page, ` hi` completes nothing, and the
whole span returns to prose with no recipient and sends as typed. The undo
test's title now states its assertions: the first undo removes ` hi`, the
second leaves `@mira`.

Observed, no code change: with the bundled provider the intermediate
`Resolving mention mira Example` label is never painted. Its chained resolve
is synchronous over the already-read page, so React commits once in the
settled state; in the browser-fixture recording the accessible name a screen
reader hears goes straight from `Resolving mention mira` to `Person Mira
Example`. The scripted test above is the one place the extended chip is
observable while pending.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ge already read for its prefix

Measured in the browser fixture at d9a1706: with `Mira Example` only in
the directory and every member named, `@mira Example hi` typed at 50 ms/key
lost the mention whenever the `mira` page landed between the first Space and
the end of `Example`. The resolver saw only a partial remainder and returned
the chip to text, as intended, but the reopened chooser could not recover
the name: the resolver's read filled the exact-key `pages` cache under
`mira`, so `mira E`, `mira Exam` and `mira Example` all missed it, the
multi-word query was admitted by no known name, the 200 ms debounce never
fired between keystrokes, and the second Space was literal. Pages landing
before the first Space (declined by `continuedMention`) and after the full
name (the chip's extension) already worked; only this window was lost.

The relay matches names by prefix, so a complete page (no more pages) for
`mira` holds every match for any query extending it, the fact the refuted
prefix shortcut and the chained `continued[].resolve` already rely on.
`useMentionDirectory` and `fetchMentionDirectoryPage` now share
`prefixPage`: the longest cached complete page for a proper prefix of the
query, filtered to the query by `matchPerson`, served synchronously as a
settled page (`loading` false, no "Searching community…", no request, no
debounce). The hook keys the derived page on the source page's identity so
it is stable across renders, and like the refuted shortcut it serves only
until a retry asks for a fresh read. A truncated page or a failed read is
no evidence for a longer query, and an exact 64-hex key is an author lookup
that never derives. At `@mira E` the candidate is present on the keystroke,
the query is admitted, the row shows, and the Space after `Example` selects
under the Space rule with `spaceId` set and no pending chip; the settled
outsider still goes through the Invite / Do nothing prompt at send. The
trailing-space query `mira ` is admitted by the same derived candidate, so
admission needed no change. The chip's settle-to-text for a partial
remainder is unchanged.

Beyond this window the chooser reads less: once a complete page for a
prefix is cached, every extension of it (a longer name, a trailing space,
the browse page of a community with fewer than 15 profiles after a pause on
`@`) is answered without a relay read, and the resolver reads a longer
name's page from the same cache.

Tests, with the real `MentionCompletion` over the real `@` syntax and a
scripted `directMessages.people`: the page released after `@mira Exam` is
typed returns the chip to text with no busy chip left, the chooser shows
`Mira Example` at once with no search, `ple ` selects it on Space with one
`people` call, and Enter sends Mira's tag after the non-member prompt; the
page released right after the first Space leaves `@mira `, the chooser
reopens over it, and `Example ` selects. Both fail with the derivation
disabled. Unit: a complete cached prefix page serves a longer query with
`loading` false, no request through the debounce and `spaceId` decided over
it; a truncated prefix page does not and the request still goes out; the
resolver reads a longer name's page from the chooser's complete page with no
further read. The slow-typist host test and the two plugin tests that
counted the reads this removes assert the derived page instead, or use a
truncated page where a read is the behavior under test; the `@Hone`
dismissal step of `completion-work.spec.mjs` now runs before any `Hon`
search is cached so its held read is deterministic (6/6 on Chromium and
WebKit). The extension and `continuedMention` decline tests pass unchanged.
README sections 2, 5 and 6 and docs/agents.md state the rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…, and bound the first resolution in the host

Three follow-ups from review of 17aeff3, which settled a longer query from
the complete directory page cached for its prefix.

Retry hung under a cached prefix page. `retry()` deletes the exact key and
bumps `attempt`, and the hook's render-time derivation is gated on `attempt
=== 0`, but the effect's `fetchMentionDirectoryPage` still derived from the
prefix page and returned without caching. The next render found no exact
page, no `own.page`, `derived` and `refuted` gated off, and no error:
"Searching community…" with no Retry and unchanged effect deps, so it never
read again. Reachable when a transient failure on `@mira e` in one composer
is followed by a successful `mira` read in another consumer of the same
session (a thread composer, or the pending-mention resolver). The fetch
takes `{ fresh }`, which skips the prefix page; the effect passes `attempt >
0`, the resolver keeps the default. README's "A retry searches afresh" and
the hook comment now hold.

The browse page derived every query. A pause on `@` reads the listing under
the empty key, and once complete (a community with fewer than 15 profiles)
it is a prefix of every query, so no name search reached the relay again for
the session and a member who joined or renamed after it could not be found
by `@name` until restart. `prefixPage` now derives only from a non-blank
key, a finished name search; the listing is a plain profile read with a
different limit and answers no query but its own. README section 6 says so.

The first resolution was unbounded by the host. `CompletionPending.resolve`
does not ask a provider to time itself; only the bundled resolver races its
own first resolution against `MENTION_RESOLVE_TIMEOUT_MS`, and d9a1706
bounded the chained links only. A provider whose `spacePending.resolve`
never settled left the chip busy and `send()` waiting with the editor
disabled. `resolvePendingMention` now passes the first link through
`bounded` too, so the host owns the bound for every link; the bundled
resolver's internal race is redundant under it but harmless, and the
`bounded` and send-wait comments say what holds.

Tests, each failing at 17aeff3 at the stated assertion: a failed `@mira e`
read, a complete `mira` page read through the shared fetch meanwhile, and
Retry issues a fresh `mira e` read whose page renders with no status (HEAD:
no third read); a complete listing for the empty query cached, `@mi` still
searches (HEAD: served from the listing, no "Searching community…"), and the
finished `mi` search then answers `mir` with no read; a scripted provider
whose first resolve never settles, under fake timers set before Space,
keeps the chip busy and the send waiting one ms short of the bound, then
returns `@mira ` to prose and sends it with no recipient (HEAD: the chip
stays `aria-busy`). The recorded scenarios (first `mira` search on an empty
cache, no retry, no listing) take the same paths as before: `fresh` is false
at `attempt` 0, the key filter excludes nothing they cache, and the host's
bound equals the resolver's own.

Verified: `src/bundled/mentions` and `MessageComposer.test.tsx` 594/594,
`tsc --noEmit` clean, biome clean on the changed files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…d provider to stop

Two follow-ups from review of d749984.

A retry held the picker's whole session fresh. `useMentionDirectory`
counted retries per hook instance and never reset, and `MentionPicker`
keeps one hook mounted across openings, so after one Retry on a
transiently failed `mira e` every later query of the session passed
`fresh: true`, and the render-side prefix-page and refuted-prefix
shortcuts (17aeff3, d749984) stayed off until restart. The count now
belongs to the query it was asked for: `retried` is `{ query, attempt }`,
`attempt` is read for the current query only, and a Retry on another
query starts at one. Back on the retried query its failure and Retry
stand, and a second Retry still reads afresh past a complete page cached
for its prefix meanwhile. Inline completion remounts its provider per
keystroke and is unchanged.

`bounded` told the provider nothing. It passed `resolve` the chip's own
signal, which nothing aborts when `settled` deletes the entry, so at the
8 s bound a third-party provider's in-flight work ran on under a live
signal; and its timer was cleared only in `finally`, so a provider that
ignores abort left an 8 s timer behind after Backspace, undo or unmount.
The provider now gets a child signal that follows the chip's and is
aborted at expiry, and the chip's abort clears the timer and aborts the
child, so every end of a link (settle, expiry, chip removal) ends the
provider's work and the timer. The bundled resolver already aborts itself
at its own bound, so the recorded flows are unchanged.

Tests, each failing at d749984 at the stated assertion: in one
long-lived `MentionCompletion`, Retry on `mira e` fails again, then `lucy`
is answered from a complete `lu` page cached meanwhile and `zedd` by an
empty `zed` search, with no read (HEAD: "Searching community…"), and a
second Retry on `mira e` reads it afresh past a complete `mira` page; in
the host, a provider whose first resolution never settles has its signal
aborted at the bound while the chip is still pending (HEAD: aborted only
after the chip had left the draft, by the reconcile effect), and removing
the chip before the bound aborts the provider's signal and leaves no
timer behind under fake timers, once the editor's own short typing timer
has fired (HEAD: the bound's timer remains to 8 s).

Verified: solo-agents.test.tsx 111/111, MessageComposer.test.tsx 371/371,
`tsc --noEmit` clean, biome clean on the changed files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…an open query

Observed in the browser fixture (Chromium, `@mira Exam` at 50 ms/key, every
member named, `Mira Example` directory-only, a MutationObserver on the
chooser `section`): the popup left the DOM for 3-16 ms on every keystroke,
nine absent states over ten keys, so about half the 25 fps frames taken
while typing showed no chooser. Pre-existing on main: the merge-base keys
and resets the display the same way, and the recording's Before arm
blinked identically.

The sequence. The editor calls `completion.observe(true)` synchronously in
`dispatchTransaction`, so the new observation (revision and query) commits
with the draft. `ComposerCompletions` keys its `ContributionBoundary` on
the query text and revision, so that commit unmounts the old
`OwnedCompletion`, portal and all, and mounts a new one with `result`
undefined: popup gone. Its layout effect sets `publish`, a sync re-render
mounts the provider, and React flushes that sync commit's passive effects
at once, so the provider's mount `useEffect` publishes in the same task;
but `setResult` from inside a passive flush is a default-lane update, so
the rows commit in a later task, after the browser could paint.

The host now owns what the popup shows across the remount.
`ComposerCompletions` holds one display store, keyed by provider and
trigger start; `OwnedCompletion` reads it through `useSyncExternalStore`
and `publish` and its withdraw write to it. A new owner for a continuing
query (same provider, same `@`) shows the previous query's result in its
first commit, and its provider's first publication replaces it in the same
task: a store update is a sync-lane render, flushed at the end of the
passive flush. An owner whose provider has not published by the end of its
mount effects clears the carried display (its own effect runs after its
children's), so a provider that publishes later leaves the popup closed as
a first query does, and a stale display is never painted. A query at
another trigger or from another provider starts empty, as before. `latest`
stays per lifetime: Space, Enter, Tab and click read only this query's
publication (a682e4e), so carried rows are never acted on. Closing is
unchanged: a settled empty result, Escape, a Space that selects or holds a
chip; the pending gate and the `spaceId` publication on the keystroke are
untouched. The `section` is still a new node per keystroke, replaced within
one commit rather than across two.

Tests: the real `@` syntax with a provider that publishes from its mount
effect; outside act, each keystroke changes the draft inside a dispatched
discrete `select` event so React commits the observation in a microtask,
as under typing, and after one microtask the rows for the new query are in
the document while no MutationObserver delivery saw the popup gone (fails
at HEAD at the first continued keystroke with `null`). The popup still
closes on `@hon `, on Escape and on a selecting Space. The browser probe
after the fix: one absent state over the same keys, the Space that held
the pending chip.

Verified: MessageComposer, EditableInput, conversation, mentions, emoji and
ChannelCompletion suites 995/995; `tsc --noEmit` clean; biome clean on the
changed files; typeahead, completion-work, mention-rules and product-ui
browser specs 34/34 on Chromium and 34/34 on WebKit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…oses

Two follow-ups from review of aa2438b, which carried the previous query's
result across the per-keystroke remount of `OwnedCompletion`.

The display store was keyed `provider:query.start` with no lifetime and was
never evicted, so it meant more than the comment said: a query closed by
Escape, Backspace through its `@`, a send that cleared the draft, or a
pending-mention hold, and reopened later at the same offset, read the old
result in its first commit. The owner's guard effect cleared it before any
task boundary, so nothing stale was painted, but the store did not mean
"continuation of an open query". `ComposerCompletions` now evicts it from an
effect in its closed render (an effect, not render time, so the owner's store
listener is never fired during a render), and the store comment says so. A
reopened query starts empty and its provider's first publication fills it in
the same task, as a first query does; a continuing keystroke still carries.
For the pending-mention hold this is the sequence Space -> chip held -> popup
closed -> evicted -> chip reverts -> chooser reopens empty at the same `@`.

The blink test's `deliveries` check re-queried the DOM in the observer
callback, which runs at the microtask checkpoint and so could only ever see
the final DOM of each synchronous chain. It now classifies each delivery by
its `MutationRecord`s: whether any record removed a `section[aria-label$="
suggestions"]` node (or an ancestor holding one) and whether any added one.
Every delivery that removed the popup must have added its replacement, and
the four keystrokes each replace it once.

Tests: the records check at aa2438b~1, with the inline rows assertion
disabled, fails with the one delivery `{ removed: true, added: false }`; at
HEAD, four replacing deliveries, none without an addition. New: the typeahead
provider takes a `mounted` probe run in its mount effect before publishing,
which runs before the owner's guard effect and so sees what the owner showed
on its own; after `@hon`, `@hone`, Escape, `@honey` it records `hon` null,
`hone` carried (`Honey hon`) and `honey` null, once per query under
StrictMode. Without the eviction, `honey` shows `Honey hone`.

Verified: MessageComposer.test.tsx and src/features/conversation 470/470,
`tsc --noEmit` clean, biome clean on the changed files (one pre-existing
`useTemplate` info elsewhere in the test file).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
… popup uses

Since aa2438b a continuing `@` query shows the previous query's
"Loading recipients…" status in its first commit, so the popup's portal
mounts while the agent inventory is still loading. In jsdom that runs
`useCompletionPosition`, whose `new ResizeObserver` throws, and the reveal
of a selected retry row, whose `scrollIntoView` is not implemented. Either
throw fails the completion's `ContributionBoundary`, so Space never held
the pending chip in the two "typed with Space" tests (the retry case also
left the failed owner's key handler installed with `live` false).

Stub both in the suite, as MessageComposer and TerminalMentionEdit
already do. No product change: both APIs exist in every webview.

Verified: SoloDraft.test.tsx 40/40 (fails 38/40 at HEAD~); biome clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…r the typed words

`typedContinuation` took the longest remainder after the chip that, joined to
the chip's name, normalized to a continued name at a `mentionBoundary`.
`normalizeName` trims, so with the draft ending at `@mira Example ` when the
directory page landed, both `Example` and `Example ` spelled `Mira Example`,
and the trailing-space remainder, found last, won. The editor's extension
deleted that remainder, separator included: the settled draft was
`@Mira Example`, typing `hi` produced `@Mira Examplehi`, and the joined word
dropped the explicit recipient, so the message went out with none. Reported
on PR #1623 (kalvinnchau), reproduced in Chromium and WebKit; the existing
extension tests type `Example hi` before releasing the page and never met
the boundary.

A remainder that ends in whitespace is no longer a candidate: it spells the
same name as the shorter remainder before it, which the loop already took,
so the extension stops on the name's last character and the separator stays
in the text. Punctuation needed no rule: `normalizeName` keeps it, so
`Example,` never spelled the name.

Test: the page is released while the draft ends at `Example `; the chip
extends and settles to `@Mira Example ` with the recipient, `hi` typed after
it reads `@Mira Example hi` with the recipient kept, and the send carries
Mira's tag. It fails at HEAD~ at the settled-value assertion.

Verified: MessageComposer.test.tsx 375/375, `tsc --noEmit` clean, biome clean
on the changed files (one pre-existing `useTemplate` info in the test file).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…r returns to prose

Two follow-ups from review of PR #1623.

A send that waited on a pending chip went out as prose when the chip
reverted: `@mira hi`, Enter on a slow relay, the resolver hits its bound or
finds a namesake, and the message leaves with no `p` tag and no notice. The
user saw a chip, waited, and notified no one. `send()` now learns how each
chip it waited for settled: the resolution chain (`resolveChip` ->
`settlePendingMention` -> `settled`) settles with the typed name when the
chip returned to prose, under the name the chip had when it settled (an
extended chip reports `mira Example`). Any such name keeps the draft and
sets the composer's alert: "@mira could not be matched to a person or
agent. Send again to send it as text.", or, where a recipient is required
and none is left, the existing recipient hint in its place. The next send
goes out as text, since nothing is pending by then. A send whose waited
chips all became recipients continues as before.

The settle path stored a provider's resolution verbatim, skipping the shape
check `insertMention` applies, so `{ pubkey, name: undefined }` from a
third-party provider became an `@undefined` chip. `wellFormedRecipient`
(64-hex pubkey, non-empty name) is now one module-level guard used by both,
and settle copies only `{ pubkey, name }`.

Tests: the two bound tests now assert the kept draft and alert, then a
second submit sends the text; the wait table splits into the recipient case
and the prose case with its second send (alert gone after it); a required
recipient gets the hint with Send `aria-disabled`; a name-less resolution
returns the chip to prose. README section 5 and docs/agents.md state the
rule.

Verified: MessageComposer.test.tsx 377/377 (one earlier run under load
average 90 failed `…pending chip to prose mid-word` once; it passes 3/3
alone and in the full rerun), SoloDraft.test.tsx 40/40, `tsc --noEmit`
clean, biome clean on the changed files (one pre-existing `useTemplate`
info in the test file).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…s undecorated tokens

Since the rebase onto main, every pending mention reverted to plain text the
moment Space held it: CI on c45830a failed all 25 pending-chip tests in
MessageComposer.test.tsx, 13 in EditableInput.test.tsx and the two Solo
draft cases, and the chip never appeared in the app.

b6e1941 (#1558, GIF previews) changed the editor's normalize pass to
demote any token that is neither a recipient nor a resource unless a
decoration still covers it, so a presentation-only token whose renderer
went away becomes editable source again. A pending chip has no recipient
yet and no resource, and nothing decorates it: the token portal paints it.
The pass demoted it in the transaction that inserted it. Nothing conflicted
textually, so the rebase applied cleanly.

A pending chip is a semantic atom like the other two: the host holds its
resolver by the chip's id, and settling it is the only way it should leave.
The pass now keeps it.

Verified: MessageComposer.test.tsx 411/411; EditableInput.test.tsx,
SoloDraft.test.tsx and src/bundled/mentions 533/533; `tsc --noEmit` clean.
The first pending-chip case fails locally at HEAD~ as in CI, and every
case CI failed passes here. In one combined run with
src/features/conversation at load average ~70, an unrelated case ("leaves
Space literal beside a loaded longer member name…") failed once; it passes
alone, in the 411/411 rerun and in all 11 CI runs of c45830a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…gged

Review of PR #1623 (r4400503808): with 32 recipients already in the draft,
`@honey ` held and Enter pressed, the resolver finds Honey but the
recipient cap refuses it, and the alert said "@honey could not be matched
to a person or agent". The same wording covered a recipient that is not
admitted here and one whose current name would overrun the length limit.
The user is told the name matched no one and retries it, when the fix is
to remove a recipient or shorten the message.

The settle path already makes each check in turn, so it now keeps the
reason with the typed name: `settled` resolves `{ name, reason }`, where
the reason is `unmatched` (no resolution, a malformed one, a timeout, a
namesake or a continuation the words did not complete), `unavailable`
(admission refused), `limit` (32 recipients), or `length` (accepted, but
the editor kept the typed text because the name would overrun the
limit). The send alert gives one sentence per reason, naming its chips:

- "@honey could not be matched to a person or agent."
- "@honey could not be tagged here."
- "@honey could not be tagged: a message can tag at most 32 recipients."
- "@honey could not be tagged: the message would be too long."

followed, as before, by "Send again to send it as text." or the
recipient hint. Draft retention and the second-send behavior are
unchanged. README section 5 and docs/agents.md say the alert names why.

Tests: a resolved recipient refused at send for each of the cap, admission
and length, asserting the reason in the alert, the kept draft and no send.
All three fail at HEAD~, which says the name could not be matched. Two
chips refused for different reasons get a sentence each and "them".

Verified: MessageComposer.test.tsx 411/411 at load average 112, including
the four new cases; the combined run with EditableInput, SoloDraft,
src/bundled/mentions and src/features/conversation passed 1040/1041, the
one failure being the unrelated load-sensitive case noted in the previous
commit; `tsc --noEmit` clean; biome clean on the changed files (one
existing `useTemplate` info in the test file).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…nd alert

Review of PR #1623 (r4401471378): every null resolution of a pending chip
a send waited for was reported as "@mira could not be matched to a person
or agent". The resolver returns null for more than no match: a namesake,
a team block, a DM outsider, a truncated page, a failed search and the
8 s bound all end the same way, and for most of them a match may well
exist. The alert told the user the name matched no one when the lookup was
ambiguous or incomplete.

The reason is now `unresolved` and says "@mira could not be confirmed as
one person or agent.", true of every null and still a hint that picking
the name from the list is the way to tag it. The other reasons (not
taggable here, the recipient limit, the message length) and the draft
retention are unchanged. README section 5 and docs/agents.md use the same
wording.

Tests: the two bound tests (first resolution and chained extension) assert
the new wording for a timeout, and a new case over the real chooser holds
`@mira`, sends, and answers the search with two people named Mira; the
alert says the name could not be confirmed and the draft is kept. All
three fail with the previous wording.

Verified: MessageComposer.test.tsx 412/412, `tsc --noEmit` clean, biome
clean on the changed files (one existing `useTemplate` info).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e force-pushed the manual-agent-tagging-buggy branch from bfa2081 to a848853 Compare October 29, 2026 00:11
matt2e and others added 2 commits October 29, 2026 12:00
`team-mentions.test.tsx > directory pages rebuild the name index only for
people they add` (0bc0752, #1828) failed on every run of this branch
since the rebase onto main: `people` was called once, not three times.
It assumes every keystroke reads the directory. Its mock returned
`hasMore: false`, so on this branch the complete `p` page answers `pe`
and `per` through `prefixPage` (95050d1) with no read, and the later
`pers` step, which swaps an outsider into the page, would be served from
the cache too and never see it.

The mock now returns truncated pages, as 95050d1 did for the other
tests where a read is the behavior under test, so each keystroke still
reads and the test checks what #1828 meant it to. No product change.

Verified: team-mentions.test.tsx 13/13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…dary

Review of PR #1623 (r4385103855): a pending chip could settle into a
recipient inside a longer word, sending a `p` tag for a word that only
starts with the name. Every other path that creates a recipient
(`mentionDraft`, the normalize pass, `promoteRecipients`,
`typedContinuation`) requires a name boundary after it; the pending chip
escaped it twice.

- `applyExtend` checked only that the text after the chip still began
  with the words the host read. An extension held through an IME
  composition that committed ` Examples` over ` Example` absorbed
  ` Example` and left the `s` glued on, and the chip settled as
  `@Mira Example` inside `@Mira Examples`. It now also requires a name
  boundary after the words, and answers false otherwise, so the span
  returns to prose through the existing path.
- The normalize pass kept every pending token unconditionally. `@honey`,
  Space, Backspace and `suckle` kept a pending chip on `honey`, the
  resolver answered Honey, and `@Honeysuckle` went out tagging Honey. A
  pending chip now stays only while a name boundary follows it; otherwise
  it returns to its typed query in the same keystroke's history event,
  the reconcile effect aborts its resolution (keeping the resolver for
  undo), and the chooser reopens over the whole word. Punctuation keeps
  the chip, as it keeps a recipient.

README section 5 states that a pending mention must end at a name
boundary.

Tests: a held extension whose composition commits ` Examples` answers
false and leaves the chip on `mira`; Backspace and `nda` after a chip
leave `@miranda` with no chip, a settle that finds nothing, and undo
restores the chip, while `,` keeps it; in the composer, `@honey`, Space,
Backspace, `suckle` removes the chip, aborts its signal, reopens the
chooser, and the late resolution changes nothing, so the send carries no
`p` tag. All three fail without the production change.

Verified: EditableInput, MessageComposer, SoloDraft and
src/bundled/mentions 957/957; `tsc --noEmit` clean; biome clean on the
changed files (one existing `useTemplate` info).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Comment thread src/features/messages/EditableInput.tsx Outdated
… demotion

Review of PR #1623 (r4407955350): c305533 gave pending tokens their own
branch in the normalize pass, keeping one only while a name boundary
follows it, and that branch skipped the containment check every other
non-recipient token gets. A decoration formed around a pending chip
adopts the chip into its own token first; the demotion then found no
boundary after the chip and wrote its typed query at the deleted
token's mapped position. Typing `[@honey`, Space, Backspace and pasting
`suckle](nostr:npub1…)` left `[@honeysuckle](nostr:npub1…)@honey`.

A pending chip is now kept when a name boundary follows it or when a
larger decoration has already adopted it, the condition the pass
applies to resources and presentation-only tokens. A chip glued to a
longer word with no decoration around it still returns to its typed
query, as before.

Test: the paste above leaves exactly the link, adopted as one identity
chip, with nothing pending and a later settle finding no chip. It fails
at HEAD~ with `@honey` appended.

Verified: EditableInput, MessageComposer, SoloDraft and
src/bundled/mentions 958/958; `tsc --noEmit` clean; biome clean on the
changed files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>

@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.

🤖 The prior P2 cross-community namesake/admission gap remains: carry the chooser’s joinable agents into pending resolution and reuse the host’s current admission policy. The IME boundary, refusal wording and decoration-containment fixes address the other reported issues; no new actionable finding in this follow-up.

Star Lord’s automated source review via Wes’s account, integrating Rocket, Mantis and Groot. Source assessment: 8/10 pending the linked P2. Head 946f355b5fc3051b73a4db5ccaf10f8d64357a25, base 49fac0341e6ffeb1b9cae841d5ed8d96a6734aad, including rebase integration. No tests, app execution or CI assessment. Screenshot inspected; demo video, native/IME behavior and rendered focus/accessibility remain unverified.

…nts the chooser offers

Review of PR #1623 (r4385103853, r4390638220): the chooser counts the
owner's agents from their other communities (`Agents.joinable()`) as
local choices, but the pending-mention resolver never received them, and
the host's settle check left them out of admission.

- With a member `Mira` and the owner's agent `Mira` from another
  community, the chooser saw a namesake and held a chip on Space; the
  resolver saw only the member and tagged them, notifying a recipient the
  rule says needs an explicit pick. `SpaceMentionScope.elsewhere` now
  carries `joinable()`, read when the host starts the resolution, and the
  resolver passes it to `mentionCandidates` for the local step and to
  `namedCandidates` for every decision, continued names included. It is
  synchronous and never loading, so the resolver waits on nothing new.
- `settlePendingMention` called `mentionAdmission` without the bringable
  set `admits()` passes, so an other-community agent archived here was
  refused at settle although Tab would insert it, and the chip went back
  to prose. `admits()` now reads the roster through `currentRoster` (the
  settle runs in a closure from an earlier render) and the settle uses
  it, so "exactly what Tab would have inserted" holds.

Tests, through the real chooser in a stream channel: a member and an
other-community agent both named Mira, Space held while the search is
pending, an empty page returns `@mira ` to prose and Send carries no `p`
tag (fails at HEAD~: the member is tagged); an other-community `Rocket`
archived here with one member's name unloaded settles to `@Rocket ` with
the recipient and Send opens the "Bring Rocket into …" prompt (fails at
HEAD~, and still fails with only the resolver change).

Verified: MessageComposer.test.tsx, src/bundled/mentions and
SoloDraft.test.tsx 689/689; `tsc --noEmit` clean; biome clean on the
changed files (one existing `useTemplate` info in the test file).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>

@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.

🤖 No further changes requested: the prior cross-community namesake/admission finding is repaired, and the four-file follow-up introduces no new actionable finding. The added tests cover ambiguous-name fallback and reaching import consent; successful import/delivery remains unverified.

Star Lord’s automated source follow-up via Wes’s account, integrating Rocket, Mantis and Groot; source assessment 9/10. Head 175ab31f5bd714e3bb628111bc8cf8ffecc04b51, base 49fac0341e6ffeb1b9cae841d5ed8d96a6734aad, compared with 946f355b; screenshot and sampled demo frames inspected. No tests, app execution or CI assessment; native/IME behavior and rendered focus/accessibility remain unverified.

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