Capture per-session token usage from pi-coding-agent's getSessionStats() after each promptWithFallback in the executor and merger paths, so task.tokenUsage populates live during runs and reflects final totals on done tasks. Previously the executor never read session usage and only the heartbeat path bumped agent token totals, leaving task.tokenUsage undefined even after completion. Stats panel and done-card timing also now reflect live state: the modal overlays the SSE-updated task prop on top of the one-shot fullDetail snapshot, in-progress workflow steps contribute live elapsed to the Workflow runtime metric, and the done card uses Timed duration (matching the stats tab) with workflow runtime as fallback. Time indicator labels coarsened to <1m / Nm / Nh / Nd. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50 lines
1.7 KiB
TypeScript
50 lines
1.7 KiB
TypeScript
import type { TaskLogEntry } from "@fusion/core";
|
|
|
|
export interface TimingEvent {
|
|
timestamp: string;
|
|
durationMs?: number;
|
|
summary: string;
|
|
}
|
|
|
|
function summarizeTimingLabel(entry: TaskLogEntry): string {
|
|
const timingText = entry.action || entry.outcome || "";
|
|
const stripped = timingText
|
|
.replace(/^\[timing\]\s*/i, "")
|
|
.replace(/^\[[^\]]+\]\s*/i, "")
|
|
.replace(/\s+in\s+\d+(?:\.\d+)?ms\b/i, "")
|
|
.replace(/\s+after\s+\d+(?:\.\d+)?ms\b/i, "")
|
|
.trim();
|
|
return stripped || "Timing event";
|
|
}
|
|
|
|
export function extractTimingEvents(logEntries: TaskLogEntry[]): TimingEvent[] {
|
|
return logEntries
|
|
.filter((entry) => {
|
|
const actionText = typeof entry.action === "string" ? entry.action : "";
|
|
const outcomeText = typeof entry.outcome === "string" ? entry.outcome : "";
|
|
return actionText.includes("[timing]") || outcomeText.includes("[timing]");
|
|
})
|
|
.map((entry) => {
|
|
const haystack = `${entry.action ?? ""}\n${entry.outcome ?? ""}`;
|
|
const durationMatch = haystack.match(/(\d+(?:\.\d+)?)ms\b/i);
|
|
const durationMs = durationMatch ? Number(durationMatch[1]) : undefined;
|
|
return {
|
|
timestamp: entry.timestamp,
|
|
durationMs: Number.isFinite(durationMs) ? durationMs : undefined,
|
|
summary: summarizeTimingLabel(entry),
|
|
};
|
|
});
|
|
}
|
|
|
|
export function getTimedDurationMs(logEntries: TaskLogEntry[] | undefined): number | null {
|
|
if (!logEntries || logEntries.length === 0) return null;
|
|
let total = 0;
|
|
let counted = 0;
|
|
for (const event of extractTimingEvents(logEntries)) {
|
|
if (typeof event.durationMs !== "number") continue;
|
|
total += event.durationMs;
|
|
counted += 1;
|
|
}
|
|
return counted > 0 ? total : null;
|
|
}
|