Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/splash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
125 changes: 125 additions & 0 deletions src/renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
buildFrame,
buildFrameCells,
buildContentCells,
generateSideMeteorShower,
} from "./renderer.js";
import { rowToString } from "./renderer-diff.js";
import type {
Expand Down Expand Up @@ -973,6 +974,130 @@ describe("Renderer ctrl+c", () => {
});
});

describe("Renderer meteors", () => {
function renderContentSideText(
meteorFrequency: number,
terminalHeight = 46,
): string {
vi.useFakeTimers();
vi.setSystemTime(0);
const state: OrchestratorState = {
status: "running",
gracefulStopRequested: false,
interruptHint: "resume",
currentIteration: 1,
totalInputTokens: 0,
totalOutputTokens: 0,
tokensEstimated: false,
commitCount: 0,
iterations: [],
successCount: 0,
failCount: 0,
consecutiveFailures: 0,
consecutiveErrors: 0,
startTime: new Date(0),
waitingUntil: null,
lastMessage: null,
};
const orchestrator = Object.assign(new EventEmitter(), {
getState: vi.fn(() => state),
stop: vi.fn(),
}) as unknown as Orchestrator;
const stdoutWrite = vi
.spyOn(process.stdout, "write")
.mockImplementation(() => true);
const random = vi.spyOn(Math, "random").mockReturnValue(0);
const originalStdinTty = Object.getOwnPropertyDescriptor(
process.stdin,
"isTTY",
);
const originalColumns = Object.getOwnPropertyDescriptor(
process.stdout,
"columns",
);
const originalRows = Object.getOwnPropertyDescriptor(
process.stdout,
"rows",
);
Object.defineProperty(process.stdin, "isTTY", {
configurable: true,
value: false,
});
Object.defineProperty(process.stdout, "columns", {
configurable: true,
value: 121,
});
Object.defineProperty(process.stdout, "rows", {
configurable: true,
value: terminalHeight,
});

try {
const renderer = new Renderer(
orchestrator,
"ship it",
"claude",
vi.fn(),
{
meteorFrequency,
},
);
renderer.start();
renderer.stop();

const output = stdoutWrite.mock.calls
.map((args: unknown[]) => String(args[0]))
.join("");
const frame = output.startsWith("\x1b[H") ? output.slice(3) : output;
const lines = frame.split("\n").map(stripAnsi);
const sideWidth = Math.floor((121 - 63) / 2);
const availableHeight = terminalHeight - 2;
const contentRows = buildContentCells(
"ship it",
"claude",
state,
"0s",
0,
availableHeight,
);
while (contentRows.length < Math.min(24, availableHeight)) {
contentRows.push([]);
}
const topHeight = Math.ceil((availableHeight - contentRows.length) / 2);
const contentSideText = lines
.slice(topHeight, topHeight + contentRows.length)
.map((line) => `${line.slice(0, sideWidth)}${line.slice(-sideWidth)}`)
.join("\n");

return contentSideText;
} finally {
if (originalRows)
Object.defineProperty(process.stdout, "rows", originalRows);
if (originalColumns)
Object.defineProperty(process.stdout, "columns", originalColumns);
if (originalStdinTty)
Object.defineProperty(process.stdin, "isTTY", originalStdinTty);
random.mockRestore();
stdoutWrite.mockRestore();
vi.useRealTimers();
}
}

it("renders meteors beside the main content area", () => {
expect(renderContentSideText(5)).toContain("╱");
});

it("keeps low-frequency side meteors visible on tall terminals", () => {
expect(renderContentSideText(1, 100)).toContain("╱");
});

it("honors the lowest side meteor frequency count", () => {
const meteors = generateSideMeteorShower(121, 29, 44, 1, 102);

expect(meteors).toHaveLength(1);
});
});

describe("Renderer terminal title", () => {
const escape = String.fromCharCode(27);
const bell = String.fromCharCode(7);
Expand Down
33 changes: 33 additions & 0 deletions src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,29 @@ function meteorsStartingBefore(
return meteors.filter((meteor) => rowOffset + meteor.y < maxStartRow);
}

export function generateSideMeteorShower(
terminalWidth: number,
sideWidth: number,
height: number,
count: number,
seed: number,
): Meteor[] {
if (sideWidth <= 0 || height <= 0 || count <= 0) return [];

const leftCount = Math.max(1, Math.ceil(count / 2));
const rightCount = count - leftCount;
const leftMeteors = generateMeteorShower(sideWidth, height, leftCount, seed);
const rightXOffset = terminalWidth - sideWidth;
const rightMeteors = generateMeteorShower(
sideWidth,
height,
rightCount,
seed + 1,
).map((meteor) => ({ ...meteor, x: meteor.x + rightXOffset }));

return [...leftMeteors, ...rightMeteors];
}

function placeStarsInCells(
cells: Cell[],
stars: Star[],
Expand Down Expand Up @@ -709,6 +732,7 @@ export class Renderer {
private sideStars: Star[] = [];
private topMeteors: Meteor[] = [];
private bottomMeteors: Meteor[] = [];
private sideMeteors: Meteor[] = [];
private cachedWidth = 0;
private cachedHeight = 0;
private meteorFrequency: number;
Expand Down Expand Up @@ -831,6 +855,14 @@ export class Renderer {
STAR_DENSITY,
this.seedSide,
);
const sideWidth = Math.max(0, Math.floor((w - CONTENT_WIDTH) / 2));
this.sideMeteors = generateSideMeteorShower(
w,
sideWidth,
Math.min(BASE_CONTENT_ROWS, availableHeight),
meteorCountForFrequency(this.meteorFrequency),
this.seedSide + METEOR_SEED_OFFSET,
);
this.topMeteors = generateMeteorShower(
w,
topHeight,
Expand Down Expand Up @@ -868,6 +900,7 @@ export class Renderer {
h,
this.topMeteors,
this.bottomMeteors,
this.sideMeteors,
);

if (this.isFirstFrame || resized) {
Expand Down
Loading