diff --git a/apps/web/src/components/settings/DiagnosticsSettings.tsx b/apps/web/src/components/settings/DiagnosticsSettings.tsx
index 92d8d3f6827..b1a54feb718 100644
--- a/apps/web/src/components/settings/DiagnosticsSettings.tsx
+++ b/apps/web/src/components/settings/DiagnosticsSettings.tsx
@@ -23,7 +23,7 @@ import * as Option from "effect/Option";
import { cn } from "../../lib/utils";
import { resolveAndPersistPreferredEditor } from "../../editorPreferences";
-import { formatRelativeTime } from "../../timestampFormat";
+import { formatRelativeTimeLabel, getRelativeTimeState } from "../../timestampFormat";
import { useEnvironmentQuery } from "../../state/query";
import {
primaryServerAvailableEditorsAtom,
@@ -65,8 +65,7 @@ function formatBytes(value: number): string {
function formatRelative(value: DateTime.Utc | null): string {
if (!value) return "No trace records";
- const relative = formatRelativeTime(DateTime.formatIso(value));
- return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;
+ return formatRelativeTimeLabel(DateTime.formatIso(value));
}
function formatRelativeNoWrap(value: DateTime.Utc | null): string {
@@ -755,12 +754,16 @@ function ProcessResourceHistoryTable({
function DiagnosticsLastChecked({ checkedAt }: { checkedAt: DateTime.Utc | null }) {
useRelativeTimeTick();
- const relative = checkedAt ? formatRelativeTime(DateTime.formatIso(checkedAt)) : null;
+ const relative = getRelativeTimeState(checkedAt ? DateTime.formatIso(checkedAt) : null);
- if (!relative) {
+ if (relative.status === "missing") {
return Checking;
}
+ if (relative.status === "invalid") {
+ return Checked unavailable;
+ }
+
return (
{relative.suffix ? (
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index 40017d56314..4ac3ce359e0 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -58,7 +58,7 @@ import {
import { usePrimaryEnvironment } from "../../state/environments";
import { useProjects } from "../../state/entities";
import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState";
-import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat";
+import { formatRelativeTimeLabel, getRelativeTimeState } from "../../timestampFormat";
import { Button } from "../ui/button";
import { DraftInput } from "../ui/draft-input";
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
@@ -134,12 +134,16 @@ const PROVIDER_SETTINGS = DRIVER_OPTIONS.map((definition) => ({
function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null }) {
useRelativeTimeTick();
- const lastCheckedRelative = lastCheckedAt ? formatRelativeTime(lastCheckedAt) : null;
+ const lastCheckedRelative = getRelativeTimeState(lastCheckedAt);
- if (!lastCheckedRelative) {
+ if (lastCheckedRelative.status === "missing") {
return null;
}
+ if (lastCheckedRelative.status === "invalid") {
+ return Checked unavailable;
+ }
+
return (
{lastCheckedRelative.suffix ? (
diff --git a/apps/web/src/timestampFormat.test.ts b/apps/web/src/timestampFormat.test.ts
index c68d5b7ee46..c2fe4b62714 100644
--- a/apps/web/src/timestampFormat.test.ts
+++ b/apps/web/src/timestampFormat.test.ts
@@ -3,7 +3,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"
import {
formatElapsedDurationLabel,
formatExpiresInLabel,
+ formatRelativeTime,
+ formatRelativeTimeLabel,
+ formatRelativeTimeUntil,
formatRelativeTimeUntilLabel,
+ formatShortTimestamp,
+ formatTimestamp,
+ getRelativeTimeState,
getTimestampFormatOptions,
} from "./timestampFormat";
@@ -90,6 +96,60 @@ describe("formatExpiresInLabel", () => {
});
});
+describe("invalid timestamp inputs", () => {
+ it("returns an empty timestamp instead of throwing", () => {
+ expect(() => formatTimestamp("not-a-date", "12-hour")).not.toThrow();
+ expect(formatTimestamp("not-a-date", "12-hour")).toBe("");
+ });
+
+ it("returns an empty short timestamp instead of throwing", () => {
+ expect(() => formatShortTimestamp("not-a-date", "12-hour")).not.toThrow();
+ expect(formatShortTimestamp("not-a-date", "12-hour")).toBe("");
+ });
+
+ it("returns an empty relative time label instead of a NaN label", () => {
+ expect(formatRelativeTime("not-a-date")).toBeNull();
+ expect(formatRelativeTimeLabel("not-a-date")).toBe("");
+ });
+
+ it("distinguishes missing and invalid relative time state", () => {
+ expect(getRelativeTimeState(null)).toEqual({ status: "missing" });
+ expect(getRelativeTimeState("not-a-date")).toEqual({ status: "invalid" });
+ });
+
+ it("returns an empty elapsed duration instead of a NaN label", () => {
+ expect(formatElapsedDurationLabel("not-a-date")).toBe("");
+ });
+
+ it("returns an empty relative time until label instead of a NaN label", () => {
+ expect(formatRelativeTimeUntil("not-a-date")).toBeNull();
+ expect(formatRelativeTimeUntilLabel("not-a-date")).toBe("");
+ });
+
+ it("returns an empty expires-in label instead of a NaN label", () => {
+ expect(formatExpiresInLabel("not-a-date")).toBe("");
+ });
+});
+
+describe("getRelativeTimeState", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-04-07T12:00:00.000Z"));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("returns relative parts for valid timestamps", () => {
+ expect(getRelativeTimeState("2026-04-07T11:45:00.000Z")).toEqual({
+ status: "relative",
+ value: "15m",
+ suffix: "ago",
+ });
+ });
+});
+
describe("formatElapsedDurationLabel", () => {
beforeEach(() => {
vi.useFakeTimers();
diff --git a/apps/web/src/timestampFormat.ts b/apps/web/src/timestampFormat.ts
index 28facf877e5..31cc41798eb 100644
--- a/apps/web/src/timestampFormat.ts
+++ b/apps/web/src/timestampFormat.ts
@@ -40,8 +40,15 @@ function getTimestampFormatter(
return formatter;
}
+function parseTimestampDate(isoDate: string): Date | null {
+ const date = new Date(isoDate);
+ return Number.isNaN(date.getTime()) ? null : date;
+}
+
export function formatTimestamp(isoDate: string, timestampFormat: TimestampFormat): string {
- return getTimestampFormatter(timestampFormat, true).format(new Date(isoDate));
+ const date = parseTimestampDate(isoDate);
+ if (!date) return "";
+ return getTimestampFormatter(timestampFormat, true).format(date);
}
const monthNameFormatter = new Intl.DateTimeFormat(undefined, { month: "long" });
@@ -69,8 +76,8 @@ export function formatChatTimestampTooltip(
isoDate: string,
timestampFormat: TimestampFormat,
): string {
- const date = new Date(isoDate);
- if (Number.isNaN(date.getTime())) return "";
+ const date = parseTimestampDate(isoDate);
+ if (!date) return "";
const time = formatShortTimestamp(isoDate, timestampFormat);
const day = date.getDate();
const month = monthNameFormatter.format(date);
@@ -79,7 +86,9 @@ export function formatChatTimestampTooltip(
}
export function formatShortTimestamp(isoDate: string, timestampFormat: TimestampFormat): string {
- return getTimestampFormatter(timestampFormat, false).format(new Date(isoDate));
+ const date = parseTimestampDate(isoDate);
+ if (!date) return "";
+ return getTimestampFormatter(timestampFormat, false).format(date);
}
/**
@@ -87,8 +96,16 @@ export function formatShortTimestamp(isoDate: string, timestampFormat: Timestamp
* Returns `{ value: "20s", suffix: "ago" }` or `{ value: "just now", suffix: null }`
* so callers can style the numeric portion independently.
*/
-export function formatRelativeTime(isoDate: string): { value: string; suffix: string | null } {
- const diffMs = Date.now() - new Date(isoDate).getTime();
+type RelativeTimeParts = { value: string; suffix: string | null };
+export type RelativeTimeState =
+ | { status: "missing" }
+ | { status: "invalid" }
+ | { status: "relative"; value: string; suffix: string | null };
+
+export function formatRelativeTime(isoDate: string): RelativeTimeParts | null {
+ const date = parseTimestampDate(isoDate);
+ if (!date) return null;
+ const diffMs = Date.now() - date.getTime();
if (diffMs < 0) return { value: "just now", suffix: null };
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) return { value: "just now", suffix: null };
@@ -102,15 +119,25 @@ export function formatRelativeTime(isoDate: string): { value: string; suffix: st
export function formatRelativeTimeLabel(isoDate: string) {
const relative = formatRelativeTime(isoDate);
+ if (!relative) return "";
return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;
}
+export function getRelativeTimeState(isoDate: string | null): RelativeTimeState {
+ if (!isoDate) return { status: "missing" };
+ const relative = formatRelativeTime(isoDate);
+ if (!relative) return { status: "invalid" };
+ return { status: "relative", ...relative };
+}
+
/**
* Relative elapsed duration since an ISO instant, without an "ago" suffix.
* Useful for labels like "Connected for 3m".
*/
export function formatElapsedDurationLabel(isoDate: string, nowMs: number = Date.now()): string {
- const diffMs = nowMs - new Date(isoDate).getTime();
+ const date = parseTimestampDate(isoDate);
+ if (!date) return "";
+ const diffMs = nowMs - date.getTime();
if (diffMs <= 0) return "just now";
const seconds = Math.floor(diffMs / 1000);
@@ -130,8 +157,10 @@ export function formatElapsedDurationLabel(isoDate: string, nowMs: number = Date
/**
* Relative time until an ISO instant (e.g. expiry). Mirrors {@link formatRelativeTime} but for future times.
*/
-export function formatRelativeTimeUntil(isoDate: string): { value: string; suffix: string | null } {
- const diffMs = new Date(isoDate).getTime() - Date.now();
+export function formatRelativeTimeUntil(isoDate: string): RelativeTimeParts | null {
+ const date = parseTimestampDate(isoDate);
+ if (!date) return null;
+ const diffMs = date.getTime() - Date.now();
if (diffMs <= 0) return { value: "Expired", suffix: null };
const seconds = Math.floor(diffMs / 1000);
if (seconds < 5) return { value: "Soon", suffix: null };
@@ -146,6 +175,7 @@ export function formatRelativeTimeUntil(isoDate: string): { value: string; suffi
export function formatRelativeTimeUntilLabel(isoDate: string): string {
const relative = formatRelativeTimeUntil(isoDate);
+ if (!relative) return "";
return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;
}
@@ -154,7 +184,9 @@ export function formatRelativeTimeUntilLabel(isoDate: string): string {
* Pass `nowMs` when a parent tick drives re-renders so the diff matches that snapshot.
*/
export function formatExpiresInLabel(isoDate: string, nowMs: number = Date.now()): string {
- const diffMs = new Date(isoDate).getTime() - nowMs;
+ const date = parseTimestampDate(isoDate);
+ if (!date) return "";
+ const diffMs = date.getTime() - nowMs;
if (diffMs <= 0) return "Expired";
const totalSeconds = Math.floor(diffMs / 1000);