From 2c6730ce0271e9d27e80d6f95f6c23d97ad8a2d7 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Thu, 3 Sep 2026 18:29:04 +0200 Subject: [PATCH] fix(frontend): stop a dismissed project import from running on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modal reports dismissal only through its bindable `open` — the X, Escape and the backdrop dispatch neither `confirmed` nor `canceled`. Bind it, so clearing `pick` follows the dialog closing: re-picking the same project opens it again, and a run still in flight is abandoned with a toast instead of writing to the workspace with no UI in front of it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 --- .../components/home/ImportProjectModal.svelte | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/home/ImportProjectModal.svelte b/frontend/src/lib/components/home/ImportProjectModal.svelte index bc5682de94..90a219d441 100644 --- a/frontend/src/lib/components/home/ImportProjectModal.svelte +++ b/frontend/src/lib/components/home/ImportProjectModal.svelte @@ -16,6 +16,7 @@ import type { ImportPlan } from '$lib/importWizard/plan' import { workspaceStore } from '$lib/stores' import { logFeatureUsage } from '$lib/utils/featureUsage' + import { sendUserToast } from '$lib/toast' interface Props { /** The project the picker chose. Setting it opens the dialog. */ @@ -29,6 +30,38 @@ let slug = $derived(pick?.slug) + // Bound, not one-way: the dialog closes itself on the X, Escape and the backdrop, and + // `Modal` reports none of those — it dispatches `confirmed`/`canceled` only. Left unbound, + // a dismissal hid the dialog while `pick` still held the project, so re-picking the same + // one did nothing, and a run in flight kept writing with no UI in front of it. + let modalOpen = $state(false) + let wasOpen = false + $effect(() => { + const shouldBeOpen = pick !== undefined + if (shouldBeOpen !== untrack(() => modalOpen)) modalOpen = shouldBeOpen + }) + $effect(() => { + const isOpen = modalOpen + untrack(() => { + if (!isOpen && wasOpen) dismiss() + wasOpen = isOpen + }) + }) + + /** + * Dismissed rather than finished. A run in flight is stopped at the next phase boundary — + * nothing can abort a request already sent — and what it has already written stays: a + * second run asks the workspace what it holds, so reopening the project carries on from + * there rather than importing twice. + */ + function dismiss() { + if (execution?.running) { + execution.abandon() + sendUserToast('Import stopped. What it already added stays; reopen the project to finish.') + } + onClose() + } + // The wizard route asks step 1 which workspace to import into and step 2 which one it // is. Opened from inside a workspace both answers are already given, so the dialog // starts at the import itself and the plan is fixed rather than URL-driven. @@ -173,8 +206,7 @@