Skip to content

Feat/chat search#12

Merged
DIodide merged 8 commits intostagingfrom
feat/chat-search
Mar 18, 2026
Merged

Feat/chat search#12
DIodide merged 8 commits intostagingfrom
feat/chat-search

Conversation

@jon3350
Copy link
Collaborator

@jon3350 jon3350 commented Mar 18, 2026

Main Feature: Chat Search
Allows users to search for keyword matches in conversation titles and individual messages. Users can click on search matches to automatically jump to the part of a conversation where the message was. There are show more/show less buttons for search results that are designed based on iMessage's search UI. Pagination was used but may not be the best approach (see commit messages for more details).

URL Validation:
When editing a harness or creating a new one through the onboarding page, an error message will appear if a user enters an invalid mcp url. The url is check with javascript's URL constructor and a regexp check for no spaces in the url.

UI Changes:
Changed managed harness icon from settings to horizontal slides

jon3350 added 5 commits March 15, 2026 14:55
…essage content. Updated database schema to index chat titles and chat messages. Changed managed harness icon
…ery (may not be best approach since we only load more once and usePaginatedQuery comes from convex/react while useQuery comes from @tanstack/react-query. Other changes are added yellow highlighting when jumping to a message from search
…s matches total queries. This bug occurred because convex doesn't know if it's inital fetch is everyting. Thus, functions that get the total query count were added in conversations.ts
… uses javascript's URL constructor to check for invlaid urls. Also makes sure there are no spaces in the url
@jon3350 jon3350 requested a review from DIodide March 18, 2026 01:18
@DIodide
Copy link
Owner

DIodide commented Mar 18, 2026

@claude review this, address concerns in commit messages

@claude
Copy link

claude bot commented Mar 18, 2026

Claude finished @DIodide's task in 2m 53s —— View job


PR Review: Feat/chat search

  • Gather context and read changed files
  • Review commit messages for concerns
  • Review code changes
  • Post comprehensive feedback

Summary

Good foundational feature — search results feel well-structured and the highlight/jump-to-message UX is a nice touch. There are a few correctness and performance issues worth addressing, including the library mixing concern you flagged yourself.


Commit Message Concerns Addressed

usePaginatedQuery mixing (convex/react vs @tanstack/react-query)

Your concern is valid. usePaginatedQuery is Convex's native subscription-based hook and lives outside the tanstack-query cache. This means:

  • Search results are live-reactive (Convex subscription), but all other data (conversations, harnesses, etc.) goes through tanstack's cache — different staleness and update semantics
  • The usePaginatedQuery status ("LoadingFirstPage", "CanLoadMore", "Exhausted") doesn't integrate with tanstack's isLoading / isFetching

Recommended fix: Since you only load more once (as noted), consider replacing usePaginatedQuery with useQuery + convexQuery using a higher take limit, and manage the "show more" state client-side by slicing. This eliminates the mixing entirely. Fix this →


Issues Found

1. Performance: searchContentCount is O(conversations × messages) — conversations.ts:179

const userConvos = await ctx.db.query("conversations").filter(...).collect(); // all user convos
const results = await ctx.db.query("messages").withSearchIndex(...).collect(); // all matching msgs

Both .collect() calls are unbounded. For a user with many conversations, userConvos.collect() reads every conversation document, and results.collect() reads every matched message. Convex's per-query read limit (8,192 documents) can be hit, causing a runtime error. The count queries exist purely to gate the "Show More" button — consider capping them or deriving the count differently.

Fix this →

2. Performance: N+1 query in searchContentconversations.ts:128

for (const msg of result.page) {
    const convo = await ctx.db.get(msg.conversationId); // N fetches in a loop

This issues one db.get per message in the page. For 15 initial results this is acceptable, but for the expanded load (LOAD_MORE_CONTENT_COUNT = 250) this is 250 sequential reads in a single Convex function call. Consider batching with a Map of pre-fetched conversations keyed by ID.

3. Missing userId on messages search index — schema.ts:83

.searchIndex("search_content", {
    searchField: "content",
    filterFields: ["conversationId"]  // no userId filter
}),

Unlike the title search (which filters ["userId"] at the index level), message search has no userId filter available. The search must return messages from all users and then do a per-message ownership check in JavaScript by fetching each conversation. This is a data-efficiency issue — adding userId to the messages table and including it as a filter field would let you filter at the index level. Fix this →

4. Inline URL edit skips validation — $harnessId.tsx:335

const commitUrl = () => {
    const trimmed = urlDraft.trim();
    if (trimmed && trimmed !== server.url) {
        onUpdate({ ...server, url: trimmed }); // no validation!
    }
    setEditingUrl(false);
};

The AddMcpServerForm validates URLs correctly via the URL constructor + protocol check, but the inline edit flow in McpServerRow.commitUrl() applies the new URL without any validation. A user can edit an existing server URL to not-a-url and it will be saved. The same validation from handleSubmit should be applied here. Fix this →

5. Memory leak: setTimeout in scroll effect without cleanup — chat/index.tsx:1720

useEffect(() => {
    // ...
    setTimeout(() => {
        el.classList.remove("ring-2", "ring-primary", "ring-offset-2", "highlight-fade");
    }, 3000); // no clearTimeout on cleanup
    onClearScrollTarget();
}, [scrollToMessageId, messages, onClearScrollTarget]);

If the component unmounts within 3 seconds of a search jump, the setTimeout callback fires and attempts to remove classes from a DOM element that may no longer exist. Store the timeout ID and clear it in the effect's cleanup function.

Fix this →

6. Flicker: search results shown before both queries complete — chat/index.tsx:1011

{searchQuery &&
(titleSearch.status !== "LoadingFirstPage" ||
    contentSearch.status !== "LoadingFirstPage") ? (

The || condition means the search UI renders as soon as either query finishes loading. If title search is fast but content search is slow, the UI will show title results while content results haven't loaded yet — the "No results" section won't appear but content results will also be absent, which is confusing. Using && here would wait for both, or you can show each section independently with its own loading indicator.


Minor Notes

  • HighlightText key collision: using index (the string match position) as the key for <mark> elements is actually fine here since positions are unique in a single string, but it reads confusingly — naming it matchIndex or matchStart would be clearer.
  • INITIAL_CONTENT_COUNT = 15 slicing: After loadMore(250) is called, contentSearch.results grows to 265 items. If the user then clicks "Show Less", setContentExpanded(false) causes the component to slice to 15 again. But those 265 items are already in memory — there's no way to unload them. This is fine but worth noting; the "Show Less" is visual-only.
  • Dead import: Settings is still imported from lucide-react (line 28) but the icon was replaced by SlidersHorizontal for the Manage Harnesses button. Settings is still used for the Settings button in the sidebar footer, so this is fine — disregard.

What's Working Well

  • The searchContent ownership check (fetching the conversation and verifying userId) correctly prevents cross-user data leakage even without a userId index filter.
  • The HighlightText component correctly handles multiple occurrences of the search term in a single string.
  • Resetting titlesExpanded/contentExpanded to false on query change is the right behavior.
  • The yellow highlight-fade animation on message jump is a nice UX touch.
  • URL validation with new URL() + protocol check + space check covers the important cases.

@DIodide
Copy link
Owner

DIodide commented Mar 18, 2026

@jon3350 address these

jon3350 added 2 commits March 17, 2026 22:38
…pagination. Index still paginates for now though. Stress testing has not been done yet
@DIodide
Copy link
Owner

DIodide commented Mar 18, 2026

@claude review the two most recent commits

@claude
Copy link

claude bot commented Mar 18, 2026

Claude finished @DIodide's task in 1m 37s —— View job


Review: Two Most Recent Commits

  • Gather recent commits
  • Read changed files
  • Post review feedback

Commit Summary

SHA Message
a9c11bd fixed comments from pull request — conversations uses take, index still paginates
d27cb19 minor fix (JSX formatting only)

d27cb19 is pure whitespace/formatting — nothing to review there.


What Was Fixed (a9c11bd)

All six issues from the previous review were addressed:

Good work addressing all of them. A few remaining concerns below.


Remaining Issues

1. useEffect still missing unmount cleanup — chat/index.tsx

The highlightTimeoutRef prevents duplicate timers when scrollToMessageId changes, but the useEffect still doesn't return a cleanup function. If the component unmounts while the 3-second timer is running, the callback still fires:

// current code — no cleanup on unmount
useEffect(() => {
    if (highlightTimeoutRef.current) clearTimeout(highlightTimeoutRef.current);
    // ...
    highlightTimeoutRef.current = setTimeout(() => { ... }, 3000);
    onClearScrollTarget();
}, [scrollToMessageId, messages, onClearScrollTarget]);

Fix: add a cleanup return:

useEffect(() => {
    // ... existing logic ...
    return () => {
        if (highlightTimeoutRef.current) clearTimeout(highlightTimeoutRef.current);
    };
}, [scrollToMessageId, messages, onClearScrollTarget]);

Fix this →

2. userId is optional — old messages won't appear in search — schema.ts

userId: v.optional(v.string()),  // existing messages have no userId

With .eq("userId", identity.subject) as the index filter, any message inserted before this change (no userId field) will be excluded from search results entirely. Users who had conversations before this deploy will see no content search results for those messages. This is probably fine if the app is pre-launch, but worth noting if real data exists.

A backfill migration (iterating existing messages and setting userId from their parent conversation) would fix this.

3. onBlur behavior when URL is invalid — $harnessId.tsx

When commitUrl finds a validation error it sets urlError and returns early — the input stays open. But onBlur fires when the user clicks anywhere outside the input, including the cancel button or other page elements. If the user clicks away expecting the edit to be dismissed, they're now stuck in an editing state showing an error, with no obvious escape other than pressing Escape or fixing the URL.

Consider adding a "Cancel" button next to the input when editing, or explicitly handling the case where the user clicks away on an invalid URL (e.g., revert to original URL). Currently cancelEdit is only reachable via Escape key (handleKeyDown).


Minor Notes

  • The searchContentCount cap of 1000 could undercount for users with many matching messages, causing the "Show More" button to behave incorrectly at scale. Acceptable for now — just worth a comment in the code.
  • searchContent still paginates (as the commit message notes). The mixing concern from the previous review remains for content search but is mitigated by the userId index filter reducing the result set significantly.

….tsx

Keeps data-message-id for search highlighting from feat/chat-search,
and React.Fragment wrapper + editingMessageId logic from staging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@DIodide DIodide merged commit 4586c98 into staging Mar 18, 2026
1 check passed
@DIodide DIodide deleted the feat/chat-search branch March 18, 2026 18:13
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.

2 participants