pi-crew 0.1.41 → 0.1.44
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/CHANGELOG.md +47 -0
- package/README.md +51 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +199 -0
- package/docs/research-phase11-distillation.md +201 -0
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +5 -4
- package/src/config/config.ts +28 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/management.ts +37 -8
- package/src/extension/notification-router.ts +2 -2
- package/src/extension/register.ts +130 -8
- package/src/extension/registration/commands.ts +11 -9
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +28 -19
- package/src/extension/registration/team-tool.ts +2 -1
- package/src/extension/result-watcher.ts +4 -4
- package/src/extension/run-bundle-schema.ts +8 -4
- package/src/extension/run-import.ts +4 -0
- package/src/extension/run-index.ts +23 -1
- package/src/extension/run-maintenance.ts +43 -24
- package/src/extension/team-tool/api.ts +2 -2
- package/src/extension/team-tool/cancel.ts +76 -4
- package/src/extension/team-tool/context.ts +1 -0
- package/src/extension/team-tool/doctor.ts +8 -1
- package/src/extension/team-tool/handle-settings.ts +188 -0
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +67 -0
- package/src/extension/team-tool/run.ts +6 -4
- package/src/extension/team-tool/status.ts +99 -93
- package/src/extension/team-tool-types.ts +4 -0
- package/src/extension/team-tool.ts +5 -1
- package/src/i18n.ts +184 -0
- package/src/observability/correlation.ts +2 -2
- package/src/observability/event-to-metric.ts +10 -3
- package/src/observability/exporters/adapter.ts +7 -1
- package/src/observability/exporters/otlp-exporter.ts +14 -2
- package/src/observability/exporters/prometheus-exporter.ts +9 -2
- package/src/observability/metric-registry.ts +18 -3
- package/src/observability/metric-retention.ts +11 -3
- package/src/observability/metric-sink.ts +9 -4
- package/src/observability/metrics-primitives.ts +4 -3
- package/src/prompt/prompt-runtime.ts +72 -68
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -23
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +4 -4
- package/src/runtime/completion-guard.ts +95 -4
- package/src/runtime/concurrency.ts +1 -1
- package/src/runtime/crash-recovery.ts +32 -1
- package/src/runtime/crew-agent-runtime.ts +59 -58
- package/src/runtime/deadletter.ts +14 -4
- package/src/runtime/delivery-coordinator.ts +143 -0
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +48 -4
- package/src/runtime/live-agent-control.ts +87 -87
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +305 -305
- package/src/runtime/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +272 -261
- package/src/runtime/overflow-recovery.ts +157 -0
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +1 -1
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -78
- package/src/runtime/post-exit-stdio-guard.ts +2 -2
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +5 -0
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-resources.ts +25 -0
- package/src/runtime/session-snapshot.ts +59 -0
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stale-reconciler.ts +179 -0
- package/src/runtime/subagent-manager.ts +3 -3
- package/src/runtime/supervisor-contact.ts +59 -0
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +14 -0
- package/src/runtime/team-runner.ts +9 -10
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +2 -1
- package/src/schema/team-tool-schema.ts +115 -109
- package/src/state/artifact-store.ts +4 -2
- package/src/state/atomic-write.ts +12 -4
- package/src/state/contracts.ts +109 -105
- package/src/state/event-log.ts +3 -4
- package/src/state/jsonl-writer.ts +4 -1
- package/src/state/locks.ts +9 -1
- package/src/state/task-claims.ts +44 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +2 -2
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +5 -4
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +3 -2
- package/src/ui/powerbar-publisher.ts +7 -6
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +54 -14
- package/src/ui/run-dashboard.ts +39 -11
- package/src/ui/run-snapshot-cache.ts +336 -36
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/theme-adapter.ts +1 -1
- package/src/ui/transcript-viewer.ts +7 -2
- package/src/utils/atomic-write.ts +33 -0
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +5 -3
- package/src/utils/frontmatter.ts +68 -36
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/internal-error.ts +1 -1
- package/src/utils/names.ts +27 -26
- package/src/utils/paths.ts +1 -1
- package/src/utils/redaction.ts +44 -41
- package/src/utils/safe-paths.ts +47 -34
- package/src/utils/sleep.ts +2 -2
- package/src/utils/timings.ts +2 -0
- package/src/utils/visual.ts +9 -1
- package/src/workflows/discover-workflows.ts +4 -1
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +6 -1
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -4,17 +4,24 @@ import * as path from "node:path";
|
|
|
4
4
|
import { readCrewAgents, agentsPath, agentOutputPath } from "../runtime/crew-agent-records.ts";
|
|
5
5
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
6
|
import { isActiveRunStatus } from "../runtime/process-status.ts";
|
|
7
|
-
import {
|
|
7
|
+
import type { TeamEvent } from "../state/event-log.ts";
|
|
8
8
|
import type { MailboxMessageStatus } from "../state/mailbox.ts";
|
|
9
9
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
10
10
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
11
|
-
import type { RunSnapshotCache, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
|
|
11
|
+
import type { RunSnapshotCache as RunSnapshotCacheBase, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
export interface RunSnapshotCache extends RunSnapshotCacheBase {
|
|
14
|
+
preloadStale(runId: string): Promise<RunUiSnapshot | undefined>;
|
|
15
|
+
preloadAllStale(runIds: string[]): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_TTL_MS = 500;
|
|
14
19
|
const DEFAULT_MAX_ENTRIES = 24;
|
|
15
20
|
const DEFAULT_RECENT_EVENTS = 20;
|
|
16
21
|
const DEFAULT_RECENT_OUTPUT_LINES = 20;
|
|
17
22
|
const MAX_TAIL_BYTES = 32 * 1024;
|
|
23
|
+
/** Max JSONL lines to tail when reading growing files (events, mailbox). */
|
|
24
|
+
const MAX_TAIL_LINES = 500;
|
|
18
25
|
|
|
19
26
|
interface FileStamp {
|
|
20
27
|
mtimeMs: number;
|
|
@@ -58,6 +65,16 @@ function stampFile(filePath: string | undefined): FileStamp {
|
|
|
58
65
|
}
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
async function stampFileAsync(filePath: string | undefined): Promise<FileStamp> {
|
|
69
|
+
if (!filePath) return zeroStamp();
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.promises.stat(filePath);
|
|
72
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
73
|
+
} catch {
|
|
74
|
+
return zeroStamp();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
function combineStamps(stamps: FileStamp[]): FileStamp {
|
|
62
79
|
return stamps.reduce((acc, stamp) => ({ mtimeMs: Math.max(acc.mtimeMs, stamp.mtimeMs), size: acc.size + stamp.size }), zeroStamp());
|
|
63
80
|
}
|
|
@@ -82,6 +99,26 @@ function mailboxStamp(manifest: TeamRunManifest): FileStamp {
|
|
|
82
99
|
return combineStamps(stamps);
|
|
83
100
|
}
|
|
84
101
|
|
|
102
|
+
async function mailboxStampAsync(manifest: TeamRunManifest): Promise<FileStamp> {
|
|
103
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
104
|
+
const stamps: FileStamp[] = [
|
|
105
|
+
await stampFileAsync(path.join(root, "inbox.jsonl")),
|
|
106
|
+
await stampFileAsync(path.join(root, "outbox.jsonl")),
|
|
107
|
+
await stampFileAsync(path.join(root, "delivery.json")),
|
|
108
|
+
];
|
|
109
|
+
const tasksRoot = path.join(root, "tasks");
|
|
110
|
+
try {
|
|
111
|
+
for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
|
|
112
|
+
if (!entry.isDirectory()) continue;
|
|
113
|
+
stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "inbox.jsonl")));
|
|
114
|
+
stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "outbox.jsonl")));
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// No task mailbox yet.
|
|
118
|
+
}
|
|
119
|
+
return combineStamps(stamps);
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord): string | undefined {
|
|
86
123
|
try {
|
|
87
124
|
return agentOutputPath(manifest, agent.taskId);
|
|
@@ -94,6 +131,10 @@ function outputStamp(manifest: TeamRunManifest, agents: CrewAgentRecord[]): File
|
|
|
94
131
|
return combineStamps(agents.map((agent) => stampFile(safeAgentOutputPath(manifest, agent))));
|
|
95
132
|
}
|
|
96
133
|
|
|
134
|
+
async function outputStampAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<FileStamp> {
|
|
135
|
+
return combineStamps(await Promise.all(agents.map((agent) => stampFileAsync(safeAgentOutputPath(manifest, agent)))));
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
function sameStamp(a: FileStamp, b: FileStamp): boolean {
|
|
98
139
|
return a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
99
140
|
}
|
|
@@ -116,14 +157,84 @@ function readTasks(tasksPath: string): TeamTaskState[] {
|
|
|
116
157
|
}
|
|
117
158
|
}
|
|
118
159
|
|
|
119
|
-
function
|
|
160
|
+
async function readTasksAsync(tasksPath: string): Promise<TeamTaskState[]> {
|
|
161
|
+
try {
|
|
162
|
+
const content = await fs.promises.readFile(tasksPath, "utf-8");
|
|
163
|
+
const parsed = JSON.parse(content) as unknown;
|
|
164
|
+
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
|
165
|
+
} catch {
|
|
166
|
+
throw new Error(`Failed to parse tasks at ${tasksPath}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
171
|
+
function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): T[] {
|
|
172
|
+
if (limit <= 0) return [];
|
|
173
|
+
try {
|
|
174
|
+
const stat = fs.statSync(filePath);
|
|
175
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
176
|
+
const fd = fs.openSync(filePath, "r");
|
|
177
|
+
try {
|
|
178
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
179
|
+
fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
180
|
+
const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
|
|
181
|
+
return lines.flatMap((line) => {
|
|
182
|
+
const item = parse(line);
|
|
183
|
+
return item ? [item] : [];
|
|
184
|
+
}).slice(-limit);
|
|
185
|
+
} finally {
|
|
186
|
+
fs.closeSync(fd);
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Async tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
194
|
+
async function tailJsonlLinesAsync<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): Promise<T[]> {
|
|
195
|
+
if (limit <= 0) return [];
|
|
120
196
|
try {
|
|
121
|
-
|
|
197
|
+
const stat = await fs.promises.stat(filePath);
|
|
198
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
199
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
200
|
+
try {
|
|
201
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
202
|
+
await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
203
|
+
const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
|
|
204
|
+
return lines.flatMap((line) => {
|
|
205
|
+
const item = parse(line);
|
|
206
|
+
return item ? [item] : [];
|
|
207
|
+
}).slice(-limit);
|
|
208
|
+
} finally {
|
|
209
|
+
await handle.close();
|
|
210
|
+
}
|
|
122
211
|
} catch {
|
|
123
212
|
return [];
|
|
124
213
|
}
|
|
125
214
|
}
|
|
126
215
|
|
|
216
|
+
function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
|
|
217
|
+
return tailJsonlLines(eventsPath, limit, (line) => {
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(line) as unknown;
|
|
220
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
|
|
221
|
+
} catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function safeRecentEventsAsync(eventsPath: string, limit: number): Promise<TeamEvent[]> {
|
|
228
|
+
return tailJsonlLinesAsync(eventsPath, limit, (line) => {
|
|
229
|
+
try {
|
|
230
|
+
const parsed = JSON.parse(line) as unknown;
|
|
231
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
|
|
232
|
+
} catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
127
238
|
function tailLines(filePath: string, limit: number): string[] {
|
|
128
239
|
if (limit <= 0) return [];
|
|
129
240
|
try {
|
|
@@ -142,6 +253,24 @@ function tailLines(filePath: string, limit: number): string[] {
|
|
|
142
253
|
}
|
|
143
254
|
}
|
|
144
255
|
|
|
256
|
+
async function tailLinesAsync(filePath: string, limit: number): Promise<string[]> {
|
|
257
|
+
if (limit <= 0) return [];
|
|
258
|
+
try {
|
|
259
|
+
const stat = await fs.promises.stat(filePath);
|
|
260
|
+
const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
|
|
261
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
262
|
+
try {
|
|
263
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
264
|
+
await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
|
|
265
|
+
return buffer.toString("utf-8").split(/\r?\n/).filter(Boolean).slice(-limit);
|
|
266
|
+
} finally {
|
|
267
|
+
await handle.close();
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
145
274
|
function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): string[] {
|
|
146
275
|
const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
|
|
147
276
|
const fromFiles = agents.flatMap((agent) => {
|
|
@@ -151,6 +280,16 @@ function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[],
|
|
|
151
280
|
return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
|
|
152
281
|
}
|
|
153
282
|
|
|
283
|
+
async function recentOutputLinesAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): Promise<string[]> {
|
|
284
|
+
const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
|
|
285
|
+
const fromFilesArrays = await Promise.all(agents.map((agent) => {
|
|
286
|
+
const outputPath = safeAgentOutputPath(manifest, agent);
|
|
287
|
+
return outputPath ? tailLinesAsync(outputPath, limit) : Promise.resolve([]);
|
|
288
|
+
}));
|
|
289
|
+
const fromFiles = fromFilesArrays.flat();
|
|
290
|
+
return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
|
|
291
|
+
}
|
|
292
|
+
|
|
154
293
|
function progressFromTasks(tasks: TeamTaskState[]): RunUiProgress {
|
|
155
294
|
return {
|
|
156
295
|
total: tasks.length,
|
|
@@ -195,41 +334,79 @@ function readDeliveryMessages(filePath: string): Record<string, MailboxMessageSt
|
|
|
195
334
|
}
|
|
196
335
|
}
|
|
197
336
|
|
|
198
|
-
function
|
|
337
|
+
async function readDeliveryMessagesAsync(filePath: string): Promise<Record<string, MailboxMessageStatus>> {
|
|
199
338
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} catch {
|
|
209
|
-
return [];
|
|
210
|
-
}
|
|
211
|
-
});
|
|
339
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
340
|
+
const parsed = JSON.parse(content) as unknown;
|
|
341
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
342
|
+
const messages = (parsed as { messages?: unknown }).messages;
|
|
343
|
+
if (!messages || typeof messages !== "object" || Array.isArray(messages)) return {};
|
|
344
|
+
const output: Record<string, MailboxMessageStatus> = {};
|
|
345
|
+
for (const [id, status] of Object.entries(messages)) if (isMailboxStatus(status)) output[id] = status;
|
|
346
|
+
return output;
|
|
212
347
|
} catch {
|
|
213
|
-
return
|
|
348
|
+
return {};
|
|
214
349
|
}
|
|
215
350
|
}
|
|
216
351
|
|
|
352
|
+
function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
|
|
353
|
+
return tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(line) as unknown;
|
|
356
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
357
|
+
const message = parsed as { id?: unknown; data?: unknown };
|
|
358
|
+
const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
|
|
359
|
+
if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
|
|
360
|
+
return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
|
|
361
|
+
} catch {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function readGroupJoinMailboxAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<RunUiGroupJoin[]> {
|
|
368
|
+
return tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
|
|
369
|
+
try {
|
|
370
|
+
const parsed = JSON.parse(line) as unknown;
|
|
371
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
372
|
+
const message = parsed as { id?: unknown; data?: unknown };
|
|
373
|
+
const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
|
|
374
|
+
if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
|
|
375
|
+
return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
|
|
376
|
+
} catch {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
217
382
|
function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): number {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
383
|
+
const items = tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(line) as unknown;
|
|
386
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
387
|
+
const message = parsed as { id?: unknown; status?: unknown };
|
|
388
|
+
if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
|
|
389
|
+
return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
|
|
390
|
+
} catch {
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
}) as number[];
|
|
394
|
+
return items.reduce((sum, val) => sum + val, 0);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function readMailboxCountsAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<number> {
|
|
398
|
+
const items = await tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
|
|
399
|
+
try {
|
|
400
|
+
const parsed = JSON.parse(line) as unknown;
|
|
401
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
|
|
402
|
+
const message = parsed as { id?: unknown; status?: unknown };
|
|
403
|
+
if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
|
|
404
|
+
return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
|
|
405
|
+
} catch {
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
}) as number[];
|
|
409
|
+
return items.reduce((sum, val) => sum + val, 0);
|
|
233
410
|
}
|
|
234
411
|
|
|
235
412
|
function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
|
|
@@ -238,6 +415,12 @@ function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
|
|
|
238
415
|
return readGroupJoinMailbox(path.join(root, "outbox.jsonl"), delivery).slice(-5);
|
|
239
416
|
}
|
|
240
417
|
|
|
418
|
+
async function groupJoinsFromAsync(manifest: TeamRunManifest): Promise<RunUiGroupJoin[]> {
|
|
419
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
420
|
+
const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
|
|
421
|
+
return (await readGroupJoinMailboxAsync(path.join(root, "outbox.jsonl"), delivery)).slice(-5);
|
|
422
|
+
}
|
|
423
|
+
|
|
241
424
|
function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunUiMailbox {
|
|
242
425
|
const root = path.join(manifest.stateRoot, "mailbox");
|
|
243
426
|
const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
|
|
@@ -257,9 +440,29 @@ function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunU
|
|
|
257
440
|
return { inboxUnread, outboxPending, needsAttention: inboxUnread + attentionAgents };
|
|
258
441
|
}
|
|
259
442
|
|
|
443
|
+
async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<RunUiMailbox> {
|
|
444
|
+
const root = path.join(manifest.stateRoot, "mailbox");
|
|
445
|
+
const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
|
|
446
|
+
let inboxUnread = await readMailboxCountsAsync(path.join(root, "inbox.jsonl"), delivery);
|
|
447
|
+
let outboxPending = await readMailboxCountsAsync(path.join(root, "outbox.jsonl"), delivery);
|
|
448
|
+
const tasksRoot = path.join(root, "tasks");
|
|
449
|
+
try {
|
|
450
|
+
for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
|
|
451
|
+
if (!entry.isDirectory()) continue;
|
|
452
|
+
inboxUnread += await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
|
|
453
|
+
outboxPending += await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
// No task mailboxes yet.
|
|
457
|
+
}
|
|
458
|
+
const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
|
|
459
|
+
return { inboxUnread, outboxPending, needsAttention: inboxUnread + attentionAgents };
|
|
460
|
+
}
|
|
461
|
+
|
|
260
462
|
function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, stamps: SnapshotStamps): string {
|
|
261
|
-
|
|
262
|
-
|
|
463
|
+
try {
|
|
464
|
+
const digest = createHash("sha256");
|
|
465
|
+
digest.update(JSON.stringify({
|
|
263
466
|
run: [input.manifest.runId, input.manifest.status, input.manifest.updatedAt, input.manifest.artifacts.length],
|
|
264
467
|
tasks: input.tasks.map((task) => [task.id, task.status, task.startedAt, task.finishedAt, task.agentProgress, task.usage]),
|
|
265
468
|
agents: input.agents.map((agent) => [agent.id, agent.status, agent.startedAt, agent.completedAt, agent.toolUses, agent.progress, agent.usage, agent.model]),
|
|
@@ -272,6 +475,10 @@ function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, sta
|
|
|
272
475
|
stamps,
|
|
273
476
|
}));
|
|
274
477
|
return digest.digest("hex").slice(0, 16);
|
|
478
|
+
} catch {
|
|
479
|
+
// Circular reference or non-serializable data — fall back to timestamp.
|
|
480
|
+
return String(Date.now());
|
|
481
|
+
}
|
|
275
482
|
}
|
|
276
483
|
|
|
277
484
|
function stampsFor(manifest: TeamRunManifest, agents: CrewAgentRecord[]): SnapshotStamps {
|
|
@@ -285,6 +492,18 @@ function stampsFor(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Snapsh
|
|
|
285
492
|
};
|
|
286
493
|
}
|
|
287
494
|
|
|
495
|
+
async function stampsForAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<SnapshotStamps> {
|
|
496
|
+
const [manifestStamp, tasksStamp, agentsStamp, eventsStamp, mailbox, output] = await Promise.all([
|
|
497
|
+
stampFileAsync(path.join(manifest.stateRoot, "manifest.json")),
|
|
498
|
+
stampFileAsync(manifest.tasksPath),
|
|
499
|
+
stampFileAsync(agentsPath(manifest)),
|
|
500
|
+
stampFileAsync(manifest.eventsPath),
|
|
501
|
+
mailboxStampAsync(manifest),
|
|
502
|
+
outputStampAsync(manifest, agents),
|
|
503
|
+
]);
|
|
504
|
+
return { manifest: manifestStamp, tasks: tasksStamp, agents: agentsStamp, events: eventsStamp, mailbox, output };
|
|
505
|
+
}
|
|
506
|
+
|
|
288
507
|
export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOptions = {}): RunSnapshotCache {
|
|
289
508
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
290
509
|
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
@@ -351,6 +570,51 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
351
570
|
return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
|
|
352
571
|
}
|
|
353
572
|
|
|
573
|
+
async function buildAsync(runId: string, previous?: CacheEntry): Promise<CacheEntry> {
|
|
574
|
+
let loaded: ReturnType<typeof loadRunManifestById>;
|
|
575
|
+
try {
|
|
576
|
+
loaded = loadRunManifestById(cwd, runId);
|
|
577
|
+
} catch {
|
|
578
|
+
if (previous) return previous;
|
|
579
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
580
|
+
}
|
|
581
|
+
if (!loaded) {
|
|
582
|
+
if (previous) return previous;
|
|
583
|
+
throw new Error(`Run '${runId}' not found.`);
|
|
584
|
+
}
|
|
585
|
+
let tasks: TeamTaskState[];
|
|
586
|
+
let agents: CrewAgentRecord[];
|
|
587
|
+
try {
|
|
588
|
+
tasks = await readTasksAsync(loaded.manifest.tasksPath);
|
|
589
|
+
agents = readCrewAgents(loaded.manifest);
|
|
590
|
+
} catch {
|
|
591
|
+
if (previous) return previous;
|
|
592
|
+
throw new Error(`Run '${runId}' could not be parsed.`);
|
|
593
|
+
}
|
|
594
|
+
const [mailbox, groupJoins, recentEvents, recentOutput] = await Promise.all([
|
|
595
|
+
mailboxFromAsync(loaded.manifest, agents),
|
|
596
|
+
groupJoinsFromAsync(loaded.manifest),
|
|
597
|
+
safeRecentEventsAsync(loaded.manifest.eventsPath, recentEventsLimit),
|
|
598
|
+
recentOutputLinesAsync(loaded.manifest, agents, recentOutputLimit),
|
|
599
|
+
]);
|
|
600
|
+
const base = {
|
|
601
|
+
runId: loaded.manifest.runId,
|
|
602
|
+
cwd: loaded.manifest.cwd,
|
|
603
|
+
manifest: loaded.manifest,
|
|
604
|
+
tasks,
|
|
605
|
+
agents,
|
|
606
|
+
progress: progressFromTasks(tasks),
|
|
607
|
+
usage: usageFrom(tasks, agents),
|
|
608
|
+
mailbox,
|
|
609
|
+
groupJoins,
|
|
610
|
+
recentEvents,
|
|
611
|
+
recentOutputLines: recentOutput,
|
|
612
|
+
};
|
|
613
|
+
const stamps = await stampsForAsync(loaded.manifest, agents);
|
|
614
|
+
const snapshot: RunUiSnapshot = { ...base, fetchedAt: Date.now(), signature: signatureFor(base, stamps) };
|
|
615
|
+
return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
|
|
616
|
+
}
|
|
617
|
+
|
|
354
618
|
function currentStamps(previous: CacheEntry): SnapshotStamps {
|
|
355
619
|
const manifest = previous.snapshot.manifest;
|
|
356
620
|
return {
|
|
@@ -363,6 +627,40 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
363
627
|
};
|
|
364
628
|
}
|
|
365
629
|
|
|
630
|
+
async function currentStampsAsync(previous: CacheEntry): Promise<SnapshotStamps> {
|
|
631
|
+
return stampsForAsync(previous.snapshot.manifest, previous.snapshot.agents);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function preloadStale(runId: string): Promise<RunUiSnapshot | undefined> {
|
|
635
|
+
const previous = entries.get(runId);
|
|
636
|
+
const now = Date.now();
|
|
637
|
+
// Fresh enough? Return immediately
|
|
638
|
+
if (previous && now - previous.loadedAtMs < ttlMs) {
|
|
639
|
+
return touch(runId, previous);
|
|
640
|
+
}
|
|
641
|
+
// Check stamps async
|
|
642
|
+
if (previous) {
|
|
643
|
+
const stamps = await currentStampsAsync(previous);
|
|
644
|
+
if (sameStamps(stamps, previous.stamps)) {
|
|
645
|
+
previous.loadedAtMs = now;
|
|
646
|
+
return touch(runId, previous);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// Full async build
|
|
650
|
+
const entry = await buildAsync(runId, previous);
|
|
651
|
+
entries.set(runId, entry);
|
|
652
|
+
evictIfNeeded();
|
|
653
|
+
return entry.snapshot;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function preloadAllStale(runIds: string[]): Promise<void> {
|
|
657
|
+
const batchSize = 4;
|
|
658
|
+
for (let i = 0; i < runIds.length; i += batchSize) {
|
|
659
|
+
const batch = runIds.slice(i, i + batchSize);
|
|
660
|
+
await Promise.all(batch.map((id) => preloadStale(id)));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
366
664
|
return {
|
|
367
665
|
get(runId: string): RunUiSnapshot | undefined {
|
|
368
666
|
const entry = entries.get(runId);
|
|
@@ -384,6 +682,8 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
384
682
|
if (sameStamps(stamps, previous.stamps)) return touch(runId, previous);
|
|
385
683
|
return this.refresh(runId);
|
|
386
684
|
},
|
|
685
|
+
preloadStale,
|
|
686
|
+
preloadAllStale,
|
|
387
687
|
invalidate(runId?: string): void {
|
|
388
688
|
if (runId) entries.delete(runId);
|
|
389
689
|
else entries.clear();
|
package/src/ui/spinner.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
2
|
-
export const SUBAGENT_SPINNER_FRAME_MS = 160;
|
|
3
|
-
|
|
4
|
-
export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
|
|
5
|
-
return Math.floor(now / Math.max(1, frameMs));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function hashKey(key: string): number {
|
|
9
|
-
let hash = 0;
|
|
10
|
-
for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
|
|
11
|
-
return hash;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function spinnerFrame(key = "", now = Date.now()): string {
|
|
15
|
-
const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
|
|
16
|
-
return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
|
|
17
|
-
}
|
|
1
|
+
export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
2
|
+
export const SUBAGENT_SPINNER_FRAME_MS = 160;
|
|
3
|
+
|
|
4
|
+
export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
|
|
5
|
+
return Math.floor(now / Math.max(1, frameMs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function hashKey(key: string): number {
|
|
9
|
+
let hash = 0;
|
|
10
|
+
for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
|
|
11
|
+
return hash;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function spinnerFrame(key = "", now = Date.now()): string {
|
|
15
|
+
const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
|
|
16
|
+
return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
|
|
17
|
+
}
|
package/src/ui/status-colors.ts
CHANGED
|
@@ -1,54 +1,58 @@
|
|
|
1
|
-
import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
|
|
2
|
-
|
|
3
|
-
export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
|
|
4
|
-
|
|
5
|
-
export function colorForStatus(status: RunStatus): CrewThemeColor {
|
|
6
|
-
switch (status) {
|
|
7
|
-
case "running":
|
|
8
|
-
return "accent";
|
|
9
|
-
case "
|
|
10
|
-
return "
|
|
11
|
-
case "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
case "
|
|
15
|
-
|
|
16
|
-
case "
|
|
17
|
-
|
|
18
|
-
case "
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
case "
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
case "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
case "
|
|
36
|
-
return
|
|
37
|
-
case "
|
|
38
|
-
return
|
|
39
|
-
case "
|
|
40
|
-
return "
|
|
41
|
-
|
|
42
|
-
return "
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
54
|
-
}
|
|
1
|
+
import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
|
|
2
|
+
|
|
3
|
+
export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
|
|
4
|
+
|
|
5
|
+
export function colorForStatus(status: RunStatus): CrewThemeColor {
|
|
6
|
+
switch (status) {
|
|
7
|
+
case "running":
|
|
8
|
+
return "accent";
|
|
9
|
+
case "waiting":
|
|
10
|
+
return "muted";
|
|
11
|
+
case "completed":
|
|
12
|
+
return "success";
|
|
13
|
+
case "failed":
|
|
14
|
+
case "stale":
|
|
15
|
+
return "error";
|
|
16
|
+
case "cancelled":
|
|
17
|
+
case "blocked":
|
|
18
|
+
case "stopped":
|
|
19
|
+
return "warning";
|
|
20
|
+
case "queued":
|
|
21
|
+
default:
|
|
22
|
+
return "dim";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
|
|
27
|
+
const glyph = options?.runningGlyph ?? "▶";
|
|
28
|
+
switch (status) {
|
|
29
|
+
case "completed":
|
|
30
|
+
return "✓";
|
|
31
|
+
case "failed":
|
|
32
|
+
case "stale":
|
|
33
|
+
return "✗";
|
|
34
|
+
case "cancelled":
|
|
35
|
+
case "stopped":
|
|
36
|
+
return "■";
|
|
37
|
+
case "running":
|
|
38
|
+
return glyph;
|
|
39
|
+
case "waiting":
|
|
40
|
+
return "⏳";
|
|
41
|
+
case "queued":
|
|
42
|
+
return "◦";
|
|
43
|
+
case "blocked":
|
|
44
|
+
return "⏸";
|
|
45
|
+
default:
|
|
46
|
+
return "·";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function colorForActivity(activityState: string | undefined): CrewThemeColor {
|
|
51
|
+
if (activityState === "needs_attention") return "warning";
|
|
52
|
+
if (activityState === "stale") return "error";
|
|
53
|
+
return "dim";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
|
|
57
|
+
return theme.fg(colorForStatus(status), text);
|
|
58
|
+
}
|