Files
amc/dashboard/components/AgentActivityIndicator.js
teernisse fa1fe8613a feat(dashboard): wire skills autocomplete configuration to session cards
Connect the skills enumeration API to session card input fields for
slash command autocomplete:

App.js:
- Add skillsConfig state for Claude and Codex skill configs
- Fetch skills for both agent types on mount using Promise.all
- Pass agent-appropriate autocompleteConfig to each SessionCard

SessionCard.js:
- Accept autocompleteConfig prop and forward to SimpleInput
- Move context usage display from header to footer status bar for
  better information hierarchy (activity indicator + context together)

SimpleInput.js:
- Fix autocomplete dropdown padding (py-2 -> py-1.5)
- Fix font inheritance (add font-mono to skill name)
- Fix description tooltip whitespace handling (add font-sans,
  whitespace-normal)

SpawnModal.js:
- Add SPAWN_TIMEOUT_MS (2x default) to handle pending spawn registry
  wait time plus session file confirmation polling

AgentActivityIndicator.js:
- Minor styling refinement for status display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-28 00:48:26 -05:00

116 lines
3.8 KiB
JavaScript

import { html, useState, useEffect, useRef } from '../lib/preact.js';
/**
* Shows live agent activity: elapsed time since user prompt, token usage.
* Visible when session is active/starting, pauses during needs_attention,
* shows final duration when done.
*
* @param {object} session - Session object with turn_started_at, turn_paused_at, turn_paused_ms, status
*/
export function AgentActivityIndicator({ session }) {
const [elapsed, setElapsed] = useState(0);
const intervalRef = useRef(null);
// Safely extract session fields (handles null/undefined session)
const status = session?.status;
const turn_started_at = session?.turn_started_at;
const turn_paused_at = session?.turn_paused_at;
const turn_paused_ms = session?.turn_paused_ms ?? 0;
const last_event_at = session?.last_event_at;
const context_usage = session?.context_usage;
const turn_start_tokens = session?.turn_start_tokens;
// Only show for sessions with turn timing
const hasTurnTiming = !!turn_started_at;
const isActive = status === 'active' || status === 'starting';
const isPaused = status === 'needs_attention';
const isDone = status === 'done';
useEffect(() => {
if (!hasTurnTiming) return;
const calculate = () => {
const startMs = new Date(turn_started_at).getTime();
const pausedMs = turn_paused_ms || 0;
if (isActive) {
// Running: current time - start - paused
return Date.now() - startMs - pausedMs;
} else if (isPaused && turn_paused_at) {
// Paused: frozen at pause time
const pausedAtMs = new Date(turn_paused_at).getTime();
return pausedAtMs - startMs - pausedMs;
} else if (isDone && last_event_at) {
// Done: final duration
const endMs = new Date(last_event_at).getTime();
return endMs - startMs - pausedMs;
}
return 0;
};
setElapsed(calculate());
// Only tick while active
if (isActive) {
intervalRef.current = setInterval(() => {
setElapsed(calculate());
}, 1000);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [hasTurnTiming, isActive, isPaused, isDone, turn_started_at, turn_paused_at, turn_paused_ms, last_event_at]);
// Don't render if no turn timing or session is done with no activity
if (!hasTurnTiming) return null;
// Format elapsed time (clamp to 0 for safety)
const formatElapsed = (ms) => {
const totalSec = Math.max(0, Math.floor(ms / 1000));
if (totalSec < 60) return `${totalSec}s`;
const min = Math.floor(totalSec / 60);
const sec = totalSec % 60;
return `${min}m ${sec}s`;
};
// Format token count
const formatTokens = (count) => {
if (count == null) return null;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return String(count);
};
// Calculate turn tokens (current - baseline from turn start)
const currentTokens = context_usage?.current_tokens;
const turnTokens = (currentTokens != null && turn_start_tokens != null)
? Math.max(0, currentTokens - turn_start_tokens)
: null;
const tokenDisplay = formatTokens(turnTokens);
return html`
<div class="flex items-center gap-2 font-mono text-label">
${isActive && html`
<span class="activity-spinner"></span>
`}
${isPaused && html`
<span class="h-2 w-2 rounded-full bg-attention"></span>
`}
${isDone && html`
<span class="h-2 w-2 rounded-full bg-done"></span>
`}
<span class="text-dim">
${isActive ? 'Working' : isPaused ? 'Paused' : 'Completed'}
</span>
<span class="text-bright tabular-nums">${formatElapsed(elapsed)}</span>
${tokenDisplay && html`
<span class="text-dim/70">·</span>
<span class="text-dim/90">${tokenDisplay} tokens</span>
`}
</div>
`;
}