diff --git a/ui-electrobun/src/mainview/health-store.svelte.ts b/ui-electrobun/src/mainview/health-store.svelte.ts index 9de7687..f7edf96 100644 --- a/ui-electrobun/src/mainview/health-store.svelte.ts +++ b/ui-electrobun/src/mainview/health-store.svelte.ts @@ -145,6 +145,7 @@ export function trackProject(projectId: string): void { lastActivityTs: Date.now(), lastToolName: null, toolsInFlight: 0, + activeToolMap: new Map(), costSnapshots: [], totalTokens: 0, totalCost: 0, @@ -163,6 +164,13 @@ export function recordActivity(projectId: string, toolName?: string): void { t.lastToolName = toolName; // Fix #8 (Codex audit): Increment counter for concurrent tool tracking t.toolsInFlight++; + // Feature 10: Track per-tool starts + const existing = t.activeToolMap.get(toolName); + if (existing) { + existing.count++; + } else { + t.activeToolMap.set(toolName, { startTime: Date.now(), count: 1 }); + } } if (!tickInterval) startHealthTick(); } @@ -171,9 +179,32 @@ export function recordActivity(projectId: string, toolName?: string): void { export function recordToolDone(projectId: string): void { const t = trackers.get(projectId); if (!t) return; - t.lastActivityTs = Date.now(); + const now = Date.now(); + t.lastActivityTs = now; // Fix #8 (Codex audit): Decrement counter, floor at 0 t.toolsInFlight = Math.max(0, t.toolsInFlight - 1); + + // Feature 10: Record duration in histogram, remove from activeToolMap + if (t.lastToolName) { + const entry = t.activeToolMap.get(t.lastToolName); + if (entry) { + const durationMs = now - entry.startTime; + // Update global histogram + const hist = toolDurationHistogram.get(t.lastToolName); + if (hist) { + hist.totalMs += durationMs; + hist.count++; + } else { + toolDurationHistogram.set(t.lastToolName, { + toolName: t.lastToolName, + totalMs: durationMs, + count: 1, + }); + } + entry.count--; + if (entry.count <= 0) t.activeToolMap.delete(t.lastToolName); + } + } } /** Record a token/cost snapshot for burn rate calculation. */ @@ -233,6 +264,30 @@ export function getHealthAggregates(): { return { running, idle, stalled, totalBurnRatePerHour }; } +/** Feature 10: Get all currently active tools across all projects. */ +export function getActiveTools(): Array<{ projectId: string; toolName: string; startTime: number; count: number }> { + const results: Array<{ projectId: string; toolName: string; startTime: number; count: number }> = []; + for (const t of trackers.values()) { + for (const [name, entry] of t.activeToolMap) { + results.push({ projectId: t.projectId, toolName: name, startTime: entry.startTime, count: entry.count }); + } + } + return results; +} + +/** Feature 10: Get tool duration histogram for diagnostics. */ +export function getToolHistogram(): Array<{ toolName: string; avgMs: number; count: number }> { + const results: Array<{ toolName: string; avgMs: number; count: number }> = []; + for (const entry of toolDurationHistogram.values()) { + results.push({ + toolName: entry.toolName, + avgMs: entry.count > 0 ? entry.totalMs / entry.count : 0, + count: entry.count, + }); + } + return results.sort((a, b) => b.count - a.count).slice(0, 15); +} + /** Start the health tick timer. */ function startHealthTick(): void { if (tickInterval) return;