MAESTRO: Add token/context tracking to auto-run task history details

- Add elapsedTimeMs field to HistoryEntry interface to track task duration
- Update useBatchProcessor to capture and pass usage stats (tokens, context,
  cost) and elapsed time when adding history entries for auto-run tasks
- Enhance HistoryDetailModal with a comprehensive stats panel that shows:
  - Context window progress bar with percentage
  - Token breakdown (input/output/cache tokens)
  - Elapsed time in human-readable format
  - Cost per task
- Update main process HistoryEntry interface to match renderer types

This enables users to track resource usage for each auto-run task iteration
in the history details view.
This commit is contained in:
Pedram Amini
2025-11-27 00:47:46 -06:00
parent 641cb81f83
commit 109f32dbd8
4 changed files with 131 additions and 34 deletions

View File

@@ -120,6 +120,8 @@ interface HistoryEntry {
totalCostUsd: number;
contextWindow: number;
};
success?: boolean; // For AUTO entries: whether the task completed successfully
elapsedTimeMs?: number; // Time taken to complete this task in milliseconds
}
interface HistoryData {

View File

@@ -1,9 +1,22 @@
import React, { useEffect, useRef, useState } from 'react';
import { X, Bot, User, ExternalLink, Copy, Check, CheckCircle, XCircle, Trash2 } from 'lucide-react';
import { X, Bot, User, ExternalLink, Copy, Check, CheckCircle, XCircle, Trash2, Clock, Cpu, Zap } from 'lucide-react';
import type { Theme, HistoryEntry } from '../types';
import { useLayerStack } from '../contexts/LayerStackContext';
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
// Format elapsed time in human-readable format
const formatElapsedTime = (ms: number): string => {
if (ms < 1000) return `${ms}ms`;
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
};
interface HistoryDetailModalProps {
theme: Theme;
entry: HistoryEntry;
@@ -191,40 +204,98 @@ export function HistoryDetailModal({
</span>
</div>
{/* Right side widgets */}
<div className="flex items-center gap-3">
{/* Cost Tracker - styled as pill */}
{entry.usageStats && entry.usageStats.totalCostUsd > 0 && (
<span className="text-xs font-mono font-bold px-2 py-0.5 rounded-full border border-green-500/30 text-green-500 bg-green-500/10">
${entry.usageStats.totalCostUsd.toFixed(2)}
</span>
)}
{/* Context Window Widget */}
{entry.contextUsage !== undefined && (
<div className="flex flex-col items-end">
<span className="text-[10px] font-bold uppercase" style={{ color: theme.colors.textDim }}>Context Window</span>
<div className="w-24 h-1.5 rounded-full mt-1 overflow-hidden" style={{ backgroundColor: theme.colors.border }}>
<div
className="h-full transition-all duration-500 ease-out"
style={{
width: `${entry.contextUsage}%`,
backgroundColor: getContextColor(entry.contextUsage, theme)
}}
/>
</div>
</div>
)}
<button
onClick={onClose}
className="p-1 rounded hover:bg-white/10 transition-colors"
>
<X className="w-5 h-5" style={{ color: theme.colors.textDim }} />
</button>
</div>
{/* Close button */}
<button
onClick={onClose}
className="p-1 rounded hover:bg-white/10 transition-colors"
>
<X className="w-5 h-5" style={{ color: theme.colors.textDim }} />
</button>
</div>
{/* Stats Panel - shown when we have usage stats */}
{(entry.usageStats || entry.contextUsage !== undefined || entry.elapsedTimeMs) && (
<div
className="px-6 py-4 border-b shrink-0"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain + '40' }}
>
<div className="flex items-center gap-6 flex-wrap">
{/* Context Window Widget */}
{entry.contextUsage !== undefined && entry.usageStats && (
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5">
<Cpu className="w-4 h-4" style={{ color: theme.colors.textDim }} />
<span className="text-[10px] font-bold uppercase" style={{ color: theme.colors.textDim }}>
Context
</span>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="w-32 h-2 rounded-full overflow-hidden" style={{ backgroundColor: theme.colors.border }}>
<div
className="h-full transition-all duration-500 ease-out"
style={{
width: `${entry.contextUsage}%`,
backgroundColor: getContextColor(entry.contextUsage, theme)
}}
/>
</div>
<span className="text-xs font-mono font-bold" style={{ color: getContextColor(entry.contextUsage, theme) }}>
{entry.contextUsage}%
</span>
</div>
<span className="text-[10px] font-mono" style={{ color: theme.colors.textDim }}>
{((entry.usageStats.inputTokens + entry.usageStats.outputTokens) / 1000).toFixed(1)}k / {(entry.usageStats.contextWindow / 1000).toFixed(0)}k tokens
</span>
</div>
</div>
)}
{/* Token Breakdown */}
{entry.usageStats && (
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5">
<Zap className="w-4 h-4" style={{ color: theme.colors.textDim }} />
<span className="text-[10px] font-bold uppercase" style={{ color: theme.colors.textDim }}>
Tokens
</span>
</div>
<div className="flex items-center gap-3 text-xs font-mono">
<span style={{ color: theme.colors.accent }}>
<span style={{ color: theme.colors.textDim }}>In:</span> {entry.usageStats.inputTokens.toLocaleString()}
</span>
<span style={{ color: theme.colors.success }}>
<span style={{ color: theme.colors.textDim }}>Out:</span> {entry.usageStats.outputTokens.toLocaleString()}
</span>
{entry.usageStats.cacheReadInputTokens > 0 && (
<span style={{ color: theme.colors.warning }}>
<span style={{ color: theme.colors.textDim }}>Cache:</span> {entry.usageStats.cacheReadInputTokens.toLocaleString()}
</span>
)}
</div>
</div>
)}
{/* Elapsed Time */}
{entry.elapsedTimeMs !== undefined && (
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" style={{ color: theme.colors.textDim }} />
<span className="text-xs font-mono font-bold" style={{ color: theme.colors.textMain }}>
{formatElapsedTime(entry.elapsedTimeMs)}
</span>
</div>
)}
{/* Cost */}
{entry.usageStats && entry.usageStats.totalCostUsd > 0 && (
<span className="text-xs font-mono font-bold px-2 py-0.5 rounded-full border border-green-500/30 text-green-500 bg-green-500/10">
${entry.usageStats.totalCostUsd.toFixed(4)}
</span>
)}
</div>
</div>
)}
{/* Content */}
<div
className="flex-1 overflow-y-auto px-6 py-5 scrollbar-thin"

View File

@@ -146,6 +146,9 @@ export function useBatchProcessor({
const claudeSessionIds: string[] = [];
let completedCount = 0;
// Helper to get current session state (for capturing updated usage stats)
const getCurrentSession = () => sessions.find(s => s.id === sessionId);
for (let i = 0; i < totalTasks; i++) {
// Check if stop was requested for this session
if (stopRequestedRefs.current[sessionId]) {
@@ -163,9 +166,15 @@ export function useBatchProcessor({
}));
try {
// Capture start time for elapsed time tracking
const taskStartTime = Date.now();
// Spawn agent with the prompt for this specific session
const result = await onSpawnAgent(sessionId, finalPrompt);
// Capture elapsed time
const elapsedTimeMs = Date.now() - taskStartTime;
if (result.claudeSessionId) {
claudeSessionIds.push(result.claudeSessionId);
// Register as auto-initiated Maestro session
@@ -185,6 +194,9 @@ export function useBatchProcessor({
}
}));
// Get current session state to capture usage stats
const currentSession = getCurrentSession();
// Add history entry for this task (both success and failure)
const fullResponse = result.response || '';
let summary = `Task ${i + 1} of ${totalTasks}`;
@@ -218,7 +230,18 @@ export function useBatchProcessor({
claudeSessionId: result.claudeSessionId,
projectPath: session.cwd,
sessionId: sessionId, // Associate with this Maestro session for isolation
success: result.success
success: result.success,
// Capture usage stats and context at time of task completion
contextUsage: currentSession?.contextUsage,
usageStats: currentSession?.usageStats ? {
inputTokens: currentSession.usageStats.inputTokens,
outputTokens: currentSession.usageStats.outputTokens,
cacheReadInputTokens: currentSession.usageStats.cacheReadInputTokens,
cacheCreationInputTokens: currentSession.usageStats.cacheCreationInputTokens,
totalCostUsd: currentSession.usageStats.totalCostUsd,
contextWindow: currentSession.usageStats.contextWindow,
} : undefined,
elapsedTimeMs
});
// Re-read the scratchpad file to check remaining tasks and sync to UI

View File

@@ -75,6 +75,7 @@ export interface HistoryEntry {
contextUsage?: number; // Context window usage percentage at time of entry
usageStats?: UsageStats; // Token usage and cost at time of entry
success?: boolean; // For AUTO entries: whether the task completed successfully (true) or failed (false)
elapsedTimeMs?: number; // Time taken to complete this task in milliseconds
}
// Batch processing state