This commit is contained in:
2026-08-21 02:30:51 -04:00
commit 38ecedcaa3
7 changed files with 532 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env node
// progress.mjs — render a `pi --mode json` event stream as human-readable
// progress on stdout, and write the final assistant message to a report file.
//
// Usage: pi --mode json "prompt" | node progress.mjs <report-file> [--quiet]
//
// Quiet mode still writes the report file (for harness logs); it only
// suppresses the progress display.
import { writeFileSync } from "node:fs";
const args = process.argv.slice(2);
const reportPath = args.find((a) => !a.startsWith("--"));
const quiet = args.includes("--quiet");
if (!reportPath) {
console.error("usage: progress.mjs <report-file> [--quiet]");
process.exit(2);
}
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
const c = {
dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
cyan: (s) => (useColor ? `\x1b[36m${s}\x1b[0m` : s),
red: (s) => (useColor ? `\x1b[31m${s}\x1b[0m` : s),
yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s),
green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s),
};
const out = (s = "") => {
if (!quiet) process.stdout.write(s + "\n");
};
function truncate(s, n) {
s = String(s ?? "").replace(/\s+/g, " ").trim();
return s.length > n ? s.slice(0, n - 1) + "…" : s;
}
function summarizeTool(name, a = {}) {
switch (name) {
case "bash":
return "$ " + truncate(a.command ?? a.cmd, 110);
case "read":
return truncate(a.path, 90) + (a.offset ? `:${a.offset}` : "");
case "write":
return truncate(a.path, 90);
case "edit":
return truncate(a.path, 90);
case "grep":
return `${truncate(a.pattern, 40)} in ${truncate(a.path ?? ".", 60)}`;
case "find":
return truncate(a.pattern, 40) + (a.path ? " in " + truncate(a.path, 60) : "");
case "ls":
return truncate(a.path ?? ".", 90);
default: {
let s = "";
try {
s = JSON.stringify(a);
} catch {
s = String(a);
}
return truncate(s, 100);
}
}
}
function textOf(message) {
if (!message) return "";
if (typeof message.content === "string") return message.content;
if (Array.isArray(message.content))
return message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
return "";
}
function wrap(text, indent, width = 100) {
const pad = " ".repeat(indent);
const lines = [];
for (const raw of text.split("\n")) {
if (!raw) {
lines.push("");
continue;
}
let line = raw;
while (line.length > width - indent) {
let cut = line.lastIndexOf(" ", width - indent);
if (cut < 20) cut = width - indent;
lines.push(pad + line.slice(0, cut).trimEnd());
line = line.slice(cut).trimStart();
}
lines.push(pad + line);
}
return lines.join("\n");
}
let sessionId = "";
let lastUsage = null;
let lastAssistantText = "";
let sawError = false;
function handle(ev) {
switch (ev.type) {
case "session":
sessionId = ev.id ?? "";
break;
case "tool_execution_start":
out(" " + c.cyan("⏺ " + ev.toolName) + " " + c.dim(summarizeTool(ev.toolName, ev.args)));
break;
case "tool_execution_end":
if (ev.isError) out(" " + c.red("✗ " + ev.toolName + " failed"));
break;
case "message_update":
if (ev.usage) lastUsage = ev.usage;
break;
case "message_end": {
const m = ev.message ?? {};
if (m.role === "assistant") {
const text = textOf(m);
if (text) {
lastAssistantText = text;
out(wrap(text, 2));
out("");
}
if (m.stopReason === "error") {
sawError = true;
out(c.red(" ✗ " + (m.errorMessage ?? "assistant error")));
}
}
break;
}
default:
break;
}
}
let buf = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
buf += chunk;
let idx;
while ((idx = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, idx).trim();
buf = buf.slice(idx + 1);
if (!line) continue;
let ev;
try {
ev = JSON.parse(line);
} catch {
out(c.yellow(" ? " + truncate(line, 120)));
continue;
}
handle(ev);
}
});
process.stdin.on("end", () => {
writeFileSync(
reportPath,
lastAssistantText ? lastAssistantText + "\n" : "(no final assistant message — see the .err log)\n",
);
const u = lastUsage ?? {};
const bits = [];
if (u.input) bits.push(`↑${u.input}`);
if (u.output) bits.push(`↓${u.output}`);
if (u.cacheRead) bits.push(`R${u.cacheRead}`);
if (sessionId) bits.push("session " + String(sessionId).slice(0, 8));
if (bits.length) out(c.dim(" · " + bits.join(" ")));
out(sawError ? c.red(" · child finished with errors") : c.green(" · child done"));
});