pi-crew 0.1.13 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -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 `⚙ pi-crew · ${parts.join(" · ")} · /team-dashboard`;
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[] = [statusSummary(runs)];
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
- lines.push(`${glyph(run.status, runningGlyph)} ${shortRunLabel(run)} · ${completed}/${agents.length} done · ${run.runId.slice(-8)}`);
134
- for (const agent of activeAgents.slice(0, 3)) {
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(` ${glyph(agent.status, runningGlyph)} ${agent.agent} (${agent.role}) · ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
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(` … +${activeAgents.length - 3} more agents`);
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
- const fg = this.theme.fg?.bind(this.theme) ?? ((_color: string, text: string) => text);
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 ?? 8;
195
+ const maxLines = config?.widgetMaxLines ?? 10;
173
196
  const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines);
174
- ctx.ui.setStatus("pi-crew", lines.length ? lines[0] : undefined);
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("pi-crew", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
201
+ ctx.ui.setWidget(WIDGET_KEY, undefined, { placement });
202
+ requestRender(ctx);
177
203
  return;
178
204
  }
179
- ctx.ui.setWidget("pi-crew", ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(ctx.cwd, state.frame, maxLines, theme as ThemeLike)) as never, { placement: config?.widgetPlacement ?? "aboveEditor" });
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
- ctx.ui.setStatus("pi-crew", undefined);
187
- ctx.ui.setWidget("pi-crew", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
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
  }