feat(dashboard): add working indicator for active sessions
Visual feedback when an agent is actively processing:
1. **Spinner on status dots** (SessionCard.js, Modal.js)
- Status dot gets a spinning ring animation when session is active/starting
- Uses CSS border trick with transparent borders except top
2. **Working indicator in chat** (ChatMessages.js, Modal.js)
- Shows at bottom of conversation when agent is working
- Bouncing dots animation ("...") next to "Agent is working" text
- Only visible for active/starting statuses
3. **CSS animations** (styles.css)
- spin-ring: 0.8s rotation for the status dot border
- bounce-dot: staggered vertical bounce for the working dots
4. **Status metadata** (status.js)
- Added `spinning: true` flag for active and starting statuses
- Used by components to conditionally render spinner elements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { html, useRef, useEffect } from '../lib/preact.js';
|
||||
import { getUserMessageBg } from '../utils/status.js';
|
||||
import { getUserMessageBg, getStatusMeta } from '../utils/status.js';
|
||||
import { MessageBubble, filterDisplayMessages } from './MessageBubble.js';
|
||||
|
||||
export function ChatMessages({ messages, status }) {
|
||||
const statusMeta = getStatusMeta(status);
|
||||
const containerRef = useRef(null);
|
||||
const userBgClass = getUserMessageBg(status);
|
||||
const wasAtBottomRef = useRef(true);
|
||||
@@ -56,15 +57,25 @@ export function ChatMessages({ messages, status }) {
|
||||
const displayMessages = filterDisplayMessages(messages).slice(-20);
|
||||
|
||||
return html`
|
||||
<div ref=${containerRef} class="h-full space-y-2.5 overflow-y-auto overflow-x-hidden pr-0.5">
|
||||
${displayMessages.map((msg, i) => html`
|
||||
<${MessageBubble}
|
||||
key=${i}
|
||||
msg=${msg}
|
||||
userBg=${userBgClass}
|
||||
compact=${true}
|
||||
/>
|
||||
`)}
|
||||
<div class="flex h-full flex-col">
|
||||
<div ref=${containerRef} class="flex-1 space-y-2.5 overflow-y-auto overflow-x-hidden pr-0.5">
|
||||
${displayMessages.map((msg, i) => html`
|
||||
<${MessageBubble}
|
||||
key=${i}
|
||||
msg=${msg}
|
||||
userBg=${userBgClass}
|
||||
compact=${true}
|
||||
/>
|
||||
`)}
|
||||
</div>
|
||||
${statusMeta.spinning && html`
|
||||
<div class="shrink-0 flex items-center gap-2 pt-2.5 pb-0.5 pl-1">
|
||||
<span class="h-2 w-2 rounded-full spinner-dot" style=${{ backgroundColor: statusMeta.borderColor, color: statusMeta.borderColor }}></span>
|
||||
<span class="working-dots font-mono text-xs" style=${{ color: statusMeta.borderColor }}>
|
||||
<span>.</span><span>.</span><span>.</span>
|
||||
</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ export function Modal({ session, conversations, conversationLoading, onClose, on
|
||||
<div class="mb-1 flex items-center gap-3">
|
||||
<h2 class="truncate font-display text-xl font-semibold text-bright">${session.project || session.name || session.session_id}</h2>
|
||||
<div class="flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs ${status.badge}">
|
||||
<span class="h-2 w-2 rounded-full ${status.dot}"></span>
|
||||
<span class="h-2 w-2 rounded-full ${status.dot} ${status.spinning ? 'spinner-dot' : ''}" style=${{ color: status.borderColor }}></span>
|
||||
<span class="font-mono uppercase tracking-[0.14em]">${status.label}</span>
|
||||
</div>
|
||||
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${agent === 'codex' ? 'border-emerald-400/45 bg-emerald-500/14 text-emerald-300' : 'border-violet-400/45 bg-violet-500/14 text-violet-300'}">
|
||||
@@ -178,6 +178,15 @@ export function Modal({ session, conversations, conversationLoading, onClose, on
|
||||
<p class="text-dim text-center py-12">No conversation messages</p>
|
||||
`}
|
||||
</div>
|
||||
${status.spinning && html`
|
||||
<div class="shrink-0 flex items-center gap-2 border-t border-selection/40 bg-surface px-5 py-2">
|
||||
<span class="h-2 w-2 rounded-full spinner-dot" style=${{ backgroundColor: status.borderColor, color: status.borderColor }}></span>
|
||||
<span class="font-mono text-xs" style=${{ color: status.borderColor }}>Agent is working</span>
|
||||
<span class="working-dots font-mono text-xs" style=${{ color: status.borderColor }}>
|
||||
<span>.</span><span>.</span><span>.</span>
|
||||
</span>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="border-t border-selection/70 bg-bg/55 p-4">
|
||||
|
||||
@@ -35,7 +35,7 @@ export function SessionCard({ session, onClick, conversation, onFetchConversatio
|
||||
<div class="flex items-start justify-between gap-2.5">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="h-2 w-2 shrink-0 rounded-full ${statusMeta.dot}"></span>
|
||||
<span class="h-2 w-2 shrink-0 rounded-full ${statusMeta.dot} ${statusMeta.spinning ? 'spinner-dot' : ''}" style=${{ color: statusMeta.borderColor }}></span>
|
||||
<span class="truncate font-display text-base font-medium text-bright">${session.project || session.name || 'Session'}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -67,6 +67,36 @@ body {
|
||||
animation: pulse-attention 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Active session spinner */
|
||||
@keyframes spin-ring {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner-dot {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.spinner-dot::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
animation: spin-ring 0.8s linear infinite;
|
||||
}
|
||||
|
||||
/* Working indicator at bottom of chat */
|
||||
@keyframes bounce-dot {
|
||||
0%, 80%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-4px); }
|
||||
}
|
||||
|
||||
.working-dots span:nth-child(1) { animation: bounce-dot 1.2s ease-in-out infinite; }
|
||||
.working-dots span:nth-child(2) { animation: bounce-dot 1.2s ease-in-out 0.15s infinite; }
|
||||
.working-dots span:nth-child(3) { animation: bounce-dot 1.2s ease-in-out 0.3s infinite; }
|
||||
|
||||
/* Glass panel effect */
|
||||
.glass-panel {
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
@@ -22,6 +22,7 @@ export function getStatusMeta(status) {
|
||||
dot: 'bg-active',
|
||||
badge: 'bg-active/18 text-active border-active/40',
|
||||
borderColor: '#5fd0a4',
|
||||
spinning: true,
|
||||
};
|
||||
case 'starting':
|
||||
return {
|
||||
@@ -29,6 +30,7 @@ export function getStatusMeta(status) {
|
||||
dot: 'bg-starting',
|
||||
badge: 'bg-starting/18 text-starting border-starting/40',
|
||||
borderColor: '#7cb2ff',
|
||||
spinning: true,
|
||||
};
|
||||
case 'done':
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user