diff --git a/.changeset/fn-8430-tui-log-timestamps.md b/.changeset/fn-8430-tui-log-timestamps.md new file mode 100644 index 0000000000..c3fdba9eb9 --- /dev/null +++ b/.changeset/fn-8430-tui-log-timestamps.md @@ -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). diff --git a/packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx b/packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx index 6627450af1..7ef6e080d0 100644 --- a/packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx +++ b/packages/cli/src/commands/dashboard-tui/__tests__/app.test.tsx @@ -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); + }); +}); diff --git a/packages/cli/src/commands/dashboard-tui/app.tsx b/packages/cli/src/commands/dashboard-tui/app.tsx index 795a499da2..11e647b89c 100644 --- a/packages/cli/src/commands/dashboard-tui/app.tsx +++ b/packages/cli/src/commands/dashboard-tui/app.tsx @@ -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({ {idx} {lvl} {` ${pfx} `} - {entry.message} + {displayMessage} ); @@ -708,7 +730,7 @@ function LogsPanel({ {ts} {lvl} {` ${prefixSlot} `} - {entry.message} + {displayMessage} ); @@ -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) {