pi-crew 0.1.13 → 0.1.15
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 +6 -0
- package/package.json +1 -1
- package/src/extension/team-tool.ts +2 -2
- package/src/runtime/crew-agent-runtime.ts +1 -1
- package/src/runtime/model-fallback.ts +143 -0
- package/src/runtime/task-runner.ts +4 -3
- package/src/runtime/team-runner.ts +2 -1
- package/src/ui/crew-widget.ts +50 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.15
|
|
4
|
+
|
|
5
|
+
- Child-process model selection now uses Pi-configured/available models and auto-discovers provider/model entries from Pi settings/models config.
|
|
6
|
+
- Added configured-model fallback chains for worker runs instead of forcing builtin provider hints.
|
|
7
|
+
- Fixed skipped task agent records so they no longer appear queued.
|
|
8
|
+
|
|
3
9
|
## 0.1.0
|
|
4
10
|
|
|
5
11
|
- Initial scaffold for `pi-crew`.
|
package/package.json
CHANGED
|
@@ -307,7 +307,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
307
307
|
const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
|
|
308
308
|
const executeWorkers = runtime.kind === "child-process";
|
|
309
309
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
310
|
-
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
|
|
310
|
+
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
|
|
311
311
|
const text = [
|
|
312
312
|
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
313
313
|
`Team: ${team.name}`,
|
|
@@ -430,7 +430,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
430
430
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
431
431
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
432
432
|
const executeWorkers = runtime.kind === "child-process";
|
|
433
|
-
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
|
|
433
|
+
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
|
|
434
434
|
return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
435
435
|
});
|
|
436
436
|
}
|
|
@@ -48,7 +48,7 @@ export interface CrewAgentRecord {
|
|
|
48
48
|
export function taskStatusToAgentStatus(status: TeamTaskStatus): CrewAgentStatus {
|
|
49
49
|
if (status === "completed") return "completed";
|
|
50
50
|
if (status === "failed") return "failed";
|
|
51
|
-
if (status === "cancelled") return "cancelled";
|
|
51
|
+
if (status === "cancelled" || status === "skipped") return "cancelled";
|
|
52
52
|
if (status === "running") return "running";
|
|
53
53
|
return "queued";
|
|
54
54
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
1
5
|
export interface AvailableModelInfo {
|
|
2
6
|
provider: string;
|
|
3
7
|
id: string;
|
|
@@ -11,6 +15,114 @@ export interface ModelAttemptSummary {
|
|
|
11
15
|
error?: string;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
export interface ModelLike {
|
|
19
|
+
provider?: unknown;
|
|
20
|
+
id?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModelRegistryLike {
|
|
24
|
+
getAvailable?: () => unknown[];
|
|
25
|
+
getAll?: () => unknown[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PiSettingsLike {
|
|
29
|
+
defaultProvider?: unknown;
|
|
30
|
+
defaultModel?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PiModelsJsonLike {
|
|
34
|
+
providers?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PiProviderConfigLike {
|
|
38
|
+
models?: unknown;
|
|
39
|
+
modelOverrides?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function modelInfoFromUnknown(value: unknown): AvailableModelInfo | undefined {
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
44
|
+
const record = value as ModelLike;
|
|
45
|
+
if (typeof record.provider !== "string" || typeof record.id !== "string") return undefined;
|
|
46
|
+
return { provider: record.provider, id: record.id, fullId: `${record.provider}/${record.id}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function availableModelInfosFromRegistry(registry: unknown): AvailableModelInfo[] | undefined {
|
|
50
|
+
if (!registry || typeof registry !== "object" || Array.isArray(registry)) return undefined;
|
|
51
|
+
const candidate = registry as ModelRegistryLike;
|
|
52
|
+
const raw = typeof candidate.getAvailable === "function" ? candidate.getAvailable() : typeof candidate.getAll === "function" ? candidate.getAll() : undefined;
|
|
53
|
+
if (!Array.isArray(raw)) return undefined;
|
|
54
|
+
return raw.map(modelInfoFromUnknown).filter((entry): entry is AvailableModelInfo => entry !== undefined);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function modelStringFromUnknown(model: unknown): string | undefined {
|
|
58
|
+
return modelInfoFromUnknown(model)?.fullId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function uniqueModelInfos(models: AvailableModelInfo[]): AvailableModelInfo[] {
|
|
62
|
+
const seen = new Set<string>();
|
|
63
|
+
return models.filter((model) => {
|
|
64
|
+
if (seen.has(model.fullId)) return false;
|
|
65
|
+
seen.add(model.fullId);
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readJsonObject(filePath: string): Record<string, unknown> | undefined {
|
|
71
|
+
try {
|
|
72
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
73
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
74
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function piAgentDir(): string {
|
|
81
|
+
const envDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
82
|
+
if (envDir) {
|
|
83
|
+
if (envDir === "~") return os.homedir();
|
|
84
|
+
if (envDir.startsWith("~/")) return path.join(os.homedir(), envDir.slice(2));
|
|
85
|
+
return envDir;
|
|
86
|
+
}
|
|
87
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function settingsModelInfo(settings: PiSettingsLike | undefined): AvailableModelInfo | undefined {
|
|
91
|
+
if (typeof settings?.defaultProvider !== "string" || typeof settings.defaultModel !== "string") return undefined;
|
|
92
|
+
return { provider: settings.defaultProvider, id: settings.defaultModel, fullId: `${settings.defaultProvider}/${settings.defaultModel}` };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function modelsJsonInfos(modelsJson: PiModelsJsonLike | undefined): AvailableModelInfo[] {
|
|
96
|
+
if (!modelsJson?.providers || typeof modelsJson.providers !== "object" || Array.isArray(modelsJson.providers)) return [];
|
|
97
|
+
const infos: AvailableModelInfo[] = [];
|
|
98
|
+
for (const [provider, rawConfig] of Object.entries(modelsJson.providers as Record<string, unknown>)) {
|
|
99
|
+
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) continue;
|
|
100
|
+
const config = rawConfig as PiProviderConfigLike;
|
|
101
|
+
if (Array.isArray(config.models)) {
|
|
102
|
+
for (const rawModel of config.models) {
|
|
103
|
+
if (!rawModel || typeof rawModel !== "object" || Array.isArray(rawModel)) continue;
|
|
104
|
+
const id = (rawModel as { id?: unknown }).id;
|
|
105
|
+
if (typeof id === "string") infos.push({ provider, id, fullId: `${provider}/${id}` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (config.modelOverrides && typeof config.modelOverrides === "object" && !Array.isArray(config.modelOverrides)) {
|
|
109
|
+
for (const id of Object.keys(config.modelOverrides)) infos.push({ provider, id, fullId: `${provider}/${id}` });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return infos;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function configuredModelInfosFromPiConfig(cwd?: string): AvailableModelInfo[] {
|
|
116
|
+
const agentDir = piAgentDir();
|
|
117
|
+
const globalSettings = readJsonObject(path.join(agentDir, "settings.json")) as PiSettingsLike | undefined;
|
|
118
|
+
const projectSettings = cwd ? readJsonObject(path.join(cwd, ".pi", "settings.json")) as PiSettingsLike | undefined : undefined;
|
|
119
|
+
const effectiveSettings = { ...(globalSettings ?? {}), ...(projectSettings ?? {}) };
|
|
120
|
+
return uniqueModelInfos([
|
|
121
|
+
...modelsJsonInfos(readJsonObject(path.join(agentDir, "models.json")) as PiModelsJsonLike | undefined),
|
|
122
|
+
...(settingsModelInfo(effectiveSettings) ? [settingsModelInfo(effectiveSettings)!] : []),
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
|
|
14
126
|
export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
15
127
|
const colonIdx = model.lastIndexOf(":");
|
|
16
128
|
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
@@ -99,3 +211,34 @@ export function buildModelCandidates(
|
|
|
99
211
|
}
|
|
100
212
|
return candidates;
|
|
101
213
|
}
|
|
214
|
+
|
|
215
|
+
function isAvailableModel(model: string, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
216
|
+
if (!availableModels || availableModels.length === 0) return true;
|
|
217
|
+
const { baseModel } = splitThinkingSuffix(model);
|
|
218
|
+
if (baseModel.includes("/")) return availableModels.some((entry) => entry.fullId === baseModel);
|
|
219
|
+
return availableModels.some((entry) => entry.id === baseModel);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildConfiguredModelCandidates(input: {
|
|
223
|
+
overrideModel?: string;
|
|
224
|
+
stepModel?: string;
|
|
225
|
+
agentModel?: string;
|
|
226
|
+
fallbackModels?: string[];
|
|
227
|
+
parentModel?: unknown;
|
|
228
|
+
modelRegistry?: unknown;
|
|
229
|
+
cwd?: string;
|
|
230
|
+
}): string[] {
|
|
231
|
+
const registryModels = availableModelInfosFromRegistry(input.modelRegistry);
|
|
232
|
+
const configModels = configuredModelInfosFromPiConfig(input.cwd);
|
|
233
|
+
const availableModels = registryModels && registryModels.length > 0 ? registryModels : configModels.length > 0 ? configModels : registryModels;
|
|
234
|
+
const parentModel = modelStringFromUnknown(input.parentModel);
|
|
235
|
+
const preferredProvider = parentModel?.split("/")[0] ?? availableModels?.[0]?.provider;
|
|
236
|
+
if (availableModels && availableModels.length === 0) return [];
|
|
237
|
+
const rawModels = availableModels
|
|
238
|
+
? [input.overrideModel, input.stepModel, input.agentModel, ...(input.fallbackModels ?? []), parentModel, ...availableModels.map((model) => model.fullId)]
|
|
239
|
+
: [input.overrideModel, parentModel];
|
|
240
|
+
const configuredModels = rawModels
|
|
241
|
+
.filter((model): model is string => Boolean(model?.trim()))
|
|
242
|
+
.filter((model) => isAvailableModel(model.trim(), availableModels));
|
|
243
|
+
return buildModelCandidates(configuredModels[0], configuredModels.slice(1), availableModels, preferredProvider);
|
|
244
|
+
}
|
|
@@ -9,7 +9,7 @@ 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
11
|
import { captureWorktreeDiff, captureWorktreeDiffStat, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { buildConfiguredModelCandidates, 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";
|
|
15
15
|
import { buildTaskPacket, renderTaskPacket } from "./task-packet.ts";
|
|
@@ -36,6 +36,7 @@ export interface TaskRunnerInput {
|
|
|
36
36
|
parentContext?: string;
|
|
37
37
|
parentModel?: unknown;
|
|
38
38
|
modelRegistry?: unknown;
|
|
39
|
+
modelOverride?: string;
|
|
39
40
|
limits?: CrewLimitsConfig;
|
|
40
41
|
dependencyContextText?: string;
|
|
41
42
|
}
|
|
@@ -267,8 +268,8 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
267
268
|
producer: task.id,
|
|
268
269
|
});
|
|
269
270
|
if (runtimeKind === "child-process") {
|
|
270
|
-
const candidates =
|
|
271
|
-
const attemptModels = candidates.length > 0 ? candidates : [
|
|
271
|
+
const candidates = buildConfiguredModelCandidates({ overrideModel: input.modelOverride, stepModel: input.step.model, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: manifest.cwd });
|
|
272
|
+
const attemptModels = candidates.length > 0 ? candidates : [undefined];
|
|
272
273
|
const logs: string[] = [];
|
|
273
274
|
let finalStdout = "";
|
|
274
275
|
let finalStderr = "";
|
|
@@ -30,6 +30,7 @@ export interface ExecuteTeamRunInput {
|
|
|
30
30
|
parentContext?: string;
|
|
31
31
|
parentModel?: unknown;
|
|
32
32
|
modelRegistry?: unknown;
|
|
33
|
+
modelOverride?: string;
|
|
33
34
|
signal?: AbortSignal;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -179,7 +180,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
179
180
|
const results = await Promise.all(readyBatch.map((task) => {
|
|
180
181
|
const step = findStep(input.workflow, task);
|
|
181
182
|
const agent = findAgent(input.agents, task);
|
|
182
|
-
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 });
|
|
183
|
+
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, modelOverride: input.modelOverride, limits: input.limits });
|
|
183
184
|
}));
|
|
184
185
|
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
185
186
|
tasks = mergeTaskUpdates(tasks, results);
|
package/src/ui/crew-widget.ts
CHANGED
|
@@ -17,6 +17,9 @@ const TOOL_LABELS: Record<string, string> = {
|
|
|
17
17
|
ls: "listing",
|
|
18
18
|
};
|
|
19
19
|
const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
20
|
+
const LEGACY_WIDGET_KEY = "pi-crew";
|
|
21
|
+
const WIDGET_KEY = "pi-crew-active";
|
|
22
|
+
const STATUS_KEY = "pi-crew";
|
|
20
23
|
|
|
21
24
|
type ThemeLike = { fg?: (color: string, text: string) => string; bold?: (text: string) => string };
|
|
22
25
|
type WidgetComponent = { render(width: number): string[]; invalidate(): void };
|
|
@@ -115,7 +118,18 @@ function statusSummary(runs: WidgetRun[]): string {
|
|
|
115
118
|
const parts = [`${runningAgents} running`];
|
|
116
119
|
if (queuedAgents) parts.push(`${queuedAgents} queued`);
|
|
117
120
|
if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
|
|
118
|
-
return
|
|
121
|
+
return `Crew: ${parts.join(", ")}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function widgetHeader(runs: WidgetRun[], runningGlyph: string): string {
|
|
125
|
+
const agents = runs.flatMap((item) => item.agents);
|
|
126
|
+
const runningAgents = agents.filter((agent) => agent.status === "running").length;
|
|
127
|
+
const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
|
|
128
|
+
const completedAgents = agents.filter((agent) => agent.status === "completed").length;
|
|
129
|
+
const parts = [`${runningAgents} running`];
|
|
130
|
+
if (queuedAgents) parts.push(`${queuedAgents} queued`);
|
|
131
|
+
if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
|
|
132
|
+
return `${runningGlyph} Crew agents · ${parts.join(" · ")} · /team-dashboard`;
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
function shortRunLabel(run: TeamRunManifest): string {
|
|
@@ -126,21 +140,33 @@ export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8): stri
|
|
|
126
140
|
const runs = activeWidgetRuns(cwd);
|
|
127
141
|
if (!runs.length) return [];
|
|
128
142
|
const runningGlyph = SPINNER[frame % SPINNER.length] ?? "⠋";
|
|
129
|
-
const lines: string[] = [
|
|
143
|
+
const lines: string[] = [widgetHeader(runs, runningGlyph)];
|
|
130
144
|
for (const { run, agents } of runs) {
|
|
131
145
|
const activeAgents = agents.filter((item) => item.status === "running" || item.status === "queued");
|
|
132
146
|
const completed = agents.filter((agent) => agent.status === "completed").length;
|
|
133
|
-
|
|
134
|
-
|
|
147
|
+
const runGlyph = glyph(run.status, runningGlyph);
|
|
148
|
+
lines.push(`├─ ${runGlyph} ${shortRunLabel(run)} · ${completed}/${agents.length} done · ${run.runId.slice(-8)}`);
|
|
149
|
+
const visibleAgents = activeAgents.slice(0, 3);
|
|
150
|
+
for (const [index, agent] of visibleAgents.entries()) {
|
|
151
|
+
const last = index === visibleAgents.length - 1 && activeAgents.length <= 3;
|
|
152
|
+
const branch = last ? "└─" : "├─";
|
|
135
153
|
const stats = agentStats(agent);
|
|
136
|
-
lines.push(
|
|
154
|
+
lines.push(`│ ${branch} ${glyph(agent.status, runningGlyph)} ${agent.agent} · ${agent.role}`);
|
|
155
|
+
lines.push(`│ ⎿ ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
|
|
137
156
|
}
|
|
138
|
-
if (activeAgents.length > 3) lines.push(
|
|
157
|
+
if (activeAgents.length > 3) lines.push(`│ └─ … +${activeAgents.length - 3} more agents`);
|
|
139
158
|
if (lines.length >= maxLines) break;
|
|
140
159
|
}
|
|
141
160
|
return lines.slice(0, maxLines);
|
|
142
161
|
}
|
|
143
162
|
|
|
163
|
+
function colorWidgetLine(line: string, index: number, theme: ThemeLike): string {
|
|
164
|
+
const fg = theme.fg?.bind(theme) ?? ((_color: string, text: string) => text);
|
|
165
|
+
const bold = theme.bold?.bind(theme) ?? ((text: string) => text);
|
|
166
|
+
if (index === 0) return line.replace("Crew agents", bold(fg("accent", "Crew agents")));
|
|
167
|
+
return line.replace(/([⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏▶◦✓✗■·])/, (icon: string) => fg(icon === "✓" ? "success" : icon === "✗" ? "error" : icon === "◦" ? "dim" : "accent", icon));
|
|
168
|
+
}
|
|
169
|
+
|
|
144
170
|
class CrewWidgetComponent implements WidgetComponent {
|
|
145
171
|
private cwd: string;
|
|
146
172
|
private frame: number;
|
|
@@ -155,35 +181,39 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
155
181
|
}
|
|
156
182
|
invalidate(): void {}
|
|
157
183
|
render(width: number): string[] {
|
|
158
|
-
|
|
159
|
-
const bold = this.theme.bold?.bind(this.theme) ?? ((text: string) => text);
|
|
160
|
-
return buildCrewWidgetLines(this.cwd, this.frame, this.maxLines).map((line, index) => {
|
|
161
|
-
const colored = index === 0
|
|
162
|
-
? line.replace("⚙ pi-crew", `${fg("accent", "⚙")} ${bold("pi-crew")}`)
|
|
163
|
-
: line.replace(/^\s*([⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏▶◦✓✗■·])/, (match, icon: string) => match.replace(icon, fg(icon === "✓" ? "success" : icon === "✗" ? "error" : icon === "◦" ? "dim" : "accent", icon)));
|
|
164
|
-
return truncate(colored, width);
|
|
165
|
-
});
|
|
184
|
+
return buildCrewWidgetLines(this.cwd, this.frame, this.maxLines).map((line, index) => truncate(colorWidgetLine(line, index, this.theme), width));
|
|
166
185
|
}
|
|
167
186
|
}
|
|
168
187
|
|
|
188
|
+
function requestRender(ctx: Pick<ExtensionContext, "ui">): void {
|
|
189
|
+
(ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
190
|
+
}
|
|
191
|
+
|
|
169
192
|
export function updateCrewWidget(ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">, state: CrewWidgetState, config?: CrewUiConfig): void {
|
|
170
193
|
if (!ctx.hasUI) return;
|
|
171
194
|
state.frame += 1;
|
|
172
|
-
const maxLines = config?.widgetMaxLines ??
|
|
195
|
+
const maxLines = config?.widgetMaxLines ?? 10;
|
|
173
196
|
const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines);
|
|
174
|
-
|
|
197
|
+
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
198
|
+
ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(activeWidgetRuns(ctx.cwd)) : undefined);
|
|
199
|
+
ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
|
|
175
200
|
if (!lines.length) {
|
|
176
|
-
ctx.ui.setWidget(
|
|
201
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
|
|
202
|
+
requestRender(ctx);
|
|
177
203
|
return;
|
|
178
204
|
}
|
|
179
|
-
ctx.ui.setWidget(
|
|
205
|
+
ctx.ui.setWidget(WIDGET_KEY, ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(ctx.cwd, state.frame, maxLines, theme as ThemeLike)) as never, { placement });
|
|
206
|
+
requestRender(ctx);
|
|
180
207
|
}
|
|
181
208
|
|
|
182
209
|
export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
|
|
183
210
|
if (state.interval) clearInterval(state.interval);
|
|
184
211
|
state.interval = undefined;
|
|
185
212
|
if (ctx?.hasUI) {
|
|
186
|
-
|
|
187
|
-
ctx.ui.
|
|
213
|
+
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
214
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
215
|
+
ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
|
|
216
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
|
|
217
|
+
requestRender(ctx);
|
|
188
218
|
}
|
|
189
219
|
}
|