From 58f707b23e73cf74011d26c3218f41c76e7c11f0 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 9 Jan 2026 04:52:22 +0000 Subject: [PATCH] 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}` +}