From 34c3b038ef7298d8e53170850c259419ca4426e2 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Mon, 14 Sep 2026 18:32:43 -0700 Subject: [PATCH 1/3] fix(terminal): reset stopped runs to a pristine prompt --- scripts/e2e-terminal-stop.test.mjs | 43 +++++++++++++++++++++++++++--- src/ui/terminal.js | 16 +++++------ 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/scripts/e2e-terminal-stop.test.mjs b/scripts/e2e-terminal-stop.test.mjs index 8cfacc1..559ca71 100644 --- a/scripts/e2e-terminal-stop.test.mjs +++ b/scripts/e2e-terminal-stop.test.mjs @@ -149,20 +149,55 @@ test('e2e: prompt restoration follows newline-less program output on a new line' assert.match(ctx.writes.join(''), /program output\r\n.*browser\.cpp.*:~\$ /); }); -test('e2e: Ctrl+C while running stops the program once and restores the prompt', async () => { +test('e2e: Ctrl+C while running resets to one pristine prompt', async () => { const ctx = setupTerminalHarness(); assert.equal(await startRun(), true); onRunStart({ stdinMode: 'interactive' }); - __handleTerminalKeyForTesting('', ctrlCEvent()); + writeStdout('runaway output'); + const writesBeforeStop = ctx.writes.length; __handleTerminalKeyForTesting('', ctrlCEvent()); assert.equal(ctx.runCalls.length, 1); assert.deepEqual(ctx.stopCalls, ['stop']); assert.deepEqual(ctx.runStateChanges, [true, false]); assert.equal(__getTerminalStateForTesting().running, false); - assert.ok(ctx.writes.join('').includes('^C')); - assert.ok(ctx.writes.join('').includes('Process interrupted.')); + const resetWrites = ctx.writes.slice(writesBeforeStop).join(''); + assert.ok(resetWrites.includes('browser.cpp')); + assert.equal(resetWrites.match(/browser\.cpp/g)?.length, 1); + assert.ok(!resetWrites.includes('^C')); + assert.ok(!resetWrites.includes('Process interrupted.')); + assert.equal(ctx.clearCalls.length, 1); +}); + +test('e2e: STOP discards stdout queued after a stopped run', async () => { + const ctx = setupTerminalHarness(); + + showInitialPrompt(); + assert.equal(await startRun(), true); + onRunStart({ stdinMode: 'interactive' }); + writeStdout('runaway output'); + assert.equal(stopRun(), true); + const writesAfterStop = ctx.writes.length; + + writeStdout('late output'); + + assert.equal(ctx.writes.length, writesAfterStop); + assert.ok(!ctx.writes.join('').includes('Process interrupted.')); +}); + +test('e2e: STOP ignores a late nonzero run result', async () => { + const ctx = setupTerminalHarness(); + + showInitialPrompt(); + assert.equal(await startRun(), true); + onRunStart({ stdinMode: 'interactive' }); + assert.equal(stopRun(), true); + const writesAfterStop = ctx.writes.length; + + onRunResult({ exitCode: 1 }); + + assert.equal(ctx.writes.length, writesAfterStop); }); test('e2e: stopRun is idempotent for repeated button presses during one run', async () => { diff --git a/src/ui/terminal.js b/src/ui/terminal.js index d8427be..2034609 100644 --- a/src/ui/terminal.js +++ b/src/ui/terminal.js @@ -466,17 +466,11 @@ export function onRunStart({ stdinMode = 'none', stdinSessionId = null } = {}) { * CPU-bound WASM cannot observe stdin EOF, so the main thread terminates the * worker via _onStopRun after terminal state has been reset. * - * @param {{ echoCtrlC?: boolean }} [options] * @returns {boolean} true when a running program was stopped */ -export function stopRun({ echoCtrlC = false } = {}) { +export function stopRun() { if (!running) return false; - if (echoCtrlC) { - term?.write('^C' + CRLF); - } else { - term?.write(CRLF); - } inputBuffer = ''; _clearSAB(); setRunPreparationState(false); @@ -486,7 +480,7 @@ export function stopRun({ echoCtrlC = false } = {}) { busy = false; runDone?.(); runDone = null; - term?.write(`${C.yellow}Process interrupted.${C.reset}${CRLF}`); + clearScreen(); writePrompt(); _onStopRun?.(); return true; @@ -496,6 +490,7 @@ export function stopRun({ echoCtrlC = false } = {}) { /** Write stdout text from the running program. */ export function writeStdout(text) { + if (!running) return; term?.write(text.replace(/\n/g, CRLF)); } @@ -531,6 +526,7 @@ export function onCompileResult({ success, diagnostics, outputPath }) { * @param {{ exitCode:number }} result */ export function onRunResult({ exitCode }) { + if (!running && !preparingRun) return; const shouldRestorePrompt = running || preparingRun; if (exitCode !== 0) { term?.write(`${CRLF}${C.yellow}Process exited with code ${exitCode}.${C.reset}${CRLF}`); @@ -609,7 +605,7 @@ function handleKey({ key, domEvent }) { if (activeStdinMode === 'interactive' || activeStdinMode === 'interactive-message') { handleStdinKey(key, domEvent); } else if (domEvent.ctrlKey && domEvent.key === 'c') { - stopRun({ echoCtrlC: true }); + stopRun(); } return; } @@ -740,7 +736,7 @@ function handleStdinKey(key, domEvent) { // Ctrl+C – interrupt the running program if (domEvent.ctrlKey && code === 'c') { - stopRun({ echoCtrlC: true }); + stopRun(); return; } From 45bc869245267016d753596c539a200fffb59616 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Mon, 14 Sep 2026 18:33:31 -0700 Subject: [PATCH 2/3] fix(app): terminate compiler worker during unload --- src/ui/app.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui/app.js b/src/ui/app.js index 3798326..d911f6a 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -139,8 +139,12 @@ window.addEventListener('DOMContentLoaded', async () => { if (terminalPanel) resizeObserver.observe(terminalPanel); initPanelResizers(); - // 9. Persist session on unload - window.addEventListener('beforeunload', () => persistenceGate.persist()); + // 9. Persist session on unload. Worker teardown is synchronous: browser + // unload handlers cannot safely wait for terminal or worker cleanup. + window.addEventListener('beforeunload', () => { + worker.terminate(); + persistenceGate.persist(); + }); editorAPI.focus(); }); From a43ab3011885f32a3a9229e6fb20016b8f768189 Mon Sep 17 00:00:00 2001 From: Kevin Buffardi Date: Mon, 14 Sep 2026 18:34:03 -0700 Subject: [PATCH 3/3] chore(release): bump version to 0.4.9 --- manifest.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/manifest.json b/manifest.json index f50295f..234bc1e 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "browser.cpp", "short_name": "browser.cpp", "description": "In-browser C++20 IDE powered by Monaco Editor and WASM Clang", - "version": "0.4.8", + "version": "0.4.9", "minimum_chrome_version": "105", "icons": { "16": "icons/icon16.png", diff --git a/package-lock.json b/package-lock.json index 8033747..b6cfe0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "browser.cpp", - "version": "0.4.8", + "version": "0.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "browser.cpp", - "version": "0.4.8", + "version": "0.4.9", "dependencies": { "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", diff --git a/package.json b/package.json index b8f98fd..07f66ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browser.cpp", - "version": "0.4.8", + "version": "0.4.9", "description": "In-browser C++20 IDE with WASM Clang toolchain", "private": true, "scripts": {