pi-crew 0.2.11 → 0.2.13

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.11",
3
+ "version": "0.2.13",
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
  });
@@ -387,12 +387,14 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
387
387
 
388
388
  pi.registerCommand("team-dashboard", { description: "Open a pi-crew run dashboard overlay", handler: async (_args: string, ctx: ExtensionCommandContext) => {
389
389
  for (;;) {
390
+ // Extract sessionId for workspace-scoped filtering
391
+ const sessionId = ctx.sessionManager?.getSessionId?.();
390
392
  const runs = deps.getManifestCache(ctx.cwd).list(50);
391
393
  const uiConfig = loadConfig(ctx.cwd).config.ui;
392
394
  const rightPanel = (uiConfig?.dashboardPlacement ?? DEFAULT_UI.dashboardPlacement) === "right";
393
395
  const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth)) : "90%";
394
396
  const { RunDashboard } = await ui();
395
- const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools, snapshotCache: deps.getRunSnapshotCache?.(ctx.cwd), runProvider: () => deps.getManifestCache(ctx.cwd).list(50), registry: deps.getMetricRegistry?.(), requestRender: () => requestRenderTarget(tui) }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
397
+ const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools, snapshotCache: deps.getRunSnapshotCache?.(ctx.cwd), runProvider: () => deps.getManifestCache(ctx.cwd).list(50), registry: deps.getMetricRegistry?.(), workspaceId: sessionId, requestRender: () => requestRenderTarget(tui) }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
396
398
  if (!selection) return;
397
399
  if (selection.action === "reload") continue;
398
400
  if (selection.action === "notifications-dismiss") {
@@ -152,7 +152,13 @@ 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 completedMs = act.completedAtMs || 0;
156
+ const startedMs = act.startedAtMs || 0;
157
+ // Validate: startedAtMs should be within reasonable bounds (not seconds, not far future)
158
+ const nowMs = Date.now();
159
+ const isValidStarted = startedMs > 0 && startedMs < nowMs + 60000 && startedMs > nowMs - 3155692600000;
160
+ const isValidCompleted = completedMs === 0 || (completedMs > 0 && completedMs < nowMs + 60000);
161
+ const ms = (isValidCompleted ? completedMs : nowMs) - (isValidStarted ? startedMs : nowMs);
156
162
  parts.push(`${(ms / 1000).toFixed(1)}s`);
157
163
  } else {
158
164
  if (agent.toolUses) parts.push(`${agent.toolUses} tools`);
@@ -175,7 +181,7 @@ function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
175
181
  let lastStaleReconcileAt = 0;
176
182
  const STALE_RECONCILE_INTERVAL_MS = 60_000;
177
183
 
178
- export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache, snapshotCache?: RunSnapshotCache, preloadedManifests?: TeamRunManifest[]): WidgetRun[] {
184
+ export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache, snapshotCache?: RunSnapshotCache, preloadedManifests?: TeamRunManifest[], workspaceId?: string): WidgetRun[] {
179
185
  // Evict stale live-agent handles (terminal status >10min, or running >30min with no update)
180
186
  evictStaleLiveAgentHandles();
181
187
  // Periodic stale reconciliation: detect ghost runs on disk with dead PIDs
@@ -185,7 +191,11 @@ export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache, sna
185
191
  lastStaleReconcileAt = now;
186
192
  try { reconcileAllStaleRuns(cwd, manifestCache); } catch { /* non-critical background maintenance */ }
187
193
  }
188
- const runs = preloadedManifests ?? (manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20));
194
+ let runs = preloadedManifests ?? (manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20));
195
+ // Filter by workspaceId for session isolation
196
+ if (workspaceId) {
197
+ runs = runs.filter((run) => !run.ownerSessionId || run.ownerSessionId === workspaceId);
198
+ }
189
199
  return runs
190
200
  .map((run) => {
191
201
  try {
@@ -409,7 +419,7 @@ class CrewWidgetComponent implements WidgetComponent {
409
419
  }
410
420
 
411
421
  export function updateCrewWidget(
412
- ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui">,
422
+ ctx: Pick<ExtensionContext, "cwd" | "hasUI" | "ui" | "sessionManager">,
413
423
  state: CrewWidgetState,
414
424
  config?: CrewUiConfig,
415
425
  manifestCache?: ManifestCache,
@@ -419,7 +429,14 @@ export function updateCrewWidget(
419
429
  if (!ctx.hasUI) return;
420
430
  state.frame += 1;
421
431
  const maxLines = config?.widgetMaxLines ?? MAX_LINES_DEFAULT;
422
- const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests);
432
+ // Get workspaceId from sessionManager, fallback to ownerSessionId from active runs
433
+ let workspaceId = ctx.sessionManager?.getSessionId?.();
434
+ if (!workspaceId && manifestCache) {
435
+ const runs = manifestCache.list(20);
436
+ const active = runs.find((r) => r.status === "running" || r.status === "queued");
437
+ if (active?.ownerSessionId) workspaceId = active.ownerSessionId;
438
+ }
439
+ const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId);
423
440
  const lines = buildCrewWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0);
424
441
  const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
425
442
  ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
@@ -3,7 +3,7 @@ import { iconForStatus } from "../status-colors.ts";
3
3
  import type { RunUiSnapshot } from "../snapshot-types.ts";
4
4
  import { spinnerFrame } from "../spinner.ts";
5
5
  import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
6
- import { listLiveAgents, type LiveAgentHandle } from "../../runtime/live-agent-manager.ts";
6
+ import { listLiveAgents, listLiveAgentsByWorkspace, type LiveAgentHandle } from "../../runtime/live-agent-manager.ts";
7
7
 
8
8
  /**
9
9
  * Returns true if this agent did real work (LLM call, tool use, or non-trivial duration).
@@ -52,7 +52,11 @@ function describeActivity(handle: LiveAgentHandle): string {
52
52
  export function renderAgentsPane(snapshot: RunUiSnapshot | undefined, options: RunDashboardOptions = {}): string[] {
53
53
  if (!snapshot) return ["(snapshot unavailable)"];
54
54
  if (!snapshot.agents.length) return ["(no agents)"];
55
- const liveForRun = listLiveAgents().filter(h => h.runId === snapshot.runId);
55
+ // Filter live agents by workspaceId for session isolation
56
+ const allLive = options.workspaceId
57
+ ? listLiveAgentsByWorkspace(options.workspaceId)
58
+ : listLiveAgents();
59
+ const liveForRun = allLive.filter(h => h.runId === snapshot.runId);
56
60
  const { completed, total } = snapshot.progress;
57
61
 
58
62
  const lines: string[] = [];
@@ -63,9 +63,17 @@ 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 completedMs = act.completedAtMs || 0;
68
+ const startedMs = act.startedAtMs || 0;
69
+ const nowMs = Date.now();
70
+ const isValidStarted = startedMs > 0 && startedMs < nowMs + 60000 && startedMs > nowMs - 3155692600000;
71
+ const isValidCompleted = completedMs === 0 || (completedMs > 0 && completedMs < nowMs + 60000);
72
+ return (isValidCompleted ? completedMs : nowMs) - (isValidStarted ? startedMs : nowMs);
73
+ }
66
74
  private refreshSummary(): void {
67
75
  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]`;
76
+ const summary = `${LiveConversationOverlay.SUMMARY_PREFIX}[${act.turnCount} turns · ${act.toolUses} tools · ${(this.safeElapsedMs(act) / 1000).toFixed(1)}s]`;
69
77
  const lastLine = this.cachedLines[this.cachedLines.length - 1];
70
78
  if (lastLine?.startsWith(LiveConversationOverlay.SUMMARY_PREFIX)) {
71
79
  this.cachedLines[this.cachedLines.length - 1] = summary;
@@ -100,7 +108,7 @@ export class LiveConversationOverlay {
100
108
  : iconForStatus(this.handle.status);
101
109
  const name = this.handle.agent ?? this.handle.taskId;
102
110
  const act = this.handle.activity;
103
- const elapsed = `${((act.completedAtMs ?? Date.now()) - act.startedAtMs) / 1000}s`;
111
+ const elapsed = `${(this.safeElapsedMs(act) / 1000).toFixed(1)}s`;
104
112
  const headerParts: string[] = [];
105
113
  if (act.maxTurns != null) headerParts.push(`turn ${act.turnCount}/${act.maxTurns}`);
106
114
  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;
@@ -41,6 +41,12 @@ export interface RunDashboardOptions {
41
41
  snapshotCache?: RunSnapshotCache;
42
42
  runProvider?: () => TeamRunManifest[];
43
43
  registry?: MetricRegistry;
44
+ /**
45
+ * Workspace/session ID for filtering runs and live agents. When provided,
46
+ * only runs with matching ownerSessionId and live agents with matching
47
+ * workspaceId are shown. This ensures session isolation in the UI.
48
+ */
49
+ workspaceId?: string;
44
50
  /**
45
51
  * Poke the host TUI to repaint after a state change. Must be wired from
46
52
  * `commands.ts` (`() => requestRenderTarget(tui)`) so keypresses and event-bus
@@ -279,7 +285,12 @@ export class RunDashboard implements DashboardComponent {
279
285
  theme: unknown = {},
280
286
  options: RunDashboardOptions = {},
281
287
  ) {
282
- this.runs = runs;
288
+ // Filter runs by workspaceId for session isolation
289
+ // If workspaceId is provided, only show runs owned by that session or runs with no owner (legacy)
290
+ const filteredRuns = options.workspaceId
291
+ ? runs.filter((run) => !run.ownerSessionId || run.ownerSessionId === options.workspaceId)
292
+ : runs;
293
+ this.runs = filteredRuns;
283
294
  this.done = done;
284
295
  this.theme = asCrewTheme(theme);
285
296
  this.options = options;