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:
gsxdsm
2026-07-20 10:04:20 -07:00
parent 087fd8f881
commit 81abe53957
3 changed files with 125 additions and 8 deletions

View 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).

View File

@@ -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);
});
});

View File

@@ -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) {