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 +1 -1
- package/src/extension/registration/commands.ts +4 -2
- package/src/ui/crew-widget.ts +22 -5
- package/src/ui/dashboard-panes/agents-pane.ts +6 -2
- package/src/ui/live-conversation-overlay.ts +10 -2
- package/src/ui/live-run-sidebar.ts +20 -0
- package/src/ui/run-dashboard.ts +12 -1
package/package.json
CHANGED
|
@@ -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:
|
|
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") {
|
package/src/ui/crew-widget.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 · ${(
|
|
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 = `${(
|
|
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;
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|