Skip to content

feat(chat): migrate composer to Tiptap and implement markdown mentions#11

Merged
BuckyMcYolo merged 3 commits intomainfrom
dev
Mar 4, 2026
Merged

feat(chat): migrate composer to Tiptap and implement markdown mentions#11
BuckyMcYolo merged 3 commits intomainfrom
dev

Conversation

@BuckyMcYolo
Copy link
Copy Markdown
Owner

@BuckyMcYolo BuckyMcYolo commented Mar 4, 2026

end-to-end

  • replace textarea composer with Tiptap + mention suggestion UI

  • move compose-related chat pieces into the composer component folder

  • wire Enter/Shift+Enter behavior (Enter sends, Shift+Enter inserts newline)

  • prevent self-mentions and keep mention selection Enter behavior intact

  • add plus-button popover UX (UI button + rotating plus-to-x interaction)

  • improve composer vertical alignment/spacing in the chat layout

  • add markdown message rendering with react-markdown + remark-gfm

  • render mentions as highlighted inline tokens with hover-card shell UI

  • set hover-card open delay to 500ms

  • standardize user-facing mention labels to displayUsername first

  • add mention metadata through API/realtime payloads and schemas

  • include resolved mention users on realtime broadcast/ack payloads

  • support optimistic mention metadata to avoid temporary "unknown user" flashes

  • keep mention storage ID-based (<@userId>) while rendering display labels

Chat Composer Migration to Tiptap with Markdown Mentions

This PR replaces the textarea composer with a TipTap-based rich editor and implements full-stack support for markdown-style user mentions, plus several UI and infra additions to support the feature.

Architecture & Implementation

  • Composer redesign

    • Replaces the old textarea MessageInput with a TipTap-based composer at apps/web/src/components/chat/composer/message-input.tsx (old textarea MessageInput removed).
    • Adds keyboard-driven mention suggestion UI via MentionSuggestionList and a MentionCandidate type.
    • Implements Enter to send and Shift+Enter to insert newline; preserves Enter behavior when selecting mention suggestions and prevents self-mentions.
    • Adds a plus-button popover UX (rotating plus→x) with actions: Upload File, Upload Image, Attach Link.
    • Improves composer vertical alignment and spacing in chat layout.
  • End-to-end mention support

    • API: messageWithAuthorSchema now includes mentions: messageAuthorSchema[] (apps/api/src/lib/helpers/openapi/message-schemas.ts).
    • Queries: messages endpoint resolves and embeds direct mentions per message and returns messages with mentions (apps/api/src/lib/queries/messages.ts).
    • Guild members: username and displayUsername added to member payloads/schemas for richer suggestion labels (apps/api/src/routes/v1/guilds/*).
    • Realtime: message:created payloads and ack responses now include mentions; createMessage initializes mentions: [] and fanout enriches broadcast payloads with messageMentions (apps/realtime/*).
    • Notifications: extracts markdown-style mentions, resolves user records, and builds typed messageMentions used in fanout (apps/realtime/src/services/notifications.ts).
    • Types: adds RealtimeMessageMention and exposes mentions: RealtimeMessageMention[] on RealtimeMessage (packages/realtime-types/src/events.ts).
    • Optimistic UI: createOptimisticMessage updated to accept mentions and optimistic messages include mentions to avoid "unknown user" flashes (apps/web/src/lib/realtime-adapter.ts).
  • Markdown rendering & mention UX

    • New MessageMarkdown component renders message content using react-markdown + remark-gfm with custom handling for stored mention tokens and TipTap mention nodes (apps/web/src/components/chat/message-markdown.tsx).
    • Mentions render as highlighted inline tokens that open a HoverCard shell on hover (500ms open delay) showing avatar, display name, username, and a disabled action.
    • Mention labels standardized to prefer displayUsername → username → name; unknown users fallback to an “Unknown user” label.
    • Storage format remains ID-based (<@userid>) while rendering display labels.
  • Mention candidate discovery

    • Channel and DM route components fetch guild/members to build deduplicated mentionCandidates (id, label, name, username, displayUsername, image, search); current user excluded.
    • MessageInput now accepts currentUserId and mentionCandidates props to power suggestion behavior (apps/web/src/routes/* updated to use the new composer path).

UI & Dependency Additions

  • Adds TipTap packages and react-markdown/remark-gfm to web app dependencies (apps/web/package.json).
  • Introduces multiple new UI primitives/components in packages/ui (Popover, HoverCard, Combobox, Command palette, Calendar, Select, Sheet, InputGroup, Switch, Textarea, etc.) used across the new composer/UX.

Notable Code Changes

  • New MessageInput implementation (~+400 lines) and removal of the old textarea-based MessageInput.
  • New mention-related components and types: MentionSuggestionList, mention-types, MessageMarkdown.
  • Backend and realtime changes to include mentions in API responses and real-time broadcasts.
  • createOptimisticMessage signature updated to accept mentions.

Risks, Testing & Suggestions

  • No test files detected for the new composer, mention parsing, or suggestion behavior. Recommend unit/integration tests for:
    • Mention extraction/parsing (stored and TipTap markdown forms).
    • Self-mention filtering and suggestion selection (keyboard/mouse).
    • Enter vs Shift+Enter behaviors and suggestion selection edge cases.
    • Optimistic mention rendering to avoid transient unknown-user states.
  • Notifications/fanout now includes additional DB lookups for mention resolution — monitor performance in large guilds/rooms.
  • MessageInput bundles editor state, suggestion logic, and popover UI; consider splitting concerns into smaller hooks/components for maintainability.

Confidence Score: 4/5

Well-implemented across frontend, API, and realtime layers with appropriate typing and optimistic UX handling. Primary concerns are lack of automated tests and increased complexity in the new MessageInput component; otherwise the change appears production-ready with minor follow-ups recommended.

end-to-end

- replace textarea composer with Tiptap + mention suggestion UI
- move compose-related chat pieces into the composer component folder
- wire Enter/Shift+Enter behavior (Enter sends, Shift+Enter inserts
  newline)
- prevent self-mentions and keep mention selection Enter behavior intact
- add plus-button popover UX (UI button + rotating plus-to-x
  interaction)
- improve composer vertical alignment/spacing in the chat layout

- add markdown message rendering with react-markdown + remark-gfm
- render mentions as highlighted inline tokens with hover-card shell UI
- set hover-card open delay to 500ms
- standardize user-facing mention labels to displayUsername first

- add mention metadata through API/realtime payloads and schemas
- include resolved mention users on realtime broadcast/ack payloads
- support optimistic mention metadata to avoid temporary "unknown user"
  flashes
- keep mention storage ID-based (`<@userid>`) while rendering display
  labels
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Adds end-to-end direct-user mention support: API schema and query changes, realtime fanout and mention extraction, new TipTap-based composer and suggestion UI, mention-aware message rendering and optimistic updates, plus supporting UI primitives and types.

Changes

Cohort / File(s) Summary
API: message schemas & queries
apps/api/src/lib/helpers/openapi/message-schemas.ts, apps/api/src/lib/queries/messages.ts
Added mentions to message OpenAPI schema; updated message queries to fetch direct mention rows per message ID and attach resolved mention records to each message.
API: guild member presence
apps/api/src/routes/v1/guilds/handlers.ts, apps/api/src/routes/v1/guilds/schema.ts
Extended guild member presence rows and schema to include username and displayUsername (nullable) and included them in API responses.
Realtime: types, fanout & events
packages/realtime-types/src/events.ts, apps/realtime/src/services/notifications.ts, apps/realtime/src/services/messages.ts, apps/realtime/src/index.ts
Added RealtimeMessageMention type and mentions on RealtimeMessage; extract Markdown mentions, resolve users during fanout, populate messageMentions, include mentions in created-message payloads and acks.
Web: composer, suggestion UI & types
apps/web/src/components/chat/composer/message-input.tsx, apps/web/src/components/chat/composer/mention-types.ts, apps/web/src/components/chat/composer/mention-suggestion-list.tsx
Introduced TipTap-based MessageInput with mention extension, MentionCandidate type, and keyboard/mouse-driven MentionSuggestionList component for autocompletion.
Web: rendering & adapter changes
apps/web/src/components/chat/message-markdown.tsx, apps/web/src/components/chat/message-item.tsx, apps/web/src/lib/realtime-adapter.ts
Added MessageMarkdown to render Markdown and mentions (with HoverCard), updated message-item to use it, and propagated mentions through realtime-adapter including new param on optimistic message creation.
Web: route integrations & optimistic updates
apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx, apps/web/src/routes/_authenticated/dms/$dmId.tsx
Switched to new composer import, fetched guild/DM members to build mentionCandidates, extended handleSend to accept optional mentions, and included mentions in optimistic messages.
Web: removed legacy composer
apps/web/src/components/chat/message-input.tsx
Removed the old textarea-based MessageInput in favor of the new TipTap composer.
UI library & deps
packages/ui/src/components/* (calendar, combobox, command, hover-card, input-group, popover, select, sheet, switch, textarea), packages/ui/package.json, apps/web/package.json
Added many new UI primitives (Radix/@base-ui wrappers) and frontend dependencies (TipTap packages, react-markdown, remark-gfm, cmdk, react-day-picker, date-fns), enabling mention UI and other components.

Sequence Diagram(s)

sequenceDiagram
    participant User as Web Client (User)
    participant Composer as TipTap Composer
    participant API as Backend API
    participant DB as Database
    participant Realtime as Realtime Service
    participant Other as Other Clients

    User->>Composer: Type message with `@mention` and send
    Composer->>API: POST message payload (content + mentions array)
    API->>DB: Insert message and mention relations
    API->>Realtime: Notify message created (message + mentions)
    Realtime->>DB: (optional) fetch mentioned user details
    Realtime->>Realtime: Build messageMentions array
    Realtime->>Other: Broadcast message:created with mentions
    Other->>Other: Render message content with mention HoverCards
Loading
sequenceDiagram
    participant User as User Typing
    participant TipTap as TipTap Editor
    participant Guild as Guild Members API
    participant Suggestion as Suggestion List UI

    User->>TipTap: Type "@" → trigger suggestion
    TipTap->>Guild: Request mention candidates
    Guild->>TipTap: Return member list
    TipTap->>Suggestion: Show list
    User->>Suggestion: Navigate/select candidate
    Suggestion->>TipTap: Insert mention node (id + label)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰
I nibble keys and stitch a thread,
@names bloom where my paws have led.
TipTap sings and HoverCards glow,
Mentions hop in row by row.
A carrot nod — the chat's aglow!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: migrating the chat composer to Tiptap and implementing markdown mentions support.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 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/api/src/lib/queries/messages.ts`:
- Around line 50-64: The query that builds mentionRows is pulling all
messageMention rows including fanout rows with mentionType "everyone"; update
the db query that selects from messageMention (the mentionRows code) to filter
to direct/user mentions only by adding a WHERE clause that constrains
messageMention.mentionType to the direct mention value (e.g., "user" or the
appropriate enum) so only explicit mentions are returned (keep the rest of the
join with user and the inArray(messageMention.messageId, messageIds) condition
unchanged).

In `@apps/realtime/src/services/notifications.ts`:
- Around line 142-173: The current code builds mentionedUserIds from
mentionTypeByUserId.keys(), which includes users added by an `@everyone` fanout;
change the logic so mentionedUserIds only contains direct mention recipients
(i.e., filter keys by checking mentionTypeByUserId.get(userId) !== 'everyone' or
=== 'direct' depending on your enum/value). Then use that filtered
mentionedUserIds for the DB query and subsequent mentionUserMap/messageMentions
construction (symbols: mentionTypeByUserId, mentionedUserIds, mentionUsers,
mentionUserMap, messageMentions); keep the empty-array guard when no direct
mentions remain.

In `@apps/web/src/components/chat/composer/message-input.tsx`:
- Around line 292-300: The Enter key handler (handleKeyDown) can submit while an
IME composition is active; add an IME guard at the top of the Enter branch to
skip handling when composition is ongoing (use event.nativeEvent.isComposing or
event.isComposing where available) so that when event.key === "Enter" and
!event.shiftKey you first return if composition is active or if
isMentionSuggestionOpen() is true; keep the existing
preventDefault/stopPropagation and call to handleSend only when not composing.

In `@apps/web/src/components/chat/message-markdown.tsx`:
- Around line 19-21: The escapeMarkdownText function currently only escapes
backslashes and closing brackets; update it to also escape opening brackets so
mention labels containing "[" don't break generated markdown links—add a
replacement for "[" to "\\[" inside escapeMarkdownText (referencing the
escapeMarkdownText function used when building mention markdown tokens).

In `@apps/web/src/routes/_authenticated/`$guildSlug/$channelId.tsx:
- Around line 232-241: The mapping that creates mentionCandidates is run on
every render and should be memoized: wrap the guildMembersData?.members.map(...)
expression in React's useMemo (import useMemo if not already) to return the same
array identity unless guildMembersData?.members changes; target the
mentionCandidates const and the guildMembersData?.members source so the
dependency array is [guildMembersData?.members] to avoid needless rerenders and
downstream recomputation.

In `@packages/ui/src/components/calendar.tsx`:
- Line 90: The `table` className is set as a plain string and bypasses the
existing merge pattern; update the assignment that defines `table` in the
defaultClassNames (or wherever `table` is declared) to use the same `cn()` merge
pattern as the other entries (e.g., `table: cn("w-full border-collapse",
defaultClassNames.table)` or equivalent) so custom classes and react-day-picker
defaults are preserved; locate `table` alongside `defaultClassNames` and the
`cn` helper in this file and change it to merge instead of using a raw string.

In `@packages/ui/src/components/combobox.tsx`:
- Around line 55-71: The combobox is applying the incoming className to the
wrapper (InputGroup) instead of the actual input, preventing consumers from
styling the input; to fix, stop passing the incoming className into InputGroup
and instead forward it onto the input by merging it into the
ComboboxPrimitive.Input / InputGroupInput props (e.g. combine props.className
with any InputGroupInput classes), keep the wrapper using only the fixed
"w-auto" class (or a separate wrapperClassName if needed), and ensure the
existing disabled prop and other {...props} still flow into
ComboboxPrimitive.Input so consumers' className targets the real input element
(refer to ComboboxInput, InputGroup, ComboboxPrimitive.Input, InputGroupInput,
and the className/props usage).

In `@packages/ui/src/components/command.tsx`:
- Line 54: The class on the Command wrapper uses selectors targeting
[cmdk-input-wrapper]_svg but CommandInput renders
data-slot="command-input-wrapper", so the icon size rules never apply; update
the two selectors referencing cmdk-input-wrapper to match the data-slot
attribute (use the same :data-[slot=command-input-wrapper] pattern used
elsewhere) so the SVG sizing rules apply to the CommandInput icon; look for the
Command JSX with the long className and replace [&_[cmdk-input-wrapper]_svg]:h-5
and [&_[cmdk-input-wrapper]_svg]:w-5 with the corresponding
:data-[slot=command-input-wrapper] selectors to match CommandInput.

In `@packages/ui/src/components/popover.tsx`:
- Around line 57-63: PopoverTitle is typed as React.ComponentProps<"h2"> but
currently renders a <div>, causing semantic and typing mismatches; change the
rendered element in the PopoverTitle function from <div> to <h2>, keeping
data-slot="popover-title", className={cn("font-medium", className)} and
spreading {...props} so all h2 props (including refs/aria/children) remain
supported and heading semantics are preserved.
- Line 4: PopoverTitle's props are typed as React.ComponentProps<"h2"> but the
component renders a <div>, causing a props/type mismatch; update the
PopoverTitle component so its runtime element and props type match by either
changing the props type to React.ComponentProps<"div"> or replacing the rendered
<div> with an <h2> element; locate the PopoverTitle function and adjust the type
annotation or the JSX element accordingly, ensuring any className or styling
still applies after the change.

ℹ️ Review info
Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 13aa2919-e60d-4d37-8677-83e11606da2a

📥 Commits

Reviewing files that changed from the base of the PR and between 37c4390 and 5f1db12.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (30)
  • apps/api/src/lib/helpers/openapi/message-schemas.ts
  • apps/api/src/lib/queries/messages.ts
  • apps/api/src/routes/v1/guilds/handlers.ts
  • apps/api/src/routes/v1/guilds/schema.ts
  • apps/realtime/src/index.ts
  • apps/realtime/src/services/messages.ts
  • apps/realtime/src/services/notifications.ts
  • apps/web/package.json
  • apps/web/src/components/chat/composer/mention-suggestion-list.tsx
  • apps/web/src/components/chat/composer/mention-types.ts
  • apps/web/src/components/chat/composer/message-input.tsx
  • apps/web/src/components/chat/message-input.tsx
  • apps/web/src/components/chat/message-item.tsx
  • apps/web/src/components/chat/message-markdown.tsx
  • apps/web/src/components/sidebar/channel-panel/channel-list.tsx
  • apps/web/src/lib/realtime-adapter.ts
  • apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx
  • apps/web/src/routes/_authenticated/dms/$dmId.tsx
  • packages/realtime-types/src/events.ts
  • packages/ui/package.json
  • packages/ui/src/components/calendar.tsx
  • packages/ui/src/components/combobox.tsx
  • packages/ui/src/components/command.tsx
  • packages/ui/src/components/hover-card.tsx
  • packages/ui/src/components/input-group.tsx
  • packages/ui/src/components/popover.tsx
  • packages/ui/src/components/select.tsx
  • packages/ui/src/components/sheet.tsx
  • packages/ui/src/components/switch.tsx
  • packages/ui/src/components/textarea.tsx
💤 Files with no reviewable changes (1)
  • apps/web/src/components/chat/message-input.tsx

Comment thread apps/api/src/lib/queries/messages.ts
Comment thread apps/realtime/src/services/notifications.ts Outdated
Comment thread apps/web/src/components/chat/composer/message-input.tsx
Comment thread apps/web/src/components/chat/message-markdown.tsx
Comment thread apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx Outdated
Comment thread packages/ui/src/components/calendar.tsx Outdated
Comment thread packages/ui/src/components/combobox.tsx
Comment thread packages/ui/src/components/command.tsx Outdated
Comment thread packages/ui/src/components/popover.tsx
Comment thread packages/ui/src/components/popover.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 `@apps/web/src/components/chat/composer/message-input.tsx`:
- Around line 256-286: handleSend uses editor methods without guarding for a
null editor; add an early null check at the top of handleSend (e.g., if
(!editor) return) so you never call editor.getMarkdown(),
editor.commands.clearContent, or editor.commands.focus when the editor is not
initialized, and keep the existing dependency on editor in the useCallback;
ensure all references in handleSend (getMarkdown, commands.clearContent,
commands.focus) are only executed after the editor null-guard.
- Around line 306-311: The effect currently dereferences editor.view.dom and may
throw if editor (or editor.view/dom) is null; update the effect that attaches
event listeners (the block using domNode.addEventListener and
domNode.removeEventListener) to first check that editor and editor.view and
editor.view.dom are non-null (e.g., guard with if (!editor || !editor.view ||
!editor.view.dom) return) before using domNode, and ensure the cleanup only runs
when the listener was actually added; keep references to handleKeyDown (and the
existing dependencies) intact.

In `@packages/ui/src/components/calendar.tsx`:
- Around line 12-15: The CalendarDayButton prop typing is broken because the
code imports DayButton as a type-only symbol but later uses typeof DayButton (a
value), causing a TS error; update the import to pull the proper props type from
react-day-picker (DayButtonProps) and replace uses of typeof DayButton with
DayButtonProps in the CalendarDayButton component signature/typing so the
component uses the correct prop type from react-day-picker.

In `@packages/ui/src/components/combobox.tsx`:
- Around line 42-52: ComboboxClear renders an icon-only button without an
accessible name; update the component (and the other icon-only wrappers in this
file such as the similar instances at the other noted ranges) to provide a
default accessible name by ensuring an aria-label (or aria-labelledby) is
present when none is passed in props—e.g., inside ComboboxClear check
props['aria-label'] || props['aria-labelledby'] and if missing, spread a default
aria-label="Clear" into the ComboboxPrimitive.Clear/ InputGroupButton props so
screen readers announce the control while preserving any explicit labels
provided by callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 955cd4b8-0d12-4155-a8b2-20741f0d05f4

📥 Commits

Reviewing files that changed from the base of the PR and between 5f1db12 and 135e248.

📒 Files selected for processing (9)
  • apps/api/src/lib/queries/messages.ts
  • apps/realtime/src/services/notifications.ts
  • apps/web/src/components/chat/composer/message-input.tsx
  • apps/web/src/components/chat/message-markdown.tsx
  • apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx
  • packages/ui/src/components/calendar.tsx
  • packages/ui/src/components/combobox.tsx
  • packages/ui/src/components/command.tsx
  • packages/ui/src/components/popover.tsx

Comment thread apps/web/src/components/chat/composer/message-input.tsx
Comment thread apps/web/src/components/chat/composer/message-input.tsx
Comment thread packages/ui/src/components/calendar.tsx Outdated
Comment thread packages/ui/src/components/combobox.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/ui/src/components/combobox.tsx (1)

275-282: ⚠️ Potential issue | 🟠 Major

Add an accessible name to the chip remove icon button.

Line 276 renders an icon-only remove action without aria-label/aria-labelledby, so assistive tech won’t announce a meaningful action name.

♿ Proposed patch
       {showRemove && (
         <ComboboxPrimitive.ChipRemove
+          aria-label="Remove selection"
           render={<Button variant="ghost" size="icon-xs" />}
           className="-ml-1 opacity-50 hover:opacity-100"
           data-slot="combobox-chip-remove"
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/combobox.tsx` around lines 275 - 282, The chip
remove button rendered by ComboboxPrimitive.ChipRemove lacks an accessible name;
update the component to pass an aria-label (or aria-labelledby) to the rendered
Button so screen readers announce the action (e.g., add aria-label="Remove item"
or aria-label={`Remove ${itemLabel}`} when item label is available). Locate the
ComboboxPrimitive.ChipRemove usage and add the aria-label prop to the Button
passed to render, ensuring the label is specific if you can reference the chip's
display text.
🤖 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/components/chat/composer/message-input.tsx`:
- Around line 259-261: The send validation uses two different length metrics
(plain-text for the button and stored-markdown in handleSend), causing
inconsistent behavior; unify them by computing one canonical length value (e.g.,
const stored = toStoredMarkdown(editor.getMarkdown()).trim() and const length =
stored.length) and use that same value both for the send-button enabled state
and inside handleSend (replace the current plain-text checks and the separate
stored-markdown check). Ensure references to MAX_MESSAGE_LENGTH and isSending
remain, and update any UI checks that currently call editor.getText() or
editor.getMarkdown() separately so they rely on this single computed metric.
- Around line 70-77: The mention lookup in the items function uses
candidate.label and candidate.search only, so when candidate.search is missing
it can miss matches in other fields; update the filter in items (which calls
getMentionCandidates) to build a comparable string from candidate.label plus
fallback fields like candidate.username and candidate.name (and candidate.search
when present), and perform the normalized.includes check against that combined
lowercase string so queries match label, username, name or search even if some
fields are undefined.

In `@packages/ui/src/components/calendar.tsx`:
- Around line 42-44: The month dropdown is using a hard-coded "default" locale
in formatMonthDropdown which ignores the DayPicker locale prop; update
formatMonthDropdown to accept the DayPicker locale (or derive it from the
incoming formatters/props) and call toLocaleString with that locale instead of
"default" so month labels respect the DayPicker locale prop—look for
formatMonthDropdown and where formatters are spread into the DayPicker to
pass/consume the locale value and use it in the toLocaleString call.

---

Duplicate comments:
In `@packages/ui/src/components/combobox.tsx`:
- Around line 275-282: The chip remove button rendered by
ComboboxPrimitive.ChipRemove lacks an accessible name; update the component to
pass an aria-label (or aria-labelledby) to the rendered Button so screen readers
announce the action (e.g., add aria-label="Remove item" or aria-label={`Remove
${itemLabel}`} when item label is available). Locate the
ComboboxPrimitive.ChipRemove usage and add the aria-label prop to the Button
passed to render, ensuring the label is specific if you can reference the chip's
display text.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6de22e7a-bf4d-4f3c-b758-f0f1e842c6eb

📥 Commits

Reviewing files that changed from the base of the PR and between 135e248 and f7f944f.

📒 Files selected for processing (3)
  • apps/web/src/components/chat/composer/message-input.tsx
  • packages/ui/src/components/calendar.tsx
  • packages/ui/src/components/combobox.tsx

Comment on lines +70 to +77
items: ({ query }) => {
const normalized = query.trim().toLowerCase()
const results = getMentionCandidates().filter((candidate) => {
if (!normalized) return true
return (
candidate.label.toLowerCase().includes(normalized) ||
candidate.search?.toLowerCase().includes(normalized)
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mention lookup is incomplete when search is missing.

In channel context, candidates may not include search, so query matching falls back to label only and can miss username/name matches.

🔧 Proposed fix
     items: ({ query }) => {
       const normalized = query.trim().toLowerCase()
       const results = getMentionCandidates().filter((candidate) => {
         if (!normalized) return true
-        return (
-          candidate.label.toLowerCase().includes(normalized) ||
-          candidate.search?.toLowerCase().includes(normalized)
-        )
+        const searchable = [
+          candidate.label,
+          candidate.search,
+          candidate.name,
+          candidate.username ?? undefined,
+          candidate.displayUsername ?? undefined,
+        ]
+          .filter((value): value is string => Boolean(value))
+          .join(" ")
+          .toLowerCase()
+
+        return searchable.includes(normalized)
       })
       return results.slice(0, MAX_MENTION_RESULTS)
     },
📝 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
items: ({ query }) => {
const normalized = query.trim().toLowerCase()
const results = getMentionCandidates().filter((candidate) => {
if (!normalized) return true
return (
candidate.label.toLowerCase().includes(normalized) ||
candidate.search?.toLowerCase().includes(normalized)
)
items: ({ query }) => {
const normalized = query.trim().toLowerCase()
const results = getMentionCandidates().filter((candidate) => {
if (!normalized) return true
const searchable = [
candidate.label,
candidate.search,
candidate.name,
candidate.username ?? undefined,
candidate.displayUsername ?? undefined,
]
.filter((value): value is string => Boolean(value))
.join(" ")
.toLowerCase()
return searchable.includes(normalized)
})
return results.slice(0, MAX_MENTION_RESULTS)
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/chat/composer/message-input.tsx` around lines 70 -
77, The mention lookup in the items function uses candidate.label and
candidate.search only, so when candidate.search is missing it can miss matches
in other fields; update the filter in items (which calls getMentionCandidates)
to build a comparable string from candidate.label plus fallback fields like
candidate.username and candidate.name (and candidate.search when present), and
perform the normalized.includes check against that combined lowercase string so
queries match label, username, name or search even if some fields are undefined.

Comment on lines +259 to +261
const markdown = toStoredMarkdown(editor.getMarkdown())
const trimmed = markdown.trim()
if (!trimmed || trimmed.length > MAX_MESSAGE_LENGTH || isSending) return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use one consistent length metric for send validation.

The button state uses plain-text length, but submit guard uses stored markdown length. This can enable send in UI but silently block in handleSend (notably with mention markup expansion).

🔧 Proposed fix
   const handleSend = useCallback(() => {
     if (!editor) return

     const markdown = toStoredMarkdown(editor.getMarkdown())
     const trimmed = markdown.trim()
-    if (!trimmed || trimmed.length > MAX_MESSAGE_LENGTH || isSending) return
+    const trimmedPlainText = editor.getText({ blockSeparator: "\n" }).trim()
+    if (
+      !trimmed ||
+      !trimmedPlainText ||
+      trimmedPlainText.length > MAX_MESSAGE_LENGTH ||
+      isSending
+    ) {
+      return
+    }

Also applies to: 319-323

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

In `@apps/web/src/components/chat/composer/message-input.tsx` around lines 259 -
261, The send validation uses two different length metrics (plain-text for the
button and stored-markdown in handleSend), causing inconsistent behavior; unify
them by computing one canonical length value (e.g., const stored =
toStoredMarkdown(editor.getMarkdown()).trim() and const length = stored.length)
and use that same value both for the send-button enabled state and inside
handleSend (replace the current plain-text checks and the separate
stored-markdown check). Ensure references to MAX_MESSAGE_LENGTH and isSending
remain, and update any UI checks that currently call editor.getText() or
editor.getMarkdown() separately so they rely on this single computed metric.

Comment on lines +42 to +44
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
Copy link
Copy Markdown

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

🌐 Web query:

react-day-picker v9 formatMonthDropdown signature locale prop

💡 Result:

In react-day-picker v9, the built-in formatter has this signature:

  • formatMonthDropdown(month: Date, dateLib?: DateLib): string (daypicker.dev)

How the locale prop relates:

  • You set the locale on the picker via <DayPicker locale={someLocale} /> (a DayPickerLocale, i.e. date-fns locale). (daypicker.dev)
  • That locale is carried in the DateLib instance passed as the 2nd argument to formatters, available as dateLib.options.locale (because DateLibOptions includes locale). (daypicker.dev)

(Also note: in v9 the dropdown formatters receive a Date for the month, not a number.) (daypicker.dev)

Citations:


🏁 Script executed:

# Check the actual file and surrounding context
cat -n packages/ui/src/components/calendar.tsx | head -60 | tail -30

Repository: BuckyMcYolo/townhall

Length of output: 1478


🏁 Script executed:

# Look for package.json to confirm react-day-picker version
cat packages/ui/package.json | grep -A 5 -B 5 "react-day-picker"

Repository: BuckyMcYolo/townhall

Length of output: 364


Fix locale handling in formatMonthDropdown to respect the DayPicker locale prop.

Line 42-43 hard-codes the locale as "default", which prevents the locale prop passed to DayPicker from affecting the month dropdown labels.

♻️ Suggested fix
       formatters={{
-        formatMonthDropdown: (date) =>
-          date.toLocaleString("default", { month: "short" }),
+        formatMonthDropdown: (date, dateLib) =>
+          date.toLocaleString(dateLib?.options?.locale?.code ?? undefined, {
+            month: "short",
+          }),
         ...formatters,
       }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/calendar.tsx` around lines 42 - 44, The month
dropdown is using a hard-coded "default" locale in formatMonthDropdown which
ignores the DayPicker locale prop; update formatMonthDropdown to accept the
DayPicker locale (or derive it from the incoming formatters/props) and call
toLocaleString with that locale instead of "default" so month labels respect the
DayPicker locale prop—look for formatMonthDropdown and where formatters are
spread into the DayPicker to pass/consume the locale value and use it in the
toLocaleString call.

@BuckyMcYolo BuckyMcYolo merged commit 3b77984 into main Mar 4, 2026
1 check passed
This was referenced Mar 5, 2026
Merged
Merged
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.

1 participant