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>
116 lines
3.8 KiB
JavaScript
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>
|
|
`;
|
|
}
|