feat: chat rendering toolcalls, footer overview

This commit is contained in:
2026-08-09 20:24:11 -04:00
parent cb8a88d0bd
commit 259b1d3b2d
14 changed files with 664 additions and 383 deletions

View File

@@ -25,11 +25,11 @@ import {
createPhaseStrip,
type SendChatMessage,
type CheckCompletionDetails,
type ToolCallEntry,
type PhaseLogEntry,
type PhaseLogStatus,
PHASE_LABELS,
} from "../phases.js";
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
import { createPipelineFooter, footerPhaseItems } from "../footer.js";
import {
applyPhaseStatus,
@@ -100,6 +100,12 @@ export interface RunCheckOptions {
* overviews never compete over the same status slot.
*/
footer?: boolean;
/** Optional callback that forwards raw sub-agent events to the chat
* stream (see `pygienium-stream` in `index.ts`). */
onAgentEvent?: (phase: string, event: AgentSessionEvent) => void;
/** Optional callback to emit synthetic progress lines during non-agent
* phases (verify, cleanup, recon) into the chat stream. */
sendPhaseLine?: (phase: string, text: string) => void;
/**
* Ensure `<cwd>/.gitignore` excludes `.pygienium/` before this run writes
* state/artifacts (default true). Set false with `--no-gitignore`.
@@ -173,9 +179,20 @@ async function runCheckImplInner(
ui: opts.ui,
hasUI: opts.hasUI ?? false,
checkLabel: check.label,
widget: opts.widget,
});
/** Phase tag combining check label + phase label for stream lines. */
const phaseTag = (phaseId: string): string =>
`${check.label}: ${PHASE_LABELS[phaseId] ?? phaseId}`;
/** Forward a raw agent event tagged with the current phase. */
const forward = (phaseId: string) => (event: AgentSessionEvent) =>
opts.onAgentEvent?.(phaseTag(phaseId), event);
/** Emit a synthetic stream line for non-agent phases (verify/cleanup/recon). */
const phaseLine = (phaseId: string, text: string): void =>
opts.sendPhaseLine?.(phaseTag(phaseId), text);
// Pipeline-overview footer: a static one-line view of the full phase list
// with the cursor on the current phase and what's to come. Detail lives in
// the chat (phase strip + completion tree); the footer is the overview.
@@ -229,12 +246,14 @@ async function runCheckImplInner(
footerEnter(PHASE_RECON);
applyPhaseStatus(state, check.name, PHASE_RECON, "in_progress");
await saveRunState(state);
phaseLine(PHASE_RECON, "scanning project structure…");
const snapshot = await runRecon(cwd);
state.recon = {
complete: true,
path: join(stateDir(cwd), "recon.json"),
finishedAt: snapshot.createdAt,
};
phaseLine(PHASE_RECON, "✓ recon complete");
applyPhaseStatus(state, check.name, PHASE_RECON, "complete");
await saveRunState(state);
footerComplete(PHASE_RECON);
@@ -250,21 +269,12 @@ async function runCheckImplInner(
applyPhaseStatus(state, check.name, PHASE_ANALYSIS, "in_progress");
await saveRunState(state);
const scanTask = await check.buildScanTask(cwd, scope);
const scanStartMs = Date.now();
const scanResult = await runAgentTask({
cwd: scope.target,
agentName: check.agentName,
task: scanTask,
onToolCall: (entry) => strip.pushToolCall(entry),
onEvent: forward(PHASE_ANALYSIS),
});
postAgentToolCalls(
opts,
PHASE_ANALYSIS,
scanResult.toolCalls,
scanResult.ok,
Date.now() - scanStartMs,
scanResult.error,
);
findings = scanResult.text;
recordCheckOutput(state, check.name, { findings });
if (!scanResult.ok) {
@@ -291,21 +301,12 @@ async function runCheckImplInner(
applyPhaseStatus(state, check.name, PHASE_FIX, "in_progress");
await saveRunState(state);
const fixTask = await check.buildFixTask(cwd, scope, findings);
const fixStartMs = Date.now();
const fixResult = await runAgentTask({
cwd: scope.target,
agentName: check.fixAgentName ?? "fixer",
task: fixTask,
onToolCall: (entry) => strip.pushToolCall(entry),
onEvent: forward(PHASE_FIX),
});
postAgentToolCalls(
opts,
PHASE_FIX,
fixResult.toolCalls,
fixResult.ok,
Date.now() - fixStartMs,
fixResult.error,
);
changes = fixResult.text;
recordCheckOutput(state, check.name, { changes });
if (!fixResult.ok) {
@@ -337,6 +338,7 @@ async function runCheckImplInner(
footerEnter(PHASE_VERIFY);
applyPhaseStatus(state, check.name, PHASE_VERIFY, "in_progress");
await saveRunState(state);
phaseLine(PHASE_VERIFY, "checking artifacts…");
// Verify is a lightweight self-check. A check may supply a dedicated
// `verify` hook to confirm its artifacts were produced (e.g.
// findings.md / changes.md exist). When absent, fall back to re-running
@@ -357,6 +359,7 @@ async function runCheckImplInner(
state,
};
}
phaseLine(PHASE_VERIFY, "✓ artifacts confirmed");
applyPhaseStatus(state, check.name, PHASE_VERIFY, "complete");
await saveRunState(state);
footerComplete(PHASE_VERIFY);
@@ -366,7 +369,9 @@ async function runCheckImplInner(
footerEnter(PHASE_CLEANUP);
applyPhaseStatus(state, check.name, PHASE_CLEANUP, "in_progress");
await saveRunState(state);
phaseLine(PHASE_CLEANUP, "removing transient artifacts…");
await cleanupTransientArtifacts(cwd, check.name);
phaseLine(PHASE_CLEANUP, "✓ done");
applyPhaseStatus(state, check.name, PHASE_CLEANUP, "complete");
footerComplete(PHASE_CLEANUP);
markCheckStatus(state, check.name, "complete");
@@ -491,35 +496,6 @@ function postCheckCompletion(
send(header, { phase: "complete", completion: details });
}
/**
* Post a ralpi-style per-agent-execution message into the chat: header + the
* tool-call tree captured during that sub-process (analysis or fix). One
* message per agent run, so tool calls are broken down by which execution
* produced them. No-op when no callback is wired or the run made no calls.
*/
function postAgentToolCalls(
opts: RunCheckOptions,
phaseId: string,
toolCalls: ToolCallEntry[],
okc: boolean,
durationMs: number,
error?: string,
): void {
const send = opts.sendChatMessage;
if (!send) return;
if (toolCalls.length === 0) return;
const glyph = okc ? "✓" : "✗";
const label = (PHASE_LABELS[phaseId] ?? phaseId).toLowerCase();
const dur = formatDuration(durationMs);
const header = `${glyph} pygienium ${opts.check.label} · ${label} (${dur})`;
send(header, {
phase: "agent",
phaseId,
toolCalls,
error: okc ? undefined : error,
});
}
/**
* Remove transient per-check scratch artifacts (e.g. agent-extracted
* manifests) written under `<cwd>/.pygienium/<check>-tmp-*`. Findings and