mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user