From 8402fe5f81724788049b89202e4b9dbf1f8e69e1 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Wed, 12 Aug 2026 10:40:22 +0300 Subject: [PATCH] fix(hub): source merged-mode search from the server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Searching the hub in merged mode only filtered conversations that had already been paged in, so anything past the current page was unfindable no matter how specific the query. This is the default experience — stores/settings.ts:133 sets mergeChats to true. mergedClassicItems built its conversation rows from the paged set and never consulted convSearchData, while the non-merged branch at app/index.tsx:510 was already wired to it. That asymmetry was the bug: one branch reached the server, the other did not. Take conversations from useConversationSearch while a query is active, since GET /api/search matches message bodies and so finds conversations the client has never loaded. Verified on an iPhone 17 Pro Max simulator, Release build, against the mock suite: e2e/06_search_anchor.yaml previously failed at conversation-row-conv-search-anchor, and that assertion and the tap that follows it now both pass. tsc holds at the 14-error TS2345 baseline tracked in #606; eslint clean. This fixes the source of the list but not the filtering of it. MergedClassicList still re-filters the server's results on title and preview only, so a conversation matched deep in its message history can be fetched and then dropped on the client. The shared e2e fixture cannot detect that, because its preview contains the query. Refs #646. --- app/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/index.tsx b/app/index.tsx index 278af746..2f1c221e 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -300,12 +300,18 @@ export default function ProjectsHub() { .map((s) => ({ kind: 'session' as const, ms: lastActivityMs(s), item: s })) .sort((a, b) => b.ms - a.ms) - const convs = conversations + // While a search is active, take conversations from the server rather than + // from the paged set: /api/search matches message bodies, so it finds + // conversations that were never paged in. Filtering the loaded pages alone + // makes anything past the current page unfindable. + const convSource = debouncedConvSearch ? (convSearchData?.conversations ?? []) : conversations + + const convs = convSource .map((c) => ({ kind: 'conversation' as const, ms: Date.parse(c.lastActivity) || 0, item: c })) .sort((a, b) => b.ms - a.ms) return [...liveSessions, ...idleSessions, ...convs] - }, [visibleSessions, conversations]) + }, [visibleSessions, conversations, debouncedConvSearch, convSearchData]) // FAB // When the user is drilled into a directory in TreeView, the drill store