commit d9c40b3039f590db623b607dce12d0c77a4720df
parent f28c393d592ed10b3a60c63d0613fb9f374edf1d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 02:33:27 -0400
hub: /ask honours the scope chips
AskHub reads the same list as the front page (useFederatedSites), scope
included: an archive this browser switched off with its chip is not fetched
on /ask either, so the chat grounds in what the search showed. Still not
progressive: the chat waits until every archive in scope has settled.
e2e: with Origin B off, /ask fetches nothing from it and is ready on Origin
A; switched back on, /ask reads it again. Release 9 C2 follow-up.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 45 insertions(+), 19 deletions(-)
diff --git a/export/app/ask/AskHub.tsx b/export/app/ask/AskHub.tsx
@@ -2,19 +2,20 @@
// Hub variant of the /ask chat: wires the federated site registry into the
// multi-origin search data source so AskChat's retrieval (runQueryTree) searches
-// across every shelved archive. Mirrors HubHome's MultiSiteDataProvider wiring,
-// from the same list (useHubSites): the same order, the same colour per archive.
+// the archives the hub's search covers. Mirrors HubHome's MultiSiteDataProvider
+// wiring from the same list (useFederatedSites): the same order, the same
+// colour per archive, and the same scope — an archive this browser switched off
+// with its chip on the front page is not fetched here either, so the chat
+// grounds in what the search showed. Not progressive: the chat waits until
+// every archive in scope has settled, so it never answers from a half-loaded
+// federation.
-import { useMemo } from "react";
import { PlayerProvider } from "yt-dlp-transcript-common/components/PlayerProvider";
import TranscriptModal from "yt-dlp-transcript-common/components/TranscriptModal";
import PostModal from "yt-dlp-transcript-common/components/PostModal";
-import {
- MultiSiteDataProvider,
- type FederatedSite,
-} from "yt-dlp-transcript-common/components/SearchDataContext";
+import { MultiSiteDataProvider } from "yt-dlp-transcript-common/components/SearchDataContext";
import { SearchSessionProvider } from "yt-dlp-transcript-common/components/SearchSessionContext";
-import { useHubSites } from "../components/hub/useHubSites";
+import { useFederatedSites } from "../components/hub/useHubSites";
import AskChat from "./AskChat";
// `transcriptDownloads` comes from the server parent's currentSite() (a client
@@ -25,16 +26,7 @@ export default function AskHub({
}: {
transcriptDownloads?: boolean;
}) {
- const { all } = useHubSites();
- const federated = useMemo<FederatedSite[]>(
- () =>
- all.map((s) => ({
- origin: s.origin,
- siteTitle: s.siteTitle,
- accent: s.accent,
- })),
- [all],
- );
+ const { federated } = useFederatedSites();
// The whole provider stack, in the order SiteWorkspace mounts it for a single
// site — PlayerProvider, then the data source, then the session — because
diff --git a/export/e2e-hub/federated-search.spec.ts b/export/e2e-hub/federated-search.spec.ts
@@ -11,7 +11,7 @@ import { expect, test, type Page, type Route } from "@playwright/test";
// answer (a 404 is not retried); the official archives follow the homepage's
// order (hub-summary.json's) on the cards and the chips, and an archive with no
// accent wears ONE colour — its homepage card's — on its card, its chip and its
-// results.
+// results; /ask searches only the archives the chips leave in.
const ORIGIN_A = "http://localhost:4598";
const ORIGIN_B = "http://localhost:4599";
@@ -364,4 +364,38 @@ test.describe("hub federated search — scope, per-archive state, attribution",
expect(await coloursOf(B)).toEqual({ card: first, dot: first, result: first });
expect(await coloursOf(A)).toEqual({ card: second, dot: second, result: second });
});
+
+ test("/ask searches only the archives the chips leave in", async ({ page }) => {
+ const mocks = await setup(page);
+ await page.goto("/");
+ await chip(page, B).getByRole("button", { name: /Origin B/ }).click();
+ await expect(chip(page, B)).toHaveAttribute("data-status", "off");
+
+ // A fresh page life on /ask: the scope is this browser's, so Origin B is
+ // not fetched there either, and the chat is ready once Origin A is in.
+ // (The composer also reads ready for the instant before the hub's list
+ // loads — nothing to wait for yet — so wait for Origin A's fetch first.)
+ const ready = page.getByPlaceholder(/^Ask about the transcripts/);
+ mocks.a.requests.length = 0;
+ mocks.b.requests.length = 0;
+ await page.goto("/ask");
+ await expect
+ .poll(() => mocks.a.requests)
+ .toContain("/summaries/manifest.json");
+ await expect(ready).toBeVisible();
+ expect(mocks.a.requests).toContain("/summaries/page-0000.json");
+ expect(mocks.b.requests).toEqual([]);
+
+ // Back on (on the front page), /ask reads it again.
+ await page.goto("/");
+ await chip(page, B).getByRole("button", { name: /Origin B/ }).click();
+ await expect(chip(page, B)).toHaveAttribute("data-status", "ready");
+ mocks.b.requests.length = 0;
+ await page.goto("/ask");
+ await expect
+ .poll(() => mocks.b.requests)
+ .toContain("/summaries/manifest.json");
+ await expect(ready).toBeVisible();
+ expect(mocks.b.requests).toContain("/summaries/page-0000.json");
+ });
});