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.
- package/CHANGELOG.md +99 -0
- package/dist/index.mjs +34850 -34889
- package/package.json +170 -169
- package/scripts/watch-agent-transcript.mjs +120 -0
- package/src/extension/crew-input-router.ts +2 -0
- package/src/extension/crew-vibes/config.ts +1 -56
- package/src/extension/crew-vibes/figures.ts +5 -84
- package/src/extension/crew-vibes/font-detect.ts +1 -1
- package/src/extension/crew-vibes/index.ts +36 -328
- package/src/extension/crew-vibes/render.ts +2 -58
- package/src/extension/help.ts +1 -0
- package/src/extension/registration/command-registration.ts +5 -1
- package/src/extension/registration/commands/schedules.ts +212 -0
- package/src/extension/registration/commands/shared.ts +56 -1
- package/src/extension/registration/lifecycle-handlers.ts +16 -2
- package/src/extension/registration/schedule-toast-bridge.ts +109 -0
- package/src/extension/registration/viewers.ts +66 -1
- package/src/extension/team-tool/handle-schedule.ts +120 -1
- package/src/runtime/scheduling/scheduler.ts +120 -6
- package/src/runtime/settings-store.ts +28 -2
- package/src/state/coordination/locks.ts +17 -2
- package/src/ui/agents-jobs-browser.ts +785 -0
- package/src/ui/dashboard-panes/agents-pane.ts +7 -4
- package/src/ui/dashboard-panes/schedules-pane.ts +154 -0
- package/src/ui/inline-panel/crew-editor.ts +19 -6
- package/src/ui/inline-panel/index.ts +6 -0
- package/src/ui/key-utils.ts +3 -0
- package/src/ui/keybinding-map.ts +56 -2
- package/src/ui/mascot.ts +2 -1
- package/src/ui/overlays/agent-picker-overlay.ts +3 -3
- package/src/ui/overlays/confirm-overlay.ts +3 -2
- package/src/ui/overlays/mailbox-compose-overlay.ts +4 -3
- package/src/ui/overlays/mailbox-detail-overlay.ts +3 -3
- package/src/ui/run-dashboard.ts +257 -6
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/transcript-viewer.ts +3 -2
- package/src/ui/widget/index.ts +61 -88
- package/src/ui/widget/widget-formatters.ts +7 -4
- package/src/ui/widget/widget-renderer.ts +134 -174
- package/src/ui/widget/widget-types.ts +0 -7
- package/src/utils/relative-time.ts +33 -0
- 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
|
@@ -1,91 +1,12 @@
|
|
|
1
|
-
import type { SpeedConfig } from "./config.ts";
|
|
2
|
-
import { hasCrewFontFile, isWebTerminal } from "./font-detect.ts";
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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,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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
}),
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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).
|
package/src/extension/help.ts
CHANGED
|
@@ -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]",
|