From 58f707b23e73cf74011d26c3218f41c76e7c11f0 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 9 Jan 2026 04:52:22 +0000 Subject: [PATCH 1/2] feat: add configurable timestamps to chat messages Implements Issue #10539 - Adds the ability to display timestamps on chat messages. Features: - 24-hour format (14:34) - Full date for messages from previous days (e.g., "Jan 7, 14:34") - Configurable on/off toggle in UI Settings - Timestamps appear on the right side of header rows - Same styling as other header elements Files changed: - packages/types/src/global-settings.ts: Add showTimestamps setting - src/shared/ExtensionMessage.ts: Add to ExtensionState - webview-ui/src/context/ExtensionStateContext.tsx: Add state and setter - webview-ui/src/utils/formatTimestamp.ts: New timestamp formatting utility - webview-ui/src/components/settings/UISettings.tsx: Add toggle - webview-ui/src/components/settings/SettingsView.tsx: Pass prop - webview-ui/src/components/chat/ChatRow.tsx: Display timestamps - webview-ui/src/i18n/locales/en/settings.json: Translation strings - webview-ui/src/utils/__tests__/formatTimestamp.spec.ts: Tests - webview-ui/src/components/settings/__tests__/UISettings.spec.tsx: Update tests --- packages/types/src/global-settings.ts | 5 ++ src/shared/ExtensionMessage.ts | 1 + webview-ui/src/components/chat/ChatRow.tsx | 14 +++- .../src/components/settings/SettingsView.tsx | 2 + .../src/components/settings/UISettings.tsx | 24 ++++++ .../settings/__tests__/UISettings.spec.tsx | 1 + .../src/context/ExtensionStateContext.tsx | 9 +++ webview-ui/src/i18n/locales/en/settings.json | 4 + .../utils/__tests__/formatTimestamp.spec.ts | 75 +++++++++++++++++++ webview-ui/src/utils/formatTimestamp.ts | 38 ++++++++++ 10 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 webview-ui/src/utils/__tests__/formatTimestamp.spec.ts create mode 100644 webview-ui/src/utils/formatTimestamp.ts diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 9a17834ced7..03987bdd7b4 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -193,6 +193,11 @@ export const globalSettingsSchema = z.object({ * @default "send" */ enterBehavior: z.enum(["send", "newline"]).optional(), + /** + * Whether to show timestamps on chat messages + * @default false + */ + showTimestamps: z.boolean().optional(), profileThresholds: z.record(z.string(), z.number()).optional(), hasOpenedModeSelector: z.boolean().optional(), lastModeExportPath: z.string().optional(), diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 2eec4cb6c88..3043b306c3d 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -286,6 +286,7 @@ export type ExtensionState = Pick< | "includeTaskHistoryInEnhance" | "reasoningBlockCollapsed" | "enterBehavior" + | "showTimestamps" | "includeCurrentTime" | "includeCurrentCost" | "maxGitStatusFiles" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index ef2231b26d1..1c8f75b2873 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -15,6 +15,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { findMatchingResourceOrTemplate } from "@src/utils/mcp" import { vscode } from "@src/utils/vscode" import { formatPathTooltip } from "@src/utils/formatPathTooltip" +import { formatTimestamp } from "@src/utils/formatTimestamp" import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock" @@ -167,7 +168,8 @@ export const ChatRowContent = ({ }: ChatRowContentProps) => { const { t, i18n } = useTranslation() - const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages } = useExtensionState() + const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages, showTimestamps } = + useExtensionState() const { info: model } = useSelectedModel(apiConfiguration) const [isEditing, setIsEditing] = useState(false) const [editedContent, setEditedContent] = useState("") @@ -380,6 +382,13 @@ export const ChatRowContent = ({ wordBreak: "break-word", } + // Timestamp element to be displayed on the right side of headers + const timestampElement = showTimestamps ? ( + + {formatTimestamp(message.ts)} + + ) : null + const tool = useMemo( () => (message.ask === "tool" ? safeJsonParse(message.text) : null), [message.ask, message.text], @@ -1200,6 +1209,7 @@ export const ChatRowContent = ({
{t("chat:text.rooSaid")} + {timestampElement}
@@ -1219,6 +1229,7 @@ export const ChatRowContent = ({
{t("chat:feedback.youSaid")} + {timestampElement}
{icon} {title} + {timestampElement}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 6331f13edf9..91a10f3b875 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -209,6 +209,7 @@ const SettingsView = forwardRef(({ onDone, t openRouterImageGenerationSelectedModel, reasoningBlockCollapsed, enterBehavior, + showTimestamps, includeCurrentTime, includeCurrentCost, maxGitStatusFiles, @@ -830,6 +831,7 @@ const SettingsView = forwardRef(({ onDone, t )} diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index b4e5a4e861a..a97e169420f 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -12,12 +12,14 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext" interface UISettingsProps extends HTMLAttributes { reasoningBlockCollapsed: boolean enterBehavior: "send" | "newline" + showTimestamps: boolean setCachedStateField: SetCachedStateField } export const UISettings = ({ reasoningBlockCollapsed, enterBehavior, + showTimestamps, setCachedStateField, ...props }: UISettingsProps) => { @@ -48,6 +50,15 @@ export const UISettings = ({ }) } + const handleShowTimestampsChange = (value: boolean) => { + setCachedStateField("showTimestamps", value) + + // Track telemetry event + telemetryClient.capture("ui_settings_show_timestamps_changed", { + enabled: value, + }) + } + return (
@@ -86,6 +97,19 @@ export const UISettings = ({ {t("settings:ui.requireCtrlEnterToSend.description", { primaryMod })}
+ + {/* Show Timestamps Setting */} +
+ handleShowTimestampsChange(e.target.checked)} + data-testid="show-timestamps-checkbox"> + {t("settings:ui.showTimestamps.label")} + +
+ {t("settings:ui.showTimestamps.description")} +
+
diff --git a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx index 2a21a410b38..35b1668fa10 100644 --- a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx @@ -6,6 +6,7 @@ describe("UISettings", () => { const defaultProps = { reasoningBlockCollapsed: false, enterBehavior: "send" as const, + showTimestamps: false, setCachedStateField: vi.fn(), } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 3fe5340bdbc..12e8b0036c4 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -163,6 +163,8 @@ export interface ExtensionStateContextType extends ExtensionState { setIncludeCurrentTime: (value: boolean) => void includeCurrentCost?: boolean setIncludeCurrentCost: (value: boolean) => void + showTimestamps?: boolean + setShowTimestamps: (value: boolean) => void } export const ExtensionStateContext = createContext(undefined) @@ -297,6 +299,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const [prevCloudIsAuthenticated, setPrevCloudIsAuthenticated] = useState(false) const [includeCurrentTime, setIncludeCurrentTime] = useState(true) const [includeCurrentCost, setIncludeCurrentCost] = useState(true) + const [showTimestamps, setShowTimestamps] = useState(false) // Default to false (timestamps hidden) const setListApiConfigMeta = useCallback( (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), @@ -342,6 +345,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode if ((newState as any).includeCurrentCost !== undefined) { setIncludeCurrentCost((newState as any).includeCurrentCost) } + // Update showTimestamps if present in state message + if ((newState as any).showTimestamps !== undefined) { + setShowTimestamps((newState as any).showTimestamps) + } // Handle marketplace data if present in state message if (newState.marketplaceItems !== undefined) { setMarketplaceItems(newState.marketplaceItems) @@ -592,6 +599,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setIncludeCurrentTime, includeCurrentCost, setIncludeCurrentCost, + showTimestamps, + setShowTimestamps, } return {children} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index b836ecbfc87..d87c9826bf6 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -68,6 +68,10 @@ "requireCtrlEnterToSend": { "label": "Require {{primaryMod}}+Enter to send messages", "description": "When enabled, you must press {{primaryMod}}+Enter to send messages instead of just Enter" + }, + "showTimestamps": { + "label": "Show timestamps on messages", + "description": "When enabled, timestamps will be displayed on the right side of message headers" } }, "prompts": { diff --git a/webview-ui/src/utils/__tests__/formatTimestamp.spec.ts b/webview-ui/src/utils/__tests__/formatTimestamp.spec.ts new file mode 100644 index 00000000000..8b6b60825e2 --- /dev/null +++ b/webview-ui/src/utils/__tests__/formatTimestamp.spec.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { formatTimestamp } from "../formatTimestamp" + +describe("formatTimestamp", () => { + beforeEach(() => { + // Mock current date to 2026-01-09 14:30:00 + vi.useFakeTimers() + vi.setSystemTime(new Date("2026-01-09T14:30:00.000Z")) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it("formats today's time in 24-hour format", () => { + // Same day at 10:15 + const timestamp = new Date("2026-01-09T10:15:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("10:15") + }) + + it("pads single-digit hours and minutes", () => { + const timestamp = new Date("2026-01-09T09:05:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("09:05") + }) + + it("includes date for messages from previous days", () => { + // Previous day + const timestamp = new Date("2026-01-08T14:34:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("Jan 8, 14:34") + }) + + it("includes date for messages from previous months", () => { + // Previous month + const timestamp = new Date("2025-12-25T09:00:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("Dec 25, 09:00") + }) + + it("includes date for messages from previous years", () => { + // Previous year + const timestamp = new Date("2025-06-15T18:45:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("Jun 15, 18:45") + }) + + it("handles midnight correctly", () => { + const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("00:00") + }) + + it("handles end of day correctly", () => { + const timestamp = new Date("2026-01-09T23:59:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("23:59") + }) + + it("correctly abbreviates all months", () => { + const months = [ + { date: "2025-01-15", expected: "Jan" }, + { date: "2025-02-15", expected: "Feb" }, + { date: "2025-03-15", expected: "Mar" }, + { date: "2025-04-15", expected: "Apr" }, + { date: "2025-05-15", expected: "May" }, + { date: "2025-06-15", expected: "Jun" }, + { date: "2025-07-15", expected: "Jul" }, + { date: "2025-08-15", expected: "Aug" }, + { date: "2025-09-15", expected: "Sep" }, + { date: "2025-10-15", expected: "Oct" }, + { date: "2025-11-15", expected: "Nov" }, + { date: "2025-12-15", expected: "Dec" }, + ] + + months.forEach(({ date, expected }) => { + const timestamp = new Date(`${date}T12:00:00.000Z`).getTime() + expect(formatTimestamp(timestamp)).toContain(expected) + }) + }) +}) diff --git a/webview-ui/src/utils/formatTimestamp.ts b/webview-ui/src/utils/formatTimestamp.ts new file mode 100644 index 00000000000..51a12b29d55 --- /dev/null +++ b/webview-ui/src/utils/formatTimestamp.ts @@ -0,0 +1,38 @@ +/** + * Formats a Unix timestamp (in milliseconds) to a human-readable time string. + * + * Requirements from Issue #10539: + * - 24-hour format (14:34) + * - Full date for messages from previous days (e.g., "Jan 7, 14:34") + * - Text-size same as header row text + * + * @param ts - Unix timestamp in milliseconds + * @returns Formatted time string + */ +export function formatTimestamp(ts: number): string { + const date = new Date(ts) + const now = new Date() + + // Check if the message is from today + const isToday = + date.getDate() === now.getDate() && + date.getMonth() === now.getMonth() && + date.getFullYear() === now.getFullYear() + + // Format hours and minutes in 24-hour format + const hours = date.getHours().toString().padStart(2, "0") + const minutes = date.getMinutes().toString().padStart(2, "0") + const time = `${hours}:${minutes}` + + if (isToday) { + // Just show time for today's messages + return time + } + + // For older messages, show abbreviated month, day, and time + const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + const month = months[date.getMonth()] + const day = date.getDate() + + return `${month} ${day}, ${time}` +} From 8c81d0c92c76e4b0dd7f399201e7adf3324e4b75 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 14 Jan 2026 22:18:49 +0000 Subject: [PATCH 2/2] feat: add 12-hour/24-hour timestamp format toggle option - Add timestampFormat setting to global-settings.ts with enum type - Update formatTimestamp.ts to accept format parameter (12hour/24hour) - Add comprehensive tests for 12-hour format (AM/PM display) - Add time format dropdown in UI Settings when timestamps are enabled - Update i18n translations for the new setting Resolves: Issue #10539 - Request for 12hr/24hr time format toggle --- packages/types/src/global-settings.ts | 5 + src/shared/ExtensionMessage.ts | 1 + webview-ui/src/components/chat/ChatRow.tsx | 16 +- .../src/components/settings/SettingsView.tsx | 4 + .../src/components/settings/UISettings.tsx | 38 +++- .../settings/__tests__/UISettings.spec.tsx | 1 + .../src/context/ExtensionStateContext.tsx | 9 + webview-ui/src/i18n/locales/en/settings.json | 8 + .../utils/__tests__/formatTimestamp.spec.ts | 164 ++++++++++++------ webview-ui/src/utils/formatTimestamp.ts | 37 +++- 10 files changed, 222 insertions(+), 61 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 03987bdd7b4..4a578937524 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -198,6 +198,11 @@ export const globalSettingsSchema = z.object({ * @default false */ showTimestamps: z.boolean().optional(), + /** + * Format for displaying timestamps on chat messages + * @default "24hour" + */ + timestampFormat: z.enum(["12hour", "24hour"]).optional(), profileThresholds: z.record(z.string(), z.number()).optional(), hasOpenedModeSelector: z.boolean().optional(), lastModeExportPath: z.string().optional(), diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 3043b306c3d..940cbaec430 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -287,6 +287,7 @@ export type ExtensionState = Pick< | "reasoningBlockCollapsed" | "enterBehavior" | "showTimestamps" + | "timestampFormat" | "includeCurrentTime" | "includeCurrentCost" | "maxGitStatusFiles" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 1c8f75b2873..4d61c6f4583 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -15,7 +15,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { findMatchingResourceOrTemplate } from "@src/utils/mcp" import { vscode } from "@src/utils/vscode" import { formatPathTooltip } from "@src/utils/formatPathTooltip" -import { formatTimestamp } from "@src/utils/formatTimestamp" +import { formatTimestamp, TimestampFormat } from "@src/utils/formatTimestamp" import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock" @@ -168,8 +168,16 @@ export const ChatRowContent = ({ }: ChatRowContentProps) => { const { t, i18n } = useTranslation() - const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages, showTimestamps } = - useExtensionState() + const { + mcpServers, + alwaysAllowMcp, + currentCheckpoint, + mode, + apiConfiguration, + clineMessages, + showTimestamps, + timestampFormat, + } = useExtensionState() const { info: model } = useSelectedModel(apiConfiguration) const [isEditing, setIsEditing] = useState(false) const [editedContent, setEditedContent] = useState("") @@ -385,7 +393,7 @@ export const ChatRowContent = ({ // Timestamp element to be displayed on the right side of headers const timestampElement = showTimestamps ? ( - {formatTimestamp(message.ts)} + {formatTimestamp(message.ts, (timestampFormat ?? "24hour") as TimestampFormat)} ) : null diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 91a10f3b875..c98d98f899f 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -210,6 +210,7 @@ const SettingsView = forwardRef(({ onDone, t reasoningBlockCollapsed, enterBehavior, showTimestamps, + timestampFormat, includeCurrentTime, includeCurrentCost, maxGitStatusFiles, @@ -412,6 +413,8 @@ const SettingsView = forwardRef(({ onDone, t includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true, reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, enterBehavior: enterBehavior ?? "send", + showTimestamps: showTimestamps ?? false, + timestampFormat: timestampFormat ?? "24hour", includeCurrentTime: includeCurrentTime ?? true, includeCurrentCost: includeCurrentCost ?? true, maxGitStatusFiles: maxGitStatusFiles ?? 0, @@ -832,6 +835,7 @@ const SettingsView = forwardRef(({ onDone, t reasoningBlockCollapsed={reasoningBlockCollapsed ?? true} enterBehavior={enterBehavior ?? "send"} showTimestamps={showTimestamps ?? false} + timestampFormat={timestampFormat ?? "24hour"} setCachedStateField={setCachedStateField} /> )} diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index a97e169420f..5a67ffb9618 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -1,6 +1,6 @@ import { HTMLAttributes, useMemo } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" -import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react" import { Glasses } from "lucide-react" import { telemetryClient } from "@/utils/TelemetryClient" @@ -13,6 +13,7 @@ interface UISettingsProps extends HTMLAttributes { reasoningBlockCollapsed: boolean enterBehavior: "send" | "newline" showTimestamps: boolean + timestampFormat: "12hour" | "24hour" setCachedStateField: SetCachedStateField } @@ -20,6 +21,7 @@ export const UISettings = ({ reasoningBlockCollapsed, enterBehavior, showTimestamps, + timestampFormat, setCachedStateField, ...props }: UISettingsProps) => { @@ -59,6 +61,15 @@ export const UISettings = ({ }) } + const handleTimestampFormatChange = (format: "12hour" | "24hour") => { + setCachedStateField("timestampFormat", format) + + // Track telemetry event + telemetryClient.capture("ui_settings_timestamp_format_changed", { + format, + }) + } + return (
@@ -110,6 +121,31 @@ export const UISettings = ({ {t("settings:ui.showTimestamps.description")}
+ + {/* Timestamp Format Setting - only visible when timestamps are enabled */} + {showTimestamps && ( +
+
+ {t("settings:ui.timestampFormat.label")} + + handleTimestampFormatChange(e.target.value as "12hour" | "24hour") + } + data-testid="timestamp-format-dropdown"> + + {t("settings:ui.timestampFormat.options.24hour")} + + + {t("settings:ui.timestampFormat.options.12hour")} + + +
+
+ {t("settings:ui.timestampFormat.description")} +
+
+ )} diff --git a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx index 35b1668fa10..bd302196883 100644 --- a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx @@ -7,6 +7,7 @@ describe("UISettings", () => { reasoningBlockCollapsed: false, enterBehavior: "send" as const, showTimestamps: false, + timestampFormat: "24hour" as const, setCachedStateField: vi.fn(), } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 12e8b0036c4..85a13449573 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -165,6 +165,8 @@ export interface ExtensionStateContextType extends ExtensionState { setIncludeCurrentCost: (value: boolean) => void showTimestamps?: boolean setShowTimestamps: (value: boolean) => void + timestampFormat?: "12hour" | "24hour" + setTimestampFormat: (value: "12hour" | "24hour") => void } export const ExtensionStateContext = createContext(undefined) @@ -300,6 +302,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const [includeCurrentTime, setIncludeCurrentTime] = useState(true) const [includeCurrentCost, setIncludeCurrentCost] = useState(true) const [showTimestamps, setShowTimestamps] = useState(false) // Default to false (timestamps hidden) + const [timestampFormat, setTimestampFormat] = useState<"12hour" | "24hour">("24hour") // Default to 24-hour format const setListApiConfigMeta = useCallback( (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), @@ -349,6 +352,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode if ((newState as any).showTimestamps !== undefined) { setShowTimestamps((newState as any).showTimestamps) } + // Update timestampFormat if present in state message + if ((newState as any).timestampFormat !== undefined) { + setTimestampFormat((newState as any).timestampFormat) + } // Handle marketplace data if present in state message if (newState.marketplaceItems !== undefined) { setMarketplaceItems(newState.marketplaceItems) @@ -601,6 +608,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setIncludeCurrentCost, showTimestamps, setShowTimestamps, + timestampFormat, + setTimestampFormat, } return {children} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index d87c9826bf6..47d8ed73024 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -72,6 +72,14 @@ "showTimestamps": { "label": "Show timestamps on messages", "description": "When enabled, timestamps will be displayed on the right side of message headers" + }, + "timestampFormat": { + "label": "Time format", + "description": "Choose how timestamps are displayed", + "options": { + "12hour": "12-hour (2:34 PM)", + "24hour": "24-hour (14:34)" + } } }, "prompts": { diff --git a/webview-ui/src/utils/__tests__/formatTimestamp.spec.ts b/webview-ui/src/utils/__tests__/formatTimestamp.spec.ts index 8b6b60825e2..9e815e7b6bc 100644 --- a/webview-ui/src/utils/__tests__/formatTimestamp.spec.ts +++ b/webview-ui/src/utils/__tests__/formatTimestamp.spec.ts @@ -12,64 +12,130 @@ describe("formatTimestamp", () => { vi.useRealTimers() }) - it("formats today's time in 24-hour format", () => { - // Same day at 10:15 - const timestamp = new Date("2026-01-09T10:15:00.000Z").getTime() - expect(formatTimestamp(timestamp)).toBe("10:15") - }) + describe("24-hour format (default)", () => { + it("formats today's time in 24-hour format", () => { + // Same day at 10:15 + const timestamp = new Date("2026-01-09T10:15:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("10:15") + }) - it("pads single-digit hours and minutes", () => { - const timestamp = new Date("2026-01-09T09:05:00.000Z").getTime() - expect(formatTimestamp(timestamp)).toBe("09:05") - }) + it("pads single-digit hours and minutes", () => { + const timestamp = new Date("2026-01-09T09:05:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("09:05") + }) - it("includes date for messages from previous days", () => { - // Previous day - const timestamp = new Date("2026-01-08T14:34:00.000Z").getTime() - expect(formatTimestamp(timestamp)).toBe("Jan 8, 14:34") - }) + it("includes date for messages from previous days", () => { + // Previous day + const timestamp = new Date("2026-01-08T14:34:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("Jan 8, 14:34") + }) - it("includes date for messages from previous months", () => { - // Previous month - const timestamp = new Date("2025-12-25T09:00:00.000Z").getTime() - expect(formatTimestamp(timestamp)).toBe("Dec 25, 09:00") - }) + it("includes date for messages from previous months", () => { + // Previous month + const timestamp = new Date("2025-12-25T09:00:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("Dec 25, 09:00") + }) - it("includes date for messages from previous years", () => { - // Previous year - const timestamp = new Date("2025-06-15T18:45:00.000Z").getTime() - expect(formatTimestamp(timestamp)).toBe("Jun 15, 18:45") - }) + it("includes date for messages from previous years", () => { + // Previous year + const timestamp = new Date("2025-06-15T18:45:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("Jun 15, 18:45") + }) - it("handles midnight correctly", () => { - const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime() - expect(formatTimestamp(timestamp)).toBe("00:00") + it("handles midnight correctly", () => { + const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("00:00") + }) + + it("handles end of day correctly", () => { + const timestamp = new Date("2026-01-09T23:59:00.000Z").getTime() + expect(formatTimestamp(timestamp)).toBe("23:59") + }) + + it("correctly abbreviates all months", () => { + const months = [ + { date: "2025-01-15", expected: "Jan" }, + { date: "2025-02-15", expected: "Feb" }, + { date: "2025-03-15", expected: "Mar" }, + { date: "2025-04-15", expected: "Apr" }, + { date: "2025-05-15", expected: "May" }, + { date: "2025-06-15", expected: "Jun" }, + { date: "2025-07-15", expected: "Jul" }, + { date: "2025-08-15", expected: "Aug" }, + { date: "2025-09-15", expected: "Sep" }, + { date: "2025-10-15", expected: "Oct" }, + { date: "2025-11-15", expected: "Nov" }, + { date: "2025-12-15", expected: "Dec" }, + ] + + months.forEach(({ date, expected }) => { + const timestamp = new Date(`${date}T12:00:00.000Z`).getTime() + expect(formatTimestamp(timestamp)).toContain(expected) + }) + }) }) - it("handles end of day correctly", () => { - const timestamp = new Date("2026-01-09T23:59:00.000Z").getTime() - expect(formatTimestamp(timestamp)).toBe("23:59") + describe("12-hour format", () => { + it("formats morning time with AM", () => { + const timestamp = new Date("2026-01-09T10:15:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("10:15 AM") + }) + + it("formats afternoon time with PM", () => { + const timestamp = new Date("2026-01-09T14:30:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("2:30 PM") + }) + + it("formats midnight as 12:00 AM", () => { + const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("12:00 AM") + }) + + it("formats noon as 12:00 PM", () => { + const timestamp = new Date("2026-01-09T12:00:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("12:00 PM") + }) + + it("formats end of day correctly", () => { + const timestamp = new Date("2026-01-09T23:59:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("11:59 PM") + }) + + it("includes date for messages from previous days with 12-hour format", () => { + const timestamp = new Date("2026-01-08T14:34:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("Jan 8, 2:34 PM") + }) + + it("includes date for messages from previous months with 12-hour format", () => { + const timestamp = new Date("2025-12-25T09:00:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("Dec 25, 9:00 AM") + }) + + it("handles single-digit hours without padding", () => { + const timestamp = new Date("2026-01-09T09:05:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("9:05 AM") + }) + + it("formats 1 AM correctly", () => { + const timestamp = new Date("2026-01-09T01:00:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("1:00 AM") + }) + + it("formats 1 PM correctly", () => { + const timestamp = new Date("2026-01-09T13:00:00.000Z").getTime() + expect(formatTimestamp(timestamp, "12hour")).toBe("1:00 PM") + }) }) - it("correctly abbreviates all months", () => { - const months = [ - { date: "2025-01-15", expected: "Jan" }, - { date: "2025-02-15", expected: "Feb" }, - { date: "2025-03-15", expected: "Mar" }, - { date: "2025-04-15", expected: "Apr" }, - { date: "2025-05-15", expected: "May" }, - { date: "2025-06-15", expected: "Jun" }, - { date: "2025-07-15", expected: "Jul" }, - { date: "2025-08-15", expected: "Aug" }, - { date: "2025-09-15", expected: "Sep" }, - { date: "2025-10-15", expected: "Oct" }, - { date: "2025-11-15", expected: "Nov" }, - { date: "2025-12-15", expected: "Dec" }, - ] - - months.forEach(({ date, expected }) => { - const timestamp = new Date(`${date}T12:00:00.000Z`).getTime() - expect(formatTimestamp(timestamp)).toContain(expected) + describe("explicit 24-hour format parameter", () => { + it("formats time same as default when explicitly set to 24hour", () => { + const timestamp = new Date("2026-01-09T14:30:00.000Z").getTime() + expect(formatTimestamp(timestamp, "24hour")).toBe("14:30") + }) + + it("formats midnight correctly with explicit 24-hour format", () => { + const timestamp = new Date("2026-01-09T00:00:00.000Z").getTime() + expect(formatTimestamp(timestamp, "24hour")).toBe("00:00") }) }) }) diff --git a/webview-ui/src/utils/formatTimestamp.ts b/webview-ui/src/utils/formatTimestamp.ts index 51a12b29d55..9ddf68dd619 100644 --- a/webview-ui/src/utils/formatTimestamp.ts +++ b/webview-ui/src/utils/formatTimestamp.ts @@ -1,15 +1,18 @@ +export type TimestampFormat = "12hour" | "24hour" + /** * Formats a Unix timestamp (in milliseconds) to a human-readable time string. * * Requirements from Issue #10539: - * - 24-hour format (14:34) - * - Full date for messages from previous days (e.g., "Jan 7, 14:34") + * - Configurable 12-hour (2:34 PM) or 24-hour (14:34) format + * - Full date for messages from previous days (e.g., "Jan 7, 14:34" or "Jan 7, 2:34 PM") * - Text-size same as header row text * * @param ts - Unix timestamp in milliseconds + * @param format - Time format: "12hour" for AM/PM, "24hour" for 24-hour format (default: "24hour") * @returns Formatted time string */ -export function formatTimestamp(ts: number): string { +export function formatTimestamp(ts: number, format: TimestampFormat = "24hour"): string { const date = new Date(ts) const now = new Date() @@ -19,10 +22,8 @@ export function formatTimestamp(ts: number): string { date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear() - // Format hours and minutes in 24-hour format - const hours = date.getHours().toString().padStart(2, "0") - const minutes = date.getMinutes().toString().padStart(2, "0") - const time = `${hours}:${minutes}` + // Format the time based on the selected format + const time = formatTime(date, format) if (isToday) { // Just show time for today's messages @@ -36,3 +37,25 @@ export function formatTimestamp(ts: number): string { return `${month} ${day}, ${time}` } + +/** + * Formats just the time portion of a date. + * + * @param date - Date object to format + * @param format - Time format: "12hour" for AM/PM, "24hour" for 24-hour format + * @returns Formatted time string + */ +function formatTime(date: Date, format: TimestampFormat): string { + const hours24 = date.getHours() + const minutes = date.getMinutes().toString().padStart(2, "0") + + if (format === "12hour") { + const hours12 = hours24 % 12 || 12 // Convert 0 to 12 for midnight + const period = hours24 < 12 ? "AM" : "PM" + return `${hours12}:${minutes} ${period}` + } + + // 24-hour format + const hours = hours24.toString().padStart(2, "0") + return `${hours}:${minutes}` +}