fix(relay): upgrade profile-only prefix typeahead to substring matching

Shipped desktop and mobile builds send search_mode="prefix" for user
pickers over kinds:[0] and can't be retroactively taught "contains".
Every prefix hit is a substring hit and profile-only searches are always
picker typeaheads, so the bridge maps that shape to contains — installed
apps get mid-token matching as soon as the relay deploys, no client
update needed. Message typeahead queries other kinds and keeps true
prefix matching.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Tom Bates
2026-07-23 11:39:12 -03:00
co-authored by Cursor
parent 4ebad78b63
commit 558941a3de
+56 -1
View File
@@ -346,6 +346,26 @@ fn extract_search_mode(raw: &Value) -> buzz_search::SearchMode {
}
}
/// Upgrade profile-only prefix typeahead to substring matching.
///
/// Shipped clients (desktop, mobile) send `search_mode: "prefix"` for their
/// user-picker typeahead over exactly `kinds:[0]` and expect any typed
/// fragment of a name to match — but names are single FTS lexemes, so prefix
/// matching misses mid-name fragments ("kurs" never finds "mattkursmark").
/// Every prefix hit is a substring hit, so mapping these queries to
/// `Contains` fixes installed apps without a client update. Message
/// typeahead queries other kinds and keeps true prefix matching.
fn effective_search_mode(
mode: buzz_search::SearchMode,
kinds: Option<&[i32]>,
) -> buzz_search::SearchMode {
if mode == buzz_search::SearchMode::Prefix && kinds == Some(&[0]) {
buzz_search::SearchMode::Contains
} else {
mode
}
}
fn extract_search_page(raw: &Value) -> u32 {
raw.get("page")
.or_else(|| raw.get("search_page"))
@@ -1684,6 +1704,7 @@ async fn handle_bridge_search(
});
let since = filter.since.map(|s| s.as_secs() as i64);
let until = filter.until.map(|u| u.as_secs() as i64);
let mode = effective_search_mode(search_mode, kinds.as_deref());
let search_query = buzz_search::SearchQuery {
community: tenant.community(),
@@ -1695,7 +1716,7 @@ async fn handle_bridge_search(
until,
page: search_page,
per_page: limit,
mode: search_mode,
mode,
};
let search_result = state
@@ -2264,6 +2285,40 @@ mod tests {
);
}
#[test]
fn profile_only_prefix_typeahead_upgrades_to_contains() {
use buzz_search::SearchMode;
assert_eq!(
effective_search_mode(SearchMode::Prefix, Some(&[0])),
SearchMode::Contains,
"user-picker typeahead from shipped clients gets substring matching"
);
assert_eq!(
effective_search_mode(SearchMode::Prefix, Some(&[9, 40002])),
SearchMode::Prefix,
"message typeahead keeps true prefix"
);
assert_eq!(
effective_search_mode(SearchMode::Prefix, Some(&[0, 9])),
SearchMode::Prefix,
"mixed-kind searches keep true prefix"
);
assert_eq!(
effective_search_mode(SearchMode::Prefix, None),
SearchMode::Prefix
);
assert_eq!(
effective_search_mode(SearchMode::FullText, Some(&[0])),
SearchMode::FullText,
"whole-word search is never silently widened"
);
assert_eq!(
effective_search_mode(SearchMode::Contains, Some(&[0])),
SearchMode::Contains
);
}
#[test]
fn bridge_search_mode_extension_accepts_contains() {
assert_eq!(