diff --git a/wasm/dist-repl/repl.css b/wasm/dist-repl/repl.css index 244bf2f..8935273 100644 --- a/wasm/dist-repl/repl.css +++ b/wasm/dist-repl/repl.css @@ -299,6 +299,9 @@ body.repl { opacity: 0.6; } .transcript .entry.error .tier-result { color: var(--err); } +/* Error line appended after partial streamed output — keeps the + * lines that already showed and tints just the trailing error. */ +.transcript .tier-result .err-line { color: var(--err); } .transcript .entry + .entry { margin-top: 0.4rem; } /* ─── Prompt bar ───────────────────────────────────────────────── */ diff --git a/wasm/dist-repl/repl.js b/wasm/dist-repl/repl.js index 0cf89d4..b7214d2 100644 --- a/wasm/dist-repl/repl.js +++ b/wasm/dist-repl/repl.js @@ -256,19 +256,40 @@ function attachStreaming(resultSpan, metaSpan, tier) { .map((d) => d.textContent === " " ? "" : d.textContent) .join("\n"); const expected = text.replace(/\n$/, ""); - // If the streamed text doesn't match the full eval output - // (e.g. an error fired without intermediate chunks), nuke - // and rebuild from the full text. Otherwise leave the - // streamed divs as-is — they already match. - if (streamedText !== expected || errText) { - resultSpan.textContent = ""; - const combined = (text + (errText && text ? "\n" : "") + errText).replace(/\n$/, ""); - const lines = combined.split("\n"); - for (const ln of lines) { - const d = makeLine(); - d.textContent = ln || " "; - resultSpan.appendChild(d); + // Preserving partial output on cancel/error: a long-running + // (display ...) loop that fox cancels half-way through, or + // an asm tier that hits "index out of bounds" 5000 lines + // into a sieve, used to lose every line that already + // streamed. Now: + // * success, streamed matches full output → keep DOM. + // * success, no streaming happened → rebuild from + // the final text. + // * error with prior streaming → KEEP the + // streamed divs, append a final divider + error line. + // * error with no streaming → show error. + if (!errText) { + if (streamedText !== expected) { + resultSpan.textContent = ""; + const lines = expected.split("\n"); + for (const ln of lines) { + const d = makeLine(); + d.textContent = ln || " "; + resultSpan.appendChild(d); + } } + } else if (streamedText) { + // Append the error AFTER what's already on screen so + // the user keeps every line they were watching scroll. + const d = makeLine(); + d.textContent = errText; + d.classList.add("err-line"); + resultSpan.appendChild(d); + } else { + resultSpan.textContent = ""; + const d = makeLine(); + d.textContent = errText; + d.classList.add("err-line"); + resultSpan.appendChild(d); } metaSpan.textContent = `${TIER_LABEL[tier] || tier} · ${(r.elapsed | 0)}ms`; }, diff --git a/wasm/repl/repl.css b/wasm/repl/repl.css index 244bf2f..8935273 100644 --- a/wasm/repl/repl.css +++ b/wasm/repl/repl.css @@ -299,6 +299,9 @@ body.repl { opacity: 0.6; } .transcript .entry.error .tier-result { color: var(--err); } +/* Error line appended after partial streamed output — keeps the + * lines that already showed and tints just the trailing error. */ +.transcript .tier-result .err-line { color: var(--err); } .transcript .entry + .entry { margin-top: 0.4rem; } /* ─── Prompt bar ───────────────────────────────────────────────── */ diff --git a/wasm/repl/repl.js b/wasm/repl/repl.js index 0cf89d4..b7214d2 100644 --- a/wasm/repl/repl.js +++ b/wasm/repl/repl.js @@ -256,19 +256,40 @@ function attachStreaming(resultSpan, metaSpan, tier) { .map((d) => d.textContent === " " ? "" : d.textContent) .join("\n"); const expected = text.replace(/\n$/, ""); - // If the streamed text doesn't match the full eval output - // (e.g. an error fired without intermediate chunks), nuke - // and rebuild from the full text. Otherwise leave the - // streamed divs as-is — they already match. - if (streamedText !== expected || errText) { - resultSpan.textContent = ""; - const combined = (text + (errText && text ? "\n" : "") + errText).replace(/\n$/, ""); - const lines = combined.split("\n"); - for (const ln of lines) { - const d = makeLine(); - d.textContent = ln || " "; - resultSpan.appendChild(d); + // Preserving partial output on cancel/error: a long-running + // (display ...) loop that fox cancels half-way through, or + // an asm tier that hits "index out of bounds" 5000 lines + // into a sieve, used to lose every line that already + // streamed. Now: + // * success, streamed matches full output → keep DOM. + // * success, no streaming happened → rebuild from + // the final text. + // * error with prior streaming → KEEP the + // streamed divs, append a final divider + error line. + // * error with no streaming → show error. + if (!errText) { + if (streamedText !== expected) { + resultSpan.textContent = ""; + const lines = expected.split("\n"); + for (const ln of lines) { + const d = makeLine(); + d.textContent = ln || " "; + resultSpan.appendChild(d); + } } + } else if (streamedText) { + // Append the error AFTER what's already on screen so + // the user keeps every line they were watching scroll. + const d = makeLine(); + d.textContent = errText; + d.classList.add("err-line"); + resultSpan.appendChild(d); + } else { + resultSpan.textContent = ""; + const d = makeLine(); + d.textContent = errText; + d.classList.add("err-line"); + resultSpan.appendChild(d); } metaSpan.textContent = `${TIER_LABEL[tier] || tier} · ${(r.elapsed | 0)}ms`; }, diff --git a/www/repl/repl.css b/www/repl/repl.css index 244bf2f..8935273 100644 --- a/www/repl/repl.css +++ b/www/repl/repl.css @@ -299,6 +299,9 @@ body.repl { opacity: 0.6; } .transcript .entry.error .tier-result { color: var(--err); } +/* Error line appended after partial streamed output — keeps the + * lines that already showed and tints just the trailing error. */ +.transcript .tier-result .err-line { color: var(--err); } .transcript .entry + .entry { margin-top: 0.4rem; } /* ─── Prompt bar ───────────────────────────────────────────────── */ diff --git a/www/repl/repl.js b/www/repl/repl.js index 0cf89d4..b7214d2 100644 --- a/www/repl/repl.js +++ b/www/repl/repl.js @@ -256,19 +256,40 @@ function attachStreaming(resultSpan, metaSpan, tier) { .map((d) => d.textContent === " " ? "" : d.textContent) .join("\n"); const expected = text.replace(/\n$/, ""); - // If the streamed text doesn't match the full eval output - // (e.g. an error fired without intermediate chunks), nuke - // and rebuild from the full text. Otherwise leave the - // streamed divs as-is — they already match. - if (streamedText !== expected || errText) { - resultSpan.textContent = ""; - const combined = (text + (errText && text ? "\n" : "") + errText).replace(/\n$/, ""); - const lines = combined.split("\n"); - for (const ln of lines) { - const d = makeLine(); - d.textContent = ln || " "; - resultSpan.appendChild(d); + // Preserving partial output on cancel/error: a long-running + // (display ...) loop that fox cancels half-way through, or + // an asm tier that hits "index out of bounds" 5000 lines + // into a sieve, used to lose every line that already + // streamed. Now: + // * success, streamed matches full output → keep DOM. + // * success, no streaming happened → rebuild from + // the final text. + // * error with prior streaming → KEEP the + // streamed divs, append a final divider + error line. + // * error with no streaming → show error. + if (!errText) { + if (streamedText !== expected) { + resultSpan.textContent = ""; + const lines = expected.split("\n"); + for (const ln of lines) { + const d = makeLine(); + d.textContent = ln || " "; + resultSpan.appendChild(d); + } } + } else if (streamedText) { + // Append the error AFTER what's already on screen so + // the user keeps every line they were watching scroll. + const d = makeLine(); + d.textContent = errText; + d.classList.add("err-line"); + resultSpan.appendChild(d); + } else { + resultSpan.textContent = ""; + const d = makeLine(); + d.textContent = errText; + d.classList.add("err-line"); + resultSpan.appendChild(d); } metaSpan.textContent = `${TIER_LABEL[tier] || tier} · ${(r.elapsed | 0)}ms`; },