mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:11:26 +00:00
Centralize date/time formatting utilities (#11831)
This commit is contained in:
@@ -5,8 +5,8 @@ import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import { withProgress } from "../../cli/progress.js";
|
||||
import { type OpenClawConfig, readConfigFileSnapshot } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { formatAge } from "../../infra/channel-summary.js";
|
||||
import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js";
|
||||
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
@@ -48,10 +48,10 @@ export function formatGatewayChannelsStatusLines(payload: Record<string, unknown
|
||||
? account.lastOutboundAt
|
||||
: null;
|
||||
if (inboundAt) {
|
||||
bits.push(`in:${formatAge(Date.now() - inboundAt)}`);
|
||||
bits.push(`in:${formatTimeAgo(Date.now() - inboundAt)}`);
|
||||
}
|
||||
if (outboundAt) {
|
||||
bits.push(`out:${formatAge(Date.now() - outboundAt)}`);
|
||||
bits.push(`out:${formatTimeAgo(Date.now() - outboundAt)}`);
|
||||
}
|
||||
if (typeof account.mode === "string" && account.mode.length > 0) {
|
||||
bits.push(`mode:${account.mode}`);
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
import type { SandboxBrowserInfo, SandboxContainerInfo } from "../agents/sandbox.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import {
|
||||
formatAge,
|
||||
formatImageMatch,
|
||||
formatSimpleStatus,
|
||||
formatStatus,
|
||||
} from "./sandbox-formatters.js";
|
||||
import { formatDurationCompact } from "../infra/format-time/format-duration.ts";
|
||||
import { formatImageMatch, formatSimpleStatus, formatStatus } from "./sandbox-formatters.js";
|
||||
|
||||
type DisplayConfig<T> = {
|
||||
emptyMessage: string;
|
||||
@@ -40,8 +36,12 @@ export function displayContainers(containers: SandboxContainerInfo[], runtime: R
|
||||
rt.log(` ${container.containerName}`);
|
||||
rt.log(` Status: ${formatStatus(container.running)}`);
|
||||
rt.log(` Image: ${container.image} ${formatImageMatch(container.imageMatch)}`);
|
||||
rt.log(` Age: ${formatAge(Date.now() - container.createdAtMs)}`);
|
||||
rt.log(` Idle: ${formatAge(Date.now() - container.lastUsedAtMs)}`);
|
||||
rt.log(
|
||||
` Age: ${formatDurationCompact(Date.now() - container.createdAtMs, { spaced: true }) ?? "0s"}`,
|
||||
);
|
||||
rt.log(
|
||||
` Idle: ${formatDurationCompact(Date.now() - container.lastUsedAtMs, { spaced: true }) ?? "0s"}`,
|
||||
);
|
||||
rt.log(` Session: ${container.sessionKey}`);
|
||||
rt.log("");
|
||||
},
|
||||
@@ -64,8 +64,12 @@ export function displayBrowsers(browsers: SandboxBrowserInfo[], runtime: Runtime
|
||||
if (browser.noVncPort) {
|
||||
rt.log(` noVNC: ${browser.noVncPort}`);
|
||||
}
|
||||
rt.log(` Age: ${formatAge(Date.now() - browser.createdAtMs)}`);
|
||||
rt.log(` Idle: ${formatAge(Date.now() - browser.lastUsedAtMs)}`);
|
||||
rt.log(
|
||||
` Age: ${formatDurationCompact(Date.now() - browser.createdAtMs, { spaced: true }) ?? "0s"}`,
|
||||
);
|
||||
rt.log(
|
||||
` Idle: ${formatDurationCompact(Date.now() - browser.lastUsedAtMs, { spaced: true }) ?? "0s"}`,
|
||||
);
|
||||
rt.log(` Session: ${browser.sessionKey}`);
|
||||
rt.log("");
|
||||
},
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatDurationCompact } from "../infra/format-time/format-duration.js";
|
||||
import {
|
||||
countMismatches,
|
||||
countRunning,
|
||||
formatAge,
|
||||
formatImageMatch,
|
||||
formatSimpleStatus,
|
||||
formatStatus,
|
||||
} from "./sandbox-formatters.js";
|
||||
|
||||
/** Helper matching old formatAge behavior: spaced compound duration */
|
||||
const formatAge = (ms: number) => formatDurationCompact(ms, { spaced: true }) ?? "0s";
|
||||
|
||||
describe("sandbox-formatters", () => {
|
||||
describe("formatStatus", () => {
|
||||
it("should format running status", () => {
|
||||
@@ -47,21 +50,21 @@ describe("sandbox-formatters", () => {
|
||||
|
||||
it("should format minutes", () => {
|
||||
expect(formatAge(60000)).toBe("1m");
|
||||
expect(formatAge(90000)).toBe("1m");
|
||||
expect(formatAge(90000)).toBe("1m 30s"); // 90 seconds = 1m 30s
|
||||
expect(formatAge(300000)).toBe("5m");
|
||||
});
|
||||
|
||||
it("should format hours and minutes", () => {
|
||||
expect(formatAge(3600000)).toBe("1h 0m");
|
||||
expect(formatAge(3600000)).toBe("1h");
|
||||
expect(formatAge(3660000)).toBe("1h 1m");
|
||||
expect(formatAge(7200000)).toBe("2h 0m");
|
||||
expect(formatAge(7200000)).toBe("2h");
|
||||
expect(formatAge(5400000)).toBe("1h 30m");
|
||||
});
|
||||
|
||||
it("should format days and hours", () => {
|
||||
expect(formatAge(86400000)).toBe("1d 0h");
|
||||
expect(formatAge(86400000)).toBe("1d");
|
||||
expect(formatAge(90000000)).toBe("1d 1h");
|
||||
expect(formatAge(172800000)).toBe("2d 0h");
|
||||
expect(formatAge(172800000)).toBe("2d");
|
||||
expect(formatAge(183600000)).toBe("2d 3h");
|
||||
});
|
||||
|
||||
@@ -70,9 +73,9 @@ describe("sandbox-formatters", () => {
|
||||
});
|
||||
|
||||
it("should handle edge cases", () => {
|
||||
expect(formatAge(59999)).toBe("59s"); // Just under 1 minute
|
||||
expect(formatAge(3599999)).toBe("59m"); // Just under 1 hour
|
||||
expect(formatAge(86399999)).toBe("23h 59m"); // Just under 1 day
|
||||
expect(formatAge(59999)).toBe("1m"); // Rounds to 1 minute exactly
|
||||
expect(formatAge(3599999)).toBe("1h"); // Rounds to 1 hour exactly
|
||||
expect(formatAge(86399999)).toBe("1d"); // Rounds to 1 day exactly
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,24 +14,6 @@ export function formatImageMatch(matches: boolean): string {
|
||||
return matches ? "✓" : "⚠️ mismatch";
|
||||
}
|
||||
|
||||
export function formatAge(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours % 24}h`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard and counter utilities
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { loadSessionStore, resolveStorePath, type SessionEntry } from "../config/sessions.js";
|
||||
import { info } from "../globals.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
|
||||
type SessionRow = {
|
||||
@@ -90,7 +91,7 @@ const formatKindCell = (kind: SessionRow["kind"], rich: boolean) => {
|
||||
};
|
||||
|
||||
const formatAgeCell = (updatedAt: number | null | undefined, rich: boolean) => {
|
||||
const ageLabel = updatedAt ? formatAge(Date.now() - updatedAt) : "unknown";
|
||||
const ageLabel = updatedAt ? formatTimeAgo(Date.now() - updatedAt) : "unknown";
|
||||
const padded = ageLabel.padEnd(AGE_PAD);
|
||||
return rich ? theme.muted(padded) : padded;
|
||||
};
|
||||
@@ -116,25 +117,6 @@ const formatFlagsCell = (row: SessionRow, rich: boolean) => {
|
||||
return label.length === 0 ? "" : rich ? theme.muted(label) : label;
|
||||
};
|
||||
|
||||
const formatAge = (ms: number | null | undefined) => {
|
||||
if (!ms || ms < 0) {
|
||||
return "unknown";
|
||||
}
|
||||
const minutes = Math.round(ms / 60_000);
|
||||
if (minutes < 1) {
|
||||
return "just now";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ago`;
|
||||
}
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) {
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
};
|
||||
|
||||
function classifyKey(key: string, entry?: SessionEntry): SessionRow["kind"] {
|
||||
if (key === "global") {
|
||||
return "global";
|
||||
|
||||
@@ -28,7 +28,7 @@ import { VERSION } from "../version.js";
|
||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
import { getAgentLocalStatuses } from "./status-all/agents.js";
|
||||
import { buildChannelsTable } from "./status-all/channels.js";
|
||||
import { formatDuration, formatGatewayAuthUsed } from "./status-all/format.js";
|
||||
import { formatDurationPrecise, formatGatewayAuthUsed } from "./status-all/format.js";
|
||||
import { pickGatewaySelfPresence } from "./status-all/gateway.js";
|
||||
import { buildStatusAllReportLines } from "./status-all/report-lines.js";
|
||||
|
||||
@@ -354,7 +354,7 @@ export async function statusAllCommand(
|
||||
|
||||
const gatewayTarget = remoteUrlMissing ? `fallback ${connection.url}` : connection.url;
|
||||
const gatewayStatus = gatewayReachable
|
||||
? `reachable ${formatDuration(gatewayProbe?.connectLatencyMs)}`
|
||||
? `reachable ${formatDurationPrecise(gatewayProbe?.connectLatencyMs ?? 0)}`
|
||||
: gatewayProbe?.error
|
||||
? `unreachable (${gatewayProbe.error})`
|
||||
: "unreachable";
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { sha256HexPrefix } from "../../logging/redact-identifier.js";
|
||||
import { formatAge } from "./format.js";
|
||||
import { formatTimeAgo } from "./format.js";
|
||||
|
||||
export type ChannelRow = {
|
||||
id: ChannelId;
|
||||
@@ -436,7 +436,7 @@ export async function buildChannelsTable(
|
||||
extra.push(link.selfE164);
|
||||
}
|
||||
if (link.linked && link.authAgeMs != null && link.authAgeMs >= 0) {
|
||||
extra.push(`auth ${formatAge(link.authAgeMs)}`);
|
||||
extra.push(`auth ${formatTimeAgo(link.authAgeMs)}`);
|
||||
}
|
||||
if (accounts.length > 1 || plugin.meta.forceAccountBinding) {
|
||||
extra.push(`accounts ${accounts.length || 1}`);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type RestartSentinelPayload,
|
||||
summarizeRestartSentinel,
|
||||
} from "../../infra/restart-sentinel.js";
|
||||
import { formatAge, redactSecrets } from "./format.js";
|
||||
import { formatTimeAgo, redactSecrets } from "./format.js";
|
||||
import { readFileTailLines, summarizeLogTail } from "./gateway.js";
|
||||
|
||||
type ConfigIssueLike = { path: string; message: string };
|
||||
@@ -106,7 +106,7 @@ export async function appendStatusAllDiagnosis(params: {
|
||||
if (params.sentinel?.payload) {
|
||||
emitCheck("Restart sentinel present", "warn");
|
||||
lines.push(
|
||||
` ${muted(`${summarizeRestartSentinel(params.sentinel.payload)} · ${formatAge(Date.now() - params.sentinel.payload.ts)}`)}`,
|
||||
` ${muted(`${summarizeRestartSentinel(params.sentinel.payload)} · ${formatTimeAgo(Date.now() - params.sentinel.payload.ts)}`)}`,
|
||||
);
|
||||
} else {
|
||||
emitCheck("Restart sentinel: none", "ok");
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
export const formatAge = (ms: number | null | undefined) => {
|
||||
if (!ms || ms < 0) {
|
||||
return "unknown";
|
||||
}
|
||||
const minutes = Math.round(ms / 60_000);
|
||||
if (minutes < 1) {
|
||||
return "just now";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ago`;
|
||||
}
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) {
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
};
|
||||
|
||||
export const formatDuration = (ms: number | null | undefined) => {
|
||||
if (ms == null || !Number.isFinite(ms)) {
|
||||
return "unknown";
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
};
|
||||
export { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
|
||||
export { formatDurationPrecise } from "../../infra/format-time/format-duration.ts";
|
||||
|
||||
export function formatGatewayAuthUsed(
|
||||
auth: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ProgressReporter } from "../../cli/progress.js";
|
||||
import { renderTable } from "../../terminal/table.js";
|
||||
import { isRich, theme } from "../../terminal/theme.js";
|
||||
import { appendStatusAllDiagnosis } from "./diagnosis.js";
|
||||
import { formatAge } from "./format.js";
|
||||
import { formatTimeAgo } from "./format.js";
|
||||
|
||||
type OverviewRow = { Item: string; Value: string };
|
||||
|
||||
@@ -128,7 +128,7 @@ export async function buildStatusAllReportLines(params: {
|
||||
? ok("OK")
|
||||
: "unknown",
|
||||
Sessions: String(a.sessionsCount),
|
||||
Active: a.lastActiveAgeMs != null ? formatAge(a.lastActiveAgeMs) : "unknown",
|
||||
Active: a.lastActiveAgeMs != null ? formatTimeAgo(a.lastActiveAgeMs) : "unknown",
|
||||
Store: a.sessionsPath,
|
||||
}));
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { withProgress } from "../cli/progress.js";
|
||||
import { resolveGatewayPort } from "../config/config.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { info } from "../globals.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { formatUsageReportLines, loadProviderUsageSummary } from "../infra/provider-usage.js";
|
||||
import {
|
||||
formatUpdateChannelLabel,
|
||||
@@ -26,7 +27,6 @@ import { statusAllCommand } from "./status-all.js";
|
||||
import { formatGatewayAuthUsed } from "./status-all/format.js";
|
||||
import { getDaemonStatusSummary, getNodeDaemonStatusSummary } from "./status.daemon.js";
|
||||
import {
|
||||
formatAge,
|
||||
formatDuration,
|
||||
formatKTokens,
|
||||
formatTokensCompact,
|
||||
@@ -239,7 +239,7 @@ export async function statusCommand(
|
||||
? `${agentStatus.bootstrapPendingCount} bootstrapping`
|
||||
: "no bootstraps";
|
||||
const def = agentStatus.agents.find((a) => a.id === agentStatus.defaultId);
|
||||
const defActive = def?.lastActiveAgeMs != null ? formatAge(def.lastActiveAgeMs) : "unknown";
|
||||
const defActive = def?.lastActiveAgeMs != null ? formatTimeAgo(def.lastActiveAgeMs) : "unknown";
|
||||
const defSuffix = def ? ` · default ${def.id} active ${defActive}` : "";
|
||||
return `${agentStatus.agents.length} · ${pending} · sessions ${agentStatus.totalSessions}${defSuffix}`;
|
||||
})();
|
||||
@@ -294,7 +294,7 @@ export async function statusCommand(
|
||||
if (!lastHeartbeat) {
|
||||
return muted("none");
|
||||
}
|
||||
const age = formatAge(Date.now() - lastHeartbeat.ts);
|
||||
const age = formatTimeAgo(Date.now() - lastHeartbeat.ts);
|
||||
const channel = lastHeartbeat.channel ?? "unknown";
|
||||
const accountLabel = lastHeartbeat.accountId ? `account ${lastHeartbeat.accountId}` : null;
|
||||
return [lastHeartbeat.status, `${age} ago`, channel, accountLabel].filter(Boolean).join(" · ");
|
||||
@@ -527,7 +527,7 @@ export async function statusCommand(
|
||||
? summary.sessions.recent.map((sess) => ({
|
||||
Key: shortenText(sess.key, 32),
|
||||
Kind: sess.kind,
|
||||
Age: sess.updatedAt ? formatAge(sess.age) : "no activity",
|
||||
Age: sess.updatedAt ? formatTimeAgo(sess.age) : "no activity",
|
||||
Model: sess.model ?? "unknown",
|
||||
Tokens: formatTokensCompact(sess),
|
||||
}))
|
||||
|
||||
@@ -1,35 +1,14 @@
|
||||
import type { SessionStatus } from "./status.types.js";
|
||||
import { formatDurationPrecise } from "../infra/format-time/format-duration.ts";
|
||||
|
||||
export const formatKTokens = (value: number) =>
|
||||
`${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
||||
|
||||
export const formatAge = (ms: number | null | undefined) => {
|
||||
if (!ms || ms < 0) {
|
||||
return "unknown";
|
||||
}
|
||||
const minutes = Math.round(ms / 60_000);
|
||||
if (minutes < 1) {
|
||||
return "just now";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ago`;
|
||||
}
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) {
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
};
|
||||
|
||||
export const formatDuration = (ms: number | null | undefined) => {
|
||||
if (ms == null || !Number.isFinite(ms)) {
|
||||
return "unknown";
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
return formatDurationPrecise(ms, { decimals: 1 });
|
||||
};
|
||||
|
||||
export const shortenText = (value: string, maxLen: number) => {
|
||||
|
||||
Reference in New Issue
Block a user