feat: add streaming markdown rendering with incremental output
This commit is contained in:
parent
27c6447ef2
commit
856a3160fe
4 changed files with 45 additions and 25 deletions
2
index.ts
2
index.ts
|
|
@ -91,8 +91,6 @@ async function main() {
|
||||||
convo.messages.push({ role: "assistant", content: await text });
|
convo.messages.push({ role: "assistant", content: await text });
|
||||||
await convoFile.write(JSON.stringify(convo));
|
await convoFile.write(JSON.stringify(convo));
|
||||||
|
|
||||||
await renderer.renderFinal(text);
|
|
||||||
|
|
||||||
await renderer.renderStats(totalUsage, start, stop);
|
await renderer.renderStats(totalUsage, start, stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ export default class Renderer {
|
||||||
private lineCount = 0;
|
private lineCount = 0;
|
||||||
private charsSinceNL = 0;
|
private charsSinceNL = 0;
|
||||||
private memOk = true;
|
private memOk = true;
|
||||||
|
private buffer = "";
|
||||||
|
private unsafeIndex = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private isPlain = false,
|
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>>) {
|
async renderStream(stream: AsyncIterableStream<TextStreamPart<ToolSet>>) {
|
||||||
this.resetCounts();
|
this.resetCounts();
|
||||||
|
|
||||||
|
|
@ -113,16 +142,19 @@ export default class Renderer {
|
||||||
}
|
}
|
||||||
case "text-delta": {
|
case "text-delta": {
|
||||||
this.write(part.text);
|
this.write(part.text);
|
||||||
if (!this.isPlain) this.checkContainment(part.text);
|
if (!this.isPlain) {
|
||||||
|
this.checkContainment(part.text);
|
||||||
|
await this.bufferThenRender(part.text);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text-end": {
|
case "text-end": {
|
||||||
if (!this.isPlain) {
|
if (this.buffer) {
|
||||||
this.moveCursor(-this.lineCount - 1);
|
await this.render(this.buffer);
|
||||||
this.clearScreenDown(process.stderr);
|
this.buffer = "";
|
||||||
this.resetCounts();
|
this.unsafeIndex = 0;
|
||||||
} else {
|
} else {
|
||||||
this.write("\n\n");
|
this.write("\n");
|
||||||
}
|
}
|
||||||
break;
|
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`;
|
const template = `Context: ${context.toFixed(1)}k, Output: ${output.toFixed(1)}k, Time: ${time.toFixed(2)}s\n`;
|
||||||
process.stderr.write(chalkStderr.dim(template));
|
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, `));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ import kanagawaLotus from "@shikijs/themes/kanagawa-lotus";
|
||||||
import kanagawaWave from "@shikijs/themes/kanagawa-wave";
|
import kanagawaWave from "@shikijs/themes/kanagawa-wave";
|
||||||
|
|
||||||
export async function renderMarkdown(
|
export async function renderMarkdown(
|
||||||
text: PromiseLike<string>,
|
text: PromiseLike<string> | string,
|
||||||
columns: number,
|
columns: number,
|
||||||
) {
|
) {
|
||||||
return render(await text, {
|
return render(await text, {
|
||||||
|
|
|
||||||
6
testRender.ts
Normal file
6
testRender.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { renderMarkdown } from "./src/render/markdown.ts";
|
||||||
|
|
||||||
|
const text = await Bun.file("output.md").text();
|
||||||
|
|
||||||
|
const [columns] = process.stdout.getWindowSize();
|
||||||
|
process.stdout.write(await renderMarkdown(text, columns));
|
||||||
Loading…
Add table
Add a link
Reference in a new issue