feat: show response generation time on assistant messages

Track how long each assistant response took to generate and display
it subtly next to the timestamp (e.g. '· 12.3s'). The timing is
measured from the first streaming delta to the final state, and
preserved across the history reload that follows stream completion.

Only visible for messages generated during the current session.
This commit is contained in:
Nicolas Varrot
2026-02-13 15:46:29 +00:00
parent d31374baf7
commit 622aa107f8
3 changed files with 31 additions and 0 deletions

View File

@@ -539,6 +539,13 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message
</span> </span>
)} )}
{message.timestamp && <Timestamp ts={message.timestamp} />} {message.timestamp && <Timestamp ts={message.timestamp} />}
{!isUser && message.generationTimeMs != null && (
<span className="text-[10px] text-pc-text-faint" title="Generation time">
· {message.generationTimeMs < 1000
? `${message.generationTimeMs}ms`
: `${(message.generationTimeMs / 1000).toFixed(1)}s`}
</span>
)}
{isUser && message.sendStatus === 'sending' && ( {isUser && message.sendStatus === 'sending' && (
<span title="Sending..."><Clock size={10} className="animate-pulse text-pc-text-faint" /></span> <span title="Sending..."><Clock size={10} className="animate-pulse text-pc-text-faint" /></span>
)} )}

View File

@@ -54,6 +54,8 @@ export function useGateway() {
const [activeSessions, setActiveSessions] = useState<Set<string>>(new Set()); const [activeSessions, setActiveSessions] = useState<Set<string>>(new Set());
const [unreadSessions, setUnreadSessions] = useState<Set<string>>(new Set()); const [unreadSessions, setUnreadSessions] = useState<Set<string>>(new Set());
const [agentIdentity, setAgentIdentity] = useState<AgentIdentity | null>(null); const [agentIdentity, setAgentIdentity] = useState<AgentIdentity | null>(null);
/** Map of runId → generation duration (ms), preserved across loadHistory reloads */
const generationTimesRef = useRef<Map<string, number>>(new Map());
const handleAgentEvent = useCallback((payload: JsonPayload) => { const handleAgentEvent = useCallback((payload: JsonPayload) => {
if (payload?.stream !== 'tool') return; if (payload?.stream !== 'tool') return;
@@ -228,6 +230,18 @@ export function useGateway() {
merged.push(msg); merged.push(msg);
} }
} }
// Apply stored generation time to the last assistant message if available
const genKey = sessionKey + ':latest';
const genTime = generationTimesRef.current.get(genKey);
if (genTime) {
generationTimesRef.current.delete(genKey);
for (let i = merged.length - 1; i >= 0; i--) {
if (merged[i].role === 'assistant') {
merged[i] = { ...merged[i], generationTimeMs: genTime };
break;
}
}
}
setMessages(merged); setMessages(merged);
} }
} catch { } catch {
@@ -342,10 +356,16 @@ export function useGateway() {
blocks, blocks,
isStreaming: true, isStreaming: true,
runId, runId,
streamStartedAt: Date.now(),
}; };
return [...prev, msg]; return [...prev, msg];
}); });
} else if (state === 'final') { } else if (state === 'final') {
// Compute generation time from the streaming message before history reload replaces it
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
if (lastMsg?.role === 'assistant' && lastMsg.streamStartedAt) {
generationTimesRef.current.set(activeSessionRef.current + ':latest', Date.now() - lastMsg.streamStartedAt);
}
currentRunIdRef.current = null; currentRunIdRef.current = null;
setIsGenerating(false); setIsGenerating(false);
loadHistory(activeSessionRef.current); loadHistory(activeSessionRef.current);

View File

@@ -10,6 +10,10 @@ export interface ChatMessage {
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
/** Optimistic send status for user messages */ /** Optimistic send status for user messages */
sendStatus?: 'sending' | 'sent' | 'error'; sendStatus?: 'sending' | 'sent' | 'error';
/** Timestamp (ms) when streaming started for this message */
streamStartedAt?: number;
/** Total generation time in milliseconds (set when streaming ends) */
generationTimeMs?: number;
} }
export type MessageBlock = export type MessageBlock =