feat(coding-agent/debug): Added interactive debug log viewer with selection controls
- Replaced the log preview with an interactive viewer supporting navigation, selection, expansion, and clipboard copy. - Added log formatting helpers to wrap multi-line entries and parse timestamps for session boundary detection. - Covered viewer selection, copy payload sanitization, expanded formatting, and timestamp parsing with new tests.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { formatDebugLogLine } from "../../src/debug/log-formatting";
|
||||
import {
|
||||
formatDebugLogExpandedLines,
|
||||
formatDebugLogLine,
|
||||
parseDebugLogTimestampMs,
|
||||
} from "../../src/debug/log-formatting";
|
||||
|
||||
describe("formatDebugLogLine", () => {
|
||||
it("strips ANSI codes and carriage returns", () => {
|
||||
@@ -26,4 +30,24 @@ describe("formatDebugLogLine", () => {
|
||||
expect(Bun.stringWidth(result)).toBeLessThanOrEqual(10);
|
||||
expect(result.startsWith("012345")).toBe(true);
|
||||
});
|
||||
|
||||
it("wraps expanded log lines without dropping content", () => {
|
||||
const input = "0123456789ABCDEFGHIJ";
|
||||
const lines = formatDebugLogExpandedLines(input, 6);
|
||||
expect(lines.length).toBeGreaterThan(1);
|
||||
for (const line of lines) {
|
||||
expect(Bun.stringWidth(line)).toBeLessThanOrEqual(6);
|
||||
}
|
||||
});
|
||||
|
||||
it("parses timestamp from JSON log lines", () => {
|
||||
const input = '{"timestamp":"2026-02-14T12:34:56.000Z","level":"info","message":"ok"}';
|
||||
expect(parseDebugLogTimestampMs(input)).toBe(Date.parse("2026-02-14T12:34:56.000Z"));
|
||||
});
|
||||
|
||||
it("returns undefined when timestamp is missing or invalid", () => {
|
||||
expect(parseDebugLogTimestampMs('{"message":"ok"}')).toBeUndefined();
|
||||
expect(parseDebugLogTimestampMs('{"timestamp":"not-a-date"}')).toBeUndefined();
|
||||
expect(parseDebugLogTimestampMs("not-json")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { buildLogCopyPayload, DebugLogViewerModel, SESSION_BOUNDARY_WARNING } from "../../src/debug/log-viewer";
|
||||
|
||||
describe("DebugLogViewerModel", () => {
|
||||
it("inserts session boundary warning between older and current-session logs", () => {
|
||||
const processStartMs = Date.parse("2026-02-14T12:00:00.000Z");
|
||||
const logs = [
|
||||
'{"timestamp":"2026-02-14T11:59:00.000Z","level":"info","message":"old"}',
|
||||
'{"timestamp":"2026-02-14T11:59:30.000Z","level":"info","message":"old-2"}',
|
||||
'{"timestamp":"2026-02-14T12:00:05.000Z","level":"info","message":"current"}',
|
||||
].join("\n");
|
||||
|
||||
const model = new DebugLogViewerModel(logs, processStartMs);
|
||||
const rowKinds = model.rows.map(row =>
|
||||
row.kind === "warning" ? SESSION_BOUNDARY_WARNING : `log:${row.logIndex}`,
|
||||
);
|
||||
|
||||
expect(rowKinds).toEqual(["log:0", "log:1", SESSION_BOUNDARY_WARNING, "log:2"]);
|
||||
});
|
||||
|
||||
it("supports shift-range selection and reset on plain movement", () => {
|
||||
const logs = ["a", "b", "c", "d"].join("\n");
|
||||
const model = new DebugLogViewerModel(logs, Date.now());
|
||||
|
||||
model.moveCursor(1, true);
|
||||
model.moveCursor(1, true);
|
||||
expect(model.getSelectedLogIndices()).toEqual([0, 1, 2]);
|
||||
|
||||
model.moveCursor(1, false);
|
||||
expect(model.getSelectedLogIndices()).toEqual([3]);
|
||||
});
|
||||
|
||||
it("expands and collapses all selected rows", () => {
|
||||
const logs = ["a", "b", "c"].join("\n");
|
||||
const model = new DebugLogViewerModel(logs, Date.now());
|
||||
|
||||
model.moveCursor(1, true);
|
||||
model.expandSelected();
|
||||
expect(model.isExpanded(0)).toBe(true);
|
||||
expect(model.isExpanded(1)).toBe(true);
|
||||
|
||||
model.collapseSelected();
|
||||
expect(model.isExpanded(0)).toBe(false);
|
||||
expect(model.isExpanded(1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildLogCopyPayload", () => {
|
||||
it("joins selected lines and strips control/ansi sequences", () => {
|
||||
const payload = buildLogCopyPayload(["plain", "\u001b[31mred\u001b[0m", "ok\u0007", ""]);
|
||||
expect(payload).toBe("plain\nred\nok");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user