Skip to content

human chat @mentions - #2159

Open
webguru-hypha wants to merge 36 commits into
mainfrom
cursor/human-chat-mentions-specs-93f4
Open

webguru-hypha wants to merge 36 commits into
mainfrom
cursor/human-chat-mentions-specs-93f4

Conversation

@webguru-hypha

@webguru-hypha webguru-hypha commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

@ mention list: why 1 user + wrong name (fix)

Root cause (two issues):

  1. Members tab uses Hypha useMembersPerson.name/surname. The @ picker only used room.getJoinedMembers(). Matrix RoomMember.name for bridged accounts is often the same technical localpart as prev_privy_*, so we showed that string as the “display” name.
  2. matrixMemberDisplayLabel treated member.name !== userId as human-readable—but name can equal the ugly localpart (prev_privy_…), not the full @…:homeserver, so we incorrectly trusted it.

Fix:

  • Technical displayname detection (looksLikeTechnicalMatrixDisplayName) — ignore prev_ / prod_ / privy_did_privy Matrix display names (same issue class as timeline headers).
  • Merge space roster into mention candidates: batch person.sub → matrix MXID via new matrix_user_links query + getMatrixUserIdsByPrivySubsAction, same env as Matrix. Anyone on the Members list with a link appears in @ too (not only Matrix join).
  • HumanChatMentionCandidateRow: resolve usePersonBySub when we know privySub from roster (no extra Matrix→Privy round-trip), else useUserPrivyIdByMatrixId — same pipeline as chat bubbles.

Commit: fix(chat): align @ mention picker with Members tab roster and Hypha profiles

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • @mention autocomplete in the composer with canonical MXID insertion and mention-aware send/edit
    • Timeline renders MXID “pills”, highlights rows when you’re mentioned, and supports jump-to-message from mentions
    • Mentions tab/inbox with bell, capped unread badge and jump-to-message navigation
    • Human Chat now default-on with an emergency kill-switch and per-room mention notification controls
  • Documentation
    • Added comprehensive specification for human-chat mentions and acceptance criteria
  • Localization
    • Added Mentions UI translations in multiple languages

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds end-to-end Matrix intentional @mentions: spec, wire-format utilities and tests, send/edit plumbing to produce/parse m.mentions.user_ids, Privy→Matrix mapping server action and client hook, composer autocomplete and picker, mention inbox/bell and rendering/highlight, i18n, feature-flag changes, and e2e test adjustments.

Changes

Cohort / File(s) Summary
Specification
docs/requirements/Features/human-chat-mentions/requirements.md
New requirements/spec for Matrix intentional @ mentions: composer, send-time semantics, timeline rendering, inbox/notification UI, policies, touchpoints, rollout and acceptance criteria.
E2E tests & helpers
apps/web-e2e/src/*.spec.ts, apps/web-e2e/src/pages/human-chat-panel.page.ts
Removed runtime enable cookie helper; tests assume Human Chat default-on and use HYPHA_DISABLE_HUMAN_CHAT kill-switch; updated fixtures and removed BrowserContext cookie helper.
Feature flags & cookies
packages/cookie/src/constants.ts, packages/feature-flags/src/index.ts
Added HYPHA_DISABLE_HUMAN_CHAT and HYPHA_ENABLE_SPACE_MEMORY; getEnableHumanChat precedence reworked (toolbar → disable-cookie → legacy cookie/env → default true); added getEnableSpaceMemory.
Matrix mentions core
packages/core/src/matrix/mentions.ts, packages/core/src/matrix/__tests__/mentions.test.ts, packages/core/src/matrix/index.ts
New mentions utilities: MXID regex, normalization, plaintext extraction, merging into content, resolve/parse helpers; unit tests; re-exported from matrix index.
Matrix provider / send & edit flows
packages/core/src/matrix/client/providers/matrix-provider.tsx, packages/core/src/matrix/edit-room-message-media-caption.ts, packages/core/src/matrix/rich-reply.ts, packages/core/src/matrix/types.ts
Send/edit APIs accept optional mentionUserIds; resolve mention IDs and merge m.mentions into outgoing content; parse incoming m.mentions into Message.mentionedUserIds.
Server & DB mapping
packages/core/src/matrix/server/actions.ts, packages/core/src/matrix/server/queries.ts
Added getMatrixUserIdsByPrivySubsAction and findMatrixUserIdsByPrivyUserIds to batch-resolve Privy subs → Matrix IDs (auth-guarded action + drizzle DB query).
Client hook for mappings
packages/core/src/matrix/client/hooks/use-matrix-user-ids-by-privy-subs.ts, packages/core/src/matrix/client/hooks/index.ts
New SWR hook useMatrixUserIdsByPrivySubs (normalizes subs, fetches mapping with auth token); exported via hooks index.
Composer & mention picker
packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx, .../human-chat-mention-token.ts, .../human-chat-mention-candidate-row.tsx
Active @ token detection, candidate picker UI, candidate row component, keyboard navigation/selection, token insertion with canonical MXID, send/voice coordination, layout changes.
Timeline rendering & message bubble
packages/epics/src/common/human-chat-panel/human-chat-panel-message-bubble.tsx, .../parse-simple-matrix-html.tsx
Render MXID pills in plaintext and HTML via transformText, new MXID-aware parsing pipeline, prefer roster labels via resolvers, and highlight rows when viewer is mentioned.
Mentions inbox, tabs & header
packages/epics/src/common/human-chat-panel/human-chat-panel-mention-inbox.tsx, .../human-chat-panel-tabs.tsx, .../human-chat-panel-header.tsx, packages/epics/src/common/human-chat-panel/index.ts
Added Mentions tab, bell with unread badge, mentions inbox tab, routing/href to notification centre, exported mention components and types.
Right-panel integration & unread logic
packages/epics/src/common/human-right-panel.tsx, packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts, packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts
Wired mention candidates into right-panel, introduced scroll-to-event targeting, added unread mention counting (local timeline+server highlight) and capped flag, and Matrix display-name heuristics/shortening.
Coherence / Space Memory wiring
apps/web/src/app/[lang]/dho/[id]/@tab/coherence/page.tsx, apps/web/src/app/[lang]/dho/[id]/@tab/layout.tsx, packages/epics/src/coherence/components/coherence-block.tsx
Removed Coherence gating; added spaceMemoryEnabled prop and conditional SpaceMemory section rendering; layout changes to omit server-side coherence flag fetch.
i18n
packages/i18n/src/messages/*.{json}
Added mention-related translations (en, de, fr, es, pt) for tabs, inbox, bell aria strings, empty states and controls.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Composer as Composer (client)
    participant Picker as Mention Picker UI
    participant Hook as useMatrixUserIdsByPrivySubs
    participant Provider as MatrixProvider
    participant Server as App Server
    participant DB as Database

    User->>Composer: Type "@" + query
    Composer->>Picker: open picker / request candidates
    Picker->>Hook: request mapping for privySubs (if needed)
    Hook->>Server: getMatrixUserIdsByPrivySubsAction(privyUserIds)
    Server->>DB: findMatrixUserIdsByPrivyUserIds(...)
    DB-->>Server: return mappings
    Server-->>Hook: mappings
    Hook-->>Picker: mapping results
    User->>Picker: select candidate (matrixUserId)
    Picker-->>Composer: selected matrixUserId
    Composer->>Provider: sendMessage(content, mentionUserIds)
    Provider->>Provider: mergeMatrixMentionsIntoContent(...)
    Provider->>Matrix Server: send m.room.message (includes m.mentions.user_ids)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • evgenibir
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The pull request title 'human chat @mentions' does not follow the required conventional commits format (type(scope): description). Update the title to follow conventional commits format, e.g., 'feat(chat): human chat @mentions' or 'feat(human-chat): add @mentions support'.
Docstring Coverage ⚠️ Warning Docstring coverage is 43.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/human-chat-mentions-specs-93f4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@alexprate
alexprate marked this pull request as ready for review April 18, 2026 10:23

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/requirements/Features/human-chat-mentions/requirements.md`:
- Around line 116-119: The policy matrix currently conflates the "Use category
default" and "Only `@mentions`" options, causing ambiguity; update the mapping
table so "Use category default" and "Only `@mentions`" are represented as two
distinct choices (keep the radio option names exactly: "Use category default"
and "Only `@mentions`"), ensure each maps to its own behavior and persistence key
(so the table shows separate mappings for notifications/unread counts and
mention highlighting), and mirror this change in the other instance of the table
(the block that currently mirrors lines ~210-214) so validation and persistence
checks can unambiguously reference each option.
- Around line 94-97: The requirements leave thread-mode vs room-level unread
counters ambiguous for HumanChatPanelTabs and HumanChatPanelHeader (FR-BADGE-1 /
FR-BADGE-2); update the spec to explicitly define badge semantics: state whether
badge counts come from room highlight counts, thread-specific highlight counts,
or a merged value (e.g., room highlights + thread highlights or thread-only when
in thread mode), and specify capping behavior (e.g., "99+"), scope (room vs
thread vs global), and how the bell icon mirrors the same computed value;
reference HumanChatPanelTabs and HumanChatPanelHeader and add a short example or
decision rule clarifying how thread-mode toggling updates the badge to avoid
overcounting.
- Around line 243-249: Section 10 currently lists four unresolved product
decisions (Self-mention, Category default, Bell panel vs aside route, Thread
notification) that block implementation; update the spec by making explicit
decisions for each item (or define a short-term default and an owner/ticket for
longer discussion): decide whether self-mentions are allowed in the picker or
excluded, specify the source/location of the “category” until first-class
categories exist, choose embedded NotificationCentreForm vs navigation to
`@aside/notification-centre` for the bell panel, and state whether v1 supports
room-level only or requires client-side filtering for thread notifications;
annotate the section with the chosen default, rationale in one sentence, and
link or assign an owner/ticket for any items deferred.
- Around line 159-160: The ordered list items referencing reply behavior
("m.in_reply_to") and media-only caption merging ("m.mentions") trigger
markdownlint MD029; update the numbering style to match the project's configured
ordered-list rule (either make every item start with "1." or renumber
sequentially) so the list markers are consistent (e.g., replace "3." and "4."
with the appropriate style), keeping the existing text about preserving reply
relation and merging m.mentions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35a4ee48-70ab-4c9b-ad2c-6d7ccba083ac

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc7f0d and 196e9af.

📒 Files selected for processing (1)
  • docs/requirements/Features/human-chat-mentions/requirements.md

Comment thread docs/requirements/Features/human-chat-mentions/requirements.md
Comment thread docs/requirements/Features/human-chat-mentions/requirements.md
Comment thread docs/requirements/Features/human-chat-mentions/requirements.md Outdated
Comment thread docs/requirements/Features/human-chat-mentions/requirements.md Outdated
@alexprate alexprate changed the title docs: human chat @mentions — implementation spec human chat @mentions Apr 18, 2026
cursor Bot pushed a commit that referenced this pull request Apr 18, 2026
…ursor/human-chat-mentions-specs-93f4

Integrate PR #2161 (human chat enabled by default) into PR #2159.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/epics/src/coherence/components/coherence-block.tsx (1)

52-78: ⚠️ Potential issue | 🔴 Critical

Verify signal navigation fallback when the Human Chat kill-switch is active.

When HYPHA_DISABLE_HUMAN_CHAT=true, layout.tsx passes right={undefined} to PanelWrapLayout, so the right sidebar is never rendered. However, coherence-block.tsx unconditionally calls useHumanChatPanel() and always passes onSignalClick={handleSignalClick} to SignalGrid, preventing the Link fallback (basePath/${signal.slug}) from being used. When a signal is clicked, openCoherenceChat() executes and sets state, but since the right panel slot was never passed to the layout, the panel cannot open—resulting in a silent failure with no navigation.

Either:

  1. Conditionally pass onSignalClick based on humanChatEnabled, preserving the Link fallback when disabled, OR
  2. Provide an alternative navigation mechanism in openCoherenceChat() for the disabled case.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/coherence/components/coherence-block.tsx` around lines 52
- 78, The Signal click currently always invokes
useHumanChatPanel().openCoherenceChat (via handleSignalClick) which fails
silently when the right-panel kill-switch is active; change coherence-block.tsx
so the onSignalClick prop is only passed when human chat is enabled (or have
handleSignalClick gate its behavior by checking humanChatEnabled): obtain the
humanChatEnabled flag (or read the same env/config used by layout), and either
(A) only pass onSignalClick={handleSignalClick} to SignalSection/SignalGrid when
humanChatEnabled is true so the Link fallback (basePath/${signal.slug}) is used
otherwise, or (B) update handleSignalClick to check humanChatEnabled and, when
false, navigate to `${basePath}/${signal.slug}` (e.g., router.push or
window.location) instead of calling openCoherenceChat; reference
useHumanChatPanel, openCoherenceChat, handleSignalClick, and
SignalSection/SignalGrid when making the change.
packages/feature-flags/src/index.ts (1)

25-55: ⚠️ Potential issue | 🟡 Minor

Toolbar override for enable-human-chat is read but the flag is no longer in discovery.

getEnableHumanChat (lines 113–132) still honors a Vercel toolbar override for the key enable-human-chat, but enableHumanChat was removed from flagDefinitionsForDiscovery. Without a discovery entry, the Vercel Flags toolbar won't advertise this flag, so the toolbar-driven kill path documented in the new JSDoc becomes essentially unreachable through the toolbar UI (only via manually crafted override cookies). Either re-add a discovery entry (with defaultValue: true and a kill-switch description), or drop the override read to avoid implying a capability the toolbar no longer exposes.

As per coding guidelines for packages/feature-flags/**: "Flags have clear naming conventions and documentation" and "Cleanup path exists for removing old flags."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/feature-flags/src/index.ts` around lines 25 - 55, The toolbar
override for key "enable-human-chat" referenced in getEnableHumanChat still
exists but the flag was removed from flagDefinitionsForDiscovery; restore
consistency by re-adding a discovery entry named enableHumanChat in
flagDefinitionsForDiscovery with key: 'enable-human-chat', defaultValue: true, a
clear kill-switch description (e.g., "Kill switch to disable human chat UI via
Vercel Flags toolbar"), origin: 'hypha' as const, and options: undefined as
undefined; alternatively, if you prefer removing toolbar exposure, update
getEnableHumanChat to stop reading the toolbar override for 'enable-human-chat'
so no UI-implied flag remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/requirements/Features/human-chat-mentions/requirements.md`:
- Around line 72-76: FR-COMP-2 currently restricts space-room chat suggestions
to Matrix joined members (room.getJoinedMembers()) which conflicts with the PR
adding Hypha roster candidates via Person.sub → matrix_user_links; update
FR-COMP-2 (and clarify FR-COMP-3 if needed) to allow inclusion of Hypha
"Members" tab roster candidates (e.g., Person.sub mapped through
matrix_user_links) in the suggestion list for space chats while retaining the v1
rule to exclude the current user; explicitly state that
bridged-only/profile-backed users linked via Person.sub → matrix_user_links are
valid candidates and that room.getJoinedMembers() remains the default source if
no roster is available.

In `@packages/core/src/matrix/mentions.ts`:
- Around line 15-16: The MATRIX_MXID_IN_PLAIN_TEXT regex currently allows
homeserver colons but greedily captures a trailing punctuation colon (e.g.
"@alice:matrix.org: hello"); update the constant MATRIX_MXID_IN_PLAIN_TEXT to
keep permitting colons inside the homeserver portion (so bridged/privy MXIDs
still match) but add a negative lookahead after the homeserver capture that
prevents including a colon that is immediately followed by whitespace or
end-of-string (i.e., disallow a trailing punctuation colon), ensuring internal
colons remain valid while punctuation colons are excluded.

In `@packages/epics/src/common/human-chat-panel/human-chat-mention-token.ts`:
- Around line 37-38: The regex that validates the mention query uses /\w/ which
is ASCII-only and rejects Unicode names; update the validation in
human-chat-mention-token by replacing the test /^[\w.=\-/:@]*$/i with a
Unicode-aware pattern using Unicode property escapes (e.g. \p{L} and \p{N}) and
the u flag so characters like é, ö and apostrophes are allowed, keeping the
MAX_QUERY_LEN and afterAt slicing logic unchanged; then add unit tests for
queries such as "José", "Zoë", "O'Connor" and names with diacritics to ensure
the mention picker no longer closes for these inputs.

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`:
- Around line 752-771: openMentionPicker is inserting a second '@' when the
caret is already immediately after one; update the logic inside
openMentionPicker (referencing textareaRef, value, needsSpace, prefix, caret,
onChange, syncAtState) so that if before.endsWith('@') you do not append another
'@' (only add a leading space when needed), compute next and caret accordingly,
then call onChange(next) and requestAnimationFrame to focus,
setSelectionRange(caret, caret), autoResize(), and syncAtState(next, caret) with
the corrected caret position.
- Around line 1193-1280: The capture handlers are intercepting Enter even when a
focused control (attach/mic/emoji buttons, menu items, spoiler/delete, etc.)
should receive it; update handleAttachMenuContentEnter,
handleDraftAttachmentsKeyDownCapture, and handleComposerShellKeyDownCapture to
bail out if the event target is a focusable control by adding a guard that
checks e.target is an HTMLElement and returns when it's an HTMLButtonElement,
HTMLInputElement, HTMLTextAreaElement, or has contentEditable="true" or a
semantic role like "button"/"menuitem"/"option"/"link" (or otherwise focusable),
placing this check before e.preventDefault()/sendMessage so Enter activates the
focused control instead of sending the message.

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-messages.tsx`:
- Around line 308-321: The effect in useLayoutEffect currently calls
onConsumedScrollTarget unconditionally which clears scrollTargetEventId even
when the target DOM row wasn't found; change it so that after resolving esc and
querying containerRef.current you only call onConsumedScrollTarget() and set
stickToBottomRef.current = false after a successful scroll (i.e., when row
instanceof HTMLElement and scrollIntoView was invoked), leaving the id intact if
no row was found so subsequent renders/timelineRows updates can retry scrolling;
locate this logic around the useLayoutEffect that references
scrollTargetEventId, containerRef, timelineRows, onConsumedScrollTarget, and
stickToBottomRef and move the consumption and stick-to-bottom update into the
successful-path only.

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-tabs.tsx`:
- Around line 80-117: Tab button labels hide unread counts because the badge
spans are aria-hidden; update the tab rendering in the tabs.map loop (the button
with id `chat-tab-${tab.key}` and props `aria-selected`/`aria-controls`) to
include an accessible label: when `chatBadgeLabel` or `mentionBadgeLabel` is
present, set a descriptive `aria-label` on the button (e.g., `${tab.label},
${chatBadgeLabel} unread` or translated equivalent) so screen readers announce
the count; keep the visual badge aria-hidden but ensure the `aria-label` uses
the app's i18n translation utility and includes `tab.label` plus the badge count
for both the 'chat' and 'mentions' cases.

In `@packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts`:
- Around line 8-13: The current checks on the trimmed display name (variable l
compared against matrixUserId) only detect synthetic prefixes that include
"_privy_" (e.g. /^prev_privy_/), so change the prefix checks to match the
configured synthetic prefixes themselves (e.g. /^prev_/i and /^prod_/i) and also
include a generic privy check like /^privy_/i or the existing /privy_did_privy/i
if still needed; update the three if statements that test l (the ones with
/^prev_privy_/i, /^prod_privy_/i, /privy_did_privy/i) to instead test for the
correct prefix patterns so display names like "prev_123" or "prod_bridge_user"
are treated as synthetic/trusted when compared to matrixUserId.

In `@packages/epics/src/common/human-right-panel.tsx`:
- Around line 539-589: The message renderer uses resolveMemberLabel which only
checks Matrix room membership and fallbacks, causing roster-only labels built in
mentionCandidates to be lost; fix by surfacing and prioritizing roster labels:
when constructing mentionCandidates (in mentionCandidates useMemo) store the
roster label via personRosterLabel (e.g., add privySub and/or a rosterLabel
field alongside avatarUrl), and update resolveMemberLabel to first check that
roster-derived label map (or accept the privySub/rosterLabel on the mention
payload) before falling back to matrixMemberDisplayLabel/mxid; ensure you
reference mentionCandidates, personRosterLabel, subToMatrixUserId, and
resolveMemberLabel when making these changes so selected mention pills render
the same Hypha-profile labels.
- Around line 600-609: The current mentionPickerEnabled useMemo only checks
joined Matrix members (via client.getRoom(...).getJoinedMembers()) so it returns
false when there are roster-backed candidates; change the predicate to check for
any mention candidates (joined members OR the roster-backed candidates computed
earlier for the Members tab) and return true if either list has length>0, and
include the roster/candidates variable in the useMemo dependency array alongside
client, roomId, currentUserId, and mentionMembershipEpoch so the picker enables
whenever real mention candidates exist.

In `@packages/feature-flags/src/index.ts`:
- Around line 106-132: getEnableHumanChat currently defaults to true which
violates the feature-flags guideline that features must default to off; change
the function so that when no toolbar override, cookie, legacy flag, or env var
is present it returns false instead of true, and also simplify the toolbar
override handling by replacing the two-branch check (if toolbarHumanChat ===
true / === false) with a single conditional like "if (toolbarHumanChat !==
undefined) return toolbarHumanChat;"; update the JSDoc/comment to reflect the
opt-out kill-switch semantics and ensure references to HYPHA_DISABLE_HUMAN_CHAT
remain accurate.

In `@packages/i18n/src/messages/de.json`:
- Line 1670: The ARIA label for unread mentions under the key
"mentionInboxBellAria" is not pluralized; replace the plain string with ICU
plural syntax using the count variable so singular reads "1 ungelesene
Erwähnung" and other counts use "ungelesene Erwähnungen" (e.g. use "{count,
plural, one {# ungelesene Erwähnung} other {# ungelesene Erwähnungen}}"). Ensure
the JSON value for mentionInboxBellAria uses that ICU plural pattern and keeps
the same "count" interpolation token.

In `@packages/i18n/src/messages/en.json`:
- Line 1671: The aria label value for the key mentionInboxBellAria currently
uses a plain string ("{count} unread mentions") which is incorrect for singular;
update the value to an ICU pluralization message that uses the count variable
and provides distinct forms for one (singular) and other (plural) so screen
readers announce "1 unread mention" vs "2 unread mentions" (use the
mentionInboxBellAria key and the {count, plural, ...} pattern to implement
this).

In `@packages/i18n/src/messages/es.json`:
- Line 1670: The aria label value for the key mentionInboxBellAria must use ICU
pluralization instead of a fixed string; update the value of
mentionInboxBellAria to an ICU plural format that handles singular and plural
(e.g. include at least the "one" and "other" forms, and optionally "=0") using
the {count, plural, one {...} other {...}} pattern so the label correctly reads
"1 mención sin leer" for count=1 and the plural for other counts.

In `@packages/i18n/src/messages/fr.json`:
- Line 1670: The ARIA label "mentionInboxBellAria" is not pluralized and
displays "1 mentions non lues"; update the value to use ICU plural syntax
expected by next-intl so it chooses the correct singular/plural form, e.g. use
the {count, plural, one {# mention non lue} other {# mentions non lues}} pattern
for the "mentionInboxBellAria" message key so the label is grammatically correct
for count=1 and other counts.

In `@packages/i18n/src/messages/pt.json`:
- Line 1670: The aria label for unread mentions (key mentionInboxBellAria) must
use ICU pluralization so it shows "menção" for a single unread and "menções" for
others; update the value to an ICU plural message that includes the {count}
placeholder and defines at least the "one" and "other" forms (e.g., one =>
singular "menção não lida", other => plural "menções não lidas") to ensure
correct grammar for count = 1 and other counts.

---

Outside diff comments:
In `@packages/epics/src/coherence/components/coherence-block.tsx`:
- Around line 52-78: The Signal click currently always invokes
useHumanChatPanel().openCoherenceChat (via handleSignalClick) which fails
silently when the right-panel kill-switch is active; change coherence-block.tsx
so the onSignalClick prop is only passed when human chat is enabled (or have
handleSignalClick gate its behavior by checking humanChatEnabled): obtain the
humanChatEnabled flag (or read the same env/config used by layout), and either
(A) only pass onSignalClick={handleSignalClick} to SignalSection/SignalGrid when
humanChatEnabled is true so the Link fallback (basePath/${signal.slug}) is used
otherwise, or (B) update handleSignalClick to check humanChatEnabled and, when
false, navigate to `${basePath}/${signal.slug}` (e.g., router.push or
window.location) instead of calling openCoherenceChat; reference
useHumanChatPanel, openCoherenceChat, handleSignalClick, and
SignalSection/SignalGrid when making the change.

In `@packages/feature-flags/src/index.ts`:
- Around line 25-55: The toolbar override for key "enable-human-chat" referenced
in getEnableHumanChat still exists but the flag was removed from
flagDefinitionsForDiscovery; restore consistency by re-adding a discovery entry
named enableHumanChat in flagDefinitionsForDiscovery with key:
'enable-human-chat', defaultValue: true, a clear kill-switch description (e.g.,
"Kill switch to disable human chat UI via Vercel Flags toolbar"), origin:
'hypha' as const, and options: undefined as undefined; alternatively, if you
prefer removing toolbar exposure, update getEnableHumanChat to stop reading the
toolbar override for 'enable-human-chat' so no UI-implied flag remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0e5d5937-d24b-49aa-b721-79426942c831

📥 Commits

Reviewing files that changed from the base of the PR and between 196e9af and 97ff663.

📒 Files selected for processing (44)
  • apps/web-e2e/src/human-chat-panel-avatar.spec.ts
  • apps/web-e2e/src/human-chat-panel-feature-flag.spec.ts
  • apps/web-e2e/src/human-chat-panel-header.spec.ts
  • apps/web-e2e/src/human-chat-panel-members.spec.ts
  • apps/web-e2e/src/human-chat-panel-resize.spec.ts
  • apps/web-e2e/src/human-chat-panel-space-switch.spec.ts
  • apps/web-e2e/src/menu-top-consistent-height.spec.ts
  • apps/web-e2e/src/pages/human-chat-panel.page.ts
  • apps/web-e2e/src/panel-layout.spec.ts
  • apps/web-e2e/src/panels-space-context.spec.ts
  • apps/web/src/app/[lang]/dho/[id]/@tab/coherence/page.tsx
  • docs/requirements/Features/human-chat-mentions/requirements.md
  • packages/cookie/src/constants.ts
  • packages/core/src/matrix/__tests__/mentions.test.ts
  • packages/core/src/matrix/client/hooks/index.ts
  • packages/core/src/matrix/client/hooks/use-matrix-user-ids-by-privy-subs.ts
  • packages/core/src/matrix/client/providers/matrix-provider.tsx
  • packages/core/src/matrix/edit-room-message-media-caption.ts
  • packages/core/src/matrix/index.ts
  • packages/core/src/matrix/mentions.ts
  • packages/core/src/matrix/rich-reply.ts
  • packages/core/src/matrix/server/actions.ts
  • packages/core/src/matrix/server/queries.ts
  • packages/core/src/matrix/types.ts
  • packages/epics/src/coherence/components/coherence-block.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-mention-candidate-row.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-mention-token.ts
  • packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-header.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-mention-inbox.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-message-bubble.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-messages.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-tabs.tsx
  • packages/epics/src/common/human-chat-panel/index.ts
  • packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts
  • packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts
  • packages/epics/src/common/human-chat-panel/parse-simple-matrix-html.tsx
  • packages/epics/src/common/human-right-panel.tsx
  • packages/feature-flags/src/index.ts
  • packages/i18n/src/messages/de.json
  • packages/i18n/src/messages/en.json
  • packages/i18n/src/messages/es.json
  • packages/i18n/src/messages/fr.json
  • packages/i18n/src/messages/pt.json
💤 Files with no reviewable changes (1)
  • apps/web-e2e/src/human-chat-panel-space-switch.spec.ts

Comment thread docs/requirements/Features/human-chat-mentions/requirements.md Outdated
Comment thread packages/core/src/matrix/mentions.ts
Comment thread packages/epics/src/common/human-chat-panel/human-chat-mention-token.ts Outdated
Comment thread packages/i18n/src/messages/de.json Outdated
Comment thread packages/i18n/src/messages/en.json Outdated
Comment thread packages/i18n/src/messages/es.json Outdated
Comment thread packages/i18n/src/messages/fr.json Outdated
Comment thread packages/i18n/src/messages/pt.json Outdated
@alexprate
alexprate self-requested a review April 19, 2026 00:26

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
packages/feature-flags/src/index.ts (1)

25-55: ⚠️ Potential issue | 🟡 Minor

Keep the Human Chat flag discoverable if toolbar rollback is documented.

Line 115 documents Vercel Flags Toolbar rollback for enable-human-chat, and lines 118-120 still read that override, but the flag is absent from flagDefinitionsForDiscovery. Operators may not see the toggle in the toolbar during rollback. Re-add a discovery entry or remove the toolbar rollback claim from the JSDoc. As per coding guidelines for packages/feature-flags/**: "Flags have clear naming conventions and documentation."

Also applies to: 115-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/feature-flags/src/index.ts` around lines 25 - 55,
flagDefinitionsForDiscovery is missing the discovery entry for the human chat
flag referenced in the toolbar rollback docs; either re-add the flag object or
remove the toolbar rollback mention. To fix, add a new entry under
flagDefinitionsForDiscovery named (e.g.) enableHumanChat with key:
'enable-human-chat', defaultValue: false, a brief description matching the
toolbar use, origin: 'hypha' as const, and options: undefined as undefined so
operators can discover the toggle in the toolbar; alternatively, if the flag is
intentionally removed, delete the toolbar rollback lines that reference
`enable-human-chat` so docs are consistent.
packages/i18n/src/messages/de.json (1)

1628-1672: ⚠️ Potential issue | 🟡 Minor

Separate numeric counts from capped display labels for ICU plural messages.

mentionInboxBellAria is already broken: it receives '99+' (a string) when count is capped, but next-intl's ICU plural requires a numeric argument for correct plural selection. Converting tabChatWithMentionCount and tabMentionsWithMentionCount to ICU plural without fixing this architectural issue will introduce the same defect.

Pass the numeric count for plural logic and a separate display label (countLabel or similar) for the UI string. Alternatively, use dedicated capped translation keys (e.g., "tabChatWithMentionCountCapped") that don't rely on ICU plural.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/i18n/src/messages/de.json` around lines 1628 - 1672, The ICU plural
messages (mentionInboxBellAria, tabChatWithMentionCount,
tabMentionsWithMentionCount) must use a numeric count for plural selection and a
separate display label for the capped UI value: change callers to pass count
(number) for plural logic and pass countLabel (string, e.g., "99+") for the
rendered text, then update the translations to use {count, plural, ...} for
plural-sensitive parts and {countLabel} where the capped string should appear;
alternatively add dedicated capped keys (e.g., tabChatWithMentionCountCapped)
and use those where the UI receives a capped string so no ICU plural receives a
non-numeric value.
packages/i18n/src/messages/pt.json (1)

1628-1672: ⚠️ Potential issue | 🟡 Minor

Fix ICU plural counting for capped badge labels.

The mentionInboxBellAria message uses ICU plural syntax but receives a string '99+' when the mention count is capped (line 149, human-chat-panel-mention-inbox.tsx: count: countIsCapped ? '99+' : unreadCount). ICU plural requires a numeric count to select the correct grammatical form; passing a string breaks this logic.

Additionally, tabChatWithMentionCount and tabMentionsWithMentionCount receive capped string labels ('99+') from the badge label logic, which causes grammatical issues in Portuguese (e.g., "99+ menções não lidas" is incorrect; it should vary based on singular/plural).

To resolve: Pass a numeric count value for plural selection alongside a separate countLabel display string, or create dedicated message keys for capped badge labels (e.g., mentionInboxBellAriaCapped, tabChatWithMentionCountCapped).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/i18n/src/messages/pt.json` around lines 1628 - 1672, The ICU
pluralization is broken because badge logic passes a capped string ('99+') into
plural keys; update the rendering code (where countIsCapped is used in
human-chat-panel-mention-inbox.tsx) to always pass a numeric "count" to
pluralized message keys (mentionInboxBellAria, tabChatWithMentionCount,
tabMentionsWithMentionCount) and supply a separate "countLabel" or
"countDisplay" string for visual badges when capped, or alternatively add new
capped message keys (mentionInboxBellAriaCapped, tabChatWithMentionCountCapped,
tabMentionsWithMentionCountCapped) and use those when countIsCapped is true so
ICU plural selectors receive a number while the UI shows '99+'.
packages/i18n/src/messages/en.json (1)

1629-1674: ⚠️ Potential issue | 🟠 Major

Pass numeric counts to plural formatters; use string labels for display-only text.

The mentionInboxBellAria message uses ICU plural formatting ({count, plural, ...}), but the caller in HumanChatPanelMentionBell (line 149) passes countIsCapped ? '99+' : unreadCount—when capped, this sends a non-numeric string to the formatter, breaking plural selection.

Additionally, the tab aria messages tabChatWithMentionCount and tabMentionsWithMentionCount always say "mentions" (plural form) regardless of count, so when the badge shows "1", the aria-label reads "1 unread mentions".

Create a separate key for the capped/string-label case in the bell component, and either make tab text grammar-neutral or use numeric counts for those as well. Ensure plural messages receive only numeric values.

🌐 Suggested locale adjustment
-    "tabChatWithMentionCount": "{tabLabel}, {count} unread mentions",
+    "tabChatWithMentionCount": "{tabLabel}, unread mentions: {count}",
...
-    "tabMentionsWithMentionCount": "{tabLabel}, {count} unread mentions",
+    "tabMentionsWithMentionCount": "{tabLabel}, unread mentions: {count}",
...
     "mentionInboxBellAria": "{count, plural, one {# unread mention} other {# unread mentions}}",
+    "mentionInboxBellAriaCapped": "{countLabel} unread mentions",

Then update HumanChatPanelMentionBell (line 146–151) to use the capped key when appropriate:

aria-label={
  unreadCount > 0
    ? countIsCapped
      ? t('mentionInboxBellAriaCapped', { countLabel: '99+' })
      : t('mentionInboxBellAria', { count: unreadCount })
    : t('mentionInboxBellAriaEmpty')
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/i18n/src/messages/en.json` around lines 1629 - 1674, The locale keys
send non-numeric strings into an ICU plural formatter and use incorrect plural
wording for singular counts; update locales and component usage so plural
formatters only receive numbers and capped labels use a separate string. Add a
new key (e.g., "mentionInboxBellAriaCapped") that accepts a string label
(countLabel) for the capped case, keep "mentionInboxBellAria" numeric (count)
for ICU plural, and change HumanChatPanelMentionBell to call the capped key when
countIsCapped and the numeric key otherwise; also adjust
"tabChatWithMentionCount" and "tabMentionsWithMentionCount" to be
grammar-neutral (e.g., "{tabLabel}, {count} unread mention(s)" or use ICU plural
with numeric {count}) so they do not always force the plural form when count ===
1.
packages/i18n/src/messages/fr.json (1)

1628-1673: ⚠️ Potential issue | 🟠 Major

Fix ICU plural formatter mismatch: pass numeric values, not capped string labels.

mentionInboxBellAria is an ICU plural message in all locales but receives '99+' (string) instead of a numeric count when countIsCapped is true in HumanChatPanelMentionBell. This breaks next-intl's plural form selection.

The tab aria messages (tabChatWithMentionCount, tabMentionsWithMentionCount) also receive string badge labels and produce grammatically incorrect output for singular counts (e.g., "1 mentions non lues" in French).

Solutions:

  1. Add separate message keys for capped display (e.g., mentionInboxBellAriaCapped) that accept a string label instead of a numeric count, and update both callers to use numeric paths when available.
  2. Or redesign tab messages to use plural syntax instead of simple interpolation.
Example fix for mention bell (requires caller update)

Add to all locale files (fr.json, en.json, de.json, es.json, pt.json):

"mentionInboxBellAria": "{count, plural, one {# mention non lue} other {# mentions non lues}}",
+"mentionInboxBellAriaCapped": "{countLabel} mentions non lues",

Update caller in human-chat-panel-mention-inbox.tsx:

aria-label={
  unreadCount > 0
    ? countIsCapped
      ? t('mentionInboxBellAriaCapped', { countLabel: '99+' })
      : t('mentionInboxBellAria', { count: unreadCount })
    : t('mentionInboxBellAriaEmpty')
}

Same pattern applies to tabChatWithMentionCount and tabMentionsWithMentionCount in human-chat-panel-tabs.tsx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/i18n/src/messages/fr.json` around lines 1628 - 1673, The ICU plural
keys (mentionInboxBellAria, tabChatWithMentionCount,
tabMentionsWithMentionCount) are being fed string badge labels like "99+" which
breaks plural selection; add new capped-string variants (e.g.,
mentionInboxBellAriaCapped, tabChatWithMentionCountCapped,
tabMentionsWithMentionCountCapped) that accept a string param (countLabel) in
all locale JSONs and update the callers in HumanChatPanelMentionBell and
HumanChatPanelTabs to use the capped key when countIsCapped is true and the
numeric plural key (mentionInboxBellAria / tab* with {count}) otherwise,
ensuring plural keys always receive a numeric count.
packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx (1)

1067-1096: ⚠️ Potential issue | 🟠 Major

Use stop() for user-initiated stops and abort() for send/discard interrupts.

When pressing the stop button, the current code calls abort(), which per MDN does not attempt to return the final recognized result. This can trigger the onerror handler without a final transcript, causing dictationError to display and losing the captured audio. For normal user stops, use stop() instead, which finalizes and delivers any captured audio. Reserve abort() for send/discard contexts where interruption should discard pending results.

The app already distinguishes these contexts via dictationInterruptForSendRef in prepareSendSession(). Apply this pattern to stopDictation() to call the appropriate method:

  • User stop (button click at lines 1113, 2028): stop()
  • Send interrupt (line 1089 via prepareSendSession): abort()
🎙️ Proposed fix
-  const stopDictation = useCallback(() => {
+  const stopDictation = useCallback((discard = false) => {
     const r = speechRecognitionRef.current;
     if (r) {
       try {
-        r.abort();
+        if (discard) {
+          r.abort();
+        } else {
+          r.stop();
+        }
       } catch {
         try {
-          r.stop();
+          r.abort();
         } catch {
           // ignore
         }
       }
-      speechRecognitionRef.current = null;
+      if (discard) {
+        speechRecognitionRef.current = null;
+      }
     }
-    dictationPrefixRef.current = '';
-    setIsDictating(false);
+    if (discard) {
+      dictationPrefixRef.current = '';
+      setIsDictating(false);
+    }
   }, []);
@@
     dictationInterruptForSendRef.current = true;
     discardVoiceDraftIfIdleRef.current = true;
     voiceAsAttachmentRef.current = false;
-    stopDictation();
+    stopDictation(true);
     stopVoiceRecording();
   }, [stopDictation, stopVoiceRecording]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`
around lines 1067 - 1096, The stop logic should call stop() for user-initiated
stops and abort() for send/discard interrupts: modify stopDictation() to check
dictationInterruptForSendRef.current and call r.abort() when true and r.stop()
when false (then clear speechRecognitionRef and reset
dictationPrefixRef/isDictating as before), leaving prepareSendSession() to set
dictationInterruptForSendRef.current = true (it already does) before calling
stopDictation(); also ensure any user stop handlers set
dictationInterruptForSendRef.current = false before invoking stopDictation() so
user button clicks use stop() not abort().
♻️ Duplicate comments (2)
packages/feature-flags/src/index.ts (1)

106-135: ⚠️ Potential issue | 🟠 Major

Default-on still violates the feature-flag safety guideline.

getEnableHumanChat() now defaults to true, which exposes the Matrix provider/panel and token endpoint unless a kill switch is configured. If this opt-out rollout is approved, update the repository feature-flag guideline or add an explicit exception/runbook; otherwise keep the safe default false. As per coding guidelines for packages/feature-flags/**: "Default values are safe (features default to off)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/feature-flags/src/index.ts` around lines 106 - 135,
getEnableHumanChat currently returns true by default which violates the safe
"off-by-default" feature-flag guideline; change the behavior so that
getEnableHumanChat (and its use of
readBooleanOverride/getVercelToolbarFlagOverrides and cookie/env checks like
HYPHA_DISABLE_HUMAN_CHAT, HYPHA_ENABLE_HUMAN_CHAT,
NEXT_PUBLIC_DISABLE_HUMAN_CHAT, NEXT_PUBLIC_ENABLE_HUMAN_CHAT) only enables
human chat when an explicit positive override is present and otherwise returns
false, or if you must keep the opt-out rollout add an explicit documented
exception/runbook and update the repository feature-flag guideline to reference
that runbook instead of changing the function behavior.
packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts (1)

16-21: ⚠️ Potential issue | 🟠 Major

Match generic prev_ synthetic labels too.

The PR objective calls out prev_ as a known synthetic prefix, but this only matches prev_privy. Preview bridged names like prev_bridge_user can still leak into mention rows and fallback labels.

🐛 Proposed fix
 function stemLooksLikeBridgedPrivyLocalpart(stem: string): boolean {
   const s = stem.trim();
-  if (/^prev_privy/i.test(s) || /^prod_privy/i.test(s)) return true;
+  if (/^prev_/i.test(s) || /^prod_privy/i.test(s)) return true;
   /** Production Matrix bridge locals often start with `prod_` (not always `prod_privy`). */
   if (/^prod_/i.test(s)) return true;
   return false;
 }
...
   const looksSynthetic =
-    /privy|_did_|^prod_|^prev_privy|^prod_privy/i.test(local) ||
+    /privy|_did_|^prod_|^prev_|^prod_privy/i.test(local) ||
     local.length > 28;

Also applies to: 60-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts`
around lines 16 - 21, The function stemLooksLikeBridgedPrivyLocalpart currently
only matches /^prev_privy/i for the prev_* synthetic prefix; change that to
match the generic /^prev_/i (case-insensitive) so any preview bridged names like
"prev_bridge_user" are detected, and update the analogous checks elsewhere in
the file (the similar prev_/prod_ check around the other occurrence) to use the
same /^prev_/i pattern while keeping the existing /^prod_/i and /^prod_privy/i
logic intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/matrix/mentions.ts`:
- Around line 27-49: The function normalizePlainTextMxidCaptureFromMatch fails
to strip a single trailing punctuation colon for port-qualified MXIDs (e.g.
"@alice:matrix.org:8448:") because it checks the port regexp before removing the
colon; change the loop in normalizePlainTextMxidCaptureFromMatch to first set m
= without (i.e., strip one trailing ':'), then if the new m (the `without`
value) matches /:\d+$/ break to avoid removing the port digits—this ensures you
remove only the stray punctuation colon but preserve valid host:port MXIDs and
still stop after a single removal for cases like "@user:hs: hello".

In
`@packages/epics/src/common/human-chat-panel/human-chat-panel-message-bubble.tsx`:
- Around line 935-944: The Matrix mention pill double-prefixes labels because
the span always prepends "@" while resolveMx can return a value starting with
"@"; update the rendering in the seg.kind === 'mxid' branch (the parts.push that
creates the span) to avoid adding an extra "@" by normalizing the resolved label
from resolveMx(seg.full) — e.g., strip a leading "@" from the label if present
(or conditionally prefix "@" only when the label doesn't already start with "@")
before using it in the span.
- Around line 891-919: The regex should avoid matching `@` inside emails by
requiring a left boundary; update localHandleRe (and hence reLocal in
mapPlainFragment) to include a capture for the left-context, e.g.
/(^|\s)@([^\s@]{1,100}?)(?=\s|$|[.,!?;:])/g, and then in mapPlainFragment adjust
match handling to treat mh[1] (the prefix, either empty or a whitespace) as
plain text (push it or include it in the preceding slice) and use mh[2] as the
handle to render the pill; ensure you update indices/last computation
accordingly so the prefix isn’t swallowed or turned into a pill.

In `@packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts`:
- Around line 41-75: countUnreadMentionMessagesForUser currently parses mentions
from each original event's content and skips m.replace events, which can miss
mention edits; change it to resolve the latest replacement content for the
target event before parsing. For each event ev, call
getMessageReplaceTargetEventId(ev) to get targetId; if targetId is non-null,
scan the same timeline (room.getLiveTimeline().getEvents()) for a replacement
event whose getMessageReplaceTargetEventId(...) === targetId and is not
redacted, and use that replacement's getContent() as the wire content; otherwise
fall back to ev.getContent(); then pass that resolved content into
parseMentionUserIdsFromWireContent and continue the rest of the checks in
countUnreadMentionMessagesForUser.

In `@packages/i18n/src/messages/es.json`:
- Around line 1628-1631: The tab mention-count labels (tabChatWithMentionCount
and tabMentionsWithMentionCount) currently hardcode "{count} menciones sin leer"
which is incorrect for singular; change both keys to use ICU pluralization for
{count} (e.g. a plural block handling one and other) and keep {tabLabel}
prefixed — update the string values to use "{tabLabel}, {count, plural, one {#
mención sin leer} other {# menciones sin leer}}" (and include any =0 branch if
desired) so the aria/label logic matches the pluralized bell label.

---

Outside diff comments:
In `@packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx`:
- Around line 1067-1096: The stop logic should call stop() for user-initiated
stops and abort() for send/discard interrupts: modify stopDictation() to check
dictationInterruptForSendRef.current and call r.abort() when true and r.stop()
when false (then clear speechRecognitionRef and reset
dictationPrefixRef/isDictating as before), leaving prepareSendSession() to set
dictationInterruptForSendRef.current = true (it already does) before calling
stopDictation(); also ensure any user stop handlers set
dictationInterruptForSendRef.current = false before invoking stopDictation() so
user button clicks use stop() not abort().

In `@packages/feature-flags/src/index.ts`:
- Around line 25-55: flagDefinitionsForDiscovery is missing the discovery entry
for the human chat flag referenced in the toolbar rollback docs; either re-add
the flag object or remove the toolbar rollback mention. To fix, add a new entry
under flagDefinitionsForDiscovery named (e.g.) enableHumanChat with key:
'enable-human-chat', defaultValue: false, a brief description matching the
toolbar use, origin: 'hypha' as const, and options: undefined as undefined so
operators can discover the toggle in the toolbar; alternatively, if the flag is
intentionally removed, delete the toolbar rollback lines that reference
`enable-human-chat` so docs are consistent.

In `@packages/i18n/src/messages/de.json`:
- Around line 1628-1672: The ICU plural messages (mentionInboxBellAria,
tabChatWithMentionCount, tabMentionsWithMentionCount) must use a numeric count
for plural selection and a separate display label for the capped UI value:
change callers to pass count (number) for plural logic and pass countLabel
(string, e.g., "99+") for the rendered text, then update the translations to use
{count, plural, ...} for plural-sensitive parts and {countLabel} where the
capped string should appear; alternatively add dedicated capped keys (e.g.,
tabChatWithMentionCountCapped) and use those where the UI receives a capped
string so no ICU plural receives a non-numeric value.

In `@packages/i18n/src/messages/en.json`:
- Around line 1629-1674: The locale keys send non-numeric strings into an ICU
plural formatter and use incorrect plural wording for singular counts; update
locales and component usage so plural formatters only receive numbers and capped
labels use a separate string. Add a new key (e.g., "mentionInboxBellAriaCapped")
that accepts a string label (countLabel) for the capped case, keep
"mentionInboxBellAria" numeric (count) for ICU plural, and change
HumanChatPanelMentionBell to call the capped key when countIsCapped and the
numeric key otherwise; also adjust "tabChatWithMentionCount" and
"tabMentionsWithMentionCount" to be grammar-neutral (e.g., "{tabLabel}, {count}
unread mention(s)" or use ICU plural with numeric {count}) so they do not always
force the plural form when count === 1.

In `@packages/i18n/src/messages/fr.json`:
- Around line 1628-1673: The ICU plural keys (mentionInboxBellAria,
tabChatWithMentionCount, tabMentionsWithMentionCount) are being fed string badge
labels like "99+" which breaks plural selection; add new capped-string variants
(e.g., mentionInboxBellAriaCapped, tabChatWithMentionCountCapped,
tabMentionsWithMentionCountCapped) that accept a string param (countLabel) in
all locale JSONs and update the callers in HumanChatPanelMentionBell and
HumanChatPanelTabs to use the capped key when countIsCapped is true and the
numeric plural key (mentionInboxBellAria / tab* with {count}) otherwise,
ensuring plural keys always receive a numeric count.

In `@packages/i18n/src/messages/pt.json`:
- Around line 1628-1672: The ICU pluralization is broken because badge logic
passes a capped string ('99+') into plural keys; update the rendering code
(where countIsCapped is used in human-chat-panel-mention-inbox.tsx) to always
pass a numeric "count" to pluralized message keys (mentionInboxBellAria,
tabChatWithMentionCount, tabMentionsWithMentionCount) and supply a separate
"countLabel" or "countDisplay" string for visual badges when capped, or
alternatively add new capped message keys (mentionInboxBellAriaCapped,
tabChatWithMentionCountCapped, tabMentionsWithMentionCountCapped) and use those
when countIsCapped is true so ICU plural selectors receive a number while the UI
shows '99+'.

---

Duplicate comments:
In `@packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts`:
- Around line 16-21: The function stemLooksLikeBridgedPrivyLocalpart currently
only matches /^prev_privy/i for the prev_* synthetic prefix; change that to
match the generic /^prev_/i (case-insensitive) so any preview bridged names like
"prev_bridge_user" are detected, and update the analogous checks elsewhere in
the file (the similar prev_/prod_ check around the other occurrence) to use the
same /^prev_/i pattern while keeping the existing /^prod_/i and /^prod_privy/i
logic intact.

In `@packages/feature-flags/src/index.ts`:
- Around line 106-135: getEnableHumanChat currently returns true by default
which violates the safe "off-by-default" feature-flag guideline; change the
behavior so that getEnableHumanChat (and its use of
readBooleanOverride/getVercelToolbarFlagOverrides and cookie/env checks like
HYPHA_DISABLE_HUMAN_CHAT, HYPHA_ENABLE_HUMAN_CHAT,
NEXT_PUBLIC_DISABLE_HUMAN_CHAT, NEXT_PUBLIC_ENABLE_HUMAN_CHAT) only enables
human chat when an explicit positive override is present and otherwise returns
false, or if you must keep the opt-out rollout add an explicit documented
exception/runbook and update the repository feature-flag guideline to reference
that runbook instead of changing the function behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 310beacd-b53c-4cd0-902e-12dfff3aeb92

📥 Commits

Reviewing files that changed from the base of the PR and between 97ff663 and 052eaca.

📒 Files selected for processing (17)
  • docs/requirements/Features/human-chat-mentions/requirements.md
  • packages/core/src/matrix/__tests__/mentions.test.ts
  • packages/core/src/matrix/mentions.ts
  • packages/epics/src/common/human-chat-panel/human-chat-mention-token.ts
  • packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-message-bubble.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-messages.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-tabs.tsx
  • packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts
  • packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts
  • packages/epics/src/common/human-right-panel.tsx
  • packages/feature-flags/src/index.ts
  • packages/i18n/src/messages/de.json
  • packages/i18n/src/messages/en.json
  • packages/i18n/src/messages/es.json
  • packages/i18n/src/messages/fr.json
  • packages/i18n/src/messages/pt.json

Comment thread packages/core/src/matrix/mentions.ts
Comment thread packages/epics/src/common/human-chat-panel/human-chat-panel-message-bubble.tsx Outdated
Comment thread packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts
Comment thread packages/i18n/src/messages/es.json Outdated

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/matrix/mentions.ts`:
- Around line 52-54: The current isLikelyMatrixUserId allows whitespace after
the colon (e.g. "@alice:matrix.org extra"); tighten the regex to require no
whitespace after the colon by changing the pattern in isLikelyMatrixUserId to
something like /^@[^\s:]+:[^\s]+$/ (or equivalent) so the whole string must be a
valid single Matrix ID, and update parseMentionUserIdsFromWireContent to
validate and filter each wire-provided ID by calling isLikelyMatrixUserId
(discard invalid IDs) before returning or emitting mention IDs; this ensures
consistency between explicit ID handling and wire content parsing.

In `@packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts`:
- Around line 47-109: countUnreadMentionMessagesForUser currently rescans the
room timeline for every event via wireContentForMentionParse, causing O(n²)
behavior; precompute a map from rootEventId -> latest non-redacted replacement
event once at the start of the function (iterate
room.getLiveTimeline().getEvents() once, use getMessageReplaceTargetEventId and
isRedactedRoomMessageEvent to update the map by timestamp) and then replace
calls to wireContentForMentionParse(room, ev) with a direct lookup into that map
(or change wireContentForMentionParse to accept the precomputed map) so
parseMentionUserIdsFromWireContent uses the cached latest content for each root
id while keeping the rest of the logic in countUnreadMentionMessagesForUser
(including readUpToId compare, room.compareEventOrdering, and
room.hasUserReadEvent) unchanged.

In `@packages/feature-flags/src/index.ts`:
- Around line 56-62: The flag object enableSpaceMemory uses a mixed-case key
'enable-Space-Memory'; change its key property to lowercase kebab-case
'enable-space-memory' and update any other occurrences of that key (including
the second enableSpaceMemory entry elsewhere in the file) as well as any
readBooleanOverride calls that reference the old mixed-case string so toolbar
overrides and discovery keys match exactly; ensure the description/origin remain
unchanged and run tests/typechecks after updating references.
- Around line 138-152: Move the emergency-disable checks to run before honoring
any toolbar override: first read cookies and environment disables
(HYPHA_DISABLE_HUMAN_CHAT and NEXT_PUBLIC_DISABLE_HUMAN_CHAT) and immediately
return false if set; then apply legacy enable logic (HYPHA_ENABLE_HUMAN_CHAT)
and only after those checks call getVercelToolbarFlagOverrides() and
readBooleanOverride() to consider the toolbar flag; ensure
readBooleanOverride('enable-human-chat') cannot override the disables so the
disable paths are non-overridable.

In `@packages/i18n/src/messages/en.json`:
- Around line 1629-1632: The ICU plural messages tabChatWithMentionCount,
tabMentionsWithMentionCount, and mentionInboxBellAria must be split to use a
numeric plural operand and a separate display string; update the JSON entries to
accept both count (numeric) for the plural branch and displayCount for the
visible/badge text (e.g. "{tabLabel}, {count, plural, one {# unread mention}
other {# unread mentions}} ({displayCount})" or similar), then update the
callers human-chat-panel-mention-inbox.tsx (where '99+' is passed),
human-chat-panel-tabs.tsx (where chatBadgeLabel and mentionBadgeLabel are
passed) to pass count: unreadCount (number) and displayCount: badgeLabel
(string) instead of passing the display string into the plural operand.

In `@packages/i18n/src/messages/fr.json`:
- Around line 1628-1631: The ICU plural messages (tabChatWithMentionCount,
tabMentionsWithMentionCount, mentionInboxBellAria) currently use `{count,
plural,...}` but call sites pass display strings like "99+", breaking plural
selection; update the JSON messages to accept both a numeric `count` for plural
selection and a `displayCount` for the badge text (e.g. change templates to use
`{count, plural, one {...} other {...}}` for grammar and insert `{displayCount}`
where the visible badge should appear), apply the same change to the English
counterparts, and update the component call sites (human-chat-panel-tabs and
human-chat-panel-mention-inbox) to pass `count: unreadCount` (numeric) and
`displayCount: badgeLabel` (string, e.g. `countIsCapped ? '99+' :
String(unreadCount)`).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bbf79eb6-8793-4987-b834-22b33e74e02d

📥 Commits

Reviewing files that changed from the base of the PR and between 052eaca and bd4f8ec.

📒 Files selected for processing (14)
  • apps/web/src/app/[lang]/dho/[id]/@tab/coherence/page.tsx
  • apps/web/src/app/[lang]/dho/[id]/@tab/layout.tsx
  • packages/cookie/src/constants.ts
  • packages/core/src/matrix/__tests__/mentions.test.ts
  • packages/core/src/matrix/mentions.ts
  • packages/epics/src/coherence/components/coherence-block.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-message-bubble.tsx
  • packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts
  • packages/feature-flags/src/index.ts
  • packages/i18n/src/messages/de.json
  • packages/i18n/src/messages/en.json
  • packages/i18n/src/messages/es.json
  • packages/i18n/src/messages/fr.json
  • packages/i18n/src/messages/pt.json

Comment thread packages/core/src/matrix/mentions.ts
Comment thread packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts
Comment thread packages/feature-flags/src/index.ts
Comment on lines +138 to +152
const overrides = await getVercelToolbarFlagOverrides();
const toolbarHumanChat = readBooleanOverride(overrides, 'enable-human-chat');
if (toolbarHumanChat !== undefined) return toolbarHumanChat;

const store = await cookies();

if (store.get(HYPHA_DISABLE_HUMAN_CHAT)?.value === 'true') {
return false;
}

const legacyEnable = store.get(HYPHA_ENABLE_HUMAN_CHAT)?.value;
if (legacyEnable === 'false') return false;

if (process.env.NEXT_PUBLIC_DISABLE_HUMAN_CHAT === 'true') return false;
if (process.env.NEXT_PUBLIC_ENABLE_HUMAN_CHAT === 'false') return false;

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.

⚠️ Potential issue | 🟠 Major

Make emergency disables non-overridable.

Line 140 returns a toolbar true before checking HYPHA_DISABLE_HUMAN_CHAT or NEXT_PUBLIC_DISABLE_HUMAN_CHAT, so a stale toolbar override can bypass the documented rollback path and still enable Matrix token issuance. Check disable cookie/env first, then apply non-emergency overrides.

🛠️ Proposed fix
 export async function getEnableHumanChat(): Promise<boolean> {
+  const store = await cookies();
+
+  if (store.get(HYPHA_DISABLE_HUMAN_CHAT)?.value === 'true') {
+    return false;
+  }
+
+  if (process.env.NEXT_PUBLIC_DISABLE_HUMAN_CHAT === 'true') return false;
+
   const overrides = await getVercelToolbarFlagOverrides();
   const toolbarHumanChat = readBooleanOverride(overrides, 'enable-human-chat');
   if (toolbarHumanChat !== undefined) return toolbarHumanChat;
 
-  const store = await cookies();
-
-  if (store.get(HYPHA_DISABLE_HUMAN_CHAT)?.value === 'true') {
-    return false;
-  }
-
   const legacyEnable = store.get(HYPHA_ENABLE_HUMAN_CHAT)?.value;
   if (legacyEnable === 'false') return false;
 
-  if (process.env.NEXT_PUBLIC_DISABLE_HUMAN_CHAT === 'true') return false;
   if (process.env.NEXT_PUBLIC_ENABLE_HUMAN_CHAT === 'false') return false;
 
   return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const overrides = await getVercelToolbarFlagOverrides();
const toolbarHumanChat = readBooleanOverride(overrides, 'enable-human-chat');
if (toolbarHumanChat !== undefined) return toolbarHumanChat;
const store = await cookies();
if (store.get(HYPHA_DISABLE_HUMAN_CHAT)?.value === 'true') {
return false;
}
const legacyEnable = store.get(HYPHA_ENABLE_HUMAN_CHAT)?.value;
if (legacyEnable === 'false') return false;
if (process.env.NEXT_PUBLIC_DISABLE_HUMAN_CHAT === 'true') return false;
if (process.env.NEXT_PUBLIC_ENABLE_HUMAN_CHAT === 'false') return false;
const store = await cookies();
if (store.get(HYPHA_DISABLE_HUMAN_CHAT)?.value === 'true') {
return false;
}
if (process.env.NEXT_PUBLIC_DISABLE_HUMAN_CHAT === 'true') return false;
const overrides = await getVercelToolbarFlagOverrides();
const toolbarHumanChat = readBooleanOverride(overrides, 'enable-human-chat');
if (toolbarHumanChat !== undefined) return toolbarHumanChat;
const legacyEnable = store.get(HYPHA_ENABLE_HUMAN_CHAT)?.value;
if (legacyEnable === 'false') return false;
if (process.env.NEXT_PUBLIC_ENABLE_HUMAN_CHAT === 'false') return false;
return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/feature-flags/src/index.ts` around lines 138 - 152, Move the
emergency-disable checks to run before honoring any toolbar override: first read
cookies and environment disables (HYPHA_DISABLE_HUMAN_CHAT and
NEXT_PUBLIC_DISABLE_HUMAN_CHAT) and immediately return false if set; then apply
legacy enable logic (HYPHA_ENABLE_HUMAN_CHAT) and only after those checks call
getVercelToolbarFlagOverrides() and readBooleanOverride() to consider the
toolbar flag; ensure readBooleanOverride('enable-human-chat') cannot override
the disables so the disable paths are non-overridable.

Comment on lines +1629 to +1632
"tabChatWithMentionCount": "{tabLabel}, {count, plural, one {# unread mention} other {# unread mentions}}",
"tabMembers": "Members",
"tabMentions": "Mentions",
"tabMentionsWithMentionCount": "{tabLabel}, {count, plural, one {# unread mention} other {# unread mentions}}",

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate mention-count i18n calls and inspect whether `count` is passed as a display string.
rg -n -C5 "tabChatWithMentionCount|tabMentionsWithMentionCount|mentionInboxBellAria" packages/epics

Repository: hypha-dao/hypha-web

Length of output: 3493


🏁 Script executed:

sed -n '1629,1632p; 1673,1674p' packages/i18n/src/messages/en.json

Repository: hypha-dao/hypha-web

Length of output: 497


Separate numeric plural count from the displayed badge count.

The message keys tabChatWithMentionCount, tabMentionsWithMentionCount, and mentionInboxBellAria use ICU plural syntax ({count, plural, ...}), which expects a numeric operand. However, callers pass display strings:

  • '99+' in human-chat-panel-mention-inbox.tsx:149
  • chatBadgeLabel in human-chat-panel-tabs.tsx:90
  • mentionBadgeLabel in human-chat-panel-tabs.tsx:95

Pass a numeric count for plural evaluation and a separate displayCount for the accessible label text. Update message definitions to reference both:

Proposed message shape
-    "tabChatWithMentionCount": "{tabLabel}, {count, plural, one {# unread mention} other {# unread mentions}}",
+    "tabChatWithMentionCount": "{tabLabel}, {displayCount} {count, plural, one {unread mention} other {unread mentions}}",
@@
-    "tabMentionsWithMentionCount": "{tabLabel}, {count, plural, one {# unread mention} other {# unread mentions}}",
+    "tabMentionsWithMentionCount": "{tabLabel}, {displayCount} {count, plural, one {unread mention} other {unread mentions}}",
@@
-    "mentionInboxBellAria": "{count, plural, one {# unread mention} other {# unread mentions}}",
+    "mentionInboxBellAria": "{displayCount} {count, plural, one {unread mention} other {unread mentions}}",

Update callers to pass both count: unreadCount and displayCount: badgeLabel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/i18n/src/messages/en.json` around lines 1629 - 1632, The ICU plural
messages tabChatWithMentionCount, tabMentionsWithMentionCount, and
mentionInboxBellAria must be split to use a numeric plural operand and a
separate display string; update the JSON entries to accept both count (numeric)
for the plural branch and displayCount for the visible/badge text (e.g.
"{tabLabel}, {count, plural, one {# unread mention} other {# unread mentions}}
({displayCount})" or similar), then update the callers
human-chat-panel-mention-inbox.tsx (where '99+' is passed),
human-chat-panel-tabs.tsx (where chatBadgeLabel and mentionBadgeLabel are
passed) to pass count: unreadCount (number) and displayCount: badgeLabel
(string) instead of passing the display string into the plural operand.

Comment thread packages/i18n/src/messages/fr.json
cursoragent and others added 20 commits April 19, 2026 17:49
Add implementation-ready requirements for Matrix intentional mentions,
mention highlights, tab/bell badges, mention side panel, notification
centre entry, and per-room notification policy with phased commit plan.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Add MSC3952-style m.mentions.user_ids on send/edit paths, expose
mentionedUserIds on Message, helpers for extracting MXIDs from plaintext,
and unit tests for mention helpers.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Add @-token parsing, room member suggestions, keyboard navigation, and
Matrix MXID insertion. Enable the At toolbar when members are available
and pass joined members from the human chat panel. Add i18n for the
mention list and empty state.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Map Matrix m.mentions into the panel message model and apply a
Discord-style row tint for the mentioned viewer only.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Expose unreadMentionCount from matrix-chat-unread (highlight-only) and
wire it to the Chat tab. Compute unread state even when Members is
active so mention badges stay accurate.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Add a right Sheet listing recent m.mentions events with settings link to
the aside notification centre. Place the bell in the chat header with an
unread highlight badge and scroll the timeline when a row is chosen.
Extend HumanChatPanelMessages with optional scroll-to-event targeting.
Add i18n strings for the inbox.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Relax mergeMatrixMentionsIntoContent generics so Hypha/media payloads stay
typed. Restore explicit casts where media bundle content widens union types
beyond RoomMessageEventContent — fixes Deploy Preview check-types CI.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Clarify room-level badge semantics in thread mode; split policy matrix;
fix MD029 list numbering; resolve §10 open points; update problem statement.
Exclude current user from @ mention picker to match §10 decision.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Human chat and Matrix token API are on unless disabled via emergency
kill switch (cookie, env, or Vercel toolbar). Remove enable-human-chat
from Vercel flag discovery. Simplify CoherenceBlock and e2e tests.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Use Matrix RoomMember display resolution (including profile fallback) instead of falling back to raw MXIDs when member.name equals the user id. Shorten synthetic-looking localparts for secondary lines. Render mention rows with avatars, truncation, and clearer layout.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
The bell trigger used h-9 while sidebar toggle and chat action use h-7, stretching the header vertically on preview. Align dimensions and icon/badge scale with the existing header controls.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Replace the Sheet overlay and header bell with a Mentions tab next to Chat/Members, inline mention list panel, and notification centre link aligned to the right of the secondary nav.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
DHO notification centre lives under /[lang]/dho/[id]/[tab]/notification-centre; include the active tab from the path and align network/my-spaces branches with the profile menu.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Reintroduce h-7 bell with unread mention count beside the sidebar toggle; click switches to the Mentions tab without changing header height.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
… localparts

Root cause: message body stores full MXIDs; the old mention pill regex split on the
first colon, so bridged localparts (privy_did_...) were truncated. Formatted HTML
also showed raw text. Parse MXIDs with the same pattern as the send path, resolve
labels via room member state, and pass the same resolver into rich HTML text nodes.
Export MATRIX_MXID_IN_PLAIN_TEXT; add test for localparts with colons.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
When the textarea is cleared, only setting height to auto can leave a stale
scrollHeight in some browsers. Shrink to 0 then re-measure for empty content
and reset scroll on the mirror backdrop.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
…rofiles

Merge space roster (Person + matrix_user_links batch) with Matrix joined members so
bridged-only participants appear and labels match Members. Treat Privy-style Matrix
displaynames as technical and resolve picker rows via Person like the timeline.

Adds batch Privy→MXID lookup server action and MentionCandidateRow component.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Subscribe to RoomStateEvent.Members and NewMember so mentionCandidates
recomputes when a second user joins or leaves, enabling the @ control
without a full page reload.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Enable the mention control only when another user is joined in the room,
while keeping roster-merged suggestions for the dropdown. Uses the same
membership epoch bump as candidate refresh.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
senderProfileLoading stayed true while senderPrivySub was set but usePersonBySub
never returned a person, leaving the grey Skeleton bar with no text. Only show the
skeleton during active link/person fetches; otherwise show Matrix fallback label.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
cursoragent and others added 13 commits April 19, 2026 17:49
Replace harsh red outline on flat grey with soft gradient glass, subtle
border glow, inset highlight, and an upgraded REC dot with halo animation.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
On send, abort dictation and stop voice recording, discard in-flight voice
attachment blob, and suppress SpeechRecognition finalize from repopulating the
composer after parent clears input. Route all send triggers through sendMessage.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Reset backdrop + textarea heights with min-height cleared, reflow both layers,
and run resize in useLayoutEffect when value updates so empty state paints at
one-line height (fixes stuck tall box after dictation/async clear).

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
The textarea and URL backdrop were two grid cells; the row height was the max
of both, so the shell stayed as tall as the last multiline state. Use a
single-flow textarea with an absolute-positioned mirror; only the textarea
sets block height. Simplifies autoResize (no backdrop height sync).

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
needsHyphaProfileForMatrixLabel used /^prev_privy_/ on the raw label; Matrix
often sends @prev_privy_* so resolveSenderProfile stayed false and usePersonBySub
never ran. Strip leading @ when matching bridged stems (shared helper with room labels).

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
…parts

Detect bridged `prev_`/`prod_` display strings after `shortenMatrixIdForDisplay`
(ellipsis instead of full underscore). Stop skipping those names when they equal
the raw MXID so technical-name checks and profile resolution can run.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Normalize MXID extraction when a colon trails the homeserver; widen mention
query validation for Unicode names; avoid duplicate @ and Enter hijacking in the
composer; consume scroll targets only after a successful scroll; reuse roster
labels for mention pills; sync FR-COMP-2 with roster merge; add tab ARIA labels
and ICU plurals for bell/tab badges.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Timeline headers now prefer merged Hypha roster labels like mention pills.
Unread mention badges use max(homeserver highlight, client counts from
m.mentions + read cursor) when highlight stays zero.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
…bar check

Collapse enable-human-chat toolbar override to a single undefined check.
Document intentional default-on semantics vs generic feature-flag guidelines.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
- Normalize MXID captures after host:port before stripping sentence colons
- Avoid email domains as @handle pills; prevent @@ in MXID pills
- Parse mentions from latest m.replace content for local unread counts
- Add enableHumanChat to Vercel flag discovery (default true, documented)
- ICU plural tab aria strings for mention badges (all locales)

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Remove enable-coherence checks from the Coherence route and DHO tab shell so preview deployments can validate signals without production flag parity.

Introduce enable-Space-Memory (HYPHA_ENABLE_SPACE_MEMORY / NEXT_PUBLIC_ENABLE_SPACE_MEMORY) and render Space Memory only when enabled.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
… guideline

CodeRabbit: clarify rollback (HYPHA_DISABLE_HUMAN_CHAT) and that returning false
by default would require an explicit product decision.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
@alexprate
alexprate force-pushed the cursor/human-chat-mentions-specs-93f4 branch from 880f0bd to e3472db Compare April 19, 2026 15:49

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

Actionable comments posted: 8

♻️ Duplicate comments (8)
packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts (1)

47-144: ⚠️ Potential issue | 🟠 Major

Build the replacement map once per unread-count pass.

countUnreadMentionMessagesForUser iterates the timeline, then wireContentForMentionParse scans the same timeline per message. This keeps the unread mention path O(n²), and Line 139 computes it on every state derivation.

⚡ Proposed fix
+function latestReplacementByRootId(
+  timeline: MatrixEvent[],
+): Map<string, MatrixEvent> {
+  const latestByRootId = new Map<string, MatrixEvent>();
+
+  for (const cand of timeline) {
+    const rootId = getMessageReplaceTargetEventId(cand);
+    if (!rootId) continue;
+    if (isRedactedRoomMessageEvent(cand)) continue;
+
+    const prev = latestByRootId.get(rootId);
+    if (!prev || cand.getTs() >= prev.getTs()) {
+      latestByRootId.set(rootId, cand);
+    }
+  }
+
+  return latestByRootId;
+}
+
 /** Latest message content for mention parsing (`m.replace` edits target the root id). */
 function wireContentForMentionParse(
-  room: Room,
   rootEvent: MatrixEvent,
+  replacementsByRootId: Map<string, MatrixEvent>,
 ): Record<string, unknown> | undefined {
   const rootId = rootEvent.getId();
   if (!rootId) return undefined;
 
-  let latest = rootEvent;
-  let latestTs = rootEvent.getTs();
-
-  for (const cand of room.getLiveTimeline().getEvents()) {
-    if (getMessageReplaceTargetEventId(cand) !== rootId) continue;
-    if (isRedactedRoomMessageEvent(cand)) continue;
-    const ts = cand.getTs();
-    if (ts >= latestTs) {
-      latestTs = ts;
-      latest = cand;
-    }
-  }
+  const latest = replacementsByRootId.get(rootId) ?? rootEvent;
 
   const content = latest.getContent();
   return content && typeof content === 'object'
@@
 ): number {
   const timeline = room.getLiveTimeline().getEvents();
+  const replacementsByRootId = latestReplacementByRootId(timeline);
   let n = 0;
@@
     const ids = parseMentionUserIdsFromWireContent(
-      wireContentForMentionParse(room, ev),
+      wireContentForMentionParse(ev, replacementsByRootId),
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts` around
lines 47 - 144, The unread-mention path is O(n²) because
countUnreadMentionMessagesForUser calls wireContentForMentionParse which rescans
the timeline per message; fix by building the message-replacement map once in
computeHumanChatUnreadState (map from original/root event id to the latest
non-redacted event content or its parsed wire content), then change
countUnreadMentionMessagesForUser to accept that precomputed map (or a lookup
function) instead of calling wireContentForMentionParse so each timeline event
is inspected only once; update references to wireContentForMentionParse and the
call at Line 139 to use the new precomputed map and ensure functions still
handle missing/null entries.
packages/i18n/src/messages/fr.json (1)

1637-1640: ⚠️ Potential issue | 🟠 Major

Keep numeric plural selection separate from capped display text.

These ICU plural messages still depend on count for both grammar and display. If the UI passes capped labels like 99+, plural selection breaks; use numeric count plus a separate displayCount parameter across locale counterparts and call sites.

🌐 Proposed message shape
-    "tabChatWithMentionCount": "{tabLabel}, {count, plural, one {# mention non lue} other {# mentions non lues}}",
+    "tabChatWithMentionCount": "{tabLabel}, {displayCount} {count, plural, one {mention non lue} other {mentions non lues}}",
@@
-    "tabMentionsWithMentionCount": "{tabLabel}, {count, plural, one {# mention non lue} other {# mentions non lues}}",
+    "tabMentionsWithMentionCount": "{tabLabel}, {displayCount} {count, plural, one {mention non lue} other {mentions non lues}}",
@@
-    "mentionInboxBellAria": "{count, plural, one {# mention non lue} other {# mentions non lues}}",
+    "mentionInboxBellAria": "{displayCount} {count, plural, one {mention non lue} other {mentions non lues}}",

Also applies to: 1681-1681

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/i18n/src/messages/fr.json` around lines 1637 - 1640, The ICU plural
messages tabChatWithMentionCount and tabMentionsWithMentionCount currently use
the same `count` for plural selection and the displayed label, which breaks when
the UI passes capped strings like "99+"; change the message shape to use `count`
(numeric) for the plural selection and a separate `displayCount` (string) for
the shown label, update these two keys' ICU patterns accordingly, and update all
call sites to pass numeric `count` for plural logic and `displayCount` for
rendering (ensuring other locale files follow the same parameter names).
packages/i18n/src/messages/de.json (1)

1637-1640: ⚠️ Potential issue | 🟠 Major

Apply the same count/displayCount split here.

These German ICU plural strings have the same capped-label risk: count should stay numeric for plural selection, while displayCount should render values like 99+.

Also applies to: 1681-1681

docs/requirements/Features/human-chat-mentions/requirements.md (1)

108-108: ⚠️ Potential issue | 🟡 Minor

Remove the remaining notification-centre implementation ambiguity.

Line 248 resolves v1 to aside navigation, but Line 108 still allows embedding NotificationCentreForm in the mention panel. Make FR-PANEL-4 require the aside route for v1 so implementers do not treat both as valid.

📝 Proposed doc fix
-**FR-PANEL-4** Toolbar in panel: **Settings / gear** (or ellipsis) navigates to the existing **notification centre** experience (same **content** as `AsideNotificationCentrePage` / `NotificationCentreForm`), either by **routing to aside `notification-centre`** with close URL semantics mirroring `aside-notification-centre-page.tsx`, or by **embedding** the form inside the panel—implementation choice with preference for **reuse** of `NotificationCentreForm` and **one** canonical UX.
+**FR-PANEL-4** Toolbar in panel: **Settings / gear** (or ellipsis) navigates to the existing **notification centre** experience (same **content** as `AsideNotificationCentrePage` / `NotificationCentreForm`) by **routing to aside `notification-centre`** with close URL semantics mirroring `aside-notification-centre-page.tsx`. Embedding the form inside the mention panel is non-v1 unless product reopens the decision in §10.

Also applies to: 248-248

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/requirements/Features/human-chat-mentions/requirements.md` at line 108,
Update FR-PANEL-4 to remove the embedding option and require the aside route for
v1: change the text so the Settings/gear (or ellipsis) must navigate to the
existing notification centre via routing to the aside "notification-centre"
(mirroring aside-notification-centre-page.tsx / AsideNotificationCentrePage) and
state that implementers must reuse NotificationCentreForm through that aside
route rather than embedding it in the mention panel; ensure the wording
explicitly declares one canonical UX and disallows embedding for v1.
packages/feature-flags/src/index.ts (2)

64-70: ⚠️ Potential issue | 🟡 Minor

Use lowercase kebab-case for the Space Memory flag key.

enable-Space-Memory is inconsistent with the other toolbar keys and makes exact-match overrides easy to misconfigure. Prefer enable-space-memory in both discovery metadata and runtime lookup.

🛠️ Proposed fix
   enableSpaceMemory: {
-    key: 'enable-Space-Memory',
+    key: 'enable-space-memory',
     defaultValue: false,
     description: 'Show the Space Memory panel on the Coherence tab',
     origin: 'hypha' as const,
@@
 export async function getEnableSpaceMemory(): Promise<boolean> {
   return getBooleanFlagFromToolbarCookieOrEnv(
-    'enable-Space-Memory',
+    'enable-space-memory',
     HYPHA_ENABLE_SPACE_MEMORY,
     process.env.NEXT_PUBLIC_ENABLE_SPACE_MEMORY,
   );
 }

As per coding guidelines, packages/feature-flags/** should verify “Flags have clear naming conventions and documentation.”

Also applies to: 169-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/feature-flags/src/index.ts` around lines 64 - 70, The feature flag
definition enableSpaceMemory uses an inconsistent key 'enable-Space-Memory';
change its key to lowercase kebab-case 'enable-space-memory' in the
enableSpaceMemory object (and make the identical change in the other occurrence
around the second definition referenced) so discovery metadata and runtime
lookups use 'enable-space-memory' consistently with other toolbar keys.

149-167: ⚠️ Potential issue | 🟠 Major

Make the Human Chat kill switch non-overridable.

enable-human-chat=true from the toolbar currently returns before HYPHA_DISABLE_HUMAN_CHAT / NEXT_PUBLIC_DISABLE_HUMAN_CHAT, so a stale toolbar override can bypass the documented emergency rollback path.

🛠️ Proposed fix
 export async function getEnableHumanChat(): Promise<boolean> {
+  const store = await cookies();
+
+  if (store.get(HYPHA_DISABLE_HUMAN_CHAT)?.value === 'true') {
+    return false;
+  }
+
+  if (process.env.NEXT_PUBLIC_DISABLE_HUMAN_CHAT === 'true') return false;
+
   const overrides = await getVercelToolbarFlagOverrides();
   const toolbarHumanChat = readBooleanOverride(overrides, 'enable-human-chat');
   if (toolbarHumanChat !== undefined) return toolbarHumanChat;
 
-  const store = await cookies();
-
-  if (store.get(HYPHA_DISABLE_HUMAN_CHAT)?.value === 'true') {
-    return false;
-  }
-
   const legacyEnable = store.get(HYPHA_ENABLE_HUMAN_CHAT)?.value;
   if (legacyEnable === 'false') return false;
 
-  if (process.env.NEXT_PUBLIC_DISABLE_HUMAN_CHAT === 'true') return false;
   if (process.env.NEXT_PUBLIC_ENABLE_HUMAN_CHAT === 'false') return false;
 
   return true;
 }

As per coding guidelines, packages/feature-flags/** should ensure default values and flag evaluation are safe and deterministic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/feature-flags/src/index.ts` around lines 149 - 167, The toolbar
override returned by getVercelToolbarFlagOverrides/readBooleanOverride must not
short-circuit the kill switch; change getEnableHumanChat so that it checks the
non-overridable disable signals (cookie HYPHA_DISABLE_HUMAN_CHAT and env
NEXT_PUBLIC_DISABLE_HUMAN_CHAT and legacy HYPHA_ENABLE_HUMAN_CHAT=false) before
honoring a true toolbar override—i.e., evaluate and return false for any disable
condition first, then consult toolbarHumanChat for an explicit true/false or
fall back to the default true.
packages/core/src/matrix/mentions.ts (1)

52-54: ⚠️ Potential issue | 🟡 Minor

Validate and dedupe wire mention IDs consistently.

isLikelyMatrixUserId accepts whitespace after the homeserver, and parseMentionUserIdsFromWireContent returns unvalidated strings. Malformed m.mentions.user_ids can leak into UI/highlight logic.

🛡️ Proposed fix
 export function isLikelyMatrixUserId(id: string): boolean {
-  return /^@[^\s:]+:.+/.test(id);
+  return /^@[^\s:]+:[^\s]+$/.test(id);
 }
@@
   const ids = (raw as { user_ids?: unknown }).user_ids;
   if (!Array.isArray(ids)) return undefined;
-  const out = ids.filter((id): id is string => typeof id === 'string');
+  const out = [
+    ...new Set(
+      ids.filter(
+        (id): id is string =>
+          typeof id === 'string' && isLikelyMatrixUserId(id),
+      ),
+    ),
+  ];
   return out.length > 0 ? out : undefined;
 }

Also applies to: 131-140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/matrix/mentions.ts` around lines 52 - 54, The validation
for Matrix user IDs is too permissive and parseMentionUserIdsFromWireContent
returns unvalidated, potentially malformed IDs from m.mentions.user_ids; tighten
isLikelyMatrixUserId to reject whitespace after the homeserver (e.g. use a regex
like /^@[^\s:]+:[^\s]+$/) and update parseMentionUserIdsFromWireContent to:
iterate m.mentions.user_ids, trim values, validate each with
isLikelyMatrixUserId, collect only valid IDs into a Set to dedupe, and return
the deduped array so only well-formed, unique IDs reach UI/highlight logic.
packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts (1)

16-21: ⚠️ Potential issue | 🟠 Major

Treat all prev_ locals as synthetic, not only prev_privy.

The PR objective calls out prev_ as a known synthetic prefix, but the detector and shortener still only match prev_privy. prev_bridge_user-style labels will be trusted and shown.

🐛 Proposed fix
 function stemLooksLikeBridgedPrivyLocalpart(stem: string): boolean {
   const s = stem.trim();
-  if (/^prev_privy/i.test(s) || /^prod_privy/i.test(s)) return true;
-  /** Production Matrix bridge locals often start with `prod_` (not always `prod_privy`). */
-  if (/^prod_/i.test(s)) return true;
+  if (/^prev_/i.test(s) || /^prod_/i.test(s)) return true;
   return false;
 }
@@
   const looksSynthetic =
-    /privy|_did_|^prod_|^prev_privy|^prod_privy/i.test(local) ||
+    /privy|_did_|^prod_|^prev_/i.test(local) ||
     local.length > 28;

Also applies to: 60-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts`
around lines 16 - 21, The detector function stemLooksLikeBridgedPrivyLocalpart
currently only treats names starting with "prev_privy" as synthetic; change its
logic to treat any localpart starting with "prev_" as synthetic (e.g., replace
the /^prev_privy/i check with /^prev_/i or add an additional /^prev_/i test) so
labels like "prev_bridge_user" are caught; apply the same change to the
duplicate occurrence referenced at lines 60-62 to ensure all `prev_` locals are
treated consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/app/`[lang]/dho/[id]/@tab/layout.tsx:
- Line 15: NavigationTabs is being forced to show the Coherence tab by passing a
bare coherenceEnabled prop; revert this to use the actual feature-flag value so
getEffectiveDhoTab() can guard disabled routes. Replace the hard-coded/coercive
prop on NavigationTabs in the layout component with the feature-flag boolean
(the same flag used elsewhere to determine Coherence availability) so
coherenceEnabled is true only when the feature flag indicates it should be;
ensure getEffectiveDhoTab() remains the single source-of-truth for routing
behavior.

In `@packages/core/src/matrix/server/actions.ts`:
- Around line 69-84: The current getMatrixUserIdsByPrivySubsAction exposes
Privy→Matrix mapping if any non-empty authToken is passed; to fix, validate and
authorize the token instead of presence-only: in
getMatrixUserIdsByPrivySubsAction verify the JWT (e.g., check signature and
required claims/roles/scopes) and reject requests without proper claims before
calling findMatrixUserIdsByPrivyUserIds, enforce a hard cap on privyUserIds
length (e.g., max N) and return a clear error if exceeded, and ensure this
action is only callable from a trusted server boundary (or document/rename to a
private/internal function) so arbitrary clients cannot enumerate mappings.

In
`@packages/epics/src/common/human-chat-panel/human-chat-mention-candidate-row.tsx`:
- Around line 60-76: The row is being disabled while profile/link metadata loads
(busy), making picks fail; change the disabled logic so the button remains
selectable when we have a mention identifier (matrixUserId or
matrixFallbackLabel). Update the disabled prop on the button (and any uses of
busy for interactivity) to use something like disabled = {!matrixUserId &&
!matrixFallbackLabel} while keeping busy only for visual state (opacity) so
onClick/onPick still works during loading; touch the const busy and the button's
disabled prop and preserve onClick/onMouseDown handlers.

In `@packages/epics/src/common/human-chat-panel/human-chat-mention-token.ts`:
- Around line 14-18: isAtWordStart currently only treats whitespace as a valid
preceding character, so mentions after punctuation fail; update the
isAtWordStart function to return true when atIndex <= 0 or when the previous
character is NOT an alphanumeric character (i.e., open on punctuation), for
example replacing /\s/.test(prev) with a negated alphanumeric check such as
!/[A-Za-z0-9]/.test(prev) or, for broader Unicode support,
!/[\p{L}\p{N}]/u.test(prev) so mentions like "(`@alice`" or ",`@alice`" correctly
open the picker.

In
`@packages/epics/src/common/human-chat-panel/human-chat-panel-mention-inbox.tsx`:
- Around line 37-50: The inbox loop must ignore Matrix edit replacement events
so they don't produce entries with non-scrollable eventIds; inside the for-loop
that iterates events (where variables ev, currentUserId,
parseMentionUserIdsFromWireContent, isRedactedRoomMessageEvent are used) add a
guard to skip events whose content indicates an edit replacement (e.g. if
ev.getContent()?.["m.relates_to"]?.rel_type === "m.replace") and continue the
loop before collecting the event into out.

In `@packages/epics/src/common/human-chat-panel/human-chat-panel-tabs.tsx`:
- Around line 34-46: The ICU plural messages tabChatWithMentionCount and
tabMentionsWithMentionCount are being given preformatted badge strings
(chatBadgeLabel/mentionBadgeLabel) which breaks pluralization; instead, keep
chatBadgeLabel/mentionBadgeLabel for UI display but pass the raw numeric counts
(e.g., chatMentionCount and mentionTabBadgeCount — or their capped numeric value
like 99 if you intend to cap the plural logic) as the count argument to
tabChatWithMentionCount and tabMentionsWithMentionCount. Locate usages of those
message calls and replace the badge-label argument with the underlying numeric
variables while leaving the label construction (chatBadgeLabel,
mentionBadgeLabel) unchanged for rendering.

In `@packages/epics/src/common/human-chat-panel/parse-simple-matrix-html.tsx`:
- Around line 119-124: The text-case always wraps node text in a <span> even
when no transform is needed; update the case in parse-simple-matrix-html.tsx so
that when transformText is provided you return <span
key={k}>{transformText(n.value)}</span>, and when transformText is falsy you
return the raw string n.value (no extra element or key) to preserve the original
DOM/sibling structure; locate the switch case handling 'text' and modify the
return accordingly.

In `@packages/epics/src/common/human-right-panel.tsx`:
- Around line 1720-1725: The Mentions tab is using the Matrix-only resolver via
resolveMemberLabel which causes bridged users to show MXIDs; swap it to use the
roster-aware resolver by passing resolveMentionMemberLabel (or a thin wrapper
that delegates to resolveMentionMemberLabel) into the HumanChatPanelMentionTab's
resolveMemberLabel prop so the Mentions tab uses the roster override; update the
usage around HumanChatPanelMentionTab and ensure handleSelectMentionFromInbox
remains unchanged.

---

Duplicate comments:
In `@docs/requirements/Features/human-chat-mentions/requirements.md`:
- Line 108: Update FR-PANEL-4 to remove the embedding option and require the
aside route for v1: change the text so the Settings/gear (or ellipsis) must
navigate to the existing notification centre via routing to the aside
"notification-centre" (mirroring aside-notification-centre-page.tsx /
AsideNotificationCentrePage) and state that implementers must reuse
NotificationCentreForm through that aside route rather than embedding it in the
mention panel; ensure the wording explicitly declares one canonical UX and
disallows embedding for v1.

In `@packages/core/src/matrix/mentions.ts`:
- Around line 52-54: The validation for Matrix user IDs is too permissive and
parseMentionUserIdsFromWireContent returns unvalidated, potentially malformed
IDs from m.mentions.user_ids; tighten isLikelyMatrixUserId to reject whitespace
after the homeserver (e.g. use a regex like /^@[^\s:]+:[^\s]+$/) and update
parseMentionUserIdsFromWireContent to: iterate m.mentions.user_ids, trim values,
validate each with isLikelyMatrixUserId, collect only valid IDs into a Set to
dedupe, and return the deduped array so only well-formed, unique IDs reach
UI/highlight logic.

In `@packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts`:
- Around line 47-144: The unread-mention path is O(n²) because
countUnreadMentionMessagesForUser calls wireContentForMentionParse which rescans
the timeline per message; fix by building the message-replacement map once in
computeHumanChatUnreadState (map from original/root event id to the latest
non-redacted event content or its parsed wire content), then change
countUnreadMentionMessagesForUser to accept that precomputed map (or a lookup
function) instead of calling wireContentForMentionParse so each timeline event
is inspected only once; update references to wireContentForMentionParse and the
call at Line 139 to use the new precomputed map and ensure functions still
handle missing/null entries.

In `@packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts`:
- Around line 16-21: The detector function stemLooksLikeBridgedPrivyLocalpart
currently only treats names starting with "prev_privy" as synthetic; change its
logic to treat any localpart starting with "prev_" as synthetic (e.g., replace
the /^prev_privy/i check with /^prev_/i or add an additional /^prev_/i test) so
labels like "prev_bridge_user" are caught; apply the same change to the
duplicate occurrence referenced at lines 60-62 to ensure all `prev_` locals are
treated consistently.

In `@packages/feature-flags/src/index.ts`:
- Around line 64-70: The feature flag definition enableSpaceMemory uses an
inconsistent key 'enable-Space-Memory'; change its key to lowercase kebab-case
'enable-space-memory' in the enableSpaceMemory object (and make the identical
change in the other occurrence around the second definition referenced) so
discovery metadata and runtime lookups use 'enable-space-memory' consistently
with other toolbar keys.
- Around line 149-167: The toolbar override returned by
getVercelToolbarFlagOverrides/readBooleanOverride must not short-circuit the
kill switch; change getEnableHumanChat so that it checks the non-overridable
disable signals (cookie HYPHA_DISABLE_HUMAN_CHAT and env
NEXT_PUBLIC_DISABLE_HUMAN_CHAT and legacy HYPHA_ENABLE_HUMAN_CHAT=false) before
honoring a true toolbar override—i.e., evaluate and return false for any disable
condition first, then consult toolbarHumanChat for an explicit true/false or
fall back to the default true.

In `@packages/i18n/src/messages/fr.json`:
- Around line 1637-1640: The ICU plural messages tabChatWithMentionCount and
tabMentionsWithMentionCount currently use the same `count` for plural selection
and the displayed label, which breaks when the UI passes capped strings like
"99+"; change the message shape to use `count` (numeric) for the plural
selection and a separate `displayCount` (string) for the shown label, update
these two keys' ICU patterns accordingly, and update all call sites to pass
numeric `count` for plural logic and `displayCount` for rendering (ensuring
other locale files follow the same parameter names).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eb191bd3-6d0c-48c0-9c29-c5d0d913b0e9

📥 Commits

Reviewing files that changed from the base of the PR and between bd4f8ec and e3472db.

📒 Files selected for processing (45)
  • apps/web-e2e/src/human-chat-panel-avatar.spec.ts
  • apps/web-e2e/src/human-chat-panel-feature-flag.spec.ts
  • apps/web-e2e/src/human-chat-panel-header.spec.ts
  • apps/web-e2e/src/human-chat-panel-members.spec.ts
  • apps/web-e2e/src/human-chat-panel-resize.spec.ts
  • apps/web-e2e/src/human-chat-panel-space-switch.spec.ts
  • apps/web-e2e/src/menu-top-consistent-height.spec.ts
  • apps/web-e2e/src/pages/human-chat-panel.page.ts
  • apps/web-e2e/src/panel-layout.spec.ts
  • apps/web-e2e/src/panels-space-context.spec.ts
  • apps/web/src/app/[lang]/dho/[id]/@tab/coherence/page.tsx
  • apps/web/src/app/[lang]/dho/[id]/@tab/layout.tsx
  • docs/requirements/Features/human-chat-mentions/requirements.md
  • packages/cookie/src/constants.ts
  • packages/core/src/matrix/__tests__/mentions.test.ts
  • packages/core/src/matrix/client/hooks/index.ts
  • packages/core/src/matrix/client/hooks/use-matrix-user-ids-by-privy-subs.ts
  • packages/core/src/matrix/client/providers/matrix-provider.tsx
  • packages/core/src/matrix/edit-room-message-media-caption.ts
  • packages/core/src/matrix/index.ts
  • packages/core/src/matrix/mentions.ts
  • packages/core/src/matrix/rich-reply.ts
  • packages/core/src/matrix/server/actions.ts
  • packages/core/src/matrix/server/queries.ts
  • packages/core/src/matrix/types.ts
  • packages/epics/src/coherence/components/coherence-block.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-mention-candidate-row.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-mention-token.ts
  • packages/epics/src/common/human-chat-panel/human-chat-panel-chat-bar.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-header.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-mention-inbox.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-message-bubble.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-messages.tsx
  • packages/epics/src/common/human-chat-panel/human-chat-panel-tabs.tsx
  • packages/epics/src/common/human-chat-panel/index.ts
  • packages/epics/src/common/human-chat-panel/matrix-chat-unread.ts
  • packages/epics/src/common/human-chat-panel/matrix-room-member-display.ts
  • packages/epics/src/common/human-chat-panel/parse-simple-matrix-html.tsx
  • packages/epics/src/common/human-right-panel.tsx
  • packages/feature-flags/src/index.ts
  • packages/i18n/src/messages/de.json
  • packages/i18n/src/messages/en.json
  • packages/i18n/src/messages/es.json
  • packages/i18n/src/messages/fr.json
  • packages/i18n/src/messages/pt.json
💤 Files with no reviewable changes (1)
  • apps/web-e2e/src/human-chat-panel-space-switch.spec.ts

lang={lang}
coherenceEnabled={coherenceEnabled}
/>
<NavigationTabs id={daoSlug} lang={lang} coherenceEnabled />

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.

⚠️ Potential issue | 🟠 Major

Do not force-enable the Coherence tab.

Line 15 passes coherenceEnabled={true}, so NavigationTabs always renders Coherence and getEffectiveDhoTab() no longer guards disabled Coherence routes. Restore the feature-flag value instead of using a bare prop.

🛡️ Proposed fix
 import { Locale } from '@hypha-platform/i18n';
 import { ReactNode } from 'react';
+import { getEnableCoherence } from '@hypha-platform/feature-flags';
 import { NavigationTabs } from '../_components/navigation-tabs';
@@
 }) {
   const { id: daoSlug, lang } = await params;
+  const coherenceEnabled = await getEnableCoherence();
   return (
     <>
-      <NavigationTabs id={daoSlug} lang={lang} coherenceEnabled />
+      <NavigationTabs
+        id={daoSlug}
+        lang={lang}
+        coherenceEnabled={coherenceEnabled}
+      />
       {children}
     </>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/`[lang]/dho/[id]/@tab/layout.tsx at line 15, NavigationTabs
is being forced to show the Coherence tab by passing a bare coherenceEnabled
prop; revert this to use the actual feature-flag value so getEffectiveDhoTab()
can guard disabled routes. Replace the hard-coded/coercive prop on
NavigationTabs in the layout component with the feature-flag boolean (the same
flag used elsewhere to determine Coherence availability) so coherenceEnabled is
true only when the feature flag indicates it should be; ensure
getEffectiveDhoTab() remains the single source-of-truth for routing behavior.

Comment on lines +69 to +84
/** Batch map Privy subs → Matrix MXIDs for the mention picker (space roster merge). */
export async function getMatrixUserIdsByPrivySubsAction(
{
privyUserIds,
environment,
}: {
privyUserIds: string[];
environment: Environment;
},
{ authToken }: { authToken?: string } = {},
): Promise<Array<{ privyUserId: string; matrixUserId: string }>> {
if (!authToken) {
throw new Error('authToken is required for matrix user id batch lookup');
}
return findMatrixUserIdsByPrivyUserIds({ privyUserIds, environment }, { db });
}

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.

⚠️ Potential issue | 🟠 Major

Do not expose this lookup with only an auth-token presence check.

getMatrixUserIdsByPrivySubsAction is consumed from client code, but any caller can pass a non-empty authToken and enumerate Privy→Matrix mappings for arbitrary IDs/environments. Put this behind a verified API/server boundary, or validate the JWT/claims before querying; also cap privyUserIds to a small batch size. Based on learnings, server actions in this file previously used presence-only authToken because they were exclusively called from the pre-authenticated /api/matrix/token route.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/matrix/server/actions.ts` around lines 69 - 84, The current
getMatrixUserIdsByPrivySubsAction exposes Privy→Matrix mapping if any non-empty
authToken is passed; to fix, validate and authorize the token instead of
presence-only: in getMatrixUserIdsByPrivySubsAction verify the JWT (e.g., check
signature and required claims/roles/scopes) and reject requests without proper
claims before calling findMatrixUserIdsByPrivyUserIds, enforce a hard cap on
privyUserIds length (e.g., max N) and return a clear error if exceeded, and
ensure this action is only callable from a trusted server boundary (or
document/rename to a private/internal function) so arbitrary clients cannot
enumerate mappings.

Comment on lines +60 to +76
const busy =
(!privySub && loadingLink) || (Boolean(resolvedSub) && loadingPerson);

return (
<button
type="button"
role="option"
aria-selected={isActive}
title={matrixUserId}
disabled={busy}
className={cn(
'flex w-full min-w-0 items-center gap-2.5 rounded-sm px-2 py-1.5 text-left text-sm',
busy && 'opacity-70',
isActive ? 'bg-muted text-foreground' : 'hover:bg-muted/80',
)}
onMouseDown={(ev) => ev.preventDefault()}
onClick={onPick}

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.

⚠️ Potential issue | 🟠 Major

Keep mention rows selectable while profile metadata loads.

matrixUserId and matrixFallbackLabel are enough to insert the mention. Disabling the row while profile/link data loads makes the picker unresponsive on slow or stalled lookups.

🐛 Proposed fix
       aria-selected={isActive}
       title={matrixUserId}
-      disabled={busy}
+      aria-busy={busy || undefined}
       className={cn(
         'flex w-full min-w-0 items-center gap-2.5 rounded-sm px-2 py-1.5 text-left text-sm',
         busy && 'opacity-70',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/epics/src/common/human-chat-panel/human-chat-mention-candidate-row.tsx`
around lines 60 - 76, The row is being disabled while profile/link metadata
loads (busy), making picks fail; change the disabled logic so the button remains
selectable when we have a mention identifier (matrixUserId or
matrixFallbackLabel). Update the disabled prop on the button (and any uses of
busy for interactivity) to use something like disabled = {!matrixUserId &&
!matrixFallbackLabel} while keeping busy only for visual state (opacity) so
onClick/onPick still works during loading; touch the const busy and the button's
disabled prop and preserve onClick/onMouseDown handlers.

Comment thread packages/epics/src/common/human-right-panel.tsx
cursoragent and others added 3 commits April 19, 2026 16:08
Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Tighten MXID validation; dedupe wire user_ids; O(n) replacement map for unread
mention counts; skip m.replace rows in mention inbox; roster resolver for inbox;
ICU counts for tab aria-labels; mention token after punctuation; lowercase Space
Memory toolbar key; Fragment-only rich text when no transform.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
Use translucent accent-9 chips with inset ring (matches button hue); tint
@mention rows with accent scale instead of amber so pills harmonize on highlight.

Co-authored-by: webguru-hypha <webguru-hypha@users.noreply.github.com>
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