pi-crew 0.10.6 → 0.11.1
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 +215 -0
- package/README.md +0 -1
- package/dist/index.mjs +30623 -29844
- package/package.json +170 -169
- package/schema.json +10 -0
- package/scripts/watch-agent-transcript.mjs +120 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +181 -1
- package/src/config/config-validation.ts +0 -2
- package/src/config/defaults.ts +0 -1
- package/src/config/types.ts +0 -5
- package/src/extension/crew-input-router.ts +2 -0
- package/src/extension/crew-vibes/config.ts +1 -77
- package/src/extension/crew-vibes/index.ts +36 -330
- package/src/extension/crew-vibes/render.ts +1 -106
- package/src/extension/help.ts +1 -2
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-registration.ts +5 -1
- package/src/extension/registration/commands/manage.ts +5 -4
- package/src/extension/registration/commands/schedules.ts +212 -0
- package/src/extension/registration/commands/shared.ts +56 -2
- package/src/extension/registration/lifecycle-handlers.ts +16 -2
- package/src/extension/registration/schedule-toast-bridge.ts +109 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +72 -1
- package/src/extension/team-tool/handle-schedule.ts +120 -1
- package/src/extension/team-tool/handle-settings.ts +14 -2
- package/src/runtime/scheduling/scheduler.ts +120 -6
- package/src/runtime/settings-store.ts +28 -2
- package/src/runtime/task-display.ts +1 -1
- package/src/schema/config-schema.ts +4 -0
- package/src/state/coordination/locks.ts +17 -2
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +791 -0
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +52 -17
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +177 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/inline-panel/crew-editor.ts +32 -9
- package/src/ui/inline-panel/index.ts +66 -4
- package/src/ui/key-utils.ts +3 -0
- package/src/ui/keybinding-map.ts +286 -34
- package/src/ui/live-conversation-overlay.ts +176 -46
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +34 -17
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -28
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -49
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +344 -72
- package/src/ui/settings-overlay.ts +84 -42
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +126 -8
- package/src/ui/tool-renderers/brief-mode.ts +8 -66
- package/src/ui/tool-renderers/index.ts +372 -521
- package/src/ui/transcript-viewer.ts +33 -14
- package/src/ui/widget/index.ts +86 -133
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -399
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +298 -265
- package/src/ui/widget/widget-types.ts +0 -9
- package/src/utils/relative-time.ts +33 -0
- package/src/utils/visual.ts +0 -4
- package/src/extension/crew-vibes/figures.ts +0 -101
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/extension/crew-vibes/footer.ts +0 -313
- package/src/extension/crew-vibes/speed.ts +0 -286
- package/src/ui/dock-footer.ts +0 -49
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { requestRender, setFooter } from "../../ui/pi-ui-compat.ts";
|
|
3
2
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Wrap a pi UI call in try/catch. If the call throws (e.g.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Crew-vibes must NEVER break the user's session. A broken spinner is not
|
|
12
|
-
* worth a crashed pi.
|
|
5
|
+
* Wrap a pi UI call in try/catch. If the call throws (e.g. theme helper
|
|
6
|
+
* returns undefined or pi's API signature changed), log it and silently fall
|
|
7
|
+
* back to pi's default behavior. Crew-vibes must NEVER break the user's
|
|
8
|
+
* session.
|
|
13
9
|
*/
|
|
14
10
|
function safeUiCall(scope: string, fn: () => void): void {
|
|
15
11
|
try {
|
|
@@ -19,155 +15,37 @@ function safeUiCall(scope: string, fn: () => void): void {
|
|
|
19
15
|
}
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
import { setFooterDockSinkActive } from "../../ui/dock-footer.ts";
|
|
23
18
|
import { type CrewVibesConfig, loadConfig, saveConfig } from "./config.ts";
|
|
24
|
-
import { intervalForSpeed } from "./figures.ts";
|
|
25
|
-
import { type CrewVibesFooterSource, createCrewVibesFooter } from "./footer.ts";
|
|
26
19
|
import { clearProviderUsageCache, fetchProviderUsage, type ProviderUsage } from "./provider-usage.ts";
|
|
27
|
-
import {
|
|
28
|
-
asCrewTheme,
|
|
29
|
-
clearVibesStatus,
|
|
30
|
-
crewIndicatorFrames,
|
|
31
|
-
formatSpeed,
|
|
32
|
-
getCapacityUsage,
|
|
33
|
-
renderSpeedFooter,
|
|
34
|
-
renderWorkingMessage,
|
|
35
|
-
setSpeedStatus,
|
|
36
|
-
} from "./render.ts";
|
|
37
|
-
import { SpeedAnimator, SpeedTracker } from "./speed.ts";
|
|
38
|
-
|
|
39
|
-
export const CREW_VIBES_STATUS_KEY = "pi-crew-vibes";
|
|
40
|
-
|
|
41
|
-
function isAssistantMessage(message: unknown): boolean {
|
|
42
|
-
return typeof message === "object" && message !== null && (message as { role?: string }).role === "assistant";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function assistantUsageOutput(message: unknown): number | undefined {
|
|
46
|
-
const usage = (message as { usage?: { output?: unknown } }).usage;
|
|
47
|
-
const output = usage?.output;
|
|
48
|
-
return typeof output === "number" && Number.isFinite(output) ? output : undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function assistantStopReason(message: unknown): string | undefined {
|
|
52
|
-
const reason = (message as { stopReason?: unknown }).stopReason;
|
|
53
|
-
return typeof reason === "string" ? reason : undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function assistantEventType(event: unknown): string | undefined {
|
|
57
|
-
return typeof (event as { type?: string }).type === "string" ? (event as { type: string }).type : undefined;
|
|
58
|
-
}
|
|
20
|
+
import { asCrewTheme, clearVibesStatus, renderProviderUsage, setProviderStatus } from "./render.ts";
|
|
59
21
|
|
|
22
|
+
/**
|
|
23
|
+
* crew-vibes — provider rate-limit quota publisher.
|
|
24
|
+
*
|
|
25
|
+
* Maintainer decisions 2026-09-13/14 (UI review follow-ups) stripped this
|
|
26
|
+
* module down to its one remaining job: fetching the current provider's
|
|
27
|
+
* rate-limit usage and publishing it as a STATUS entry on pi's NATIVE
|
|
28
|
+
* footer. Removed entirely: the custom footer replacement (it had drifted
|
|
29
|
+
* from native) and the tok/s speed UI (custom spinner + "Working N tok/s")
|
|
30
|
+
* — pi's built-ins are always used.
|
|
31
|
+
*/
|
|
60
32
|
export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
61
33
|
let config: CrewVibesConfig = loadConfig();
|
|
62
|
-
let lastRenderedAt = 0;
|
|
63
|
-
let currentIntervalMs = 0;
|
|
64
|
-
const speedTracker = new SpeedTracker(config.speed);
|
|
65
|
-
const footerAnimator = new SpeedAnimator(config.speed.renderIntervalMs);
|
|
66
|
-
let liveTimer: ReturnType<typeof setInterval> | undefined;
|
|
67
|
-
let footerTimer: ReturnType<typeof setInterval> | undefined;
|
|
68
|
-
let capacityTimer: ReturnType<typeof setInterval> | undefined;
|
|
69
34
|
let providerTimer: ReturnType<typeof setInterval> | undefined;
|
|
70
35
|
let lastProviderUsage: ProviderUsage | null = null;
|
|
71
36
|
let currentProvider: string | undefined;
|
|
72
|
-
let currentThinkingLevel: string | undefined;
|
|
73
37
|
|
|
74
38
|
function themeOf(ctx: ExtensionContext) {
|
|
75
39
|
return asCrewTheme(ctx.hasUI ? ctx.ui.theme : undefined);
|
|
76
40
|
}
|
|
77
41
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/** Whether the custom footer has crew-vibes meters to add on top of pi's stats. */
|
|
85
|
-
function metersActive(): boolean {
|
|
86
|
-
return config.enabled && (config.capacity.enabled || config.capacity.providerUsage);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Install our custom footer, or restore pi's built-in footer when idle.
|
|
90
|
-
* Unlike setStatus (joined + right-truncated), the footer's render(width)
|
|
91
|
-
* gets the real width and owns line layout, so the quota is never chopped. */
|
|
92
|
-
function installFooter(ctx: ExtensionContext): void {
|
|
93
|
-
if (!ctx?.hasUI) return;
|
|
94
|
-
if (!metersActive()) {
|
|
95
|
-
safeUiCall("clear-footer", () => {
|
|
96
|
-
// The crew dock can no longer render at the very bottom — the
|
|
97
|
-
// widget falls back to pi's belowEditor slot automatically.
|
|
98
|
-
setFooterDockSinkActive(false);
|
|
99
|
-
setFooter(ctx, undefined);
|
|
100
|
-
});
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
safeUiCall("install-footer", () => {
|
|
104
|
-
setFooter(ctx, (tui, theme, footerData) => createCrewVibesFooter({ tui, theme, footerData, ctx, source: footerSource }));
|
|
105
|
-
// The footer can now host the crew dock below the quota lines.
|
|
106
|
-
setFooterDockSinkActive(true);
|
|
107
|
-
requestRender(ctx);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Trigger a footer repaint; the footer recomputes capacity/quota on render.
|
|
112
|
-
*
|
|
113
|
-
* Wrapped wholly in safeUiCall because fetchProviderAndRefresh is async: after
|
|
114
|
-
* its `await` the session may have shut down (session_shutdown clears the
|
|
115
|
-
* timers, but an in-flight fetchProviderAndRefresh still resumes), making ctx
|
|
116
|
-
* stale. Accessing the `hasUI` getter on a stale ctx throws — catch it so
|
|
117
|
-
* crew-vibes never crashes pi. Matches the file's core philosophy: "must
|
|
118
|
-
* NEVER break the user's session". */
|
|
119
|
-
function refreshFooter(ctx: ExtensionContext): void {
|
|
120
|
-
safeUiCall("refresh-footer", () => {
|
|
121
|
-
if (ctx?.hasUI) requestRender(ctx);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function publishSpeedFooter(ctx: ExtensionContext, speed = footerAnimator.value()): void {
|
|
126
|
-
if (!config.enabled || !config.speed.enabled || !config.speed.footer) {
|
|
127
|
-
safeUiCall("clear-speed-status", () => setSpeedStatus(ctx, config, undefined));
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
safeUiCall("publish-speed-status", () => setSpeedStatus(ctx, config, renderSpeedFooter(themeOf(ctx), config.speed, speed)));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function applyIndicator(ctx: ExtensionContext, speed: number | null, force = false): void {
|
|
134
|
-
if (!ctx.hasUI || !ctx.ui.setWorkingIndicator) return;
|
|
135
|
-
if (!config.enabled || !config.speed.enabled || !config.speed.indicator) {
|
|
136
|
-
safeUiCall("reset-indicator", () => ctx.ui.setWorkingIndicator?.());
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const next = intervalForSpeed(config.speed, speed);
|
|
140
|
-
if (!force && Math.abs(next - currentIntervalMs) < 10) return;
|
|
141
|
-
safeUiCall("set-indicator", () =>
|
|
142
|
-
ctx.ui.setWorkingIndicator?.({
|
|
143
|
-
frames: crewIndicatorFrames(themeOf(ctx)),
|
|
144
|
-
intervalMs: next,
|
|
145
|
-
}),
|
|
42
|
+
/** Publish the provider rate-limit quota as a STATUS entry (joined into
|
|
43
|
+
* pi's NATIVE footer status line; right-truncates on very narrow
|
|
44
|
+
* terminals — pi's documented behavior, accepted). */
|
|
45
|
+
function publishQuotaStatus(ctx: ExtensionContext): void {
|
|
46
|
+
safeUiCall("publish-quota-status", () =>
|
|
47
|
+
setProviderStatus(ctx, config, lastProviderUsage ? renderProviderUsage(themeOf(ctx), lastProviderUsage) : undefined),
|
|
146
48
|
);
|
|
147
|
-
currentIntervalMs = next;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function renderWorking(ctx: ExtensionContext, speed: number | null): void {
|
|
151
|
-
if (!config.enabled || !config.speed.enabled || !ctx.hasUI) return;
|
|
152
|
-
safeUiCall("set-working-message", () => ctx.ui.setWorkingMessage?.(renderWorkingMessage(themeOf(ctx), config.speed, speed)));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function stopLiveTimer(): void {
|
|
156
|
-
if (!liveTimer) return;
|
|
157
|
-
clearInterval(liveTimer);
|
|
158
|
-
liveTimer = undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function stopFooterTimer(): void {
|
|
162
|
-
if (!footerTimer) return;
|
|
163
|
-
clearInterval(footerTimer);
|
|
164
|
-
footerTimer = undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function stopCapacityTimer(): void {
|
|
168
|
-
if (!capacityTimer) return;
|
|
169
|
-
clearInterval(capacityTimer);
|
|
170
|
-
capacityTimer = undefined;
|
|
171
49
|
}
|
|
172
50
|
|
|
173
51
|
function stopProviderTimer(): void {
|
|
@@ -176,48 +54,14 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
176
54
|
providerTimer = undefined;
|
|
177
55
|
}
|
|
178
56
|
|
|
179
|
-
|
|
180
|
-
if (liveTimer || !ctx.hasUI) return;
|
|
181
|
-
liveTimer = setInterval(() => {
|
|
182
|
-
if (!config.enabled || !config.speed.enabled || !speedTracker.isStreaming) {
|
|
183
|
-
stopLiveTimer();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const speed = speedTracker.liveTokS();
|
|
187
|
-
applyIndicator(ctx, speed);
|
|
188
|
-
renderWorking(ctx, speed);
|
|
189
|
-
}, config.speed.renderIntervalMs);
|
|
190
|
-
liveTimer.unref?.();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function startFooterTimer(ctx: ExtensionContext): void {
|
|
194
|
-
if (footerTimer || !ctx.hasUI) return;
|
|
195
|
-
footerTimer = setInterval(() => {
|
|
196
|
-
if (!config.enabled || !config.speed.enabled) {
|
|
197
|
-
stopFooterTimer();
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
publishSpeedFooter(ctx);
|
|
201
|
-
if (!footerAnimator.isAnimating()) stopFooterTimer();
|
|
202
|
-
}, config.speed.renderIntervalMs);
|
|
203
|
-
footerTimer.unref?.();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function startCapacityTimer(ctx: ExtensionContext): void {
|
|
207
|
-
if (capacityTimer) return;
|
|
208
|
-
const interval = Math.max(250, config.capacity.refreshIntervalMs);
|
|
209
|
-
capacityTimer = setInterval(() => refreshFooter(ctx), interval);
|
|
210
|
-
capacityTimer.unref?.();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Fetch provider usage for currentProvider and refresh the footer.
|
|
57
|
+
/** Fetch provider usage for currentProvider and publish the quota status.
|
|
214
58
|
* Called on start, on each timer tick, and immediately when the
|
|
215
59
|
* provider changes (model_select) so the quota reflects the new
|
|
216
|
-
* provider without waiting for the next
|
|
60
|
+
* provider without waiting for the next tick. */
|
|
217
61
|
async function fetchProviderAndRefresh(ctx: ExtensionContext): Promise<void> {
|
|
218
62
|
if (!config.enabled || !config.capacity.providerUsage) {
|
|
219
63
|
lastProviderUsage = null;
|
|
220
|
-
|
|
64
|
+
publishQuotaStatus(ctx);
|
|
221
65
|
return;
|
|
222
66
|
}
|
|
223
67
|
try {
|
|
@@ -226,7 +70,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
226
70
|
// Never crash on provider fetch failure
|
|
227
71
|
lastProviderUsage = null;
|
|
228
72
|
}
|
|
229
|
-
|
|
73
|
+
publishQuotaStatus(ctx);
|
|
230
74
|
}
|
|
231
75
|
|
|
232
76
|
function startProviderTimer(ctx: ExtensionContext): void {
|
|
@@ -239,204 +83,66 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
239
83
|
providerTimer.unref?.();
|
|
240
84
|
}
|
|
241
85
|
|
|
242
|
-
function resetWorking(ctx: ExtensionContext): void {
|
|
243
|
-
applyIndicator(ctx, null, true);
|
|
244
|
-
renderWorking(ctx, speedTracker.lastTokS);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
86
|
function applyConfig(ctx: ExtensionContext): void {
|
|
248
87
|
saveConfig(config);
|
|
249
|
-
speedTracker.updateConfig(config.speed);
|
|
250
|
-
footerAnimator.updateDuration(config.speed.renderIntervalMs);
|
|
251
88
|
if (!config.enabled) {
|
|
252
|
-
stopLiveTimer();
|
|
253
|
-
stopFooterTimer();
|
|
254
|
-
stopCapacityTimer();
|
|
255
89
|
stopProviderTimer();
|
|
256
|
-
setFooterDockSinkActive(false);
|
|
257
|
-
setFooter(ctx, undefined);
|
|
258
90
|
clearVibesStatus(ctx);
|
|
259
91
|
return;
|
|
260
92
|
}
|
|
261
|
-
installFooter(ctx);
|
|
262
|
-
publishSpeedFooter(ctx);
|
|
263
|
-
startCapacityTimer(ctx);
|
|
264
93
|
if (config.capacity.providerUsage) startProviderTimer(ctx);
|
|
94
|
+
else publishQuotaStatus(ctx); // clears the stale status when disabled
|
|
265
95
|
}
|
|
266
96
|
|
|
267
97
|
pi.on("session_start", (_event, ctx) => {
|
|
268
|
-
stopLiveTimer();
|
|
269
|
-
stopFooterTimer();
|
|
270
|
-
stopCapacityTimer();
|
|
271
98
|
stopProviderTimer();
|
|
272
99
|
config = loadConfig();
|
|
273
|
-
speedTracker.updateConfig(config.speed);
|
|
274
|
-
footerAnimator.updateDuration(config.speed.renderIntervalMs);
|
|
275
|
-
speedTracker.resetSession();
|
|
276
|
-
footerAnimator.reset(null);
|
|
277
100
|
clearProviderUsageCache();
|
|
278
101
|
// Initialize provider from current model — model_select only fires on manual switch
|
|
279
102
|
currentProvider = (ctx.model as { provider?: string } | undefined)?.provider;
|
|
280
|
-
currentThinkingLevel = undefined;
|
|
281
103
|
if (!config.enabled) {
|
|
282
|
-
setFooter(ctx, undefined);
|
|
283
104
|
clearVibesStatus(ctx);
|
|
284
105
|
return;
|
|
285
106
|
}
|
|
286
|
-
installFooter(ctx);
|
|
287
|
-
publishSpeedFooter(ctx);
|
|
288
|
-
startCapacityTimer(ctx);
|
|
289
107
|
startProviderTimer(ctx);
|
|
290
|
-
// Set the working indicator early (matches pi's official working-indicator
|
|
291
|
-
// example) so pi has the custom frames configured before streaming begins.
|
|
292
|
-
// Calls before the loading animation exists are ignored, so we re-apply on
|
|
293
|
-
// agent_start/message_update as well.
|
|
294
|
-
applyIndicator(ctx, null, true);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
298
|
-
if (!config.enabled) return;
|
|
299
|
-
resetWorking(ctx);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
pi.on("turn_start", (_event, ctx) => {
|
|
303
|
-
if (!config.enabled) return;
|
|
304
|
-
resetWorking(ctx);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
pi.on("message_start", (event, ctx) => {
|
|
308
|
-
if (!config.enabled || !config.speed.enabled || !isAssistantMessage(event.message)) return;
|
|
309
|
-
speedTracker.startMessage();
|
|
310
|
-
footerAnimator.reset(speedTracker.lastTokS);
|
|
311
|
-
startLiveTimer(ctx);
|
|
312
|
-
lastRenderedAt = 0;
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
pi.on("message_update", (event, ctx) => {
|
|
316
|
-
if (!config.enabled || !config.speed.enabled || !isAssistantMessage(event.message) || !speedTracker.isStreaming) return;
|
|
317
|
-
|
|
318
|
-
const ev = event.assistantMessageEvent;
|
|
319
|
-
const type = assistantEventType(ev);
|
|
320
|
-
if (type === "text_delta" || type === "thinking_delta") {
|
|
321
|
-
const delta = (ev as { delta?: string }).delta ?? "";
|
|
322
|
-
speedTracker.recordDelta(delta, assistantUsageOutput(event.message));
|
|
323
|
-
}
|
|
324
|
-
if (type === "start") resetWorking(ctx);
|
|
325
|
-
|
|
326
|
-
const now = Date.now();
|
|
327
|
-
if (now - lastRenderedAt < config.speed.renderIntervalMs && type !== "done") return;
|
|
328
|
-
lastRenderedAt = now;
|
|
329
|
-
|
|
330
|
-
const speed = speedTracker.liveTokS();
|
|
331
|
-
applyIndicator(ctx, speed);
|
|
332
|
-
renderWorking(ctx, speed);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
pi.on("message_end", (event, ctx) => {
|
|
336
|
-
if (!isAssistantMessage(event.message)) return;
|
|
337
|
-
refreshFooter(ctx);
|
|
338
|
-
if (!config.enabled || !config.speed.enabled || !speedTracker.isStreaming) return;
|
|
339
|
-
|
|
340
|
-
const completed = speedTracker.finishMessage(assistantUsageOutput(event.message) ?? 0, assistantStopReason(event.message));
|
|
341
|
-
if (!completed) return;
|
|
342
|
-
|
|
343
|
-
footerAnimator.setTarget(speedTracker.sessionAvgTokS());
|
|
344
|
-
publishSpeedFooter(ctx);
|
|
345
|
-
startFooterTimer(ctx);
|
|
346
|
-
applyIndicator(ctx, speedTracker.lastTokS);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
pi.on("turn_end", () => {
|
|
350
|
-
speedTracker.stopMessage();
|
|
351
|
-
stopLiveTimer();
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
pi.on("agent_end", (_event, ctx) => {
|
|
355
|
-
speedTracker.stopMessage();
|
|
356
|
-
stopLiveTimer();
|
|
357
|
-
if (ctx && config.enabled && ctx.hasUI) {
|
|
358
|
-
applyIndicator(ctx, speedTracker.lastTokS);
|
|
359
|
-
safeUiCall("clear-working-message", () => ctx.ui.setWorkingMessage?.());
|
|
360
|
-
}
|
|
361
108
|
});
|
|
362
109
|
|
|
363
110
|
pi.on("model_select", (event, ctx) => {
|
|
364
111
|
currentProvider = (event as { model?: { provider?: string } }).model?.provider;
|
|
365
112
|
clearProviderUsageCache();
|
|
366
113
|
// Fetch immediately so the quota reflects the new provider without
|
|
367
|
-
// waiting for the next
|
|
114
|
+
// waiting for the next timer tick.
|
|
368
115
|
fetchProviderAndRefresh(ctx);
|
|
369
116
|
});
|
|
370
|
-
pi.on("thinking_level_select", (event, ctx) => {
|
|
371
|
-
currentThinkingLevel = (event as { level?: unknown }).level as string | undefined;
|
|
372
|
-
refreshFooter(ctx);
|
|
373
|
-
});
|
|
374
|
-
pi.on("session_compact", (_event, ctx) => refreshFooter(ctx));
|
|
375
|
-
pi.on("session_tree", (_event, ctx) => refreshFooter(ctx));
|
|
376
117
|
|
|
377
118
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
378
|
-
stopLiveTimer();
|
|
379
|
-
stopFooterTimer();
|
|
380
|
-
stopCapacityTimer();
|
|
381
119
|
stopProviderTimer();
|
|
382
|
-
setFooterDockSinkActive(false);
|
|
383
|
-
setFooter(ctx, undefined);
|
|
384
120
|
clearVibesStatus(ctx);
|
|
385
121
|
});
|
|
386
122
|
|
|
387
123
|
async function handleCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
388
|
-
const
|
|
389
|
-
const [first, second] = tokens;
|
|
390
|
-
|
|
391
|
-
const mutate = (next: CrewVibesConfig): void => {
|
|
392
|
-
config = next;
|
|
393
|
-
applyConfig(ctx);
|
|
394
|
-
};
|
|
124
|
+
const [first] = args.trim().split(/\s+/).filter(Boolean);
|
|
395
125
|
|
|
396
126
|
if (!first) {
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
Math.max(
|
|
402
|
-
0,
|
|
403
|
-
Math.min(
|
|
404
|
-
config.capacity.labels.length - 1,
|
|
405
|
-
Math.floor(((usage.percent ?? 0) / 100) * config.capacity.labels.length),
|
|
406
|
-
),
|
|
407
|
-
)
|
|
408
|
-
]
|
|
409
|
-
: "?";
|
|
410
|
-
ctx.ui.notify(
|
|
411
|
-
`crew-vibes: ${config.enabled ? "on" : "off"} · speed ${config.speed.enabled ? "on" : "off"} (${formatSpeed(config.speed, speed)}) · capacity ${config.capacity.enabled ? "on" : "off"} (${stage})`,
|
|
412
|
-
"info",
|
|
413
|
-
);
|
|
127
|
+
const quota = lastProviderUsage
|
|
128
|
+
? `${lastProviderUsage.providerName} 5h ${Math.round(lastProviderUsage.fiveHourPercent)}% · Wk ${Math.round(lastProviderUsage.weeklyPercent)}%`
|
|
129
|
+
: "no data yet";
|
|
130
|
+
ctx.ui.notify(`crew-vibes: ${config.enabled ? "on" : "off"} · quota ${quota}`, "info");
|
|
414
131
|
return;
|
|
415
132
|
}
|
|
416
133
|
|
|
417
134
|
if (first === "on" || first === "off") {
|
|
418
|
-
|
|
135
|
+
config = { ...config, enabled: first === "on" };
|
|
136
|
+
applyConfig(ctx);
|
|
419
137
|
ctx.ui.notify(`crew-vibes ${first === "on" ? "enabled" : "disabled"}`, "info");
|
|
420
138
|
return;
|
|
421
139
|
}
|
|
422
140
|
|
|
423
|
-
|
|
424
|
-
mutate({ ...config, speed: { ...config.speed, enabled: second === "on" } });
|
|
425
|
-
ctx.ui.notify(`crew-vibes speed ${second === "on" ? "enabled" : "disabled"}`, "info");
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (first === "capacity" && (second === "on" || second === "off")) {
|
|
430
|
-
mutate({ ...config, capacity: { ...config.capacity, enabled: second === "on" } });
|
|
431
|
-
ctx.ui.notify(`crew-vibes capacity ${second === "on" ? "enabled" : "disabled"}`, "info");
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
ctx.ui.notify("Usage: /team-vibes [on|off|speed on|off|capacity on|off]", "error");
|
|
141
|
+
ctx.ui.notify("Usage: /team-vibes [on|off]", "error");
|
|
436
142
|
}
|
|
437
143
|
|
|
438
144
|
pi.registerCommand("team-vibes", {
|
|
439
|
-
description: "Toggle
|
|
145
|
+
description: "Toggle the provider-quota status (on/off)",
|
|
440
146
|
handler: handleCommand,
|
|
441
147
|
});
|
|
442
148
|
}
|
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { CrewTheme } from "../../ui/theme-adapter.ts";
|
|
3
|
-
import {
|
|
4
|
-
CAPACITY_STATUS_ID,
|
|
5
|
-
type CapacityConfig,
|
|
6
|
-
type CrewVibesConfig,
|
|
7
|
-
capacityIcons,
|
|
8
|
-
PROVIDER_STATUS_ID,
|
|
9
|
-
SPEED_STATUS_ID,
|
|
10
|
-
type SpeedConfig,
|
|
11
|
-
type TokenDisplay,
|
|
12
|
-
} from "./config.ts";
|
|
13
|
-
import { capacityIndex, crewFrames, isDangerStage } from "./figures.ts";
|
|
14
|
-
|
|
15
|
-
export type CapacityUsage = {
|
|
16
|
-
tokens: number | null;
|
|
17
|
-
percent: number | null;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function formatCount(value: number): string {
|
|
21
|
-
if (value < 1000) return value.toString();
|
|
22
|
-
if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
|
|
23
|
-
if (value < 1_000_000) return `${Math.round(value / 1000)}k`;
|
|
24
|
-
if (value < 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
|
25
|
-
return `${Math.round(value / 1_000_000)}M`;
|
|
26
|
-
}
|
|
3
|
+
import { type CrewVibesConfig, PROVIDER_STATUS_ID } from "./config.ts";
|
|
27
4
|
|
|
28
5
|
function asCrewTheme(theme: unknown): CrewTheme | undefined {
|
|
29
6
|
if (theme && typeof theme === "object" && typeof (theme as CrewTheme).fg === "function") {
|
|
@@ -32,91 +9,9 @@ function asCrewTheme(theme: unknown): CrewTheme | undefined {
|
|
|
32
9
|
return undefined;
|
|
33
10
|
}
|
|
34
11
|
|
|
35
|
-
export function getCapacityUsage(ctx: ExtensionContext): CapacityUsage {
|
|
36
|
-
const fn = (ctx as { getContextUsage?: () => { tokens?: number; percent?: number } | null }).getContextUsage;
|
|
37
|
-
const usage = typeof fn === "function" ? fn.call(ctx) : null;
|
|
38
|
-
return {
|
|
39
|
-
tokens: typeof usage?.tokens === "number" && Number.isFinite(usage.tokens) ? usage.tokens : null,
|
|
40
|
-
percent: typeof usage?.percent === "number" && Number.isFinite(usage.percent) ? usage.percent : null,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function formatSpeed(config: SpeedConfig, speed: number | null): string {
|
|
45
|
-
return speed === null ? `-- ${config.label}` : `${speed.toFixed(1)} ${config.label}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function renderSpeedFooter(theme: CrewTheme | undefined, config: SpeedConfig, speed: number | null): string {
|
|
49
|
-
const value = speed === null ? "--" : speed.toFixed(1);
|
|
50
|
-
const valueTone = speed === null ? "dim" : "accent";
|
|
51
|
-
const styled = theme ? `${theme.fg(valueTone, value)} ${theme.fg("dim", config.label)}` : `${value} ${config.label}`;
|
|
52
|
-
return styled;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function renderWorkingMessage(theme: CrewTheme | undefined, config: SpeedConfig, speed: number | null): string {
|
|
56
|
-
const left = "Working";
|
|
57
|
-
const speedText = theme
|
|
58
|
-
? `${theme.fg(speed === null ? "dim" : "accent", speed === null ? "--" : speed.toFixed(1))} ${theme.fg("dim", config.label)}`
|
|
59
|
-
: `${speed === null ? "--" : speed.toFixed(1)} ${config.label}`;
|
|
60
|
-
return theme ? `${theme.fg("muted", left)} ${speedText}` : `${left} ${speedText}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function crewIndicatorFrames(theme: CrewTheme | undefined): string[] {
|
|
64
|
-
const frames = crewFrames();
|
|
65
|
-
if (!theme) return [...frames];
|
|
66
|
-
return frames.map((frame) => theme.fg("accent", frame));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function formatCapacityPrefix(config: CapacityConfig, usage: CapacityUsage): string {
|
|
70
|
-
const display: TokenDisplay = config.tokenDisplay;
|
|
71
|
-
if (display === "off") return "";
|
|
72
|
-
if (display === "percentage") {
|
|
73
|
-
return `${usage.percent === null ? "?" : Math.round(Math.max(0, Math.min(999, usage.percent)))}% `;
|
|
74
|
-
}
|
|
75
|
-
return `${usage.tokens === null ? "?" : formatCount(usage.tokens)} `;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function colorStage(theme: CrewTheme | undefined, index: number, levels: number, text: string): string {
|
|
79
|
-
if (!theme || text.length === 0) return text;
|
|
80
|
-
return theme.fg(isDangerStage(index, levels) ? "error" : "success", text);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function renderCapacity(theme: CrewTheme | undefined, config: CapacityConfig, usage: CapacityUsage): string {
|
|
84
|
-
const icons = capacityIcons();
|
|
85
|
-
const levels = icons.length;
|
|
86
|
-
const index = capacityIndex(usage.percent, levels);
|
|
87
|
-
const icon = icons[index] ?? icons[0];
|
|
88
|
-
const label = config.labels[index] ?? config.labels[0];
|
|
89
|
-
const prefix = theme ? theme.fg("muted", formatCapacityPrefix(config, usage)) : formatCapacityPrefix(config, usage);
|
|
90
|
-
const coloredIcon = colorStage(theme, index, levels, icon);
|
|
91
|
-
const afterIcon = config.showLabel ? ` ${colorStage(theme, index, levels, label)}` : " ";
|
|
92
|
-
return `${prefix}${coloredIcon}${afterIcon}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function setSpeedStatus(ctx: ExtensionContext, config: CrewVibesConfig, text: string | undefined): void {
|
|
96
|
-
if (!ctx?.hasUI) return;
|
|
97
|
-
if (!config.enabled || !config.speed.enabled || !config.speed.footer) {
|
|
98
|
-
ctx.ui.setStatus(SPEED_STATUS_ID, undefined);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
ctx.ui.setStatus(SPEED_STATUS_ID, text);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function setCapacityStatus(ctx: ExtensionContext, config: CrewVibesConfig, text: string | undefined): void {
|
|
105
|
-
if (!ctx?.hasUI) return;
|
|
106
|
-
if (!config.enabled || !config.capacity.enabled) {
|
|
107
|
-
ctx.ui.setStatus(CAPACITY_STATUS_ID, undefined);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
ctx.ui.setStatus(CAPACITY_STATUS_ID, text);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
12
|
export function clearVibesStatus(ctx: ExtensionContext): void {
|
|
114
13
|
if (!ctx?.hasUI) return;
|
|
115
|
-
ctx.ui.setStatus(SPEED_STATUS_ID, undefined);
|
|
116
|
-
ctx.ui.setStatus(CAPACITY_STATUS_ID, undefined);
|
|
117
14
|
ctx.ui.setStatus(PROVIDER_STATUS_ID, undefined);
|
|
118
|
-
if (ctx.ui.setWorkingIndicator) ctx.ui.setWorkingIndicator();
|
|
119
|
-
if (ctx.ui.setWorkingMessage) ctx.ui.setWorkingMessage();
|
|
120
15
|
}
|
|
121
16
|
|
|
122
17
|
// Provider rate-limit usage snapshot (mirrors provider-usage.ts interface).
|
package/src/extension/help.ts
CHANGED
|
@@ -19,13 +19,13 @@ export function piTeamsHelp(): string {
|
|
|
19
19
|
"- /team-worktrees <runId>",
|
|
20
20
|
"- /team-api <runId> <operation> [taskId=<taskId>] [body=<message>]",
|
|
21
21
|
"- /team-dashboard",
|
|
22
|
+
"- /schedules [log <jobId-or-name>] — list scheduled jobs / tail latest run output",
|
|
22
23
|
"- /team-mascot",
|
|
23
24
|
"- /team-transcript <runId> [taskId]",
|
|
24
25
|
"- /team-result <runId> [taskId]",
|
|
25
26
|
"- /team-manager — interactive menu (alias: /team-cleanup-menu)",
|
|
26
27
|
"",
|
|
27
28
|
"Maintenance:",
|
|
28
|
-
"- /team-cleanup <runId> [--force] (or scope=project/user for uninstall cleanup)",
|
|
29
29
|
"- /team-forget <runId> --confirm [--force]",
|
|
30
30
|
"- /team-prune --keep=20 --confirm",
|
|
31
31
|
"",
|
|
@@ -35,7 +35,6 @@ export function piTeamsHelp(): string {
|
|
|
35
35
|
"- /team-imports",
|
|
36
36
|
"",
|
|
37
37
|
"Diagnostics:",
|
|
38
|
-
"- /team-health",
|
|
39
38
|
"- /team-doctor",
|
|
40
39
|
"- /team-init [--copy-builtins] [--overwrite]",
|
|
41
40
|
"- /team-config [key=value] [--unset=key.path] [--project]",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* • command-registration — registerTeamCommands
|
|
16
16
|
* • crash-recovery-cache — lazy importCrashRecovery
|
|
17
17
|
* • wire-cross-extension — RPC handle + global registry install
|
|
18
|
+
* • terminal-status-wiring — tab title + Ghostty progress (runEventBus)
|
|
18
19
|
*/
|
|
19
20
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
21
|
import { loadConfig } from "../config/config.ts";
|
|
@@ -26,7 +27,6 @@ import { installChildProcessAbortShield } from "../utils/child-process-shield.ts
|
|
|
26
27
|
import { resetTimings, time } from "../utils/timings.ts";
|
|
27
28
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
28
29
|
import { registerContextStatusInjection } from "./context-status-injection.ts";
|
|
29
|
-
import { registerCleanupHandler } from "./crew-cleanup.ts";
|
|
30
30
|
import { registerCrewInputRouter } from "./crew-input-router.ts";
|
|
31
31
|
import { registerCrewShortcuts } from "./crew-shortcuts.ts";
|
|
32
32
|
import { registerCrewVibes } from "./crew-vibes/index.ts";
|
|
@@ -43,6 +43,7 @@ import { installCrewBrokerLifecycleController, installSessionLifecycleHandlers }
|
|
|
43
43
|
import { installRuntimeCleanup } from "./registration/runtime-cleanup.ts";
|
|
44
44
|
import { __test__subagentSpawnParams } from "./registration/subagent-helpers.ts";
|
|
45
45
|
import { installSubagentManager } from "./registration/subagent-manager-setup.ts";
|
|
46
|
+
import { installTerminalStatus } from "./registration/terminal-status-wiring.ts";
|
|
46
47
|
import { registerPiTools } from "./registration/tool-registration.ts";
|
|
47
48
|
import { installCrossExtensionWiring } from "./registration/wire-cross-extension.ts";
|
|
48
49
|
|
|
@@ -101,7 +102,11 @@ export async function registerPiTeams(pi: ExtensionAPI): Promise<void> {
|
|
|
101
102
|
// subagents or when the flag is off, it returns a no-op controller.
|
|
102
103
|
ctx.brokerController = installCrewBrokerLifecycleController(pi, ctx);
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
// M3-1 (UI-AUDIT P0-2/P1-1): wire the tab-title + Ghostty progress
|
|
106
|
+
// controller to runEventBus. Before this the controller was never
|
|
107
|
+
// constructed (dead since v0.8.3). Registers the dispose hook that the
|
|
108
|
+
// SIGTERM/SIGHUP handler calls (crew-cleanup.ts).
|
|
109
|
+
installTerminalStatus(pi, ctx);
|
|
105
110
|
registerCompactionGuard(pi, {
|
|
106
111
|
foregroundControllers: ctx.foregroundControllers,
|
|
107
112
|
foregroundTeamRunControllers: ctx.foregroundTeamRunControllers,
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
* Command registration installer for pi-crew.
|
|
3
3
|
*
|
|
4
4
|
* Single entry point that wires every `pi.registerCommand(...)` call
|
|
5
|
-
* site in the extension. Today that means
|
|
5
|
+
* site in the extension. Today that means:
|
|
6
6
|
* • `registerTeamCommands` — registers every `/crew-*`, `/team-*`,
|
|
7
7
|
* `/teams`, and friends. The full set lives in
|
|
8
8
|
* `registration/commands.ts` (already extracted); this file is
|
|
9
9
|
* the orchestrator-level wrapper that calls it with the right deps.
|
|
10
|
+
* • `registerSchedulesCommands` — the headless `/schedules` command
|
|
11
|
+
* (Scheduled Jobs UI tier E; no deps beyond the ExtensionAPI).
|
|
10
12
|
*/
|
|
11
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
14
|
import { loadConfig } from "../../config/config.ts";
|
|
13
15
|
import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
|
|
14
16
|
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
17
|
+
import { registerSchedulesCommands } from "./commands/schedules.ts";
|
|
15
18
|
import { registerTeamCommands } from "./commands.ts";
|
|
16
19
|
import type { RegistrationContext } from "./registration-types.ts";
|
|
17
20
|
|
|
@@ -56,4 +59,5 @@ export function registerPiCommands(pi: ExtensionAPI, ctx: RegistrationContext):
|
|
|
56
59
|
}
|
|
57
60
|
},
|
|
58
61
|
});
|
|
62
|
+
registerSchedulesCommands(pi);
|
|
59
63
|
}
|