pi-crew 0.1.12 → 0.1.14
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/package.json +1 -1
- package/src/extension/register.ts +24 -8
- package/src/runtime/team-runner.ts +2 -0
- package/src/ui/crew-widget.ts +62 -26
package/package.json
CHANGED
|
@@ -100,15 +100,38 @@ function setNestedConfig(config: Record<string, unknown>, key: string, value: un
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
103
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
104
|
+
const runtimeCleanupStoreKey = "__piCrewRuntimeCleanup";
|
|
105
|
+
const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
|
|
106
|
+
if (typeof previousRuntimeCleanup === "function") {
|
|
107
|
+
try { previousRuntimeCleanup(); } catch {}
|
|
108
|
+
}
|
|
103
109
|
const notifierState: AsyncNotifierState = { seenFinishedRunIds: new Set() };
|
|
104
110
|
let currentCtx: ExtensionContext | undefined;
|
|
105
111
|
let rpcHandle: PiCrewRpcHandle | undefined;
|
|
112
|
+
let cleanedUp = false;
|
|
106
113
|
const widgetState: CrewWidgetState = { frame: 0 };
|
|
107
114
|
const foregroundControllers = new Set<AbortController>();
|
|
108
115
|
registerAutonomousPolicy(pi);
|
|
109
116
|
rpcHandle = registerPiCrewRpc((pi as unknown as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events, () => currentCtx);
|
|
117
|
+
const cleanupRuntime = (): void => {
|
|
118
|
+
if (cleanedUp) return;
|
|
119
|
+
cleanedUp = true;
|
|
120
|
+
for (const controller of foregroundControllers) controller.abort();
|
|
121
|
+
foregroundControllers.clear();
|
|
122
|
+
terminateActiveChildPiProcesses();
|
|
123
|
+
stopAsyncRunNotifier(notifierState);
|
|
124
|
+
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
125
|
+
clearPiCrewPowerbar(pi.events);
|
|
126
|
+
rpcHandle?.unsubscribe();
|
|
127
|
+
rpcHandle = undefined;
|
|
128
|
+
currentCtx = undefined;
|
|
129
|
+
if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime) delete globalStore[runtimeCleanupStoreKey];
|
|
130
|
+
};
|
|
131
|
+
globalStore[runtimeCleanupStoreKey] = cleanupRuntime;
|
|
110
132
|
|
|
111
133
|
pi.on("session_start", (_event, ctx) => {
|
|
134
|
+
cleanedUp = false;
|
|
112
135
|
currentCtx = ctx;
|
|
113
136
|
notifyActiveRuns(ctx);
|
|
114
137
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
@@ -125,14 +148,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
125
148
|
widgetState.interval.unref?.();
|
|
126
149
|
});
|
|
127
150
|
pi.on("session_shutdown", () => {
|
|
128
|
-
|
|
129
|
-
terminateActiveChildPiProcesses();
|
|
130
|
-
stopAsyncRunNotifier(notifierState);
|
|
131
|
-
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
132
|
-
clearPiCrewPowerbar(pi.events);
|
|
133
|
-
currentCtx = undefined;
|
|
134
|
-
rpcHandle?.unsubscribe();
|
|
135
|
-
rpcHandle = undefined;
|
|
151
|
+
cleanupRuntime();
|
|
136
152
|
});
|
|
137
153
|
|
|
138
154
|
const tool: ToolDefinition = {
|
|
@@ -160,6 +160,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
160
160
|
if (failed) {
|
|
161
161
|
tasks = markBlocked(tasks, `Blocked by failed task '${failed.id}'.`);
|
|
162
162
|
saveRunTasks(manifest, tasks);
|
|
163
|
+
saveCrewAgents(manifest, tasks.map((task) => recordFromTask(manifest, task, runtimeKind)));
|
|
163
164
|
manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
|
|
164
165
|
return { manifest, tasks };
|
|
165
166
|
}
|
|
@@ -169,6 +170,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
169
170
|
if (readyBatch.length === 0) {
|
|
170
171
|
tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
|
|
171
172
|
saveRunTasks(manifest, tasks);
|
|
173
|
+
saveCrewAgents(manifest, tasks.map((task) => recordFromTask(manifest, task, runtimeKind)));
|
|
172
174
|
manifest = updateRunStatus(manifest, "blocked", "No ready queued task.");
|
|
173
175
|
return { manifest, tasks };
|
|
174
176
|
}
|
package/src/ui/crew-widget.ts
CHANGED
|
@@ -17,6 +17,9 @@ const TOOL_LABELS: Record<string, string> = {
|
|
|
17
17
|
ls: "listing",
|
|
18
18
|
};
|
|
19
19
|
const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
20
|
+
const LEGACY_WIDGET_KEY = "pi-crew";
|
|
21
|
+
const WIDGET_KEY = "pi-crew-active";
|
|
22
|
+
const STATUS_KEY = "pi-crew";
|
|
20
23
|
|
|
21
24
|
type ThemeLike = { fg?: (color: string, text: string) => string; bold?: (text: string) => string };
|
|
22
25
|
type WidgetComponent = { render(width: number): string[]; invalidate(): void };
|
|
@@ -108,33 +111,62 @@ function activeWidgetRuns(cwd: string): WidgetRun[] {
|
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
function statusSummary(runs: WidgetRun[]): string {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
+
const agents = runs.flatMap((item) => item.agents);
|
|
115
|
+
const runningAgents = agents.filter((agent) => agent.status === "running").length;
|
|
116
|
+
const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
|
|
117
|
+
const completedAgents = agents.filter((agent) => agent.status === "completed").length;
|
|
118
|
+
const parts = [`${runningAgents} running`];
|
|
119
|
+
if (queuedAgents) parts.push(`${queuedAgents} queued`);
|
|
120
|
+
if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
|
|
121
|
+
return `Crew: ${parts.join(", ")}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function widgetHeader(runs: WidgetRun[], runningGlyph: string): string {
|
|
125
|
+
const agents = runs.flatMap((item) => item.agents);
|
|
126
|
+
const runningAgents = agents.filter((agent) => agent.status === "running").length;
|
|
127
|
+
const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
|
|
128
|
+
const completedAgents = agents.filter((agent) => agent.status === "completed").length;
|
|
129
|
+
const parts = [`${runningAgents} running`];
|
|
130
|
+
if (queuedAgents) parts.push(`${queuedAgents} queued`);
|
|
131
|
+
if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
|
|
132
|
+
return `${runningGlyph} Crew agents · ${parts.join(" · ")} · /team-dashboard`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function shortRunLabel(run: TeamRunManifest): string {
|
|
136
|
+
return `${run.team}/${run.workflow ?? "none"}`;
|
|
114
137
|
}
|
|
115
138
|
|
|
116
139
|
export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8): string[] {
|
|
117
140
|
const runs = activeWidgetRuns(cwd);
|
|
118
141
|
if (!runs.length) return [];
|
|
119
142
|
const runningGlyph = SPINNER[frame % SPINNER.length] ?? "⠋";
|
|
120
|
-
const lines: string[] = [
|
|
143
|
+
const lines: string[] = [widgetHeader(runs, runningGlyph)];
|
|
121
144
|
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
145
|
const activeAgents = agents.filter((item) => item.status === "running" || item.status === "queued");
|
|
128
|
-
|
|
146
|
+
const completed = agents.filter((agent) => agent.status === "completed").length;
|
|
147
|
+
const runGlyph = glyph(run.status, runningGlyph);
|
|
148
|
+
lines.push(`├─ ${runGlyph} ${shortRunLabel(run)} · ${completed}/${agents.length} done · ${run.runId.slice(-8)}`);
|
|
149
|
+
const visibleAgents = activeAgents.slice(0, 3);
|
|
150
|
+
for (const [index, agent] of visibleAgents.entries()) {
|
|
151
|
+
const last = index === visibleAgents.length - 1 && activeAgents.length <= 3;
|
|
152
|
+
const branch = last ? "└─" : "├─";
|
|
129
153
|
const stats = agentStats(agent);
|
|
130
|
-
lines.push(
|
|
154
|
+
lines.push(`│ ${branch} ${glyph(agent.status, runningGlyph)} ${agent.agent} · ${agent.role}`);
|
|
155
|
+
lines.push(`│ ⎿ ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
|
|
131
156
|
}
|
|
132
|
-
if (activeAgents.length > 3) lines.push(
|
|
157
|
+
if (activeAgents.length > 3) lines.push(`│ └─ … +${activeAgents.length - 3} more agents`);
|
|
133
158
|
if (lines.length >= maxLines) break;
|
|
134
159
|
}
|
|
135
160
|
return lines.slice(0, maxLines);
|
|
136
161
|
}
|
|
137
162
|
|
|
163
|
+
function colorWidgetLine(line: string, index: number, theme: ThemeLike): string {
|
|
164
|
+
const fg = theme.fg?.bind(theme) ?? ((_color: string, text: string) => text);
|
|
165
|
+
const bold = theme.bold?.bind(theme) ?? ((text: string) => text);
|
|
166
|
+
if (index === 0) return line.replace("Crew agents", bold(fg("accent", "Crew agents")));
|
|
167
|
+
return line.replace(/([⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏▶◦✓✗■·])/, (icon: string) => fg(icon === "✓" ? "success" : icon === "✗" ? "error" : icon === "◦" ? "dim" : "accent", icon));
|
|
168
|
+
}
|
|
169
|
+
|
|
138
170
|
class CrewWidgetComponent implements WidgetComponent {
|
|
139
171
|
private cwd: string;
|
|
140
172
|
private frame: number;
|
|
@@ -149,35 +181,39 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
149
181
|
}
|
|
150
182
|
invalidate(): void {}
|
|
151
183
|
render(width: number): string[] {
|
|
152
|
-
|
|
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
|
-
});
|
|
184
|
+
return buildCrewWidgetLines(this.cwd, this.frame, this.maxLines).map((line, index) => truncate(colorWidgetLine(line, index, this.theme), width));
|
|
160
185
|
}
|
|
161
186
|
}
|
|
162
187
|
|
|
188
|
+
function requestRender(ctx: Pick<ExtensionContext, "ui">): void {
|
|
189
|
+
(ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
190
|
+
}
|
|
191
|
+
|
|
163
192
|
export function updateCrewWidget(ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">, state: CrewWidgetState, config?: CrewUiConfig): void {
|
|
164
193
|
if (!ctx.hasUI) return;
|
|
165
194
|
state.frame += 1;
|
|
166
|
-
const maxLines = config?.widgetMaxLines ??
|
|
195
|
+
const maxLines = config?.widgetMaxLines ?? 10;
|
|
167
196
|
const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines);
|
|
168
|
-
|
|
197
|
+
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
198
|
+
ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(activeWidgetRuns(ctx.cwd)) : undefined);
|
|
199
|
+
ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
|
|
169
200
|
if (!lines.length) {
|
|
170
|
-
ctx.ui.setWidget(
|
|
201
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
|
|
202
|
+
requestRender(ctx);
|
|
171
203
|
return;
|
|
172
204
|
}
|
|
173
|
-
ctx.ui.setWidget(
|
|
205
|
+
ctx.ui.setWidget(WIDGET_KEY, ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(ctx.cwd, state.frame, maxLines, theme as ThemeLike)) as never, { placement });
|
|
206
|
+
requestRender(ctx);
|
|
174
207
|
}
|
|
175
208
|
|
|
176
209
|
export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
|
|
177
210
|
if (state.interval) clearInterval(state.interval);
|
|
178
211
|
state.interval = undefined;
|
|
179
212
|
if (ctx?.hasUI) {
|
|
180
|
-
|
|
181
|
-
ctx.ui.
|
|
213
|
+
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
214
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
215
|
+
ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
|
|
216
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
|
|
217
|
+
requestRender(ctx);
|
|
182
218
|
}
|
|
183
219
|
}
|