mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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
This commit is contained in:
parent
9ac449919e
commit
58f707b23e
10 changed files with 172 additions and 1 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ export type ExtensionState = Pick<
|
|||
| "includeTaskHistoryInEnhance"
|
||||
| "reasoningBlockCollapsed"
|
||||
| "enterBehavior"
|
||||
| "showTimestamps"
|
||||
| "includeCurrentTime"
|
||||
| "includeCurrentCost"
|
||||
| "maxGitStatusFiles"
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<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],
|
||||
|
|
@ -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} />
|
||||
|
|
@ -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(
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
openRouterImageGenerationSelectedModel,
|
||||
reasoningBlockCollapsed,
|
||||
enterBehavior,
|
||||
showTimestamps,
|
||||
includeCurrentTime,
|
||||
includeCurrentCost,
|
||||
maxGitStatusFiles,
|
||||
|
|
@ -830,6 +831,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
<UISettings
|
||||
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
|
||||
enterBehavior={enterBehavior ?? "send"}
|
||||
showTimestamps={showTimestamps ?? false}
|
||||
setCachedStateField={setCachedStateField}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ describe("UISettings", () => {
|
|||
const defaultProps = {
|
||||
reasoningBlockCollapsed: false,
|
||||
enterBehavior: "send" as const,
|
||||
showTimestamps: false,
|
||||
setCachedStateField: vi.fn(),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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 <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
75
webview-ui/src/utils/__tests__/formatTimestamp.spec.ts
Normal file
75
webview-ui/src/utils/__tests__/formatTimestamp.spec.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
38
webview-ui/src/utils/formatTimestamp.ts
Normal file
38
webview-ui/src/utils/formatTimestamp.ts
Normal file
|
|
@ -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…
Add table
Reference in a new issue