/** * Formatting utilities for display output */ import * as fs from "node:fs"; import * as path from "node:path"; import type { ChainStep, SequentialStep } from "./settings.js"; import type { SingleResult, Usage } from "./types.js"; import { isParallelStep } from "./settings.js"; /** * Format token count with k suffix for large numbers */ export function formatTokens(n: number): string { return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`; } /** * Format usage statistics into a compact string */ export function formatUsage(u: Usage, model?: string): string { const parts: string[] = []; if (u.turns) { parts.push(`${u.turns} turn${u.turns > 1 ? "s" : ""}`); } if (u.input) { parts.push(`in:${formatTokens(u.input)}`); } if (u.output) { parts.push(`out:${formatTokens(u.output)}`); } if (u.cacheRead) { parts.push(`R${formatTokens(u.cacheRead)}`); } if (u.cacheWrite) { parts.push(`W${formatTokens(u.cacheWrite)}`); } if (u.cost) { parts.push(`$${u.cost.toFixed(4)}`); } if (model) { parts.push(model); } return parts.join(" "); } /** * Format duration in human-readable form */ export function formatDuration(ms: number): string { if (ms < 1000) { return `${ms}ms`; } if (ms < 60_000) { return `${(ms / 1000).toFixed(1)}s`; } return `${Math.floor(ms / 60_000)}m${Math.floor((ms % 60_000) / 1000)}s`; } /** * Build a summary string for a completed/failed chain */ export function buildChainSummary( steps: ChainStep[], results: SingleResult[], chainDir: string, status: "completed" | "failed", failedStep?: { index: number; error: string }, ): string { // Build step names for display const stepNames = steps .map((s) => (isParallelStep(s) ? `parallel[${s.parallel.length}]` : (s as SequentialStep).agent)) .join(" → "); // Calculate total duration from results const totalDuration = results.reduce((sum, r) => sum + (r.progress?.durationMs || 0), 0); const durationStr = formatDuration(totalDuration); // Check for progress.md const progressPath = path.join(chainDir, "progress.md"); const hasProgress = fs.existsSync(progressPath); const allSkills = new Set(); for (const r of results) { if (r.skills) { r.skills.forEach((s) => allSkills.add(s)); } } const skillsLine = allSkills.size > 0 ? `Skills: ${[...allSkills].join(", ")}` : ""; if (status === "completed") { const stepWord = results.length === 1 ? "step" : "steps"; return `Chain completed: ${stepNames} (${results.length} ${stepWord}, ${durationStr})${ skillsLine ? `\n${skillsLine}` : "" } Progress: ${hasProgress ? progressPath : "(none)"} 📁 Artifacts: ${chainDir}`; } const stepInfo = failedStep ? ` at step ${failedStep.index + 1}` : ""; const errorInfo = failedStep?.error ? `: ${failedStep.error}` : ""; return `Chain failed${stepInfo}${errorInfo}${skillsLine ? `\n${skillsLine}` : ""} Progress: ${hasProgress ? progressPath : "(none)"} 📁 Artifacts: ${chainDir}`; } /** * Format a tool call for display */ export function formatToolCall(name: string, args: Record): string { switch (name) { case "bash": { return `$ ${((args.command as string) || "").slice(0, 60)}${(args.command as string)?.length > 60 ? "..." : ""}`; } case "read": { return `read ${shortenPath((args.path || args.file_path || "") as string)}`; } case "write": { return `write ${shortenPath((args.path || args.file_path || "") as string)}`; } case "edit": { return `edit ${shortenPath((args.path || args.file_path || "") as string)}`; } default: { const s = JSON.stringify(args); return `${name} ${s.slice(0, 40)}${s.length > 40 ? "..." : ""}`; } } } /** * Shorten a path by replacing home directory with ~ */ export function shortenPath(p: string): string { const home = process.env.HOME; // Only shorten if HOME is defined and non-empty, and path starts with it if (home && p.startsWith(home)) { return `~${p.slice(home.length)}`; } return p; }