@aliou/pi-synthetic 0.10.2 → 0.12.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/package.json +3 -2
- package/src/extensions/command-quotas/command.ts +13 -4
- package/src/extensions/command-quotas/components/quotas-display.ts +93 -338
- package/src/extensions/provider/models.ts +4 -4
- package/src/extensions/quota-warnings/index.ts +22 -0
- package/src/extensions/quota-warnings/notifier.test.ts +280 -0
- package/src/extensions/quota-warnings/notifier.ts +200 -0
- package/src/utils/quotas-severity.test.ts +278 -0
- package/src/utils/quotas-severity.ts +272 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"extensions": [
|
|
18
18
|
"./src/extensions/provider/index.ts",
|
|
19
19
|
"./src/extensions/web-search/index.ts",
|
|
20
|
-
"./src/extensions/command-quotas/index.ts"
|
|
20
|
+
"./src/extensions/command-quotas/index.ts",
|
|
21
|
+
"./src/extensions/quota-warnings/index.ts"
|
|
21
22
|
],
|
|
22
23
|
"video": "https://assets.aliou.me/pi-extensions/demos/pi-synthetic.mp4"
|
|
23
24
|
},
|
|
@@ -19,10 +19,19 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
|
19
19
|
|
|
20
20
|
const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
|
|
21
21
|
const controller = new AbortController();
|
|
22
|
-
const component = new QuotasComponent(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const component = new QuotasComponent(
|
|
23
|
+
theme,
|
|
24
|
+
tui,
|
|
25
|
+
() => {
|
|
26
|
+
controller.abort();
|
|
27
|
+
done(null);
|
|
28
|
+
},
|
|
29
|
+
() => {
|
|
30
|
+
component.setState({ type: "loading" });
|
|
31
|
+
tui.requestRender();
|
|
32
|
+
void loadQuotas();
|
|
33
|
+
},
|
|
34
|
+
);
|
|
26
35
|
|
|
27
36
|
async function loadQuotas(): Promise<void> {
|
|
28
37
|
const fetchResult = await fetchQuotas(key, controller.signal);
|
|
@@ -1,196 +1,32 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
4
|
-
import {
|
|
5
|
-
Loader,
|
|
6
|
-
matchesKey,
|
|
7
|
-
truncateToWidth,
|
|
8
|
-
visibleWidth,
|
|
9
|
-
} from "@mariozechner/pi-tui";
|
|
4
|
+
import { Loader, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
10
5
|
import type { QuotasResponse } from "../../../types/quotas";
|
|
6
|
+
import {
|
|
7
|
+
assessWindow,
|
|
8
|
+
formatTimeRemaining,
|
|
9
|
+
getSeverityColor,
|
|
10
|
+
type QuotaWindow,
|
|
11
|
+
toWindows,
|
|
12
|
+
} from "../../../utils/quotas-severity";
|
|
11
13
|
|
|
12
14
|
type QuotasState =
|
|
13
15
|
| { type: "loading" }
|
|
14
16
|
| { type: "error"; message: string }
|
|
15
17
|
| { type: "loaded"; quotas: QuotasResponse };
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Safely compute percentage, guarding against division by zero */
|
|
31
|
-
function safePercent(used: number, limit: number): number {
|
|
32
|
-
if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) return 0;
|
|
33
|
-
return Math.max(0, Math.min(100, (used / limit) * 100));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Parse currency string like "$1,234.56" to number */
|
|
37
|
-
function parseCurrency(value: string): number {
|
|
38
|
-
const n = Number(value.replace(/[^0-9.-]/g, ""));
|
|
39
|
-
return Number.isFinite(n) ? n : 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function toWindows(quotas: QuotasResponse): QuotaWindow[] {
|
|
43
|
-
const windows: QuotaWindow[] = [];
|
|
44
|
-
|
|
45
|
-
// Weekly token limit (credits-based)
|
|
46
|
-
if (quotas.weeklyTokenLimit) {
|
|
47
|
-
const { weeklyTokenLimit } = quotas;
|
|
48
|
-
const limitValue = parseCurrency(weeklyTokenLimit.maxCredits);
|
|
49
|
-
const remainingValue = parseCurrency(weeklyTokenLimit.remainingCredits);
|
|
50
|
-
windows.push({
|
|
51
|
-
label: "Credits",
|
|
52
|
-
usedPercent: Math.max(
|
|
53
|
-
0,
|
|
54
|
-
Math.min(100, 100 - weeklyTokenLimit.percentRemaining),
|
|
55
|
-
),
|
|
56
|
-
resetsAt: new Date(weeklyTokenLimit.nextRegenAt),
|
|
57
|
-
windowSeconds: 7 * 24 * 60 * 60,
|
|
58
|
-
usedValue: limitValue - remainingValue,
|
|
59
|
-
limitValue,
|
|
60
|
-
isCredits: true,
|
|
61
|
-
nextRegenCredits: weeklyTokenLimit.nextRegenCredits,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Rolling 5-hour limit (request-based)
|
|
66
|
-
if (quotas.rollingFiveHourLimit && quotas.rollingFiveHourLimit.max > 0) {
|
|
67
|
-
const { rollingFiveHourLimit } = quotas;
|
|
68
|
-
windows.push({
|
|
69
|
-
label: "5h",
|
|
70
|
-
usedPercent: safePercent(
|
|
71
|
-
rollingFiveHourLimit.max - rollingFiveHourLimit.remaining,
|
|
72
|
-
rollingFiveHourLimit.max,
|
|
73
|
-
),
|
|
74
|
-
resetsAt: new Date(rollingFiveHourLimit.nextTickAt),
|
|
75
|
-
windowSeconds: 5 * 60 * 60,
|
|
76
|
-
usedValue: rollingFiveHourLimit.max - rollingFiveHourLimit.remaining,
|
|
77
|
-
limitValue: rollingFiveHourLimit.max,
|
|
78
|
-
isLimited: rollingFiveHourLimit.limited,
|
|
79
|
-
tickPercent: rollingFiveHourLimit.tickPercent,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Legacy subscription (fallback if rollingFiveHourLimit not available)
|
|
84
|
-
if (
|
|
85
|
-
!quotas.rollingFiveHourLimit &&
|
|
86
|
-
quotas.subscription?.limit &&
|
|
87
|
-
quotas.subscription.limit > 0
|
|
88
|
-
) {
|
|
89
|
-
windows.push({
|
|
90
|
-
label: "Completions",
|
|
91
|
-
usedPercent: safePercent(
|
|
92
|
-
quotas.subscription.requests,
|
|
93
|
-
quotas.subscription.limit,
|
|
94
|
-
),
|
|
95
|
-
resetsAt: new Date(quotas.subscription.renewsAt),
|
|
96
|
-
windowSeconds: 5 * 60 * 60,
|
|
97
|
-
usedValue: quotas.subscription.requests,
|
|
98
|
-
limitValue: quotas.subscription.limit,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (quotas.search?.hourly?.limit && quotas.search.hourly.limit > 0) {
|
|
103
|
-
windows.push({
|
|
104
|
-
label: "Search",
|
|
105
|
-
usedPercent: safePercent(
|
|
106
|
-
quotas.search.hourly.requests,
|
|
107
|
-
quotas.search.hourly.limit,
|
|
108
|
-
),
|
|
109
|
-
resetsAt: new Date(quotas.search.hourly.renewsAt),
|
|
110
|
-
windowSeconds: 60 * 60,
|
|
111
|
-
usedValue: quotas.search.hourly.requests,
|
|
112
|
-
limitValue: quotas.search.hourly.limit,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (quotas.freeToolCalls?.limit && quotas.freeToolCalls.limit > 0) {
|
|
117
|
-
windows.push({
|
|
118
|
-
label: "Free Tool Calls",
|
|
119
|
-
usedPercent: safePercent(
|
|
120
|
-
quotas.freeToolCalls.requests,
|
|
121
|
-
quotas.freeToolCalls.limit,
|
|
122
|
-
),
|
|
123
|
-
resetsAt: new Date(quotas.freeToolCalls.renewsAt),
|
|
124
|
-
windowSeconds: 24 * 60 * 60,
|
|
125
|
-
usedValue: quotas.freeToolCalls.requests,
|
|
126
|
-
limitValue: quotas.freeToolCalls.limit,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return windows;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function getPacePercent(window: QuotaWindow): number | null {
|
|
134
|
-
const totalMs = window.windowSeconds * 1000;
|
|
135
|
-
if (totalMs <= 0) return null;
|
|
136
|
-
const remainingMs = window.resetsAt.getTime() - Date.now();
|
|
137
|
-
const elapsedMs = totalMs - remainingMs;
|
|
138
|
-
return Math.max(0, Math.min(100, (elapsedMs / totalMs) * 100));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function getProjectedPercent(
|
|
142
|
-
usedPercent: number,
|
|
143
|
-
pacePercent: number | null,
|
|
144
|
-
): number {
|
|
145
|
-
if (pacePercent === null) return usedPercent;
|
|
146
|
-
const effectivePace = Math.max(5, pacePercent);
|
|
147
|
-
return Math.max(0, (usedPercent / effectivePace) * 100);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function getSeverity(
|
|
151
|
-
projectedPercent: number,
|
|
152
|
-
pacePercent: number | null,
|
|
153
|
-
): "success" | "warning" | "error" {
|
|
154
|
-
if (pacePercent === null) {
|
|
155
|
-
if (projectedPercent >= 100) return "error";
|
|
156
|
-
if (projectedPercent >= 90) return "warning";
|
|
157
|
-
return "success";
|
|
158
|
-
}
|
|
159
|
-
// Dynamic thresholds based on window progress
|
|
160
|
-
const progress = pacePercent / 100;
|
|
161
|
-
const warnThreshold = 260 - (260 - 120) * progress;
|
|
162
|
-
const highThreshold = 320 - (320 - 145) * progress;
|
|
163
|
-
const criticalThreshold = 400 - (400 - 170) * progress;
|
|
164
|
-
|
|
165
|
-
if (projectedPercent >= criticalThreshold) return "error";
|
|
166
|
-
if (projectedPercent >= highThreshold) return "error";
|
|
167
|
-
if (projectedPercent >= warnThreshold) return "warning";
|
|
168
|
-
return "success";
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function formatResetDateTime(date: Date): string {
|
|
172
|
-
const now = new Date();
|
|
173
|
-
const isToday =
|
|
174
|
-
date.getDate() === now.getDate() &&
|
|
175
|
-
date.getMonth() === now.getMonth() &&
|
|
176
|
-
date.getFullYear() === now.getFullYear();
|
|
177
|
-
|
|
178
|
-
const timeStr = date.toLocaleTimeString("en-US", {
|
|
179
|
-
hour: "numeric",
|
|
180
|
-
minute: "2-digit",
|
|
181
|
-
hour12: true,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
if (isToday) {
|
|
185
|
-
return `today ${timeStr}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const dateStr = date.toLocaleDateString("en-US", {
|
|
189
|
-
month: "short",
|
|
190
|
-
day: "numeric",
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
return `${dateStr} ${timeStr}`;
|
|
19
|
+
/**
|
|
20
|
+
* Convert a foreground ANSI escape to its background equivalent.
|
|
21
|
+
* Handles truecolor (38;2), 256-color (38;5), and basic (3X) escapes.
|
|
22
|
+
*/
|
|
23
|
+
function fgAnsiToBg(fgAnsi: string): string {
|
|
24
|
+
// Convert fg escape sequences to bg equivalents by replacing the
|
|
25
|
+
// discriminating digit: 38 (truecolor/256) → 48, 3X (basic) → 4X.
|
|
26
|
+
return fgAnsi
|
|
27
|
+
.split("[38;")
|
|
28
|
+
.join("[48;")
|
|
29
|
+
.replace(/\[3([0-9])m/g, "[4$1m");
|
|
194
30
|
}
|
|
195
31
|
|
|
196
32
|
function renderProgressBar(
|
|
@@ -202,45 +38,40 @@ function renderProgressBar(
|
|
|
202
38
|
): string {
|
|
203
39
|
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
204
40
|
const filled = Math.round((clamped / 100) * width);
|
|
205
|
-
const paceIndex =
|
|
206
|
-
pacePercent === null || pacePercent === undefined || pacePercent <= percent
|
|
207
|
-
? null
|
|
208
|
-
: Math.round((Math.max(0, Math.min(100, pacePercent)) / 100) * width);
|
|
209
41
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
42
|
+
const showPace =
|
|
43
|
+
pacePercent !== null &&
|
|
44
|
+
pacePercent !== undefined &&
|
|
45
|
+
pacePercent >= 5 &&
|
|
46
|
+
Math.abs(pacePercent - percent) >= 5;
|
|
47
|
+
const paceIndex = showPace
|
|
48
|
+
? Math.min(
|
|
49
|
+
width - 1,
|
|
50
|
+
Math.round(
|
|
51
|
+
(Math.max(0, Math.min(100, pacePercent ?? 0)) / 100) * width,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
: null;
|
|
220
55
|
|
|
221
|
-
|
|
222
|
-
}
|
|
56
|
+
const reset = "\x1b[0m";
|
|
223
57
|
|
|
224
|
-
function renderSimpleIndicatorBar(
|
|
225
|
-
usedPercent: number,
|
|
226
|
-
width: number,
|
|
227
|
-
theme: Theme,
|
|
228
|
-
severity: "success" | "warning" | "error",
|
|
229
|
-
): string {
|
|
230
|
-
const clampedPercent = Math.max(0, Math.min(100, usedPercent));
|
|
231
|
-
// Clamp to width - 1 to avoid off-by-one when usedPercent === 100
|
|
232
|
-
const usedIndex = Math.min(
|
|
233
|
-
Math.round((clampedPercent / 100) * width),
|
|
234
|
-
width - 1,
|
|
235
|
-
);
|
|
236
58
|
const parts: string[] = [];
|
|
237
|
-
|
|
238
|
-
// Hide marker when within 5% of edges
|
|
239
|
-
const showMarker = clampedPercent >= 5 && clampedPercent <= 95;
|
|
240
|
-
|
|
241
59
|
for (let idx = 0; idx < width; idx++) {
|
|
242
|
-
if (
|
|
243
|
-
|
|
60
|
+
if (paceIndex !== null && idx === paceIndex) {
|
|
61
|
+
// Inside fill = ahead of pace: accent. Outside = behind pace: severity.
|
|
62
|
+
const markerColor = idx < filled ? "accent" : fillColor;
|
|
63
|
+
// Inside fill: set bg to fill color so `|` doesn't expose the panel bg
|
|
64
|
+
// through the thin character. Outside fill: ░ uses terminal bg naturally,
|
|
65
|
+
// so leave bg unset to match.
|
|
66
|
+
if (idx < filled) {
|
|
67
|
+
const bgAnsi = fgAnsiToBg(theme.getFgAnsi(fillColor));
|
|
68
|
+
const fgAnsi = theme.getFgAnsi(markerColor);
|
|
69
|
+
parts.push(`${bgAnsi}${fgAnsi}|${reset}`);
|
|
70
|
+
} else {
|
|
71
|
+
parts.push(theme.fg(markerColor, "|"));
|
|
72
|
+
}
|
|
73
|
+
} else if (idx < filled) {
|
|
74
|
+
parts.push(theme.fg(fillColor, "█"));
|
|
244
75
|
} else {
|
|
245
76
|
parts.push(theme.fg("dim", "░"));
|
|
246
77
|
}
|
|
@@ -254,12 +85,19 @@ export class QuotasComponent implements Component {
|
|
|
254
85
|
private theme: Theme;
|
|
255
86
|
private tui: TUI;
|
|
256
87
|
private onClose: () => void;
|
|
88
|
+
private onRefetch: () => void;
|
|
257
89
|
private loader: Loader | null = null;
|
|
258
90
|
|
|
259
|
-
constructor(
|
|
91
|
+
constructor(
|
|
92
|
+
theme: Theme,
|
|
93
|
+
tui: TUI,
|
|
94
|
+
onClose: () => void,
|
|
95
|
+
onRefetch: () => void,
|
|
96
|
+
) {
|
|
260
97
|
this.theme = theme;
|
|
261
98
|
this.tui = tui;
|
|
262
99
|
this.onClose = onClose;
|
|
100
|
+
this.onRefetch = onRefetch;
|
|
263
101
|
this.startLoader();
|
|
264
102
|
}
|
|
265
103
|
|
|
@@ -278,7 +116,10 @@ export class QuotasComponent implements Component {
|
|
|
278
116
|
}
|
|
279
117
|
|
|
280
118
|
setState(state: QuotasState): void {
|
|
281
|
-
if (
|
|
119
|
+
if (state.type === "loading") {
|
|
120
|
+
this.loader?.stop();
|
|
121
|
+
this.startLoader();
|
|
122
|
+
} else if (this.state.type === "loading") {
|
|
282
123
|
this.loader?.stop();
|
|
283
124
|
this.loader = null;
|
|
284
125
|
}
|
|
@@ -290,6 +131,10 @@ export class QuotasComponent implements Component {
|
|
|
290
131
|
this.onClose();
|
|
291
132
|
return true;
|
|
292
133
|
}
|
|
134
|
+
if (data === "r") {
|
|
135
|
+
this.onRefetch();
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
293
138
|
return false;
|
|
294
139
|
}
|
|
295
140
|
|
|
@@ -326,7 +171,7 @@ export class QuotasComponent implements Component {
|
|
|
326
171
|
}
|
|
327
172
|
|
|
328
173
|
lines.push("");
|
|
329
|
-
lines.push(this.theme.fg("dim", " q/Esc to close"));
|
|
174
|
+
lines.push(this.theme.fg("dim", " r to refresh q/Esc to close"));
|
|
330
175
|
lines.push(...border.render(width));
|
|
331
176
|
|
|
332
177
|
return lines;
|
|
@@ -362,134 +207,44 @@ export class QuotasComponent implements Component {
|
|
|
362
207
|
const lines: string[] = [];
|
|
363
208
|
const theme = this.theme;
|
|
364
209
|
|
|
365
|
-
const
|
|
366
|
-
const
|
|
367
|
-
window.usedPercent,
|
|
368
|
-
pacePercent,
|
|
369
|
-
);
|
|
370
|
-
const severity = getSeverity(projectedPercent, pacePercent);
|
|
210
|
+
const assessment = assessWindow(window);
|
|
211
|
+
const color = getSeverityColor(assessment.severity);
|
|
371
212
|
|
|
372
213
|
// Label
|
|
373
214
|
lines.push(
|
|
374
215
|
truncateToWidth(` ${theme.fg("accent", window.label)}`, maxWidth),
|
|
375
216
|
);
|
|
376
217
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
maxWidth,
|
|
396
|
-
),
|
|
397
|
-
);
|
|
398
|
-
} else {
|
|
399
|
-
// Traditional progress bar for legacy quota types
|
|
400
|
-
const bar = renderProgressBar(
|
|
401
|
-
window.usedPercent,
|
|
402
|
-
barWidth,
|
|
403
|
-
theme,
|
|
404
|
-
severity,
|
|
405
|
-
pacePercent,
|
|
406
|
-
);
|
|
407
|
-
const usedStr = `${window.usedValue.toLocaleString()}/${window.limitValue.toLocaleString()} (${Math.round(window.usedPercent)}%)`;
|
|
408
|
-
lines.push(
|
|
409
|
-
truncateToWidth(` ${bar} ${theme.fg(severity, usedStr)}`, maxWidth),
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Metadata: estimated + pace left, reset time right
|
|
414
|
-
const leftParts: string[] = [];
|
|
415
|
-
|
|
416
|
-
// Show tick info for rolling window
|
|
417
|
-
if (window.tickPercent !== undefined) {
|
|
418
|
-
const now = Date.now();
|
|
419
|
-
const remainingMs = window.resetsAt.getTime() - now;
|
|
420
|
-
const remainingMins = Math.ceil(remainingMs / (1000 * 60));
|
|
421
|
-
const remainingSecs = Math.ceil(remainingMs / 1000);
|
|
422
|
-
const timeStr =
|
|
423
|
-
remainingMs <= 0
|
|
424
|
-
? "now"
|
|
425
|
-
: remainingMins >= 1
|
|
426
|
-
? `${remainingMins}m`
|
|
427
|
-
: `${remainingSecs}s`;
|
|
428
|
-
const tickValue = (window.tickPercent / 100) * window.limitValue;
|
|
429
|
-
const tickStr = `+${tickValue.toFixed(1)} in ${timeStr}`;
|
|
430
|
-
leftParts.push(theme.fg("dim", tickStr));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Show next regen credits for weekly token limit
|
|
434
|
-
if (window.nextRegenCredits !== undefined) {
|
|
435
|
-
const now = Date.now();
|
|
436
|
-
const remainingMs = window.resetsAt.getTime() - now;
|
|
437
|
-
const remainingHours = Math.ceil(remainingMs / (1000 * 60 * 60));
|
|
438
|
-
const remainingMins = Math.ceil(remainingMs / (1000 * 60));
|
|
439
|
-
const timeStr =
|
|
440
|
-
remainingMs <= 0
|
|
441
|
-
? "now"
|
|
442
|
-
: remainingHours >= 1
|
|
443
|
-
? `${remainingHours}h`
|
|
444
|
-
: `${remainingMins}m`;
|
|
445
|
-
const regenStr = `+${window.nextRegenCredits} in ${timeStr}`;
|
|
446
|
-
leftParts.push(theme.fg("dim", regenStr));
|
|
447
|
-
}
|
|
218
|
+
// Bar + usage
|
|
219
|
+
const bar = renderProgressBar(
|
|
220
|
+
window.usedPercent,
|
|
221
|
+
barWidth,
|
|
222
|
+
theme,
|
|
223
|
+
color,
|
|
224
|
+
assessment.pacePercent,
|
|
225
|
+
);
|
|
226
|
+
const usedStr = window.isCurrency
|
|
227
|
+
? `${Math.round(window.usedPercent)}%/$${window.limitValue.toFixed(2)}`
|
|
228
|
+
: `${Math.round(window.usedPercent)}%/${window.limitValue}`;
|
|
229
|
+
const limitedBadge = window.limited ? theme.fg("error", " LIMITED") : "";
|
|
230
|
+
lines.push(
|
|
231
|
+
truncateToWidth(
|
|
232
|
+
` ${bar} ${theme.fg(color, usedStr)}${limitedBadge}`,
|
|
233
|
+
maxWidth,
|
|
234
|
+
),
|
|
235
|
+
);
|
|
448
236
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
window.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
? theme.fg(severity, estStr)
|
|
458
|
-
: theme.fg("dim", estStr),
|
|
237
|
+
// Subtitle: next event info
|
|
238
|
+
if (window.nextLabel) {
|
|
239
|
+
const timeStr = formatTimeRemaining(window.resetsAt);
|
|
240
|
+
const subtitleStr = window.nextAmount
|
|
241
|
+
? `${window.nextAmount} in ${timeStr}`
|
|
242
|
+
: `${window.nextLabel} in ${timeStr}`;
|
|
243
|
+
lines.push(
|
|
244
|
+
truncateToWidth(` ${theme.fg("dim", subtitleStr)}`, maxWidth),
|
|
459
245
|
);
|
|
460
246
|
}
|
|
461
247
|
|
|
462
|
-
if (
|
|
463
|
-
pacePercent !== null &&
|
|
464
|
-
window.tickPercent === undefined &&
|
|
465
|
-
window.nextRegenCredits === undefined
|
|
466
|
-
) {
|
|
467
|
-
const paceDiff = window.usedPercent - pacePercent;
|
|
468
|
-
if (Math.abs(paceDiff) > 5) {
|
|
469
|
-
if (paceDiff > 0) {
|
|
470
|
-
leftParts.push(
|
|
471
|
-
theme.fg("warning", `${Math.round(Math.abs(paceDiff))}% ahead`),
|
|
472
|
-
);
|
|
473
|
-
} else {
|
|
474
|
-
leftParts.push(
|
|
475
|
-
theme.fg("success", `${Math.round(Math.abs(paceDiff))}% behind`),
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const leftStr = leftParts.join(" ");
|
|
482
|
-
const resetStr = formatResetDateTime(window.resetsAt);
|
|
483
|
-
const rightStr = theme.fg("dim", resetStr);
|
|
484
|
-
|
|
485
|
-
const leftW = visibleWidth(leftStr);
|
|
486
|
-
const rightW = visibleWidth(rightStr);
|
|
487
|
-
const gap = Math.max(2, barWidth - leftW - rightW);
|
|
488
|
-
|
|
489
|
-
lines.push(
|
|
490
|
-
truncateToWidth(` ${leftStr}${" ".repeat(gap)}${rightStr}`, maxWidth),
|
|
491
|
-
);
|
|
492
|
-
|
|
493
248
|
return lines;
|
|
494
249
|
}
|
|
495
250
|
|
|
@@ -36,7 +36,7 @@ const SYNTHETIC_REASONING_EFFORT_MAP = {
|
|
|
36
36
|
} as const;
|
|
37
37
|
|
|
38
38
|
export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
39
|
-
// API: hf:zai-org/GLM-4.7 → ctx=202752
|
|
39
|
+
// API: hf:zai-org/GLM-4.7 → ctx=202752
|
|
40
40
|
{
|
|
41
41
|
id: "hf:zai-org/GLM-4.7",
|
|
42
42
|
name: "zai-org/GLM-4.7",
|
|
@@ -45,11 +45,11 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
45
45
|
supportsReasoningEffort: true,
|
|
46
46
|
reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
|
|
47
47
|
},
|
|
48
|
-
input: ["text"
|
|
48
|
+
input: ["text"],
|
|
49
49
|
cost: {
|
|
50
|
-
input:
|
|
50
|
+
input: 0.45,
|
|
51
51
|
output: 2.19,
|
|
52
|
-
cacheRead:
|
|
52
|
+
cacheRead: 0.45,
|
|
53
53
|
cacheWrite: 0,
|
|
54
54
|
},
|
|
55
55
|
contextWindow: 202752,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { clearAlertState, triggerCheck } from "./notifier";
|
|
3
|
+
|
|
4
|
+
export default async function (pi: ExtensionAPI) {
|
|
5
|
+
// Session start: reset local warning state and run an immediate check
|
|
6
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
7
|
+
if (ctx.model?.provider !== "synthetic") return;
|
|
8
|
+
clearAlertState();
|
|
9
|
+
triggerCheck(ctx, ctx.model, false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Check after agent turn - only warn for newly crossed thresholds
|
|
13
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
14
|
+
if (ctx.model?.provider !== "synthetic") return;
|
|
15
|
+
triggerCheck(ctx, ctx.model, true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Clear state on shutdown
|
|
19
|
+
pi.on("session_shutdown", async () => {
|
|
20
|
+
clearAlertState();
|
|
21
|
+
});
|
|
22
|
+
}
|