import type { Theme } from "@earendil-works/pi-coding-agent"; import { truncateToWidth } from "@earendil-works/pi-tui"; import { truncateSubagentOutput } from "../shared/subagent/output-limit.js"; import { formatModelWithThinking } from "../shared/tui/tool-format.js"; import { statusGlyph } from "../shared/tui/visual-language.js"; import type { MultiTaskBatch, MultiTaskBatchView, MultiTaskWorker, MultiTaskWorkerView, } from "./types.js"; type ClipValue = (value: unknown, width?: number) => string; type ToolPreviewer = (args: Record, clip: ClipValue) => string; const TOOL_PREVIEWERS: Record = { read: (args, clip) => `read ${clip(args.path ?? "...")}`, write: (args, clip) => `write ${clip(args.path ?? "...")}`, edit: (args, clip) => `edit ${clip(args.path ?? "...")}`, grep: (args, clip) => `grep /${clip(args.pattern ?? "", 36)}/ in ${clip(args.path ?? ".")}`, find: (args, clip) => `find ${clip(args.pattern ?? "*", 36)} in ${clip(args.path ?? ".")}`, ls: (args, clip) => `ls ${clip(args.path ?? ".")}`, }; function previewToolCall( name: string, args: Record, ): string { const clip: ClipValue = (value, width = 56) => truncateToWidth(String(value ?? ""), width, "…"); return TOOL_PREVIEWERS[name]?.(args, clip) ?? clip(name, 72); } function workerHandle( worker: MultiTaskBatchView["workers"][number], ): string { return worker.reusable && worker.subagentId ? ` · #${worker.subagentId.slice(0, 8)} · turn ${worker.turn ?? 0}` : ""; } function workerStatusVisual( status: MultiTaskBatchView["workers"][number]["status"], ): "active" | "success" | "error" | "pending" { if (status === "running") return "active"; if (status === "completed") return "success"; if (status === "queued") return "pending"; return "error"; } function snapshotWorker( worker: MultiTaskWorker, includeOutput: boolean, ): MultiTaskWorkerView { const view: MultiTaskWorkerView = { id: worker.id, task: worker.task, paths: worker.paths, kind: worker.kind, model: worker.model, thinkingLevel: worker.thinkingLevel, status: worker.status, startedAt: worker.startedAt, completedAt: worker.completedAt, progress: worker.progress, toolCalls: worker.toolCalls, subagentId: worker.subagentId, reusable: worker.reusable, turn: worker.turn, error: worker.error, }; if (includeOutput) { view.output = worker.output; view.runDir = worker.runDir; } return view; } export function snapshot( batch: MultiTaskBatch, includeOutput: boolean, ): MultiTaskBatchView { return { id: batch.id, model: batch.model, thinkingLevel: batch.thinkingLevel, status: batch.status, createdAt: batch.createdAt, completedAt: batch.completedAt, maxConcurrency: batch.maxConcurrency, workers: batch.workers.map((worker) => snapshotWorker(worker, includeOutput), ), }; } export function summarize(batch: MultiTaskBatchView): string { const counts = new Map(); for (const worker of batch.workers) counts.set(worker.status, (counts.get(worker.status) ?? 0) + 1); const statuses = Array.from(counts.entries()) .map(([status, count]) => `${status}=${count}`) .join(", "); return `Batch ${batch.id}: ${batch.status} (${statuses})`; } export function progressText(batch: MultiTaskBatchView): string { const workers = batch.workers.map((worker) => { const lastCall = worker.toolCalls.slice(-1)[0]; const activity = lastCall ? `→ ${previewToolCall(lastCall.name, lastCall.arguments)}` : worker.progress ?? worker.status; return `${worker.id} [${worker.kind}]: ${worker.status} ${activity}`; }); return [summarize(batch), ...workers].join("\n"); } export function progressDetails( batch: MultiTaskBatchView, theme: Theme, expanded: boolean, ): string[] { const details: string[] = []; for (const worker of batch.workers) { const lastCall = worker.toolCalls.slice(-1)[0]; const activity = lastCall ? previewToolCall(lastCall.name, lastCall.arguments) : worker.progress ?? worker.status; details.push( `${statusGlyph(theme, workerStatusVisual(worker.status))} ${truncateToWidth(`${worker.id} · ${worker.kind} · ${worker.status} · ${activity}`, 120, "…")}`, ); if (expanded) { details.push( ` └ ${truncateToWidth(`${formatModelWithThinking(worker.model, worker.thinkingLevel)}${workerHandle(worker)} · ${worker.paths.length} scope${worker.paths.length === 1 ? "" : "s"}`, 116, "…")}`, ); for (const call of worker.toolCalls.slice(-4, -1)) details.push( ` └ ${truncateToWidth(`${worker.id}: ${previewToolCall(call.name, call.arguments)}`, 116, "…")}`, ); } } return details; } export function collectText(batch: MultiTaskBatchView): string { const reports = batch.workers.map((worker) => { const result = worker.output ?? worker.error ?? "No result yet."; return `## ${worker.id} · ${worker.kind} · ${worker.status}\n\n${result}`; }); const handles = batch.workers.flatMap((worker) => worker.reusable && worker.subagentId ? [ `- ${worker.id}: Reusable subagentId: ${worker.subagentId} (turn ${worker.turn ?? 0}).`, ] : [], ); const visible = truncateSubagentOutput( `${summarize(batch)}\n\n${reports.join("\n\n")}`, "[Multi Task 输出已截断;完整 worker 输出保存在工具 details 中。]", ).content; return handles.length > 0 ? `${visible}\n\nReusable workers:\n${handles.join("\n")}` : visible; } export function workerSummary( count: number | undefined, batchId: string | undefined, ): string | undefined { if (count === undefined) return batchId; return `${count} worker${count === 1 ? "" : "s"}`; } export function batchVisualStatus( status: MultiTaskBatchView["status"], ): "active" | "success" | "error" { if (status === "running") return "active"; return status === "completed" ? "success" : "error"; }