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.
Files changed (88) hide show
  1. package/CHANGELOG.md +215 -0
  2. package/README.md +0 -1
  3. package/dist/index.mjs +30623 -29844
  4. package/package.json +170 -169
  5. package/schema.json +10 -0
  6. package/scripts/watch-agent-transcript.mjs +120 -0
  7. package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
  8. package/skills/real-test-pi-crew/SKILL.md +181 -1
  9. package/src/config/config-validation.ts +0 -2
  10. package/src/config/defaults.ts +0 -1
  11. package/src/config/types.ts +0 -5
  12. package/src/extension/crew-input-router.ts +2 -0
  13. package/src/extension/crew-vibes/config.ts +1 -77
  14. package/src/extension/crew-vibes/index.ts +36 -330
  15. package/src/extension/crew-vibes/render.ts +1 -106
  16. package/src/extension/help.ts +1 -2
  17. package/src/extension/register.ts +7 -2
  18. package/src/extension/registration/command-registration.ts +5 -1
  19. package/src/extension/registration/commands/manage.ts +5 -4
  20. package/src/extension/registration/commands/schedules.ts +212 -0
  21. package/src/extension/registration/commands/shared.ts +56 -2
  22. package/src/extension/registration/lifecycle-handlers.ts +16 -2
  23. package/src/extension/registration/schedule-toast-bridge.ts +109 -0
  24. package/src/extension/registration/subagent-tools.ts +16 -6
  25. package/src/extension/registration/team-tool.ts +10 -3
  26. package/src/extension/registration/terminal-status-wiring.ts +172 -0
  27. package/src/extension/registration/viewers.ts +72 -1
  28. package/src/extension/team-tool/handle-schedule.ts +120 -1
  29. package/src/extension/team-tool/handle-settings.ts +14 -2
  30. package/src/runtime/scheduling/scheduler.ts +120 -6
  31. package/src/runtime/settings-store.ts +28 -2
  32. package/src/runtime/task-display.ts +1 -1
  33. package/src/schema/config-schema.ts +4 -0
  34. package/src/state/coordination/locks.ts +17 -2
  35. package/src/ui/adaptive-card.ts +65 -0
  36. package/src/ui/agents-jobs-browser.ts +791 -0
  37. package/src/ui/card-colors.ts +36 -7
  38. package/src/ui/dashboard-panes/agents-pane.ts +52 -17
  39. package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
  40. package/src/ui/dashboard-panes/health-pane.ts +7 -5
  41. package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
  42. package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
  43. package/src/ui/dashboard-panes/pane-theme.ts +21 -0
  44. package/src/ui/dashboard-panes/plan-pane.ts +63 -30
  45. package/src/ui/dashboard-panes/progress-pane.ts +3 -2
  46. package/src/ui/dashboard-panes/schedules-pane.ts +177 -0
  47. package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
  48. package/src/ui/dwf-phase-display.ts +3 -20
  49. package/src/ui/format-helpers.ts +22 -0
  50. package/src/ui/inline-panel/crew-editor.ts +32 -9
  51. package/src/ui/inline-panel/index.ts +66 -4
  52. package/src/ui/key-utils.ts +3 -0
  53. package/src/ui/keybinding-map.ts +286 -34
  54. package/src/ui/live-conversation-overlay.ts +176 -46
  55. package/src/ui/live-run-sidebar.ts +134 -55
  56. package/src/ui/mascot.ts +34 -17
  57. package/src/ui/overlays/agent-picker-overlay.ts +81 -26
  58. package/src/ui/overlays/confirm-overlay.ts +55 -28
  59. package/src/ui/overlays/help-overlay.ts +108 -53
  60. package/src/ui/overlays/mailbox-compose-overlay.ts +89 -49
  61. package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
  62. package/src/ui/powerbar-publisher.ts +0 -1
  63. package/src/ui/rail.ts +333 -0
  64. package/src/ui/run-dashboard.ts +344 -72
  65. package/src/ui/settings-overlay.ts +84 -42
  66. package/src/ui/terminal-status.ts +7 -1
  67. package/src/ui/theme-adapter.ts +0 -45
  68. package/src/ui/theme-discovery.ts +12 -6
  69. package/src/ui/tool-progress-formatter.ts +126 -8
  70. package/src/ui/tool-renderers/brief-mode.ts +8 -66
  71. package/src/ui/tool-renderers/index.ts +372 -521
  72. package/src/ui/transcript-viewer.ts +33 -14
  73. package/src/ui/widget/index.ts +86 -133
  74. package/src/ui/widget/task-list.ts +64 -32
  75. package/src/ui/widget/widget-formatters.ts +3 -399
  76. package/src/ui/widget/widget-model.ts +28 -7
  77. package/src/ui/widget/widget-renderer.ts +298 -265
  78. package/src/ui/widget/widget-types.ts +0 -9
  79. package/src/utils/relative-time.ts +33 -0
  80. package/src/utils/visual.ts +0 -4
  81. package/src/extension/crew-vibes/figures.ts +0 -101
  82. package/src/extension/crew-vibes/font-detect.ts +0 -71
  83. package/src/extension/crew-vibes/footer.ts +0 -313
  84. package/src/extension/crew-vibes/speed.ts +0 -286
  85. package/src/ui/dock-footer.ts +0 -49
  86. package/src/ui/dynamic-border.ts +0 -35
  87. package/src/ui/loaders.ts +0 -6
  88. 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. 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,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
- 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
- }),
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
- 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.
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 5-minute tick. */
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
- refreshFooter(ctx);
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
- refreshFooter(ctx);
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 5-minute timer tick.
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 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
- };
124
+ const [first] = args.trim().split(/\s+/).filter(Boolean);
395
125
 
396
126
  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
- );
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
- mutate({ ...config, enabled: first === "on" });
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
- 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");
141
+ ctx.ui.notify("Usage: /team-vibes [on|off]", "error");
436
142
  }
437
143
 
438
144
  pi.registerCommand("team-vibes", {
439
- description: "Toggle crew-vibes speed + context meters (on/off, speed, capacity)",
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).
@@ -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
- registerCleanupHandler(pi, { disposeTerminalStatus: () => ctx.terminalStatus?.dispose?.() });
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 one heavy call:
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
  }