From 895d1ffa9a6276fa128515b013bce1d7c8e77f97 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 4 Mar 2026 20:08:52 +0200 Subject: [PATCH] feat: add optional activity header timestamps in chat settings Add a new Chat checkbox to toggle timestamps for tool, reasoning, and justification headers (default off). Keep assistant message footer timestamps visible and switch to a more readable compact date format. Preserve hover timestamp overlays with chat-surface background when the new setting is enabled. --- .../components/chat/message/MessageBody.tsx | 38 ++++++++----------- .../chat/message/parts/ReasoningPart.tsx | 20 +++++----- .../chat/message/parts/ToolPart.tsx | 12 +++--- .../src/components/chat/message/timeFormat.ts | 24 +++++++++--- .../sections/openchamber/OpenChamberPage.tsx | 4 +- .../openchamber/OpenChamberVisualSettings.tsx | 28 +++++++++++++- packages/ui/src/stores/useUIStore.ts | 8 ++++ 7 files changed, 86 insertions(+), 48 deletions(-) diff --git a/packages/ui/src/components/chat/message/MessageBody.tsx b/packages/ui/src/components/chat/message/MessageBody.tsx index d02a3681..6309194d 100644 --- a/packages/ui/src/components/chat/message/MessageBody.tsx +++ b/packages/ui/src/components/chat/message/MessageBody.tsx @@ -1295,6 +1295,8 @@ const AssistantMessageBody: React.FC> = ({ return formatted.length > 0 ? formatted : null; }, [messageCompletedAt, messageCreatedAt]); + const footerTimestampClassName = 'text-sm text-muted-foreground/60 tabular-nums flex items-center gap-1'; + const footerButtons = ( <> {onCopyMessage && ( @@ -1462,17 +1464,13 @@ const AssistantMessageBody: React.FC> = ({ ) : null} {footerTimestamp ? ( - - - - - - - {footerTimestamp} - + + + {footerTimestamp} + ) : null} @@ -1495,17 +1493,13 @@ const AssistantMessageBody: React.FC> = ({ ) : null} {footerTimestamp ? ( - - - - - - - {footerTimestamp} - + + + {footerTimestamp} + ) : null} diff --git a/packages/ui/src/components/chat/message/parts/ReasoningPart.tsx b/packages/ui/src/components/chat/message/parts/ReasoningPart.tsx index 0e74274b..c232a4c4 100644 --- a/packages/ui/src/components/chat/message/parts/ReasoningPart.tsx +++ b/packages/ui/src/components/chat/message/parts/ReasoningPart.tsx @@ -83,9 +83,6 @@ const LiveDuration: React.FC<{ start: number; end?: number; active: boolean }> = return <>{formatDuration(start, end, now)}; }; -// TODO: Re-enable reasoning/justification header timestamps after hover UX is redesigned. -const ENABLE_REASONING_HEADER_TIMESTAMPS = false; - type ReasoningTimelineBlockProps = { text: string; variant: ReasoningVariant; @@ -103,6 +100,7 @@ export const ReasoningTimelineBlock: React.FC = ({ }) => { const [isExpanded, setIsExpanded] = React.useState(false); const isMobile = useUIStore((state) => state.isMobile); + const showActivityHeaderTimestamps = useUIStore((state) => state.showActivityHeaderTimestamps); const summary = React.useMemo(() => getReasoningSummary(text), [text]); const { label, Icon } = variantConfig[variant]; @@ -166,21 +164,21 @@ export const ReasoningTimelineBlock: React.FC = ({ {typeof timeStart === 'number' ? ( + className={cn( + 'text-muted-foreground/80 transition-opacity duration-150', + !isMobile && endedTimestampText && showActivityHeaderTimestamps && 'group-hover/tool:opacity-0' + )} + > - {!isMobile && endedTimestampText && ENABLE_REASONING_HEADER_TIMESTAMPS ? ( + {!isMobile && endedTimestampText && showActivityHeaderTimestamps ? ( @@ -189,7 +187,7 @@ export const ReasoningTimelineBlock: React.FC = ({ ) : null} ) : null} - {typeof timeStart !== 'number' && !isMobile && endedTimestampText && ENABLE_REASONING_HEADER_TIMESTAMPS ? ( + {typeof timeStart !== 'number' && !isMobile && endedTimestampText && showActivityHeaderTimestamps ? ( {endedTimestampText} diff --git a/packages/ui/src/components/chat/message/parts/ToolPart.tsx b/packages/ui/src/components/chat/message/parts/ToolPart.tsx index 54956962..8d49c781 100644 --- a/packages/ui/src/components/chat/message/parts/ToolPart.tsx +++ b/packages/ui/src/components/chat/message/parts/ToolPart.tsx @@ -141,9 +141,6 @@ const LiveDuration: React.FC<{ start: number; end?: number; active: boolean }> = return <>{formatDuration(start, end, now)}; }; -// TODO: Re-enable tool header timestamp display after hover UX is redesigned. -const ENABLE_TOOL_HEADER_TIMESTAMPS = false; - const parseDiffStats = (metadata?: Record): { added: number; removed: number } | null => { if (!metadata?.diff || typeof metadata.diff !== 'string') return null; @@ -1543,6 +1540,7 @@ const ToolPart: React.FC = ({ }) => { const state = part.state; const currentDirectory = useDirectoryStore((s) => s.currentDirectory); + const showActivityHeaderTimestamps = useUIStore((store) => store.showActivityHeaderTimestamps); const isTaskTool = part.tool.toLowerCase() === 'task'; @@ -1877,7 +1875,7 @@ const ToolPart: React.FC = ({ = ({ active={Boolean(isActive && typeof effectiveTimeEnd !== 'number')} /> - {!isMobile && endedTimestampText && ENABLE_TOOL_HEADER_TIMESTAMPS ? ( + {!isMobile && endedTimestampText && showActivityHeaderTimestamps ? ( @@ -1898,7 +1896,7 @@ const ToolPart: React.FC = ({ ) : null} ) : null} - {typeof effectiveTimeStart !== 'number' && !isMobile && endedTimestampText && ENABLE_TOOL_HEADER_TIMESTAMPS ? ( + {typeof effectiveTimeStart !== 'number' && !isMobile && endedTimestampText && showActivityHeaderTimestamps ? ( {endedTimestampText} diff --git a/packages/ui/src/components/chat/message/timeFormat.ts b/packages/ui/src/components/chat/message/timeFormat.ts index f4dbf496..bd92f08e 100644 --- a/packages/ui/src/components/chat/message/timeFormat.ts +++ b/packages/ui/src/components/chat/message/timeFormat.ts @@ -8,6 +8,12 @@ const isSameDay = (left: Date, right: Date): boolean => { ); }; +const isYesterday = (date: Date, now: Date): boolean => { + const yesterday = new Date(now); + yesterday.setDate(now.getDate() - 1); + return isSameDay(date, yesterday); +}; + const isValidTimestamp = (timestamp: number): boolean => { return Number.isFinite(timestamp) && !Number.isNaN(new Date(timestamp).getTime()); }; @@ -20,15 +26,23 @@ export const formatTimestampForDisplay = (timestamp: number): string => { const date = new Date(timestamp); const now = new Date(); - const timePart = `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`; + const timePart = `${pad2(date.getHours())}:${pad2(date.getMinutes())}`; if (isSameDay(date, now)) { return timePart; } - const yearPart = String(date.getFullYear()).slice(-2); - const monthPart = pad2(date.getMonth() + 1); - const dayPart = pad2(date.getDate()); + if (isYesterday(date, now)) { + return `Yesterday ${timePart}`; + } - return `${yearPart}-${monthPart}-${dayPart} ${timePart}`; + const monthPart = date.toLocaleString(undefined, { month: 'short' }); + const dayPart = date.getDate(); + const datePart = `${monthPart} ${dayPart}`; + + if (date.getFullYear() === now.getFullYear()) { + return `${datePart}, ${timePart}`; + } + + return `${datePart}, ${date.getFullYear()}, ${timePart}`; }; diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx index 0a5f394f..f1ff9147 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberPage.tsx @@ -115,9 +115,9 @@ const VisualSectionContent: React.FC = () => { ]} />; }; -// Chat section: Default Tool Output, User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Queue mode, Persist draft +// Chat section: Default Tool Output, User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Justification activity, Activity header timestamps, Queue mode, Persist draft const ChatSectionContent: React.FC = () => { - return ; + return ; }; // Sessions section: Default model & agent, Session retention, Memory limits diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 1515730a..ab59330d 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -124,7 +124,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' return mode === 'markdown' ? 'markdown' : 'plain'; }; -export type VisibleSetting = 'theme' | 'pwaInstallName' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'cornerRadius' | 'inputBarOffset' | 'navRail' | 'toolOutput' | 'mermaidRendering' | 'userMessageRendering' | 'stickyUserHeader' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'queueMode' | 'textJustificationActivity' | 'terminalQuickKeys' | 'persistDraft'; +export type VisibleSetting = 'theme' | 'pwaInstallName' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'cornerRadius' | 'inputBarOffset' | 'navRail' | 'toolOutput' | 'mermaidRendering' | 'userMessageRendering' | 'stickyUserHeader' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'queueMode' | 'textJustificationActivity' | 'activityHeaderTimestamps' | 'terminalQuickKeys' | 'persistDraft'; interface OpenChamberVisualSettingsProps { /** Which settings to show. If undefined, shows all. */ @@ -139,6 +139,8 @@ export const OpenChamberVisualSettings: React.FC const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces); const showTextJustificationActivity = useUIStore(state => state.showTextJustificationActivity); const setShowTextJustificationActivity = useUIStore(state => state.setShowTextJustificationActivity); + const showActivityHeaderTimestamps = useUIStore(state => state.showActivityHeaderTimestamps); + const setShowActivityHeaderTimestamps = useUIStore(state => state.setShowActivityHeaderTimestamps); const toolCallExpansion = useUIStore(state => state.toolCallExpansion); const setToolCallExpansion = useUIStore(state => state.setToolCallExpansion); const mermaidRenderingMode = useUIStore(state => state.mermaidRenderingMode); @@ -242,6 +244,7 @@ export const OpenChamberVisualSettings: React.FC || shouldShow('reasoning') || shouldShow('queueMode') || shouldShow('textJustificationActivity') + || shouldShow('activityHeaderTimestamps') || shouldShow('persistDraft'); const selectedToolExpansionOption = TOOL_EXPANSION_OPTIONS.find((option) => option.value === toolCallExpansion); @@ -1056,6 +1059,29 @@ export const OpenChamberVisualSettings: React.FC Show Justification Activity )} + + {shouldShow('activityHeaderTimestamps') && ( +
setShowActivityHeaderTimestamps(!showActivityHeaderTimestamps)} + onKeyDown={(event) => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + setShowActivityHeaderTimestamps(!showActivityHeaderTimestamps); + } + }} + > + + Show Activity Header Timestamps +
+ )} )} diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 377b3340..673c4985 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -498,6 +498,7 @@ interface UIStore { eventStreamHint: string | null; showReasoningTraces: boolean; showTextJustificationActivity: boolean; + showActivityHeaderTimestamps: boolean; showDeletionDialog: boolean; autoDeleteEnabled: boolean; autoDeleteAfterDays: number; @@ -613,6 +614,7 @@ interface UIStore { setEventStreamStatus: (status: EventStreamStatus, hint?: string | null) => void; setShowReasoningTraces: (value: boolean) => void; setShowTextJustificationActivity: (value: boolean) => void; + setShowActivityHeaderTimestamps: (value: boolean) => void; setShowDeletionDialog: (value: boolean) => void; setAutoDeleteEnabled: (value: boolean) => void; setAutoDeleteAfterDays: (days: number) => void; @@ -721,6 +723,7 @@ export const useUIStore = create()( eventStreamHint: null, showReasoningTraces: true, showTextJustificationActivity: false, + showActivityHeaderTimestamps: false, showDeletionDialog: true, autoDeleteEnabled: false, autoDeleteAfterDays: 30, @@ -1293,6 +1296,10 @@ export const useUIStore = create()( set({ showTextJustificationActivity: value }); }, + setShowActivityHeaderTimestamps: (value) => { + set({ showActivityHeaderTimestamps: value }); + }, + setShowDeletionDialog: (value) => { set({ showDeletionDialog: value }); }, @@ -1814,6 +1821,7 @@ export const useUIStore = create()( // Note: isSettingsDialogOpen intentionally NOT persisted showReasoningTraces: state.showReasoningTraces, showTextJustificationActivity: state.showTextJustificationActivity, + showActivityHeaderTimestamps: state.showActivityHeaderTimestamps, showDeletionDialog: state.showDeletionDialog, autoDeleteEnabled: state.autoDeleteEnabled, autoDeleteAfterDays: state.autoDeleteAfterDays,