pi-crew 0.1.30 → 0.1.32

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.
Files changed (159) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +80 -27
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/architecture.md +173 -164
  14. package/docs/refactor-tasks-phase3.md +394 -394
  15. package/docs/refactor-tasks-phase4.md +564 -564
  16. package/docs/refactor-tasks-phase5.md +402 -402
  17. package/docs/refactor-tasks-phase6.md +662 -662
  18. package/docs/research-extension-examples.md +297 -0
  19. package/docs/research-extension-system.md +324 -0
  20. package/docs/research-optimization-plan.md +548 -0
  21. package/docs/research-pi-coding-agent.md +357 -0
  22. package/docs/resource-formats.md +4 -3
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/docs/usage.md +3 -3
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/schema.json +53 -1
  29. package/skills/git-master/SKILL.md +24 -19
  30. package/skills/read-only-explorer/SKILL.md +26 -21
  31. package/skills/safe-bash/SKILL.md +21 -16
  32. package/skills/task-packet/SKILL.md +28 -23
  33. package/skills/verify-evidence/SKILL.md +27 -22
  34. package/src/agents/agent-serializer.ts +34 -34
  35. package/src/agents/discover-agents.ts +102 -102
  36. package/src/config/config.ts +14 -1
  37. package/src/config/defaults.ts +3 -2
  38. package/src/extension/cross-extension-rpc.ts +82 -82
  39. package/src/extension/import-index.ts +4 -3
  40. package/src/extension/management.ts +2 -2
  41. package/src/extension/project-init.ts +9 -7
  42. package/src/extension/register.ts +63 -0
  43. package/src/extension/registration/artifact-cleanup.ts +15 -14
  44. package/src/extension/registration/commands.ts +208 -208
  45. package/src/extension/registration/compaction-guard.ts +125 -0
  46. package/src/extension/registration/subagent-tools.ts +27 -8
  47. package/src/extension/registration/team-tool.ts +61 -44
  48. package/src/extension/result-watcher.ts +98 -98
  49. package/src/extension/run-import.ts +4 -4
  50. package/src/extension/run-index.ts +14 -14
  51. package/src/extension/run-maintenance.ts +24 -24
  52. package/src/extension/team-tool/api.ts +3 -0
  53. package/src/extension/team-tool/cancel.ts +31 -31
  54. package/src/extension/team-tool/doctor.ts +179 -178
  55. package/src/extension/team-tool/inspect.ts +41 -41
  56. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  57. package/src/extension/team-tool/plan.ts +19 -19
  58. package/src/extension/team-tool/status.ts +73 -73
  59. package/src/prompt/prompt-runtime.ts +68 -68
  60. package/src/runtime/agent-control.ts +64 -64
  61. package/src/runtime/agent-memory.ts +72 -72
  62. package/src/runtime/agent-observability.ts +113 -113
  63. package/src/runtime/async-marker.ts +26 -26
  64. package/src/runtime/background-runner.ts +53 -53
  65. package/src/runtime/crew-agent-runtime.ts +58 -58
  66. package/src/runtime/direct-run.ts +35 -35
  67. package/src/runtime/foreground-control.ts +82 -82
  68. package/src/runtime/green-contract.ts +46 -46
  69. package/src/runtime/group-join.ts +88 -88
  70. package/src/runtime/live-agent-control.ts +78 -78
  71. package/src/runtime/live-agent-manager.ts +85 -85
  72. package/src/runtime/live-control-realtime.ts +36 -36
  73. package/src/runtime/live-session-runtime.ts +299 -299
  74. package/src/runtime/manifest-cache.ts +212 -214
  75. package/src/runtime/model-fallback.ts +261 -261
  76. package/src/runtime/parallel-research.ts +44 -44
  77. package/src/runtime/parallel-utils.ts +99 -99
  78. package/src/runtime/pi-json-output.ts +111 -111
  79. package/src/runtime/pi-spawn.ts +96 -96
  80. package/src/runtime/policy-engine.ts +78 -78
  81. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  82. package/src/runtime/process-status.ts +56 -56
  83. package/src/runtime/progress-event-coalescer.ts +43 -43
  84. package/src/runtime/recovery-recipes.ts +74 -74
  85. package/src/runtime/role-permission.ts +39 -39
  86. package/src/runtime/session-usage.ts +79 -79
  87. package/src/runtime/sidechain-output.ts +28 -28
  88. package/src/runtime/subagent-manager.ts +29 -2
  89. package/src/runtime/task-display.ts +38 -38
  90. package/src/runtime/task-output-context.ts +106 -106
  91. package/src/runtime/task-packet.ts +84 -84
  92. package/src/runtime/task-runner/live-executor.ts +98 -98
  93. package/src/runtime/task-runner/progress.ts +111 -111
  94. package/src/runtime/task-runner/prompt-builder.ts +72 -72
  95. package/src/runtime/task-runner/result-utils.ts +14 -14
  96. package/src/runtime/task-runner/state-helpers.ts +22 -22
  97. package/src/runtime/worker-heartbeat.ts +21 -21
  98. package/src/runtime/worker-startup.ts +57 -57
  99. package/src/schema/config-schema.ts +12 -0
  100. package/src/schema/team-tool-schema.ts +100 -100
  101. package/src/state/artifact-store.ts +108 -108
  102. package/src/state/contracts.ts +105 -105
  103. package/src/state/jsonl-writer.ts +77 -77
  104. package/src/state/state-store.ts +8 -9
  105. package/src/state/task-claims.ts +42 -42
  106. package/src/state/types.ts +180 -180
  107. package/src/state/usage.ts +29 -29
  108. package/src/subagents/async-entry.ts +1 -1
  109. package/src/subagents/index.ts +3 -3
  110. package/src/subagents/live/control.ts +1 -1
  111. package/src/subagents/live/manager.ts +1 -1
  112. package/src/subagents/live/realtime.ts +1 -1
  113. package/src/subagents/live/session-runtime.ts +1 -1
  114. package/src/subagents/manager.ts +1 -1
  115. package/src/subagents/spawn.ts +1 -1
  116. package/src/teams/discover-teams.ts +2 -2
  117. package/src/teams/team-serializer.ts +36 -36
  118. package/src/types/diff.d.ts +18 -0
  119. package/src/ui/crew-footer.ts +101 -101
  120. package/src/ui/crew-select-list.ts +111 -111
  121. package/src/ui/crew-widget.ts +285 -285
  122. package/src/ui/dynamic-border.ts +25 -25
  123. package/src/ui/layout-primitives.ts +106 -106
  124. package/src/ui/loaders.ts +158 -158
  125. package/src/ui/mascot.ts +441 -441
  126. package/src/ui/powerbar-publisher.ts +94 -94
  127. package/src/ui/render-diff.ts +119 -119
  128. package/src/ui/run-dashboard.ts +372 -372
  129. package/src/ui/status-colors.ts +54 -54
  130. package/src/ui/syntax-highlight.ts +116 -116
  131. package/src/ui/transcript-viewer.ts +302 -302
  132. package/src/utils/completion-dedupe.ts +63 -63
  133. package/src/utils/file-coalescer.ts +84 -84
  134. package/src/utils/frontmatter.ts +36 -36
  135. package/src/utils/fs-watch.ts +31 -31
  136. package/src/utils/git.ts +262 -262
  137. package/src/utils/ids.ts +12 -12
  138. package/src/utils/names.ts +26 -26
  139. package/src/utils/paths.ts +34 -7
  140. package/src/utils/sleep.ts +32 -32
  141. package/src/utils/timings.ts +31 -31
  142. package/src/utils/visual.ts +159 -159
  143. package/src/workflows/discover-workflows.ts +2 -2
  144. package/src/workflows/validate-workflow.ts +40 -40
  145. package/src/worktree/branch-freshness.ts +45 -45
  146. package/src/worktree/cleanup.ts +71 -69
  147. package/src/worktree/worktree-manager.ts +3 -1
  148. package/teams/default.team.md +12 -12
  149. package/teams/fast-fix.team.md +11 -11
  150. package/teams/implementation.team.md +18 -18
  151. package/teams/parallel-research.team.md +14 -14
  152. package/teams/research.team.md +11 -11
  153. package/teams/review.team.md +12 -12
  154. package/workflows/default.workflow.md +29 -29
  155. package/workflows/fast-fix.workflow.md +22 -22
  156. package/workflows/implementation.workflow.md +38 -38
  157. package/workflows/parallel-research.workflow.md +46 -46
  158. package/workflows/research.workflow.md +22 -22
  159. package/workflows/review.workflow.md +30 -30
@@ -1,285 +1,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
-
15
- const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
16
- const TOOL_LABELS: Record<string, string> = {
17
- read: "reading",
18
- bash: "running command",
19
- edit: "editing",
20
- write: "writing",
21
- grep: "searching",
22
- find: "finding files",
23
- ls: "listing",
24
- };
25
- const LEGACY_WIDGET_KEY = "pi-crew";
26
- const WIDGET_KEY = "pi-crew-active";
27
- const STATUS_KEY = "pi-crew";
28
-
29
- const MAX_LINES_DEFAULT = 10;
30
- const MAX_AGENTS_DISPLAY = 3;
31
-
32
- type WidgetComponent = { render(width: number): string[]; invalidate(): void };
33
-
34
- export interface CrewWidgetState {
35
- frame: number;
36
- interval?: ReturnType<typeof setInterval>;
37
- }
38
-
39
- interface WidgetRun {
40
- run: TeamRunManifest;
41
- agents: CrewAgentRecord[];
42
- }
43
-
44
- function elapsed(iso: string | undefined, now = Date.now()): string | undefined {
45
- if (!iso) return undefined;
46
- const ms = Math.max(0, now - new Date(iso).getTime());
47
- if (!Number.isFinite(ms)) return undefined;
48
- if (ms < 1000) return "now";
49
- if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
50
- if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
51
- return `${Math.floor(ms / 3_600_000)}h`;
52
- }
53
-
54
- function agentActivity(agent: CrewAgentRecord): string {
55
- if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
56
- const recent = agent.progress?.recentOutput?.at(-1);
57
- if (recent) return recent.replace(/\s+/g, " ").trim();
58
- if (agent.progress?.activityState === "needs_attention") return "needs attention";
59
- if (agent.status === "queued") return "queued";
60
- if (agent.status === "running") return "thinking…";
61
- if (agent.status === "failed") return agent.error ?? "failed";
62
- return "done";
63
- }
64
-
65
- function agentStats(agent: CrewAgentRecord): string {
66
- const parts: string[] = [];
67
- if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
68
- if (agent.progress?.tokens) parts.push(`${agent.progress.tokens} tok`);
69
- if (agent.progress?.turns) parts.push(`⟳${agent.progress.turns}`);
70
- const age = elapsed(agent.completedAt ?? agent.startedAt);
71
- if (age) parts.push(agent.completedAt ? age : `${age} ago`);
72
- return parts.join(" · ");
73
- }
74
-
75
- function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
76
- try {
77
- return readCrewAgents(run);
78
- } catch {
79
- return [];
80
- }
81
- }
82
-
83
- export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache): WidgetRun[] {
84
- const runs = manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20);
85
- return runs
86
- .map((run) => ({ run, agents: agentsFor(run) }))
87
- .filter((item) => isDisplayActiveRun(item.run, item.agents));
88
- }
89
-
90
- function statusSummary(runs: WidgetRun[]): string {
91
- const agents = runs.flatMap((item) => item.agents);
92
- const runningAgents = agents.filter((agent) => agent.status === "running").length;
93
- const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
94
- const completedAgents = agents.filter((agent) => agent.status === "completed").length;
95
- const parts = [`${runningAgents} running`];
96
- if (queuedAgents) parts.push(`${queuedAgents} queued`);
97
- if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
98
- return `Crew: ${parts.join(", ")}`;
99
- }
100
-
101
- export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines = 20): string {
102
- const agents = runs.flatMap((item) => item.agents);
103
- const runningAgents = agents.filter((agent) => agent.status === "running").length;
104
- const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
105
- const completedAgents = agents.filter((agent) => agent.status === "completed").length;
106
- const parts = [`${runningAgents} running`];
107
- if (queuedAgents) parts.push(`${queuedAgents} queued`);
108
- if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
109
- return `${runningGlyph} Crew agents · ${parts.join(" · ")} · /team-dashboard`;
110
- }
111
-
112
- function shortRunLabel(run: TeamRunManifest): string {
113
- return `${run.team}/${run.workflow ?? "none"}`;
114
- }
115
-
116
- export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[]): string[] {
117
- const runs = providedRuns ?? activeWidgetRuns(cwd);
118
- if (!runs.length) return [];
119
- const runningGlyph = SPINNER[frame % SPINNER.length] ?? SPINNER[0];
120
- const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines)];
121
- for (const { run, agents } of runs) {
122
- const activeAgents = agents.filter((item) => item.status === "running" || item.status === "queued");
123
- const completed = agents.filter((agent) => agent.status === "completed").length;
124
- const runGlyph = iconForStatus(run.status, { runningGlyph });
125
- lines.push(`├─ ${runGlyph} ${shortRunLabel(run)} · ${completed}/${agents.length} done · ${run.runId.slice(-8)}`);
126
- const visibleAgents = activeAgents.slice(0, MAX_AGENTS_DISPLAY);
127
- for (const [index, agent] of visibleAgents.entries()) {
128
- const last = index === visibleAgents.length - 1 && activeAgents.length <= MAX_AGENTS_DISPLAY;
129
- const branch = last ? "└─" : "├─";
130
- const agentGlyph = iconForStatus(agent.status, { runningGlyph });
131
- const stats = agentStats(agent);
132
- lines.push(`│ ${branch} ${agentGlyph} ${agent.agent} · ${agent.role}`);
133
- lines.push(`│ ⎿ ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
134
- }
135
- if (activeAgents.length > MAX_AGENTS_DISPLAY) lines.push(`│ └─ … +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`);
136
- if (lines.length >= maxLines) break;
137
- }
138
- return lines.slice(0, maxLines);
139
- }
140
-
141
- function statusGlyphColor(icon: string): Parameters<CrewTheme["fg"]>[0] {
142
- const mapping: Record<string, Parameters<CrewTheme["fg"]>[0]> = {
143
- "✓": "success",
144
- "✗": "error",
145
- "■": "warning",
146
- "⏸": "warning",
147
- "◦": "dim",
148
- "·": "dim",
149
- "▶": "accent",
150
- };
151
- return mapping[icon] ?? "accent";
152
- }
153
-
154
- function colorWidgetLine(line: string, index: number, theme: CrewTheme): string {
155
- let result = line;
156
- if (index === 0) {
157
- result = result.replace("Crew agents", theme.bold(theme.fg("accent", "Crew agents")));
158
- }
159
- result = result.replace(/[✓✗■⏸◦·▶]/g, (icon) => theme.fg(statusGlyphColor(icon), icon));
160
- if (index === 0) {
161
- result = theme.fg("accent", result);
162
- }
163
- return result;
164
- }
165
-
166
- function renderLines(lines: string[], width: number): string[] {
167
- const box = new Box(0, 0);
168
- for (const line of lines) {
169
- box.addChild(new Text(line));
170
- }
171
- return box.render(width);
172
- }
173
-
174
- class CrewWidgetComponent implements WidgetComponent {
175
- private cwd: string;
176
- private frame: number;
177
- private maxLines: number;
178
- private theme: CrewTheme;
179
- private cacheSignature: string;
180
- private cachedWidth = 0;
181
- private cachedLines: string[] = [];
182
- private cachedBaseLines: string[] = [];
183
- private cachedTheme: CrewTheme;
184
- private manifestCache?: ManifestCache;
185
- private readonly unsubscribeTheme: () => void;
186
-
187
- constructor(cwd: string, frame: number, maxLines: number, themeLike: unknown, manifestCache?: ManifestCache) {
188
- this.cwd = cwd;
189
- this.frame = frame;
190
- this.maxLines = maxLines;
191
- this.theme = asCrewTheme(themeLike);
192
- this.cachedTheme = this.theme;
193
- this.manifestCache = manifestCache;
194
- this.cacheSignature = "";
195
- this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
196
- }
197
-
198
- private buildSignature(runs: WidgetRun[]): string {
199
- return runs
200
- .map((entry) => `${entry.run.runId}:${entry.run.status}:${entry.run.updatedAt}:` + entry.agents.map((agent) => `${agent.status}:${agent.startedAt}:${agent.completedAt ?? ""}`).join(","))
201
- .join("|");
202
- }
203
-
204
- private colorize(lines: string[], width: number): string[] {
205
- return renderLines(lines.map((line, index) => colorWidgetLine(line, index, this.theme)), width);
206
- }
207
-
208
- invalidate(): void {
209
- this.cacheSignature = "";
210
- this.cachedBaseLines = [];
211
- this.cachedLines = [];
212
- }
213
-
214
- dispose(): void {
215
- this.unsubscribeTheme();
216
- }
217
-
218
- render(width: number): string[] {
219
- const runs = activeWidgetRuns(this.cwd, this.manifestCache);
220
- const signature = this.buildSignature(runs);
221
- const runningGlyph = SPINNER[this.frame % SPINNER.length] ?? SPINNER[0];
222
- const headerGlyph = runs.length ? SPINNER[0] : " ";
223
-
224
- if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
225
- this.cachedBaseLines = buildCrewWidgetLines(this.cwd, 0, this.maxLines, runs).map((line, index) => {
226
- if (index === 0 && line.length > 0) return `${headerGlyph}${line.slice(1)}`;
227
- return line;
228
- });
229
- this.cachedLines = this.colorize(this.cachedBaseLines, width);
230
- this.cachedWidth = width;
231
- this.cachedTheme = this.theme;
232
- this.cacheSignature = signature;
233
- }
234
-
235
- if (runs.length === 0) return [];
236
-
237
- // Update only spinner and command icon on header line to avoid full re-color for every frame.
238
- const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
239
- this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
240
- return this.cachedLines;
241
- }
242
- }
243
-
244
- function requestRender(ctx: Pick<ExtensionContext, "ui">): void {
245
- (ctx.ui as { requestRender?: () => void }).requestRender?.();
246
- }
247
-
248
- export function updateCrewWidget(
249
- ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">,
250
- state: CrewWidgetState,
251
- config?: CrewUiConfig,
252
- manifestCache?: ManifestCache,
253
- ): void {
254
- if (!ctx.hasUI) return;
255
- state.frame += 1;
256
- const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
257
- const runs = activeWidgetRuns(ctx.cwd, manifestCache);
258
- const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs);
259
- const placement = config?.widgetPlacement ?? "aboveEditor";
260
- ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
261
- ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
262
- if (!lines.length) {
263
- ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
264
- requestRender(ctx);
265
- return;
266
- }
267
- ctx.ui.setWidget(
268
- WIDGET_KEY,
269
- ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(ctx.cwd, state.frame, maxLines, theme, manifestCache)) as never,
270
- { placement },
271
- );
272
- requestRender(ctx);
273
- }
274
-
275
- export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
276
- if (state.interval) clearInterval(state.interval);
277
- state.interval = undefined;
278
- if (ctx?.hasUI) {
279
- const placement = config?.widgetPlacement ?? "aboveEditor";
280
- ctx.ui.setStatus(STATUS_KEY, undefined);
281
- ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
282
- ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
283
- requestRender(ctx);
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
+
15
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
16
+ const TOOL_LABELS: Record<string, string> = {
17
+ read: "reading",
18
+ bash: "running command",
19
+ edit: "editing",
20
+ write: "writing",
21
+ grep: "searching",
22
+ find: "finding files",
23
+ ls: "listing",
24
+ };
25
+ const LEGACY_WIDGET_KEY = "pi-crew";
26
+ const WIDGET_KEY = "pi-crew-active";
27
+ const STATUS_KEY = "pi-crew";
28
+
29
+ const MAX_LINES_DEFAULT = 10;
30
+ const MAX_AGENTS_DISPLAY = 3;
31
+
32
+ type WidgetComponent = { render(width: number): string[]; invalidate(): void };
33
+
34
+ export interface CrewWidgetState {
35
+ frame: number;
36
+ interval?: ReturnType<typeof setInterval>;
37
+ }
38
+
39
+ interface WidgetRun {
40
+ run: TeamRunManifest;
41
+ agents: CrewAgentRecord[];
42
+ }
43
+
44
+ function elapsed(iso: string | undefined, now = Date.now()): string | undefined {
45
+ if (!iso) return undefined;
46
+ const ms = Math.max(0, now - new Date(iso).getTime());
47
+ if (!Number.isFinite(ms)) return undefined;
48
+ if (ms < 1000) return "now";
49
+ if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
50
+ if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m`;
51
+ return `${Math.floor(ms / 3_600_000)}h`;
52
+ }
53
+
54
+ function agentActivity(agent: CrewAgentRecord): string {
55
+ if (agent.progress?.currentTool) return `${TOOL_LABELS[agent.progress.currentTool] ?? agent.progress.currentTool}…`;
56
+ const recent = agent.progress?.recentOutput?.at(-1);
57
+ if (recent) return recent.replace(/\s+/g, " ").trim();
58
+ if (agent.progress?.activityState === "needs_attention") return "needs attention";
59
+ if (agent.status === "queued") return "queued";
60
+ if (agent.status === "running") return "thinking…";
61
+ if (agent.status === "failed") return agent.error ?? "failed";
62
+ return "done";
63
+ }
64
+
65
+ function agentStats(agent: CrewAgentRecord): string {
66
+ const parts: string[] = [];
67
+ if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
68
+ if (agent.progress?.tokens) parts.push(`${agent.progress.tokens} tok`);
69
+ if (agent.progress?.turns) parts.push(`⟳${agent.progress.turns}`);
70
+ const age = elapsed(agent.completedAt ?? agent.startedAt);
71
+ if (age) parts.push(agent.completedAt ? age : `${age} ago`);
72
+ return parts.join(" · ");
73
+ }
74
+
75
+ function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
76
+ try {
77
+ return readCrewAgents(run);
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+
83
+ export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache): WidgetRun[] {
84
+ const runs = manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20);
85
+ return runs
86
+ .map((run) => ({ run, agents: agentsFor(run) }))
87
+ .filter((item) => isDisplayActiveRun(item.run, item.agents));
88
+ }
89
+
90
+ function statusSummary(runs: WidgetRun[]): string {
91
+ const agents = runs.flatMap((item) => item.agents);
92
+ const runningAgents = agents.filter((agent) => agent.status === "running").length;
93
+ const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
94
+ const completedAgents = agents.filter((agent) => agent.status === "completed").length;
95
+ const parts = [`${runningAgents} running`];
96
+ if (queuedAgents) parts.push(`${queuedAgents} queued`);
97
+ if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
98
+ return `Crew: ${parts.join(", ")}`;
99
+ }
100
+
101
+ export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines = 20): string {
102
+ const agents = runs.flatMap((item) => item.agents);
103
+ const runningAgents = agents.filter((agent) => agent.status === "running").length;
104
+ const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
105
+ const completedAgents = agents.filter((agent) => agent.status === "completed").length;
106
+ const parts = [`${runningAgents} running`];
107
+ if (queuedAgents) parts.push(`${queuedAgents} queued`);
108
+ if (completedAgents) parts.push(`${completedAgents}/${agents.length} done`);
109
+ return `${runningGlyph} Crew agents · ${parts.join(" · ")} · /team-dashboard`;
110
+ }
111
+
112
+ function shortRunLabel(run: TeamRunManifest): string {
113
+ return `${run.team}/${run.workflow ?? "none"}`;
114
+ }
115
+
116
+ export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[]): string[] {
117
+ const runs = providedRuns ?? activeWidgetRuns(cwd);
118
+ if (!runs.length) return [];
119
+ const runningGlyph = SPINNER[frame % SPINNER.length] ?? SPINNER[0];
120
+ const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines)];
121
+ for (const { run, agents } of runs) {
122
+ const activeAgents = agents.filter((item) => item.status === "running" || item.status === "queued");
123
+ const completed = agents.filter((agent) => agent.status === "completed").length;
124
+ const runGlyph = iconForStatus(run.status, { runningGlyph });
125
+ lines.push(`├─ ${runGlyph} ${shortRunLabel(run)} · ${completed}/${agents.length} done · ${run.runId.slice(-8)}`);
126
+ const visibleAgents = activeAgents.slice(0, MAX_AGENTS_DISPLAY);
127
+ for (const [index, agent] of visibleAgents.entries()) {
128
+ const last = index === visibleAgents.length - 1 && activeAgents.length <= MAX_AGENTS_DISPLAY;
129
+ const branch = last ? "└─" : "├─";
130
+ const agentGlyph = iconForStatus(agent.status, { runningGlyph });
131
+ const stats = agentStats(agent);
132
+ lines.push(`│ ${branch} ${agentGlyph} ${agent.agent} · ${agent.role}`);
133
+ lines.push(`│ ⎿ ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
134
+ }
135
+ if (activeAgents.length > MAX_AGENTS_DISPLAY) lines.push(`│ └─ … +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`);
136
+ if (lines.length >= maxLines) break;
137
+ }
138
+ return lines.slice(0, maxLines);
139
+ }
140
+
141
+ function statusGlyphColor(icon: string): Parameters<CrewTheme["fg"]>[0] {
142
+ const mapping: Record<string, Parameters<CrewTheme["fg"]>[0]> = {
143
+ "✓": "success",
144
+ "✗": "error",
145
+ "■": "warning",
146
+ "⏸": "warning",
147
+ "◦": "dim",
148
+ "·": "dim",
149
+ "▶": "accent",
150
+ };
151
+ return mapping[icon] ?? "accent";
152
+ }
153
+
154
+ function colorWidgetLine(line: string, index: number, theme: CrewTheme): string {
155
+ let result = line;
156
+ if (index === 0) {
157
+ result = result.replace("Crew agents", theme.bold(theme.fg("accent", "Crew agents")));
158
+ }
159
+ result = result.replace(/[✓✗■⏸◦·▶]/g, (icon) => theme.fg(statusGlyphColor(icon), icon));
160
+ if (index === 0) {
161
+ result = theme.fg("accent", result);
162
+ }
163
+ return result;
164
+ }
165
+
166
+ function renderLines(lines: string[], width: number): string[] {
167
+ const box = new Box(0, 0);
168
+ for (const line of lines) {
169
+ box.addChild(new Text(line));
170
+ }
171
+ return box.render(width);
172
+ }
173
+
174
+ class CrewWidgetComponent implements WidgetComponent {
175
+ private cwd: string;
176
+ private frame: number;
177
+ private maxLines: number;
178
+ private theme: CrewTheme;
179
+ private cacheSignature: string;
180
+ private cachedWidth = 0;
181
+ private cachedLines: string[] = [];
182
+ private cachedBaseLines: string[] = [];
183
+ private cachedTheme: CrewTheme;
184
+ private manifestCache?: ManifestCache;
185
+ private readonly unsubscribeTheme: () => void;
186
+
187
+ constructor(cwd: string, frame: number, maxLines: number, themeLike: unknown, manifestCache?: ManifestCache) {
188
+ this.cwd = cwd;
189
+ this.frame = frame;
190
+ this.maxLines = maxLines;
191
+ this.theme = asCrewTheme(themeLike);
192
+ this.cachedTheme = this.theme;
193
+ this.manifestCache = manifestCache;
194
+ this.cacheSignature = "";
195
+ this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
196
+ }
197
+
198
+ private buildSignature(runs: WidgetRun[]): string {
199
+ return runs
200
+ .map((entry) => `${entry.run.runId}:${entry.run.status}:${entry.run.updatedAt}:` + entry.agents.map((agent) => `${agent.status}:${agent.startedAt}:${agent.completedAt ?? ""}`).join(","))
201
+ .join("|");
202
+ }
203
+
204
+ private colorize(lines: string[], width: number): string[] {
205
+ return renderLines(lines.map((line, index) => colorWidgetLine(line, index, this.theme)), width);
206
+ }
207
+
208
+ invalidate(): void {
209
+ this.cacheSignature = "";
210
+ this.cachedBaseLines = [];
211
+ this.cachedLines = [];
212
+ }
213
+
214
+ dispose(): void {
215
+ this.unsubscribeTheme();
216
+ }
217
+
218
+ render(width: number): string[] {
219
+ const runs = activeWidgetRuns(this.cwd, this.manifestCache);
220
+ const signature = this.buildSignature(runs);
221
+ const runningGlyph = SPINNER[this.frame % SPINNER.length] ?? SPINNER[0];
222
+ const headerGlyph = runs.length ? SPINNER[0] : " ";
223
+
224
+ if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
225
+ this.cachedBaseLines = buildCrewWidgetLines(this.cwd, 0, this.maxLines, runs).map((line, index) => {
226
+ if (index === 0 && line.length > 0) return `${headerGlyph}${line.slice(1)}`;
227
+ return line;
228
+ });
229
+ this.cachedLines = this.colorize(this.cachedBaseLines, width);
230
+ this.cachedWidth = width;
231
+ this.cachedTheme = this.theme;
232
+ this.cacheSignature = signature;
233
+ }
234
+
235
+ if (runs.length === 0) return [];
236
+
237
+ // Update only spinner and command icon on header line to avoid full re-color for every frame.
238
+ const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
239
+ this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
240
+ return this.cachedLines;
241
+ }
242
+ }
243
+
244
+ function requestRender(ctx: Pick<ExtensionContext, "ui">): void {
245
+ (ctx.ui as { requestRender?: () => void }).requestRender?.();
246
+ }
247
+
248
+ export function updateCrewWidget(
249
+ ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">,
250
+ state: CrewWidgetState,
251
+ config?: CrewUiConfig,
252
+ manifestCache?: ManifestCache,
253
+ ): void {
254
+ if (!ctx.hasUI) return;
255
+ state.frame += 1;
256
+ const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
257
+ const runs = activeWidgetRuns(ctx.cwd, manifestCache);
258
+ const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs);
259
+ const placement = config?.widgetPlacement ?? "aboveEditor";
260
+ ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
261
+ ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
262
+ if (!lines.length) {
263
+ ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
264
+ requestRender(ctx);
265
+ return;
266
+ }
267
+ ctx.ui.setWidget(
268
+ WIDGET_KEY,
269
+ ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(ctx.cwd, state.frame, maxLines, theme, manifestCache)) as never,
270
+ { placement },
271
+ );
272
+ requestRender(ctx);
273
+ }
274
+
275
+ export function stopCrewWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined, state: CrewWidgetState, config?: CrewUiConfig): void {
276
+ if (state.interval) clearInterval(state.interval);
277
+ state.interval = undefined;
278
+ if (ctx?.hasUI) {
279
+ const placement = config?.widgetPlacement ?? "aboveEditor";
280
+ ctx.ui.setStatus(STATUS_KEY, undefined);
281
+ ctx.ui.setWidget(LEGACY_WIDGET_KEY, undefined, { placement });
282
+ ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
283
+ requestRender(ctx);
284
+ }
285
+ }
@@ -1,25 +1,25 @@
1
- import type { CrewTheme } from "./theme-adapter.ts";
2
-
3
- export interface DynamicCrewBorderOptions {
4
- color?: (value: string) => string;
5
- char?: string;
6
- }
7
-
8
- export class DynamicCrewBorder {
9
- private readonly theme: CrewTheme;
10
- private readonly color?: (value: string) => string;
11
- private readonly char: string;
12
-
13
- constructor(theme: CrewTheme, options: DynamicCrewBorderOptions = {}) {
14
- this.theme = theme;
15
- this.color = options.color;
16
- this.char = options.char && options.char.length > 0 ? options.char : "─";
17
- }
18
-
19
- render(width: number): string[] {
20
- const line = this.char.repeat(Math.max(0, width));
21
- return [this.color ? this.color(line) : this.theme.fg("border", line)];
22
- }
23
-
24
- invalidate(): void {}
25
- }
1
+ import type { CrewTheme } from "./theme-adapter.ts";
2
+
3
+ export interface DynamicCrewBorderOptions {
4
+ color?: (value: string) => string;
5
+ char?: string;
6
+ }
7
+
8
+ export class DynamicCrewBorder {
9
+ private readonly theme: CrewTheme;
10
+ private readonly color?: (value: string) => string;
11
+ private readonly char: string;
12
+
13
+ constructor(theme: CrewTheme, options: DynamicCrewBorderOptions = {}) {
14
+ this.theme = theme;
15
+ this.color = options.color;
16
+ this.char = options.char && options.char.length > 0 ? options.char : "─";
17
+ }
18
+
19
+ render(width: number): string[] {
20
+ const line = this.char.repeat(Math.max(0, width));
21
+ return [this.color ? this.color(line) : this.theme.fg("border", line)];
22
+ }
23
+
24
+ invalidate(): void {}
25
+ }