({
message,
isStreaming,
toolResults,
modelNames,
cwd,
onOpenFile,
showTimestamp,
prevTimestamp,
sessionId,
entryId,
}: {
message: AssistantMessage;
isStreaming?: boolean;
toolResults?: Map<string, ToolResultMessage>;
modelNames?: Record<string, string>;
cwd?: string;
onOpenFile?: (filePath: string) => void;
showTimestamp?: boolean;
prevTimestamp?: number;
sessionId?: string;
entryId?: string;
})
| 337 | } |
| 338 | |
| 339 | function AssistantMessageView({ |
| 340 | message, |
| 341 | isStreaming, |
| 342 | toolResults, |
| 343 | modelNames, |
| 344 | cwd, |
| 345 | onOpenFile, |
| 346 | showTimestamp, |
| 347 | prevTimestamp, |
| 348 | sessionId, |
| 349 | entryId, |
| 350 | }: { |
| 351 | message: AssistantMessage; |
| 352 | isStreaming?: boolean; |
| 353 | toolResults?: Map<string, ToolResultMessage>; |
| 354 | modelNames?: Record<string, string>; |
| 355 | cwd?: string; |
| 356 | onOpenFile?: (filePath: string) => void; |
| 357 | showTimestamp?: boolean; |
| 358 | prevTimestamp?: number; |
| 359 | sessionId?: string; |
| 360 | entryId?: string; |
| 361 | }) { |
| 362 | const time = showTimestamp ? formatTime(message.timestamp) : null; |
| 363 | const blockItems = (message.content ?? []) |
| 364 | .map((block, originalIndex) => ({ block, originalIndex })) |
| 365 | .filter(({ block }) => !isEmptyThinkingBlock(block, { isStreaming })); |
| 366 | const blocks = blockItems.map(({ block }) => block); |
| 367 | const [hovered, setHovered] = useState(false); |
| 368 | const [copied, setCopied] = useState(false); |
| 369 | const streamStartRef = useRef<number | null>(null); |
| 370 | const [tps, setTps] = useState<number | null>(null); |
| 371 | const blockItemsRef = useRef(blockItems); |
| 372 | blockItemsRef.current = blockItems; |
| 373 | |
| 374 | // Streaming-based timing for thinking blocks |
| 375 | const blockStartTimesRef = useRef<Map<number, number>>(new Map()); |
| 376 | const [streamingDurations, setStreamingDurations] = useState<Map<number, number>>(new Map()); |
| 377 | |
| 378 | // Thinking duration derived from file timestamps: time from prev message end to this message end |
| 379 | // This is the total generation time (thinking + any text before first tool call) |
| 380 | const thinkingDurationFromFile = useMemo<number | undefined>(() => { |
| 381 | if (!message.timestamp || !prevTimestamp) return undefined; |
| 382 | const secs = Math.round((message.timestamp - prevTimestamp) / 1000); |
| 383 | return secs > 0 ? secs : undefined; |
| 384 | }, [message.timestamp, prevTimestamp]); |
| 385 | |
| 386 | // Tool call durations derived from session file timestamps (accurate for completed messages) |
| 387 | // assistant message timestamp = when generation ended = when tools started running |
| 388 | // toolResult timestamp = when tool execution finished |
| 389 | const toolCallDurations = useMemo<Map<string, number>>(() => { |
| 390 | const map = new Map<string, number>(); |
| 391 | if (!toolResults || !message.timestamp) return map; |
| 392 | for (const [callId, result] of toolResults) { |
| 393 | if (result.timestamp && message.timestamp) { |
| 394 | const secs = Math.round((result.timestamp - message.timestamp) / 1000); |
| 395 | if (secs > 0) map.set(callId, secs); |
| 396 | } |
nothing calls this directly
no test coverage detected