Skip to content

Dev#25

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

Dev#25
BuckyMcYolo merged 4 commits intomainfrom
dev

Conversation

@BuckyMcYolo
Copy link
Copy Markdown
Owner

@BuckyMcYolo BuckyMcYolo commented Mar 22, 2026

Summary

This PR implements comprehensive message search plus several related UI and infra improvements:

Core search feature

  • New DM message search endpoint: GET /dms/search — paginated, case-insensitive content search scoped to DM/group DM channels the user belongs to; returns paginated results with author info, channelName, ISO timestamps.
  • New guild message search endpoint: GET /guilds/{guildSlug}/search — paginated, case-insensitive search across text/announcement/forum channels with optional channelId filter and guildAuthMiddleware; returns paginated results with channelName, author details, and ISO timestamps.
  • Both endpoints use Drizzle ORM ilike with escaped %/_/\ patterns, run parallel count/fetch queries (Promise.all), and compute nextPage/prevPage.

Frontend integration

  • SearchBar rewritten to an interactive, debounced (300ms) search input with clear button, click-outside handling, loading/empty states, result dropdown with avatars, channel context, timestamps, and MessageMarkdown previews; clicking a result navigates to the message (passes msgId in search params).
  • New HeaderSearch component providing an inline header search UI (guild/dm modes) with similar behavior and results dropdown.
  • ChatHeader now accepts channelId prop and renders HeaderSearch (mode="guild" for channels, "dm" otherwise) and a PanelRight toggle when appropriate.
  • Channel and DM routes accept a msgId search param; added scrollToMessage helpers that find, highlight, and scroll to a message, and clear the search param after navigation.
  • DMPanel updated to pass mode="dm" to SearchBar.

Right sidebar & layout UX

  • Added persistent right-sidebar state: isCollapsed, toggleCollapsed, panelWidth, setPanelWidth, isHydrated (localStorage hydration).
  • RightSidebarPanel and related panels updated to support collapse/expand and a resize handle with animated mount/unmount via AnimatePresence/motion.div; Resizable handle behavior commits width on mouseup.
  • Several right-panel headers wired to toggleCollapsed and PanelRight icon.

Database & infra

  • Added a GIN trigram index (message_content_trgm_idx) on message.content to support efficient text search.
  • Exposed a new exported type re-export (PanelImperativeHandle) and bumped react-resizable-panels dependency to ^4.7.5.
  • biome.json: disabled noStaticElementInteractions accessibility lint rule override.

Routes/schemas/types

  • New Zod/OpenAPI schemas for search queries and paginated responses (guild and DM): search query constraints (1–100 chars), optional channelId/dmId UUID where applicable.
  • New exported route types: SearchMessagesRoute, SearchDMMessagesRoute; handlers exported: searchMessages, searchDMMessages.
  • Router registrations added via .openapi(routes.X, handlers.X).

Edge cases & robustness

  • Empty/unauthorized channel sets return empty paginated responses.
  • Guild search limits to text/announcement/forum channel types; channelId filter validated against eligible channels.
  • DM search enforces membership; channelName resolution for group_dm uses channel.name, otherwise joins other member names (excludes current user) falling back to "DM".
  • Frontend debounce and click-outside listeners include proper cleanup; search disabled when query empty or required route context missing.

Documentation

  • ROADMAP.md updated: Message search marked completed (Phase 5) with guild-wide and DM search APIs and interactive dropdown UI; Privacy settings marked completed with expanded scope noted; other roadmap refinements (desktop app, bots/webhooks) adjusted.

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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Guild Message Search API
apps/api/src/routes/v1/guilds/handlers.ts, apps/api/src/routes/v1/guilds/routes.ts, apps/api/src/routes/v1/guilds/schema.ts, apps/api/src/routes/v1/guilds/index.ts
New GET /guilds/{guildSlug}/search route, query/schema additions, and handler implementing paginated, case-insensitive content search across text/announcement/forum channels, returning message+author+channelName results.
DM Message Search API
apps/api/src/routes/v1/dms/handlers.ts, apps/api/src/routes/v1/dms/routes.ts, apps/api/src/routes/v1/dms/schema.ts, apps/api/src/routes/v1/dms/index.ts
New GET /dms/search route, schemas, and handler that scopes to user DM channels (optional dmId), builds channel labels, and returns paginated ilike-based results with author data.
Database index
packages/db/src/schemas/messages.ts
Added a GIN trigram index (message_content_trgm_idx) on message.content to support faster text search.
Search UI / Navigation
apps/web/src/components/sidebar/channel-panel/search-bar.tsx, apps/web/src/components/chat/header-search.tsx, apps/web/src/components/chat/header.tsx, apps/web/src/components/sidebar/dm-panel/dm-panel.tsx, apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx, apps/web/src/routes/_authenticated/dms/$dmId.tsx
Introduced interactive SearchBar and HeaderSearch components (debounced 300ms), wired to guild/DM endpoints, render dropdown results, navigate to selected messages (adds msgId search param and scroll-to-message behavior), and updated ChatHeader to accept channelId prop.
Right sidebar: state, panel, and UX
apps/web/src/components/sidebar/index.tsx, apps/web/src/components/sidebar/right-panel/right-sidebar-context.tsx, apps/web/src/components/sidebar/right-panel/right-sidebar-panel.tsx, apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx, apps/web/src/components/sidebar/right-panel/pinned-messages-panel.tsx
Added persistent right-sidebar state (isCollapsed, panelWidth, hydration), collapse toggle and resize handle with animation, updated panels to use collapse control and adjusted layout/rendering.
UI package & resizable export
packages/ui/package.json, packages/ui/src/components/resizable.tsx
Bumped react-resizable-panels dependency and re-exported PanelImperativeHandle type.
Lint config
biome.json
Disabled noStaticElementInteractions accessibility rule via linter config.
Roadmap / Docs
ROADMAP.md
Marked Privacy settings and Message search as completed and expanded roadmap notes (desktop app notifications, inbound webhooks mention behavior).

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰✨ I dug through threads both guild and DM,
A twitch, a hop — results come to me,
Dropdown of whispers, avatars in line,
I point, you jump — the message you find,
Thump-thump hooray, search carrots for all to see!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Dev' is vague and generic, providing no meaningful information about the changeset's primary purpose or scope. Replace with a descriptive title that summarizes the main change, such as 'Add message search functionality for guilds and DMs' or 'Implement DM and guild message search with UI components'.
✅ 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9534cde and 146f9e2.

📒 Files selected for processing (11)
  • ROADMAP.md
  • apps/api/src/routes/v1/dms/handlers.ts
  • apps/api/src/routes/v1/dms/index.ts
  • apps/api/src/routes/v1/dms/routes.ts
  • apps/api/src/routes/v1/dms/schema.ts
  • apps/api/src/routes/v1/guilds/handlers.ts
  • apps/api/src/routes/v1/guilds/index.ts
  • apps/api/src/routes/v1/guilds/routes.ts
  • apps/api/src/routes/v1/guilds/schema.ts
  • apps/web/src/components/sidebar/channel-panel/search-bar.tsx
  • apps/web/src/components/sidebar/dm-panel/dm-panel.tsx

Comment thread apps/api/src/routes/v1/dms/handlers.ts
Comment thread apps/api/src/routes/v1/dms/handlers.ts Outdated
Comment thread apps/api/src/routes/v1/guilds/handlers.ts Outdated
Comment thread apps/web/src/components/sidebar/channel-panel/search-bar.tsx
Comment thread apps/web/src/components/sidebar/channel-panel/search-bar.tsx Outdated
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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 146f9e2 and 3528755.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • apps/api/src/routes/v1/dms/handlers.ts
  • apps/api/src/routes/v1/dms/schema.ts
  • apps/api/src/routes/v1/guilds/handlers.ts
  • apps/web/src/components/chat/header-search.tsx
  • apps/web/src/components/chat/header.tsx
  • apps/web/src/components/sidebar/channel-panel/search-bar.tsx
  • apps/web/src/components/sidebar/index.tsx
  • apps/web/src/components/sidebar/right-panel/guild-members-panel.tsx
  • apps/web/src/components/sidebar/right-panel/pinned-messages-panel.tsx
  • apps/web/src/components/sidebar/right-panel/right-sidebar-context.tsx
  • apps/web/src/components/sidebar/right-panel/right-sidebar-panel.tsx
  • apps/web/src/routes/_authenticated/$guildSlug/$channelId.tsx
  • apps/web/src/routes/_authenticated/dms/$dmId.tsx
  • biome.json
  • packages/db/src/schemas/messages.ts
  • packages/ui/package.json
  • packages/ui/src/components/resizable.tsx

Comment on lines +134 to +172
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>
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

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.

Comment on lines +80 to +92
{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>
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 | 🟡 Minor

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.

Comment on lines +132 to +152
<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>
)}
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

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.

Comment on lines +38 to +54
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))
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 | 🟡 Minor

Seed widthRef from the current width before the drag starts.

If the user presses and releases without moving—or after hydration changed panelWidthhandleMouseUp 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.

Comment on lines +82 to +112
{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>
)}
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

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.

Comment on lines +42 to +48
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
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

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

Comment on lines +35 to +42
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
}
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

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

Comment thread biome.json
Comment on lines +48 to +50
"a11y": {
"noStaticElementInteractions": "off"
},
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

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.

Suggested change
"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".

Comment on lines +66 to +69
index("message_content_trgm_idx").using(
"gin",
sql`${table.content} gin_trgm_ops`
),
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 | 🔴 Critical

🧩 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/db

Repository: 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 -20

Repository: 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 -10

Repository: 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.

Comment thread packages/ui/package.json
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.4",
"react-resizable-panels": "^4.6.4",
"react-resizable-panels": "^4.7.5",
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 | 🔴 Critical

🧩 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:


🏁 Script executed:

cat packages/ui/package.json | head -40

Repository: 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).

@BuckyMcYolo BuckyMcYolo merged commit 78f577b into main Mar 23, 2026
1 check passed
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