Dev#25
Conversation
📝 WalkthroughWalkthroughAdds guild and DM message search end-to-end: API endpoints, schemas, DB trigram index, handlers, and interactive web UI (search bar/header search) with navigation-to-message support; also updates ROADMAP and right-sidebar UI/state for collapsible/resizable behavior. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as Search UI
participant Router as Web Router
participant API as API Server
participant DB as Database
User->>UI: Type query (debounced 300ms)
UI->>API: GET /guilds/{guildSlug}/search?query=... or /dms/search?query=...
API->>API: Determine scope (guild channels or user's DM channel IDs)
API->>DB: Parallel requests — COUNT(...) and SELECT ... WHERE content ILIKE %q% AND channelId IN (scope)
DB-->>API: total count + message rows (with author)
API->>UI: Return paginated { itemsTotal, data[], currentPage, nextPage, prevPage }
UI->>UI: Render dropdown results
User->>UI: Click result
UI->>Router: Navigate to channel/DM route with ?msgId=selected
Router->>UI: On load, locate message element, scroll into view, apply temporary highlight
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/routes/v1/dms/handlers.ts`:
- Around line 621-648: The current search builds searchPattern = `%${query}%`
and passes it to ilike(message.content, searchPattern) so user input with % or _
acts as SQL wildcards and causes incorrect matches and potential scans; replace
this by escaping LIKE metacharacters in query (escape % and _ and the escape
char, or use parameterized LIKE with an ESCAPE clause) before constructing
searchPattern and using ilike in the queries that produce countResult and
messages, and additionally add a text-search index on message.content (e.g., a
pg_trgm GIN index or a full-text index) in the messages schema so the count and
paginated queries use an index instead of full-table scans.
- Around line 596-619: The loop rebuilds dmMembers filters per channel causing
O(channels×members) work; instead, pre-group dmMembers into a Map keyed by
channelId (e.g., build dmMembersByChannel: Map<string,
Array<{channelId,userId,name}>>) before iterating dmChannels, then in the
channelNameMap population use dmMembersByChannel.get(ch.id) (defaulting to empty
array) and filter out currentUser.id to produce the label; update references to
dmMembers in the channelNameMap loop to use this precomputed map.
In `@apps/api/src/routes/v1/guilds/handlers.ts`:
- Around line 515-519: The ilike pattern is built from query without escaping
SQL LIKE wildcards, so escape '%' and '_' in the input before constructing
searchPattern: create an escape helper (e.g., escapeLike) that replaces '%' ->
'\%' and '_' -> '\_' (and escapes the escape char itself), use escaped =
escapeLike(query) then build searchPattern = `%${escaped}%`, and update
whereConditions that use ilike(schema.message.content, searchPattern) to ensure
the DB interprets the backslash escape (add an ESCAPE '\\' clause or use your DB
driver's escape option if needed); refer to searchPattern, query, ilike,
schema.message.content and whereConditions when implementing.
In `@apps/web/src/components/sidebar/channel-panel/search-bar.tsx`:
- Around line 95-107: handleResultClick currently only takes channelId and drops
the clicked hit's msg.id, so navigation lands at the channel but not the matched
message; modify handleResultClick to accept a messageId (e.g., msgId) and pass
that through to navigate either as a search param or navigation state (for
example include { params: ..., search: { msgId } } or { state: { msgId } }) and
update the callers that invoke handleResultClick (the other call sites around
lines 151-156) to forward the clicked hit.msg.id; ensure the destination route
reading logic can extract the msgId from location.search or location.state and
perform scroll/highlight there.
- Around line 40-49: In handleQueryChange ensure you both
clearTimeout(debounceTimer.current) and set debounceTimer.current = null
whenever you short-circuit the debounce (e.g., in the !value.trim() branch) so a
previously scheduled timer cannot later setDebouncedQuery; apply the same fix to
the other reset paths referenced (the selection handler around lines 95-113,
e.g. onSelectResult/handleSelect) by clearing the timeout and nulling
debounceTimer.current before calling setDebouncedQuery("") or otherwise
resetting UI state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2c1f60f5-6720-4067-a581-8abe1947a725
📒 Files selected for processing (11)
ROADMAP.mdapps/api/src/routes/v1/dms/handlers.tsapps/api/src/routes/v1/dms/index.tsapps/api/src/routes/v1/dms/routes.tsapps/api/src/routes/v1/dms/schema.tsapps/api/src/routes/v1/guilds/handlers.tsapps/api/src/routes/v1/guilds/index.tsapps/api/src/routes/v1/guilds/routes.tsapps/api/src/routes/v1/guilds/schema.tsapps/web/src/components/sidebar/channel-panel/search-bar.tsxapps/web/src/components/sidebar/dm-panel/dm-panel.tsx
added per Channel search bar
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 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/header-search.tsx`:
- Around line 134-172: The search controls are icon-only and lack accessible
labels; add ARIA labels and states: give the closed-state button an aria-label
like "Open search" and aria-expanded={isOpen}; ensure the input (inside the open
state) has an explicit aria-label (dynamic based on mode, e.g., "Search this
conversation" or "Search this channel") rather than relying on placeholder; and
add an aria-label to the close button (e.g., "Close search"). Update the JSX
around the Search button, the input element referenced by inputRef, and the
close X button (where setIsOpen, isOpen, handleQueryChange, handleClose, Search
and X are used) to include these attributes.
In `@apps/web/src/components/chat/header.tsx`:
- Around line 80-92: The tooltip incorrectly promises "Show Members" while the
button only calls toggleCollapsed() (seen in the JSX using isCollapsed,
context.type === "channel" and toggleCollapsed); update the behavior so the
label matches the actual view: either derive the tooltip/aria-label from
view?.type (use that to set TooltipContent and button aria-label) or change the
click handler to first set the view to "guild-members" before calling
toggleCollapsed(); adjust the component around the button (where isCollapsed,
context.type and toggleCollapsed are used) to implement one of these fixes and
ensure the button has an appropriate aria-label reflecting the selected view.
In `@apps/web/src/components/sidebar/channel-panel/search-bar.tsx`:
- Around line 132-152: The search input and clear button lack accessible names;
add aria-labels to both: on the input element (where inputRef, value={query},
onChange -> handleQueryChange, onFocus -> setIsOpen) add an aria-label that
varies by mode (e.g., aria-label={mode === "dm" ? "Search all direct messages" :
"Search all channels"}), and on the clear button (the element that calls
handleClear and renders <X />) add an aria-label like "Clear search" so screen
readers can identify both controls.
In `@apps/web/src/components/sidebar/index.tsx`:
- Around line 96-105: The resize divider currently only handles mouse input via
onMouseDown; make it accessible by converting the div into a focusable,
ARIA-enabled separator and adding keyboard handlers: add role="separator",
aria-orientation="vertical", aria-valuemin and aria-valuemax and aria-valuenow
(bind to the same sidebar width state), add tabIndex={0} so it can receive
focus, and implement an onKeyDown handler (e.g., handleKeyDown) that responds to
ArrowLeft/ArrowRight (and optionally Home/End/PageUp/PageDown) to adjust the
width using the same resize logic used by handleMouseDown/dragging; also ensure
visual focus styles are preserved (keep isResizing styling) and reuse existing
helper functions/state (e.g., handleMouseDown, isResizing, width/state setter)
so the keyboard updates and ARIA value stay in sync.
- Around line 82-112: The resizable right sidebar is now mounted at all viewport
widths, causing it to steal space on small screens; add a breakpoint guard so
the resizable side-by-side panel (the motion.div that uses panelRef, panelWidth
and handleMouseDown and renders <RightSidebarPanel />) only mounts on large
viewports (>= xl/min-width:1280px). Implement the guard by checking the viewport
width (e.g., window.matchMedia('(min-width:1280px)') or your existing breakpoint
hook) and wrap the motion.div rendering with that condition (keeping
showRightPanel && isHydrated && !isCollapsed), and ensure the smaller-screen
code path still renders the overlay variant of the right panel instead of the
resizable side-by-side panel.
- Around line 38-54: The drag flow can commit a stale width because
widthRef.current isn't seeded at drag start; before attaching
handleMouseMove/handleMouseUp set widthRef.current = panelWidth (or startWidth)
so handleMouseUp and handleMouseMove operate on the current width even if the
user clicks without moving or hydration changed panelWidth; update the
drag-start logic where startX and startWidth are set (the same block that
initializes startX/startWidth and registers handleMouseMove/handleMouseUp) to
assign widthRef.current and then proceed to setIsResizing(true) and add the
event listeners so setPanelWidth(widthRef.current) on mouse up will be correct.
In `@apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx`:
- Around line 640-646: The collapse toggle button is missing an accessible name;
update the button in guild-members-panel.tsx (the element rendering the icon and
using onClick={toggleCollapsed} and PanelRight) to include an aria-label (for
example "Collapse members panel") so screen readers can announce its purpose;
ensure the label accurately reflects the action and is placed directly on the
button element.
In `@apps/web/src/routes/_authenticated/`$guildSlug/$channelId.tsx:
- Around line 42-48: The scrollToMessage function currently only queries the DOM
and scrolls an already-rendered node, so clicks for older search hits fail when
the target message isn't loaded; update the flow to attempt a fetch-by-id (or
fetch-around-id) for the given messageId before querying/scrolling and before
clearing the msgId state. Specifically, add a retrieval step in scrollToMessage
(or its caller) that calls the message fetch API to load the target message into
messagesData (or requests surrounding context) and await its completion; only
after the fetch resolves and the DOM has been updated attempt
document.querySelector and then clear the msgId state (also apply the same
fetch-before-clear fix to the analogous logic used in the other handler
referenced around lines 112-122).
In `@apps/web/src/routes/_authenticated/dms/`$dmId.tsx:
- Around line 35-42: The current scrollToMessage function only works if the
targeted message is already in the loaded messagesData page; to fix this, update
the navigation logic that reads msgId (and the scrollToMessage function) to
first attempt to load the message (or the page/window containing it) when it
isn’t present: call the API or messageStore to fetch messageById (or compute the
page that contains the id), merge or replace messagesData with that page, then
call scrollIntoView and apply the highlight class; ensure you only remove the
msgId navigation token after the fetch+scroll+highlight completes (also apply
the same change to the other handler at lines ~76-85 that processes msgId).
In `@biome.json`:
- Around line 48-50: The global accessibility rule
a11y.noStaticElementInteractions was turned off in the config object
a11y.noStaticElementInteractions; revert this to enable the rule globally (or
remove the global override) and instead add narrow overrides or local
suppressions only for validated exceptions (e.g., resize handles/backdrops)
using file-specific config overrides or inline disabling comments in the
specific components that require it; search for instances like the
previously-flagged interactive patterns to add targeted suppressions rather than
a repo-wide "off".
In `@packages/db/src/schemas/messages.ts`:
- Around line 66-69: The GIN trigram index declaration using gin_trgm_ops (index
"message_content_trgm_idx") requires the pg_trgm extension to be present; add
enabling of pg_trgm in your DB setup (e.g., a migration or initialization script
that runs CREATE EXTENSION IF NOT EXISTS pg_trgm) so the operator class
gin_trgm_ops exists before applying the schema containing
index("message_content_trgm_idx"). Ensure the migration runs prior to schema
creation and document/remove the dependency if you prefer to avoid the
extension.
In `@packages/ui/package.json`:
- Line 29: The package.json dependency for react-resizable-panels is pinned to a
non-existent version ^4.7.5 causing npm install to fail; update the dependency
to ^4.7.4 in package.json (look for the "react-resizable-panels" entry) so it
resolves correctly, and adjust any related documentation/comments that claim
PanelImperativeHandle was introduced in the newer version—note that
PanelImperativeHandle has been exported since v4.0.2 (update comments/tests if
they assert the wrong introduction version).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 43c38000-b834-4455-bff0-754aa6d2c15d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
apps/api/src/routes/v1/dms/handlers.tsapps/api/src/routes/v1/dms/schema.tsapps/api/src/routes/v1/guilds/handlers.tsapps/web/src/components/chat/header-search.tsxapps/web/src/components/chat/header.tsxapps/web/src/components/sidebar/channel-panel/search-bar.tsxapps/web/src/components/sidebar/index.tsxapps/web/src/components/sidebar/right-panel/guild-members-panel.tsxapps/web/src/components/sidebar/right-panel/pinned-messages-panel.tsxapps/web/src/components/sidebar/right-panel/right-sidebar-context.tsxapps/web/src/components/sidebar/right-panel/right-sidebar-panel.tsxapps/web/src/routes/_authenticated/$guildSlug/$channelId.tsxapps/web/src/routes/_authenticated/dms/$dmId.tsxbiome.jsonpackages/db/src/schemas/messages.tspackages/ui/package.jsonpackages/ui/src/components/resizable.tsx
| if (!isOpen) { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| setIsOpen(true) | ||
| setTimeout(() => inputRef.current?.focus(), 0) | ||
| }} | ||
| className="rounded-sm p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground" | ||
| > | ||
| <Search className="size-4" /> | ||
| </button> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <div ref={containerRef} className="relative"> | ||
| <div className="flex h-7 w-56 items-center gap-1.5 rounded-md border border-border bg-background px-2 text-xs"> | ||
| <Search className="size-3 shrink-0 text-muted-foreground" /> | ||
| <input | ||
| ref={inputRef} | ||
| type="text" | ||
| placeholder={ | ||
| mode === "dm" | ||
| ? "Search this conversation..." | ||
| : "Search this channel..." | ||
| } | ||
| value={query} | ||
| onChange={(e) => handleQueryChange(e.target.value)} | ||
| onKeyDown={(e) => e.key === "Escape" && handleClose()} | ||
| className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground" | ||
| /> | ||
| <button | ||
| type="button" | ||
| onClick={handleClose} | ||
| className="shrink-0 rounded-sm p-0.5 text-muted-foreground hover:text-foreground" | ||
| > | ||
| <X className="size-3" /> | ||
| </button> |
There was a problem hiding this comment.
Label the header search button, field, and close control.
The closed-state search button, the inline input, and the close X button are all unlabeled/icon-only. That makes the new header search effectively invisible to screen readers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/chat/header-search.tsx` around lines 134 - 172, The
search controls are icon-only and lack accessible labels; add ARIA labels and
states: give the closed-state button an aria-label like "Open search" and
aria-expanded={isOpen}; ensure the input (inside the open state) has an explicit
aria-label (dynamic based on mode, e.g., "Search this conversation" or "Search
this channel") rather than relying on placeholder; and add an aria-label to the
close button (e.g., "Close search"). Update the JSX around the Search button,
the input element referenced by inputRef, and the close X button (where
setIsOpen, isOpen, handleQueryChange, handleClose, Search and X are used) to
include these attributes.
| {isCollapsed && context.type === "channel" && ( | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <button | ||
| type="button" | ||
| onClick={toggleCollapsed} | ||
| className="rounded-sm p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground" | ||
| > | ||
| <PanelRight className="size-4" /> | ||
| </button> | ||
| </TooltipTrigger> | ||
| <TooltipContent>Show Members</TooltipContent> | ||
| </Tooltip> |
There was a problem hiding this comment.
Don't promise "Members" when this only toggles collapse state.
This button just calls toggleCollapsed(). If the user collapsed the pinned-messages panel, clicking it will reopen pinned messages while the tooltip still says "Show Members". Either derive the label from view?.type and expose that via aria-label, or switch the view back to guild-members before expanding.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/chat/header.tsx` around lines 80 - 92, The tooltip
incorrectly promises "Show Members" while the button only calls
toggleCollapsed() (seen in the JSX using isCollapsed, context.type === "channel"
and toggleCollapsed); update the behavior so the label matches the actual view:
either derive the tooltip/aria-label from view?.type (use that to set
TooltipContent and button aria-label) or change the click handler to first set
the view to "guild-members" before calling toggleCollapsed(); adjust the
component around the button (where isCollapsed, context.type and toggleCollapsed
are used) to implement one of these fixes and ensure the button has an
appropriate aria-label reflecting the selected view.
| <div ref={containerRef} className="relative px-3 pt-3 pb-1"> | ||
| <div className="flex h-8 items-center gap-2 rounded-md border border-border bg-background px-2.5 text-[13px]"> | ||
| <Search className="size-3.5 shrink-0 text-muted-foreground" /> | ||
| <input | ||
| ref={inputRef} | ||
| type="text" | ||
| placeholder={mode === "dm" ? "Search all DMs" : "Search all channels"} | ||
| value={query} | ||
| onChange={(e) => handleQueryChange(e.target.value)} | ||
| onFocus={() => setIsOpen(true)} | ||
| className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" | ||
| /> | ||
| {query && ( | ||
| <button | ||
| type="button" | ||
| onClick={handleClear} | ||
| className="text-muted-foreground hover:text-foreground" | ||
| > | ||
| <X className="size-3.5" /> | ||
| </button> | ||
| )} |
There was a problem hiding this comment.
Add accessible names to the new search controls.
The input is only labeled by its placeholder, and the clear X button is icon-only. Screen readers won't get a reliable name for either control. Add an aria-label (or visible label) to the input and an aria-label to the clear button.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/sidebar/channel-panel/search-bar.tsx` around lines
132 - 152, The search input and clear button lack accessible names; add
aria-labels to both: on the input element (where inputRef, value={query},
onChange -> handleQueryChange, onFocus -> setIsOpen) add an aria-label that
varies by mode (e.g., aria-label={mode === "dm" ? "Search all direct messages" :
"Search all channels"}), and on the clear button (the element that calls
handleClear and renders <X />) add an aria-label like "Clear search" so screen
readers can identify both controls.
| const startX = e.clientX | ||
| const startWidth = panelWidth | ||
|
|
||
| const handleMouseMove = (moveEvent: MouseEvent) => { | ||
| const delta = startX - moveEvent.clientX | ||
| const newWidth = Math.min(Math.max(startWidth + delta, 240), 480) | ||
| if (panelRef.current) { | ||
| panelRef.current.style.width = `${newWidth}px` | ||
| } | ||
| widthRef.current = newWidth | ||
| } | ||
|
|
||
| const handleMouseUp = () => { | ||
| // Commit width first so the next render has the correct value | ||
| setPanelWidth(widthRef.current) | ||
| // Use rAF to clear resizing after React has committed the new width | ||
| requestAnimationFrame(() => setIsResizing(false)) |
There was a problem hiding this comment.
Seed widthRef from the current width before the drag starts.
If the user presses and releases without moving—or after hydration changed panelWidth—handleMouseUp will commit whatever stale value is still sitting in widthRef.current, which can snap the panel back to an older width.
Suggested fix
const startX = e.clientX
const startWidth = panelWidth
+ widthRef.current = startWidth🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/sidebar/index.tsx` around lines 38 - 54, The drag
flow can commit a stale width because widthRef.current isn't seeded at drag
start; before attaching handleMouseMove/handleMouseUp set widthRef.current =
panelWidth (or startWidth) so handleMouseUp and handleMouseMove operate on the
current width even if the user clicks without moving or hydration changed
panelWidth; update the drag-start logic where startX and startWidth are set (the
same block that initializes startX/startWidth and registers
handleMouseMove/handleMouseUp) to assign widthRef.current and then proceed to
setIsResizing(true) and add the event listeners so
setPanelWidth(widthRef.current) on mouse up will be correct.
| {showRightPanel && isHydrated && ( | ||
| <AnimatePresence> | ||
| {!isCollapsed && ( | ||
| <motion.div | ||
| ref={panelRef} | ||
| initial={{ width: 0, opacity: 0 }} | ||
| animate={{ width: panelWidth, opacity: 1 }} | ||
| exit={{ width: 0, opacity: 0 }} | ||
| transition={{ | ||
| duration: isResizing ? 0 : 0.2, | ||
| ease: [0.4, 0, 0.2, 1], | ||
| }} | ||
| className="flex h-full overflow-hidden" | ||
| > | ||
| <div | ||
| onMouseDown={handleMouseDown} | ||
| className={cn( | ||
| "relative flex h-full w-1.5 shrink-0 cursor-ew-resize items-center justify-center", | ||
| "after:absolute after:inset-y-0 after:left-1/2 after:w-px after:-translate-x-1/2 after:bg-border", | ||
| isResizing | ||
| ? "after:!bg-primary" | ||
| : "hover:after:!bg-primary" | ||
| )} | ||
| /> | ||
| <div className="h-full min-w-0 flex-1 overflow-hidden"> | ||
| <RightSidebarPanel view={view} /> | ||
| </div> | ||
| </motion.div> | ||
| )} | ||
| </AnimatePresence> | ||
| )} |
There was a problem hiding this comment.
Reintroduce the breakpoint guard for the right sidebar.
The old panel was hidden below xl; this wrapper now mounts the resizable sidebar at every viewport width. On smaller layouts that side-by-side panel will steal 240–480px from the message pane instead of switching to an overlay, which is a significant responsive regression.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/sidebar/index.tsx` around lines 82 - 112, The
resizable right sidebar is now mounted at all viewport widths, causing it to
steal space on small screens; add a breakpoint guard so the resizable
side-by-side panel (the motion.div that uses panelRef, panelWidth and
handleMouseDown and renders <RightSidebarPanel />) only mounts on large
viewports (>= xl/min-width:1280px). Implement the guard by checking the viewport
width (e.g., window.matchMedia('(min-width:1280px)') or your existing breakpoint
hook) and wrap the motion.div rendering with that condition (keeping
showRightPanel && isHydrated && !isCollapsed), and ensure the smaller-screen
code path still renders the overlay variant of the right panel instead of the
resizable side-by-side panel.
| function scrollToMessage(messageId: string) { | ||
| const el = document.querySelector(`[data-message-id="${messageId}"]`) | ||
| if (!el) return false | ||
| el.scrollIntoView({ behavior: "smooth", block: "center" }) | ||
| el.classList.add("bg-primary/10") | ||
| setTimeout(() => el.classList.remove("bg-primary/10"), 2000) | ||
| return true |
There was a problem hiding this comment.
msgId navigation still misses older search hits.
This only waits for the default messagesData page and then scrolls an already-rendered DOM node. The new search flows can return hits from much older history, so those clicks will land in the channel without ever loading the target message. You need a fetch-by-id / fetch-around-id step before clearing msgId.
Also applies to: 112-122
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/routes/_authenticated/`$guildSlug/$channelId.tsx around lines 42
- 48, The scrollToMessage function currently only queries the DOM and scrolls an
already-rendered node, so clicks for older search hits fail when the target
message isn't loaded; update the flow to attempt a fetch-by-id (or
fetch-around-id) for the given messageId before querying/scrolling and before
clearing the msgId state. Specifically, add a retrieval step in scrollToMessage
(or its caller) that calls the message fetch API to load the target message into
messagesData (or requests surrounding context) and await its completion; only
after the fetch resolves and the DOM has been updated attempt
document.querySelector and then clear the msgId state (also apply the same
fetch-before-clear fix to the analogous logic used in the other handler
referenced around lines 112-122).
| function scrollToMessage(messageId: string) { | ||
| const el = document.querySelector(`[data-message-id="${messageId}"]`) | ||
| if (!el) return false | ||
| el.scrollIntoView({ behavior: "smooth", block: "center" }) | ||
| el.classList.add("bg-primary/10") | ||
| setTimeout(() => el.classList.remove("bg-primary/10"), 2000) | ||
| return true | ||
| } |
There was a problem hiding this comment.
msgId navigation still misses older DM search hits.
This only scrolls if the target message is already in the currently loaded messagesData page. For older hits returned by DM search, navigation will open the conversation but never load or highlight the matched message. Please fetch the message itself—or the page around it—before removing msgId.
Also applies to: 76-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/routes/_authenticated/dms/`$dmId.tsx around lines 35 - 42, The
current scrollToMessage function only works if the targeted message is already
in the loaded messagesData page; to fix this, update the navigation logic that
reads msgId (and the scrollToMessage function) to first attempt to load the
message (or the page/window containing it) when it isn’t present: call the API
or messageStore to fetch messageById (or compute the page that contains the id),
merge or replace messagesData with that page, then call scrollIntoView and apply
the highlight class; ensure you only remove the msgId navigation token after the
fetch+scroll+highlight completes (also apply the same change to the other
handler at lines ~76-85 that processes msgId).
| "a11y": { | ||
| "noStaticElementInteractions": "off" | ||
| }, |
There was a problem hiding this comment.
Do not disable noStaticElementInteractions globally
Line 49 turns this rule off for the whole repo, which can silently permit new non-semantic interactive elements outside the previous apps/www-only scope. This already affects patterns visible in apps/web/src/routes/_authenticated/invite/$code.tsx (Lines 94-103) and apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx (Lines 756-765). Keep the global rule enabled and use narrow overrides or local suppressions only for validated exceptions (e.g., resize handles/backdrops).
Proposed config fix
"style": {
"noNonNullAssertion": "error"
},
- "a11y": {
- "noStaticElementInteractions": "off"
- },
"security": {
"noDangerouslySetInnerHtml": "error"
}📝 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.
| "a11y": { | |
| "noStaticElementInteractions": "off" | |
| }, | |
| "style": { | |
| "noNonNullAssertion": "error" | |
| }, | |
| "security": { | |
| "noDangerouslySetInnerHtml": "error" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@biome.json` around lines 48 - 50, The global accessibility rule
a11y.noStaticElementInteractions was turned off in the config object
a11y.noStaticElementInteractions; revert this to enable the rule globally (or
remove the global override) and instead add narrow overrides or local
suppressions only for validated exceptions (e.g., resize handles/backdrops)
using file-specific config overrides or inline disabling comments in the
specific components that require it; search for instances like the
previously-flagged interactive patterns to add targeted suppressions rather than
a repo-wide "off".
| index("message_content_trgm_idx").using( | ||
| "gin", | ||
| sql`${table.content} gin_trgm_ops` | ||
| ), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for pg_trgm extension creation in migrations or schema files
rg -n -i 'pg_trgm|create extension' --type sql
rg -n -i 'pg_trgm|create extension' packages/dbRepository: BuckyMcYolo/townhall
Length of output: 212
🏁 Script executed:
#!/bin/bash
# First, understand the structure of packages/db
echo "=== packages/db directory structure ==="
find packages/db -type f -name "*.ts" -o -name "*.js" -o -name "*.sql" | head -30
echo -e "\n=== Search for pg_trgm without type restrictions ==="
rg -i 'pg_trgm' packages/db || echo "No matches found"
echo -e "\n=== Search for create extension (case-insensitive) ==="
rg -i 'create.*extension' packages/db || echo "No matches found"
echo -e "\n=== Search for extension-related patterns ==="
rg -i 'extension' packages/db | head -20Repository: BuckyMcYolo/townhall
Length of output: 1255
🏁 Script executed:
#!/bin/bash
# Verify the actual file and check for imports
echo "=== Checking packages/db/src/schemas/messages.ts ==="
cat -n packages/db/src/schemas/messages.ts | head -80
echo -e "\n=== Check for sql import ==="
head -20 packages/db/src/schemas/messages.ts | grep -n "sql"Repository: BuckyMcYolo/townhall
Length of output: 2968
🏁 Script executed:
#!/bin/bash
# Search for migration or setup files that might enable extensions
echo "=== Look for migration patterns ==="
find packages/db -type f \( -name "*migrat*" -o -name "*init*" -o -name "*setup*" \) | head -20
echo -e "\n=== Check drizzle config ==="
find packages/db -type f -name "*.config.*" -o -name "drizzle.config.*" | xargs cat 2>/dev/null || echo "No drizzle config found"
echo -e "\n=== Search for trgm operator or index patterns ==="
rg 'gin_trgm|trgm|gin.*ops' packages/db || echo "No gin_trgm patterns found"Repository: BuckyMcYolo/townhall
Length of output: 578
🏁 Script executed:
#!/bin/bash
# Check if there's any database initialization or setup script
echo "=== Search for db setup/init files ==="
find . -type f \( -name "*.sql" -o -name "*init*" -o -name "*setup*" -o -name "seed*" \) -path "*/db/*" | head -20
echo -e "\n=== Check for any SQL files in packages/db ==="
find packages/db -type f -name "*.sql" | head -20
echo -e "\n=== Check for raw SQL or DDL patterns in schema files ==="
rg -i 'raw|ddl|extension|sql\`' packages/db/src/schemas/ | head -20
echo -e "\n=== Check root level for database setup ==="
ls -la | grep -i db
find . -maxdepth 2 -type f -name "*sql" | head -10Repository: BuckyMcYolo/townhall
Length of output: 382
🏁 Script executed:
#!/bin/bash
# Check Drizzle documentation approach - look for how extensions might be declared
echo "=== Check if any schema file uses .raw or raw SQL ==="
rg 'sql\`.*CREATE|raw\(' packages/db/src/schemas/ || echo "No raw SQL found"
echo -e "\n=== Check for drizzle folder output ==="
ls -la packages/db/drizzle 2>/dev/null | head -20 || echo "No drizzle folder yet"
echo -e "\n=== Check if there's any documentation or comments about pg_trgm ==="
rg -i 'trgm|extension' packages/db --type ts | grep -v node_modules || echo "No references found"Repository: BuckyMcYolo/townhall
Length of output: 509
Ensure the pg_trgm PostgreSQL extension is enabled in the database.
The gin_trgm_ops operator class requires the pg_trgm extension to exist in PostgreSQL. Without it, schema application will fail with an error like operator class "gin_trgm_ops" does not exist for access method "gin". The extension is not currently defined in the codebase and must be set up separately (e.g., in database initialization scripts or manually via CREATE EXTENSION pg_trgm).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/db/src/schemas/messages.ts` around lines 66 - 69, The GIN trigram
index declaration using gin_trgm_ops (index "message_content_trgm_idx") requires
the pg_trgm extension to be present; add enabling of pg_trgm in your DB setup
(e.g., a migration or initialization script that runs CREATE EXTENSION IF NOT
EXISTS pg_trgm) so the operator class gin_trgm_ops exists before applying the
schema containing index("message_content_trgm_idx"). Ensure the migration runs
prior to schema creation and document/remove the dependency if you prefer to
avoid the extension.
| "react-day-picker": "^9.14.0", | ||
| "react-dom": "^19.2.4", | ||
| "react-resizable-panels": "^4.6.4", | ||
| "react-resizable-panels": "^4.7.5", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
react-resizable-panels 4.7.5 changelog PanelImperativeHandle
💡 Result:
Version 4.7.5 of react-resizable-panels does not appear to exist as of 2026-03-23; the latest version is 4.7.4. No specific changelog entries mention changes to PanelImperativeHandle in the 4.7.x series. PanelImperativeHandle is the TypeScript type for the imperative ref API exposed by the Panel component (via the panelRef prop or usePanelRef hook). It was exported starting in version 4.0.2 after user feedback about missing types during v4 migration. Methods available on PanelImperativeHandle (stable across recent versions including 4.7.x): - collapse: void - Collapses the panel - expand: void - Expands the panel (in 4.7.3, improved to support non-percentage sizes) - getSize: number - Returns current size (as percentage by default) - isCollapsed: boolean - Returns true if panel is collapsed - resize(size: number): void - Resizes to specified size Usage example: import { usePanelRef, PanelImperativeHandle } from 'react-resizable-panels'; const panelRef = usePanelRef; // Later: const handle = panelRef.current; if (handle) { handle.resize(50); handle.expand; } In 4.7.3 changelog: "Imperative Panel API supports non-percentage sizes" - this likely improves expand/resize for pixel/REM units introduced in v4.0.0. Official docs: https://react-resizable-panels.vercel.app/ (Panel component section lists API). CHANGELOG: https://github.com/bvaughn/react-resizable-panels/blob/main/CHANGELOG.md
Citations:
- 1: https://github.com/bvaughn/react-resizable-panels/blob/main/CHANGELOG.md
- 2: https://github.com/bvaughn/react-resizable-panels/releases
- 3: https://www.npmjs.com/package/react-resizable-panels
- 4: https://github.com/bvaughn/react-resizable-panels
- 5: https://react-resizable-panels.vercel.app/
- 6: V4 introduces multiple breaking changes without migration guide and clear documentation bvaughn/react-resizable-panels#532
🏁 Script executed:
cat packages/ui/package.json | head -40Repository: BuckyMcYolo/townhall
Length of output: 1167
Version 4.7.5 does not exist. The latest available version of react-resizable-panels is 4.7.4. Change the dependency to ^4.7.4 to resolve a failed npm install when this dependency is resolved.
Additionally, PanelImperativeHandle has been exported since v4.0.2, not introduced in this version.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ui/package.json` at line 29, The package.json dependency for
react-resizable-panels is pinned to a non-existent version ^4.7.5 causing npm
install to fail; update the dependency to ^4.7.4 in package.json (look for the
"react-resizable-panels" entry) so it resolves correctly, and adjust any related
documentation/comments that claim PanelImperativeHandle was introduced in the
newer version—note that PanelImperativeHandle has been exported since v4.0.2
(update comments/tests if they assert the wrong introduction version).
Summary
This PR implements comprehensive message search plus several related UI and infra improvements:
Core search feature
Frontend integration
Right sidebar & layout UX
Database & infra
Routes/schemas/types
Edge cases & robustness
Documentation
Confidence Score: 4/5
Rationale: well-structured, full-stack implementation following existing patterns with proper typing, schemas, and pagination. Strengths include user isolation, parallelized queries, trigram index addition for performance, and cohesive frontend UX. Minor items to consider before merging: add explicit UI error feedback for failed searches, test coverage for search handlers and scroll-to-message behavior, and verify database migration/application of the new trigram index in deployment processes.