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
4 changes: 3 additions & 1 deletion src/tui/log.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ function LogScreen({
// biome-ignore lint/suspicious/noArrayIndexKey: the absolute file line is the stable record identity.
<Text key={`${position + index}`} wrap="truncate">
<Highlight
text={cells(presentLogLine(line, raw), columns, horizontal)}
text={
cells(presentLogLine(line, raw), columns, horizontal) || " "
}
query={query}
/>
</Text>
Expand Down
151 changes: 151 additions & 0 deletions tests/tui.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,157 @@ test("log follows, searches whole file, isolates input and escapes back to selec
}
});

test("horizontal log panning preserves record rows and live follow", async () => {
const directory = await mkdtemp(join(tmpdir(), "aw-tui-"));
const { source, sessions } = monitorFixture();
sessions[0]!.log = join(directory, "events.log");
const records = Array.from({ length: 26 }, (_, index) => `line-${index + 1}`);
records[4] = JSON.stringify({
type: "event",
metadata: "m".repeat(60),
text: "short",
});
records[5] = `${"a".repeat(60)}active-anchor`;
records[6] = `${"b".repeat(60)}active-next`;
await writeFile(sessions[0]!.log, `${records.join("\n")}\n`);
const view = render(<Monitor source={source} size={wide} />);
try {
await until(() => view.lastFrame()!.includes("invocation 1"));
view.stdin.write("l");
await until(() =>
view.lastFrame()!.includes("LIVE FOLLOW · Readable · lines 3–26/26"),
);
const initialRows = view.lastFrame()!.split("\n");
const anchorRow = initialRows.findIndex((row) =>
row.includes("active-anchor"),
);
const nextRow = initialRows.findIndex((row) => row.includes("active-next"));
assert.ok(anchorRow >= 0);
assert.ok(initialRows[anchorRow]!.startsWith("a".repeat(60)));
assert.equal(nextRow, anchorRow + 1);
assert.ok(initialRows[anchorRow - 1]!.startsWith("event: short"));

view.stdin.write("\u001b[C");
view.stdin.write("\u001b[C");
view.stdin.write("\u001b[C");
await settle();

const pannedRows = view.lastFrame()!.split("\n");
assert.equal(
pannedRows.findIndex((row) => row.includes("active-anchor")),
anchorRow,
);
assert.equal(
pannedRows.findIndex((row) => row.includes("active-next")),
nextRow,
);
assert.ok(pannedRows[anchorRow]!.startsWith("active-anchor"));
assert.equal(pannedRows[anchorRow - 1]!.trim(), "");
assert.match(view.lastFrame()!, /LIVE FOLLOW · Readable · lines 3–26\/26/);

view.stdin.write("R");
Comment thread
mingchuno marked this conversation as resolved.
await settle();
assert.match(view.lastFrame()!, /LIVE FOLLOW · Raw · lines 3–26\/26/);
const rawRows = view.lastFrame()!.split("\n");
assert.ok(rawRows[anchorRow - 1]!.startsWith("m".repeat(20)));
assert.ok(rawRows[anchorRow]!.startsWith("active-anchor"));
assert.equal(
rawRows.findIndex((row) => row.includes("active-anchor")),
anchorRow,
);
assert.equal(
rawRows.findIndex((row) => row.includes("active-next")),
nextRow,
);

await appendFile(sessions[0]!.log, `${"z".repeat(60)}live-after-pan\n`);
await until(() =>
view.lastFrame()!.includes("LIVE FOLLOW · Raw · lines 4–27/27"),
);
assert.match(view.lastFrame()!, /live-after-pan/);
const rowAfterAppend = view
.lastFrame()!
.split("\n")
.findIndex((row) => row.includes("active-anchor"));

view.stdin.write("\u001b[D");
view.stdin.write("\u001b[D");
view.stdin.write("\u001b[D");
await settle();
assert.equal(
view
.lastFrame()!
.split("\n")
.findIndex((row) => row.includes("active-anchor")),
rowAfterAppend,
);
assert.equal(
view
.lastFrame()!
.split("\n")
.findIndex((row) => row.includes("active-next")),
rowAfterAppend + 1,
);
assert.ok(
view.lastFrame()!.split("\n")[rowAfterAppend]!.startsWith("a".repeat(60)),
);
assert.match(view.lastFrame()!, /LIVE FOLLOW · Raw · lines 4–27\/27/);
} finally {
view.unmount();
await rm(directory, { recursive: true, force: true });
}
});

test("completed log panning preserves a manually selected line range", async () => {
const directory = await mkdtemp(join(tmpdir(), "aw-tui-"));
const { run, sessions, source } = monitorFixture();
run.outcome = "completed";
run.executions![0]!.outcome = "completed";
sessions[0]!.outcome = "completed";
sessions[0]!.log = join(directory, "completed.log");
const records = Array.from({ length: 30 }, (_, index) => `line-${index + 1}`);
records[3] = "short";
records[4] = `${"b".repeat(20)}completed-anchor`;
await writeFile(sessions[0]!.log, `${records.join("\n")}\n`);
const view = render(<Monitor source={source} size={wide} />);
try {
await until(() => view.lastFrame()!.includes("Implement feature"));
view.stdin.write("\r");
await until(() => view.lastFrame()!.includes("l log: implementation"));
view.stdin.write("l");
await until(() =>
view.lastFrame()!.includes("LIVE FOLLOW · Readable · lines 7–30/30"),
);
for (let index = 0; index < 5; index++) view.stdin.write("\u001b[A");
await until(() =>
view.lastFrame()!.includes("SCROLLING · Readable · lines 2–25/30"),
);
const anchorRow = view
.lastFrame()!
.split("\n")
.findIndex((row) => row.includes("completed-anchor"));
assert.ok(anchorRow >= 0);

view.stdin.write("\u001b[C");
await settle();

assert.equal(
view
.lastFrame()!
.split("\n")
.findIndex((row) => row.includes("completed-anchor")),
anchorRow,
);
assert.ok(
view.lastFrame()!.split("\n")[anchorRow]!.startsWith("completed-anchor"),
);
assert.match(view.lastFrame()!, /SCROLLING · Readable · lines 2–25\/30/);
} finally {
view.unmount();
await rm(directory, { recursive: true, force: true });
}
});

test("selection survives new rows and stale selected-run responses", async () => {
const { source, run, sessions } = monitorFixture();
let runs = [
Expand Down
Loading