pi-crew 0.10.6 → 0.11.0

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/index.mjs +34850 -34889
  3. package/package.json +170 -169
  4. package/scripts/watch-agent-transcript.mjs +120 -0
  5. package/src/extension/crew-input-router.ts +2 -0
  6. package/src/extension/crew-vibes/config.ts +1 -56
  7. package/src/extension/crew-vibes/figures.ts +5 -84
  8. package/src/extension/crew-vibes/font-detect.ts +1 -1
  9. package/src/extension/crew-vibes/index.ts +36 -328
  10. package/src/extension/crew-vibes/render.ts +2 -58
  11. package/src/extension/help.ts +1 -0
  12. package/src/extension/registration/command-registration.ts +5 -1
  13. package/src/extension/registration/commands/schedules.ts +212 -0
  14. package/src/extension/registration/commands/shared.ts +56 -1
  15. package/src/extension/registration/lifecycle-handlers.ts +16 -2
  16. package/src/extension/registration/schedule-toast-bridge.ts +109 -0
  17. package/src/extension/registration/viewers.ts +66 -1
  18. package/src/extension/team-tool/handle-schedule.ts +120 -1
  19. package/src/runtime/scheduling/scheduler.ts +120 -6
  20. package/src/runtime/settings-store.ts +28 -2
  21. package/src/state/coordination/locks.ts +17 -2
  22. package/src/ui/agents-jobs-browser.ts +785 -0
  23. package/src/ui/dashboard-panes/agents-pane.ts +7 -4
  24. package/src/ui/dashboard-panes/schedules-pane.ts +154 -0
  25. package/src/ui/inline-panel/crew-editor.ts +19 -6
  26. package/src/ui/inline-panel/index.ts +6 -0
  27. package/src/ui/key-utils.ts +3 -0
  28. package/src/ui/keybinding-map.ts +56 -2
  29. package/src/ui/mascot.ts +2 -1
  30. package/src/ui/overlays/agent-picker-overlay.ts +3 -3
  31. package/src/ui/overlays/confirm-overlay.ts +3 -2
  32. package/src/ui/overlays/mailbox-compose-overlay.ts +4 -3
  33. package/src/ui/overlays/mailbox-detail-overlay.ts +3 -3
  34. package/src/ui/run-dashboard.ts +257 -6
  35. package/src/ui/settings-overlay.ts +3 -3
  36. package/src/ui/transcript-viewer.ts +3 -2
  37. package/src/ui/widget/index.ts +61 -88
  38. package/src/ui/widget/widget-formatters.ts +7 -4
  39. package/src/ui/widget/widget-renderer.ts +134 -174
  40. package/src/ui/widget/widget-types.ts +0 -7
  41. package/src/utils/relative-time.ts +33 -0
  42. package/src/extension/crew-vibes/footer.ts +0 -313
  43. package/src/extension/crew-vibes/speed.ts +0 -286
  44. package/src/ui/dock-footer.ts +0 -49
@@ -1,91 +1,12 @@
1
- import type { SpeedConfig } from "./config.ts";
2
- import { hasCrewFontFile, isWebTerminal } from "./font-detect.ts";
3
-
4
1
  /**
5
- * Crew figures replacing the original cat glyphs from pi-speeed / pi-chonk.
6
- *
7
- * The working indicator uses braille spinner characters by default — the
8
- * same characters pi uses for its built-in loading animation (loaders.ts
9
- * DEFAULT_FRAMES). These render on ANY terminal with basic Unicode support,
10
- * no custom font required.
2
+ * Capacity-stage helpers (shared by renderCapacity, which remains exported
3
+ * for its unit tests after the footer retirement).
11
4
  *
12
- * PUA glyphs (U+E700..U+E70F) from the bundled crew-vibes.ttf font are
13
- * available as an opt-in alternative (set speed.indicatorStyle = "pua" in
14
- * config). Requires the font to be installed via `npm run install:crew-font`
15
- * AND a terminal that renders Private Use Area codepoints.
5
+ * The speed-UI parts of this module (braille/PUA spinner frames,
6
+ * intervalForSpeed) were REMOVED with the tok/s speed UI (maintainer
7
+ * decision 2026-09-14) — pi's built-in working indicator is always used.
16
8
  */
17
9
 
18
- // ── Braille spinner frames (web terminal fallback) ───────────────────
19
- // Standard Unicode braille spinner — same characters pi uses for its
20
- // built-in loading animation. Renders on ANY terminal.
21
- const BRAILLE_FRAMES: readonly string[] = [
22
- "\u280B ", // ⠋
23
- "\u2819 ", // ⠙
24
- "\u2839 ", // ⠹
25
- "\u2838 ", // ⠸
26
- "\u283C ", // ⠼
27
- "\u2834 ", // ⠴
28
- "\u2826 ", // ⠦
29
- "\u2827 ", // ⠧
30
- "\u2807 ", // ⠇
31
- "\u280F ", // ⠏
32
- ] as const;
33
-
34
- // ── PUA runner frames (opt-in) ────────────────────────────────────────
35
- // 16 runner poses from the bundled crew-vibes.ttf font. Only usable when
36
- // the font is installed AND the terminal renders PUA codepoints correctly.
37
- // Each frame is one glyph + trailing space for constant 2-cell width.
38
- const PUA_CREW_FRAMES: readonly string[] = [
39
- "\uE700 ",
40
- "\uE701 ",
41
- "\uE702 ",
42
- "\uE703 ",
43
- "\uE704 ",
44
- "\uE705 ",
45
- "\uE706 ",
46
- "\uE707 ",
47
- "\uE708 ",
48
- "\uE709 ",
49
- "\uE70A ",
50
- "\uE70B ",
51
- "\uE70C ",
52
- "\uE70D ",
53
- "\uE70E ",
54
- "\uE70F ",
55
- ] as const;
56
-
57
- /**
58
- * Return the indicator frames for the given style.
59
- *
60
- * Default is `"braille"` — standard Unicode braille spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏)
61
- * that renders correctly on every terminal, every platform, with zero
62
- * font dependency. PUA runner-glyphs require crew-vibes.ttf installed AND
63
- * a terminal that does font fallback for Private Use Area codepoints —
64
- * Windows Terminal does NOT do this reliably, showing garbage chars even
65
- * with the font installed.
66
- *
67
- * To opt into the PUA runner look, pass `style: "pua"` explicitly.
68
- */
69
- export function crewFrames(style: "braille" | "pua" = "braille"): readonly string[] {
70
- // Even when explicitly requested, fall back to braille if the font is
71
- // missing or we're on a web terminal (which can't render PUA at all).
72
- if (style === "pua" && (isWebTerminal() || !hasCrewFontFile())) return BRAILLE_FRAMES;
73
- return style === "pua" ? PUA_CREW_FRAMES : BRAILLE_FRAMES;
74
- }
75
-
76
- // Re-export PUA frames for backward compatibility and direct consumers.
77
- export const RUN_CREW_FRAMES = PUA_CREW_FRAMES;
78
-
79
- /**
80
- * Map a tok/s reading to a working-indicator frame interval in ms.
81
- * Ported from pi-speeed's runcatInterval: interval = scale / speed, clamped
82
- * to [min, max]. A null/zero speed falls back to the default (idle) cadence.
83
- */
84
- export function intervalForSpeed(config: SpeedConfig, speed: number | null): number {
85
- if (speed === null || !Number.isFinite(speed) || speed <= 0) return config.defaultIntervalMs;
86
- return Math.max(config.minIntervalMs, Math.min(config.maxIntervalMs, Math.round(config.scale / speed)));
87
- }
88
-
89
10
  /**
90
11
  * Pick the capacity stage index (0..levels-1) for a context-fill percent.
91
12
  * Ported from pi-chonk's chonkIndex.
@@ -8,7 +8,7 @@ import { join } from "node:path";
8
8
  * guarantee the terminal can render PUA glyphs (many terminals have their
9
9
  * own font stacks). For reliable animation, braille fallback frames are
10
10
  * used by default; PUA frames are only activated when the user explicitly
11
- * enables them via config (speed.indicatorStyle = "pua").
11
+ * (used by the capacity icons).
12
12
  */
13
13
 
14
14
  function fontPath(): string {
@@ -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. terminal doesn't
7
- * support PUA glyphs, theme helper returns undefined, or pi's API signature
8
- * changed), log it and silently fall back to pi's default behavior — which
9
- * is simply "do nothing", letting pi show its own built-in indicator.
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,39 @@ 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";
20
+ import { asCrewTheme, clearVibesStatus, renderProviderUsage, setProviderStatus } from "./render.ts";
38
21
 
39
22
  export const CREW_VIBES_STATUS_KEY = "pi-crew-vibes";
40
23
 
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
- }
59
-
24
+ /**
25
+ * crew-vibes — provider rate-limit quota publisher.
26
+ *
27
+ * Maintainer decisions 2026-09-13/14 (UI review follow-ups) stripped this
28
+ * module down to its one remaining job: fetching the current provider's
29
+ * rate-limit usage and publishing it as a STATUS entry on pi's NATIVE
30
+ * footer. Removed entirely: the custom footer replacement (it had drifted
31
+ * from native) and the tok/s speed UI (custom spinner + "Working N tok/s")
32
+ * — pi's built-ins are always used.
33
+ */
60
34
  export function registerCrewVibes(pi: ExtensionAPI): void {
61
35
  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
36
  let providerTimer: ReturnType<typeof setInterval> | undefined;
70
37
  let lastProviderUsage: ProviderUsage | null = null;
71
38
  let currentProvider: string | undefined;
72
- let currentThinkingLevel: string | undefined;
73
39
 
74
40
  function themeOf(ctx: ExtensionContext) {
75
41
  return asCrewTheme(ctx.hasUI ? ctx.ui.theme : undefined);
76
42
  }
77
43
 
78
- const footerSource: CrewVibesFooterSource = {
79
- getConfig: () => config,
80
- getQuotaUsage: () => lastProviderUsage,
81
- getThinkingLevel: () => currentThinkingLevel,
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
- }),
44
+ /** Publish the provider rate-limit quota as a STATUS entry (joined into
45
+ * pi's NATIVE footer status line; right-truncates on very narrow
46
+ * terminals — pi's documented behavior, accepted). */
47
+ function publishQuotaStatus(ctx: ExtensionContext): void {
48
+ safeUiCall("publish-quota-status", () =>
49
+ setProviderStatus(ctx, config, lastProviderUsage ? renderProviderUsage(themeOf(ctx), lastProviderUsage) : undefined),
146
50
  );
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
51
  }
172
52
 
173
53
  function stopProviderTimer(): void {
@@ -176,48 +56,14 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
176
56
  providerTimer = undefined;
177
57
  }
178
58
 
179
- function startLiveTimer(ctx: ExtensionContext): void {
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.
59
+ /** Fetch provider usage for currentProvider and publish the quota status.
214
60
  * Called on start, on each timer tick, and immediately when the
215
61
  * provider changes (model_select) so the quota reflects the new
216
- * provider without waiting for the next 5-minute tick. */
62
+ * provider without waiting for the next tick. */
217
63
  async function fetchProviderAndRefresh(ctx: ExtensionContext): Promise<void> {
218
64
  if (!config.enabled || !config.capacity.providerUsage) {
219
65
  lastProviderUsage = null;
220
- refreshFooter(ctx);
66
+ publishQuotaStatus(ctx);
221
67
  return;
222
68
  }
223
69
  try {
@@ -226,7 +72,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
226
72
  // Never crash on provider fetch failure
227
73
  lastProviderUsage = null;
228
74
  }
229
- refreshFooter(ctx);
75
+ publishQuotaStatus(ctx);
230
76
  }
231
77
 
232
78
  function startProviderTimer(ctx: ExtensionContext): void {
@@ -239,204 +85,66 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
239
85
  providerTimer.unref?.();
240
86
  }
241
87
 
242
- function resetWorking(ctx: ExtensionContext): void {
243
- applyIndicator(ctx, null, true);
244
- renderWorking(ctx, speedTracker.lastTokS);
245
- }
246
-
247
88
  function applyConfig(ctx: ExtensionContext): void {
248
89
  saveConfig(config);
249
- speedTracker.updateConfig(config.speed);
250
- footerAnimator.updateDuration(config.speed.renderIntervalMs);
251
90
  if (!config.enabled) {
252
- stopLiveTimer();
253
- stopFooterTimer();
254
- stopCapacityTimer();
255
91
  stopProviderTimer();
256
- setFooterDockSinkActive(false);
257
- setFooter(ctx, undefined);
258
92
  clearVibesStatus(ctx);
259
93
  return;
260
94
  }
261
- installFooter(ctx);
262
- publishSpeedFooter(ctx);
263
- startCapacityTimer(ctx);
264
95
  if (config.capacity.providerUsage) startProviderTimer(ctx);
96
+ else publishQuotaStatus(ctx); // clears the stale status when disabled
265
97
  }
266
98
 
267
99
  pi.on("session_start", (_event, ctx) => {
268
- stopLiveTimer();
269
- stopFooterTimer();
270
- stopCapacityTimer();
271
100
  stopProviderTimer();
272
101
  config = loadConfig();
273
- speedTracker.updateConfig(config.speed);
274
- footerAnimator.updateDuration(config.speed.renderIntervalMs);
275
- speedTracker.resetSession();
276
- footerAnimator.reset(null);
277
102
  clearProviderUsageCache();
278
103
  // Initialize provider from current model — model_select only fires on manual switch
279
104
  currentProvider = (ctx.model as { provider?: string } | undefined)?.provider;
280
- currentThinkingLevel = undefined;
281
105
  if (!config.enabled) {
282
- setFooter(ctx, undefined);
283
106
  clearVibesStatus(ctx);
284
107
  return;
285
108
  }
286
- installFooter(ctx);
287
- publishSpeedFooter(ctx);
288
- startCapacityTimer(ctx);
289
109
  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
110
  });
362
111
 
363
112
  pi.on("model_select", (event, ctx) => {
364
113
  currentProvider = (event as { model?: { provider?: string } }).model?.provider;
365
114
  clearProviderUsageCache();
366
115
  // Fetch immediately so the quota reflects the new provider without
367
- // waiting for the next 5-minute timer tick.
116
+ // waiting for the next timer tick.
368
117
  fetchProviderAndRefresh(ctx);
369
118
  });
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
119
 
377
120
  pi.on("session_shutdown", (_event, ctx) => {
378
- stopLiveTimer();
379
- stopFooterTimer();
380
- stopCapacityTimer();
381
121
  stopProviderTimer();
382
- setFooterDockSinkActive(false);
383
- setFooter(ctx, undefined);
384
122
  clearVibesStatus(ctx);
385
123
  });
386
124
 
387
125
  async function handleCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
388
- const tokens = args.trim().split(/\s+/).filter(Boolean);
389
- const [first, second] = tokens;
390
-
391
- const mutate = (next: CrewVibesConfig): void => {
392
- config = next;
393
- applyConfig(ctx);
394
- };
126
+ const [first] = args.trim().split(/\s+/).filter(Boolean);
395
127
 
396
128
  if (!first) {
397
- const speed = speedTracker.liveTokS();
398
- const usage = getCapacityUsage(ctx);
399
- const stage = config.capacity.icons.length
400
- ? config.capacity.labels[
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
- );
129
+ const quota = lastProviderUsage
130
+ ? `${lastProviderUsage.providerName} 5h ${Math.round(lastProviderUsage.fiveHourPercent)}% · Wk ${Math.round(lastProviderUsage.weeklyPercent)}%`
131
+ : "no data yet";
132
+ ctx.ui.notify(`crew-vibes: ${config.enabled ? "on" : "off"} · quota ${quota}`, "info");
414
133
  return;
415
134
  }
416
135
 
417
136
  if (first === "on" || first === "off") {
418
- mutate({ ...config, enabled: first === "on" });
137
+ config = { ...config, enabled: first === "on" };
138
+ applyConfig(ctx);
419
139
  ctx.ui.notify(`crew-vibes ${first === "on" ? "enabled" : "disabled"}`, "info");
420
140
  return;
421
141
  }
422
142
 
423
- if (first === "speed" && (second === "on" || second === "off")) {
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");
143
+ ctx.ui.notify("Usage: /team-vibes [on|off]", "error");
436
144
  }
437
145
 
438
146
  pi.registerCommand("team-vibes", {
439
- description: "Toggle crew-vibes speed + context meters (on/off, speed, capacity)",
147
+ description: "Toggle the provider-quota status (on/off)",
440
148
  handler: handleCommand,
441
149
  });
442
150
  }
@@ -1,16 +1,7 @@
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";
3
+ import { type CapacityConfig, type CrewVibesConfig, capacityIcons, PROVIDER_STATUS_ID, type TokenDisplay } from "./config.ts";
4
+ import { capacityIndex, isDangerStage } from "./figures.ts";
14
5
 
15
6
  export type CapacityUsage = {
16
7
  tokens: number | null;
@@ -41,31 +32,6 @@ export function getCapacityUsage(ctx: ExtensionContext): CapacityUsage {
41
32
  };
42
33
  }
43
34
 
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
35
  function formatCapacityPrefix(config: CapacityConfig, usage: CapacityUsage): string {
70
36
  const display: TokenDisplay = config.tokenDisplay;
71
37
  if (display === "off") return "";
@@ -92,31 +58,9 @@ export function renderCapacity(theme: CrewTheme | undefined, config: CapacityCon
92
58
  return `${prefix}${coloredIcon}${afterIcon}`;
93
59
  }
94
60
 
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
61
  export function clearVibesStatus(ctx: ExtensionContext): void {
114
62
  if (!ctx?.hasUI) return;
115
- ctx.ui.setStatus(SPEED_STATUS_ID, undefined);
116
- ctx.ui.setStatus(CAPACITY_STATUS_ID, undefined);
117
63
  ctx.ui.setStatus(PROVIDER_STATUS_ID, undefined);
118
- if (ctx.ui.setWorkingIndicator) ctx.ui.setWorkingIndicator();
119
- if (ctx.ui.setWorkingMessage) ctx.ui.setWorkingMessage();
120
64
  }
121
65
 
122
66
  // Provider rate-limit usage snapshot (mirrors provider-usage.ts interface).
@@ -19,6 +19,7 @@ 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]",