From cdf3601923f48c78e407be20face858767fc3ce2 Mon Sep 17 00:00:00 2001
From: Devin Foley
Date: Thu, 30 Jul 2026 13:22:12 -0700
Subject: [PATCH] fix(ui): settle superseded preview requests silently
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A preview left in flight across a structural configuration change could
still publish its result or error, so the page described a package that
was no longer selected. Previews now carry a request generation that
structural changes bump: a superseded preview keeps its progress panel
while running (imports and previews stay attached per the earlier fix)
but settles silently — no stale preview render, error panel, or toast.
Imports are deliberately not gated: they mutate the server, so their
outcome is always published. Component tests cover the silent settle and
the current-request failure path.
---
ui/src/pages/CompanyImport.test.tsx | 15 +++++++++++++--
ui/src/pages/CompanyImport.tsx | 22 +++++++++++++++++-----
2 files changed, 30 insertions(+), 7 deletions(-)
diff --git a/ui/src/pages/CompanyImport.test.tsx b/ui/src/pages/CompanyImport.test.tsx
index 9e282bbdf55..d9d32abdfaa 100644
--- a/ui/src/pages/CompanyImport.test.tsx
+++ b/ui/src/pages/CompanyImport.test.tsx
@@ -346,8 +346,9 @@ describe("CompanyImport", () => {
expect(container.textContent).toContain("Uploading and analyzing your package");
expect(container.textContent).toContain("Keep this page open.");
- // Config edits while the request is in flight must not detach it from
- // the UI: the progress panel keeps reporting it until it settles.
+ // A mid-flight config edit keeps the progress panel visible (the request
+ // really is still running) but supersedes the request, so its failure
+ // settles silently instead of describing a package no longer selected.
await enterGithubUrl("https://github.com/acme/starter-b/tree/main/company");
expect(container.textContent).toContain("Uploading and analyzing your package");
@@ -358,6 +359,16 @@ describe("CompanyImport", () => {
await flushReact();
expect(container.textContent).not.toContain("Uploading and analyzing your package");
+ expect(container.textContent).not.toContain("Preview failed:");
+ expect(mockPushToast).not.toHaveBeenCalled();
+
+ // A failure of the currently configured request renders a durable panel.
+ await clickButton((text) => text === "Preview import");
+ await act(async () => {
+ rejectPreview(new Error("stream disconnected"));
+ });
+ await flushReact();
+
expect(container.textContent).toContain("Preview failed: stream disconnected");
expect(container.textContent).toContain("Retry, or use the CLI folder import");
expect(mockPushToast).toHaveBeenCalledWith(expect.objectContaining({ tone: "error" }));
diff --git a/ui/src/pages/CompanyImport.tsx b/ui/src/pages/CompanyImport.tsx
index 9556b472ea3..0f22c4d7061 100644
--- a/ui/src/pages/CompanyImport.tsx
+++ b/ui/src/pages/CompanyImport.tsx
@@ -770,9 +770,16 @@ export function CompanyImport() {
return { type: "github", url };
}
+ // Monotonic id for preview requests. Structural configuration changes bump
+ // it, so an in-flight preview they supersede settles silently instead of
+ // publishing a result or error for a package that is no longer selected.
+ // Imports are not gated this way: they mutate the server, so their outcome
+ // is always published.
+ const previewGenerationRef = useRef(0);
+
// Preview mutation
const previewMutation = useMutation({
- mutationFn: () => {
+ mutationFn: (_generation: number) => {
const source = buildSource();
if (!source) throw new Error("No source configured.");
return companiesApi.importPreview({
@@ -785,7 +792,8 @@ export function CompanyImport() {
collisionStrategy,
});
},
- onSuccess: (result) => {
+ onSuccess: (result, generation) => {
+ if (generation !== previewGenerationRef.current) return;
setImportPreview(result);
// Build conflicts and set default name overrides with prefix
@@ -848,7 +856,8 @@ export function CompanyImport() {
const firstFile = Object.keys(result.files)[0];
if (firstFile) setSelectedFile(firstFile);
},
- onError: (err) => {
+ onError: (err, generation) => {
+ if (generation !== previewGenerationRef.current) return;
pushToast({
tone: "error",
title: "Preview failed",
@@ -939,6 +948,7 @@ export function CompanyImport() {
}
function resetImportFlowState() {
+ previewGenerationRef.current += 1;
setImportPreview(null);
resetMutationState();
}
@@ -1457,7 +1467,7 @@ export function CompanyImport() {