fix(editor): show spinner + block re-clicks during circuit verification

Clicking Run runs a pre-flight circuit-verification SPICE solve before
compiling. On a cold ngspice worker that solve takes a second or two, but the
Run button kept showing the play icon and stayed enabled, so it looked dead
and got clicked repeatedly -- each click stacking another verification (the
reported 6x [handleRun] click).

- Add a `verifying` state: the Run button now shows the same spinner as the
  Compile button and is disabled while the pre-flight solve runs.
- Synchronous re-entrancy guard (runInFlightRef) ignores re-clicks while a
  verification is already in flight.
This commit is contained in:
David Montero 2026-06-17 22:06:15 +02:00
parent 9ec48d5021
commit b7d1e6469d
1 changed files with 53 additions and 15 deletions

View File

@ -235,6 +235,13 @@ export const EditorToolbar = ({
[currentProject],
);
const [compiling, setCompiling] = useState(false);
// True while the pre-flight circuit verification SPICE solve is running.
// Drives the Run-button spinner so the user gets feedback during the
// (sometimes multi-second, cold-worker) solve instead of a dead button.
const [verifying, setVerifying] = useState(false);
// Synchronous re-entrancy guard: a click while a run/verify is already in
// flight is ignored, so rapid clicks can't stack multiple verifications.
const runInFlightRef = useRef(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [libManagerOpen, setLibManagerOpen] = useState(false);
const [pendingLibraries, setPendingLibraries] = useState<string[]>([]);
@ -738,7 +745,20 @@ export const EditorToolbar = ({
// overpower. If anything trips we hand control to the modal, which
// resumes by calling `handleRun(true)` for "Run anyway".
if (!skipVerify) {
const ok = await checkOrBlock(() => handleRun(true));
// The verification solve can take a second or two (cold ngspice worker).
// Show the Run-button spinner and ignore re-clicks while it runs — the
// button otherwise looks idle and gets clicked repeatedly, stacking
// multiple verifications.
if (runInFlightRef.current) return;
runInFlightRef.current = true;
setVerifying(true);
let ok = false;
try {
ok = await checkOrBlock(() => handleRun(true));
} finally {
setVerifying(false);
runInFlightRef.current = false;
}
if (!ok) return;
}
@ -1443,25 +1463,43 @@ export const EditorToolbar = ({
onClick={() => handleRun()}
disabled={
isBoardless
? digitalRunning
: running || compiling || !activeBoard
? digitalRunning || verifying
: running || compiling || verifying || !activeBoard
}
className="tb-btn tb-btn-run"
title={
isBoardless
? digitalRunning
? 'Digital simulation running'
: 'Resume digital simulation'
: !activeBoard
? t('editor.toolbar.run.addBoard')
: activeBoard?.languageMode === 'micropython'
? t('editor.toolbar.run.runMicropython')
: t('editor.toolbar.run.run')
verifying
? t('editor.toolbar.run.verifying', 'Checking circuit...')
: isBoardless
? digitalRunning
? 'Digital simulation running'
: 'Resume digital simulation'
: !activeBoard
? t('editor.toolbar.run.addBoard')
: activeBoard?.languageMode === 'micropython'
? t('editor.toolbar.run.runMicropython')
: t('editor.toolbar.run.run')
}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<polygon points="5,3 19,12 5,21" />
</svg>
{verifying || compiling ? (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="spin"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<polygon points="5,3 19,12 5,21" />
</svg>
)}
</button>
{/* Stop */}