FN-8430: simplify TUI log timestamps
Keep dashboard TUI log rows focused on useful message content. - Show capture times as HH:MM:SS across list, expanded, and copied log output. - Strip leading detailed upstream timestamps from list and clipboard text while preserving raw expanded messages. - Add regression coverage and a patch changeset. Files changed: .changeset/fn-8430-tui-log-timestamps.md | 7 ++ .../commands/dashboard-tui/__tests__/app.test.tsx | 93 +++++++++++++++++++++- packages/cli/src/commands/dashboard-tui/app.tsx | 33 ++++++-- 3 files changed, 125 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-8430 Fusion-Task-Lineage: b656dfa1-3c16-4a6f-8ba8-a936fdfdd42d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8430-tui-log-timestamps.md
Normal file
7
.changeset/fn-8430-tui-log-timestamps.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Stop the dashboard TUI Logs tab from showing detailed timestamps on each log line.
|
||||
category: fix
|
||||
dev: Compact LogsPanel time to HH:MM:SS; strip leading YYYY-MM-DD HH:MM:SS.mmm TZ prefixes from displayed messages (e.g. embedded Postgres logs).
|
||||
@@ -2,8 +2,14 @@ import React from "react";
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { render } from "ink-testing-library";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { DashboardApp } from "../app.js";
|
||||
import { DashboardApp, stripLeadingDetailedTimestamp } from "../app.js";
|
||||
import { initCliI18n } from "../../../i18n/index.js";
|
||||
|
||||
const copyToClipboardMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../utils.js", () => ({
|
||||
copyToClipboard: copyToClipboardMock,
|
||||
}));
|
||||
import { DashboardTUI } from "../controller.js";
|
||||
import { createInitialState } from "../state.js";
|
||||
import type { ProjectItem, TaskItem, AgentItem, AgentDetailItem, ModelItem, SettingsValues, TaskDetailData } from "../state.js";
|
||||
@@ -168,6 +174,7 @@ function setTerminalSize(instance: { stdout: unknown }, columns: number, rows: n
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
copyToClipboardMock.mockReset();
|
||||
});
|
||||
|
||||
// 10s bound: ink schedules frames on timer ticks and has flaked past 3s under
|
||||
@@ -1189,7 +1196,7 @@ describe("LogsPanel narrow formatting", () => {
|
||||
wideRender.unmount();
|
||||
});
|
||||
|
||||
it("preserves full timestamp formatting in wide terminals", async () => {
|
||||
it("uses compact timestamp formatting in wide terminals", async () => {
|
||||
const controller = newController();
|
||||
controller.setSystemInfo(makeSystemInfo());
|
||||
controller.setActiveSection("logs");
|
||||
@@ -1203,7 +1210,87 @@ describe("LogsPanel narrow formatting", () => {
|
||||
const frame = rendered.lastFrame() ?? "";
|
||||
|
||||
expect(frame).toContain("wide timestamp");
|
||||
expect(frame).toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/);
|
||||
expect(frame).toMatch(/\d{2}:\d{2}:\d{2}/);
|
||||
expect(frame).not.toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/);
|
||||
rendered.unmount();
|
||||
});
|
||||
|
||||
it("strips embedded detailed timestamps from wide and narrow log rows", async () => {
|
||||
for (const columns of [60, 120]) {
|
||||
const controller = newController();
|
||||
controller.setSystemInfo(makeSystemInfo());
|
||||
controller.setActiveSection("logs");
|
||||
controller.log("2026-07-20 09:20:38.221 PDT checkpoint starting", "startup-factory");
|
||||
controller.setSelectedLogIndex(0);
|
||||
|
||||
const rendered = render(renderDashboardAppNode(controller));
|
||||
setTerminalSize(rendered, columns, 24);
|
||||
rendered.rerender(renderDashboardAppNode(controller));
|
||||
await flushFrames();
|
||||
const frame = rendered.lastFrame() ?? "";
|
||||
|
||||
expect(frame).toContain("checkpoint starting");
|
||||
expect(frame).toContain(columns < 80 ? "[start…]" : "[startup-facto");
|
||||
expect(frame).toMatch(/[✓⚠✗]/);
|
||||
expect(frame).not.toContain("2026-07-20");
|
||||
expect(frame).not.toContain("PDT");
|
||||
expect(frame).not.toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/);
|
||||
rendered.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps raw embedded timestamp text in expanded logs with compact capture time", async () => {
|
||||
const controller = newController();
|
||||
controller.setSystemInfo(makeSystemInfo());
|
||||
controller.setActiveSection("logs");
|
||||
controller.log("2026-07-20 09:20:38.221 PDT checkpoint starting", "startup-factory");
|
||||
controller.setSelectedLogIndex(0);
|
||||
|
||||
const rendered = render(renderDashboardAppNode(controller));
|
||||
setTerminalSize(rendered, 120, 24);
|
||||
rendered.rerender(renderDashboardAppNode(controller));
|
||||
rendered.stdin.write("\r");
|
||||
await waitForFrameContains(rendered.lastFrame, "2026-07-20 09:20:38.221 PDT checkpoint starting");
|
||||
const frame = rendered.lastFrame() ?? "";
|
||||
const timeLine = frame.split("\n").find((line) => line.includes("Time:")) ?? "";
|
||||
|
||||
expect(timeLine).toMatch(/Time:\s+\d{2}:\d{2}:\d{2}/);
|
||||
expect(timeLine).not.toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/);
|
||||
expect(frame).toContain("2026-07-20 09:20:38.221 PDT checkpoint starting");
|
||||
rendered.unmount();
|
||||
});
|
||||
|
||||
it("copies compact time and stripped list message for timestamped entries", async () => {
|
||||
copyToClipboardMock.mockResolvedValue(true);
|
||||
const controller = newController();
|
||||
controller.setSystemInfo(makeSystemInfo());
|
||||
controller.setActiveSection("logs");
|
||||
controller.log("2026-07-20 09:20:38.221 PDT checkpoint starting", "startup-factory");
|
||||
controller.setSelectedLogIndex(0);
|
||||
|
||||
const rendered = render(renderDashboardAppNode(controller));
|
||||
rendered.stdin.write("c");
|
||||
await vi.waitFor(() => {
|
||||
expect(copyToClipboardMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const copied = copyToClipboardMock.mock.calls[0][0] as string;
|
||||
expect(copied).toMatch(/^\d{2}:\d{2}:\d{2} INFO \[startup-factory\] checkpoint starting$/);
|
||||
expect(copied).not.toContain("2026-07-20");
|
||||
expect(copied).not.toContain("PDT");
|
||||
expect(copied).not.toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/);
|
||||
rendered.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripLeadingDetailedTimestamp", () => {
|
||||
it.each([
|
||||
["clean message", "clean message"],
|
||||
["2026-07-20 09:20:38.221 PDT checkpoint starting", "checkpoint starting"],
|
||||
["2026-07-20 09:20:38 UTC checkpoint starting", "checkpoint starting"],
|
||||
["checkpoint starting", "checkpoint starting"],
|
||||
["", ""],
|
||||
])("returns %j for %j", (message, expected) => {
|
||||
expect(stripLeadingDetailedTimestamp(message)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,12 +89,32 @@ import {
|
||||
|
||||
// ── Format helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
/*
|
||||
FNXC:TuiLogs 2026-07-20-09:50:
|
||||
Dense TUI log rows must reserve horizontal space for the level, prefix, and
|
||||
message instead of detailed capture timestamps. A compact clock is sufficient
|
||||
for list rows, clipboard text, and expanded-entry capture time; the Date stays
|
||||
intact for ordering and raw-message debugging.
|
||||
*/
|
||||
function formatTimestamp(date: Date): string {
|
||||
const h = date.getHours().toString().padStart(2, "0");
|
||||
const m = date.getMinutes().toString().padStart(2, "0");
|
||||
const s = date.getSeconds().toString().padStart(2, "0");
|
||||
const ms = date.getMilliseconds().toString().padStart(3, "0");
|
||||
return `${h}:${m}:${s}.${ms}`;
|
||||
return `${h}:${m}:${s}`;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TuiLogs 2026-07-20-09:55:
|
||||
Captured upstream logs can repeat a date, fractional seconds, and timezone in
|
||||
message text after the TUI already records capture time. Strip that leading
|
||||
wall-clock block only at list/copy display time so narrow panes show the useful
|
||||
content while ExpandedLog keeps the raw message for diagnosis.
|
||||
*/
|
||||
export function stripLeadingDetailedTimestamp(message: string): string {
|
||||
return message.replace(
|
||||
/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?\s+(?:[A-Za-z]{2,5}|[+-]\d{2}:?\d{2})\s*/,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
function formatUptime(ms: number): string {
|
||||
@@ -665,6 +685,8 @@ function LogsPanel({
|
||||
// the outer header off the top of the alt-screen.
|
||||
const entryHeight = logsWrapEnabled ? undefined : 1;
|
||||
|
||||
const displayMessage = stripLeadingDetailedTimestamp(entry.message);
|
||||
|
||||
if (isNarrow) {
|
||||
const idx = narrowTimestamp(absoluteIndex);
|
||||
const pfx = narrowPrefix(entry.prefix, NARROW_PREFIX_WIDTH);
|
||||
@@ -683,7 +705,7 @@ function LogsPanel({
|
||||
<Text color={fg} dimColor={!isSelected}>{idx} </Text>
|
||||
<Text color={lvlColor}>{lvl}</Text>
|
||||
<Text color={fg} dimColor={!isSelected}>{` ${pfx} `}</Text>
|
||||
<Text color={fg} bold={isSelected}>{entry.message}</Text>
|
||||
<Text color={fg} bold={isSelected}>{displayMessage}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -708,7 +730,7 @@ function LogsPanel({
|
||||
<Text color={fg} dimColor={!isSelected}>{ts} </Text>
|
||||
<Text color={lvlColor}>{lvl}</Text>
|
||||
<Text color={fg} dimColor={!isSelected}>{` ${prefixSlot} `}</Text>
|
||||
<Text color={fg} bold={isSelected}>{entry.message}</Text>
|
||||
<Text color={fg} bold={isSelected}>{displayMessage}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -4574,7 +4596,8 @@ export function DashboardApp({ controller }: DashboardAppProps) {
|
||||
if (target) {
|
||||
const ts = formatTimestamp(target.timestamp);
|
||||
const prefix = target.prefix ? `[${target.prefix}] ` : "";
|
||||
const text = `${ts} ${target.level.toUpperCase()} ${prefix}${target.message}`;
|
||||
const displayMessage = stripLeadingDetailedTimestamp(target.message);
|
||||
const text = `${ts} ${target.level.toUpperCase()} ${prefix}${displayMessage}`;
|
||||
void copyToClipboard(text).then((ok) => {
|
||||
controller.flashClipboard(ok);
|
||||
if (ok) {
|
||||
|
||||
Reference in New Issue
Block a user