pi-crew 0.9.41 → 0.9.44
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/CHANGELOG.md +253 -0
- package/README.md +14 -0
- package/dist/build-meta.json +6450 -12419
- package/dist/index.mjs +57998 -68417
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/agents/agent-config.ts +1 -1
- package/src/config/config.ts +20 -1
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/config/types.ts +0 -2
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/font-detect.ts +16 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +26 -9
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +7 -2
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +69 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/prompt/prompt-runtime.ts +15 -0
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-spawn.ts +234 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +80 -822
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/dwf-state-store.ts +5 -0
- package/src/runtime/dynamic-workflow-context.ts +78 -18
- package/src/runtime/dynamic-workflow-runner.ts +18 -2
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-evaluator.ts +59 -27
- package/src/runtime/goal-loop-runner.ts +46 -13
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +30 -5
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +4 -3
- package/src/runtime/pi-spawn.ts +7 -1
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +18 -23
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +3 -3
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +8 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +4 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/locks.ts +132 -49
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/state/worker-atomic-writer.ts +1 -0
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/paths.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/worktree/worktree-manager.ts +47 -19
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -9,7 +9,8 @@ import { aggregateUsage, formatUsage } from "../state/usage.ts";
|
|
|
9
9
|
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
10
10
|
import { pad, truncate } from "../utils/visual.ts";
|
|
11
11
|
import { Box, Text } from "./layout-primitives.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { RenderScheduler } from "./render-scheduler.ts";
|
|
13
|
+
import { runEventBusAsRenderScheduler } from "./run-event-bus.ts";
|
|
13
14
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
14
15
|
import { spinnerBucket, spinnerFrame } from "./spinner.ts";
|
|
15
16
|
import { colorizeStatusGlyphs, iconForStatus } from "./status-colors.ts";
|
|
@@ -60,7 +61,7 @@ export class LiveRunSidebar {
|
|
|
60
61
|
private readonly theme: CrewTheme;
|
|
61
62
|
private readonly config: CrewUiConfig;
|
|
62
63
|
private readonly unsubscribeTheme: () => void;
|
|
63
|
-
private readonly
|
|
64
|
+
private readonly renderScheduler: RenderScheduler;
|
|
64
65
|
private readonly snapshotCache?: RunSnapshotCache;
|
|
65
66
|
private cachedLines: string[] = [];
|
|
66
67
|
private cachedWidth = 0;
|
|
@@ -83,16 +84,21 @@ export class LiveRunSidebar {
|
|
|
83
84
|
this.config = input.config ?? {};
|
|
84
85
|
this.snapshotCache = input.snapshotCache;
|
|
85
86
|
this.unsubscribeTheme = subscribeThemeChange(input.theme, () => this.invalidate());
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
// 1.10 (UI-P1-1): route run:state / worker:lifecycle / ui:invalidate
|
|
88
|
+
// through a RenderScheduler (debounce + fallback) instead of three
|
|
89
|
+
// direct runEventBus.onChannel subscriptions. With 3 overlays
|
|
90
|
+
// subscribing independently a single event triggered up to 9 callbacks
|
|
91
|
+
// and ~150 invalidates/sec under load. The scheduler collapses bursts
|
|
92
|
+
// into one debounced invalidate.
|
|
93
|
+
this.renderScheduler = new RenderScheduler(
|
|
94
|
+
runEventBusAsRenderScheduler(["run:state", "worker:lifecycle", "ui:invalidate"]),
|
|
95
|
+
() => this.invalidate(),
|
|
96
|
+
{
|
|
97
|
+
debounceMs: 75,
|
|
98
|
+
fallbackMs: 750,
|
|
99
|
+
events: ["run:state", "worker:lifecycle", "ui:invalidate"],
|
|
100
|
+
},
|
|
101
|
+
);
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
private buildSignature(
|
|
@@ -152,34 +158,52 @@ export class LiveRunSidebar {
|
|
|
152
158
|
this.autoCloseTimeout = undefined;
|
|
153
159
|
}
|
|
154
160
|
this.unsubscribeTheme();
|
|
155
|
-
this.
|
|
161
|
+
this.renderScheduler.dispose();
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
render(width: number): string[] {
|
|
159
165
|
const w = Math.max(36, width);
|
|
160
|
-
const loaded = loadRunManifestById(this.cwd, this.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency;
|
|
161
|
-
if (!loaded) {
|
|
162
|
-
return renderLines(
|
|
163
|
-
[
|
|
164
|
-
border("╭", "─", "╮", w),
|
|
165
|
-
line(`${this.theme.fg("accent", "▐")} ${this.theme.bold("pi-crew live sidebar")}`, w),
|
|
166
|
-
line("run not found", w),
|
|
167
|
-
border("╰", "─", "╯", w),
|
|
168
|
-
],
|
|
169
|
-
w,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
166
|
|
|
167
|
+
// P0-6: render from snapshots only — never read disk on every render tick.
|
|
168
|
+
// Production wires a snapshotCache (extension/registration/ui.ts). When
|
|
169
|
+
// the cache hasn't populated yet we paint a single "(loading…)" line so
|
|
170
|
+
// the pre-load frame is well-formed instead of an empty panel. When the
|
|
171
|
+
// cache is undefined (tests/dev) we fall back to direct disk reads so
|
|
172
|
+
// existing unit tests keep working.
|
|
173
|
+
let run: import("../state/types.ts").TeamRunManifest;
|
|
174
|
+
let tasks: TeamTaskState[];
|
|
175
|
+
let rawAgents: ReturnType<typeof readCrewAgents>;
|
|
173
176
|
let snapshot: RunUiSnapshot | undefined;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
if (this.snapshotCache) {
|
|
178
|
+
try {
|
|
179
|
+
snapshot = this.snapshotCache.refreshIfStale(this.runId);
|
|
180
|
+
} catch {
|
|
181
|
+
snapshot = undefined;
|
|
182
|
+
}
|
|
183
|
+
if (!snapshot) {
|
|
184
|
+
return ["(loading…)"];
|
|
185
|
+
}
|
|
186
|
+
run = snapshot.manifest;
|
|
187
|
+
tasks = snapshot.tasks;
|
|
188
|
+
rawAgents = snapshot.agents;
|
|
189
|
+
} else {
|
|
190
|
+
const loaded = loadRunManifestById(this.cwd, this.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency;
|
|
191
|
+
if (!loaded) {
|
|
192
|
+
return renderLines(
|
|
193
|
+
[
|
|
194
|
+
border("╭", "─", "╮", w),
|
|
195
|
+
line(`${this.theme.fg("accent", "▐")} ${this.theme.bold("pi-crew live sidebar")}`, w),
|
|
196
|
+
line("run not found", w),
|
|
197
|
+
border("╰", "─", "╯", w),
|
|
198
|
+
],
|
|
199
|
+
w,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
run = loaded.manifest;
|
|
203
|
+
tasks = readTasks(run.tasksPath);
|
|
204
|
+
rawAgents = readCrewAgents(run);
|
|
178
205
|
}
|
|
179
|
-
const run = snapshot?.manifest ?? loaded.manifest;
|
|
180
|
-
const tasks = snapshot?.tasks ?? readTasks(run.tasksPath);
|
|
181
206
|
const controlConfig = resolveCrewControlConfig({ ui: this.config });
|
|
182
|
-
const rawAgents = snapshot?.agents ?? readCrewAgents(run);
|
|
183
207
|
const agents = rawAgents.map((agent) => applyAttentionState(run, agent, controlConfig));
|
|
184
208
|
const active = agents.filter((agent) => agent.status === "running");
|
|
185
209
|
const completed = agents.filter((agent) => agent.status !== "running").slice(-5);
|
package/src/ui/mascot.ts
CHANGED
|
@@ -307,23 +307,29 @@ export class AnimatedMascot {
|
|
|
307
307
|
const state = this.effectState;
|
|
308
308
|
if (state.phase === undefined || state.glitchFrames === undefined) return true;
|
|
309
309
|
if (state.phase < state.glitchFrames) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
const offset = Math.floor(Math.random() * 7) - 3;
|
|
311
|
+
const swapRow = Math.floor(Math.random() * ARMIN_DISPLAY_HEIGHT);
|
|
312
|
+
const doShift = Math.random() < 0.3;
|
|
313
|
+
const doSwap = !doShift && Math.random() < 0.2;
|
|
314
|
+
for (let row = 0; row < ARMIN_DISPLAY_HEIGHT; row++) {
|
|
315
|
+
const src = doSwap ? this.finalArminGrid[swapRow] : this.finalArminGrid[row];
|
|
316
|
+
for (let x = 0; x < ARMIN_WIDTH; x++) {
|
|
317
|
+
if (doShift) {
|
|
318
|
+
this.currentArminGrid[row][x] = src[(x + offset + ARMIN_WIDTH) % ARMIN_WIDTH];
|
|
319
|
+
} else {
|
|
320
|
+
this.currentArminGrid[row][x] = src[x];
|
|
321
|
+
}
|
|
316
322
|
}
|
|
317
|
-
|
|
318
|
-
const swapRow = Math.floor(Math.random() * ARMIN_DISPLAY_HEIGHT);
|
|
319
|
-
return [...this.finalArminGrid[swapRow]];
|
|
320
|
-
}
|
|
321
|
-
return glitchRow;
|
|
322
|
-
});
|
|
323
|
+
}
|
|
323
324
|
state.phase++;
|
|
324
325
|
return false;
|
|
325
326
|
}
|
|
326
|
-
|
|
327
|
+
// Restore final grid in-place
|
|
328
|
+
for (let row = 0; row < ARMIN_DISPLAY_HEIGHT; row++) {
|
|
329
|
+
for (let x = 0; x < ARMIN_WIDTH; x++) {
|
|
330
|
+
this.currentArminGrid[row][x] = this.finalArminGrid[row][x];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
327
333
|
return true;
|
|
328
334
|
}
|
|
329
335
|
|
|
@@ -10,7 +10,6 @@ import { aggregateUsage } from "../state/usage.ts";
|
|
|
10
10
|
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
11
11
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
12
12
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
13
|
-
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
14
13
|
import { RenderCoalescer } from "./render-coalescer.ts";
|
|
15
14
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
16
15
|
import { notificationBadge } from "./widget/widget-formatters.ts";
|
|
@@ -99,155 +98,16 @@ export function updatePiCrewPowerbar(
|
|
|
99
98
|
notificationCount = 0,
|
|
100
99
|
preloadedManifests?: TeamRunManifest[],
|
|
101
100
|
): void {
|
|
102
|
-
|
|
103
|
-
const useStatusFallback = !hasPowerbarConsumer(events);
|
|
104
|
-
const runs = preloadedManifests ?? (manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20));
|
|
105
|
-
const active = runs
|
|
106
|
-
.map((run) => {
|
|
107
|
-
let snapshot: RunUiSnapshot | undefined;
|
|
108
|
-
try {
|
|
109
|
-
// 1.2: render path is read-only. Use cache.get() only; the background
|
|
110
|
-
// preload loop in register.ts populates entries on its own cadence.
|
|
111
|
-
snapshot = snapshotCache?.get(run.runId);
|
|
112
|
-
} catch (error) {
|
|
113
|
-
logInternalError("powerbar.snapshot", error, run.runId);
|
|
114
|
-
}
|
|
115
|
-
if (snapshot)
|
|
116
|
-
return {
|
|
117
|
-
run: snapshot.manifest,
|
|
118
|
-
agents: snapshot.agents,
|
|
119
|
-
tasks: snapshot.tasks,
|
|
120
|
-
snapshot,
|
|
121
|
-
};
|
|
122
|
-
let agents: ReturnType<typeof readCrewAgents> = [];
|
|
123
|
-
try {
|
|
124
|
-
agents = readCrewAgents(run);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
logInternalError("powerbar.readCrewAgents", error, run.runId);
|
|
127
|
-
}
|
|
128
|
-
return { run, agents, tasks: readTasks(run.tasksPath), snapshot };
|
|
129
|
-
})
|
|
130
|
-
.filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
131
|
-
if (!active.length) {
|
|
132
|
-
lastActiveKey = undefined;
|
|
133
|
-
lastProgressKey = undefined;
|
|
134
|
-
lastStepsKey = undefined;
|
|
135
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
136
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
137
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const agents = active.flatMap((item) => item.agents);
|
|
141
|
-
const tasks = active.flatMap((item) => item.tasks);
|
|
142
|
-
const running = agents.filter((agent) => agent.status === "running").length;
|
|
143
|
-
const waiting = active.reduce(
|
|
144
|
-
(sum, item) =>
|
|
145
|
-
sum +
|
|
146
|
-
(item.snapshot
|
|
147
|
-
? item.snapshot.progress.queued + (item.snapshot.progress.waiting ?? 0)
|
|
148
|
-
: item.tasks.reduce((s, t) => s + (t.status === "queued" || t.status === "waiting" ? 1 : 0), 0)),
|
|
149
|
-
0,
|
|
150
|
-
);
|
|
151
|
-
const completed = active.reduce(
|
|
152
|
-
(sum, item) => sum + (item.snapshot?.progress.completed ?? item.tasks.reduce((s, t) => s + (t.status === "completed" ? 1 : 0), 0)),
|
|
153
|
-
0,
|
|
154
|
-
);
|
|
155
|
-
const total = Math.max(1, active.reduce((sum, item) => sum + (item.snapshot?.progress.total ?? item.tasks.length), 0) || agents.length);
|
|
156
|
-
const usage = aggregateUsage(tasks);
|
|
157
|
-
const snapshotTokens = active.reduce(
|
|
158
|
-
(sum, item) => sum + (item.snapshot ? item.snapshot.usage.tokensIn + item.snapshot.usage.tokensOut : 0),
|
|
159
|
-
0,
|
|
160
|
-
);
|
|
161
|
-
const hasUsage = usage && (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0) > 0;
|
|
162
|
-
const tokenTotal = hasUsage
|
|
163
|
-
? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0)
|
|
164
|
-
: snapshotTokens;
|
|
165
|
-
const model =
|
|
166
|
-
config?.showModel === false
|
|
167
|
-
? undefined
|
|
168
|
-
: agents
|
|
169
|
-
.find((agent) => agent.model)
|
|
170
|
-
?.model?.split("/")
|
|
171
|
-
.at(-1);
|
|
172
|
-
const tokenText = config?.showTokens === false || !tokenTotal ? undefined : compactTokens(tokenTotal);
|
|
173
|
-
const liveRunning = listLiveAgents().filter((a) => a.status === "running").length;
|
|
174
|
-
// Always show consistent status: running count + queued count from live tasks only
|
|
175
|
-
// Avoid snapshot cache for counts to prevent UI jumping
|
|
176
|
-
const runningCount = agents.filter((a) => a.status === "running").length;
|
|
177
|
-
// Count queued/waiting tasks directly from tasks array (not snapshot) for consistency
|
|
178
|
-
const queuedCount = active.reduce(
|
|
179
|
-
(sum, item) => sum + item.tasks.reduce((s, t) => s + (t.status === "queued" || t.status === "waiting" ? 1 : 0), 0),
|
|
180
|
-
0,
|
|
181
|
-
);
|
|
182
|
-
// Format: "1 running", "2 running · 1 queued", "3 queued", "idle"
|
|
183
|
-
const runningLabel = runningCount === 1 ? "1 running" : `${runningCount} running`;
|
|
184
|
-
const queuedLabel = queuedCount === 1 ? "1 queued" : `${queuedCount} queued`;
|
|
185
|
-
const crewStatus =
|
|
186
|
-
runningCount > 0 && queuedCount > 0
|
|
187
|
-
? `${runningLabel} · ${queuedLabel}`
|
|
188
|
-
: runningCount > 0
|
|
189
|
-
? runningLabel
|
|
190
|
-
: queuedCount > 0
|
|
191
|
-
? queuedLabel
|
|
192
|
-
: "idle";
|
|
193
|
-
const liveSuffix = liveRunning > 0 ? ` (${liveRunning} live)` : "";
|
|
194
|
-
const notificationText = notificationBadge(notificationCount);
|
|
195
|
-
// Always show model + tokens as suffix when available (for activePayload consistency)
|
|
196
|
-
const suffixParts = [model, tokenText].filter(Boolean);
|
|
197
|
-
const activeSuffix = suffixParts.length > 0 ? suffixParts.join(" · ") : undefined;
|
|
198
|
-
// Progress always includes token count for consistency
|
|
199
|
-
const progressSuffix = `${completed}/${total}${tokenText ? ` · ${tokenText}` : ""}`;
|
|
200
|
-
// Build complete, always-consistent fallback text AND event payload to prevent UI flickering
|
|
201
|
-
// Both fallback and events must use the SAME format - no conditional display
|
|
202
|
-
// Format: "⚙ 1 running · 1 queued · model · 30k · 0/1" (never changes based on availability)
|
|
203
|
-
const progressPart = `${completed}/${total}`;
|
|
204
|
-
const allParts = [`⚙ ${crewStatus}`, model ?? "", tokenText ?? "", progressPart].filter(Boolean);
|
|
205
|
-
const unifiedText = allParts.join(" · ");
|
|
206
|
-
// activePayload.text includes notification badge for event payload
|
|
207
|
-
const activePayload = {
|
|
208
|
-
id: "pi-crew-active",
|
|
209
|
-
icon: "⚙",
|
|
210
|
-
text: `⚙ ${crewStatus}${liveSuffix}${notificationText}${activeSuffix ? ` · ${activeSuffix}` : ""}`,
|
|
211
|
-
suffix: activeSuffix,
|
|
212
|
-
color: running ? "accent" : "warning",
|
|
213
|
-
} as const;
|
|
214
|
-
const progressPayload = {
|
|
215
|
-
id: "pi-crew-progress",
|
|
216
|
-
text: (active[0]?.run as TeamRunManifest)?.team ?? "crew",
|
|
217
|
-
bar: Math.round((completed / total) * 100),
|
|
218
|
-
suffix: progressSuffix,
|
|
219
|
-
color: completed === total ? "success" : "accent",
|
|
220
|
-
barSegments: 8,
|
|
221
|
-
} as const;
|
|
222
|
-
// Build step progress: "explorer > planner > executor > verifier" with current step highlighted
|
|
223
|
-
const stepsPayload = buildStepsPayload(active, tasks);
|
|
224
|
-
// 1.8: dedup per segment using a key over every visible field. Previously
|
|
225
|
-
// the dedup string only carried text/suffix/running, so changes to `bar`
|
|
226
|
-
// (progress %) or `color` could be swallowed and stale UI emitted again
|
|
227
|
-
// later as a single noisy burst.
|
|
228
|
-
const activeKey = powerbarKey(activePayload);
|
|
229
|
-
const progressKey = powerbarKey(progressPayload);
|
|
230
|
-
const stepsKey = powerbarKey(stepsPayload);
|
|
231
|
-
if (activeKey !== lastActiveKey) {
|
|
232
|
-
lastActiveKey = activeKey;
|
|
233
|
-
safeEmit(events, "powerbar:update", activePayload);
|
|
234
|
-
}
|
|
235
|
-
if (progressKey !== lastProgressKey) {
|
|
236
|
-
lastProgressKey = progressKey;
|
|
237
|
-
safeEmit(events, "powerbar:update", progressPayload);
|
|
238
|
-
}
|
|
239
|
-
if (stepsKey !== lastStepsKey) {
|
|
240
|
-
lastStepsKey = stepsKey;
|
|
241
|
-
safeEmit(events, "powerbar:update", stepsPayload);
|
|
242
|
-
}
|
|
243
|
-
// Never call setStatusFallback - crew-widget manages "pi-crew" status with its own widget format
|
|
244
|
-
// Powerbar only emits events; it does not set status directly
|
|
101
|
+
powerbarPublisher.update(events, cwd, config, manifestCache, snapshotCache, ctx, notificationCount, preloadedManifests);
|
|
245
102
|
}
|
|
246
103
|
|
|
247
|
-
// --- Dedup state
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
104
|
+
// --- Dedup state + coalescer live on a class instance so they survive hot-reload
|
|
105
|
+
// cleanly: previously these were module-level globals (`lastActiveKey`,
|
|
106
|
+
// `latestArgs`, `powerbarCoalescer`) that survived across reloads and could
|
|
107
|
+
// leak between independent sessions. Encapsulating the state on an instance
|
|
108
|
+
// lets `dispose()` tear it down and a fresh `new PowerbarPublisher()` start
|
|
109
|
+
// from clean slate. The module-level functions remain for back-compat and
|
|
110
|
+
// delegate to a process-wide singleton.
|
|
251
111
|
|
|
252
112
|
interface PowerbarPayloadShape {
|
|
253
113
|
id?: string;
|
|
@@ -333,14 +193,239 @@ interface PowerbarUpdateArgs {
|
|
|
333
193
|
preloadedManifests?: TeamRunManifest[];
|
|
334
194
|
}
|
|
335
195
|
|
|
336
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Owns the per-segment dedup keys and the coalescer. Module-level functions
|
|
198
|
+
* (`updatePiCrewPowerbar`, `requestPowerbarUpdate`, etc.) are thin facades
|
|
199
|
+
* over a process-wide singleton so existing call sites and tests keep
|
|
200
|
+
* working without code changes.
|
|
201
|
+
*/
|
|
202
|
+
class PowerbarPublisher {
|
|
203
|
+
#lastActiveKey: string | undefined;
|
|
204
|
+
#lastProgressKey: string | undefined;
|
|
205
|
+
#lastStepsKey: string | undefined;
|
|
206
|
+
#latestArgs: PowerbarUpdateArgs | null = null;
|
|
207
|
+
readonly #coalescer: RenderCoalescer;
|
|
208
|
+
|
|
209
|
+
constructor() {
|
|
210
|
+
this.#coalescer = new RenderCoalescer(() => {
|
|
211
|
+
const a = this.#latestArgs;
|
|
212
|
+
this.#latestArgs = null;
|
|
213
|
+
if (!a) return;
|
|
214
|
+
this.update(a.events, a.cwd, a.config, a.manifestCache, a.snapshotCache, a.ctx, a.notificationCount, a.preloadedManifests);
|
|
215
|
+
}, 200);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
update(
|
|
219
|
+
events: EventBus,
|
|
220
|
+
cwd: string,
|
|
221
|
+
config?: CrewUiConfig,
|
|
222
|
+
manifestCache?: ManifestCache,
|
|
223
|
+
snapshotCache?: RunSnapshotCache,
|
|
224
|
+
ctx?: StatusContext,
|
|
225
|
+
notificationCount = 0,
|
|
226
|
+
preloadedManifests?: TeamRunManifest[],
|
|
227
|
+
): void {
|
|
228
|
+
if (config?.powerbar === false) return;
|
|
229
|
+
const useStatusFallback = !hasPowerbarConsumer(events);
|
|
230
|
+
const runs = preloadedManifests ?? (manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20));
|
|
231
|
+
const active = runs
|
|
232
|
+
.map((run) => {
|
|
233
|
+
let snapshot: RunUiSnapshot | undefined;
|
|
234
|
+
try {
|
|
235
|
+
// 1.2: render path is read-only. Use cache.get() only; the background
|
|
236
|
+
// preload loop in register.ts populates entries on its own cadence.
|
|
237
|
+
snapshot = snapshotCache?.get(run.runId);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logInternalError("powerbar.snapshot", error, run.runId);
|
|
240
|
+
}
|
|
241
|
+
if (snapshot)
|
|
242
|
+
return {
|
|
243
|
+
run: snapshot.manifest,
|
|
244
|
+
agents: snapshot.agents,
|
|
245
|
+
tasks: snapshot.tasks,
|
|
246
|
+
snapshot,
|
|
247
|
+
};
|
|
248
|
+
let agents: ReturnType<typeof readCrewAgents> = [];
|
|
249
|
+
try {
|
|
250
|
+
agents = readCrewAgents(run);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logInternalError("powerbar.readCrewAgents", error, run.runId);
|
|
253
|
+
}
|
|
254
|
+
return { run, agents, tasks: readTasks(run.tasksPath), snapshot };
|
|
255
|
+
})
|
|
256
|
+
.filter((item) => isDisplayActiveRun(item.run, item.agents));
|
|
257
|
+
if (!active.length) {
|
|
258
|
+
this.#lastActiveKey = undefined;
|
|
259
|
+
this.#lastProgressKey = undefined;
|
|
260
|
+
this.#lastStepsKey = undefined;
|
|
261
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
262
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
263
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const agents = active.flatMap((item) => item.agents);
|
|
267
|
+
const tasks = active.flatMap((item) => item.tasks);
|
|
268
|
+
const running = agents.filter((agent) => agent.status === "running").length;
|
|
269
|
+
const waiting = active.reduce(
|
|
270
|
+
(sum, item) =>
|
|
271
|
+
sum +
|
|
272
|
+
(item.snapshot
|
|
273
|
+
? item.snapshot.progress.queued + (item.snapshot.progress.waiting ?? 0)
|
|
274
|
+
: item.tasks.reduce((s, t) => s + (t.status === "queued" || t.status === "waiting" ? 1 : 0), 0)),
|
|
275
|
+
0,
|
|
276
|
+
);
|
|
277
|
+
const completed = active.reduce(
|
|
278
|
+
(sum, item) =>
|
|
279
|
+
sum + (item.snapshot?.progress.completed ?? item.tasks.reduce((s, t) => s + (t.status === "completed" ? 1 : 0), 0)),
|
|
280
|
+
0,
|
|
281
|
+
);
|
|
282
|
+
const total = Math.max(
|
|
283
|
+
1,
|
|
284
|
+
active.reduce((sum, item) => sum + (item.snapshot?.progress.total ?? item.tasks.length), 0) || agents.length,
|
|
285
|
+
);
|
|
286
|
+
const usage = aggregateUsage(tasks);
|
|
287
|
+
const snapshotTokens = active.reduce(
|
|
288
|
+
(sum, item) => sum + (item.snapshot ? item.snapshot.usage.tokensIn + item.snapshot.usage.tokensOut : 0),
|
|
289
|
+
0,
|
|
290
|
+
);
|
|
291
|
+
const hasUsage = usage && (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0) > 0;
|
|
292
|
+
const tokenTotal = hasUsage
|
|
293
|
+
? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0)
|
|
294
|
+
: snapshotTokens;
|
|
295
|
+
const model =
|
|
296
|
+
config?.showModel === false
|
|
297
|
+
? undefined
|
|
298
|
+
: agents
|
|
299
|
+
.find((agent) => agent.model)
|
|
300
|
+
?.model?.split("/")
|
|
301
|
+
.at(-1);
|
|
302
|
+
const tokenText = config?.showTokens === false || !tokenTotal ? undefined : compactTokens(tokenTotal);
|
|
303
|
+
const liveRunning = listLiveAgents().filter((a) => a.status === "running").length;
|
|
304
|
+
// Always show consistent status: running count + queued count from live tasks only
|
|
305
|
+
// Avoid snapshot cache for counts to prevent UI jumping
|
|
306
|
+
const runningCount = agents.filter((a) => a.status === "running").length;
|
|
307
|
+
// Count queued/waiting tasks directly from tasks array (not snapshot) for consistency
|
|
308
|
+
const queuedCount = active.reduce(
|
|
309
|
+
(sum, item) => sum + item.tasks.reduce((s, t) => s + (t.status === "queued" || t.status === "waiting" ? 1 : 0), 0),
|
|
310
|
+
0,
|
|
311
|
+
);
|
|
312
|
+
// Format: "1 running", "2 running · 1 queued", "3 queued", "idle"
|
|
313
|
+
const runningLabel = runningCount === 1 ? "1 running" : `${runningCount} running`;
|
|
314
|
+
const queuedLabel = queuedCount === 1 ? "1 queued" : `${queuedCount} queued`;
|
|
315
|
+
const crewStatus =
|
|
316
|
+
runningCount > 0 && queuedCount > 0
|
|
317
|
+
? `${runningLabel} · ${queuedLabel}`
|
|
318
|
+
: runningCount > 0
|
|
319
|
+
? runningLabel
|
|
320
|
+
: queuedCount > 0
|
|
321
|
+
? queuedLabel
|
|
322
|
+
: "idle";
|
|
323
|
+
const liveSuffix = liveRunning > 0 ? ` (${liveRunning} live)` : "";
|
|
324
|
+
const notificationText = notificationBadge(notificationCount);
|
|
325
|
+
// Always show model + tokens as suffix when available (for activePayload consistency)
|
|
326
|
+
const suffixParts = [model, tokenText].filter(Boolean);
|
|
327
|
+
const activeSuffix = suffixParts.length > 0 ? suffixParts.join(" · ") : undefined;
|
|
328
|
+
// Progress always includes token count for consistency
|
|
329
|
+
const progressSuffix = `${completed}/${total}${tokenText ? ` · ${tokenText}` : ""}`;
|
|
330
|
+
// Build complete, always-consistent fallback text AND event payload to prevent UI flickering
|
|
331
|
+
// Both fallback and events must use the SAME format - no conditional display
|
|
332
|
+
// Format: "⚙ 1 running · 1 queued · model · 30k · 0/1" (never changes based on availability)
|
|
333
|
+
const progressPart = `${completed}/${total}`;
|
|
334
|
+
const allParts = [`⚙ ${crewStatus}`, model ?? "", tokenText ?? "", progressPart].filter(Boolean);
|
|
335
|
+
const unifiedText = allParts.join(" · ");
|
|
336
|
+
// activePayload.text includes notification badge for event payload
|
|
337
|
+
const activePayload = {
|
|
338
|
+
id: "pi-crew-active",
|
|
339
|
+
icon: "⚙",
|
|
340
|
+
text: `⚙ ${crewStatus}${liveSuffix}${notificationText}${activeSuffix ? ` · ${activeSuffix}` : ""}`,
|
|
341
|
+
suffix: activeSuffix,
|
|
342
|
+
color: running ? "accent" : "warning",
|
|
343
|
+
} as const;
|
|
344
|
+
const progressPayload = {
|
|
345
|
+
id: "pi-crew-progress",
|
|
346
|
+
text: (active[0]?.run as TeamRunManifest)?.team ?? "crew",
|
|
347
|
+
bar: Math.round((completed / total) * 100),
|
|
348
|
+
suffix: progressSuffix,
|
|
349
|
+
color: completed === total ? "success" : "accent",
|
|
350
|
+
barSegments: 8,
|
|
351
|
+
} as const;
|
|
352
|
+
// Build step progress: "explorer > planner > executor > verifier" with current step highlighted
|
|
353
|
+
const stepsPayload = buildStepsPayload(active, tasks);
|
|
354
|
+
// 1.8: dedup per segment using a key over every visible field. Previously
|
|
355
|
+
// the dedup string only carried text/suffix/running, so changes to `bar`
|
|
356
|
+
// (progress %) or `color` could be swallowed and stale UI emitted again
|
|
357
|
+
// later as a single noisy burst.
|
|
358
|
+
const activeKey = powerbarKey(activePayload);
|
|
359
|
+
const progressKey = powerbarKey(progressPayload);
|
|
360
|
+
const stepsKey = powerbarKey(stepsPayload);
|
|
361
|
+
if (activeKey !== this.#lastActiveKey) {
|
|
362
|
+
this.#lastActiveKey = activeKey;
|
|
363
|
+
safeEmit(events, "powerbar:update", activePayload);
|
|
364
|
+
}
|
|
365
|
+
if (progressKey !== this.#lastProgressKey) {
|
|
366
|
+
this.#lastProgressKey = progressKey;
|
|
367
|
+
safeEmit(events, "powerbar:update", progressPayload);
|
|
368
|
+
}
|
|
369
|
+
if (stepsKey !== this.#lastStepsKey) {
|
|
370
|
+
this.#lastStepsKey = stepsKey;
|
|
371
|
+
safeEmit(events, "powerbar:update", stepsPayload);
|
|
372
|
+
}
|
|
373
|
+
// Never call setStatusFallback - crew-widget manages "pi-crew" status with its own widget format
|
|
374
|
+
// Powerbar only emits events; it does not set status directly
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Request a coalesced powerbar update. Multiple rapid calls are batched
|
|
379
|
+
* into a single render pass within 200ms, preventing UI flicker from
|
|
380
|
+
* event bursts.
|
|
381
|
+
*/
|
|
382
|
+
request(
|
|
383
|
+
events: EventBus,
|
|
384
|
+
cwd: string,
|
|
385
|
+
config?: CrewUiConfig,
|
|
386
|
+
manifestCache?: ManifestCache,
|
|
387
|
+
snapshotCache?: RunSnapshotCache,
|
|
388
|
+
ctx?: StatusContext,
|
|
389
|
+
notificationCount = 0,
|
|
390
|
+
preloadedManifests?: TeamRunManifest[],
|
|
391
|
+
): void {
|
|
392
|
+
if (config?.powerbar === false) return;
|
|
393
|
+
this.#latestArgs = {
|
|
394
|
+
events,
|
|
395
|
+
cwd,
|
|
396
|
+
config,
|
|
397
|
+
manifestCache,
|
|
398
|
+
snapshotCache,
|
|
399
|
+
ctx,
|
|
400
|
+
notificationCount,
|
|
401
|
+
preloadedManifests,
|
|
402
|
+
};
|
|
403
|
+
this.#coalescer.request();
|
|
404
|
+
}
|
|
337
405
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
406
|
+
clear(events: EventBus): void {
|
|
407
|
+
this.#lastActiveKey = undefined;
|
|
408
|
+
this.#lastProgressKey = undefined;
|
|
409
|
+
this.#lastStepsKey = undefined;
|
|
410
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
411
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
412
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Reset dedup state on session lifecycle events. */
|
|
416
|
+
resetDedupState(): void {
|
|
417
|
+
this.#lastActiveKey = undefined;
|
|
418
|
+
this.#lastProgressKey = undefined;
|
|
419
|
+
this.#lastStepsKey = undefined;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
dispose(): void {
|
|
423
|
+
this.#coalescer.flush();
|
|
424
|
+
this.#coalescer.dispose();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const powerbarPublisher = new PowerbarPublisher();
|
|
344
429
|
|
|
345
430
|
/**
|
|
346
431
|
* Request a coalesced powerbar update. Multiple rapid calls are batched into a single
|
|
@@ -356,38 +441,19 @@ export function requestPowerbarUpdate(
|
|
|
356
441
|
notificationCount = 0,
|
|
357
442
|
preloadedManifests?: TeamRunManifest[],
|
|
358
443
|
): void {
|
|
359
|
-
|
|
360
|
-
latestArgs = {
|
|
361
|
-
events,
|
|
362
|
-
cwd,
|
|
363
|
-
config,
|
|
364
|
-
manifestCache,
|
|
365
|
-
snapshotCache,
|
|
366
|
-
ctx,
|
|
367
|
-
notificationCount,
|
|
368
|
-
preloadedManifests,
|
|
369
|
-
};
|
|
370
|
-
powerbarCoalescer.request();
|
|
444
|
+
powerbarPublisher.request(events, cwd, config, manifestCache, snapshotCache, ctx, notificationCount, preloadedManifests);
|
|
371
445
|
}
|
|
372
446
|
|
|
373
447
|
/** Dispose the powerbar coalescer. Call during extension cleanup. */
|
|
374
448
|
export function disposePowerbarCoalescer(): void {
|
|
375
|
-
|
|
376
|
-
powerbarCoalescer.dispose();
|
|
449
|
+
powerbarPublisher.dispose();
|
|
377
450
|
}
|
|
378
451
|
|
|
379
452
|
export function clearPiCrewPowerbar(events: EventBus): void {
|
|
380
|
-
|
|
381
|
-
lastProgressKey = undefined;
|
|
382
|
-
lastStepsKey = undefined;
|
|
383
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
384
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
385
|
-
safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
|
|
453
|
+
powerbarPublisher.clear(events);
|
|
386
454
|
}
|
|
387
455
|
|
|
388
456
|
/** Reset dedup state on session lifecycle events. */
|
|
389
457
|
export function resetPowerbarDedupState(): void {
|
|
390
|
-
|
|
391
|
-
lastProgressKey = undefined;
|
|
392
|
-
lastStepsKey = undefined;
|
|
458
|
+
powerbarPublisher.resetDedupState();
|
|
393
459
|
}
|