Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions src/ui/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>(false);
const turnStartedAtRef = useRef<number | null>(null);
const [lastTurnDuration, setLastTurnDuration] = useState<number | null>(
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.`,
});
}
}
}
}, [
Expand All @@ -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 ? (
<div className="agent-client-turn-meta">
* Took {formatDuration(lastTurnDuration)}
</div>
) : null;

// ============================================================
// Effects - System Notification on Permission Request
// ============================================================
Expand Down Expand Up @@ -1095,6 +1129,7 @@ export function ChatPanel({
<div className="agent-client-floating-content">
<div className="agent-client-floating-messages-container">
{messageListElement}
{turnMetaElement}
</div>
{inputAreaElement}
</div>
Expand All @@ -1112,6 +1147,7 @@ export function ChatPanel({
{headerElement}
{cwdBanner}
{messageListElement}
{turnMetaElement}
{inputAreaElement}
</div>
);
Expand Down
23 changes: 23 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading