diff --git a/packages/ui/src/components/chat/ChatMessage.tsx b/packages/ui/src/components/chat/ChatMessage.tsx index b7d0ce7e..d75ef36c 100644 --- a/packages/ui/src/components/chat/ChatMessage.tsx +++ b/packages/ui/src/components/chat/ChatMessage.tsx @@ -856,6 +856,7 @@ const ChatMessage: React.FC = ({ isUser={isUser} isMessageCompleted={isMessageCompleted} messageFinish={messageFinish} + messageCompletedAt={messageCompletedAt ?? undefined} syntaxTheme={syntaxTheme} isMobile={isMobile} hasTouchInput={hasTouchInput} diff --git a/packages/ui/src/components/chat/hooks/useTurnGrouping.ts b/packages/ui/src/components/chat/hooks/useTurnGrouping.ts index 5088629f..1eebf877 100644 --- a/packages/ui/src/components/chat/hooks/useTurnGrouping.ts +++ b/packages/ui/src/components/chat/hooks/useTurnGrouping.ts @@ -51,6 +51,7 @@ export interface TurnGroupingContext { hasTools: boolean; hasReasoning: boolean; diffStats?: TurnDiffStats; + userMessageCreatedAt?: number; isWorking: boolean; isGroupExpanded: boolean; @@ -488,6 +489,9 @@ export const useTurnGrouping = (messages: ChatMessageEntry[]): UseTurnGroupingRe const uiState = getOrCreateTurnState(turn.turnId); const isTurnWorking = sessionIsWorking && lastTurnId === turn.turnId; + const userTimeInfo = turn.userMessage.info.time as { created?: number } | undefined; + const userMessageCreatedAt = typeof userTimeInfo?.created === 'number' ? userTimeInfo.created : undefined; + return { turnId: turn.turnId, isFirstAssistantInTurn, @@ -499,6 +503,7 @@ export const useTurnGrouping = (messages: ChatMessageEntry[]): UseTurnGroupingRe hasTools, hasReasoning, diffStats, + userMessageCreatedAt, isWorking: isTurnWorking, isGroupExpanded: uiState.isExpanded, previewedPartIds: uiState.previewedPartIds, diff --git a/packages/ui/src/components/chat/message/MessageBody.tsx b/packages/ui/src/components/chat/message/MessageBody.tsx index d49609a5..9419c925 100644 --- a/packages/ui/src/components/chat/message/MessageBody.tsx +++ b/packages/ui/src/components/chat/message/MessageBody.tsx @@ -16,7 +16,7 @@ import { isEmptyTextPart, extractTextContent } from './partUtils'; import { FadeInOnReveal } from './FadeInOnReveal'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { RiCheckLine, RiFileCopyLine, RiChatNewLine, RiArrowGoBackLine, RiGitBranchLine } from '@remixicon/react'; +import { RiCheckLine, RiFileCopyLine, RiChatNewLine, RiArrowGoBackLine, RiGitBranchLine, RiHourglassLine } from '@remixicon/react'; import { ArrowsMerge } from '@/components/icons/ArrowsMerge'; import type { ContentChangeReason } from '@/hooks/useChatScrollManager'; @@ -27,6 +27,16 @@ import { useUIStore } from '@/stores/useUIStore'; import { flattenAssistantTextParts } from '@/lib/messages/messageText'; import { MULTIRUN_EXECUTION_FORK_PROMPT_META_TEXT } from '@/lib/messages/executionMeta'; +const formatTurnDuration = (durationMs: number): string => { + const totalSeconds = durationMs / 1000; + if (totalSeconds < 60) { + return `${totalSeconds.toFixed(1)}s`; + } + const minutes = Math.floor(totalSeconds / 60); + const seconds = Math.round(totalSeconds % 60); + return `${minutes}m ${seconds}s`; +}; + const useMigrationTimer = ( turnGroupingContext: TurnGroupingContext | undefined, @@ -110,6 +120,7 @@ interface MessageBodyProps { isUser: boolean; isMessageCompleted: boolean; messageFinish?: string; + messageCompletedAt?: number; syntaxTheme: { [key: string]: React.CSSProperties }; @@ -327,6 +338,7 @@ const AssistantMessageBody: React.FC> = ({ parts, isMessageCompleted, messageFinish, + messageCompletedAt, syntaxTheme, isMobile, @@ -1099,6 +1111,14 @@ const AssistantMessageBody: React.FC> = ({ const shouldShowFooter = isLastAssistantInTurn && hasTextContent && (hasStopFinish || Boolean(errorMessage)); const [isSummaryHovered, setIsSummaryHovered] = React.useState(false); + const turnDurationText = React.useMemo(() => { + if (!isLastAssistantInTurn || !hasStopFinish) return undefined; + const userCreatedAt = turnGroupingContext?.userMessageCreatedAt; + if (typeof userCreatedAt !== 'number' || typeof messageCompletedAt !== 'number') return undefined; + if (messageCompletedAt <= userCreatedAt) return undefined; + return formatTurnDuration(messageCompletedAt - userCreatedAt); + }, [isLastAssistantInTurn, hasStopFinish, turnGroupingContext?.userMessageCreatedAt, messageCompletedAt]); + const footerButtons = ( <> @@ -1208,13 +1228,21 @@ const AssistantMessageBody: React.FC> = ({ > {shouldShowFooter && ( -
- {footerButtons} +
+ {turnDurationText ? ( + + + {turnDurationText} + + ) : } +
+ {footerButtons} +
)}
@@ -1223,8 +1251,16 @@ const AssistantMessageBody: React.FC> = ({ {!showSummaryBody && shouldShowFooter && ( -
- {footerButtons} +
+ {turnDurationText ? ( + + + {turnDurationText} + + ) : } +
+ {footerButtons} +
)}