Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mexport/app/ask/AskHub.tsx | 28++++++++++------------------
Mexport/e2e-hub/federated-search.spec.ts | 36+++++++++++++++++++++++++++++++++++-
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"); + }); });