|
560 | 560 | } |
561 | 561 | let pyodideReady = $state(false); |
562 | 562 | let pyodideLoading = $state(false); |
| 563 | + // True once startup `bootstrapToolboxes()` has finished (or failed). The |
| 564 | + // engine wheel being up (`pyodideReady`) is not enough: the bootstrap still |
| 565 | + // installs the preloaded catalog toolboxes afterwards (and, in engine builds |
| 566 | + // that resolve dependencies, the engine base + docutils) via micropip. The |
| 567 | + // run button folds this in so it stays in its loading state until that work |
| 568 | + // is done, instead of unlocking the moment the wheel is ready. |
| 569 | + let bootstrapComplete = $state(false); |
| 570 | + let runLoading = $derived(pyodideLoading || !bootstrapComplete); |
| 571 | + let runReady = $derived(pyodideReady && bootstrapComplete); |
563 | 572 | let simRunning = $state(false); |
564 | 573 | let isRunStarting = false; // Synchronous flag to prevent race conditions |
565 | 574 | let isContinuing = false; // Synchronous flag to prevent rapid continue calls |
|
592 | 601 | await autoDetectBackend(); |
593 | 602 | await initBackendFromUrl(); |
594 | 603 | await initPyodide(); |
| 604 | + statusText = 'Loading toolboxes...'; |
595 | 605 | await bootstrapToolboxes(); |
| 606 | + statusText = 'Ready'; |
596 | 607 | } catch (e) { |
597 | 608 | console.error('[startup] backend init failed', e); |
598 | 609 | throw e; |
| 610 | + } finally { |
| 611 | + // Unlock the run button even if bootstrap failed — a broken |
| 612 | + // toolbox shouldn't leave the button stuck in its loading state. |
| 613 | + bootstrapComplete = true; |
599 | 614 | } |
600 | 615 | })(); |
601 | 616 | void loadFromUrlParam(backendReady).catch((e) => { |
|
995 | 1010 | // Run simulation (auto-initializes if needed) |
996 | 1011 | async function handleRun() { |
997 | 1012 | // Prevent concurrent simulation runs (synchronous check for rapid key presses) |
998 | | - if (simRunning || isRunStarting || pyodideLoading) return; |
| 1013 | + if (simRunning || isRunStarting || runLoading) return; |
999 | 1014 |
|
1000 | 1015 | // Set flag before any async operations to prevent race conditions |
1001 | 1016 | isRunStarting = true; |
|
1331 | 1346 | <Icon name="stop-filled" size={16} /> |
1332 | 1347 | </button> |
1333 | 1348 | {:else} |
1334 | | - <div class="run-btn-wrapper" class:loading={pyodideLoading}> |
| 1349 | + <div class="run-btn-wrapper" class:loading={runLoading}> |
1335 | 1350 | <button |
1336 | 1351 | class="toolbar-btn run-btn" |
1337 | | - class:active={!pyodideLoading} |
1338 | | - class:loading={pyodideLoading} |
| 1352 | + class:active={!runLoading} |
| 1353 | + class:loading={runLoading} |
1339 | 1354 | onclick={handleRun} |
1340 | | - disabled={pyodideLoading} |
1341 | | - use:tooltip={{ text: pyodideReady ? "Run" : "Initialize & Run", shortcut: "Ctrl+Enter" }} |
| 1355 | + disabled={runLoading} |
| 1356 | + use:tooltip={{ text: runReady ? "Run" : "Initialize & Run", shortcut: "Ctrl+Enter" }} |
1342 | 1357 | aria-label="Run" |
1343 | 1358 | data-tour="toolbar-run" |
1344 | 1359 | > |
1345 | | - {#if pyodideLoading} |
| 1360 | + {#if runLoading} |
1346 | 1361 | <span class="loading-status">{statusText}</span> |
1347 | 1362 | <span class="spinner"><Icon name="loader" size={16} /></span> |
1348 | 1363 | {:else} |
|
1353 | 1368 | {/if} |
1354 | 1369 | <button |
1355 | 1370 | class="toolbar-btn" |
1356 | | - class:active={hasRunSimulation && pyodideReady && !simRunning} |
| 1371 | + class:active={hasRunSimulation && runReady && !simRunning} |
1357 | 1372 | onclick={handleContinue} |
1358 | | - disabled={!hasRunSimulation || !pyodideReady || simRunning} |
| 1373 | + disabled={!hasRunSimulation || !runReady || simRunning} |
1359 | 1374 | use:tooltip={continueTooltip} |
1360 | 1375 | aria-label="Continue" |
1361 | 1376 | > |
|
0 commit comments