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
This commit is contained in:
Roo Code 2026-01-14 22:18:49 +00:00
parent 58f707b23e
commit 8c81d0c92c
10 changed files with 226 additions and 65 deletions

View file

@ -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(),

View file

@ -287,6 +287,7 @@ export type ExtensionState = Pick<
| "reasoningBlockCollapsed"
| "enterBehavior"
| "showTimestamps"
| "timestampFormat"
| "includeCurrentTime"
| "includeCurrentCost"
| "maxGitStatusFiles"

View file

@ -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 ? (
<span className="text-vscode-descriptionForeground ml-auto shrink-0" style={{ fontWeight: "normal" }}>
{formatTimestamp(message.ts)}
{formatTimestamp(message.ts, (timestampFormat ?? "24hour") as TimestampFormat)}
</span>
) : null

View file

@ -210,6 +210,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
reasoningBlockCollapsed,
enterBehavior,
showTimestamps,
timestampFormat,
includeCurrentTime,
includeCurrentCost,
maxGitStatusFiles,
@ -412,6 +413,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone, t
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
enterBehavior={enterBehavior ?? "send"}
showTimestamps={showTimestamps ?? false}
timestampFormat={timestampFormat ?? "24hour"}
setCachedStateField={setCachedStateField}
/>
)}

View file

@ -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<HTMLDivElement> {
reasoningBlockCollapsed: boolean
enterBehavior: "send" | "newline"
showTimestamps: boolean
timestampFormat: "12hour" | "24hour"
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
}
@ -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 (
<div {...props}>
<SectionHeader>
@ -110,6 +121,31 @@ export const UISettings = ({
{t("settings:ui.showTimestamps.description")}
</div>
</div>
{/* Timestamp Format Setting - only visible when timestamps are enabled */}
{showTimestamps && (
<div className="flex flex-col gap-1 ml-5">
<div className="flex items-center gap-2">
<span className="font-medium">{t("settings:ui.timestampFormat.label")}</span>
<VSCodeDropdown
value={timestampFormat}
onChange={(e: any) =>
handleTimestampFormatChange(e.target.value as "12hour" | "24hour")
}
data-testid="timestamp-format-dropdown">
<VSCodeOption value="24hour">
{t("settings:ui.timestampFormat.options.24hour")}
</VSCodeOption>
<VSCodeOption value="12hour">
{t("settings:ui.timestampFormat.options.12hour")}
</VSCodeOption>
</VSCodeDropdown>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:ui.timestampFormat.description")}
</div>
</div>
)}
</div>
</Section>
</div>

View file

@ -7,6 +7,7 @@ describe("UISettings", () => {
reasoningBlockCollapsed: false,
enterBehavior: "send" as const,
showTimestamps: false,
timestampFormat: "24hour" as const,
setCachedStateField: vi.fn(),
}

View file

@ -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<ExtensionStateContextType | undefined>(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 <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

View file

@ -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": {

View file

@ -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("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)
})
})
})
it("pads single-digit hours and minutes", () => {
const timestamp = new Date("2026-01-09T09:05:00.000Z").getTime()
expect(formatTimestamp(timestamp)).toBe("09:05")
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("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")
})
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("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)
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")
})
})
})

View file

@ -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}`
}