mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
feat: add token consumption tracking per task
- Add TokenUsage interface to track input/output tokens and cost - Capture usage from Claude SDK result messages in auto-mode-service - Accumulate token usage across multiple streams and follow-up sessions - Store token usage in feature.json for persistence - Display token count and cost on Kanban cards with tooltip breakdown - Add token usage summary header in log viewer Closes #166 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
Circle,
|
||||
Play,
|
||||
Loader2,
|
||||
Coins,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
@@ -34,10 +35,35 @@ import {
|
||||
type LogEntryType,
|
||||
type ToolCategory,
|
||||
} from "@/lib/log-parser";
|
||||
import type { TokenUsage } from "@/store/app-store";
|
||||
|
||||
interface LogViewerProps {
|
||||
output: string;
|
||||
className?: string;
|
||||
tokenUsage?: TokenUsage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats token counts for compact display (e.g., 12500 -> "12.5K")
|
||||
*/
|
||||
function formatTokenCount(count: number): string {
|
||||
if (count >= 1000000) {
|
||||
return `${(count / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (count >= 1000) {
|
||||
return `${(count / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats cost for display (e.g., 0.0847 -> "$0.0847")
|
||||
*/
|
||||
function formatCost(cost: number): string {
|
||||
if (cost < 0.01) {
|
||||
return `$${cost.toFixed(4)}`;
|
||||
}
|
||||
return `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
const getLogIcon = (type: LogEntryType) => {
|
||||
@@ -413,7 +439,7 @@ interface ToolCategoryStats {
|
||||
other: number;
|
||||
}
|
||||
|
||||
export function LogViewer({ output, className }: LogViewerProps) {
|
||||
export function LogViewer({ output, className, tokenUsage }: LogViewerProps) {
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [hiddenTypes, setHiddenTypes] = useState<Set<LogEntryType>>(new Set());
|
||||
@@ -615,6 +641,40 @@ export function LogViewer({ output, className }: LogViewerProps) {
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)}>
|
||||
{/* Token Usage Summary Header */}
|
||||
{tokenUsage && tokenUsage.totalTokens > 0 && (
|
||||
<div className="mb-3 p-2 bg-zinc-900/50 rounded-lg border border-zinc-700/50">
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Coins className="w-3.5 h-3.5 text-amber-400" />
|
||||
<span className="font-medium">{formatTokenCount(tokenUsage.totalTokens)}</span>
|
||||
<span className="text-muted-foreground/60">tokens</span>
|
||||
</span>
|
||||
<span className="text-muted-foreground/30">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-green-400">IN:</span>
|
||||
<span>{formatTokenCount(tokenUsage.inputTokens)}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-blue-400">OUT:</span>
|
||||
<span>{formatTokenCount(tokenUsage.outputTokens)}</span>
|
||||
</span>
|
||||
{tokenUsage.cacheReadInputTokens > 0 && (
|
||||
<>
|
||||
<span className="text-muted-foreground/30">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-purple-400">Cache:</span>
|
||||
<span>{formatTokenCount(tokenUsage.cacheReadInputTokens)}</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-muted-foreground/30">|</span>
|
||||
<span className="flex items-center gap-1 text-amber-400 font-medium">
|
||||
{formatCost(tokenUsage.costUSD)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Sticky header with search, stats, and filters */}
|
||||
{/* Use -top-4 to compensate for parent's p-4 padding, pt-4 to restore visual spacing */}
|
||||
<div className="sticky -top-4 z-10 bg-zinc-950/95 backdrop-blur-sm pt-4 pb-2 space-y-2 -mx-4 px-4">
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
Wand2,
|
||||
Archive,
|
||||
Lock,
|
||||
Coins,
|
||||
} from "lucide-react";
|
||||
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
@@ -90,6 +91,29 @@ function formatThinkingLevel(level: ThinkingLevel | undefined): string {
|
||||
return labels[level];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats token counts for compact display (e.g., 12500 -> "12.5K")
|
||||
*/
|
||||
function formatTokenCount(count: number): string {
|
||||
if (count >= 1000000) {
|
||||
return `${(count / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (count >= 1000) {
|
||||
return `${(count / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats cost for display (e.g., 0.0847 -> "$0.0847")
|
||||
*/
|
||||
function formatCost(cost: number): string {
|
||||
if (cost < 0.01) {
|
||||
return `$${cost.toFixed(4)}`;
|
||||
}
|
||||
return `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
interface KanbanCardProps {
|
||||
feature: Feature;
|
||||
onEdit: () => void;
|
||||
@@ -873,6 +897,36 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Token Usage Display */}
|
||||
{feature.tokenUsage && feature.tokenUsage.totalTokens > 0 && (
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground/60 pt-2 border-t border-border/30">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center gap-1 cursor-help">
|
||||
<Coins className="w-2.5 h-2.5 text-amber-400" />
|
||||
{formatTokenCount(feature.tokenUsage.totalTokens)} tokens
|
||||
<span className="text-amber-400/80">
|
||||
({formatCost(feature.tokenUsage.costUSD)})
|
||||
</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
<div className="space-y-1">
|
||||
<p>Input: {formatTokenCount(feature.tokenUsage.inputTokens)}</p>
|
||||
<p>Output: {formatTokenCount(feature.tokenUsage.outputTokens)}</p>
|
||||
{feature.tokenUsage.cacheReadInputTokens > 0 && (
|
||||
<p>Cache read: {formatTokenCount(feature.tokenUsage.cacheReadInputTokens)}</p>
|
||||
)}
|
||||
<p className="font-medium pt-1 border-t border-border/30">
|
||||
Cost: {formatCost(feature.tokenUsage.costUSD)}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,8 @@ export function AgentOutputModal({
|
||||
const autoScrollRef = useRef(true);
|
||||
const projectPathRef = useRef<string>("");
|
||||
const useWorktrees = useAppStore((state) => state.useWorktrees);
|
||||
const features = useAppStore((state) => state.features);
|
||||
const feature = features.find((f) => f.id === featureId);
|
||||
|
||||
// Auto-scroll to bottom when output changes
|
||||
useEffect(() => {
|
||||
@@ -387,7 +389,7 @@ export function AgentOutputModal({
|
||||
No output yet. The agent will stream output here as it works.
|
||||
</div>
|
||||
) : viewMode === "parsed" ? (
|
||||
<LogViewer output={output} />
|
||||
<LogViewer output={output} tokenUsage={feature?.tokenUsage} />
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap break-words text-zinc-300">
|
||||
{output}
|
||||
|
||||
@@ -278,6 +278,16 @@ export interface AIProfile {
|
||||
icon?: string; // Optional icon name from lucide
|
||||
}
|
||||
|
||||
// Token usage statistics for tracking API costs per task
|
||||
export interface TokenUsage {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheReadInputTokens: number;
|
||||
cacheCreationInputTokens: number;
|
||||
totalTokens: number;
|
||||
costUSD: number;
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
id: string;
|
||||
category: string;
|
||||
@@ -305,6 +315,7 @@ export interface Feature {
|
||||
planningMode?: PlanningMode; // Planning mode for this feature
|
||||
planSpec?: PlanSpec; // Generated spec/plan data
|
||||
requirePlanApproval?: boolean; // Whether to pause and require manual approval before implementation
|
||||
tokenUsage?: TokenUsage; // Token consumption tracking for this task
|
||||
}
|
||||
|
||||
// Parsed task from spec (for spec and full planning modes)
|
||||
|
||||
Reference in New Issue
Block a user