mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Centralize date/time formatting utilities (#11831)
This commit is contained in:
@@ -1,34 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatAgo, stripThinkingTags } from "./format.ts";
|
||||
import { formatRelativeTimestamp, stripThinkingTags } from "./format.ts";
|
||||
|
||||
describe("formatAgo", () => {
|
||||
it("returns 'in <1m' for timestamps less than 60s in the future", () => {
|
||||
expect(formatAgo(Date.now() + 30_000)).toBe("in <1m");
|
||||
expect(formatRelativeTimestamp(Date.now() + 30_000)).toBe("in <1m");
|
||||
});
|
||||
|
||||
it("returns 'Xm from now' for future timestamps", () => {
|
||||
expect(formatAgo(Date.now() + 5 * 60_000)).toBe("5m from now");
|
||||
expect(formatRelativeTimestamp(Date.now() + 5 * 60_000)).toBe("5m from now");
|
||||
});
|
||||
|
||||
it("returns 'Xh from now' for future timestamps", () => {
|
||||
expect(formatAgo(Date.now() + 3 * 60 * 60_000)).toBe("3h from now");
|
||||
expect(formatRelativeTimestamp(Date.now() + 3 * 60 * 60_000)).toBe("3h from now");
|
||||
});
|
||||
|
||||
it("returns 'Xd from now' for future timestamps beyond 48h", () => {
|
||||
expect(formatAgo(Date.now() + 3 * 24 * 60 * 60_000)).toBe("3d from now");
|
||||
expect(formatRelativeTimestamp(Date.now() + 3 * 24 * 60 * 60_000)).toBe("3d from now");
|
||||
});
|
||||
|
||||
it("returns 'Xs ago' for recent past timestamps", () => {
|
||||
expect(formatAgo(Date.now() - 10_000)).toBe("10s ago");
|
||||
expect(formatRelativeTimestamp(Date.now() - 10_000)).toBe("10s ago");
|
||||
});
|
||||
|
||||
it("returns 'Xm ago' for past timestamps", () => {
|
||||
expect(formatAgo(Date.now() - 5 * 60_000)).toBe("5m ago");
|
||||
expect(formatRelativeTimestamp(Date.now() - 5 * 60_000)).toBe("5m ago");
|
||||
});
|
||||
|
||||
it("returns 'n/a' for null/undefined", () => {
|
||||
expect(formatAgo(null)).toBe("n/a");
|
||||
expect(formatAgo(undefined)).toBe("n/a");
|
||||
expect(formatRelativeTimestamp(null)).toBe("n/a");
|
||||
expect(formatRelativeTimestamp(undefined)).toBe("n/a");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { formatDurationHuman } from "../../../src/infra/format-time/format-duration.ts";
|
||||
import { formatRelativeTimestamp } from "../../../src/infra/format-time/format-relative.ts";
|
||||
import { stripReasoningTagsFromText } from "../../../src/shared/text/reasoning-tags.js";
|
||||
|
||||
export { formatRelativeTimestamp, formatDurationHuman };
|
||||
|
||||
export function formatMs(ms?: number | null): string {
|
||||
if (!ms && ms !== 0) {
|
||||
return "n/a";
|
||||
@@ -7,52 +11,6 @@ export function formatMs(ms?: number | null): string {
|
||||
return new Date(ms).toLocaleString();
|
||||
}
|
||||
|
||||
export function formatAgo(ms?: number | null): string {
|
||||
if (!ms && ms !== 0) {
|
||||
return "n/a";
|
||||
}
|
||||
const diff = Date.now() - ms;
|
||||
const absDiff = Math.abs(diff);
|
||||
const suffix = diff < 0 ? "from now" : "ago";
|
||||
const sec = Math.round(absDiff / 1000);
|
||||
if (sec < 60) {
|
||||
return diff < 0 ? "in <1m" : `${sec}s ago`;
|
||||
}
|
||||
const min = Math.round(sec / 60);
|
||||
if (min < 60) {
|
||||
return `${min}m ${suffix}`;
|
||||
}
|
||||
const hr = Math.round(min / 60);
|
||||
if (hr < 48) {
|
||||
return `${hr}h ${suffix}`;
|
||||
}
|
||||
const day = Math.round(hr / 24);
|
||||
return `${day}d ${suffix}`;
|
||||
}
|
||||
|
||||
export function formatDurationMs(ms?: number | null): string {
|
||||
if (!ms && ms !== 0) {
|
||||
return "n/a";
|
||||
}
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
}
|
||||
const sec = Math.round(ms / 1000);
|
||||
if (sec < 60) {
|
||||
return `${sec}s`;
|
||||
}
|
||||
const min = Math.round(sec / 60);
|
||||
if (min < 60) {
|
||||
return `${min}m`;
|
||||
}
|
||||
const hr = Math.round(min / 60);
|
||||
if (hr < 48) {
|
||||
return `${hr}h`;
|
||||
}
|
||||
const day = Math.round(hr / 24);
|
||||
return `${day}d`;
|
||||
}
|
||||
|
||||
export function formatList(values?: Array<string | null | undefined>): string {
|
||||
if (!values || values.length === 0) {
|
||||
return "none";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CronJob, GatewaySessionRow, PresenceEntry } from "./types.ts";
|
||||
import { formatAgo, formatDurationMs, formatMs } from "./format.ts";
|
||||
import { formatRelativeTimestamp, formatDurationHuman, formatMs } from "./format.ts";
|
||||
|
||||
export function formatPresenceSummary(entry: PresenceEntry): string {
|
||||
const host = entry.host ?? "unknown";
|
||||
@@ -11,14 +11,14 @@ export function formatPresenceSummary(entry: PresenceEntry): string {
|
||||
|
||||
export function formatPresenceAge(entry: PresenceEntry): string {
|
||||
const ts = entry.ts ?? null;
|
||||
return ts ? formatAgo(ts) : "n/a";
|
||||
return ts ? formatRelativeTimestamp(ts) : "n/a";
|
||||
}
|
||||
|
||||
export function formatNextRun(ms?: number | null) {
|
||||
if (!ms) {
|
||||
return "n/a";
|
||||
}
|
||||
return `${formatMs(ms)} (${formatAgo(ms)})`;
|
||||
return `${formatMs(ms)} (${formatRelativeTimestamp(ms)})`;
|
||||
}
|
||||
|
||||
export function formatSessionTokens(row: GatewaySessionRow) {
|
||||
@@ -57,7 +57,7 @@ export function formatCronSchedule(job: CronJob) {
|
||||
return Number.isFinite(atMs) ? `At ${formatMs(atMs)}` : `At ${s.at}`;
|
||||
}
|
||||
if (s.kind === "every") {
|
||||
return `Every ${formatDurationMs(s.everyMs)}`;
|
||||
return `Every ${formatDurationHuman(s.everyMs)}`;
|
||||
}
|
||||
return `Cron ${s.expr}${s.tz ? ` (${s.tz})` : ""}`;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
normalizeToolName,
|
||||
resolveToolProfilePolicy,
|
||||
} from "../../../../src/agents/tool-policy.js";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import {
|
||||
formatCronPayload,
|
||||
formatCronSchedule,
|
||||
@@ -1112,7 +1112,9 @@ function renderAgentChannels(params: {
|
||||
params.agentIdentity,
|
||||
);
|
||||
const entries = resolveChannelEntries(params.snapshot);
|
||||
const lastSuccessLabel = params.lastSuccess ? formatAgo(params.lastSuccess) : "never";
|
||||
const lastSuccessLabel = params.lastSuccess
|
||||
? formatRelativeTimestamp(params.lastSuccess)
|
||||
: "never";
|
||||
return html`
|
||||
<section class="grid grid-cols-2">
|
||||
${renderAgentContextCard(context, "Workspace, identity, and model configuration.")}
|
||||
@@ -1407,7 +1409,7 @@ function renderAgentFiles(params: {
|
||||
function renderAgentFileRow(file: AgentFileEntry, active: string | null, onSelect: () => void) {
|
||||
const status = file.missing
|
||||
? "Missing"
|
||||
: `${formatBytes(file.size)} · ${formatAgo(file.updatedAtMs ?? null)}`;
|
||||
: `${formatBytes(file.size)} · ${formatRelativeTimestamp(file.updatedAtMs ?? null)}`;
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { DiscordStatus } from "../types.ts";
|
||||
import type { ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
|
||||
export function renderDiscordCard(params: {
|
||||
@@ -28,11 +28,11 @@ export function renderDiscordCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${discord?.lastStartAt ? formatAgo(discord.lastStartAt) : "n/a"}</span>
|
||||
<span>${discord?.lastStartAt ? formatRelativeTimestamp(discord.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${discord?.lastProbeAt ? formatAgo(discord.lastProbeAt) : "n/a"}</span>
|
||||
<span>${discord?.lastProbeAt ? formatRelativeTimestamp(discord.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { GoogleChatStatus } from "../types.ts";
|
||||
import type { ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
|
||||
export function renderGoogleChatCard(params: {
|
||||
@@ -42,11 +42,11 @@ export function renderGoogleChatCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${googleChat?.lastStartAt ? formatAgo(googleChat.lastStartAt) : "n/a"}</span>
|
||||
<span>${googleChat?.lastStartAt ? formatRelativeTimestamp(googleChat.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${googleChat?.lastProbeAt ? formatAgo(googleChat.lastProbeAt) : "n/a"}</span>
|
||||
<span>${googleChat?.lastProbeAt ? formatRelativeTimestamp(googleChat.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { IMessageStatus } from "../types.ts";
|
||||
import type { ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
|
||||
export function renderIMessageCard(params: {
|
||||
@@ -28,11 +28,11 @@ export function renderIMessageCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${imessage?.lastStartAt ? formatAgo(imessage.lastStartAt) : "n/a"}</span>
|
||||
<span>${imessage?.lastStartAt ? formatRelativeTimestamp(imessage.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${imessage?.lastProbeAt ? formatAgo(imessage.lastProbeAt) : "n/a"}</span>
|
||||
<span>${imessage?.lastProbeAt ? formatRelativeTimestamp(imessage.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { ChannelAccountSnapshot, NostrStatus } from "../types.ts";
|
||||
import type { ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
import {
|
||||
renderNostrProfileForm,
|
||||
@@ -79,7 +79,7 @@ export function renderNostrCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||
<span>${account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"}</span>
|
||||
</div>
|
||||
${
|
||||
account.lastError
|
||||
@@ -213,7 +213,7 @@ export function renderNostrCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${summaryLastStartAt ? formatAgo(summaryLastStartAt) : "n/a"}</span>
|
||||
<span>${summaryLastStartAt ? formatRelativeTimestamp(summaryLastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -2,22 +2,6 @@ import { html, nothing } from "lit";
|
||||
import type { ChannelAccountSnapshot } from "../types.ts";
|
||||
import type { ChannelKey, ChannelsProps } from "./channels.types.ts";
|
||||
|
||||
export function formatDuration(ms?: number | null) {
|
||||
if (!ms && ms !== 0) {
|
||||
return "n/a";
|
||||
}
|
||||
const sec = Math.round(ms / 1000);
|
||||
if (sec < 60) {
|
||||
return `${sec}s`;
|
||||
}
|
||||
const min = Math.round(sec / 60);
|
||||
if (min < 60) {
|
||||
return `${min}m`;
|
||||
}
|
||||
const hr = Math.round(min / 60);
|
||||
return `${hr}h`;
|
||||
}
|
||||
|
||||
export function channelEnabled(key: ChannelKey, props: ChannelsProps) {
|
||||
const snapshot = props.snapshot;
|
||||
const channels = snapshot?.channels as Record<string, unknown> | null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { SignalStatus } from "../types.ts";
|
||||
import type { ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
|
||||
export function renderSignalCard(params: {
|
||||
@@ -32,11 +32,11 @@ export function renderSignalCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${signal?.lastStartAt ? formatAgo(signal.lastStartAt) : "n/a"}</span>
|
||||
<span>${signal?.lastStartAt ? formatRelativeTimestamp(signal.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${signal?.lastProbeAt ? formatAgo(signal.lastProbeAt) : "n/a"}</span>
|
||||
<span>${signal?.lastProbeAt ? formatRelativeTimestamp(signal.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { SlackStatus } from "../types.ts";
|
||||
import type { ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
|
||||
export function renderSlackCard(params: {
|
||||
@@ -28,11 +28,11 @@ export function renderSlackCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${slack?.lastStartAt ? formatAgo(slack.lastStartAt) : "n/a"}</span>
|
||||
<span>${slack?.lastStartAt ? formatRelativeTimestamp(slack.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${slack?.lastProbeAt ? formatAgo(slack.lastProbeAt) : "n/a"}</span>
|
||||
<span>${slack?.lastProbeAt ? formatRelativeTimestamp(slack.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { ChannelAccountSnapshot, TelegramStatus } from "../types.ts";
|
||||
import type { ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
|
||||
export function renderTelegramCard(params: {
|
||||
@@ -36,7 +36,7 @@ export function renderTelegramCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||
<span>${account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"}</span>
|
||||
</div>
|
||||
${
|
||||
account.lastError
|
||||
@@ -81,11 +81,11 @@ export function renderTelegramCard(params: {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${telegram?.lastStartAt ? formatAgo(telegram.lastStartAt) : "n/a"}</span>
|
||||
<span>${telegram?.lastStartAt ? formatRelativeTimestamp(telegram.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${telegram?.lastProbeAt ? formatAgo(telegram.lastProbeAt) : "n/a"}</span>
|
||||
<span>${telegram?.lastProbeAt ? formatRelativeTimestamp(telegram.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
WhatsAppStatus,
|
||||
} from "../types.ts";
|
||||
import type { ChannelKey, ChannelsChannelData, ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
import { renderDiscordCard } from "./channels.discord.ts";
|
||||
import { renderGoogleChatCard } from "./channels.googlechat.ts";
|
||||
@@ -73,7 +73,7 @@ export function renderChannels(props: ChannelsProps) {
|
||||
<div class="card-title">Channel health</div>
|
||||
<div class="card-sub">Channel status snapshots from the gateway.</div>
|
||||
</div>
|
||||
<div class="muted">${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}</div>
|
||||
<div class="muted">${props.lastSuccessAt ? formatRelativeTimestamp(props.lastSuccessAt) : "n/a"}</div>
|
||||
</div>
|
||||
${
|
||||
props.lastError
|
||||
@@ -308,7 +308,7 @@ function renderGenericAccount(account: ChannelAccountSnapshot) {
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span>
|
||||
<span>${account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"}</span>
|
||||
</div>
|
||||
${
|
||||
account.lastError
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { WhatsAppStatus } from "../types.ts";
|
||||
import type { ChannelsProps } from "./channels.types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts";
|
||||
import { renderChannelConfigSection } from "./channels.config.ts";
|
||||
import { formatDuration } from "./channels.shared.ts";
|
||||
|
||||
export function renderWhatsAppCard(params: {
|
||||
props: ChannelsProps;
|
||||
@@ -38,19 +37,19 @@ export function renderWhatsAppCard(params: {
|
||||
<div>
|
||||
<span class="label">Last connect</span>
|
||||
<span>
|
||||
${whatsapp?.lastConnectedAt ? formatAgo(whatsapp.lastConnectedAt) : "n/a"}
|
||||
${whatsapp?.lastConnectedAt ? formatRelativeTimestamp(whatsapp.lastConnectedAt) : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last message</span>
|
||||
<span>
|
||||
${whatsapp?.lastMessageAt ? formatAgo(whatsapp.lastMessageAt) : "n/a"}
|
||||
${whatsapp?.lastMessageAt ? formatRelativeTimestamp(whatsapp.lastMessageAt) : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Auth age</span>
|
||||
<span>
|
||||
${whatsapp?.authAgeMs != null ? formatDuration(whatsapp.authAgeMs) : "n/a"}
|
||||
${whatsapp?.authAgeMs != null ? formatDurationHuman(whatsapp.authAgeMs) : "n/a"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { ChannelUiMetaEntry, CronJob, CronRunLogEntry, CronStatus } from "../types.ts";
|
||||
import type { CronFormState } from "../ui-types.ts";
|
||||
import { formatAgo, formatMs } from "../format.ts";
|
||||
import { formatRelativeTimestamp, formatMs } from "../format.ts";
|
||||
import { pathForTab } from "../navigation.ts";
|
||||
import { formatCronSchedule, formatNextRun } from "../presenter.ts";
|
||||
|
||||
@@ -482,7 +482,7 @@ function formatStateRelative(ms?: number) {
|
||||
if (typeof ms !== "number" || !Number.isFinite(ms)) {
|
||||
return "n/a";
|
||||
}
|
||||
return formatAgo(ms);
|
||||
return formatRelativeTimestamp(ms);
|
||||
}
|
||||
|
||||
function renderJobState(job: CronJob) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
ExecApprovalsFile,
|
||||
ExecApprovalsSnapshot,
|
||||
} from "../controllers/exec-approvals.ts";
|
||||
import { clampText, formatAgo, formatList } from "../format.ts";
|
||||
import { clampText, formatRelativeTimestamp, formatList } from "../format.ts";
|
||||
|
||||
export type NodesProps = {
|
||||
loading: boolean;
|
||||
@@ -130,7 +130,7 @@ function renderDevices(props: NodesProps) {
|
||||
|
||||
function renderPendingDevice(req: PendingDevice, props: NodesProps) {
|
||||
const name = req.displayName?.trim() || req.deviceId;
|
||||
const age = typeof req.ts === "number" ? formatAgo(req.ts) : "n/a";
|
||||
const age = typeof req.ts === "number" ? formatRelativeTimestamp(req.ts) : "n/a";
|
||||
const role = req.role?.trim() ? `role: ${req.role}` : "role: -";
|
||||
const repair = req.isRepair ? " · repair" : "";
|
||||
const ip = req.remoteIp ? ` · ${req.remoteIp}` : "";
|
||||
@@ -189,7 +189,9 @@ function renderPairedDevice(device: PairedDevice, props: NodesProps) {
|
||||
function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: NodesProps) {
|
||||
const status = token.revokedAtMs ? "revoked" : "active";
|
||||
const scopes = `scopes: ${formatList(token.scopes)}`;
|
||||
const when = formatAgo(token.rotatedAtMs ?? token.createdAtMs ?? token.lastUsedAtMs ?? null);
|
||||
const when = formatRelativeTimestamp(
|
||||
token.rotatedAtMs ?? token.createdAtMs ?? token.lastUsedAtMs ?? null,
|
||||
);
|
||||
return html`
|
||||
<div class="row" style="justify-content: space-between; gap: 8px;">
|
||||
<div class="list-sub">${token.role} · ${status} · ${scopes} · ${when}</div>
|
||||
@@ -931,7 +933,7 @@ function renderAllowlistEntry(
|
||||
entry: ExecApprovalsAllowlistEntry,
|
||||
index: number,
|
||||
) {
|
||||
const lastUsed = entry.lastUsedAt ? formatAgo(entry.lastUsedAt) : "never";
|
||||
const lastUsed = entry.lastUsedAt ? formatRelativeTimestamp(entry.lastUsedAt) : "never";
|
||||
const lastCommand = entry.lastUsedCommand ? clampText(entry.lastUsedCommand, 120) : null;
|
||||
const lastPath = entry.lastResolvedPath ? clampText(entry.lastResolvedPath, 120) : null;
|
||||
return html`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html } from "lit";
|
||||
import type { GatewayHelloOk } from "../gateway.ts";
|
||||
import type { UiSettings } from "../storage.ts";
|
||||
import { formatAgo, formatDurationMs } from "../format.ts";
|
||||
import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts";
|
||||
import { formatNextRun } from "../presenter.ts";
|
||||
|
||||
export type OverviewProps = {
|
||||
@@ -26,7 +26,7 @@ export function renderOverview(props: OverviewProps) {
|
||||
const snapshot = props.hello?.snapshot as
|
||||
| { uptimeMs?: number; policy?: { tickIntervalMs?: number } }
|
||||
| undefined;
|
||||
const uptime = snapshot?.uptimeMs ? formatDurationMs(snapshot.uptimeMs) : "n/a";
|
||||
const uptime = snapshot?.uptimeMs ? formatDurationHuman(snapshot.uptimeMs) : "n/a";
|
||||
const tick = snapshot?.policy?.tickIntervalMs ? `${snapshot.policy.tickIntervalMs}ms` : "n/a";
|
||||
const authHint = (() => {
|
||||
if (props.connected || !props.lastError) {
|
||||
@@ -198,7 +198,7 @@ export function renderOverview(props: OverviewProps) {
|
||||
<div class="stat">
|
||||
<div class="stat-label">Last Channels Refresh</div>
|
||||
<div class="stat-value">
|
||||
${props.lastChannelsRefresh ? formatAgo(props.lastChannelsRefresh) : "n/a"}
|
||||
${props.lastChannelsRefresh ? formatRelativeTimestamp(props.lastChannelsRefresh) : "n/a"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../types.ts";
|
||||
import { formatAgo } from "../format.ts";
|
||||
import { formatRelativeTimestamp } from "../format.ts";
|
||||
import { pathForTab } from "../navigation.ts";
|
||||
import { formatSessionTokens } from "../presenter.ts";
|
||||
|
||||
@@ -221,7 +221,7 @@ function renderRow(
|
||||
onDelete: SessionsProps["onDelete"],
|
||||
disabled: boolean,
|
||||
) {
|
||||
const updated = row.updatedAt ? formatAgo(row.updatedAt) : "n/a";
|
||||
const updated = row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : "n/a";
|
||||
const rawThinking = row.thinkingLevel ?? "";
|
||||
const isBinaryThinking = isBinaryThinkingProvider(row.modelProvider);
|
||||
const thinking = resolveThinkLevelDisplay(rawThinking, isBinaryThinking);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { html, svg, nothing } from "lit";
|
||||
import { formatDurationCompact } from "../../../../src/infra/format-time/format-duration.ts";
|
||||
import { extractQueryTerms, filterSessionsByQuery, parseToolSummary } from "../usage-helpers.ts";
|
||||
|
||||
// Inline styles for usage view (app uses light DOM, so static styles don't work)
|
||||
@@ -2461,19 +2462,6 @@ function formatIsoDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function formatDurationShort(ms?: number): string {
|
||||
if (!ms || ms <= 0) {
|
||||
return "0s";
|
||||
}
|
||||
if (ms >= 60_000) {
|
||||
return `${Math.round(ms / 60000)}m`;
|
||||
}
|
||||
if (ms >= 1000) {
|
||||
return `${Math.round(ms / 1000)}s`;
|
||||
}
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
|
||||
function parseYmdDate(dateStr: string): Date | null {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
|
||||
if (!match) {
|
||||
@@ -2500,23 +2488,6 @@ function formatFullDate(dateStr: string): string {
|
||||
return date.toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" });
|
||||
}
|
||||
|
||||
function formatDurationMs(ms?: number): string {
|
||||
if (!ms || ms <= 0) {
|
||||
return "—";
|
||||
}
|
||||
const totalSeconds = Math.round(ms / 1000);
|
||||
const seconds = totalSeconds % 60;
|
||||
const minutes = Math.floor(totalSeconds / 60) % 60;
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
function downloadTextFile(filename: string, content: string, type = "text/plain") {
|
||||
const blob = new Blob([content], { type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -3467,7 +3438,10 @@ function renderUsageInsights(
|
||||
stats.throughputCostPerMin !== undefined
|
||||
? `${formatCost(stats.throughputCostPerMin, 4)} / min`
|
||||
: "—";
|
||||
const avgDurationLabel = stats.durationCount > 0 ? formatDurationShort(stats.avgDurationMs) : "—";
|
||||
const avgDurationLabel =
|
||||
stats.durationCount > 0
|
||||
? (formatDurationCompact(stats.avgDurationMs, { spaced: true }) ?? "—")
|
||||
: "—";
|
||||
const cacheHint = "Cache hit rate = cache read / (input + cache read). Higher is better.";
|
||||
const errorHint = "Error rate = errors / total messages. Lower is better.";
|
||||
const throughputHint = "Throughput shows tokens per minute over active time. Higher is better.";
|
||||
@@ -3672,7 +3646,7 @@ function renderSessionsCard(
|
||||
parts.push(`errors:${s.usage.messageCounts.errors}`);
|
||||
}
|
||||
if (showColumn("duration") && s.usage?.durationMs) {
|
||||
parts.push(`dur:${formatDurationMs(s.usage.durationMs)}`);
|
||||
parts.push(`dur:${formatDurationCompact(s.usage.durationMs, { spaced: true }) ?? "—"}`);
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
@@ -3976,7 +3950,7 @@ function renderSessionSummary(session: UsageSessionEntry) {
|
||||
</div>
|
||||
<div class="session-summary-card">
|
||||
<div class="session-summary-title">Duration</div>
|
||||
<div class="session-summary-value">${formatDurationMs(usage.durationMs)}</div>
|
||||
<div class="session-summary-value">${formatDurationCompact(usage.durationMs, { spaced: true }) ?? "—"}</div>
|
||||
<div class="session-summary-meta">${formatTs(usage.firstActivity)} → ${formatTs(usage.lastActivity)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user