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.
@@ -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, input.executeWorkers ? "child-process" : "scaffold"));
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: input.executeWorkers ? "pi" : "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 });
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 (input.executeWorkers) {
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}` : input.executeWorkers ? "Worker finished without reporting a verification failure." : "Safe scaffold mode; verification commands were not executed."),
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, input.executeWorkers ? "child-process" : "scaffold"));
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
+ }
@@ -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
- for (let i = 0; i < Math.min(this.runs.length, 10); i++) {
128
- const run = this.runs[i]!;
129
- const marker = i === this.selected ? "›" : " ";
130
- const text = `${marker} ${statusIcon(run.status)} ${run.runId} ${run.status} ${run.team}/${run.workflow ?? "none"} ${run.goal}`;
131
- lines.push(`│ ${padVisible(truncate(text, innerWidth - 1), innerWidth - 1)}│`);
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[this.selected];
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[this.selected]?.runId;
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[this.selected]?.runId;
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[this.selected]?.runId;
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[this.selected]?.runId;
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[this.selected]?.runId;
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[this.selected]?.runId;
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[this.selected]?.runId;
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[this.selected]?.runId;
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[this.selected]?.runId ?? "", action: "reload" });
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
- this.selected = Math.min(Math.max(0, this.runs.length - 1), this.selected + 1);
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
  }