feat: add streaming markdown rendering with incremental output

This commit is contained in:
mitchell 2026-04-18 05:04:00 -04:00
parent 27c6447ef2
commit 856a3160fe
4 changed files with 45 additions and 25 deletions

View file

@ -18,6 +18,8 @@ export default class Renderer {
private lineCount = 0;
private charsSinceNL = 0;
private memOk = true;
private buffer = "";
private unsafeIndex = 0;
constructor(
private isPlain = false,
@ -66,6 +68,33 @@ export default class Renderer {
}
}
private async bufferThenRender(text: string, pipe = process.stdout) {
this.buffer += text;
const idx = this.buffer.lastIndexOf("\n#");
const blockDelims = this.buffer.match(/```/g)?.length;
if (blockDelims && blockDelims % 2 !== 0) this.unsafeIndex = idx;
if (idx > this.unsafeIndex) {
const str = this.buffer.substring(0, idx) ?? "";
await this.render(str, pipe);
this.buffer = this.buffer.substring(idx);
this.unsafeIndex = 0;
}
}
private async render(str: string, pipe = process.stdout) {
this.moveCursor(-this.lineCount, pipe);
this.clearScreenDown(pipe);
this.resetCounts();
const [columns] = pipe.getWindowSize();
const rendered = await renderMarkdown(str, columns);
pipe.write(rendered);
pipe.write("\n");
}
async renderStream(stream: AsyncIterableStream<TextStreamPart<ToolSet>>) {
this.resetCounts();
@ -113,16 +142,19 @@ export default class Renderer {
}
case "text-delta": {
this.write(part.text);
if (!this.isPlain) this.checkContainment(part.text);
if (!this.isPlain) {
this.checkContainment(part.text);
await this.bufferThenRender(part.text);
}
break;
}
case "text-end": {
if (!this.isPlain) {
this.moveCursor(-this.lineCount - 1);
this.clearScreenDown(process.stderr);
this.resetCounts();
if (this.buffer) {
await this.render(this.buffer);
this.buffer = "";
this.unsafeIndex = 0;
} else {
this.write("\n\n");
this.write("\n");
}
break;
}
@ -144,20 +176,4 @@ export default class Renderer {
const template = `Context: ${context.toFixed(1)}k, Output: ${output.toFixed(1)}k, Time: ${time.toFixed(2)}s\n`;
process.stderr.write(chalkStderr.dim(template));
}
async renderFinal(text: PromiseLike<string>) {
if (this.isPlain) return;
const [columns] = process.stdout.getWindowSize();
const start = Date.now();
const rendered = await renderMarkdown(text, columns);
const stop = Date.now();
this.write("\n");
this.write(rendered);
this.write("\n");
process.stderr.write(chalkStderr.dim(`Markdown: ${stop - start}ms, `));
}
}