diff --git a/web/src/components/GlobalSearch.test.tsx b/web/src/components/GlobalSearch.test.tsx
index 9758a0a2..ca67ae5d 100644
--- a/web/src/components/GlobalSearch.test.tsx
+++ b/web/src/components/GlobalSearch.test.tsx
@@ -94,7 +94,11 @@ describe("GlobalSearch", () => {
mocks.useQuery.mockReset();
mocks.useCanRequest.mockReset();
mocks.useRequestSearch.mockReset();
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: false,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
mocks.useRequestSearch.mockReturnValue({
data: undefined,
isLoading: false,
@@ -145,7 +149,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => {
mocks.useQuery.mockReset();
mocks.useCanRequest.mockReset();
mocks.useRequestSearch.mockReset();
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: false,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
mocks.useRequestSearch.mockReturnValue({
data: undefined,
isLoading: false,
@@ -159,7 +167,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => {
});
it("renders the section with libraryHadHits=true when library returned results", () => {
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: true,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
mocks.useRequestSearch.mockReturnValue({
data: {
page: 1,
@@ -186,7 +198,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => {
});
it("renders the section with libraryHadHits=false when library returned 0 results", () => {
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: true,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
mocks.useQuery.mockReturnValue({
data: { total: 0, has_more: false, items: [] },
isFetching: false,
@@ -216,7 +232,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => {
});
it("does not call useRequestSearch with enabled=true when discoveryEnabled is false", () => {
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: false,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
renderSearchMarkup({ defaultOpen: true, initialQuery: "Dune" });
const call = mocks.useRequestSearch.mock.calls[mocks.useRequestSearch.mock.calls.length - 1];
@@ -224,14 +244,22 @@ describe("GlobalSearch + RequestToAddSection wiring", () => {
});
it("does not mount RequestToAddSection when discovery is disabled", () => {
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: false, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: false,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
const markup = renderSearchMarkup({ defaultOpen: true, initialQuery: "Dune" });
expect(markup).not.toContain('data-testid="request-section"');
});
it("suppresses 'No matches' when library is empty and TMDB is still loading", () => {
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: true,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
mocks.useQuery.mockReturnValue({
data: { total: 0, has_more: false, items: [] },
isFetching: false,
@@ -248,7 +276,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => {
});
it("suppresses 'No matches' when library is empty and TMDB has missing results", () => {
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: true,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
mocks.useQuery.mockReturnValue({
data: { total: 0, has_more: false, items: [] },
isFetching: false,
@@ -278,7 +310,11 @@ describe("GlobalSearch + RequestToAddSection wiring", () => {
});
it("still shows 'No matches' when both library and TMDB are empty", () => {
- mocks.useCanRequest.mockReturnValue({ discoveryEnabled: true, submitDisabledReason: null });
+ mocks.useCanRequest.mockReturnValue({
+ discoveryEnabled: true,
+ isResolving: false,
+ submitDisabledReason: null,
+ });
mocks.useQuery.mockReturnValue({
data: { total: 0, has_more: false, items: [] },
isFetching: false,
diff --git a/web/src/components/GlobalSearch.tsx b/web/src/components/GlobalSearch.tsx
index 9d910a55..5eaf3f80 100644
--- a/web/src/components/GlobalSearch.tsx
+++ b/web/src/components/GlobalSearch.tsx
@@ -111,9 +111,15 @@ export function GlobalSearch({
});
const tmdbMissingCount =
tmdbQuery.data?.results?.filter((result) => result.availability !== "available").length ?? 0;
+ // Cap at DIALOG_LIMIT (4) — RequestToAddSection slices results to that many rows.
+ const tmdbVisibleCount = Math.min(tmdbMissingCount, 4);
const tmdbStillLoading =
canRequest.discoveryEnabled && tmdbDebouncedQuery.length > 1 && tmdbQuery.isLoading;
const tmdbWillRender = canRequest.discoveryEnabled && tmdbMissingCount > 0;
+ // Hide empty state while the TMDB debounce trails the library debounce; otherwise
+ // the user sees "No matches" flash between t=200ms and t=400ms after typing.
+ const tmdbDebounceCatchingUp =
+ canRequest.discoveryEnabled && tmdbDebouncedQuery !== debouncedQuery;
const searchState = useMemo(
() => createCatalogSearchState("query", { q: debouncedQuery || undefined }),
@@ -195,7 +201,9 @@ export function GlobalSearch({
items.length === 0 &&
!previewQuery.isError &&
!tmdbStillLoading &&
- !tmdbWillRender;
+ !tmdbWillRender &&
+ !canRequest.isResolving &&
+ !tmdbDebounceCatchingUp;
const showError = previewQuery.isError;
return (
@@ -252,34 +260,33 @@ export function GlobalSearch({
{showResultsPanel && (
-
- {showLoading && (
-
- Searching...
-
- )}
- {showError && (
-
- Could not load results. Press Enter to open the search page.
-
- )}
- {showEmpty && (
-
- No matches
-
- )}
- {items.map((item, i) => (
-
- ))}
+
+
+ {showLoading && (
+
+ Searching...
+
+ )}
+ {showError && (
+
+ Could not load results. Press Enter to open the search page.
+
+ )}
+ {showEmpty && (
+
+ No matches
+
+ )}
+ {items.map((item, i) => (
+
+ ))}
+
{tmdbDebouncedQuery.length > 1 && canRequest.discoveryEnabled && (
- {items.length} results found
+ {tmdbVisibleCount > 0
+ ? `${items.length} library results, ${tmdbVisibleCount} request suggestions`
+ : `${items.length} results found`}
{total > PREVIEW_LIMIT ? (
diff --git a/web/src/components/RequestPosterCard.test.tsx b/web/src/components/RequestPosterCard.test.tsx
index 215a5ccf..904c9bcd 100644
--- a/web/src/components/RequestPosterCard.test.tsx
+++ b/web/src/components/RequestPosterCard.test.tsx
@@ -24,7 +24,9 @@ describe("RequestPosterCard (discover variant)", () => {
/>
,
);
- expect(markup).toContain("Request");
+ // Must render an actual