Fix Quick Start flash when navigating to /runs with archived prefs

The Runs nav link goes to /runs (no query string), so the route briefly
rendered with default params (view=columns, archived=false) before a
post-commit useEffect restored the URL from stored preferences. On
repeat clicks the useAllRuns SWR cache for {includeArchived:false}
returned zero rows immediately, flashing the Quick Start landing for
users whose only runs are archived.

Resolve workspace search params synchronously during render via
resolveRunsWorkspaceSearchParams(), so the first frame already reflects
stored prefs. The effect now just writes the URL back to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-23 21:35:15 -04:00
parent 39fa73d5e2
commit 6eb57685d4
No known key found for this signature in database
2 changed files with 62 additions and 24 deletions

View file

@ -72,9 +72,17 @@ const pageRuns: PaginatedRunList = {
meta: { has_more: false, total: 1 },
};
const queryCalls: Array<{ hook: string; args: unknown[] }> = [];
mock.module("../lib/queries", () => ({
useAllRuns: () => ({ data: allRuns, isLoading: false }),
useRunsPage: () => ({ data: pageRuns, isLoading: false }),
useAllRuns: (...args: unknown[]) => {
queryCalls.push({ hook: "useAllRuns", args });
return { data: allRuns, isLoading: false };
},
useRunsPage: (...args: unknown[]) => {
queryCalls.push({ hook: "useRunsPage", args });
return { data: pageRuns, isLoading: false };
},
useAuthConfig: () => ({ data: { methods: ["github"] } }),
useSystemInfo: () => ({ data: { server_url: "http://127.0.0.1:32276" } }),
}));
@ -166,6 +174,7 @@ beforeEach(() => {
storage = new MemoryStorage();
teardownReactEnv = setupReactTestEnv();
installWindow();
queryCalls.length = 0;
});
afterEach(() => {
@ -191,6 +200,26 @@ describe("Runs workspace preference restoration", () => {
expect(buttonByLabel(renderer, "List view").props["aria-pressed"]).toBe(true);
});
test("/runs applies stored list+archived prefs on the first render (no Quick Start flash)", async () => {
storage.setItem(
RUNS_PREFERENCES_STORAGE_KEY,
JSON.stringify({ version: 1, view: "list", archived: true }),
);
await renderRuns("/runs");
// The first frame the user sees must already reflect stored prefs.
// Before this was fixed, the route briefly rendered the columns view
// with includeArchived=false (default state) before a post-commit
// useEffect restored the URL, flashing the Quick Start empty state for
// users whose only runs were archived.
const firstAllRuns = queryCalls.find((c) => c.hook === "useAllRuns");
const firstRunsPage = queryCalls.find((c) => c.hook === "useRunsPage");
expect(firstAllRuns?.args).toEqual([{ includeArchived: true }, false]);
expect(firstRunsPage?.args[0]).toMatchObject({ includeArchived: true });
expect(firstRunsPage?.args[1]).toBe(true);
});
test("/runs?view=columns ignores stored list view", async () => {
storage.setItem(
RUNS_PREFERENCES_STORAGE_KEY,

View file

@ -783,6 +783,20 @@ export function loadStoredRunsWorkspaceSearchParams(
}
}
// Resolve which search params should drive rendering. If the URL has no
// workspace params (e.g. the user clicked the Runs nav link, which goes to
// `/runs`), fall back to stored preferences so the first render already
// reflects the user's view/archived/etc. choice instead of route defaults.
// Without this, users whose only runs are archived briefly see the empty
// Quick Start landing before a post-commit effect restores `archived=1`.
export function resolveRunsWorkspaceSearchParams(
urlSearchParams: URLSearchParams,
): URLSearchParams {
if (hasRunsWorkspaceParams(urlSearchParams)) return urlSearchParams;
const stored = loadStoredRunsWorkspaceSearchParams();
return stored.toString() === "" ? urlSearchParams : stored;
}
export function persistRunsWorkspaceSearchParams(
searchParams: URLSearchParams,
storage: Pick<Storage, "setItem"> | null = runsPreferencesStorage(),
@ -1719,7 +1733,11 @@ function RunsLandingEmpty({
}
export default function Runs() {
const [searchParams, setSearchParams] = useSearchParams();
const [urlSearchParams, setSearchParams] = useSearchParams();
const searchParams = useMemo(
() => resolveRunsWorkspaceSearchParams(urlSearchParams),
[urlSearchParams],
);
const query = searchParams.get("search") ?? "";
const repoFilter = searchParams.get("repo") ?? "all";
const workflowFilter = searchParams.get("workflow") ?? "all";
@ -1737,21 +1755,16 @@ export default function Runs() {
const updateParam = useCallback(
(key: string, value: string | null) => {
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
if (value == null || value === "") {
next.delete(key);
} else {
next.set(key, value);
}
persistRunsWorkspaceSearchParams(next);
return next;
},
{ replace: true },
);
const next = new URLSearchParams(searchParams);
if (value == null || value === "") {
next.delete(key);
} else {
next.set(key, value);
}
persistRunsWorkspaceSearchParams(next);
setSearchParams(next, { replace: true });
},
[setSearchParams],
[searchParams, setSearchParams],
);
const setQuery = (value: string) => updateParam("search", value || null);
@ -1789,13 +1802,9 @@ export default function Runs() {
);
useEffect(() => {
if (hasRunsWorkspaceParams(searchParams)) return;
const storedParams = loadStoredRunsWorkspaceSearchParams();
if (storedParams.toString() === "") return;
setSearchParams(storedParams, { replace: true });
}, [searchParams, setSearchParams]);
if (searchParams === urlSearchParams) return;
setSearchParams(searchParams, { replace: true });
}, [searchParams, urlSearchParams, setSearchParams]);
const boardRuns = useAllRuns({ includeArchived }, view === "columns");
const listRunsPage = useRunsPage(