feat: calvana application microsite + ship-log extension
- Static site: /manifesto, /live, /hire pages - Ship-log Pi extension: calvana_ship, calvana_oops, calvana_deploy tools - Docker + nginx deploy to calvana.quikcue.com - Terminal-ish dark aesthetic, mobile responsive - Auto-updating /live page from extension state
This commit is contained in:
971
extensions/agent-dashboard.ts
Normal file
971
extensions/agent-dashboard.ts
Normal file
@@ -0,0 +1,971 @@
|
||||
/**
|
||||
* Agent Dashboard — Unified observability across all agent interfaces
|
||||
*
|
||||
* Passively tracks agent activity from team dispatches, subagent spawns,
|
||||
* and chain pipeline runs. Provides a compact always-visible widget plus
|
||||
* a full-screen overlay with four switchable views.
|
||||
*
|
||||
* Hooks into: dispatch_agent, subagent_create, subagent_continue, run_chain
|
||||
* tool calls and their completions. Completely passive — never blocks.
|
||||
*
|
||||
* Commands:
|
||||
* /dashboard — toggle full-screen overlay
|
||||
* /dashboard clear — reset all tracked state
|
||||
*
|
||||
* Usage: pi -e extensions/agent-dashboard.ts
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||
import { Container, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { applyExtensionDefaults } from "./themeMap.ts";
|
||||
|
||||
// ── Data Types ─────────────────────────────────────────────────────────
|
||||
|
||||
type AgentInterface = "team" | "subagent" | "chain";
|
||||
|
||||
interface TrackedAgent {
|
||||
id: string;
|
||||
name: string;
|
||||
iface: AgentInterface;
|
||||
status: "running" | "done" | "error";
|
||||
task: string;
|
||||
startedAt: number;
|
||||
endedAt?: number;
|
||||
elapsed: number;
|
||||
toolCount: number;
|
||||
lastText: string;
|
||||
turnCount: number;
|
||||
chainStep?: number;
|
||||
chainName?: string;
|
||||
teamName?: string;
|
||||
}
|
||||
|
||||
interface AgentRun {
|
||||
id: string;
|
||||
name: string;
|
||||
iface: AgentInterface;
|
||||
task: string;
|
||||
status: "done" | "error";
|
||||
startedAt: number;
|
||||
endedAt: number;
|
||||
duration: number;
|
||||
toolCount: number;
|
||||
resultPreview: string;
|
||||
chainStep?: number;
|
||||
chainName?: string;
|
||||
teamName?: string;
|
||||
}
|
||||
|
||||
interface DashboardStats {
|
||||
totalRuns: number;
|
||||
totalSuccess: number;
|
||||
totalError: number;
|
||||
totalDuration: number;
|
||||
agentRunCounts: Record<string, number>;
|
||||
ifaceCounts: Record<AgentInterface, number>;
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function fmtDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
const secs = Math.floor(ms / 1000);
|
||||
if (secs < 60) return `${secs}s`;
|
||||
const mins = Math.floor(secs / 60);
|
||||
const remSecs = secs % 60;
|
||||
if (mins < 60) return `${mins}m ${remSecs}s`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
const remMins = mins % 60;
|
||||
return `${hrs}h ${remMins}m`;
|
||||
}
|
||||
|
||||
function shortId(): string {
|
||||
return Math.random().toString(36).slice(2, 6);
|
||||
}
|
||||
|
||||
function truncate(s: string, max: number): string {
|
||||
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
||||
}
|
||||
|
||||
function emptyStats(): DashboardStats {
|
||||
return {
|
||||
totalRuns: 0,
|
||||
totalSuccess: 0,
|
||||
totalError: 0,
|
||||
totalDuration: 0,
|
||||
agentRunCounts: {},
|
||||
ifaceCounts: { team: 0, subagent: 0, chain: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
// ── Extension ──────────────────────────────────────────────────────────
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
// ── State ──────────────────────────────────────────────────────────
|
||||
|
||||
const activeAgents: Map<string, TrackedAgent> = new Map();
|
||||
let history: AgentRun[] = [];
|
||||
let stats: DashboardStats = emptyStats();
|
||||
let widgetCtx: ExtensionContext | null = null;
|
||||
let tickTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// Mapping from toolCallId → tracked agent info (with timestamp for staleness)
|
||||
const pendingCalls: Map<string, { agentId: string; ts: number }> = new Map();
|
||||
|
||||
// Staleness threshold: 10 minutes
|
||||
const STALE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
// Inactivity auto-stop: stop tick after 30s with no active agents
|
||||
let lastActivityTs = Date.now();
|
||||
|
||||
// ── Tracked tool names ─────────────────────────────────────────────
|
||||
|
||||
const TRACKED_TOOLS = new Set([
|
||||
"dispatch_agent",
|
||||
"subagent_create",
|
||||
"subagent_continue",
|
||||
"run_chain",
|
||||
]);
|
||||
|
||||
// ── State Management ───────────────────────────────────────────────
|
||||
|
||||
function clearState() {
|
||||
activeAgents.clear();
|
||||
history = [];
|
||||
stats = emptyStats();
|
||||
pendingCalls.clear();
|
||||
lastActivityTs = Date.now();
|
||||
}
|
||||
|
||||
function addToHistory(agent: TrackedAgent) {
|
||||
const run: AgentRun = {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
iface: agent.iface,
|
||||
task: agent.task,
|
||||
status: agent.status === "error" ? "error" : "done",
|
||||
startedAt: agent.startedAt,
|
||||
endedAt: agent.endedAt || Date.now(),
|
||||
duration: agent.elapsed,
|
||||
toolCount: agent.toolCount,
|
||||
resultPreview: truncate(agent.lastText, 200),
|
||||
chainStep: agent.chainStep,
|
||||
chainName: agent.chainName,
|
||||
teamName: agent.teamName,
|
||||
};
|
||||
|
||||
history.push(run);
|
||||
// Ring buffer capped at 200
|
||||
if (history.length > 200) {
|
||||
history = history.slice(-200);
|
||||
}
|
||||
|
||||
// Update stats
|
||||
stats.totalRuns++;
|
||||
if (run.status === "done") stats.totalSuccess++;
|
||||
else stats.totalError++;
|
||||
stats.totalDuration += run.duration;
|
||||
stats.agentRunCounts[run.name] = (stats.agentRunCounts[run.name] || 0) + 1;
|
||||
stats.ifaceCounts[run.iface] = (stats.ifaceCounts[run.iface] || 0) + 1;
|
||||
}
|
||||
|
||||
// ── Tick Timer ─────────────────────────────────────────────────────
|
||||
|
||||
function startTick() {
|
||||
if (tickTimer) return;
|
||||
tickTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
|
||||
// Update elapsed on running agents
|
||||
for (const agent of activeAgents.values()) {
|
||||
if (agent.status === "running") {
|
||||
agent.elapsed = now - agent.startedAt;
|
||||
}
|
||||
}
|
||||
|
||||
// Staleness check: expire pending calls older than 10 minutes
|
||||
for (const [callId, pending] of pendingCalls) {
|
||||
if (now - pending.ts > STALE_TIMEOUT_MS) {
|
||||
pendingCalls.delete(callId);
|
||||
const agent = activeAgents.get(pending.agentId);
|
||||
if (agent && agent.status === "running") {
|
||||
agent.status = "error";
|
||||
agent.endedAt = now;
|
||||
agent.elapsed = now - agent.startedAt;
|
||||
agent.lastText = "Timed out (no completion after 10m)";
|
||||
addToHistory(agent);
|
||||
activeAgents.delete(pending.agentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-stop tick after 30s of inactivity (no active agents, no pending calls)
|
||||
if (activeAgents.size === 0 && pendingCalls.size === 0) {
|
||||
if (now - lastActivityTs > 30_000) {
|
||||
stopTick();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
lastActivityTs = now;
|
||||
}
|
||||
|
||||
updateWidget();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTick() {
|
||||
if (tickTimer) {
|
||||
clearInterval(tickTimer);
|
||||
tickTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Widget Rendering ───────────────────────────────────────────────
|
||||
|
||||
function updateWidget() {
|
||||
if (!widgetCtx) return;
|
||||
|
||||
try {
|
||||
widgetCtx.ui.setWidget("agent-dashboard", (_tui, theme) => {
|
||||
const container = new Container();
|
||||
const borderFn = (s: string) => theme.fg("accent", s);
|
||||
|
||||
container.addChild(new DynamicBorder(borderFn));
|
||||
|
||||
const headerText = new Text("", 1, 0);
|
||||
container.addChild(headerText);
|
||||
|
||||
const agentLines: Text[] = [];
|
||||
// Pre-allocate up to 4 lines for active agents
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const t = new Text("", 1, 0);
|
||||
agentLines.push(t);
|
||||
container.addChild(t);
|
||||
}
|
||||
|
||||
const hintText = new Text("", 1, 0);
|
||||
container.addChild(hintText);
|
||||
|
||||
container.addChild(new DynamicBorder(borderFn));
|
||||
|
||||
return {
|
||||
render(width: number): string[] {
|
||||
const activeCount = activeAgents.size;
|
||||
const doneCount = stats.totalSuccess;
|
||||
const errorCount = stats.totalError;
|
||||
|
||||
// Line 1: summary bar
|
||||
const line1 =
|
||||
theme.fg("accent", " 📊 Dashboard") +
|
||||
theme.fg("dim", " │ Active: ") + theme.fg(activeCount > 0 ? "accent" : "muted", `${activeCount}`) +
|
||||
theme.fg("dim", " │ Done: ") + theme.fg("success", `${doneCount}`) +
|
||||
theme.fg("dim", " │ Errors: ") + theme.fg(errorCount > 0 ? "error" : "muted", `${errorCount}`);
|
||||
headerText.setText(truncateToWidth(line1, width - 4));
|
||||
|
||||
// Active agent lines
|
||||
const agents = Array.from(activeAgents.values());
|
||||
for (let i = 0; i < agentLines.length; i++) {
|
||||
if (i < agents.length) {
|
||||
const a = agents[i];
|
||||
const icon = a.status === "running" ? "⟳"
|
||||
: a.status === "done" ? "✓" : "✗";
|
||||
const statusColor = a.status === "running" ? "accent"
|
||||
: a.status === "done" ? "success" : "error";
|
||||
const ifaceTag = theme.fg("dim", `[${a.iface}]`);
|
||||
const elapsed = theme.fg("muted", fmtDuration(a.elapsed));
|
||||
const tools = theme.fg("dim", `🔧${a.toolCount}`);
|
||||
const lastText = a.lastText
|
||||
? theme.fg("muted", truncate(a.lastText, Math.max(20, width - 60)))
|
||||
: "";
|
||||
|
||||
const line =
|
||||
" " + theme.fg(statusColor, icon) + " " +
|
||||
theme.fg("accent", truncate(a.name, 16)) + " " +
|
||||
ifaceTag + " " +
|
||||
elapsed + " " +
|
||||
tools +
|
||||
(lastText ? theme.fg("dim", " │ ") + lastText : "");
|
||||
agentLines[i].setText(truncateToWidth(line, width - 4));
|
||||
} else {
|
||||
agentLines[i].setText("");
|
||||
}
|
||||
}
|
||||
|
||||
// Hint line
|
||||
const hintLine =
|
||||
theme.fg("dim", " /dashboard") + theme.fg("muted", " — full view") +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", `${stats.totalRuns} total runs`) +
|
||||
(stats.totalDuration > 0
|
||||
? theme.fg("dim", " │ avg ") + theme.fg("muted", fmtDuration(Math.round(stats.totalDuration / Math.max(1, stats.totalRuns))))
|
||||
: "");
|
||||
hintText.setText(truncateToWidth(hintLine, width - 4));
|
||||
|
||||
return container.render(width);
|
||||
},
|
||||
invalidate() {
|
||||
container.invalidate();
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ── Overlay ────────────────────────────────────────────────────────
|
||||
|
||||
async function openOverlay(ctx: ExtensionContext) {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
let currentView = 0; // 0=Live, 1=History, 2=Interfaces, 3=Stats
|
||||
let scrollOffset = 0;
|
||||
|
||||
const viewNames = ["1:Live", "2:History", "3:Interfaces", "4:Stats"];
|
||||
|
||||
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
||||
return {
|
||||
render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
// ── Header ──
|
||||
lines.push("");
|
||||
const tabs = viewNames.map((name, i) =>
|
||||
i === currentView
|
||||
? theme.fg("accent", theme.bold(`[${name}]`))
|
||||
: theme.fg("dim", `[${name}]`)
|
||||
).join(" ");
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold("📊 Agent Dashboard")) +
|
||||
" ".repeat(Math.max(1, width - 20 - viewNames.join(" ").length - 2)) +
|
||||
tabs,
|
||||
width,
|
||||
));
|
||||
lines.push(theme.fg("dim", "─".repeat(width)));
|
||||
|
||||
// ── View content ──
|
||||
const contentLines = renderView(currentView, width, theme, scrollOffset);
|
||||
lines.push(...contentLines);
|
||||
|
||||
// ── Footer controls ──
|
||||
lines.push("");
|
||||
lines.push(theme.fg("dim", "─".repeat(width)));
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("dim", "1-4/Tab: views │ j/k: scroll │ c: clear │ q/Esc: close"),
|
||||
width,
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
return lines;
|
||||
},
|
||||
handleInput(data: string) {
|
||||
if (matchesKey(data, "escape") || data === "q") {
|
||||
done(undefined);
|
||||
return;
|
||||
}
|
||||
if (data === "1") { currentView = 0; scrollOffset = 0; }
|
||||
else if (data === "2") { currentView = 1; scrollOffset = 0; }
|
||||
else if (data === "3") { currentView = 2; scrollOffset = 0; }
|
||||
else if (data === "4") { currentView = 3; scrollOffset = 0; }
|
||||
else if (data === "\t") { currentView = (currentView + 1) % 4; scrollOffset = 0; }
|
||||
else if (matchesKey(data, "up") || data === "k") { scrollOffset = Math.max(0, scrollOffset - 1); }
|
||||
else if (matchesKey(data, "down") || data === "j") { scrollOffset++; }
|
||||
else if (matchesKey(data, "pageUp")) { scrollOffset = Math.max(0, scrollOffset - 20); }
|
||||
else if (matchesKey(data, "pageDown")) { scrollOffset += 20; }
|
||||
else if (data === "c") {
|
||||
clearState();
|
||||
scrollOffset = 0;
|
||||
}
|
||||
_tui.requestRender();
|
||||
},
|
||||
invalidate() {},
|
||||
};
|
||||
}, {
|
||||
overlay: true,
|
||||
overlayOptions: { width: "90%", anchor: "center" },
|
||||
});
|
||||
}
|
||||
|
||||
// ── View Renderers ─────────────────────────────────────────────────
|
||||
|
||||
function renderView(view: number, width: number, theme: any, offset: number): string[] {
|
||||
switch (view) {
|
||||
case 0: return renderLiveView(width, theme, offset);
|
||||
case 1: return renderHistoryView(width, theme, offset);
|
||||
case 2: return renderInterfacesView(width, theme, offset);
|
||||
case 3: return renderStatsView(width, theme, offset);
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── View 1: Live ───────────────────────────────────────────────────
|
||||
|
||||
function renderLiveView(width: number, theme: any, offset: number): string[] {
|
||||
const lines: string[] = [];
|
||||
const agents = Array.from(activeAgents.values());
|
||||
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold("Active Agents")) +
|
||||
theme.fg("dim", ` (${agents.length} running)`),
|
||||
width,
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
if (agents.length === 0) {
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("dim", "No agents currently running. Activity will appear here when"),
|
||||
width,
|
||||
));
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("dim", "dispatch_agent, subagent_create, subagent_continue, or run_chain is called."),
|
||||
width,
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
// Show recent completions as context
|
||||
if (history.length > 0) {
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("muted", `Last completed: ${history.length} agents`),
|
||||
width,
|
||||
));
|
||||
const recent = history.slice(-3).reverse();
|
||||
for (const run of recent) {
|
||||
const icon = run.status === "done" ? "✓" : "✗";
|
||||
const color = run.status === "done" ? "success" : "error";
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg(color, `${icon} ${run.name}`) +
|
||||
theme.fg("dim", ` [${run.iface}] `) +
|
||||
theme.fg("muted", fmtDuration(run.duration)) +
|
||||
theme.fg("dim", " — ") +
|
||||
theme.fg("muted", truncate(run.task, 50)),
|
||||
width,
|
||||
));
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
const allLines: string[] = [];
|
||||
for (const agent of agents) {
|
||||
const icon = agent.status === "running" ? "●"
|
||||
: agent.status === "done" ? "✓" : "✗";
|
||||
const statusColor = agent.status === "running" ? "accent"
|
||||
: agent.status === "done" ? "success" : "error";
|
||||
|
||||
// Card top
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("dim", "┌─ ") +
|
||||
theme.fg(statusColor, `${icon} ${agent.name}`) +
|
||||
theme.fg("dim", ` [${agent.iface}]`) +
|
||||
(agent.chainName ? theme.fg("dim", ` chain:${agent.chainName}`) : "") +
|
||||
(agent.teamName ? theme.fg("dim", ` team:${agent.teamName}`) : "") +
|
||||
(agent.chainStep !== undefined ? theme.fg("dim", ` step:${agent.chainStep}`) : "") +
|
||||
theme.fg("dim", " ─".repeat(Math.max(0, Math.floor((width - 50) / 2)))),
|
||||
width,
|
||||
));
|
||||
|
||||
// Task
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("dim", "│ ") +
|
||||
theme.fg("muted", "Task: ") +
|
||||
theme.fg("accent", truncate(agent.task, width - 20)),
|
||||
width,
|
||||
));
|
||||
|
||||
// Metrics
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("dim", "│ ") +
|
||||
theme.fg("muted", "Elapsed: ") + theme.fg("success", fmtDuration(agent.elapsed)) +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", "Tools: ") + theme.fg("accent", `${agent.toolCount}`) +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", "Turns: ") + theme.fg("accent", `${agent.turnCount}`),
|
||||
width,
|
||||
));
|
||||
|
||||
// Streaming text
|
||||
if (agent.lastText) {
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("dim", "│ ") +
|
||||
theme.fg("muted", truncate(agent.lastText, width - 10)),
|
||||
width,
|
||||
));
|
||||
}
|
||||
|
||||
// Card bottom
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("dim", "└" + "─".repeat(Math.max(0, width - 5))),
|
||||
width,
|
||||
));
|
||||
allLines.push("");
|
||||
}
|
||||
|
||||
const visible = allLines.slice(offset);
|
||||
lines.push(...visible);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── View 2: History ────────────────────────────────────────────────
|
||||
|
||||
function renderHistoryView(width: number, theme: any, offset: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold("Completed Runs")) +
|
||||
theme.fg("dim", ` (${history.length} total)`),
|
||||
width,
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
if (history.length === 0) {
|
||||
lines.push(truncateToWidth(" " + theme.fg("dim", "No completed runs yet."), width));
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Table header
|
||||
const hdr =
|
||||
theme.fg("accent", " Status") +
|
||||
theme.fg("accent", " │ Name ") +
|
||||
theme.fg("accent", " │ Interface ") +
|
||||
theme.fg("accent", " │ Duration ") +
|
||||
theme.fg("accent", " │ Tools ") +
|
||||
theme.fg("accent", " │ Task");
|
||||
lines.push(truncateToWidth(hdr, width));
|
||||
lines.push(truncateToWidth(" " + theme.fg("dim", "─".repeat(Math.min(80, width - 4))), width));
|
||||
|
||||
// Show newest first
|
||||
const rows: string[] = [];
|
||||
const reversed = [...history].reverse();
|
||||
for (const run of reversed) {
|
||||
const icon = run.status === "done" ? "✓" : "✗";
|
||||
const color = run.status === "done" ? "success" : "error";
|
||||
const ifaceLabel = run.iface.padEnd(9);
|
||||
const nameLabel = truncate(run.name, 14).padEnd(14);
|
||||
const durLabel = fmtDuration(run.duration).padEnd(8);
|
||||
const toolLabel = String(run.toolCount).padStart(5);
|
||||
const taskPreview = truncate(run.task, Math.max(10, width - 70));
|
||||
|
||||
const row =
|
||||
" " + theme.fg(color, ` ${icon} `) +
|
||||
theme.fg("dim", " │ ") + theme.fg("accent", nameLabel) +
|
||||
theme.fg("dim", " │ ") + theme.fg("muted", ifaceLabel) +
|
||||
theme.fg("dim", " │ ") + theme.fg("success", durLabel) +
|
||||
theme.fg("dim", " │ ") + theme.fg("accent", toolLabel) +
|
||||
theme.fg("dim", " │ ") + theme.fg("muted", taskPreview);
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
const visible = rows.slice(offset);
|
||||
for (const row of visible) {
|
||||
lines.push(truncateToWidth(row, width));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── View 3: Interfaces ─────────────────────────────────────────────
|
||||
|
||||
function renderInterfacesView(width: number, theme: any, offset: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold("Agents by Interface")),
|
||||
width,
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
const ifaceLabels: Record<AgentInterface, string> = {
|
||||
team: "🏢 Team (dispatch_agent)",
|
||||
subagent: "🤖 Subagent (subagent_create/continue)",
|
||||
chain: "🔗 Chain (run_chain)",
|
||||
};
|
||||
|
||||
const allLines: string[] = [];
|
||||
|
||||
for (const iface of ["team", "subagent", "chain"] as AgentInterface[]) {
|
||||
const activeForIface = Array.from(activeAgents.values()).filter(a => a.iface === iface);
|
||||
const historyForIface = history.filter(r => r.iface === iface);
|
||||
const totalCount = stats.ifaceCounts[iface] || 0;
|
||||
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold(ifaceLabels[iface])) +
|
||||
theme.fg("dim", ` — ${activeForIface.length} active, ${totalCount} completed`),
|
||||
width,
|
||||
));
|
||||
allLines.push(truncateToWidth(" " + theme.fg("dim", "─".repeat(Math.min(60, width - 6))), width));
|
||||
|
||||
// Active
|
||||
if (activeForIface.length > 0) {
|
||||
for (const agent of activeForIface) {
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", "● ") +
|
||||
theme.fg("accent", agent.name) +
|
||||
theme.fg("dim", " — ") +
|
||||
theme.fg("success", fmtDuration(agent.elapsed)) +
|
||||
theme.fg("dim", " │ 🔧") + theme.fg("muted", `${agent.toolCount}`) +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", truncate(agent.task, 40)),
|
||||
width,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Recent completed (last 5)
|
||||
const recent = historyForIface.slice(-5).reverse();
|
||||
if (recent.length > 0) {
|
||||
for (const run of recent) {
|
||||
const icon = run.status === "done" ? "✓" : "✗";
|
||||
const color = run.status === "done" ? "success" : "error";
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg(color, `${icon} `) +
|
||||
theme.fg("muted", run.name) +
|
||||
theme.fg("dim", " — ") +
|
||||
theme.fg("muted", fmtDuration(run.duration)) +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", truncate(run.task, 40)),
|
||||
width,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (activeForIface.length === 0 && recent.length === 0) {
|
||||
allLines.push(truncateToWidth(" " + theme.fg("dim", "No activity recorded."), width));
|
||||
}
|
||||
|
||||
allLines.push("");
|
||||
}
|
||||
|
||||
const visible = allLines.slice(offset);
|
||||
lines.push(...visible);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── View 4: Stats ──────────────────────────────────────────────────
|
||||
|
||||
function renderStatsView(width: number, theme: any, offset: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold("Aggregate Statistics")),
|
||||
width,
|
||||
));
|
||||
lines.push("");
|
||||
|
||||
const avgDur = stats.totalRuns > 0
|
||||
? fmtDuration(Math.round(stats.totalDuration / stats.totalRuns))
|
||||
: "—";
|
||||
const successRate = stats.totalRuns > 0
|
||||
? `${Math.round((stats.totalSuccess / stats.totalRuns) * 100)}%`
|
||||
: "—";
|
||||
|
||||
const allLines: string[] = [];
|
||||
|
||||
// Summary cards
|
||||
allLines.push(truncateToWidth(
|
||||
" " +
|
||||
theme.fg("muted", "Total Runs: ") + theme.fg("accent", `${stats.totalRuns}`) +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", "Success: ") + theme.fg("success", `${stats.totalSuccess}`) +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", "Errors: ") + theme.fg(stats.totalError > 0 ? "error" : "muted", `${stats.totalError}`) +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", "Success Rate: ") + theme.fg("success", successRate),
|
||||
width,
|
||||
));
|
||||
allLines.push(truncateToWidth(
|
||||
" " +
|
||||
theme.fg("muted", "Total Duration: ") + theme.fg("success", fmtDuration(stats.totalDuration)) +
|
||||
theme.fg("dim", " │ ") +
|
||||
theme.fg("muted", "Avg Duration: ") + theme.fg("accent", avgDur),
|
||||
width,
|
||||
));
|
||||
allLines.push("");
|
||||
|
||||
// Interface breakdown
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold("Interface Breakdown")),
|
||||
width,
|
||||
));
|
||||
allLines.push("");
|
||||
|
||||
const ifaceTotal = Math.max(1, stats.ifaceCounts.team + stats.ifaceCounts.subagent + stats.ifaceCounts.chain);
|
||||
const barWidth = Math.min(30, Math.floor(width * 0.3));
|
||||
|
||||
for (const [iface, label] of [["team", "Team "], ["subagent", "Subagent "], ["chain", "Chain "]] as [AgentInterface, string][]) {
|
||||
const count = stats.ifaceCounts[iface] || 0;
|
||||
const ratio = count / ifaceTotal;
|
||||
const filled = Math.round(ratio * barWidth);
|
||||
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
|
||||
|
||||
allLines.push(truncateToWidth(
|
||||
" " +
|
||||
theme.fg("accent", label) + " " +
|
||||
theme.fg("success", bar) + " " +
|
||||
theme.fg("muted", `${count}`) +
|
||||
theme.fg("dim", ` (${Math.round(ratio * 100)}%)`),
|
||||
width,
|
||||
));
|
||||
}
|
||||
allLines.push("");
|
||||
|
||||
// Most-used agents bar chart
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold("Most-Used Agents")),
|
||||
width,
|
||||
));
|
||||
allLines.push("");
|
||||
|
||||
const agentEntries = Object.entries(stats.agentRunCounts).sort((a, b) => b[1] - a[1]);
|
||||
|
||||
if (agentEntries.length === 0) {
|
||||
allLines.push(truncateToWidth(" " + theme.fg("dim", "No agent runs recorded yet."), width));
|
||||
} else {
|
||||
const maxCount = agentEntries[0][1];
|
||||
for (const [name, count] of agentEntries.slice(0, 15)) {
|
||||
const ratio = maxCount > 0 ? count / maxCount : 0;
|
||||
const filled = Math.round(ratio * barWidth);
|
||||
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
|
||||
|
||||
allLines.push(truncateToWidth(
|
||||
" " +
|
||||
theme.fg("accent", name.padEnd(16)) + " " +
|
||||
theme.fg("success", bar) + " " +
|
||||
theme.fg("muted", `${count}`),
|
||||
width,
|
||||
));
|
||||
}
|
||||
}
|
||||
allLines.push("");
|
||||
|
||||
// Per-agent average durations
|
||||
if (history.length > 0) {
|
||||
allLines.push(truncateToWidth(
|
||||
" " + theme.fg("accent", theme.bold("Average Duration by Agent")),
|
||||
width,
|
||||
));
|
||||
allLines.push("");
|
||||
|
||||
const durByAgent: Record<string, number[]> = {};
|
||||
for (const run of history) {
|
||||
if (!durByAgent[run.name]) durByAgent[run.name] = [];
|
||||
durByAgent[run.name].push(run.duration);
|
||||
}
|
||||
|
||||
const durEntries = Object.entries(durByAgent).sort((a, b) => {
|
||||
const avgA = a[1].reduce((s, v) => s + v, 0) / a[1].length;
|
||||
const avgB = b[1].reduce((s, v) => s + v, 0) / b[1].length;
|
||||
return avgB - avgA;
|
||||
});
|
||||
|
||||
for (const [name, durations] of durEntries) {
|
||||
const avg = durations.reduce((s, v) => s + v, 0) / durations.length;
|
||||
const min = Math.min(...durations);
|
||||
const max = Math.max(...durations);
|
||||
|
||||
allLines.push(truncateToWidth(
|
||||
" " +
|
||||
theme.fg("accent", name.padEnd(16)) +
|
||||
theme.fg("dim", " avg: ") + theme.fg("success", fmtDuration(Math.round(avg)).padEnd(8)) +
|
||||
theme.fg("dim", " min: ") + theme.fg("muted", fmtDuration(min).padEnd(8)) +
|
||||
theme.fg("dim", " max: ") + theme.fg("muted", fmtDuration(max).padEnd(8)) +
|
||||
theme.fg("dim", " runs: ") + theme.fg("muted", `${durations.length}`),
|
||||
width,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const visible = allLines.slice(offset);
|
||||
lines.push(...visible);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── Commands ───────────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("dashboard", {
|
||||
description: "Open Agent Dashboard overlay. Args: clear",
|
||||
handler: async (args, ctx) => {
|
||||
widgetCtx = ctx;
|
||||
const arg = (args || "").trim().toLowerCase();
|
||||
|
||||
if (arg === "clear") {
|
||||
stopTick();
|
||||
clearState();
|
||||
startTick();
|
||||
ctx.ui.notify("📊 Dashboard: All data cleared.", "info");
|
||||
updateWidget();
|
||||
return;
|
||||
}
|
||||
|
||||
await openOverlay(ctx);
|
||||
},
|
||||
});
|
||||
|
||||
// ── Event Handlers ─────────────────────────────────────────────────
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
applyExtensionDefaults(import.meta.url, ctx);
|
||||
stopTick();
|
||||
widgetCtx = ctx;
|
||||
clearState();
|
||||
startTick();
|
||||
updateWidget();
|
||||
});
|
||||
|
||||
pi.on("before_agent_start", async (_event, ctx) => {
|
||||
widgetCtx = ctx;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
pi.on("agent_end", async (_event, ctx) => {
|
||||
widgetCtx = ctx;
|
||||
updateWidget();
|
||||
});
|
||||
|
||||
pi.on("tool_call", async (event, _ctx) => {
|
||||
try {
|
||||
const toolName = event.toolName;
|
||||
if (!TRACKED_TOOLS.has(toolName)) return undefined;
|
||||
|
||||
const input = event.input;
|
||||
const now = Date.now();
|
||||
const callId = event.toolCallId;
|
||||
lastActivityTs = now;
|
||||
|
||||
if (toolName === "dispatch_agent") {
|
||||
const agentName = (input.agent as string) || "unknown";
|
||||
const task = (input.task as string) || "";
|
||||
const id = `team:${agentName}:${shortId()}`;
|
||||
|
||||
const tracked: TrackedAgent = {
|
||||
id,
|
||||
name: agentName,
|
||||
iface: "team",
|
||||
status: "running",
|
||||
task,
|
||||
startedAt: now,
|
||||
elapsed: 0,
|
||||
toolCount: 0,
|
||||
lastText: "",
|
||||
turnCount: 1,
|
||||
teamName: agentName,
|
||||
};
|
||||
|
||||
activeAgents.set(id, tracked);
|
||||
pendingCalls.set(callId, { agentId: id, ts: now });
|
||||
|
||||
} else if (toolName === "subagent_create") {
|
||||
const task = (input.task as string) || "";
|
||||
const id = `sub:create:${shortId()}`;
|
||||
|
||||
const tracked: TrackedAgent = {
|
||||
id,
|
||||
name: "Subagent",
|
||||
iface: "subagent",
|
||||
status: "running",
|
||||
task,
|
||||
startedAt: now,
|
||||
elapsed: 0,
|
||||
toolCount: 0,
|
||||
lastText: "",
|
||||
turnCount: 1,
|
||||
};
|
||||
|
||||
activeAgents.set(id, tracked);
|
||||
pendingCalls.set(callId, { agentId: id, ts: now });
|
||||
|
||||
} else if (toolName === "subagent_continue") {
|
||||
// Always create a new tracking entry using the widget's ID from input
|
||||
const subId = input.id;
|
||||
const prompt = (input.prompt as string) || "";
|
||||
const id = `sub:cont:${subId}:${shortId()}`;
|
||||
|
||||
const tracked: TrackedAgent = {
|
||||
id,
|
||||
name: `Subagent #${subId}`,
|
||||
iface: "subagent",
|
||||
status: "running",
|
||||
task: prompt,
|
||||
startedAt: now,
|
||||
elapsed: 0,
|
||||
toolCount: 0,
|
||||
lastText: "",
|
||||
turnCount: 1,
|
||||
};
|
||||
|
||||
activeAgents.set(id, tracked);
|
||||
pendingCalls.set(callId, { agentId: id, ts: now });
|
||||
|
||||
} else if (toolName === "run_chain") {
|
||||
const task = (input.task as string) || "";
|
||||
const id = `chain:${shortId()}`;
|
||||
|
||||
const tracked: TrackedAgent = {
|
||||
id,
|
||||
name: "chain",
|
||||
iface: "chain",
|
||||
status: "running",
|
||||
task,
|
||||
startedAt: now,
|
||||
elapsed: 0,
|
||||
toolCount: 0,
|
||||
lastText: "",
|
||||
turnCount: 1,
|
||||
chainName: "pipeline",
|
||||
};
|
||||
|
||||
activeAgents.set(id, tracked);
|
||||
pendingCalls.set(callId, { agentId: id, ts: now });
|
||||
}
|
||||
|
||||
// Ensure tick is running when we have active agents
|
||||
startTick();
|
||||
updateWidget();
|
||||
} catch {}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
pi.on("tool_execution_end", async (event) => {
|
||||
try {
|
||||
const toolName = event.toolName;
|
||||
if (!TRACKED_TOOLS.has(toolName)) return;
|
||||
|
||||
const now = Date.now();
|
||||
const callId = event.toolCallId;
|
||||
lastActivityTs = now;
|
||||
|
||||
const pending = pendingCalls.get(callId);
|
||||
if (pending) {
|
||||
pendingCalls.delete(callId);
|
||||
|
||||
const agent = activeAgents.get(pending.agentId);
|
||||
if (agent) {
|
||||
agent.status = event.isError ? "error" : "done";
|
||||
agent.endedAt = now;
|
||||
agent.elapsed = now - agent.startedAt;
|
||||
|
||||
// Extract result preview if available
|
||||
try {
|
||||
const result = event.result;
|
||||
if (result?.content) {
|
||||
for (const block of result.content) {
|
||||
if (block.type === "text" && block.text) {
|
||||
agent.lastText = block.text.slice(0, 200);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Move to history
|
||||
addToHistory(agent);
|
||||
activeAgents.delete(pending.agentId);
|
||||
}
|
||||
}
|
||||
|
||||
updateWidget();
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user