diff --git a/src/components/ProjectSidebar.tsx b/src/components/ProjectSidebar.tsx index 3c59a0a21..9f69d8b80 100644 --- a/src/components/ProjectSidebar.tsx +++ b/src/components/ProjectSidebar.tsx @@ -12,6 +12,7 @@ import { useDrag, useDrop, useDragLayer } from "react-dnd"; import { sortProjectsByOrder, reorderProjects, normalizeOrder } from "@/utils/projectOrdering"; import { matchesKeybind, formatKeybind, KEYBINDS } from "@/utils/ui/keybinds"; import { abbreviatePath } from "@/utils/ui/pathAbbreviation"; +import { formatRelativeTime } from "@/utils/ui/dateTime"; import { TooltipWrapper, Tooltip } from "./Tooltip"; import { StatusIndicator } from "./StatusIndicator"; // Removed: import { getModelName } from "@/utils/ai/models"; @@ -1070,6 +1071,8 @@ const ProjectSidebar: React.FC = ({ "Assistant is responding" ) : isUnread ? ( "Unread messages" + ) : workspaceState.recencyTimestamp ? ( + `Idle • Last used ${formatRelativeTime(workspaceState.recencyTimestamp)}` ) : ( "Idle" ) diff --git a/src/utils/ui/dateTime.test.ts b/src/utils/ui/dateTime.test.ts new file mode 100644 index 000000000..be9b28be7 --- /dev/null +++ b/src/utils/ui/dateTime.test.ts @@ -0,0 +1,56 @@ +import { describe, test, expect } from "bun:test"; +import { formatRelativeTime } from "./dateTime"; + +describe("formatRelativeTime", () => { + test("should return 'just now' for very recent timestamps", () => { + const now = Date.now(); + expect(formatRelativeTime(now)).toBe("just now"); + expect(formatRelativeTime(now - 30 * 1000)).toBe("just now"); // 30 seconds ago + }); + + test("should return 'just now' for future timestamps", () => { + const future = Date.now() + 5000; + expect(formatRelativeTime(future)).toBe("just now"); + }); + + test("should format minutes correctly", () => { + const now = Date.now(); + expect(formatRelativeTime(now - 1 * 60 * 1000)).toBe("1 minute ago"); + expect(formatRelativeTime(now - 5 * 60 * 1000)).toBe("5 minutes ago"); + expect(formatRelativeTime(now - 59 * 60 * 1000)).toBe("59 minutes ago"); + }); + + test("should format hours correctly", () => { + const now = Date.now(); + expect(formatRelativeTime(now - 1 * 60 * 60 * 1000)).toBe("1 hour ago"); + expect(formatRelativeTime(now - 3 * 60 * 60 * 1000)).toBe("3 hours ago"); + expect(formatRelativeTime(now - 23 * 60 * 60 * 1000)).toBe("23 hours ago"); + }); + + test("should format days correctly", () => { + const now = Date.now(); + expect(formatRelativeTime(now - 1 * 24 * 60 * 60 * 1000)).toBe("1 day ago"); + expect(formatRelativeTime(now - 3 * 24 * 60 * 60 * 1000)).toBe("3 days ago"); + expect(formatRelativeTime(now - 6 * 24 * 60 * 60 * 1000)).toBe("6 days ago"); + }); + + test("should format weeks correctly", () => { + const now = Date.now(); + expect(formatRelativeTime(now - 7 * 24 * 60 * 60 * 1000)).toBe("1 week ago"); + expect(formatRelativeTime(now - 14 * 24 * 60 * 60 * 1000)).toBe("2 weeks ago"); + expect(formatRelativeTime(now - 27 * 24 * 60 * 60 * 1000)).toBe("3 weeks ago"); + }); + + test("should format months correctly", () => { + const now = Date.now(); + expect(formatRelativeTime(now - 30 * 24 * 60 * 60 * 1000)).toBe("1 month ago"); + expect(formatRelativeTime(now - 60 * 24 * 60 * 60 * 1000)).toBe("2 months ago"); + expect(formatRelativeTime(now - 180 * 24 * 60 * 60 * 1000)).toBe("6 months ago"); + }); + + test("should format years correctly", () => { + const now = Date.now(); + expect(formatRelativeTime(now - 365 * 24 * 60 * 60 * 1000)).toBe("1 year ago"); + expect(formatRelativeTime(now - 730 * 24 * 60 * 60 * 1000)).toBe("2 years ago"); + }); +}); diff --git a/src/utils/ui/dateTime.ts b/src/utils/ui/dateTime.ts index 5efa5bf9b..4502a4e7d 100644 --- a/src/utils/ui/dateTime.ts +++ b/src/utils/ui/dateTime.ts @@ -54,3 +54,44 @@ export function formatFullTimestamp(timestamp: number): string { hour12: true, }); } + +/** + * Formats a Unix timestamp (milliseconds) into a human-readable relative time string. + * Examples: "2 minutes ago", "3 hours ago", "2 days ago", "3 weeks ago" + * + * @param timestamp Unix timestamp in milliseconds + * @returns Humanized relative time string + */ +export function formatRelativeTime(timestamp: number): string { + const now = Date.now(); + const diffMs = now - timestamp; + + // Handle future timestamps + if (diffMs < 0) { + return "just now"; + } + + const seconds = Math.floor(diffMs / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const weeks = Math.floor(days / 7); + const months = Math.floor(days / 30); + const years = Math.floor(days / 365); + + if (seconds < 60) { + return "just now"; + } else if (minutes < 60) { + return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`; + } else if (hours < 24) { + return hours === 1 ? "1 hour ago" : `${hours} hours ago`; + } else if (days < 7) { + return days === 1 ? "1 day ago" : `${days} days ago`; + } else if (weeks < 4) { + return weeks === 1 ? "1 week ago" : `${weeks} weeks ago`; + } else if (months < 12) { + return months === 1 ? "1 month ago" : `${months} months ago`; + } else { + return years === 1 ? "1 year ago" : `${years} years ago`; + } +}