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:
160
src/index.ts
160
src/index.ts
@@ -21,15 +21,45 @@ import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionCommandContext,
|
||||
ExtensionContext,
|
||||
MessageRenderer,
|
||||
SessionStartEvent,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { registerPygieniumCommands } from "./commands.js";
|
||||
import { Box, Text } from "@earendil-works/pi-tui";
|
||||
import { registerPygieniumCommands, type PygieniumCtx } from "./commands.js";
|
||||
import {
|
||||
type SendChatMessage,
|
||||
type CheckCompletionDetails,
|
||||
type ToolCallEntry,
|
||||
PHASE_GLYPH,
|
||||
} from "./phases.js";
|
||||
|
||||
/** Startup hint mirrored after piolium's convention. */
|
||||
export const PYGIENIUM_STARTUP_HINT =
|
||||
"Pygienium loaded. Run /pygienium-help for available checks and flags.";
|
||||
|
||||
/**
|
||||
* Local structural supertypes for the progress-message renderer params.
|
||||
* These avoid relying on contextual typing from `MessageRenderer` (which
|
||||
* requires resolving pi's internal `Theme`/`CustomMessage` cross-references
|
||||
* via node_modules — not always available in dev environments). Using
|
||||
* `(...args: any[])` for theme methods makes the type bidirectionally
|
||||
* compatible under strict function types, so the cast to `MessageRenderer`
|
||||
* in `registerMessageRenderer` is valid.
|
||||
*/
|
||||
interface ProgressMessage {
|
||||
content: unknown;
|
||||
details?: unknown;
|
||||
}
|
||||
interface ProgressRenderOptions {
|
||||
expanded: boolean;
|
||||
}
|
||||
interface ProgressTheme {
|
||||
fg: (...args: any[]) => string;
|
||||
bg: (...args: any[]) => string;
|
||||
}
|
||||
|
||||
export { buildPygieniumHelpLines } from "./help.js";
|
||||
|
||||
/**
|
||||
@@ -53,16 +83,142 @@ async function loadCheckModules(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Max tool calls shown collapsed in a chat message (matches ralpi). */
|
||||
const RENDERER_MAX_COLLAPSED = 3;
|
||||
|
||||
/**
|
||||
* Create a callback to send messages to the main chat window.
|
||||
*/
|
||||
function makeSendChatMessage(pi: ExtensionAPI): SendChatMessage {
|
||||
return (content: string, meta?: Record<string, unknown>) => {
|
||||
pi.sendMessage({
|
||||
customType: "pygienium-progress",
|
||||
content,
|
||||
display: true,
|
||||
details: {
|
||||
phase: meta?.phase || "info",
|
||||
...meta,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default async function pygieniumExtension(
|
||||
pi: ExtensionAPI,
|
||||
): Promise<void> {
|
||||
// Self-register every shipped check before wiring commands.
|
||||
await loadCheckModules();
|
||||
|
||||
const sendChatMessage = makeSendChatMessage(pi);
|
||||
|
||||
// Register custom message renderer for pygienium progress messages.
|
||||
// Renders an expandable phase tree: collapsed shows the header + a hint,
|
||||
// expanded (Ctrl+O) shows every phase with its status and notes.
|
||||
const progressRenderer = (
|
||||
message: ProgressMessage,
|
||||
{ expanded }: ProgressRenderOptions,
|
||||
theme: ProgressTheme,
|
||||
) => {
|
||||
const details = message.details as
|
||||
| {
|
||||
phase?: string;
|
||||
completion?: CheckCompletionDetails;
|
||||
toolCalls?: ToolCallEntry[];
|
||||
error?: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(String(message.content));
|
||||
|
||||
const completion = details?.completion;
|
||||
const toolCalls = details?.toolCalls;
|
||||
if (completion) {
|
||||
if (expanded) {
|
||||
// Expanded: show every phase with status glyph + branch.
|
||||
const phases = completion.phases;
|
||||
for (let i = 0; i < phases.length; i++) {
|
||||
const entry = phases[i];
|
||||
if (!entry) continue;
|
||||
const isLast = i === phases.length - 1;
|
||||
const branch = isLast ? " └── " : " ├── ";
|
||||
const glyph = PHASE_GLYPH[entry.status] ?? "?";
|
||||
const tag = theme.fg("accent", entry.label);
|
||||
const note = entry.note ? ` · ${entry.note}` : "";
|
||||
lines.push(`${branch}${glyph} ${tag}${note}`);
|
||||
}
|
||||
if (completion.error) {
|
||||
lines.push(theme.fg("error", ` error: ${completion.error}`));
|
||||
}
|
||||
} else {
|
||||
// Collapsed: summary line + hint.
|
||||
const done = completion.phases.filter(
|
||||
(p) => p.status === "complete",
|
||||
).length;
|
||||
const total = completion.phases.length;
|
||||
const hint = completion.error
|
||||
? theme.fg("error", ` ├── ${completion.error}`)
|
||||
: theme.fg(
|
||||
"dim",
|
||||
` ├── ${done}/${total} phases · press Ctrl+O for detail`,
|
||||
);
|
||||
lines.push(hint);
|
||||
}
|
||||
} else if (toolCalls && toolCalls.length > 0) {
|
||||
const all = toolCalls;
|
||||
if (expanded) {
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
const entry = all[i]!;
|
||||
const isLast = i === all.length - 1;
|
||||
const branch = isLast ? " └── " : " ├── ";
|
||||
const tag = theme.fg("accent", `[${entry.name}]`);
|
||||
lines.push(`${branch}${tag} ${entry.label}`);
|
||||
}
|
||||
} else {
|
||||
const shown = all.slice(-RENDERER_MAX_COLLAPSED);
|
||||
const remaining = all.length - shown.length;
|
||||
if (remaining > 0) {
|
||||
lines.push(theme.fg("dim", ` ├── ${remaining} more`));
|
||||
}
|
||||
for (let i = 0; i < shown.length; i++) {
|
||||
const entry = shown[i]!;
|
||||
const isLast = i === shown.length - 1;
|
||||
const branch = isLast ? " └── " : " ├── ";
|
||||
const tag = theme.fg("accent", `[${entry.name}]`);
|
||||
lines.push(`${branch}${tag} ${entry.label}`);
|
||||
}
|
||||
}
|
||||
if (details?.error) {
|
||||
lines.push(theme.fg("error", ` error: ${details.error}`));
|
||||
}
|
||||
} else if (!expanded) {
|
||||
lines.push(theme.fg("dim", " ├── press Ctrl+O for detail"));
|
||||
}
|
||||
|
||||
const text = lines.join("\n");
|
||||
const box = new Box(1, 1, (t: string) => theme.bg("customMessageBg", t));
|
||||
box.addChild(new Text(text, 0, 0));
|
||||
return box;
|
||||
};
|
||||
pi.registerMessageRenderer(
|
||||
"pygienium-progress",
|
||||
progressRenderer as MessageRenderer,
|
||||
);
|
||||
|
||||
registerPygieniumCommands((name, options) => {
|
||||
pi.registerCommand(name, {
|
||||
description: options.description,
|
||||
handler: options.handler,
|
||||
handler: (args: string, ctx: ExtensionCommandContext) => {
|
||||
// Create PygieniumCtx with sendChatMessage callback
|
||||
const pygieniumCtx: PygieniumCtx = {
|
||||
cwd: ctx.cwd,
|
||||
mode: ctx.mode,
|
||||
hasUI: ctx.hasUI,
|
||||
ui: ctx.ui,
|
||||
sendChatMessage,
|
||||
};
|
||||
return options.handler(args, pygieniumCtx);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user