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.
This commit is contained in:
@@ -1295,6 +1295,8 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
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<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
</span>
|
||||
) : null}
|
||||
{footerTimestamp ? (
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="text-sm text-muted-foreground/60 tabular-nums flex items-center"
|
||||
aria-label={`Message time: ${footerTimestamp}`}
|
||||
>
|
||||
<RiTimeLine className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={6}>{footerTimestamp}</TooltipContent>
|
||||
</Tooltip>
|
||||
<span
|
||||
className={footerTimestampClassName}
|
||||
aria-label={`Message time: ${footerTimestamp}`}
|
||||
>
|
||||
<RiTimeLine className="h-3.5 w-3.5" />
|
||||
{footerTimestamp}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1495,17 +1493,13 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
</span>
|
||||
) : null}
|
||||
{footerTimestamp ? (
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="text-sm text-muted-foreground/60 tabular-nums flex items-center"
|
||||
aria-label={`Message time: ${footerTimestamp}`}
|
||||
>
|
||||
<RiTimeLine className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={6}>{footerTimestamp}</TooltipContent>
|
||||
</Tooltip>
|
||||
<span
|
||||
className={footerTimestampClassName}
|
||||
aria-label={`Message time: ${footerTimestamp}`}
|
||||
>
|
||||
<RiTimeLine className="h-3.5 w-3.5" />
|
||||
{footerTimestamp}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<ReasoningTimelineBlockProps> = ({
|
||||
}) => {
|
||||
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<ReasoningTimelineBlockProps> = ({
|
||||
{typeof timeStart === 'number' ? (
|
||||
<span className="relative flex-shrink-0 tabular-nums text-right">
|
||||
<span
|
||||
className={cn(
|
||||
'text-muted-foreground/80 transition-opacity duration-150',
|
||||
!isMobile && endedTimestampText && ENABLE_REASONING_HEADER_TIMESTAMPS && 'group-hover/tool:opacity-0'
|
||||
)}
|
||||
>
|
||||
className={cn(
|
||||
'text-muted-foreground/80 transition-opacity duration-150',
|
||||
!isMobile && endedTimestampText && showActivityHeaderTimestamps && 'group-hover/tool:opacity-0'
|
||||
)}
|
||||
>
|
||||
<LiveDuration
|
||||
start={timeStart}
|
||||
end={timeEnd}
|
||||
active={typeof timeEnd !== 'number'}
|
||||
/>
|
||||
</span>
|
||||
{!isMobile && endedTimestampText && ENABLE_REASONING_HEADER_TIMESTAMPS ? (
|
||||
{!isMobile && endedTimestampText && showActivityHeaderTimestamps ? (
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none absolute right-0 top-0 whitespace-nowrap text-muted-foreground/70 transition-opacity duration-150',
|
||||
'pointer-events-none absolute right-0 top-0 z-10 whitespace-nowrap rounded-sm bg-[var(--surface-background)] px-1 text-muted-foreground/70 transition-opacity duration-150',
|
||||
'opacity-0 group-hover/tool:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -189,7 +187,7 @@ export const ReasoningTimelineBlock: React.FC<ReasoningTimelineBlockProps> = ({
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
{typeof timeStart !== 'number' && !isMobile && endedTimestampText && ENABLE_REASONING_HEADER_TIMESTAMPS ? (
|
||||
{typeof timeStart !== 'number' && !isMobile && endedTimestampText && showActivityHeaderTimestamps ? (
|
||||
<span className="text-muted-foreground/70 flex-shrink-0 tabular-nums">
|
||||
{endedTimestampText}
|
||||
</span>
|
||||
|
||||
@@ -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<string, unknown>): { added: number; removed: number } | null => {
|
||||
if (!metadata?.diff || typeof metadata.diff !== 'string') return null;
|
||||
|
||||
@@ -1543,6 +1540,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
}) => {
|
||||
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<ToolPartProps> = ({
|
||||
<span
|
||||
className={cn(
|
||||
'text-muted-foreground/80 transition-opacity duration-150',
|
||||
!isMobile && endedTimestampText && ENABLE_TOOL_HEADER_TIMESTAMPS && 'group-hover/tool:opacity-0'
|
||||
!isMobile && endedTimestampText && showActivityHeaderTimestamps && 'group-hover/tool:opacity-0'
|
||||
)}
|
||||
>
|
||||
<LiveDuration
|
||||
@@ -1886,10 +1884,10 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
active={Boolean(isActive && typeof effectiveTimeEnd !== 'number')}
|
||||
/>
|
||||
</span>
|
||||
{!isMobile && endedTimestampText && ENABLE_TOOL_HEADER_TIMESTAMPS ? (
|
||||
{!isMobile && endedTimestampText && showActivityHeaderTimestamps ? (
|
||||
<span
|
||||
className={cn(
|
||||
'pointer-events-none absolute right-0 top-0 whitespace-nowrap text-muted-foreground/70 transition-opacity duration-150',
|
||||
'pointer-events-none absolute right-0 top-0 z-10 whitespace-nowrap rounded-sm bg-[var(--surface-background)] px-1 text-muted-foreground/70 transition-opacity duration-150',
|
||||
'opacity-0 group-hover/tool:opacity-100'
|
||||
)}
|
||||
>
|
||||
@@ -1898,7 +1896,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
{typeof effectiveTimeStart !== 'number' && !isMobile && endedTimestampText && ENABLE_TOOL_HEADER_TIMESTAMPS ? (
|
||||
{typeof effectiveTimeStart !== 'number' && !isMobile && endedTimestampText && showActivityHeaderTimestamps ? (
|
||||
<span className="ml-auto text-muted-foreground/70 flex-shrink-0 tabular-nums">
|
||||
{endedTimestampText}
|
||||
</span>
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user