refactor(app): extract terminal runtime out of react lifecycle

This commit is contained in:
Mohamed Boudra
2026-02-15 15:40:18 +07:00
parent 9244f1fbd6
commit adb511d2be
10 changed files with 1858 additions and 629 deletions
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import {
appendTerminalOutputBuffer,
createTerminalOutputBuffer,
readTerminalOutputBuffer,
} from "./terminal-output-buffer";
describe("terminal-output-buffer", () => {
it("keeps appended text within max chars without rebuilding from scratch", () => {
const buffer = createTerminalOutputBuffer();
appendTerminalOutputBuffer({ buffer, text: "abc", maxChars: 5 });
appendTerminalOutputBuffer({ buffer, text: "de", maxChars: 5 });
expect(readTerminalOutputBuffer({ buffer })).toBe("abcde");
appendTerminalOutputBuffer({ buffer, text: "f", maxChars: 5 });
expect(readTerminalOutputBuffer({ buffer })).toBe("bcdef");
appendTerminalOutputBuffer({ buffer, text: "gh", maxChars: 5 });
expect(readTerminalOutputBuffer({ buffer })).toBe("defgh");
});
it("ignores empty appends and preserves current content", () => {
const buffer = createTerminalOutputBuffer();
appendTerminalOutputBuffer({ buffer, text: "hello", maxChars: 10 });
appendTerminalOutputBuffer({ buffer, text: "", maxChars: 10 });
expect(readTerminalOutputBuffer({ buffer })).toBe("hello");
});
it("handles large overflow by trimming entire leading segments", () => {
const buffer = createTerminalOutputBuffer();
appendTerminalOutputBuffer({ buffer, text: "12345", maxChars: 8 });
appendTerminalOutputBuffer({ buffer, text: "6789", maxChars: 8 });
appendTerminalOutputBuffer({ buffer, text: "ABCDEF", maxChars: 8 });
expect(readTerminalOutputBuffer({ buffer })).toBe("89ABCDEF");
});
});
@@ -0,0 +1,95 @@
export interface TerminalOutputBuffer {
segments: string[];
startIndex: number;
totalChars: number;
}
const COMPACT_SEGMENT_THRESHOLD = 256;
export function createTerminalOutputBuffer(): TerminalOutputBuffer {
return {
segments: [],
startIndex: 0,
totalChars: 0,
};
}
function normalizeMaxChars(input: { maxChars: number }): number {
if (!Number.isFinite(input.maxChars)) {
return 0;
}
return Math.max(0, Math.floor(input.maxChars));
}
function compactTerminalOutputBuffer(input: { buffer: TerminalOutputBuffer }): void {
const { buffer } = input;
if (buffer.startIndex <= COMPACT_SEGMENT_THRESHOLD) {
return;
}
buffer.segments = buffer.segments.slice(buffer.startIndex);
buffer.startIndex = 0;
}
function trimTerminalOutputBufferToMax(input: {
buffer: TerminalOutputBuffer;
maxChars: number;
}): void {
const { buffer } = input;
const maxChars = normalizeMaxChars({ maxChars: input.maxChars });
while (buffer.totalChars > maxChars) {
const leadingSegment = buffer.segments[buffer.startIndex];
if (!leadingSegment) {
buffer.segments = [];
buffer.startIndex = 0;
buffer.totalChars = 0;
return;
}
const overflowChars = buffer.totalChars - maxChars;
if (leadingSegment.length <= overflowChars) {
buffer.startIndex += 1;
buffer.totalChars -= leadingSegment.length;
continue;
}
buffer.segments[buffer.startIndex] = leadingSegment.slice(overflowChars);
buffer.totalChars -= overflowChars;
break;
}
compactTerminalOutputBuffer({ buffer });
}
export function appendTerminalOutputBuffer(input: {
buffer: TerminalOutputBuffer;
text: string;
maxChars: number;
}): void {
if (!input.text) {
return;
}
input.buffer.segments.push(input.text);
input.buffer.totalChars += input.text.length;
trimTerminalOutputBufferToMax({
buffer: input.buffer,
maxChars: input.maxChars,
});
}
export function readTerminalOutputBuffer(input: {
buffer: TerminalOutputBuffer;
}): string {
const { buffer } = input;
if (buffer.totalChars <= 0) {
return "";
}
if (buffer.startIndex === 0) {
return buffer.segments.join("");
}
return buffer.segments.slice(buffer.startIndex).join("");
}