pi-crew 0.1.7 → 0.1.9
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/agents/agent-config.ts +1 -0
- package/src/agents/discover-agents.ts +5 -0
- package/src/config/config.ts +25 -0
- package/src/extension/async-notifier.ts +3 -1
- package/src/extension/cross-extension-rpc.ts +82 -0
- package/src/extension/help.ts +2 -0
- package/src/extension/register.ts +79 -2
- package/src/extension/result-watcher.ts +89 -0
- package/src/extension/team-tool.ts +105 -11
- package/src/runtime/agent-memory.ts +72 -0
- package/src/runtime/agent-observability.ts +29 -4
- package/src/runtime/live-agent-control.ts +78 -0
- package/src/runtime/live-agent-manager.ts +85 -0
- package/src/runtime/live-control-realtime.ts +36 -0
- package/src/runtime/live-session-runtime.ts +271 -5
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/sidechain-output.ts +28 -0
- package/src/runtime/task-runner.ts +77 -12
- package/src/runtime/team-runner.ts +4 -1
- package/src/ui/crew-widget.ts +113 -0
- package/src/ui/run-dashboard.ts +56 -16
- package/src/ui/transcript-viewer.ts +204 -0
- package/src/utils/file-coalescer.ts +33 -0
- package/src/worktree/worktree-manager.ts +73 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SidechainEntry {
|
|
5
|
+
isSidechain: true;
|
|
6
|
+
agentId: string;
|
|
7
|
+
type: string;
|
|
8
|
+
message: unknown;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
19
|
+
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function eventToSidechainType(event: unknown): string | undefined {
|
|
23
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
|
|
24
|
+
const type = (event as { type?: unknown }).type;
|
|
25
|
+
if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
|
|
26
|
+
if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
|
|
27
|
+
return typeof type === "string" ? type : undefined;
|
|
28
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
3
|
-
import type { CrewLimitsConfig } from "../config/config.ts";
|
|
3
|
+
import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
4
4
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
5
5
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
6
6
|
import { appendEvent } from "../state/event-log.ts";
|
|
@@ -8,7 +8,7 @@ import { saveRunManifest, saveRunTasks } from "../state/state-store.ts";
|
|
|
8
8
|
import { createTaskClaim } from "../state/task-claims.ts";
|
|
9
9
|
import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
|
|
10
10
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
11
|
-
import { captureWorktreeDiff, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
|
|
11
|
+
import { captureWorktreeDiff, captureWorktreeDiffStat, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
|
|
12
12
|
import { buildModelCandidates, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
|
|
13
13
|
import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
|
|
14
14
|
import { runChildPi } from "./child-pi.ts";
|
|
@@ -19,7 +19,9 @@ import { permissionForRole } from "./role-permission.ts";
|
|
|
19
19
|
import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
|
|
20
20
|
import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
|
|
21
21
|
import { parseSessionUsage } from "./session-usage.ts";
|
|
22
|
-
import type { CrewAgentProgress } from "./crew-agent-runtime.ts";
|
|
22
|
+
import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
23
|
+
import { buildMemoryBlock } from "./agent-memory.ts";
|
|
24
|
+
import { runLiveSessionTask } from "./live-session-runtime.ts";
|
|
23
25
|
|
|
24
26
|
export interface TaskRunnerInput {
|
|
25
27
|
manifest: TeamRunManifest;
|
|
@@ -29,6 +31,11 @@ export interface TaskRunnerInput {
|
|
|
29
31
|
agent: AgentConfig;
|
|
30
32
|
signal?: AbortSignal;
|
|
31
33
|
executeWorkers: boolean;
|
|
34
|
+
runtimeKind?: CrewRuntimeKind;
|
|
35
|
+
runtimeConfig?: CrewRuntimeConfig;
|
|
36
|
+
parentContext?: string;
|
|
37
|
+
parentModel?: unknown;
|
|
38
|
+
modelRegistry?: unknown;
|
|
32
39
|
limits?: CrewLimitsConfig;
|
|
33
40
|
dependencyContextText?: string;
|
|
34
41
|
}
|
|
@@ -57,7 +64,8 @@ function coordinationBridgeInstructions(task: TeamTaskState): string {
|
|
|
57
64
|
].join("\n");
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): string {
|
|
67
|
+
function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState, agent?: AgentConfig): string {
|
|
68
|
+
const memoryBlock = agent?.memory ? buildMemoryBlock(agent.name, agent.memory, task.cwd, Boolean(agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
|
|
61
69
|
return [
|
|
62
70
|
"# pi-crew Worker Runtime Context",
|
|
63
71
|
`Run ID: ${manifest.runId}`,
|
|
@@ -88,6 +96,7 @@ function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: T
|
|
|
88
96
|
task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
|
|
89
97
|
"",
|
|
90
98
|
(inputDependencyContext(task) || ""),
|
|
99
|
+
memoryBlock,
|
|
91
100
|
"Task:",
|
|
92
101
|
step.task.replaceAll("{goal}", manifest.goal),
|
|
93
102
|
].join("\n");
|
|
@@ -227,12 +236,13 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
227
236
|
...(dependencyContextText ? { dependencyContextText } : {}),
|
|
228
237
|
} as TeamTaskState;
|
|
229
238
|
let tasks = updateTask(input.tasks, task);
|
|
239
|
+
const runtimeKind = input.runtimeKind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
230
240
|
saveRunTasks(manifest, tasks);
|
|
231
|
-
upsertCrewAgent(manifest, recordFromTask(manifest, task,
|
|
232
|
-
appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
|
|
241
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
|
|
242
|
+
appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, runtime: runtimeKind, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
|
|
233
243
|
const permissionMode = permissionForRole(task.role);
|
|
234
244
|
|
|
235
|
-
const prompt = renderTaskPrompt(manifest, input.step, task);
|
|
245
|
+
const prompt = renderTaskPrompt(manifest, input.step, task, input.agent);
|
|
236
246
|
const promptArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
237
247
|
kind: "prompt",
|
|
238
248
|
relativePath: `prompts/${task.id}.md`,
|
|
@@ -248,7 +258,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
248
258
|
let modelAttempts: ModelAttemptSummary[] | undefined;
|
|
249
259
|
let parsedOutput: ParsedPiJsonOutput | undefined;
|
|
250
260
|
|
|
251
|
-
let startupEvidence = createStartupEvidence({ command:
|
|
261
|
+
let startupEvidence = createStartupEvidence({ command: runtimeKind === "child-process" ? "pi" : runtimeKind === "live-session" ? "live-session" : "safe-scaffold", startedAt: new Date(task.startedAt ?? new Date().toISOString()), finishedAt: new Date(), promptSentAt: new Date(task.startedAt ?? new Date().toISOString()), promptAccepted: true, exitCode: 0 });
|
|
252
262
|
const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
|
|
253
263
|
const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
254
264
|
kind: "metadata",
|
|
@@ -256,7 +266,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
256
266
|
content: `${coordinationBridgeInstructions(task)}\n`,
|
|
257
267
|
producer: task.id,
|
|
258
268
|
});
|
|
259
|
-
if (
|
|
269
|
+
if (runtimeKind === "child-process") {
|
|
260
270
|
const candidates = buildModelCandidates(input.step.model ?? input.agent.model, input.agent.fallbackModels, undefined);
|
|
261
271
|
const attemptModels = candidates.length > 0 ? candidates : [input.step.model ?? input.agent.model];
|
|
262
272
|
const logs: string[] = [];
|
|
@@ -326,6 +336,55 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
326
336
|
producer: task.id,
|
|
327
337
|
});
|
|
328
338
|
}
|
|
339
|
+
} else if (runtimeKind === "live-session") {
|
|
340
|
+
const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
|
|
341
|
+
const attemptStartedAt = new Date();
|
|
342
|
+
const liveResult = await runLiveSessionTask({
|
|
343
|
+
manifest,
|
|
344
|
+
task,
|
|
345
|
+
step: input.step,
|
|
346
|
+
agent: input.agent,
|
|
347
|
+
prompt,
|
|
348
|
+
signal: input.signal,
|
|
349
|
+
transcriptPath,
|
|
350
|
+
runtimeConfig: input.runtimeConfig,
|
|
351
|
+
parentContext: input.parentContext,
|
|
352
|
+
parentModel: input.parentModel,
|
|
353
|
+
modelRegistry: input.modelRegistry,
|
|
354
|
+
onOutput: (text) => appendCrewAgentOutput(manifest, task.id, text),
|
|
355
|
+
onEvent: (event) => {
|
|
356
|
+
appendCrewAgentEvent(manifest, task.id, event);
|
|
357
|
+
task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
|
|
358
|
+
tasks = updateTask(tasks, task);
|
|
359
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "live-session"));
|
|
360
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { event } });
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
startupEvidence = createStartupEvidence({ command: "live-session", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: liveResult.exitCode === 0 && !liveResult.error, stderr: liveResult.stderr, error: liveResult.error, exitCode: liveResult.exitCode });
|
|
364
|
+
exitCode = liveResult.exitCode;
|
|
365
|
+
error = liveResult.error || (liveResult.exitCode && liveResult.exitCode !== 0 ? liveResult.stderr || `Live session exited with ${liveResult.exitCode}` : undefined);
|
|
366
|
+
parsedOutput = { finalText: liveResult.stdout, textEvents: liveResult.stdout ? [liveResult.stdout] : [], jsonEvents: liveResult.jsonEvents, usage: liveResult.usage };
|
|
367
|
+
if (liveResult.usage) task = { ...task, usage: liveResult.usage, agentProgress: applyUsageToProgress(task.agentProgress, liveResult.usage) };
|
|
368
|
+
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
369
|
+
kind: "result",
|
|
370
|
+
relativePath: `results/${task.id}.txt`,
|
|
371
|
+
content: liveResult.stdout || liveResult.stderr || "(no output)",
|
|
372
|
+
producer: task.id,
|
|
373
|
+
});
|
|
374
|
+
logArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
375
|
+
kind: "log",
|
|
376
|
+
relativePath: `logs/${task.id}.log`,
|
|
377
|
+
content: [`runtime=live-session`, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${liveResult.jsonEvents}`, liveResult.usage ? `usage=${JSON.stringify(liveResult.usage)}` : "", "", "STDOUT:", liveResult.stdout, "", "STDERR:", liveResult.stderr].join("\n"),
|
|
378
|
+
producer: task.id,
|
|
379
|
+
});
|
|
380
|
+
if (fs.existsSync(transcriptPath)) {
|
|
381
|
+
transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
382
|
+
kind: "log",
|
|
383
|
+
relativePath: `transcripts/${task.id}.jsonl`,
|
|
384
|
+
content: fs.readFileSync(transcriptPath, "utf-8"),
|
|
385
|
+
producer: task.id,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
329
388
|
} else {
|
|
330
389
|
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
331
390
|
kind: "result",
|
|
@@ -346,6 +405,12 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
346
405
|
content: captureWorktreeDiff(workspace.worktreePath),
|
|
347
406
|
producer: task.id,
|
|
348
407
|
}) : undefined;
|
|
408
|
+
const diffStatArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
|
|
409
|
+
kind: "metadata",
|
|
410
|
+
relativePath: `metadata/${task.id}.diff-stat.json`,
|
|
411
|
+
content: `${JSON.stringify({ ...captureWorktreeDiffStat(workspace.worktreePath), syntheticPaths: workspace.syntheticPaths ?? [], nodeModulesLinked: workspace.nodeModulesLinked ?? false }, null, 2)}\n`,
|
|
412
|
+
producer: task.id,
|
|
413
|
+
}) : undefined;
|
|
349
414
|
|
|
350
415
|
task = {
|
|
351
416
|
...task,
|
|
@@ -357,7 +422,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
357
422
|
jsonEvents: parsedOutput?.jsonEvents,
|
|
358
423
|
agentProgress: error && task.agentProgress?.currentTool ? { ...task.agentProgress, failedTool: task.agentProgress.currentTool } : task.agentProgress,
|
|
359
424
|
error,
|
|
360
|
-
verification: createVerificationEvidence(taskPacket.verification, !error, error ? `Task failed: ${error}` :
|
|
425
|
+
verification: createVerificationEvidence(taskPacket.verification, !error, error ? `Task failed: ${error}` : runtimeKind === "scaffold" ? "Safe scaffold mode; verification commands were not executed." : `${runtimeKind} worker finished without reporting a verification failure.`),
|
|
361
426
|
promptArtifact,
|
|
362
427
|
resultArtifact,
|
|
363
428
|
claim: undefined,
|
|
@@ -391,10 +456,10 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
391
456
|
content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
|
|
392
457
|
producer: task.id,
|
|
393
458
|
});
|
|
394
|
-
manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : [])] };
|
|
459
|
+
manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : []), ...(diffStatArtifact ? [diffStatArtifact] : [])] };
|
|
395
460
|
saveRunManifest(manifest);
|
|
396
461
|
saveRunTasks(manifest, tasks);
|
|
397
|
-
upsertCrewAgent(manifest, recordFromTask(manifest, task,
|
|
462
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
|
|
398
463
|
appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
|
|
399
464
|
return { manifest, tasks };
|
|
400
465
|
}
|
|
@@ -27,6 +27,9 @@ export interface ExecuteTeamRunInput {
|
|
|
27
27
|
limits?: CrewLimitsConfig;
|
|
28
28
|
runtime?: CrewRuntimeCapabilities;
|
|
29
29
|
runtimeConfig?: CrewRuntimeConfig;
|
|
30
|
+
parentContext?: string;
|
|
31
|
+
parentModel?: unknown;
|
|
32
|
+
modelRegistry?: unknown;
|
|
30
33
|
signal?: AbortSignal;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -174,7 +177,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
174
177
|
const results = await Promise.all(readyBatch.map((task) => {
|
|
175
178
|
const step = findStep(input.workflow, task);
|
|
176
179
|
const agent = findAgent(input.agents, task);
|
|
177
|
-
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, limits: input.limits });
|
|
180
|
+
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, limits: input.limits });
|
|
178
181
|
}));
|
|
179
182
|
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
180
183
|
tasks = mergeTaskUpdates(tasks, results);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { listRuns } from "../extension/run-index.ts";
|
|
3
|
+
import { isActiveRunStatus } from "../runtime/process-status.ts";
|
|
4
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
|
+
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
7
|
+
|
|
8
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
|
+
const TOOL_LABELS: Record<string, string> = {
|
|
10
|
+
read: "reading",
|
|
11
|
+
bash: "running command",
|
|
12
|
+
edit: "editing",
|
|
13
|
+
write: "writing",
|
|
14
|
+
grep: "searching",
|
|
15
|
+
find: "finding files",
|
|
16
|
+
ls: "listing",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface CrewWidgetState {
|
|
20
|
+
frame: number;
|
|
21
|
+
interval?: ReturnType<typeof setInterval>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function elapsed(iso: string | undefined, now = Date.now()): string | undefined {
|
|
25
|
+
if (!iso) return undefined;
|
|
26
|
+
const ms = Math.max(0, now - new Date(iso).getTime());
|
|
27
|
+
if (!Number.isFinite(ms)) return undefined;
|
|
28
|
+
if (ms < 1000) return "now";
|
|
29
|
+
if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
|
|
30
|
+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
|
|
31
|
+
return `${Math.floor(ms / 3_600_000)}h`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function glyph(status: string, runningGlyph: string): string {
|
|
35
|
+
if (status === "running") return runningGlyph;
|
|
36
|
+
if (status === "queued") return "◦";
|
|
37
|
+
if (status === "completed") return "✓";
|
|
38
|
+
if (status === "failed") return "✗";
|
|
39
|
+
if (status === "cancelled" || status === "stopped") return "■";
|
|
40
|
+
return "·";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function agentActivity(agent: CrewAgentRecord): string {
|
|
44
|
+
if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
|
|
45
|
+
const recent = agent.progress?.recentOutput?.at(-1);
|
|
46
|
+
if (recent) return recent.replace(/\s+/g, " ").trim();
|
|
47
|
+
if (agent.progress?.activityState === "needs_attention") return "needs attention";
|
|
48
|
+
if (agent.status === "queued") return "queued…";
|
|
49
|
+
if (agent.status === "running") return "thinking…";
|
|
50
|
+
if (agent.status === "failed") return agent.error ?? "failed";
|
|
51
|
+
return "done";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function agentStats(agent: CrewAgentRecord): string {
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
|
|
57
|
+
if (agent.progress?.tokens) parts.push(`${agent.progress.tokens} tok`);
|
|
58
|
+
if (agent.progress?.turns) parts.push(`⟳${agent.progress.turns}`);
|
|
59
|
+
const age = elapsed(agent.completedAt ?? agent.startedAt);
|
|
60
|
+
if (age) parts.push(agent.completedAt ? age : `${age} ago`);
|
|
61
|
+
return parts.join(" · ");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runStep(run: TeamRunManifest, agents: CrewAgentRecord[]): string {
|
|
65
|
+
const running = agents.find((agent) => agent.status === "running");
|
|
66
|
+
if (running) return running.taskId;
|
|
67
|
+
const queued = agents.find((agent) => agent.status === "queued");
|
|
68
|
+
if (queued) return queued.taskId;
|
|
69
|
+
return run.status;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8): string[] {
|
|
73
|
+
const runs = listRuns(cwd).slice(0, 20);
|
|
74
|
+
const activeRuns = runs.filter((run) => isActiveRunStatus(run.status));
|
|
75
|
+
const recentRuns = runs.filter((run) => !isActiveRunStatus(run.status)).slice(0, 3);
|
|
76
|
+
const shownRuns = [...activeRuns, ...recentRuns];
|
|
77
|
+
if (!shownRuns.length) return [];
|
|
78
|
+
const runningGlyph = SPINNER[frame % SPINNER.length] ?? "⠋";
|
|
79
|
+
const lines: string[] = [];
|
|
80
|
+
const activeCount = activeRuns.length;
|
|
81
|
+
lines.push(`${activeCount ? "●" : "○"} pi-crew · active=${activeCount} recent=${recentRuns.length} · /team-dashboard`);
|
|
82
|
+
for (const run of shownRuns) {
|
|
83
|
+
let agents: CrewAgentRecord[] = [];
|
|
84
|
+
try { agents = readCrewAgents(run); } catch { agents = []; }
|
|
85
|
+
const counts = new Map<string, number>();
|
|
86
|
+
for (const agent of agents) counts.set(agent.status, (counts.get(agent.status) ?? 0) + 1);
|
|
87
|
+
const countText = [...counts.entries()].map(([status, count]) => `${status}:${count}`).join(" ") || run.status;
|
|
88
|
+
lines.push(`${glyph(run.status, runningGlyph)} ${run.runId.slice(-8)} ${run.team}/${run.workflow ?? "none"} · ${runStep(run, agents)} · ${countText}`);
|
|
89
|
+
for (const agent of agents.filter((item) => item.status === "running" || item.status === "queued").slice(0, 2)) {
|
|
90
|
+
const stats = agentStats(agent);
|
|
91
|
+
lines.push(` ${glyph(agent.status, runningGlyph)} ${agent.taskId} ${agent.role}→${agent.agent} · ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
|
|
92
|
+
}
|
|
93
|
+
if (lines.length >= maxLines) break;
|
|
94
|
+
}
|
|
95
|
+
return lines.slice(0, maxLines);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function updateCrewWidget(ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">, state: CrewWidgetState): void {
|
|
99
|
+
if (!ctx.hasUI) return;
|
|
100
|
+
state.frame += 1;
|
|
101
|
+
const lines = buildCrewWidgetLines(ctx.cwd, state.frame);
|
|
102
|
+
ctx.ui.setStatus("pi-crew", lines.length ? lines[0] : undefined);
|
|
103
|
+
ctx.ui.setWidget("pi-crew", lines.length ? lines : undefined, { placement: "aboveEditor" });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState): void {
|
|
107
|
+
if (state.interval) clearInterval(state.interval);
|
|
108
|
+
state.interval = undefined;
|
|
109
|
+
if (ctx?.hasUI) {
|
|
110
|
+
ctx.ui.setStatus("pi-crew", undefined);
|
|
111
|
+
ctx.ui.setWidget("pi-crew", undefined, { placement: "aboveEditor" });
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
3
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
4
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
5
|
+
import { isActiveRunStatus } from "../runtime/process-status.ts";
|
|
5
6
|
|
|
6
7
|
interface DashboardComponent {
|
|
7
8
|
invalidate(): void;
|
|
@@ -68,6 +69,16 @@ function readProgressPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function formatAge(iso: string | undefined): string | undefined {
|
|
73
|
+
if (!iso) return undefined;
|
|
74
|
+
const ms = Math.max(0, Date.now() - new Date(iso).getTime());
|
|
75
|
+
if (!Number.isFinite(ms)) return undefined;
|
|
76
|
+
if (ms < 1000) return "now";
|
|
77
|
+
if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
|
|
78
|
+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
|
|
79
|
+
return `${Math.floor(ms / 3_600_000)}h`;
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
function agentPreviewLine(agent: CrewAgentRecord): string {
|
|
72
83
|
const stats = [
|
|
73
84
|
agent.progress?.activityState,
|
|
@@ -76,6 +87,7 @@ function agentPreviewLine(agent: CrewAgentRecord): string {
|
|
|
76
87
|
agent.progress?.tokens !== undefined ? `${agent.progress.tokens} tok` : undefined,
|
|
77
88
|
agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
|
|
78
89
|
agent.progress?.failedTool ? `failedTool=${agent.progress.failedTool}` : undefined,
|
|
90
|
+
agent.startedAt ? `age=${formatAge(agent.completedAt ?? agent.startedAt)}` : undefined,
|
|
79
91
|
].filter((part): part is string => Boolean(part));
|
|
80
92
|
const recent = agent.progress?.recentOutput?.at(-1);
|
|
81
93
|
return `Agent: ${statusIcon(agent.status)} ${agent.taskId} ${agent.role}->${agent.agent}${stats.length ? ` · ${stats.join(" · ")}` : ""}${recent ? ` ⎿ ${recent}` : ""}`;
|
|
@@ -92,6 +104,29 @@ function readAgentPreview(run: TeamRunManifest, maxLines = 5): string[] {
|
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
function runLabel(run: TeamRunManifest, selected: boolean): string {
|
|
108
|
+
let agents: CrewAgentRecord[] = [];
|
|
109
|
+
try { agents = readCrewAgents(run); } catch { agents = []; }
|
|
110
|
+
const running = agents.find((agent) => agent.status === "running");
|
|
111
|
+
const queued = agents.find((agent) => agent.status === "queued");
|
|
112
|
+
const step = running ? `step ${running.taskId}` : queued ? `queued ${queued.taskId}` : `agents ${agents.length}`;
|
|
113
|
+
const marker = selected ? "›" : " ";
|
|
114
|
+
return `${marker} ${statusIcon(run.status)} ${run.runId.slice(-8)} ${run.status} | ${run.team}/${run.workflow ?? "none"} | ${step} | ${run.goal}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function groupedRuns(runs: TeamRunManifest[]): Array<{ label: string; run?: TeamRunManifest }> {
|
|
118
|
+
const active = runs.filter((run) => isActiveRunStatus(run.status));
|
|
119
|
+
const recent = runs.filter((run) => !isActiveRunStatus(run.status));
|
|
120
|
+
const rows: Array<{ label: string; run?: TeamRunManifest }> = [];
|
|
121
|
+
if (active.length) rows.push({ label: "Active" }, ...active.map((run) => ({ label: run.runId, run })));
|
|
122
|
+
if (recent.length) rows.push({ label: "Recent" }, ...recent.map((run) => ({ label: run.runId, run })));
|
|
123
|
+
return rows;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function selectedRunFromGrouped(runs: TeamRunManifest[], selected: number): TeamRunManifest | undefined {
|
|
127
|
+
return groupedRuns(runs).filter((row) => row.run)[selected]?.run;
|
|
128
|
+
}
|
|
129
|
+
|
|
95
130
|
function countByStatus(runs: TeamRunManifest[]): string {
|
|
96
131
|
const counts = new Map<string, number>();
|
|
97
132
|
for (const run of runs) counts.set(run.status, (counts.get(run.status) ?? 0) + 1);
|
|
@@ -124,13 +159,17 @@ export class RunDashboard implements DashboardComponent {
|
|
|
124
159
|
if (this.runs.length === 0) {
|
|
125
160
|
lines.push(`│ ${padVisible(truncate("No runs found.", innerWidth - 1), innerWidth - 1)}│`);
|
|
126
161
|
} else {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
162
|
+
const rows = groupedRuns(this.runs).slice(0, 16);
|
|
163
|
+
const runRows = rows.filter((row) => row.run);
|
|
164
|
+
for (const row of rows) {
|
|
165
|
+
if (!row.run) {
|
|
166
|
+
lines.push(`│ ${padVisible(truncate(row.label, innerWidth - 1), innerWidth - 1)}│`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const index = runRows.findIndex((candidate) => candidate.run?.runId === row.run?.runId);
|
|
170
|
+
lines.push(`│ ${padVisible(truncate(runLabel(row.run, index === this.selected), innerWidth - 1), innerWidth - 1)}│`);
|
|
132
171
|
}
|
|
133
|
-
const selectedRun = this.runs
|
|
172
|
+
const selectedRun = selectedRunFromGrouped(this.runs, this.selected);
|
|
134
173
|
if (selectedRun) {
|
|
135
174
|
lines.push(`├${"─".repeat(borderWidth)}┤`);
|
|
136
175
|
const details = [
|
|
@@ -157,47 +196,47 @@ export class RunDashboard implements DashboardComponent {
|
|
|
157
196
|
return;
|
|
158
197
|
}
|
|
159
198
|
if (data === "\r" || data === "\n" || data === "s") {
|
|
160
|
-
const runId = this.runs
|
|
199
|
+
const runId = selectedRunFromGrouped(this.runs, this.selected)?.runId;
|
|
161
200
|
this.done(runId ? { runId, action: "status" } : undefined);
|
|
162
201
|
return;
|
|
163
202
|
}
|
|
164
203
|
if (data === "u") {
|
|
165
|
-
const runId = this.runs
|
|
204
|
+
const runId = selectedRunFromGrouped(this.runs, this.selected)?.runId;
|
|
166
205
|
this.done(runId ? { runId, action: "summary" } : undefined);
|
|
167
206
|
return;
|
|
168
207
|
}
|
|
169
208
|
if (data === "a") {
|
|
170
|
-
const runId = this.runs
|
|
209
|
+
const runId = selectedRunFromGrouped(this.runs, this.selected)?.runId;
|
|
171
210
|
this.done(runId ? { runId, action: "artifacts" } : undefined);
|
|
172
211
|
return;
|
|
173
212
|
}
|
|
174
213
|
if (data === "i") {
|
|
175
|
-
const runId = this.runs
|
|
214
|
+
const runId = selectedRunFromGrouped(this.runs, this.selected)?.runId;
|
|
176
215
|
this.done(runId ? { runId, action: "api" } : undefined);
|
|
177
216
|
return;
|
|
178
217
|
}
|
|
179
218
|
if (data === "d") {
|
|
180
|
-
const runId = this.runs
|
|
219
|
+
const runId = selectedRunFromGrouped(this.runs, this.selected)?.runId;
|
|
181
220
|
this.done(runId ? { runId, action: "agents" } : undefined);
|
|
182
221
|
return;
|
|
183
222
|
}
|
|
184
223
|
if (data === "e") {
|
|
185
|
-
const runId = this.runs
|
|
224
|
+
const runId = selectedRunFromGrouped(this.runs, this.selected)?.runId;
|
|
186
225
|
this.done(runId ? { runId, action: "agent-events" } : undefined);
|
|
187
226
|
return;
|
|
188
227
|
}
|
|
189
228
|
if (data === "o") {
|
|
190
|
-
const runId = this.runs
|
|
229
|
+
const runId = selectedRunFromGrouped(this.runs, this.selected)?.runId;
|
|
191
230
|
this.done(runId ? { runId, action: "agent-output" } : undefined);
|
|
192
231
|
return;
|
|
193
232
|
}
|
|
194
233
|
if (data === "v") {
|
|
195
|
-
const runId = this.runs
|
|
234
|
+
const runId = selectedRunFromGrouped(this.runs, this.selected)?.runId;
|
|
196
235
|
this.done(runId ? { runId, action: "agent-transcript" } : undefined);
|
|
197
236
|
return;
|
|
198
237
|
}
|
|
199
238
|
if (data === "r") {
|
|
200
|
-
this.done({ runId: this.runs
|
|
239
|
+
this.done({ runId: selectedRunFromGrouped(this.runs, this.selected)?.runId ?? "", action: "reload" });
|
|
201
240
|
return;
|
|
202
241
|
}
|
|
203
242
|
if (data === "p") {
|
|
@@ -209,7 +248,8 @@ export class RunDashboard implements DashboardComponent {
|
|
|
209
248
|
return;
|
|
210
249
|
}
|
|
211
250
|
if (data === "j" || data === "\u001b[B") {
|
|
212
|
-
|
|
251
|
+
const selectableCount = groupedRuns(this.runs).filter((row) => row.run).length;
|
|
252
|
+
this.selected = Math.min(Math.max(0, selectableCount - 1), this.selected + 1);
|
|
213
253
|
}
|
|
214
254
|
}
|
|
215
255
|
}
|