Public: anyone can read this; the crew can change it.
The terminal launcher finds every repository you can see, not only the first 200 #10985
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/1068-the-terminal-launcher-finds-every-reposi → main 2 commits fork
2 files
changed +69
−1
src/tui/screens/dashboard.ts +40 −1
| @@ -175,6 +175,8 @@ const ORDER: readonly PaneId[] = ["repos", "onyou", "machine"]; | ||
| 175 | 175 | export function dashboardScreen(f: Session, ctx: Ctx): Screen { |
| 176 | 176 | let repos: Repo[] = []; |
| 177 | 177 | let all: Repo[] = []; |
| 178 | /** What a typed filter found past `all`, for the query it was read for. */ | |
| 179 | let found: { query: string; repos: Repo[] } | undefined; | |
| 178 | 180 | let pins: Pin[] = []; |
| 179 | 181 | let pinned = new Set<string>(); |
| 180 | 182 | let onYou: OnYouItem[] = []; |
| @@ -341,6 +343,7 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen { | ||
| 341 | 343 | everyday = everydayHead(); |
| 342 | 344 | pinned = f.user ? pinnedRepoIds(f.db, f.user.id) : new Set<string>(); |
| 343 | 345 | all = listReposVisibleTo(f.db, f.user, { limit: 200 }); |
| 346 | found = undefined; | |
| 344 | 347 | onYou = assignedToMe(); |
| 345 | 348 | onYouSteps = new Map( |
| 346 | 349 | onYou.flatMap((item) => { |
| @@ -854,10 +857,27 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen { | ||
| 854 | 857 | /** Yours with an empty filter; anything you can see once you type — |
| 855 | 858 | * the one way to find someone else's repository from here. */ |
| 856 | 859 | const shown = (): Repo[] => |
| 857 | exploring() ? [] : (filter.query !== "" ? all : repos).filter((repo) => | |
| 860 | exploring() ? [] : (filter.query !== "" ? typedPool() : repos).filter((repo) => | |
| 858 | 861 | filter.matches(`${repo.ownerName}/${repo.name} ${repo.description}`), |
| 859 | 862 | ); |
| 860 | 863 | |
| 864 | /** | |
| 865 | * Everything a typed filter can reach: the newest 200 already read, and | |
| 866 | * whatever the forge's own search finds among every visible repository | |
| 867 | * past them, read once per query so drawing stays a filter. | |
| 868 | */ | |
| 869 | const typedPool = (): Repo[] => { | |
| 870 | const query = filter.query.trim(); | |
| 871 | if (found === undefined || found.query !== query) { | |
| 872 | const seen = new Set(all.map((r) => r.id)); | |
| 873 | found = { | |
| 874 | query, | |
| 875 | repos: launcherSearch(f.db, f.user, query).filter((r) => !seen.has(r.id)), | |
| 876 | }; | |
| 877 | } | |
| 878 | return found.repos.length === 0 ? all : [...all, ...found.repos]; | |
| 879 | }; | |
| 880 | ||
| 861 | 881 | /** |
| 862 | 882 | * The panes the last frame drew, which is what `tab` can reach — a focus |
| 863 | 883 | * ring on a card that is not on screen is worse than no focus ring. The |
| @@ -1577,3 +1597,22 @@ function note(s: Surface, r: Rect, c: Tokens, lines: readonly string[]): void { | ||
| 1577 | 1597 | ), |
| 1578 | 1598 | ); |
| 1579 | 1599 | } |
| 1600 | ||
| 1601 | /** | |
| 1602 | * The repositories a typed launcher filter finds among every one the viewer | |
| 1603 | * can see: the listing's own visibility rule and its owner/name match, so | |
| 1604 | * nothing past the first page is out of reach. `owner/name` is split, since | |
| 1605 | * the match reads the two columns apart. | |
| 1606 | */ | |
| 1607 | export function launcherSearch( | |
| 1608 | db: Parameters<typeof listReposVisibleTo>[0], | |
| 1609 | viewer: Parameters<typeof listReposVisibleTo>[1], | |
| 1610 | query: string, | |
| 1611 | ): Repo[] { | |
| 1612 | const q = query.trim(); | |
| 1613 | if (q === "") return []; | |
| 1614 | const slash = q.indexOf("/"); | |
| 1615 | if (slash > 0 && slash < q.length - 1) | |
| 1616 | return listReposVisibleTo(db, viewer, { owner: q.slice(0, slash), query: q.slice(slash + 1), limit: 200 }); | |
| 1617 | return listReposVisibleTo(db, viewer, { query: q.replace(/\//g, ""), limit: 200 }); | |
| 1618 | } |
test/tui-apps.test.ts +29
| @@ -457,6 +457,35 @@ describe("terminal: apps, runs and the crew", () => { | ||
| 457 | 457 | } |
| 458 | 458 | }, 30_000); |
| 459 | 459 | |
| 460 | test("a typed filter finds a repository past the newest 200 the launcher read first (#1068)", async () => { | |
| 461 | // Rows alone: the launcher reads the table, and 250 bare repositories | |
| 462 | // would take minutes to init. Ids sort newest first, so the lantern is oldest. | |
| 463 | const insert = forge.db.query( | |
| 464 | "INSERT INTO repo (id, owner_id, name, visibility) VALUES (?1, ?2, ?3, ?4)", | |
| 465 | ); | |
| 466 | insert.run("zz1068-000", alice.id, "faraway-lantern", "public"); | |
| 467 | insert.run("zz1068-001", alice.id, "faraway-secret", "private"); | |
| 468 | for (let i = 2; i <= 250; i++) | |
| 469 | insert.run(`zz1068-${String(i).padStart(3, "0")}`, alice.id, `filler-${i}`, "public"); | |
| 470 | const bob = getUserByName(forge.db, "bob")!; | |
| 471 | const h = boot(forge, identityFor(bob)); | |
| 472 | try { | |
| 473 | await h.until("On you"); | |
| 474 | await h.press("/"); | |
| 475 | await h.type("faraway"); | |
| 476 | await h.until("alice/faraway-lantern"); | |
| 477 | // Past the first page, visibility still holds. | |
| 478 | expect(h.text()).not.toContain("faraway-secret"); | |
| 479 | await h.press("esc"); | |
| 480 | await h.press("/"); | |
| 481 | await h.type("alice/faraway-l"); | |
| 482 | await h.until("alice/faraway-lantern"); | |
| 483 | } finally { | |
| 484 | h.stop(); | |
| 485 | forge.db.run("DELETE FROM repo WHERE id LIKE 'zz1068-%'"); | |
| 486 | } | |
| 487 | }, 30_000); | |
| 488 | ||
| 460 | 489 | test("the removed screens are unreachable: no palette entry, no dashboard or account key", async () => { |
| 461 | 490 | const h = boot(forge, identityFor(alice)); |
| 462 | 491 | try { |