pi-crew 0.2.12 → 0.2.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.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -204,7 +204,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
204
204
  pi.registerCommand("team-run", {
205
205
  description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
206
206
  handler: async (args: string, ctx: ExtensionCommandContext) => {
207
- const result = await handleTeamTool(parseRunArgs(args), { ...teamCommandContext(ctx), metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx as ExtensionContext, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: (runId) => deps.openLiveSidebar(ctx as ExtensionContext, runId) });
207
+ const result = await handleTeamTool(parseRunArgs(args), { ...teamCommandContext(ctx), metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx as ExtensionContext, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: undefined });
208
208
  await notifyCommandResult(ctx, commandText(result));
209
209
  },
210
210
  });
@@ -152,7 +152,19 @@ function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): strin
152
152
  const ctxPct = stats?.contextUsage?.percent;
153
153
  if (ctxPct != null) parts.push(`${Math.round(ctxPct)}% ctx`);
154
154
  } catch { /* ignore */ }
155
- const ms = (act.completedAtMs ?? Date.now()) - act.startedAtMs;
155
+ const rawStarted = act.startedAtMs || 0;
156
+ const rawCompleted = act.completedAtMs || 0;
157
+ const nowMs = Date.now();
158
+ const nowSec = Math.floor(nowMs / 1000);
159
+ // Detect if value is in seconds (Unix timestamp) vs milliseconds
160
+ // If value looks like Unix seconds (within range of ±2 years from now), convert to ms
161
+ const isSeconds = (v: number) => v > 1000000000 && v < 2000000000 + 31536000 * 2;
162
+ const startedMs = isSeconds(rawStarted) ? rawStarted * 1000 : rawStarted;
163
+ const completedMs = isSeconds(rawCompleted) ? rawCompleted * 1000 : rawCompleted;
164
+ // Validate: startedAtMs should be within reasonable bounds
165
+ const isValidStarted = startedMs > 0 && startedMs < nowMs + 60000 && startedMs > nowMs - 3155692600000;
166
+ const isValidCompleted = completedMs === 0 || (completedMs > 0 && completedMs < nowMs + 60000);
167
+ const ms = (isValidCompleted ? completedMs : nowMs) - (isValidStarted ? startedMs : nowMs);
156
168
  parts.push(`${(ms / 1000).toFixed(1)}s`);
157
169
  } else {
158
170
  if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
@@ -423,7 +435,13 @@ export function updateCrewWidget(
423
435
  if (!ctx.hasUI) return;
424
436
  state.frame += 1;
425
437
  const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
426
- const workspaceId = ctx.sessionManager?.getSessionId?.();
438
+ // Get workspaceId from sessionManager, fallback to ownerSessionId from active runs
439
+ let workspaceId = ctx.sessionManager?.getSessionId?.();
440
+ if (!workspaceId && manifestCache) {
441
+ const runs = manifestCache.list(20);
442
+ const active = runs.find((r) => r.status === "running" || r.status === "queued");
443
+ if (active?.ownerSessionId) workspaceId = active.ownerSessionId;
444
+ }
427
445
  const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId);
428
446
  const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0);
429
447
  const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
@@ -63,9 +63,21 @@ export class LiveConversationOverlay {
63
63
 
64
64
  private static readonly SUMMARY_PREFIX = "\u200B"; // zero-width space as summary sentinel
65
65
 
66
+ private safeElapsedMs(act: typeof this.handle.activity): number {
67
+ const rawStarted = act.startedAtMs || 0;
68
+ const rawCompleted = act.completedAtMs || 0;
69
+ const nowMs = Date.now();
70
+ // Detect if value is in seconds vs milliseconds
71
+ const isSeconds = (v: number) => v > 1000000000 && v < 2000000000 + 31536000 * 2;
72
+ const startedMs = isSeconds(rawStarted) ? rawStarted * 1000 : rawStarted;
73
+ const completedMs = isSeconds(rawCompleted) ? rawCompleted * 1000 : rawCompleted;
74
+ const isValidStarted = startedMs > 0 && startedMs < nowMs + 60000 && startedMs > nowMs - 3155692600000;
75
+ const isValidCompleted = completedMs === 0 || (completedMs > 0 && completedMs < nowMs + 60000);
76
+ return (isValidCompleted ? completedMs : nowMs) - (isValidStarted ? startedMs : nowMs);
77
+ }
66
78
  private refreshSummary(): void {
67
79
  const act = this.handle.activity;
68
- const summary = `${LiveConversationOverlay.SUMMARY_PREFIX}[${act.turnCount} turns · ${act.toolUses} tools · ${((act.completedAtMs ?? Date.now()) - act.startedAtMs) / 1000}s]`;
80
+ const summary = `${LiveConversationOverlay.SUMMARY_PREFIX}[${act.turnCount} turns · ${act.toolUses} tools · ${(this.safeElapsedMs(act) / 1000).toFixed(1)}s]`;
69
81
  const lastLine = this.cachedLines[this.cachedLines.length - 1];
70
82
  if (lastLine?.startsWith(LiveConversationOverlay.SUMMARY_PREFIX)) {
71
83
  this.cachedLines[this.cachedLines.length - 1] = summary;
@@ -100,7 +112,7 @@ export class LiveConversationOverlay {
100
112
  : iconForStatus(this.handle.status);
101
113
  const name = this.handle.agent ?? this.handle.taskId;
102
114
  const act = this.handle.activity;
103
- const elapsed = `${((act.completedAtMs ?? Date.now()) - act.startedAtMs) / 1000}s`;
115
+ const elapsed = `${(this.safeElapsedMs(act) / 1000).toFixed(1)}s`;
104
116
  const headerParts: string[] = [];
105
117
  if (act.maxTurns != null) headerParts.push(`turn ${act.turnCount}/${act.maxTurns}`);
106
118
  else if (act.turnCount > 0) headerParts.push(`turn ${act.turnCount}`);
@@ -65,6 +65,8 @@ export class LiveRunSidebar {
65
65
  private cachedLines: string[] = [];
66
66
  private cachedWidth = 0;
67
67
  private cachedSignature = "";
68
+ private autoCloseTimeout?: NodeJS.Timeout;
69
+ private hasAutoClosed = false;
68
70
 
69
71
  constructor(input: { cwd: string; runId: string; done: Done; theme?: unknown; config?: CrewUiConfig; snapshotCache?: RunSnapshotCache }) {
70
72
  this.cwd = input.cwd;
@@ -167,6 +169,24 @@ export class LiveRunSidebar {
167
169
  lines.push(border("├", "─", "┤", w));
168
170
  for (const entry of formatTaskGraphLines(tasks).slice(0, 6)) lines.push(line(entry, w));
169
171
  lines.push(line("q close · /team-dashboard details", w), border("╰", "─", "╯", w));
172
+ // Auto-close logic: if run is terminal and no active agents, close after delay
173
+ const isTerminal = ["completed", "failed", "cancelled", "blocked"].includes(run.status);
174
+ const hasActiveAgents = agents.some((a) => a.status === "running");
175
+ if (isTerminal && !hasActiveAgents && !this.hasAutoClosed) {
176
+ const autoCloseMs = (this.config?.autoCloseDashboardMs ?? 3000);
177
+ if (autoCloseMs > 0) {
178
+ this.autoCloseTimeout = setTimeout(() => {
179
+ this.hasAutoClosed = true;
180
+ this.done(undefined);
181
+ }, autoCloseMs);
182
+ lines.push(line(`auto-close in ${Math.round(autoCloseMs / 1000)}s…`, w));
183
+ }
184
+ }
185
+ // Clear timeout if conditions change
186
+ else if (this.autoCloseTimeout) {
187
+ clearTimeout(this.autoCloseTimeout);
188
+ this.autoCloseTimeout = undefined;
189
+ }
170
190
  this.cachedLines = renderLines(lines.map((entry) => this.colorLine(entry)), w);
171
191
  this.cachedSignature = signature;
172
192
  this.cachedWidth = w;