feat(ui): ralpi-style chat progress and pipeline-overview footer
phases.ts becomes a live chat widget (spinner + tool-call tree) and the check-runner posts per-agent tool-call summaries and an expandable completion tree through a custom message renderer; footer.ts adds the static pipeline-overview status strip for single checks and /pygienium-all.
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, join } from "node:path";
|
||||
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
||||
import { loadAgents, type AgentDef } from "./agents.js";
|
||||
import type { ToolCallEntry } from "./phases.js";
|
||||
|
||||
export interface AgentTaskOptions {
|
||||
/** Absolute working directory for the sub-agent. */
|
||||
@@ -31,6 +31,8 @@ export interface AgentTaskOptions {
|
||||
allowedTools?: string[];
|
||||
/** Optional explicit agent definition (skips `loadAgents`). */
|
||||
agent?: AgentDef;
|
||||
/** Live callback fired as each tool call starts (ralpi-style stream). */
|
||||
onToolCall?: (entry: ToolCallEntry) => void;
|
||||
}
|
||||
|
||||
export interface AgentRunResult {
|
||||
@@ -40,6 +42,8 @@ export interface AgentRunResult {
|
||||
text: string;
|
||||
/** Error message when `ok` is false. */
|
||||
error?: string;
|
||||
/** Every tool invocation captured during the run (name + label). */
|
||||
toolCalls: ToolCallEntry[];
|
||||
}
|
||||
|
||||
export type AgentRunner = (opts: AgentTaskOptions) => Promise<AgentRunResult>;
|
||||
@@ -112,6 +116,7 @@ export async function defaultAgentRunner(
|
||||
|
||||
try {
|
||||
let text = "";
|
||||
const toolCalls: ToolCallEntry[] = [];
|
||||
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
||||
if (
|
||||
event.type === "message_update" &&
|
||||
@@ -119,15 +124,24 @@ export async function defaultAgentRunner(
|
||||
) {
|
||||
text += event.assistantMessageEvent.delta;
|
||||
}
|
||||
if (event.type === "tool_execution_start") {
|
||||
const entry: ToolCallEntry = {
|
||||
name: event.toolName,
|
||||
label: formatToolArg(event.toolName, event.args),
|
||||
};
|
||||
toolCalls.push(entry);
|
||||
opts.onToolCall?.(entry);
|
||||
}
|
||||
});
|
||||
await session.prompt(opts.task, { expandPromptTemplates: false });
|
||||
unsubscribe();
|
||||
return { ok: true, text };
|
||||
return { ok: true, text, toolCalls };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
text: "",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
toolCalls: [],
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
@@ -170,12 +184,57 @@ export const fakeAgentRunner: AgentRunner = async (opts) => {
|
||||
findings.push((echo[1] ?? "").replace(/^["']|["']$/g, ""));
|
||||
}
|
||||
}
|
||||
return { ok: true, text: findings.join("\n") };
|
||||
return { ok: true, text: findings.join("\n"), toolCalls: [] };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
text: findings.join("\n"),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
toolCalls: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Tool-call label formatting (ported from ralpi) ─────────────────────────────
|
||||
|
||||
/** Collapse newlines and strip control chars so a tool arg fits one line. */
|
||||
function sanitizeLabel(s: string): string {
|
||||
return s
|
||||
.replace(/\r?\n/g, " ")
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Keep the start and end of a long value, inserting an ellipsis in the middle. */
|
||||
function truncateMiddle(s: string, maxLen: number): string {
|
||||
if (s.length <= maxLen) return s;
|
||||
const half = Math.floor((maxLen - 1) / 2);
|
||||
return s.slice(0, half) + "…" + s.slice(s.length - half);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a tool call's arguments into a short one-line label, by tool name.
|
||||
* Mirrors ralpi's `formatToolArg` so the chat tree shows the useful argument
|
||||
* (command for bash, path for read/write/edit, pattern for grep, …).
|
||||
*/
|
||||
export function formatToolArg(name: string, args: unknown): string {
|
||||
const a = (args ?? {}) as Record<string, unknown>;
|
||||
switch (name) {
|
||||
case "bash":
|
||||
return sanitizeLabel(truncateMiddle(String(a.command ?? ""), 70));
|
||||
case "write":
|
||||
case "read":
|
||||
case "edit":
|
||||
return sanitizeLabel(truncateMiddle(String(a.path ?? ""), 60));
|
||||
case "grep":
|
||||
return sanitizeLabel(
|
||||
`${a.pattern ?? "?"} — ${truncateMiddle(String(a.path ?? ""), 40)}`,
|
||||
);
|
||||
case "find":
|
||||
return sanitizeLabel(`${a.path ?? "."} — ${a.glob ?? "*"}`);
|
||||
case "ls":
|
||||
return sanitizeLabel(truncateMiddle(String(a.path ?? "."), 60));
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user