From 51a76830aded64db972ffbc80fcd6c4834e44c3c Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 7 Sep 2026 15:10:12 +0200 Subject: [PATCH] fix(frontend): give the list reload one owner, taken by both exits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finish reloaded immediately while dismissal waited for the run to stop, so Finish pressed during a retry — `done` survives one, which is what makes the button clickable then — read the list mid-write, and its `finishing` flag stopped the deferred reload from correcting it. Both exits now go through the same wait. One reload per closing, always after the writing stops, whichever way the dialog was left. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 --- .../components/home/ImportProjectModal.svelte | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/components/home/ImportProjectModal.svelte b/frontend/src/lib/components/home/ImportProjectModal.svelte index b089e9fdda..af372341f0 100644 --- a/frontend/src/lib/components/home/ImportProjectModal.svelte +++ b/frontend/src/lib/components/home/ImportProjectModal.svelte @@ -89,11 +89,13 @@ } /** - * Reloads the caller's list once the run is no longer writing. Immediate for a run that - * has finished, which is the common dismissal — but `abandon()` only stops the *next* - * phase, and the request already sent still lands, so a reload issued at that moment can - * read the list before that write commits and leave it stale again. The cap is there so a - * run that never settles still ends in a reload rather than in nothing. + * The one way this dialog reloads the caller's list: once the run is no longer writing. + * Both exits use it, because both can be taken mid-write — `abandon()` only stops the + * *next* phase and the request already sent still lands, and `done` survives a retry so + * Finish is clickable while the run is going again. A reload issued at either of those + * moments reads the list before the write commits and leaves it stale, which is what the + * reload exists to prevent. Immediate when nothing is running, which is the common case. + * The cap is there so a run that never settles still ends in a reload rather than nothing. */ async function reloadWhenSettled(run: ImportExecution, reload: (() => void) | undefined) { for (let i = 0; i < 60 && run.running; i++) { @@ -205,7 +207,12 @@ if (slug) logFeatureUsage('home', 'template_import', { key: slug }) logFeatureUsage('home', 'template_setup', { key: setupKey(setupOutcome, outstanding) }) finishing = true - onImported?.() + // Through the same deferred reload every closing uses. `done` survives a retry, so + // Finish is clickable while the run is going again — reloading here would read the + // list mid-write, and `finishing` then stops `dismiss()` from reloading after it. + // One reload per closing, always after the writing stops. + if (execution) void reloadWhenSettled(execution, onImported) + else onImported?.() onClose() }