diff --git a/src/ui/ChatPanel.tsx b/src/ui/ChatPanel.tsx index 58a36520..8dd80ecd 100644 --- a/src/ui/ChatPanel.tsx +++ b/src/ui/ChatPanel.tsx @@ -681,31 +681,49 @@ export function ChatPanel({ }, [isSessionReady, session.agentInfo, logger]); // ============================================================ - // Effects - Save Session Messages on Turn End + // Effects - Save Session Messages on Turn End + track turn duration // ============================================================ const prevIsSendingRef = useRef(false); + const turnStartedAtRef = useRef(null); + const [lastTurnDuration, setLastTurnDuration] = useState( + null, + ); useEffect(() => { const wasSending = prevIsSendingRef.current; prevIsSendingRef.current = isSending; - // Save when turn ends (isSending: true -> false) and has messages - if ( - wasSending && - !isSending && - session.sessionId && - messages.length > 0 - ) { - sessionHistory.saveSessionMessages(session.sessionId, messages); - logger.log( - `[ChatPanel] Session messages saved: ${session.sessionId}`, - ); + // Turn START — stamp the clock and clear the previous footer. + if (!wasSending && isSending) { + turnStartedAtRef.current = Date.now(); + setLastTurnDuration(null); + } - // System notification on response completion - if (settings.enableSystemNotifications && !document.hasFocus()) { - new Notification("Agent Client", { - body: `${activeAgentLabel} has completed the response.`, - }); + // Turn END — save messages, compute duration, and surface the + // "* Took N.Ns" footer line. + if (wasSending && !isSending) { + if (turnStartedAtRef.current != null) { + setLastTurnDuration(Date.now() - turnStartedAtRef.current); + turnStartedAtRef.current = null; + } + if (session.sessionId && messages.length > 0) { + sessionHistory.saveSessionMessages( + session.sessionId, + messages, + ); + logger.log( + `[ChatPanel] Session messages saved: ${session.sessionId}`, + ); + + // System notification on response completion + if ( + settings.enableSystemNotifications && + !document.hasFocus() + ) { + new Notification("Agent Client", { + body: `${activeAgentLabel} has completed the response.`, + }); + } } } }, [ @@ -718,6 +736,22 @@ export function ChatPanel({ logger, ]); + // Format duration as "0.4s" (sub-second), "12s" (whole seconds), or + // "1m 23s" (minute+). Avoids "0s" for very fast turns. + const formatDuration = (ms: number): string => { + if (ms < 1000) return `${(ms / 1000).toFixed(1)}s`; + const totalSec = Math.round(ms / 1000); + if (totalSec < 60) return `${totalSec}s`; + return `${Math.floor(totalSec / 60)}m ${totalSec % 60}s`; + }; + + const turnMetaElement = + !isSending && lastTurnDuration != null && lastTurnDuration >= 100 ? ( +
+ * Took {formatDuration(lastTurnDuration)} +
+ ) : null; + // ============================================================ // Effects - System Notification on Permission Request // ============================================================ @@ -1095,6 +1129,7 @@ export function ChatPanel({
{messageListElement} + {turnMetaElement}
{inputAreaElement}
@@ -1112,6 +1147,7 @@ export function ChatPanel({ {headerElement} {cwdBanner} {messageListElement} + {turnMetaElement} {inputAreaElement} ); diff --git a/styles.css b/styles.css index 3342040d..b3181dfe 100644 --- a/styles.css +++ b/styles.css @@ -661,6 +661,29 @@ If your plugin does not need CSS, delete this file. margin-top: 20px; } +/* Turn meta — italic muted "* Took N.Ns" line that appears once a turn + completes. Personality moment matching Claudian's "* Crunched for 32s" + convention. */ +.agent-client-turn-meta { + margin-top: 8px; + font-style: italic; + font-size: 12px; + color: var(--text-muted); + user-select: none; + animation: agent-client-turn-meta-fade-in 0.18s ease; +} + +@keyframes agent-client-turn-meta-fade-in { + from { + opacity: 0; + transform: translateY(-2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + /* Input area */ .agent-client-chat-input-container { flex-shrink: 0;