Add 5 pi extensions: pi-subagents, pi-crew, rpiv-pi, pi-interactive-shell, pi-intercom
This commit is contained in:
460
extensions/pi-crew/src/ui/run-dashboard.ts
Normal file
460
extensions/pi-crew/src/ui/run-dashboard.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import * as fs from "node:fs";
|
||||
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
||||
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
||||
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
||||
import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
||||
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
||||
import type { CrewTheme } from "./theme-adapter.ts";
|
||||
import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
|
||||
import { applyStatusColor, iconForStatus, type RunStatus } from "./status-colors.ts";
|
||||
import { pad, truncate } from "../utils/visual.ts";
|
||||
import { Box, Text } from "./layout-primitives.ts";
|
||||
import { DynamicCrewBorder } from "./dynamic-border.ts";
|
||||
import { CrewFooter } from "./crew-footer.ts";
|
||||
import { aggregateUsage } from "../state/usage.ts";
|
||||
import { logInternalError } from "../utils/internal-error.ts";
|
||||
import { renderAgentsPane } from "./dashboard-panes/agents-pane.ts";
|
||||
import { renderMailboxPane } from "./dashboard-panes/mailbox-pane.ts";
|
||||
import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
|
||||
import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
|
||||
import { renderHealthPane } from "./dashboard-panes/health-pane.ts";
|
||||
import { renderMetricsPane } from "./dashboard-panes/metrics-pane.ts";
|
||||
import { dashboardActionForKey } from "./keybinding-map.ts";
|
||||
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
||||
import { spinnerBucket, spinnerFrame } from "./spinner.ts";
|
||||
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
||||
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
||||
|
||||
interface DashboardComponent {
|
||||
invalidate(): void;
|
||||
render(width: number): string[];
|
||||
handleInput(data: string): void;
|
||||
}
|
||||
|
||||
export interface RunDashboardOptions {
|
||||
placement?: "center" | "right";
|
||||
showModel?: boolean;
|
||||
showTokens?: boolean;
|
||||
showTools?: boolean;
|
||||
snapshotCache?: RunSnapshotCache;
|
||||
runProvider?: () => TeamRunManifest[];
|
||||
registry?: MetricRegistry;
|
||||
}
|
||||
|
||||
export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "events" | "agents" | "agent-events" | "agent-output" | "agent-transcript" | "mailbox" | "reload" | "mailbox-detail" | "health-recovery" | "health-kill-stale" | "health-diagnostic-export" | "notifications-dismiss";
|
||||
export interface RunDashboardSelection {
|
||||
runId: string;
|
||||
action: RunDashboardAction;
|
||||
}
|
||||
|
||||
const TASK_READ_TTL_MS = 200;
|
||||
|
||||
function formatAge(iso: string | undefined): string | undefined {
|
||||
if (!iso) return undefined;
|
||||
const ms = Math.max(0, Date.now() - new Date(iso).getTime());
|
||||
if (!Number.isFinite(ms)) return undefined;
|
||||
if (ms < 1000) return "now";
|
||||
if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
|
||||
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
|
||||
return `${Math.floor(ms / 3_600_000)}h`;
|
||||
}
|
||||
|
||||
function renderLines(lines: string[], width: number): string[] {
|
||||
const box = new Box(0, 0);
|
||||
for (const line of lines) {
|
||||
box.addChild(new Text(line));
|
||||
}
|
||||
return box.render(width);
|
||||
}
|
||||
|
||||
function readProgressPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
||||
const progress = [...run.artifacts].reverse().find((artifact) => artifact.kind === "progress");
|
||||
if (!progress) return ["Progress: (none)"];
|
||||
try {
|
||||
const progressPath = resolveRealContainedPath(run.artifactsRoot, progress.path);
|
||||
if (!fs.existsSync(progressPath)) return ["Progress: (none)"];
|
||||
return ["Progress:", ...fs.readFileSync(progressPath, "utf-8").split(/\r?\n/).filter(Boolean).slice(0, maxLines)];
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return [`Progress: failed to read (${message})`];
|
||||
}
|
||||
}
|
||||
|
||||
function formatTokens(usage: UsageState | undefined): string | undefined {
|
||||
if (!usage) return undefined;
|
||||
const total = (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
||||
if (!total) return undefined;
|
||||
const compact = total >= 1000 ? `${(total / 1000).toFixed(total >= 10_000 ? 0 : 1)}k` : `${total}`;
|
||||
const parts = [`tok=${compact}`];
|
||||
if (usage.input) parts.push(`in=${usage.input}`);
|
||||
if (usage.output) parts.push(`out=${usage.output}`);
|
||||
if (usage.cacheRead) parts.push(`cache=${usage.cacheRead}`);
|
||||
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
function snapshotFor(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): RunUiSnapshot | undefined {
|
||||
try {
|
||||
return snapshotCache?.refreshIfStale(run.runId);
|
||||
} catch {
|
||||
return snapshotCache?.get(run.runId);
|
||||
}
|
||||
}
|
||||
|
||||
function readRunTasks(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): TeamTaskState[] {
|
||||
const snapshot = snapshotFor(run, snapshotCache);
|
||||
if (snapshot) return snapshot.tasks;
|
||||
const parse = () => {
|
||||
if (!fs.existsSync(run.tasksPath)) return [];
|
||||
const parsed = JSON.parse(fs.readFileSync(run.tasksPath, "utf-8"));
|
||||
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
||||
};
|
||||
try {
|
||||
return readJsonFileCoalesced(run.tasksPath, TASK_READ_TTL_MS, parse);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function taskForAgent(tasks: TeamTaskState[], agent: CrewAgentRecord): TeamTaskState | undefined {
|
||||
return tasks.find((task) => task.id === agent.taskId);
|
||||
}
|
||||
|
||||
function modelForTask(task: TeamTaskState | undefined): string | undefined {
|
||||
const attempts = task?.modelAttempts;
|
||||
if (!attempts?.length) return undefined;
|
||||
return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
|
||||
}
|
||||
|
||||
function modelForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined): string | undefined {
|
||||
return modelForTask(task) ?? agent.model;
|
||||
}
|
||||
|
||||
function usageForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined): UsageState | undefined {
|
||||
return task?.usage ?? agent.usage;
|
||||
}
|
||||
|
||||
function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefined, options: RunDashboardOptions): string {
|
||||
const stats = [
|
||||
agent.progress?.activityState,
|
||||
options.showModel !== false && modelForAgent(agent, task) ? `model=${modelForAgent(agent, task)}` : undefined,
|
||||
options.showTokens !== false
|
||||
? formatTokens(usageForAgent(agent, task)) ?? (agent.progress?.tokens !== undefined ? `tok=${agent.progress.tokens}` : undefined)
|
||||
: undefined,
|
||||
options.showTools !== false && agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
|
||||
options.showTools !== false && agent.toolUses !== undefined ? `${agent.toolUses} tools` : undefined,
|
||||
agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
|
||||
agent.progress?.failedTool ? `failedTool=${agent.progress.failedTool}` : undefined,
|
||||
agent.startedAt ? `age=${formatAge(agent.completedAt ?? agent.startedAt)}` : undefined,
|
||||
].filter((part): part is string => Boolean(part));
|
||||
const recent = agent.progress?.recentOutput?.at(-1);
|
||||
const icon = iconForStatus(agent.status, { runningGlyph: spinnerFrame(agent.taskId) });
|
||||
return `Agent: ${icon} ${agent.taskId} ${agent.role}->${agent.agent}${stats.length ? ` · ${stats.join(" · ")}` : ""}${recent ? ` ⎿ ${recent}` : ""}`;
|
||||
}
|
||||
|
||||
function readAgentPreview(run: TeamRunManifest, maxLines = 5, options: RunDashboardOptions = {}): string[] {
|
||||
try {
|
||||
const snapshot = snapshotFor(run, options.snapshotCache);
|
||||
const agents = snapshot?.agents ?? readCrewAgents(run);
|
||||
const tasks = snapshot?.tasks ?? readRunTasks(run, options.snapshotCache);
|
||||
if (!agents.length) return ["Agents: (none)"];
|
||||
const totals = tasks.reduce((acc, task) => {
|
||||
acc.input += task.usage?.input ?? 0;
|
||||
acc.output += task.usage?.output ?? 0;
|
||||
acc.cacheRead += task.usage?.cacheRead ?? 0;
|
||||
acc.cacheWrite += task.usage?.cacheWrite ?? 0;
|
||||
acc.cost += task.usage?.cost ?? 0;
|
||||
return acc;
|
||||
}, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 } as { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number });
|
||||
const header = formatTokens(totals) ? `Agents: ${formatTokens(totals)}` : "Agents:";
|
||||
return [
|
||||
header,
|
||||
...agents
|
||||
.slice(0, maxLines)
|
||||
.map((agent) => agentPreviewLine(agent, taskForAgent(tasks, agent), options)),
|
||||
...(agents.length > maxLines ? [`Agents: +${agents.length - maxLines} more`] : []),
|
||||
];
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return [`Agents: failed to read (${message})`];
|
||||
}
|
||||
}
|
||||
|
||||
function agentsFor(run: TeamRunManifest, snapshotCache?: RunSnapshotCache): CrewAgentRecord[] {
|
||||
const snapshot = snapshotFor(run, snapshotCache);
|
||||
if (snapshot) return snapshot.agents;
|
||||
try {
|
||||
return readCrewAgents(run);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function runLabel(run: TeamRunManifest, selected: boolean, snapshotCache?: RunSnapshotCache): string {
|
||||
const agents = agentsFor(run, snapshotCache);
|
||||
const stale = isLikelyOrphanedActiveRun(run, agents);
|
||||
const running = agents.find((agent) => agent.status === "running");
|
||||
const queued = agents.find((agent) => agent.status === "queued");
|
||||
const step = stale ? "orphaned queued run" : running ? `step ${running.taskId.replace(/[\x00-\x1f\x7f-\x9f]/g, "")}` : queued ? `queued ${queued.taskId.replace(/[\x00-\x1f\x7f-\x9f]/g, "")}` : `agents ${agents.length}`;
|
||||
const status: RunStatus = stale ? "stale" : (run.status as RunStatus);
|
||||
const marker = selected ? "›" : " ";
|
||||
return `${marker} ${iconForStatus(status, { runningGlyph: spinnerFrame(run.runId) })} ${run.runId.slice(-8)} ${status} | ${run.team}/${run.workflow ?? "none"} | ${step} | ${run.goal}`;
|
||||
}
|
||||
|
||||
interface ResolvedRun {
|
||||
manifest: TeamRunManifest;
|
||||
snapshot: RunUiSnapshot | undefined;
|
||||
agents: CrewAgentRecord[];
|
||||
status: RunStatus;
|
||||
}
|
||||
|
||||
function resolveRuns(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): Map<string, ResolvedRun> {
|
||||
const map = new Map<string, ResolvedRun>();
|
||||
for (const run of runs) {
|
||||
const snapshot = snapshotFor(run, snapshotCache);
|
||||
const agents = snapshot?.agents ?? agentsFor(run, snapshotCache);
|
||||
const displayRun = snapshot?.manifest ?? run;
|
||||
const status: RunStatus = isLikelyOrphanedActiveRun(displayRun, agents) ? "stale" : (displayRun.status as RunStatus);
|
||||
map.set(run.runId, { manifest: run, snapshot, agents, status });
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function groupedRuns(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): Array<{ label: string; run?: TeamRunManifest }> {
|
||||
const resolved = resolveRuns(runs, snapshotCache);
|
||||
const rows: Array<{ label: string; run?: TeamRunManifest }> = [];
|
||||
const active = runs.filter((run) => isDisplayActiveRun(resolved.get(run.runId)?.snapshot?.manifest ?? run, resolved.get(run.runId)?.agents ?? []));
|
||||
const rest = runs.filter((run) => !isDisplayActiveRun(resolved.get(run.runId)?.snapshot?.manifest ?? run, resolved.get(run.runId)?.agents ?? []));
|
||||
if (active.length) rows.push({ label: "Active" }, ...active.map((run) => ({ label: run.runId, run })));
|
||||
if (rest.length) rows.push({ label: "Recent" }, ...rest.map((run) => ({ label: run.runId, run })));
|
||||
return rows;
|
||||
}
|
||||
|
||||
function selectedRunFromGrouped(runs: TeamRunManifest[], selected: number, snapshotCache?: RunSnapshotCache): TeamRunManifest | undefined {
|
||||
return groupedRuns(runs, snapshotCache).filter((row) => row.run)[selected]?.run;
|
||||
}
|
||||
|
||||
function countByStatus(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache): string {
|
||||
const resolved = resolveRuns(runs, snapshotCache);
|
||||
const counts = new Map<RunStatus, number>();
|
||||
for (const r of resolved.values()) counts.set(r.status, (counts.get(r.status) ?? 0) + 1);
|
||||
return [...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none";
|
||||
}
|
||||
|
||||
export class RunDashboard implements DashboardComponent {
|
||||
private selected = 0;
|
||||
private showFullProgress = false;
|
||||
private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = "agents";
|
||||
private runs: TeamRunManifest[];
|
||||
private readonly done: (selection: RunDashboardSelection | undefined) => void;
|
||||
private readonly theme: CrewTheme;
|
||||
private readonly options: RunDashboardOptions;
|
||||
private cachedWidth = 0;
|
||||
private cachedVersion = "";
|
||||
private cachedLines: string[] = [];
|
||||
private readonly unsubscribeTheme: () => void;
|
||||
|
||||
constructor(
|
||||
runs: TeamRunManifest[],
|
||||
done: (selection: RunDashboardSelection | undefined) => void,
|
||||
theme: unknown = {},
|
||||
options: RunDashboardOptions = {},
|
||||
) {
|
||||
this.runs = runs;
|
||||
this.done = done;
|
||||
this.theme = asCrewTheme(theme);
|
||||
this.options = options;
|
||||
this.unsubscribeTheme = subscribeThemeChange(theme, () => this.invalidate());
|
||||
}
|
||||
|
||||
private refreshRuns(): void {
|
||||
if (!this.options.runProvider) return;
|
||||
const selectedRunId = this.selectedRunId();
|
||||
const next = this.options.runProvider();
|
||||
this.runs = Array.isArray(next) ? next : this.runs;
|
||||
if (selectedRunId) {
|
||||
const nextIndex = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).findIndex((row) => row.run?.runId === selectedRunId);
|
||||
if (nextIndex >= 0) this.selected = nextIndex;
|
||||
else this.selected = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private buildSignature(): string {
|
||||
let hasRunning = false;
|
||||
const statuses = this.runs.map((run) => {
|
||||
const snapshot = snapshotFor(run, this.options.snapshotCache);
|
||||
const displayRun = snapshot?.manifest ?? run;
|
||||
const agents = snapshot?.agents ?? agentsFor(run, this.options.snapshotCache);
|
||||
const stale = isLikelyOrphanedActiveRun(displayRun, agents);
|
||||
const status: RunStatus = stale ? "stale" : (displayRun.status as RunStatus);
|
||||
if (status === "running" || agents.some((agent) => agent.status === "running")) hasRunning = true;
|
||||
return snapshot?.signature ?? `${displayRun.runId}:${displayRun.status}:${displayRun.updatedAt}:${status}`;
|
||||
}).join("|");
|
||||
const metricsSig = this.activePane === "metrics" ? `:metrics=${this.options.registry?.snapshot().length ?? 0}:${spinnerBucket()}` : "";
|
||||
return `${this.selected}:${this.showFullProgress ? 1 : 0}:${this.activePane}:${statuses}${hasRunning ? `:spin=${spinnerBucket()}` : ""}${metricsSig}`;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedVersion = "";
|
||||
this.cachedLines = [];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.unsubscribeTheme();
|
||||
}
|
||||
|
||||
private selectedRunId(): string | undefined {
|
||||
return selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache)?.runId;
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
try {
|
||||
return this.renderUnsafe(width);
|
||||
} catch (error) {
|
||||
logInternalError("run-dashboard.render", error);
|
||||
return renderLines(["Dashboard error — see logs for details."], width);
|
||||
}
|
||||
}
|
||||
|
||||
private renderUnsafe(width: number): string[] {
|
||||
this.refreshRuns();
|
||||
const signature = this.buildSignature();
|
||||
if (signature !== this.cachedVersion || this.cachedWidth !== width) {
|
||||
const innerWidth = Math.max(20, width - 4);
|
||||
const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
|
||||
const fg = (color: Parameters<CrewTheme["fg"]>[0], text: string) => this.theme.fg(color, text);
|
||||
const borderFill = (count: number) => new DynamicCrewBorder(this.theme).render(count)[0];
|
||||
const border = (left: string, right: string) => `${fg("border", left)}${borderFill(borderWidth)}${fg("border", right)}`;
|
||||
|
||||
const lines = [
|
||||
border("╭", "╮"),
|
||||
`│ ${pad(truncate(`${fg("accent", "▐")} ${this.theme.bold(this.options.placement === "right" ? "pi-crew right sidebar (anchored top-right)" : "pi-crew dashboard")}`, innerWidth - 1), innerWidth - 1)}│`,
|
||||
`│ ${pad(truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs, this.options.snapshotCache)}`, innerWidth - 1), innerWidth - 1)}│`,
|
||||
`│ ${pad(truncate(`↑/↓ select • 1 agents 2 progress 3 mailbox 4 output 5 health 6 metrics • s/u/a/i actions • R/K/D health • H hush`, innerWidth - 1), innerWidth - 1)}│`,
|
||||
border("├", "┤"),
|
||||
];
|
||||
if (this.runs.length === 0) {
|
||||
lines.push(`│ ${pad(truncate("No runs found.", innerWidth - 1), innerWidth - 1)}│`);
|
||||
} else {
|
||||
const rows = groupedRuns(this.runs, this.options.snapshotCache).slice(0, 16);
|
||||
const selectableRuns = rows.filter((row) => row.run);
|
||||
for (const row of rows) {
|
||||
if (!row.run) {
|
||||
lines.push(`│ ${pad(truncate(fg("accent", row.label), innerWidth - 1), innerWidth - 1)}│`);
|
||||
continue;
|
||||
}
|
||||
const index = selectableRuns.findIndex((candidate) => candidate.run?.runId === row.run?.runId);
|
||||
const rowSnapshot = snapshotFor(row.run, this.options.snapshotCache);
|
||||
const rowRun = rowSnapshot?.manifest ?? row.run;
|
||||
const rowAgents = rowSnapshot?.agents ?? agentsFor(row.run, this.options.snapshotCache);
|
||||
const rowStatus = isLikelyOrphanedActiveRun(rowRun, rowAgents) ? "stale" : (rowRun.status as RunStatus);
|
||||
const label = runLabel(rowRun, index === this.selected, this.options.snapshotCache);
|
||||
lines.push(`│ ${pad(applyStatusColor(this.theme, rowStatus, label), innerWidth - 1)}│`);
|
||||
}
|
||||
const selectedRun = selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache);
|
||||
if (selectedRun) {
|
||||
const selectedSnapshot = snapshotFor(selectedRun, this.options.snapshotCache);
|
||||
const selectedDisplayRun = selectedSnapshot?.manifest ?? selectedRun;
|
||||
const selectedAgents = selectedSnapshot?.agents ?? agentsFor(selectedRun, this.options.snapshotCache);
|
||||
lines.push(border("├", "┤"));
|
||||
const details = [
|
||||
`Selected: ${selectedDisplayRun.runId}`,
|
||||
`Status: ${isLikelyOrphanedActiveRun(selectedDisplayRun, selectedAgents) ? "stale" : selectedDisplayRun.status} | Team: ${selectedDisplayRun.team} | Workflow: ${selectedDisplayRun.workflow ?? "none"}`,
|
||||
`Created: ${selectedDisplayRun.createdAt}`,
|
||||
`Updated: ${selectedDisplayRun.updatedAt}`,
|
||||
`Artifacts: ${selectedDisplayRun.artifacts.length} | Workspace: ${selectedDisplayRun.workspaceMode}`,
|
||||
selectedDisplayRun.async ? `Async: pid=${selectedDisplayRun.async.pid ?? "unknown"} log=${selectedDisplayRun.async.logPath}` : "Async: no",
|
||||
`Goal: ${selectedDisplayRun.goal}`,
|
||||
];
|
||||
const paneLines = selectedSnapshot
|
||||
? this.activePane === "agents"
|
||||
? renderAgentsPane(selectedSnapshot, this.options)
|
||||
: this.activePane === "progress"
|
||||
? renderProgressPane(selectedSnapshot)
|
||||
: this.activePane === "mailbox"
|
||||
? renderMailboxPane(selectedSnapshot)
|
||||
: this.activePane === "health"
|
||||
? renderHealthPane(selectedSnapshot, { isForeground: selectedDisplayRun.async ? false : true })
|
||||
: this.activePane === "metrics"
|
||||
? renderMetricsPane(selectedSnapshot, { registry: this.options.registry })
|
||||
: renderTranscriptPane(selectedSnapshot)
|
||||
: [
|
||||
...readAgentPreview(selectedDisplayRun, this.showFullProgress ? 20 : 8, this.options),
|
||||
...readProgressPreview(selectedDisplayRun, this.showFullProgress ? 20 : 5),
|
||||
];
|
||||
for (const detail of [
|
||||
...details,
|
||||
`Pane: ${this.activePane}`,
|
||||
...paneLines,
|
||||
...(this.showFullProgress ? readProgressPreview(selectedDisplayRun, 20) : []),
|
||||
]) {
|
||||
lines.push(`│ ${pad(truncate(detail, innerWidth - 1), innerWidth - 1)}│`);
|
||||
}
|
||||
const selectedTasks = selectedSnapshot?.tasks ?? readRunTasks(selectedDisplayRun, this.options.snapshotCache);
|
||||
const footer = new CrewFooter({
|
||||
pwd: selectedDisplayRun.cwd,
|
||||
runId: selectedDisplayRun.runId,
|
||||
status: isLikelyOrphanedActiveRun(selectedDisplayRun, selectedAgents) ? "stale" : selectedDisplayRun.status,
|
||||
usage: aggregateUsage(selectedTasks),
|
||||
badges: [`team ${selectedDisplayRun.team}`, `workflow ${selectedDisplayRun.workflow ?? "none"}`, `${selectedDisplayRun.artifacts.length} artifacts`, selectedDisplayRun.workspaceMode],
|
||||
}, this.theme);
|
||||
lines.push(border("├", "┤"));
|
||||
for (const footerLine of footer.render(innerWidth - 1)) {
|
||||
lines.push(`│ ${pad(truncate(footerLine, innerWidth - 1), innerWidth - 1)}│`);
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push(border("╰", "╯"));
|
||||
this.cachedLines = renderLines(lines.map((line) => truncate(line, width)), width);
|
||||
this.cachedVersion = signature;
|
||||
this.cachedWidth = width;
|
||||
}
|
||||
return this.cachedLines;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
const action = dashboardActionForKey(data, this.activePane);
|
||||
const selectedRunId = this.selectedRunId();
|
||||
if (action === "close") {
|
||||
this.done(undefined);
|
||||
return;
|
||||
}
|
||||
if (action === "select") {
|
||||
this.done(selectedRunId ? { runId: selectedRunId, action: "status" } : undefined);
|
||||
return;
|
||||
}
|
||||
if (action === "summary" || action === "artifacts" || action === "api" || action === "agents" || action === "mailbox" || action === "reload" || action === "mailbox-detail" || action === "health-recovery" || action === "health-kill-stale" || action === "health-diagnostic-export" || action === "notifications-dismiss") {
|
||||
this.done(selectedRunId ? { runId: selectedRunId, action } : action === "reload" ? { runId: "", action } : undefined);
|
||||
return;
|
||||
}
|
||||
if (action === "events") {
|
||||
this.done(selectedRunId ? { runId: selectedRunId, action: "agent-events" } : undefined);
|
||||
return;
|
||||
}
|
||||
if (action === "output") {
|
||||
this.done(selectedRunId ? { runId: selectedRunId, action: "agent-output" } : undefined);
|
||||
return;
|
||||
}
|
||||
if (action === "transcript") {
|
||||
this.done(selectedRunId ? { runId: selectedRunId, action: "agent-transcript" } : undefined);
|
||||
return;
|
||||
}
|
||||
if (action === "progressToggle") {
|
||||
this.showFullProgress = !this.showFullProgress;
|
||||
this.invalidate();
|
||||
return;
|
||||
}
|
||||
if (action === "pane-agents") this.activePane = "agents";
|
||||
else if (action === "pane-progress") this.activePane = "progress";
|
||||
else if (action === "pane-mailbox") this.activePane = "mailbox";
|
||||
else if (action === "pane-output") this.activePane = "output";
|
||||
else if (action === "pane-health") this.activePane = "health";
|
||||
else if (action === "pane-metrics") this.activePane = "metrics";
|
||||
else if (action === "up") this.selected = Math.max(0, this.selected - 1);
|
||||
else if (action === "down") {
|
||||
const selectableCount = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).length;
|
||||
this.selected = Math.min(Math.max(0, selectableCount - 1), this.selected + 1);
|
||||
}
|
||||
if (action) this.invalidate();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user