repl: preserve streamed output on cancel + mid-stream tier errors
attachStreaming.finalize() used to nuke every streamed line and
rebuild from the eval's final text whenever the streamed buffer
didn't match — which always happened on cancel (no final text)
and on tier errors fired mid-stream (asm 'index out of bounds'
5000 lines into a sieve, say). The user watched output scroll for
30 seconds, hit cancel, and saw it all disappear replaced by
'error: cancelled'.
New finalize logic:
* success + streamed matches full output → keep DOM
* success + no streaming happened → rebuild from text
* error + prior streaming → KEEP streamed divs,
append the error as a single trailing line
* error + no streaming → show error only
Trailing error line gets an .err-line class so just the error is
tinted with --err, the preserved output stays the original colour.
This commit is contained in:
parent
2b46c1b79e
commit
84bd7cac8c
6 changed files with 108 additions and 36 deletions
|
|
@ -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 ───────────────────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue