pi-crew 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -9
- package/docs/architecture.md +2 -2
- package/docs/usage.md +19 -9
- package/package.json +1 -1
- package/schema.json +10 -0
- package/src/config/config.ts +25 -0
- package/src/extension/help.ts +1 -1
- package/src/extension/register.ts +32 -8
- package/src/extension/run-maintenance.ts +1 -1
- package/src/extension/team-tool.ts +11 -4
- package/src/runtime/child-pi.ts +29 -1
- package/src/runtime/process-status.ts +8 -1
- package/src/runtime/runtime-resolver.ts +7 -8
- package/src/ui/crew-widget.ts +97 -33
- package/src/ui/powerbar-publisher.ts +55 -0
- package/src/ui/run-dashboard.ts +26 -9
- package/src/ui/transcript-viewer.ts +219 -204
package/src/ui/crew-widget.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { CrewUiConfig } from "../config/config.ts";
|
|
2
3
|
import { listRuns } from "../extension/run-index.ts";
|
|
3
|
-
import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
4
4
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
5
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
|
+
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
6
7
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
7
8
|
|
|
8
9
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -15,12 +16,48 @@ const TOOL_LABELS: Record<string, string> = {
|
|
|
15
16
|
find: "finding files",
|
|
16
17
|
ls: "listing",
|
|
17
18
|
};
|
|
19
|
+
const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
20
|
+
|
|
21
|
+
type ThemeLike = { fg?: (color: string, text: string) => string; bold?: (text: string) => string };
|
|
22
|
+
type WidgetComponent = { render(width: number): string[]; invalidate(): void };
|
|
18
23
|
|
|
19
24
|
export interface CrewWidgetState {
|
|
20
25
|
frame: number;
|
|
21
26
|
interval?: ReturnType<typeof setInterval>;
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
interface WidgetRun {
|
|
30
|
+
run: TeamRunManifest;
|
|
31
|
+
agents: CrewAgentRecord[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function visibleWidth(value: string): number {
|
|
35
|
+
return value.replace(ANSI_PATTERN, "").length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function truncate(value: string, width: number): string {
|
|
39
|
+
if (width <= 0) return "";
|
|
40
|
+
if (visibleWidth(value) <= width) return value;
|
|
41
|
+
if (width <= 1) return "…";
|
|
42
|
+
let output = "";
|
|
43
|
+
let visible = 0;
|
|
44
|
+
for (let index = 0; index < value.length;) {
|
|
45
|
+
const slice = value.slice(index);
|
|
46
|
+
const ansi = slice.match(/^\u001b\[[0-?]*[ -/]*[@-~]/);
|
|
47
|
+
if (ansi?.[0]) {
|
|
48
|
+
output += ansi[0];
|
|
49
|
+
index += ansi[0].length;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const char = value[index]!;
|
|
53
|
+
if (visible >= width - 1) break;
|
|
54
|
+
output += char;
|
|
55
|
+
visible += 1;
|
|
56
|
+
index += char.length;
|
|
57
|
+
}
|
|
58
|
+
return `${output}\u001b[0m…`;
|
|
59
|
+
}
|
|
60
|
+
|
|
24
61
|
function elapsed(iso: string | undefined, now = Date.now()): string | undefined {
|
|
25
62
|
if (!iso) return undefined;
|
|
26
63
|
const ms = Math.max(0, now - new Date(iso).getTime());
|
|
@@ -45,7 +82,7 @@ function agentActivity(agent: CrewAgentRecord): string {
|
|
|
45
82
|
const recent = agent.progress?.recentOutput?.at(-1);
|
|
46
83
|
if (recent) return recent.replace(/\s+/g, " ").trim();
|
|
47
84
|
if (agent.progress?.activityState === "needs_attention") return "needs attention";
|
|
48
|
-
if (agent.status === "queued") return "queued
|
|
85
|
+
if (agent.status === "queued") return "queued";
|
|
49
86
|
if (agent.status === "running") return "thinking…";
|
|
50
87
|
if (agent.status === "failed") return agent.error ?? "failed";
|
|
51
88
|
return "done";
|
|
@@ -61,59 +98,86 @@ function agentStats(agent: CrewAgentRecord): string {
|
|
|
61
98
|
return parts.join(" · ");
|
|
62
99
|
}
|
|
63
100
|
|
|
64
|
-
function runStep(run: TeamRunManifest, agents: CrewAgentRecord[]): string {
|
|
65
|
-
if (isLikelyOrphanedActiveRun(run, agents)) return "stale";
|
|
66
|
-
const running = agents.find((agent) => agent.status === "running");
|
|
67
|
-
if (running) return running.taskId;
|
|
68
|
-
const queued = agents.find((agent) => agent.status === "queued");
|
|
69
|
-
if (queued) return queued.taskId;
|
|
70
|
-
return run.status;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
101
|
function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
|
|
74
102
|
try { return readCrewAgents(run); } catch { return []; }
|
|
75
103
|
}
|
|
76
104
|
|
|
77
|
-
|
|
105
|
+
function activeWidgetRuns(cwd: string): WidgetRun[] {
|
|
78
106
|
const runs = listRuns(cwd).slice(0, 20);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
return runs.map((run) => ({ run, agents: agentsFor(run) })).filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function statusSummary(runs: WidgetRun[]): string {
|
|
111
|
+
const runningAgents = runs.flatMap((item) => item.agents).filter((agent) => agent.status === "running").length;
|
|
112
|
+
const queuedAgents = runs.flatMap((item) => item.agents).filter((agent) => agent.status === "queued").length;
|
|
113
|
+
return `● pi-crew · runs=${runs.length} agents=${runningAgents} running${queuedAgents ? ` · ${queuedAgents} queued` : ""} · /team-dashboard`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8): string[] {
|
|
117
|
+
const runs = activeWidgetRuns(cwd);
|
|
118
|
+
if (!runs.length) return [];
|
|
84
119
|
const runningGlyph = SPINNER[frame % SPINNER.length] ?? "⠋";
|
|
85
|
-
const lines: string[] = [];
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
lines.push(`${glyph(stale ? "failed" : run.status, runningGlyph)} ${run.runId.slice(-8)} ${run.team}/${run.workflow ?? "none"} · ${runStep(run, agents)} · ${countText}`);
|
|
95
|
-
for (const agent of (stale ? [] : agents.filter((item) => item.status === "running" || item.status === "queued").slice(0, 2))) {
|
|
120
|
+
const lines: string[] = [statusSummary(runs)];
|
|
121
|
+
for (const { run, agents } of runs) {
|
|
122
|
+
const running = agents.find((agent) => agent.status === "running");
|
|
123
|
+
const queued = agents.find((agent) => agent.status === "queued");
|
|
124
|
+
const step = running?.taskId ?? queued?.taskId ?? run.status;
|
|
125
|
+
const completed = agents.filter((agent) => agent.status === "completed").length;
|
|
126
|
+
lines.push(`${glyph(run.status, runningGlyph)} ${run.runId.slice(-8)} ${run.team}/${run.workflow ?? "none"} · ${step} · ${completed}/${agents.length} done`);
|
|
127
|
+
const activeAgents = agents.filter((item) => item.status === "running" || item.status === "queued");
|
|
128
|
+
for (const agent of activeAgents.slice(0, 3)) {
|
|
96
129
|
const stats = agentStats(agent);
|
|
97
130
|
lines.push(` ${glyph(agent.status, runningGlyph)} ${agent.taskId} ${agent.role}→${agent.agent} · ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
|
|
98
131
|
}
|
|
132
|
+
if (activeAgents.length > 3) lines.push(` … +${activeAgents.length - 3} more agents`);
|
|
99
133
|
if (lines.length >= maxLines) break;
|
|
100
134
|
}
|
|
101
135
|
return lines.slice(0, maxLines);
|
|
102
136
|
}
|
|
103
137
|
|
|
104
|
-
|
|
138
|
+
class CrewWidgetComponent implements WidgetComponent {
|
|
139
|
+
private cwd: string;
|
|
140
|
+
private frame: number;
|
|
141
|
+
private maxLines: number;
|
|
142
|
+
private theme: ThemeLike;
|
|
143
|
+
|
|
144
|
+
constructor(cwd: string, frame: number, maxLines: number, theme: ThemeLike) {
|
|
145
|
+
this.cwd = cwd;
|
|
146
|
+
this.frame = frame;
|
|
147
|
+
this.maxLines = maxLines;
|
|
148
|
+
this.theme = theme;
|
|
149
|
+
}
|
|
150
|
+
invalidate(): void {}
|
|
151
|
+
render(width: number): string[] {
|
|
152
|
+
const fg = this.theme.fg?.bind(this.theme) ?? ((_color: string, text: string) => text);
|
|
153
|
+
const bold = this.theme.bold?.bind(this.theme) ?? ((text: string) => text);
|
|
154
|
+
return buildCrewWidgetLines(this.cwd, this.frame, this.maxLines).map((line, index) => {
|
|
155
|
+
const colored = index === 0
|
|
156
|
+
? line.replace("● pi-crew", `${fg("accent", "●")} ${bold("pi-crew")}`)
|
|
157
|
+
: line.replace(/^\s*([⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏▶◦✓✗■·])/, (match, icon: string) => match.replace(icon, fg(icon === "✓" ? "success" : icon === "✗" ? "error" : icon === "◦" ? "dim" : "accent", icon)));
|
|
158
|
+
return truncate(colored, width);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function updateCrewWidget(ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">, state: CrewWidgetState, config?: CrewUiConfig): void {
|
|
105
164
|
if (!ctx.hasUI) return;
|
|
106
165
|
state.frame += 1;
|
|
107
|
-
const
|
|
166
|
+
const maxLines = config?.widgetMaxLines ?? 8;
|
|
167
|
+
const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines);
|
|
108
168
|
ctx.ui.setStatus("pi-crew", lines.length ? lines[0] : undefined);
|
|
109
|
-
|
|
169
|
+
if (!lines.length) {
|
|
170
|
+
ctx.ui.setWidget("pi-crew", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
ctx.ui.setWidget("pi-crew", ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(ctx.cwd, state.frame, maxLines, theme as ThemeLike)) as never, { placement: config?.widgetPlacement ?? "aboveEditor" });
|
|
110
174
|
}
|
|
111
175
|
|
|
112
|
-
export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState): void {
|
|
176
|
+
export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
|
|
113
177
|
if (state.interval) clearInterval(state.interval);
|
|
114
178
|
state.interval = undefined;
|
|
115
179
|
if (ctx?.hasUI) {
|
|
116
180
|
ctx.ui.setStatus("pi-crew", undefined);
|
|
117
|
-
ctx.ui.setWidget("pi-crew", undefined, { placement: "aboveEditor" });
|
|
181
|
+
ctx.ui.setWidget("pi-crew", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
|
|
118
182
|
}
|
|
119
183
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CrewUiConfig } from "../config/config.ts";
|
|
2
|
+
import { listRuns } from "../extension/run-index.ts";
|
|
3
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
|
+
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
5
|
+
|
|
6
|
+
type EventBus = { emit?: (event: string, data: unknown) => void } | undefined;
|
|
7
|
+
|
|
8
|
+
function safeEmit(events: EventBus, event: string, data: unknown): void {
|
|
9
|
+
try { events?.emit?.(event, data); } catch {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUiConfig): void {
|
|
13
|
+
if (config?.powerbar === false) return;
|
|
14
|
+
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-active", label: "pi-crew active agents" });
|
|
15
|
+
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-progress", label: "pi-crew run progress" });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: CrewUiConfig): void {
|
|
19
|
+
if (config?.powerbar === false) return;
|
|
20
|
+
const active = listRuns(cwd).slice(0, 20).map((run) => {
|
|
21
|
+
let agents = [] as ReturnType<typeof readCrewAgents>;
|
|
22
|
+
try { agents = readCrewAgents(run); } catch {}
|
|
23
|
+
return { run, agents };
|
|
24
|
+
}).filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
25
|
+
if (!active.length) {
|
|
26
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
27
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const agents = active.flatMap((item) => item.agents);
|
|
31
|
+
const running = agents.filter((agent) => agent.status === "running").length;
|
|
32
|
+
const queued = agents.filter((agent) => agent.status === "queued").length;
|
|
33
|
+
const completed = agents.filter((agent) => agent.status === "completed").length;
|
|
34
|
+
const total = Math.max(1, agents.length);
|
|
35
|
+
safeEmit(events, "powerbar:update", {
|
|
36
|
+
id: "pi-crew-active",
|
|
37
|
+
icon: "⚙",
|
|
38
|
+
text: `crew ${running || active.length}`,
|
|
39
|
+
suffix: queued ? `${queued}q` : undefined,
|
|
40
|
+
color: running ? "accent" : "warning",
|
|
41
|
+
});
|
|
42
|
+
safeEmit(events, "powerbar:update", {
|
|
43
|
+
id: "pi-crew-progress",
|
|
44
|
+
text: active[0]?.run.team ?? "crew",
|
|
45
|
+
bar: Math.round((completed / total) * 100),
|
|
46
|
+
suffix: `${completed}/${total}`,
|
|
47
|
+
color: completed === total ? "success" : "accent",
|
|
48
|
+
barSegments: 8,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function clearPiCrewPowerbar(events: EventBus): void {
|
|
53
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
54
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
55
|
+
}
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -10,6 +10,8 @@ interface DashboardComponent {
|
|
|
10
10
|
handleInput(data: string): void;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
type DashboardTheme = { fg?: (color: string, text: string) => string; bold?: (text: string) => string };
|
|
14
|
+
|
|
13
15
|
export type RunDashboardAction = "status" | "summary" | "artifacts" | "api" | "events" | "agents" | "agent-events" | "agent-output" | "agent-transcript" | "reload";
|
|
14
16
|
export interface RunDashboardSelection {
|
|
15
17
|
runId: string;
|
|
@@ -49,6 +51,14 @@ function padVisible(value: string, width: number): string {
|
|
|
49
51
|
return `${value}${" ".repeat(Math.max(0, width - visibleLength(value)))}`;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function colorForStatus(status: string): string {
|
|
55
|
+
if (status === "completed") return "success";
|
|
56
|
+
if (status === "failed" || status === "stale") return "error";
|
|
57
|
+
if (status === "cancelled" || status === "blocked") return "warning";
|
|
58
|
+
if (status === "running") return "accent";
|
|
59
|
+
return "dim";
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
function statusIcon(status: string): string {
|
|
53
63
|
if (status === "completed") return "✓";
|
|
54
64
|
if (status === "failed" || status === "stale") return "✗";
|
|
@@ -143,23 +153,28 @@ export class RunDashboard implements DashboardComponent {
|
|
|
143
153
|
private showFullProgress = false;
|
|
144
154
|
private readonly runs: TeamRunManifest[];
|
|
145
155
|
private readonly done: (selection: RunDashboardSelection | undefined) => void;
|
|
156
|
+
private readonly theme: DashboardTheme;
|
|
146
157
|
|
|
147
|
-
constructor(runs: TeamRunManifest[], done: (selection: RunDashboardSelection | undefined) => void) {
|
|
158
|
+
constructor(runs: TeamRunManifest[], done: (selection: RunDashboardSelection | undefined) => void, theme: unknown = {}) {
|
|
148
159
|
this.runs = runs;
|
|
149
160
|
this.done = done;
|
|
161
|
+
this.theme = theme as DashboardTheme;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
invalidate(): void {}
|
|
153
165
|
|
|
154
166
|
render(width: number): string[] {
|
|
167
|
+
const fg = this.theme.fg?.bind(this.theme) ?? ((_color: string, text: string) => text);
|
|
168
|
+
const bold = this.theme.bold?.bind(this.theme) ?? ((text: string) => text);
|
|
155
169
|
const innerWidth = Math.max(20, width - 4);
|
|
156
170
|
const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
|
|
171
|
+
const border = (text: string) => fg("border", text);
|
|
157
172
|
const lines = [
|
|
158
|
-
`╭${"─".repeat(borderWidth)}
|
|
159
|
-
`│ ${padVisible(truncate("pi-crew dashboard"
|
|
160
|
-
`│ ${padVisible(truncate("↑/↓/j/k select • r reload • p progress • s/u/a/i actions • d agents • e/v/o viewers • q close", innerWidth - 1), innerWidth - 1)}│`,
|
|
173
|
+
border(`╭${"─".repeat(borderWidth)}╮`),
|
|
174
|
+
`│ ${padVisible(truncate(`${fg("accent", "●")} ${bold("pi-crew dashboard")}`, innerWidth - 1), innerWidth - 1)}│`,
|
|
175
|
+
`│ ${padVisible(truncate(fg("dim", "↑/↓/j/k select • r reload • p progress • s/u/a/i actions • d agents • e/v/o viewers • q close"), innerWidth - 1), innerWidth - 1)}│`,
|
|
161
176
|
`│ ${padVisible(truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs)}`, innerWidth - 1), innerWidth - 1)}│`,
|
|
162
|
-
`├${"─".repeat(borderWidth)}
|
|
177
|
+
border(`├${"─".repeat(borderWidth)}┤`),
|
|
163
178
|
];
|
|
164
179
|
if (this.runs.length === 0) {
|
|
165
180
|
lines.push(`│ ${padVisible(truncate("No runs found.", innerWidth - 1), innerWidth - 1)}│`);
|
|
@@ -168,15 +183,17 @@ export class RunDashboard implements DashboardComponent {
|
|
|
168
183
|
const runRows = rows.filter((row) => row.run);
|
|
169
184
|
for (const row of rows) {
|
|
170
185
|
if (!row.run) {
|
|
171
|
-
lines.push(`│ ${padVisible(truncate(row.label, innerWidth - 1), innerWidth - 1)}│`);
|
|
186
|
+
lines.push(`│ ${padVisible(truncate(fg("accent", row.label), innerWidth - 1), innerWidth - 1)}│`);
|
|
172
187
|
continue;
|
|
173
188
|
}
|
|
174
189
|
const index = runRows.findIndex((candidate) => candidate.run?.runId === row.run?.runId);
|
|
175
|
-
|
|
190
|
+
const label = runLabel(row.run, index === this.selected);
|
|
191
|
+
const status = isLikelyOrphanedActiveRun(row.run, agentsFor(row.run)) ? "stale" : row.run.status;
|
|
192
|
+
lines.push(`│ ${padVisible(truncate(fg(colorForStatus(status), label), innerWidth - 1), innerWidth - 1)}│`);
|
|
176
193
|
}
|
|
177
194
|
const selectedRun = selectedRunFromGrouped(this.runs, this.selected);
|
|
178
195
|
if (selectedRun) {
|
|
179
|
-
lines.push(`├${"─".repeat(borderWidth)}┤`);
|
|
196
|
+
lines.push(border(`├${"─".repeat(borderWidth)}┤`));
|
|
180
197
|
const details = [
|
|
181
198
|
`Selected: ${selectedRun.runId}`,
|
|
182
199
|
`Status: ${selectedRun.status} | Team: ${selectedRun.team} | Workflow: ${selectedRun.workflow ?? "none"}`,
|
|
@@ -191,7 +208,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
191
208
|
}
|
|
192
209
|
}
|
|
193
210
|
}
|
|
194
|
-
lines.push(`╰${"─".repeat(borderWidth)}╯`);
|
|
211
|
+
lines.push(border(`╰${"─".repeat(borderWidth)}╯`));
|
|
195
212
|
return lines.map((line) => truncate(line, width));
|
|
196
213
|
}
|
|
197
214
|
|