Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ export type ExtensionState = Pick<
| "includeTaskHistoryInEnhance"
| "reasoningBlockCollapsed"
| "enterBehavior"
| "showTimestamps"
| "includeCurrentTime"
| "includeCurrentCost"
| "maxGitStatusFiles"
Expand Down
14 changes: 13 additions & 1 deletion webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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("")
Expand Down Expand Up @@ -380,6 +382,13 @@ export const ChatRowContent = ({
wordBreak: "break-word",
}

// 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)}
</span>
) : null

const tool = useMemo(
() => (message.ask === "tool" ? safeJsonParse<ClineSayTool>(message.text) : null),
[message.ask, message.text],
Expand Down Expand Up @@ -1200,6 +1209,7 @@ export const ChatRowContent = ({
<div style={headerStyle}>
<MessageCircle className="w-4 shrink-0" aria-label="Speech bubble icon" />
<span style={{ fontWeight: "bold" }}>{t("chat:text.rooSaid")}</span>
{timestampElement}
</div>
<div className="pl-6">
<Markdown markdown={message.text} partial={message.partial} />
Expand All @@ -1219,6 +1229,7 @@ export const ChatRowContent = ({
<div style={headerStyle}>
<User className="w-4 shrink-0" aria-label="User icon" />
<span style={{ fontWeight: "bold" }}>{t("chat:feedback.youSaid")}</span>
{timestampElement}
</div>
<div
className={cn(
Expand Down Expand Up @@ -1336,6 +1347,7 @@ export const ChatRowContent = ({
<div style={headerStyle}>
{icon}
{title}
{timestampElement}
</div>
<div className="border-l border-green-600/30 ml-2 pl-4 pb-1">
<Markdown markdown={message.text} />
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
openRouterImageGenerationSelectedModel,
reasoningBlockCollapsed,
enterBehavior,
showTimestamps,
includeCurrentTime,
includeCurrentCost,
maxGitStatusFiles,
Expand Down Expand Up @@ -830,6 +831,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
<UISettings
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
enterBehavior={enterBehavior ?? "send"}
showTimestamps={showTimestamps ?? false}
setCachedStateField={setCachedStateField}
/>
)}
Expand Down
24 changes: 24 additions & 0 deletions webview-ui/src/components/settings/UISettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
reasoningBlockCollapsed: boolean
enterBehavior: "send" | "newline"
showTimestamps: boolean
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
}

export const UISettings = ({
reasoningBlockCollapsed,
enterBehavior,
showTimestamps,
setCachedStateField,
...props
}: UISettingsProps) => {
Expand Down Expand Up @@ -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 (
<div {...props}>
<SectionHeader>
Expand Down Expand Up @@ -86,6 +97,19 @@ export const UISettings = ({
{t("settings:ui.requireCtrlEnterToSend.description", { primaryMod })}
</div>
</div>

{/* Show Timestamps Setting */}
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={showTimestamps}
onChange={(e: any) => handleShowTimestampsChange(e.target.checked)}
data-testid="show-timestamps-checkbox">
<span className="font-medium">{t("settings:ui.showTimestamps.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.showTimestamps.description")}
</div>
</div>
</div>
</Section>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe("UISettings", () => {
const defaultProps = {
reasoningBlockCollapsed: false,
enterBehavior: "send" as const,
showTimestamps: false,
setCachedStateField: vi.fn(),
}

Expand Down
9 changes: 9 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExtensionStateContextType | undefined>(undefined)
Expand Down Expand Up @@ -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 })),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -592,6 +599,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setIncludeCurrentTime,
includeCurrentCost,
setIncludeCurrentCost,
showTimestamps,
setShowTimestamps,
}

return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
75 changes: 75 additions & 0 deletions webview-ui/src/utils/__tests__/formatTimestamp.spec.ts
Original file line number Diff line number Diff line change
@@ -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"))
})
Comment on lines +5 to +9
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are timezone-dependent and could fail in non-UTC environments. The mock time and test timestamps are specified in UTC, but formatTimestamp uses local time methods (getHours(), getDate()). In UTC+10, for example, 14:30 UTC becomes Jan 10, 00:30 local while 10:15 UTC becomes Jan 9, 20:15 local, making them different days and causing the "today's time" test to fail. Consider using getUTC*() methods in the implementation for consistent test behavior, or restructure tests to use timestamps that remain on the same calendar day across all timezones.

Fix it with Roo Code or mention @roomote and request a fix.


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)
})
})
})
38 changes: 38 additions & 0 deletions webview-ui/src/utils/formatTimestamp.ts
Original file line number Diff line number Diff line change
@@ -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}`
}
Loading