fix(ui): keep Lens investigation preview rows in place while matches reload

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yujong Lee 2026-10-04 19:26:36 -07:00
parent fbb6facc0f
commit 3cd22643cc
3 changed files with 37 additions and 3 deletions

View file

@ -430,6 +430,30 @@ it("fetches one preview for two keystrokes inside the debounce window", async ()
}
});
it("shows skeleton rows on first load and keeps the previous rows while a new search loads", async () => {
const pending = new Map<string, (page: unknown) => void>();
proxy.post.mockImplementation(
(_path, options) =>
new Promise((resolve) => {
pending.set((options?.body as { selection: Settings }).selection.q, resolve);
}),
);
const user = userEvent.setup();
renderWithProviders(<InvestigationSetup mode="new" onClose={vi.fn()} onSave={vi.fn()} />);
const preview = within(screen.getByRole("region", { name: "Matching activity" }));
expect((await preview.findAllByTestId("runs-placeholder")).length).toBeGreaterThan(0);
act(() => pending.get("")?.({ eligible: 1, selected: 1, executions: [run("Old run", 1)] }));
expect(await preview.findByText("Old run")).toBeVisible();
expect(preview.queryByTestId("runs-placeholder")).not.toBeInTheDocument();
await user.type(screen.getByRole("combobox", { name: "Search runs" }), "new");
await waitFor(() => expect(pending.has("new")).toBe(true));
expect(preview.getByText("Old run")).toBeVisible();
expect(preview.queryByTestId("runs-placeholder")).not.toBeInTheDocument();
act(() => pending.get("new")?.({ eligible: 1, selected: 1, executions: [run("New run", 2)] }));
expect(await preview.findByText("New run")).toBeVisible();
expect(preview.queryByText("Old run")).not.toBeInTheDocument();
});
it.each(["empty", "error"])("blocks a new investigation when its preview is %s", async (state) => {
if (state === "error") proxy.post.mockRejectedValue(new Error("Storage unavailable"));
else proxy.post.mockResolvedValue({ eligible: 0, selected: 0, executions: [] });

View file

@ -60,6 +60,7 @@ export function MatchingActivityPreview({
...props
}: MatchingActivityPreviewProps) {
const runs = page.executions.flatMap((run) => (run.summary ? [run.summary] : []));
const shown = status.ready || status.stale;
return (
<section
aria-label="Matching activity"
@ -97,21 +98,22 @@ export function MatchingActivityPreview({
No matches. Try removing a filter from the search. Recent trace updates need two minutes to settle.
</p>
)}
{status.ready && runs.length > 0 && (
{(status.loading || (shown && !status.error && runs.length > 0)) && (
<div className="max-h-[60dvh] min-h-0 overflow-auto">
<AgentTracesTable
traces={runs}
isLoading={false}
isLoading={status.loading}
error={null}
hasMore={page.hasMore}
isFetching={page.loadingMore}
isPlaceholder={status.stale}
onLoadMore={page.loadMore}
onSetUpTracing={() => {}}
picks={selection ? runPicks(selection) : undefined}
/>
</div>
)}
{status.ready && <PreviewFooter page={page} selection={selection} />}
{shown && <PreviewFooter page={page} selection={selection} />}
</section>
);
}

View file

@ -29,6 +29,10 @@ export interface PreviewStatus {
readonly title: string;
readonly windowLabel: string;
readonly ready: boolean;
/** No results yet for any search: the table shows skeleton rows. */
readonly loading: boolean;
/** The rows belong to the previous search while this one loads. */
readonly stale: boolean;
readonly error: Error | null;
readonly refresh: () => void;
}
@ -144,6 +148,8 @@ export function useMatchingActivity(): MatchingActivity {
}, [empty, valid, asOf, refresh]);
const pending = settling || preview.isLoading || preview.isPlaceholderData;
const ready = !pending && valid;
const loading = valid && pending && !firstPage;
const stale = valid && pending && !!firstPage;
const setExecutionIds = (next: readonly string[]) =>
setValue("selection.execution_ids", [...next], { shouldValidate: true });
const picked = manualSelection ? manualPicks(selection, setExecutionIds) : null;
@ -156,6 +162,8 @@ export function useMatchingActivity(): MatchingActivity {
title: previewTitle({ pending, validWindow: windowValid, valid }, firstPage),
windowLabel: windowLabel(selection),
ready,
loading,
stale,
error: preview.error,
refresh,
},