pi-crew 0.1.32 → 0.1.34
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/docs/architecture.md +3 -3
- package/docs/research-phase8-operator-experience-plan.md +819 -0
- package/docs/research-phase9-observability-reliability-plan.md +1190 -0
- package/docs/research-ui-optimization-plan.md +480 -0
- package/package.json +1 -1
- package/schema.json +14 -0
- package/src/config/config.ts +69 -0
- package/src/config/defaults.ts +7 -0
- package/src/extension/autonomous-policy.ts +56 -2
- package/src/extension/notification-router.ts +116 -0
- package/src/extension/notification-sink.ts +51 -0
- package/src/extension/register.ts +133 -35
- package/src/extension/registration/commands.ts +110 -3
- package/src/extension/registration/team-tool.ts +5 -2
- package/src/extension/registration/viewers.ts +3 -1
- package/src/extension/team-recommendation.ts +16 -8
- package/src/runtime/child-pi.ts +1 -0
- package/src/runtime/diagnostic-export.ts +107 -0
- package/src/runtime/pi-spawn.ts +4 -1
- package/src/runtime/task-packet.ts +11 -2
- package/src/runtime/task-runner/prompt-builder.ts +3 -0
- package/src/schema/config-schema.ts +11 -0
- package/src/ui/crew-widget.ts +350 -285
- package/src/ui/dashboard-panes/agents-pane.ts +25 -0
- package/src/ui/dashboard-panes/health-pane.ts +30 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +10 -0
- package/src/ui/dashboard-panes/progress-pane.ts +14 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
- package/src/ui/heartbeat-aggregator.ts +53 -0
- package/src/ui/keybinding-map.ts +92 -0
- package/src/ui/live-run-sidebar.ts +20 -8
- package/src/ui/overlays/agent-picker-overlay.ts +57 -0
- package/src/ui/overlays/confirm-overlay.ts +58 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -0
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -0
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -0
- package/src/ui/pi-ui-compat.ts +57 -0
- package/src/ui/powerbar-publisher.ts +128 -94
- package/src/ui/render-scheduler.ts +103 -0
- package/src/ui/run-action-dispatcher.ts +107 -0
- package/src/ui/run-dashboard.ts +418 -372
- package/src/ui/run-snapshot-cache.ts +359 -0
- package/src/ui/snapshot-types.ts +47 -0
- package/src/ui/transcript-cache.ts +94 -0
- package/src/ui/transcript-viewer.ts +316 -302
package/src/ui/crew-widget.ts
CHANGED
|
@@ -1,285 +1,350 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { CrewUiConfig } from "../config/config.ts";
|
|
3
|
-
import { listRecentRuns } from "../extension/run-index.ts";
|
|
4
|
-
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
|
-
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
|
-
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
7
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
8
|
-
import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
9
|
-
import { colorForStatus, iconForStatus, type RunStatus } from "./status-colors.ts";
|
|
10
|
-
import { pad, truncate } from "../utils/visual.ts";
|
|
11
|
-
import type { CrewTheme } from "./theme-adapter.ts";
|
|
12
|
-
import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
|
|
13
|
-
import { Box, Text } from "./layout-primitives.ts";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
if (
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
{
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { CrewUiConfig } from "../config/config.ts";
|
|
3
|
+
import { listRecentRuns } from "../extension/run-index.ts";
|
|
4
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
|
+
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
|
+
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
7
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
8
|
+
import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
9
|
+
import { colorForStatus, iconForStatus, type RunStatus } from "./status-colors.ts";
|
|
10
|
+
import { pad, truncate } from "../utils/visual.ts";
|
|
11
|
+
import type { CrewTheme } from "./theme-adapter.ts";
|
|
12
|
+
import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
|
|
13
|
+
import { Box, Text } from "./layout-primitives.ts";
|
|
14
|
+
import { requestRender, setExtensionWidget } from "./pi-ui-compat.ts";
|
|
15
|
+
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
16
|
+
|
|
17
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
18
|
+
const TOOL_LABELS: Record<string, string> = {
|
|
19
|
+
read: "reading",
|
|
20
|
+
bash: "running command",
|
|
21
|
+
edit: "editing",
|
|
22
|
+
write: "writing",
|
|
23
|
+
grep: "searching",
|
|
24
|
+
find: "finding files",
|
|
25
|
+
ls: "listing",
|
|
26
|
+
};
|
|
27
|
+
const LEGACY_WIDGET_KEY = "pi-crew";
|
|
28
|
+
const WIDGET_KEY = "pi-crew-active";
|
|
29
|
+
const STATUS_KEY = "pi-crew";
|
|
30
|
+
|
|
31
|
+
const MAX_LINES_DEFAULT = 10;
|
|
32
|
+
const MAX_AGENTS_DISPLAY = 3;
|
|
33
|
+
|
|
34
|
+
type WidgetComponent = { render(width: number): string[]; invalidate(): void };
|
|
35
|
+
|
|
36
|
+
interface CrewWidgetModel {
|
|
37
|
+
cwd: string;
|
|
38
|
+
frame: number;
|
|
39
|
+
maxLines: number;
|
|
40
|
+
notificationCount?: number;
|
|
41
|
+
manifestCache?: ManifestCache;
|
|
42
|
+
snapshotCache?: RunSnapshotCache;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CrewWidgetState {
|
|
46
|
+
frame: number;
|
|
47
|
+
interval?: ReturnType<typeof setInterval>;
|
|
48
|
+
lastPlacement?: string;
|
|
49
|
+
lastVisibility?: "hidden" | "visible";
|
|
50
|
+
lastKey?: string;
|
|
51
|
+
lastMaxLines?: number;
|
|
52
|
+
lastCwd?: string;
|
|
53
|
+
legacyCleared?: boolean;
|
|
54
|
+
model?: CrewWidgetModel;
|
|
55
|
+
notificationCount?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface WidgetRun {
|
|
59
|
+
run: TeamRunManifest;
|
|
60
|
+
agents: CrewAgentRecord[];
|
|
61
|
+
snapshot?: RunUiSnapshot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function elapsed(iso: string | undefined, now = Date.now()): string | undefined {
|
|
65
|
+
if (!iso) return undefined;
|
|
66
|
+
const ms = Math.max(0, now - new Date(iso).getTime());
|
|
67
|
+
if (!Number.isFinite(ms)) return undefined;
|
|
68
|
+
if (ms < 1000) return "now";
|
|
69
|
+
if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
|
|
70
|
+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
|
|
71
|
+
return `${Math.floor(ms / 3_600_000)}h`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function agentActivity(agent: CrewAgentRecord): string {
|
|
75
|
+
if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
|
|
76
|
+
const recent = agent.progress?.recentOutput?.at(-1);
|
|
77
|
+
if (recent) return recent.replace(/\s+/g, " ").trim();
|
|
78
|
+
if (agent.progress?.activityState === "needs_attention") return "needs attention";
|
|
79
|
+
if (agent.status === "queued") return "queued";
|
|
80
|
+
if (agent.status === "running") return "thinking…";
|
|
81
|
+
if (agent.status === "failed") return agent.error ?? "failed";
|
|
82
|
+
return "done";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function agentStats(agent: CrewAgentRecord): string {
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
|
|
88
|
+
if (agent.progress?.tokens) parts.push(`${agent.progress.tokens} tok`);
|
|
89
|
+
if (agent.progress?.turns) parts.push(`⟳${agent.progress.turns}`);
|
|
90
|
+
const age = elapsed(agent.completedAt ?? agent.startedAt);
|
|
91
|
+
if (age) parts.push(agent.completedAt ? age : `${age} ago`);
|
|
92
|
+
return parts.join(" · ");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
|
|
96
|
+
try {
|
|
97
|
+
return readCrewAgents(run);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache, snapshotCache?: RunSnapshotCache): WidgetRun[] {
|
|
104
|
+
const runs = manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20);
|
|
105
|
+
return runs
|
|
106
|
+
.map((run) => {
|
|
107
|
+
try {
|
|
108
|
+
const snapshot = snapshotCache?.refreshIfStale(run.runId);
|
|
109
|
+
return snapshot ? { run: snapshot.manifest, agents: snapshot.agents, snapshot } : { run, agents: agentsFor(run) };
|
|
110
|
+
} catch {
|
|
111
|
+
return { run, agents: agentsFor(run) };
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
.filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function statusSummary(runs: WidgetRun[]): string {
|
|
118
|
+
const agents = runs.flatMap((item) => item.agents);
|
|
119
|
+
const runningAgents = agents.filter((agent) => agent.status === "running").length;
|
|
120
|
+
const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
|
|
121
|
+
const completedAgents = agents.filter((agent) => agent.status === "completed").length;
|
|
122
|
+
const parts = [`${runningAgents} running`];
|
|
123
|
+
if (queuedAgents) parts.push(`${queuedAgents} queued`);
|
|
124
|
+
if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
|
|
125
|
+
return `Crew: ${parts.join(", ")}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function notificationBadge(count: number | undefined, env: NodeJS.ProcessEnv = process.env): string {
|
|
129
|
+
if (!count || count <= 0) return "";
|
|
130
|
+
const term = `${env.TERM ?? ""} ${env.WT_SESSION ?? ""} ${env.TERM_PROGRAM ?? ""}`.toLowerCase();
|
|
131
|
+
const supportsEmoji = !term.includes("dumb") && env.NO_COLOR !== "1";
|
|
132
|
+
return supportsEmoji ? ` 🔔${count}` : ` [!${count}]`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines = 20, notificationCount = 0): string {
|
|
136
|
+
const agents = runs.flatMap((item) => item.agents);
|
|
137
|
+
const runningAgents = agents.filter((agent) => agent.status === "running").length;
|
|
138
|
+
const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
|
|
139
|
+
const completedAgents = agents.filter((agent) => agent.status === "completed").length;
|
|
140
|
+
const parts = [`${runningAgents} running`];
|
|
141
|
+
if (queuedAgents) parts.push(`${queuedAgents} queued`);
|
|
142
|
+
if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
|
|
143
|
+
return `${runningGlyph} Crew agents${notificationBadge(notificationCount)} · ${parts.join(" · ")} · /team-dashboard`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function shortRunLabel(run: TeamRunManifest): string {
|
|
147
|
+
return `${run.team}/${run.workflow ?? "none"}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[], notificationCount = 0): string[] {
|
|
151
|
+
const runs = providedRuns ?? activeWidgetRuns(cwd);
|
|
152
|
+
if (!runs.length) return [];
|
|
153
|
+
const runningGlyph = SPINNER[frame % SPINNER.length] ?? SPINNER[0];
|
|
154
|
+
const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
|
|
155
|
+
for (const { run, agents } of runs) {
|
|
156
|
+
const activeAgents = agents.filter((item) => item.status === "running" || item.status === "queued");
|
|
157
|
+
const completed = agents.filter((agent) => agent.status === "completed").length;
|
|
158
|
+
const runGlyph = iconForStatus(run.status, { runningGlyph });
|
|
159
|
+
lines.push(`├─ ${runGlyph} ${shortRunLabel(run)} · ${completed}/${agents.length} done · ${run.runId.slice(-8)}`);
|
|
160
|
+
const visibleAgents = activeAgents.slice(0, MAX_AGENTS_DISPLAY);
|
|
161
|
+
for (const [index, agent] of visibleAgents.entries()) {
|
|
162
|
+
const last = index === visibleAgents.length - 1 && activeAgents.length <= MAX_AGENTS_DISPLAY;
|
|
163
|
+
const branch = last ? "└─" : "├─";
|
|
164
|
+
const agentGlyph = iconForStatus(agent.status, { runningGlyph });
|
|
165
|
+
const stats = agentStats(agent);
|
|
166
|
+
lines.push(`│ ${branch} ${agentGlyph} ${agent.agent} · ${agent.role}`);
|
|
167
|
+
lines.push(`│ ⎿ ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
|
|
168
|
+
}
|
|
169
|
+
if (activeAgents.length > MAX_AGENTS_DISPLAY) lines.push(`│ └─ … +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`);
|
|
170
|
+
if (lines.length >= maxLines) break;
|
|
171
|
+
}
|
|
172
|
+
return lines.slice(0, maxLines);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function statusGlyphColor(icon: string): Parameters<CrewTheme["fg"]>[0] {
|
|
176
|
+
const mapping: Record<string, Parameters<CrewTheme["fg"]>[0]> = {
|
|
177
|
+
"✓": "success",
|
|
178
|
+
"✗": "error",
|
|
179
|
+
"■": "warning",
|
|
180
|
+
"⏸": "warning",
|
|
181
|
+
"◦": "dim",
|
|
182
|
+
"·": "dim",
|
|
183
|
+
"▶": "accent",
|
|
184
|
+
};
|
|
185
|
+
return mapping[icon] ?? "accent";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function colorWidgetLine(line: string, index: number, theme: CrewTheme): string {
|
|
189
|
+
let result = line;
|
|
190
|
+
if (index === 0) {
|
|
191
|
+
result = result.replace("Crew agents", theme.bold(theme.fg("accent", "Crew agents")));
|
|
192
|
+
}
|
|
193
|
+
result = result.replace(/[✓✗■⏸◦·▶]/g, (icon) => theme.fg(statusGlyphColor(icon), icon));
|
|
194
|
+
if (index === 0) {
|
|
195
|
+
result = theme.fg("accent", result);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderLines(lines: string[], width: number): string[] {
|
|
201
|
+
const box = new Box(0, 0);
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
box.addChild(new Text(line));
|
|
204
|
+
}
|
|
205
|
+
return box.render(width);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
class CrewWidgetComponent implements WidgetComponent {
|
|
209
|
+
private readonly model: CrewWidgetModel;
|
|
210
|
+
private theme: CrewTheme;
|
|
211
|
+
private cacheSignature: string;
|
|
212
|
+
private cachedWidth = 0;
|
|
213
|
+
private cachedLines: string[] = [];
|
|
214
|
+
private cachedBaseLines: string[] = [];
|
|
215
|
+
private cachedTheme: CrewTheme;
|
|
216
|
+
private readonly unsubscribeTheme: () => void;
|
|
217
|
+
|
|
218
|
+
constructor(model: CrewWidgetModel, themeLike: unknown) {
|
|
219
|
+
this.model = model;
|
|
220
|
+
this.theme = asCrewTheme(themeLike);
|
|
221
|
+
this.cachedTheme = this.theme;
|
|
222
|
+
this.cacheSignature = "";
|
|
223
|
+
this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private buildSignature(runs: WidgetRun[]): string {
|
|
227
|
+
return runs
|
|
228
|
+
.map((entry) => entry.snapshot?.signature ?? `${entry.run.runId}:${entry.run.status}:${entry.run.updatedAt}:` + entry.agents.map((agent) => {
|
|
229
|
+
const recentOutput = agent.progress?.recentOutput.at(-1) ?? "";
|
|
230
|
+
const progress = [agent.progress?.currentTool ?? "", agent.progress?.toolCount ?? 0, agent.progress?.tokens ?? 0, agent.progress?.turns ?? 0, agent.progress?.lastActivityAt ?? "", recentOutput].join(":");
|
|
231
|
+
return `${agent.status}:${agent.startedAt}:${agent.completedAt ?? ""}:${agent.toolUses ?? 0}:${progress}`;
|
|
232
|
+
}).join(","))
|
|
233
|
+
.join("|");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private colorize(lines: string[], width: number): string[] {
|
|
237
|
+
return renderLines(lines.map((line, index) => colorWidgetLine(line, index, this.theme)), width);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
invalidate(): void {
|
|
241
|
+
this.cacheSignature = "";
|
|
242
|
+
this.cachedBaseLines = [];
|
|
243
|
+
this.cachedLines = [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
dispose(): void {
|
|
247
|
+
this.unsubscribeTheme();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
render(width: number): string[] {
|
|
251
|
+
const runs = activeWidgetRuns(this.model.cwd, this.model.manifestCache, this.model.snapshotCache);
|
|
252
|
+
const signature = `${this.buildSignature(runs)}:${this.model.notificationCount ?? 0}`;
|
|
253
|
+
const runningGlyph = SPINNER[this.model.frame % SPINNER.length] ?? SPINNER[0];
|
|
254
|
+
const headerGlyph = runs.length ? SPINNER[0] : " ";
|
|
255
|
+
|
|
256
|
+
if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
|
|
257
|
+
this.cachedBaseLines = buildCrewWidgetLines(this.model.cwd, 0, this.model.maxLines, runs, this.model.notificationCount ?? 0).map((line, index) => {
|
|
258
|
+
if (index === 0 && line.length > 0) return `${headerGlyph}${line.slice(1)}`;
|
|
259
|
+
return line;
|
|
260
|
+
});
|
|
261
|
+
this.cachedLines = this.colorize(this.cachedBaseLines, width);
|
|
262
|
+
this.cachedWidth = width;
|
|
263
|
+
this.cachedTheme = this.theme;
|
|
264
|
+
this.cacheSignature = signature;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (runs.length === 0) return [];
|
|
268
|
+
|
|
269
|
+
// Update only spinner and command icon on header line to avoid full re-color for every frame.
|
|
270
|
+
const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
|
|
271
|
+
this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
|
|
272
|
+
return this.cachedLines;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function updateCrewWidget(
|
|
277
|
+
ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">,
|
|
278
|
+
state: CrewWidgetState,
|
|
279
|
+
config?: CrewUiConfig,
|
|
280
|
+
manifestCache?: ManifestCache,
|
|
281
|
+
snapshotCache?: RunSnapshotCache,
|
|
282
|
+
): void {
|
|
283
|
+
if (!ctx.hasUI) return;
|
|
284
|
+
state.frame += 1;
|
|
285
|
+
const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
|
|
286
|
+
const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache);
|
|
287
|
+
const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0);
|
|
288
|
+
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
289
|
+
ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
|
|
290
|
+
const shouldClearLegacy = state.legacyCleared !== true || state.lastPlacement !== placement;
|
|
291
|
+
if (shouldClearLegacy) {
|
|
292
|
+
setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
|
|
293
|
+
state.legacyCleared = true;
|
|
294
|
+
}
|
|
295
|
+
if (!lines.length) {
|
|
296
|
+
if (state.lastVisibility !== "hidden" || state.lastPlacement !== placement) {
|
|
297
|
+
setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
|
|
298
|
+
state.lastVisibility = "hidden";
|
|
299
|
+
state.lastPlacement = placement;
|
|
300
|
+
state.lastKey = WIDGET_KEY;
|
|
301
|
+
state.lastMaxLines = maxLines;
|
|
302
|
+
state.lastCwd = ctx.cwd;
|
|
303
|
+
state.model = undefined;
|
|
304
|
+
}
|
|
305
|
+
requestRender(ctx);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const needsWidgetInstall = state.lastVisibility !== "visible" || state.lastPlacement !== placement || state.lastKey !== WIDGET_KEY || state.lastMaxLines !== maxLines || state.lastCwd !== ctx.cwd || !state.model;
|
|
309
|
+
if (!state.model) state.model = { cwd: ctx.cwd, frame: state.frame, maxLines, notificationCount: state.notificationCount ?? 0, manifestCache, snapshotCache };
|
|
310
|
+
else {
|
|
311
|
+
state.model.cwd = ctx.cwd;
|
|
312
|
+
state.model.frame = state.frame;
|
|
313
|
+
state.model.maxLines = maxLines;
|
|
314
|
+
state.model.notificationCount = state.notificationCount ?? 0;
|
|
315
|
+
state.model.manifestCache = manifestCache;
|
|
316
|
+
state.model.snapshotCache = snapshotCache;
|
|
317
|
+
}
|
|
318
|
+
if (needsWidgetInstall) {
|
|
319
|
+
const model = state.model;
|
|
320
|
+
setExtensionWidget(
|
|
321
|
+
ctx,
|
|
322
|
+
WIDGET_KEY,
|
|
323
|
+
((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme)) as never,
|
|
324
|
+
{ placement, persist: true },
|
|
325
|
+
);
|
|
326
|
+
state.lastVisibility = "visible";
|
|
327
|
+
state.lastPlacement = placement;
|
|
328
|
+
state.lastKey = WIDGET_KEY;
|
|
329
|
+
state.lastMaxLines = maxLines;
|
|
330
|
+
state.lastCwd = ctx.cwd;
|
|
331
|
+
}
|
|
332
|
+
requestRender(ctx);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
|
|
336
|
+
if (state.interval) clearInterval(state.interval);
|
|
337
|
+
state.interval = undefined;
|
|
338
|
+
if (ctx?.hasUI) {
|
|
339
|
+
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
340
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
341
|
+
setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
|
|
342
|
+
setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
|
|
343
|
+
state.lastVisibility = "hidden";
|
|
344
|
+
state.lastPlacement = placement;
|
|
345
|
+
state.lastKey = WIDGET_KEY;
|
|
346
|
+
state.model = undefined;
|
|
347
|
+
state.legacyCleared = true;
|
|
348
|
+
requestRender(ctx);
|
|
349
|
+
}
|
|
350
|
+
}
|