@aliou/pi-synthetic 0.10.1 → 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/package.json
CHANGED
|
@@ -15,50 +15,65 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
|
15
15
|
ctx.ui.notify(MISSING_AUTH_MESSAGE, "warning");
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
+
const key: string = apiKey;
|
|
18
19
|
|
|
19
20
|
const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} else {
|
|
31
|
-
component.setState({ type: "loaded", quotas });
|
|
32
|
-
}
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const component = new QuotasComponent(
|
|
23
|
+
theme,
|
|
24
|
+
tui,
|
|
25
|
+
() => {
|
|
26
|
+
controller.abort();
|
|
27
|
+
done(null);
|
|
28
|
+
},
|
|
29
|
+
() => {
|
|
30
|
+
component.setState({ type: "loading" });
|
|
33
31
|
tui.requestRender();
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
void loadQuotas();
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
async function loadQuotas(): Promise<void> {
|
|
37
|
+
const fetchResult = await fetchQuotas(key, controller.signal);
|
|
38
|
+
if (controller.signal.aborted) return;
|
|
39
|
+
if (fetchResult.success) {
|
|
40
|
+
component.setState({
|
|
41
|
+
type: "loaded",
|
|
42
|
+
quotas: fetchResult.data.quotas,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
36
45
|
component.setState({
|
|
37
46
|
type: "error",
|
|
38
|
-
message:
|
|
39
|
-
"Failed to fetch quotas. Check your Synthetic subscription status.",
|
|
47
|
+
message: fetchResult.error.message,
|
|
40
48
|
});
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
}
|
|
50
|
+
tui.requestRender();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
void loadQuotas();
|
|
43
54
|
|
|
44
55
|
return {
|
|
45
56
|
render: (width: number) => component.render(width),
|
|
46
57
|
invalidate: () => component.invalidate(),
|
|
47
58
|
handleInput: (data: string) => component.handleInput(data),
|
|
59
|
+
dispose: () => {
|
|
60
|
+
controller.abort();
|
|
61
|
+
component.destroy();
|
|
62
|
+
},
|
|
48
63
|
};
|
|
49
64
|
});
|
|
50
65
|
|
|
51
|
-
//
|
|
66
|
+
// Non-interactive fallback (RPC, print, JSON modes)
|
|
52
67
|
if (result === undefined) {
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
68
|
+
const fetchResult = await fetchQuotas(key);
|
|
69
|
+
if (!fetchResult.success) {
|
|
55
70
|
ctx.ui.notify(
|
|
56
|
-
JSON.stringify({ error:
|
|
71
|
+
JSON.stringify({ error: fetchResult.error.message }),
|
|
57
72
|
"error",
|
|
58
73
|
);
|
|
59
74
|
return;
|
|
60
75
|
}
|
|
61
|
-
ctx.ui.notify(JSON.stringify(quotas
|
|
76
|
+
ctx.ui.notify(JSON.stringify(fetchResult.data.quotas), "info");
|
|
62
77
|
}
|
|
63
78
|
},
|
|
64
79
|
});
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import type { Component } from "@mariozechner/pi-tui";
|
|
4
|
-
import {
|
|
5
|
-
matchesKey,
|
|
6
|
-
truncateToWidth,
|
|
7
|
-
visibleWidth,
|
|
8
|
-
} from "@mariozechner/pi-tui";
|
|
3
|
+
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
4
|
+
import { Loader, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
9
5
|
import type { QuotasResponse } from "../../../types/quotas";
|
|
10
6
|
|
|
11
7
|
type QuotasState =
|
|
@@ -20,10 +16,12 @@ interface QuotaWindow {
|
|
|
20
16
|
windowSeconds: number;
|
|
21
17
|
usedValue: number;
|
|
22
18
|
limitValue: number;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
isCurrency?: boolean;
|
|
20
|
+
showPace?: boolean;
|
|
21
|
+
paceScale?: number;
|
|
22
|
+
limited?: boolean;
|
|
23
|
+
nextAmount?: string;
|
|
24
|
+
nextLabel?: string;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
/** Safely compute percentage, guarding against division by zero */
|
|
@@ -41,80 +39,65 @@ function parseCurrency(value: string): number {
|
|
|
41
39
|
function toWindows(quotas: QuotasResponse): QuotaWindow[] {
|
|
42
40
|
const windows: QuotaWindow[] = [];
|
|
43
41
|
|
|
44
|
-
// Weekly token limit (credits-based)
|
|
45
42
|
if (quotas.weeklyTokenLimit) {
|
|
46
43
|
const { weeklyTokenLimit } = quotas;
|
|
47
44
|
const limitValue = parseCurrency(weeklyTokenLimit.maxCredits);
|
|
48
45
|
const remainingValue = parseCurrency(weeklyTokenLimit.remainingCredits);
|
|
49
46
|
windows.push({
|
|
50
|
-
label: "Credits",
|
|
47
|
+
label: "Credits / week",
|
|
51
48
|
usedPercent: Math.max(
|
|
52
49
|
0,
|
|
53
50
|
Math.min(100, 100 - weeklyTokenLimit.percentRemaining),
|
|
54
51
|
),
|
|
55
52
|
resetsAt: new Date(weeklyTokenLimit.nextRegenAt),
|
|
56
|
-
windowSeconds:
|
|
53
|
+
windowSeconds: 24 * 60 * 60,
|
|
57
54
|
usedValue: limitValue - remainingValue,
|
|
58
55
|
limitValue,
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
isCurrency: true,
|
|
57
|
+
showPace: true,
|
|
58
|
+
paceScale: 1 / 7,
|
|
59
|
+
nextAmount: `+${weeklyTokenLimit.nextRegenCredits}`,
|
|
60
|
+
nextLabel: "Next regen",
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Rolling 5-hour limit (request-based)
|
|
65
64
|
if (quotas.rollingFiveHourLimit && quotas.rollingFiveHourLimit.max > 0) {
|
|
66
65
|
const { rollingFiveHourLimit } = quotas;
|
|
66
|
+
const used = rollingFiveHourLimit.max - rollingFiveHourLimit.remaining;
|
|
67
|
+
const tickAmount =
|
|
68
|
+
rollingFiveHourLimit.tickPercent * rollingFiveHourLimit.max;
|
|
67
69
|
windows.push({
|
|
68
|
-
label: "5h",
|
|
69
|
-
usedPercent: safePercent(
|
|
70
|
-
rollingFiveHourLimit.max - rollingFiveHourLimit.remaining,
|
|
71
|
-
rollingFiveHourLimit.max,
|
|
72
|
-
),
|
|
70
|
+
label: "Requests / 5h",
|
|
71
|
+
usedPercent: safePercent(used, rollingFiveHourLimit.max),
|
|
73
72
|
resetsAt: new Date(rollingFiveHourLimit.nextTickAt),
|
|
74
73
|
windowSeconds: 5 * 60 * 60,
|
|
75
|
-
usedValue:
|
|
74
|
+
usedValue: Math.round(used),
|
|
76
75
|
limitValue: rollingFiveHourLimit.max,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Legacy subscription (fallback if rollingFiveHourLimit not available)
|
|
83
|
-
if (
|
|
84
|
-
!quotas.rollingFiveHourLimit &&
|
|
85
|
-
quotas.subscription?.limit &&
|
|
86
|
-
quotas.subscription.limit > 0
|
|
87
|
-
) {
|
|
88
|
-
windows.push({
|
|
89
|
-
label: "Completions",
|
|
90
|
-
usedPercent: safePercent(
|
|
91
|
-
quotas.subscription.requests,
|
|
92
|
-
quotas.subscription.limit,
|
|
93
|
-
),
|
|
94
|
-
resetsAt: new Date(quotas.subscription.renewsAt),
|
|
95
|
-
windowSeconds: 5 * 60 * 60,
|
|
96
|
-
usedValue: quotas.subscription.requests,
|
|
97
|
-
limitValue: quotas.subscription.limit,
|
|
76
|
+
showPace: false,
|
|
77
|
+
limited: rollingFiveHourLimit.limited,
|
|
78
|
+
nextAmount: `+${tickAmount.toFixed(1)}`,
|
|
79
|
+
nextLabel: "Next tick",
|
|
98
80
|
});
|
|
99
81
|
}
|
|
100
82
|
|
|
101
83
|
if (quotas.search?.hourly?.limit && quotas.search.hourly.limit > 0) {
|
|
84
|
+
const { hourly } = quotas.search;
|
|
102
85
|
windows.push({
|
|
103
|
-
label: "Search",
|
|
104
|
-
usedPercent: safePercent(
|
|
105
|
-
|
|
106
|
-
quotas.search.hourly.limit,
|
|
107
|
-
),
|
|
108
|
-
resetsAt: new Date(quotas.search.hourly.renewsAt),
|
|
86
|
+
label: "Search / hour",
|
|
87
|
+
usedPercent: safePercent(hourly.requests, hourly.limit),
|
|
88
|
+
resetsAt: new Date(hourly.renewsAt),
|
|
109
89
|
windowSeconds: 60 * 60,
|
|
110
|
-
usedValue:
|
|
111
|
-
limitValue:
|
|
90
|
+
usedValue: hourly.requests,
|
|
91
|
+
limitValue: hourly.limit,
|
|
92
|
+
showPace: true,
|
|
93
|
+
paceScale: 1,
|
|
94
|
+
nextLabel: "Resets",
|
|
112
95
|
});
|
|
113
96
|
}
|
|
114
97
|
|
|
115
98
|
if (quotas.freeToolCalls?.limit && quotas.freeToolCalls.limit > 0) {
|
|
116
99
|
windows.push({
|
|
117
|
-
label: "Free Tool Calls",
|
|
100
|
+
label: "Free Tool Calls / day",
|
|
118
101
|
usedPercent: safePercent(
|
|
119
102
|
quotas.freeToolCalls.requests,
|
|
120
103
|
quotas.freeToolCalls.limit,
|
|
@@ -123,6 +106,9 @@ function toWindows(quotas: QuotasResponse): QuotaWindow[] {
|
|
|
123
106
|
windowSeconds: 24 * 60 * 60,
|
|
124
107
|
usedValue: quotas.freeToolCalls.requests,
|
|
125
108
|
limitValue: quotas.freeToolCalls.limit,
|
|
109
|
+
showPace: true,
|
|
110
|
+
paceScale: 1,
|
|
111
|
+
nextLabel: "Resets",
|
|
126
112
|
});
|
|
127
113
|
}
|
|
128
114
|
|
|
@@ -167,29 +153,28 @@ function getSeverity(
|
|
|
167
153
|
return "success";
|
|
168
154
|
}
|
|
169
155
|
|
|
170
|
-
function
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
hour12: true,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (isToday) {
|
|
184
|
-
return `today ${timeStr}`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const dateStr = date.toLocaleDateString("en-US", {
|
|
188
|
-
month: "short",
|
|
189
|
-
day: "numeric",
|
|
190
|
-
});
|
|
156
|
+
function formatTimeRemaining(date: Date): string {
|
|
157
|
+
const ms = date.getTime() - Date.now();
|
|
158
|
+
if (ms <= 0) return "now";
|
|
159
|
+
const totalMins = Math.ceil(ms / (1000 * 60));
|
|
160
|
+
const hours = Math.floor(totalMins / 60);
|
|
161
|
+
const mins = totalMins % 60;
|
|
162
|
+
if (hours >= 1) return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
|
163
|
+
const totalSecs = Math.ceil(ms / 1000);
|
|
164
|
+
return totalMins >= 1 ? `${totalMins}m` : `${totalSecs}s`;
|
|
165
|
+
}
|
|
191
166
|
|
|
192
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Convert a foreground ANSI escape to its background equivalent.
|
|
169
|
+
* Handles truecolor (38;2), 256-color (38;5), and basic (3X) escapes.
|
|
170
|
+
*/
|
|
171
|
+
function fgAnsiToBg(fgAnsi: string): string {
|
|
172
|
+
// Convert fg escape sequences to bg equivalents by replacing the
|
|
173
|
+
// discriminating digit: 38 (truecolor/256) → 48, 3X (basic) → 4X.
|
|
174
|
+
return fgAnsi
|
|
175
|
+
.split("[38;")
|
|
176
|
+
.join("[48;")
|
|
177
|
+
.replace(/\[3([0-9])m/g, "[4$1m");
|
|
193
178
|
}
|
|
194
179
|
|
|
195
180
|
function renderProgressBar(
|
|
@@ -201,45 +186,40 @@ function renderProgressBar(
|
|
|
201
186
|
): string {
|
|
202
187
|
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
203
188
|
const filled = Math.round((clamped / 100) * width);
|
|
204
|
-
const paceIndex =
|
|
205
|
-
pacePercent === null || pacePercent === undefined || pacePercent <= percent
|
|
206
|
-
? null
|
|
207
|
-
: Math.round((Math.max(0, Math.min(100, pacePercent)) / 100) * width);
|
|
208
189
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
190
|
+
const showPace =
|
|
191
|
+
pacePercent !== null &&
|
|
192
|
+
pacePercent !== undefined &&
|
|
193
|
+
pacePercent >= 5 &&
|
|
194
|
+
Math.abs(pacePercent - percent) >= 5;
|
|
195
|
+
const paceIndex = showPace
|
|
196
|
+
? Math.min(
|
|
197
|
+
width - 1,
|
|
198
|
+
Math.round(
|
|
199
|
+
(Math.max(0, Math.min(100, pacePercent ?? 0)) / 100) * width,
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
: null;
|
|
219
203
|
|
|
220
|
-
|
|
221
|
-
}
|
|
204
|
+
const reset = "\x1b[0m";
|
|
222
205
|
|
|
223
|
-
function renderSimpleIndicatorBar(
|
|
224
|
-
usedPercent: number,
|
|
225
|
-
width: number,
|
|
226
|
-
theme: Theme,
|
|
227
|
-
severity: "success" | "warning" | "error",
|
|
228
|
-
): string {
|
|
229
|
-
const clampedPercent = Math.max(0, Math.min(100, usedPercent));
|
|
230
|
-
// Clamp to width - 1 to avoid off-by-one when usedPercent === 100
|
|
231
|
-
const usedIndex = Math.min(
|
|
232
|
-
Math.round((clampedPercent / 100) * width),
|
|
233
|
-
width - 1,
|
|
234
|
-
);
|
|
235
206
|
const parts: string[] = [];
|
|
236
|
-
|
|
237
|
-
// Hide marker when within 5% of edges
|
|
238
|
-
const showMarker = clampedPercent >= 5 && clampedPercent <= 95;
|
|
239
|
-
|
|
240
207
|
for (let idx = 0; idx < width; idx++) {
|
|
241
|
-
if (
|
|
242
|
-
|
|
208
|
+
if (paceIndex !== null && idx === paceIndex) {
|
|
209
|
+
// Inside fill = ahead of pace: accent. Outside = behind pace: severity.
|
|
210
|
+
const markerColor = idx < filled ? "accent" : fillColor;
|
|
211
|
+
// Inside fill: set bg to fill color so `|` doesn't expose the panel bg
|
|
212
|
+
// through the thin character. Outside fill: ░ uses terminal bg naturally,
|
|
213
|
+
// so leave bg unset to match.
|
|
214
|
+
if (idx < filled) {
|
|
215
|
+
const bgAnsi = fgAnsiToBg(theme.getFgAnsi(fillColor));
|
|
216
|
+
const fgAnsi = theme.getFgAnsi(markerColor);
|
|
217
|
+
parts.push(`${bgAnsi}${fgAnsi}|${reset}`);
|
|
218
|
+
} else {
|
|
219
|
+
parts.push(theme.fg(markerColor, "|"));
|
|
220
|
+
}
|
|
221
|
+
} else if (idx < filled) {
|
|
222
|
+
parts.push(theme.fg(fillColor, "█"));
|
|
243
223
|
} else {
|
|
244
224
|
parts.push(theme.fg("dim", "░"));
|
|
245
225
|
}
|
|
@@ -251,14 +231,46 @@ function renderSimpleIndicatorBar(
|
|
|
251
231
|
export class QuotasComponent implements Component {
|
|
252
232
|
private state: QuotasState = { type: "loading" };
|
|
253
233
|
private theme: Theme;
|
|
234
|
+
private tui: TUI;
|
|
254
235
|
private onClose: () => void;
|
|
255
|
-
|
|
256
|
-
|
|
236
|
+
private onRefetch: () => void;
|
|
237
|
+
private loader: Loader | null = null;
|
|
238
|
+
|
|
239
|
+
constructor(
|
|
240
|
+
theme: Theme,
|
|
241
|
+
tui: TUI,
|
|
242
|
+
onClose: () => void,
|
|
243
|
+
onRefetch: () => void,
|
|
244
|
+
) {
|
|
257
245
|
this.theme = theme;
|
|
246
|
+
this.tui = tui;
|
|
258
247
|
this.onClose = onClose;
|
|
248
|
+
this.onRefetch = onRefetch;
|
|
249
|
+
this.startLoader();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private startLoader(): void {
|
|
253
|
+
this.loader = new Loader(
|
|
254
|
+
this.tui,
|
|
255
|
+
(s: string) => this.theme.fg("accent", s),
|
|
256
|
+
(s: string) => this.theme.fg("muted", s),
|
|
257
|
+
"Fetching quotas...",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
destroy(): void {
|
|
262
|
+
this.loader?.stop();
|
|
263
|
+
this.loader = null;
|
|
259
264
|
}
|
|
260
265
|
|
|
261
266
|
setState(state: QuotasState): void {
|
|
267
|
+
if (state.type === "loading") {
|
|
268
|
+
this.loader?.stop();
|
|
269
|
+
this.startLoader();
|
|
270
|
+
} else if (this.state.type === "loading") {
|
|
271
|
+
this.loader?.stop();
|
|
272
|
+
this.loader = null;
|
|
273
|
+
}
|
|
262
274
|
this.state = state;
|
|
263
275
|
}
|
|
264
276
|
|
|
@@ -267,6 +279,10 @@ export class QuotasComponent implements Component {
|
|
|
267
279
|
this.onClose();
|
|
268
280
|
return true;
|
|
269
281
|
}
|
|
282
|
+
if (data === "r") {
|
|
283
|
+
this.onRefetch();
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
270
286
|
return false;
|
|
271
287
|
}
|
|
272
288
|
|
|
@@ -286,7 +302,11 @@ export class QuotasComponent implements Component {
|
|
|
286
302
|
|
|
287
303
|
switch (this.state.type) {
|
|
288
304
|
case "loading":
|
|
289
|
-
|
|
305
|
+
if (this.loader) {
|
|
306
|
+
lines.push(...this.loader.render(width));
|
|
307
|
+
} else {
|
|
308
|
+
lines.push(this.theme.fg("muted", " Fetching quotas..."));
|
|
309
|
+
}
|
|
290
310
|
break;
|
|
291
311
|
case "error":
|
|
292
312
|
lines.push(this.theme.fg("error", ` ${this.state.message}`));
|
|
@@ -299,7 +319,7 @@ export class QuotasComponent implements Component {
|
|
|
299
319
|
}
|
|
300
320
|
|
|
301
321
|
lines.push("");
|
|
302
|
-
lines.push(this.theme.fg("dim", " q/Esc to close"));
|
|
322
|
+
lines.push(this.theme.fg("dim", " r to refresh q/Esc to close"));
|
|
303
323
|
lines.push(...border.render(width));
|
|
304
324
|
|
|
305
325
|
return lines;
|
|
@@ -335,134 +355,51 @@ export class QuotasComponent implements Component {
|
|
|
335
355
|
const lines: string[] = [];
|
|
336
356
|
const theme = this.theme;
|
|
337
357
|
|
|
338
|
-
const
|
|
358
|
+
const rawPace = window.showPace ? getPacePercent(window) : null;
|
|
359
|
+
const pacePercent =
|
|
360
|
+
rawPace !== null ? rawPace * (window.paceScale ?? 1) : null;
|
|
339
361
|
const projectedPercent = getProjectedPercent(
|
|
340
362
|
window.usedPercent,
|
|
341
363
|
pacePercent,
|
|
342
364
|
);
|
|
343
|
-
|
|
365
|
+
let severity = getSeverity(projectedPercent, pacePercent);
|
|
366
|
+
if (window.limited) severity = "error";
|
|
344
367
|
|
|
345
368
|
// Label
|
|
346
369
|
lines.push(
|
|
347
370
|
truncateToWidth(` ${theme.fg("accent", window.label)}`, maxWidth),
|
|
348
371
|
);
|
|
349
372
|
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
maxWidth,
|
|
369
|
-
),
|
|
370
|
-
);
|
|
371
|
-
} else {
|
|
372
|
-
// Traditional progress bar for legacy quota types
|
|
373
|
-
const bar = renderProgressBar(
|
|
374
|
-
window.usedPercent,
|
|
375
|
-
barWidth,
|
|
376
|
-
theme,
|
|
377
|
-
severity,
|
|
378
|
-
pacePercent,
|
|
379
|
-
);
|
|
380
|
-
const usedStr = `${window.usedValue.toLocaleString()}/${window.limitValue.toLocaleString()} (${Math.round(window.usedPercent)}%)`;
|
|
381
|
-
lines.push(
|
|
382
|
-
truncateToWidth(` ${bar} ${theme.fg(severity, usedStr)}`, maxWidth),
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Metadata: estimated + pace left, reset time right
|
|
387
|
-
const leftParts: string[] = [];
|
|
388
|
-
|
|
389
|
-
// Show tick info for rolling window
|
|
390
|
-
if (window.tickPercent !== undefined) {
|
|
391
|
-
const now = Date.now();
|
|
392
|
-
const remainingMs = window.resetsAt.getTime() - now;
|
|
393
|
-
const remainingMins = Math.ceil(remainingMs / (1000 * 60));
|
|
394
|
-
const remainingSecs = Math.ceil(remainingMs / 1000);
|
|
395
|
-
const timeStr =
|
|
396
|
-
remainingMs <= 0
|
|
397
|
-
? "now"
|
|
398
|
-
: remainingMins >= 1
|
|
399
|
-
? `${remainingMins}m`
|
|
400
|
-
: `${remainingSecs}s`;
|
|
401
|
-
const tickValue = (window.tickPercent / 100) * window.limitValue;
|
|
402
|
-
const tickStr = `+${tickValue.toFixed(1)} in ${timeStr}`;
|
|
403
|
-
leftParts.push(theme.fg("dim", tickStr));
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Show next regen credits for weekly token limit
|
|
407
|
-
if (window.nextRegenCredits !== undefined) {
|
|
408
|
-
const now = Date.now();
|
|
409
|
-
const remainingMs = window.resetsAt.getTime() - now;
|
|
410
|
-
const remainingHours = Math.ceil(remainingMs / (1000 * 60 * 60));
|
|
411
|
-
const remainingMins = Math.ceil(remainingMs / (1000 * 60));
|
|
412
|
-
const timeStr =
|
|
413
|
-
remainingMs <= 0
|
|
414
|
-
? "now"
|
|
415
|
-
: remainingHours >= 1
|
|
416
|
-
? `${remainingHours}h`
|
|
417
|
-
: `${remainingMins}m`;
|
|
418
|
-
const regenStr = `+${window.nextRegenCredits} in ${timeStr}`;
|
|
419
|
-
leftParts.push(theme.fg("dim", regenStr));
|
|
420
|
-
}
|
|
373
|
+
// Bar + usage
|
|
374
|
+
const bar = renderProgressBar(
|
|
375
|
+
window.usedPercent,
|
|
376
|
+
barWidth,
|
|
377
|
+
theme,
|
|
378
|
+
severity,
|
|
379
|
+
pacePercent,
|
|
380
|
+
);
|
|
381
|
+
const usedStr = window.isCurrency
|
|
382
|
+
? `${Math.round(window.usedPercent)}%/$${window.limitValue.toFixed(2)}`
|
|
383
|
+
: `${Math.round(window.usedPercent)}%/${window.limitValue}`;
|
|
384
|
+
const limitedBadge = window.limited ? theme.fg("error", " LIMITED") : "";
|
|
385
|
+
lines.push(
|
|
386
|
+
truncateToWidth(
|
|
387
|
+
` ${bar} ${theme.fg(severity, usedStr)}${limitedBadge}`,
|
|
388
|
+
maxWidth,
|
|
389
|
+
),
|
|
390
|
+
);
|
|
421
391
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
window.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
? theme.fg(severity, estStr)
|
|
431
|
-
: theme.fg("dim", estStr),
|
|
392
|
+
// Subtitle: next event info
|
|
393
|
+
if (window.nextLabel) {
|
|
394
|
+
const timeStr = formatTimeRemaining(window.resetsAt);
|
|
395
|
+
const subtitleStr = window.nextAmount
|
|
396
|
+
? `${window.nextAmount} in ${timeStr}`
|
|
397
|
+
: `${window.nextLabel} in ${timeStr}`;
|
|
398
|
+
lines.push(
|
|
399
|
+
truncateToWidth(` ${theme.fg("dim", subtitleStr)}`, maxWidth),
|
|
432
400
|
);
|
|
433
401
|
}
|
|
434
402
|
|
|
435
|
-
if (
|
|
436
|
-
pacePercent !== null &&
|
|
437
|
-
window.tickPercent === undefined &&
|
|
438
|
-
window.nextRegenCredits === undefined
|
|
439
|
-
) {
|
|
440
|
-
const paceDiff = window.usedPercent - pacePercent;
|
|
441
|
-
if (Math.abs(paceDiff) > 5) {
|
|
442
|
-
if (paceDiff > 0) {
|
|
443
|
-
leftParts.push(
|
|
444
|
-
theme.fg("warning", `${Math.round(Math.abs(paceDiff))}% ahead`),
|
|
445
|
-
);
|
|
446
|
-
} else {
|
|
447
|
-
leftParts.push(
|
|
448
|
-
theme.fg("success", `${Math.round(Math.abs(paceDiff))}% behind`),
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const leftStr = leftParts.join(" ");
|
|
455
|
-
const resetStr = formatResetDateTime(window.resetsAt);
|
|
456
|
-
const rightStr = theme.fg("dim", resetStr);
|
|
457
|
-
|
|
458
|
-
const leftW = visibleWidth(leftStr);
|
|
459
|
-
const rightW = visibleWidth(rightStr);
|
|
460
|
-
const gap = Math.max(2, barWidth - leftW - rightW);
|
|
461
|
-
|
|
462
|
-
lines.push(
|
|
463
|
-
truncateToWidth(` ${leftStr}${" ".repeat(gap)}${rightStr}`, maxWidth),
|
|
464
|
-
);
|
|
465
|
-
|
|
466
403
|
return lines;
|
|
467
404
|
}
|
|
468
405
|
|
|
@@ -107,10 +107,13 @@ async function emitCurrentUsage(
|
|
|
107
107
|
): Promise<void> {
|
|
108
108
|
const apiKey = await getSyntheticApiKey(authStorage);
|
|
109
109
|
if (!apiKey) return;
|
|
110
|
-
const
|
|
111
|
-
if (!
|
|
110
|
+
const result = await fetchQuotas(apiKey);
|
|
111
|
+
if (!result.success) return;
|
|
112
112
|
pi.events.emit("sub-core:update-current", {
|
|
113
|
-
state: {
|
|
113
|
+
state: {
|
|
114
|
+
provider: "synthetic",
|
|
115
|
+
usage: toUsageSnapshot(result.data.quotas),
|
|
116
|
+
},
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
119
|
|
package/src/types/quotas.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export type QuotasErrorKind =
|
|
2
|
+
| "cancelled"
|
|
3
|
+
| "timeout"
|
|
4
|
+
| "config"
|
|
5
|
+
| "http"
|
|
6
|
+
| "network";
|
|
7
|
+
|
|
8
|
+
export type QuotasResult =
|
|
9
|
+
| { success: true; data: { quotas: QuotasResponse } }
|
|
10
|
+
| { success: false; error: { message: string; kind: QuotasErrorKind } };
|
|
11
|
+
|
|
1
12
|
export interface QuotasResponse {
|
|
2
13
|
subscription?: {
|
|
3
14
|
limit: number;
|
package/src/utils/quotas.ts
CHANGED
|
@@ -1,20 +1,73 @@
|
|
|
1
|
-
import type { QuotasResponse } from "../types/quotas";
|
|
1
|
+
import type { QuotasResponse, QuotasResult } from "../types/quotas";
|
|
2
|
+
|
|
3
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
4
|
+
|
|
5
|
+
function isTimeoutReason(reason: unknown): boolean {
|
|
6
|
+
return (
|
|
7
|
+
(reason instanceof DOMException && reason.name === "TimeoutError") ||
|
|
8
|
+
(reason instanceof Error && reason.name === "TimeoutError")
|
|
9
|
+
);
|
|
10
|
+
}
|
|
2
11
|
|
|
3
12
|
export async function fetchQuotas(
|
|
4
13
|
apiKey: string,
|
|
5
|
-
|
|
6
|
-
|
|
14
|
+
signal?: AbortSignal,
|
|
15
|
+
): Promise<QuotasResult> {
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
error: { message: "No API key provided", kind: "config" },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const signals: AbortSignal[] = [AbortSignal.timeout(FETCH_TIMEOUT_MS)];
|
|
24
|
+
if (signal) signals.push(signal);
|
|
25
|
+
const combined = AbortSignal.any(signals);
|
|
7
26
|
|
|
8
27
|
try {
|
|
9
28
|
const response = await fetch("https://api.synthetic.new/v2/quotas", {
|
|
10
29
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
30
|
+
signal: combined,
|
|
11
31
|
});
|
|
12
32
|
|
|
13
|
-
if (!response.ok)
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
let message = response.statusText;
|
|
35
|
+
try {
|
|
36
|
+
const body = await response.text();
|
|
37
|
+
if (body) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(body) as { error?: string };
|
|
40
|
+
if (parsed.error) message = parsed.error;
|
|
41
|
+
} catch {
|
|
42
|
+
message = body;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
return { success: false, error: { message, kind: "http" } };
|
|
47
|
+
}
|
|
48
|
+
return { success: false, error: { message, kind: "http" } };
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
const data: QuotasResponse = await response.json();
|
|
15
|
-
return data;
|
|
16
|
-
} catch {
|
|
17
|
-
|
|
52
|
+
return { success: true, data: { quotas: data } };
|
|
53
|
+
} catch (err: unknown) {
|
|
54
|
+
const isAbort =
|
|
55
|
+
combined.aborted ||
|
|
56
|
+
(err instanceof DOMException && err.name === "AbortError");
|
|
57
|
+
if (isAbort) {
|
|
58
|
+
if (isTimeoutReason(combined.reason)) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: { message: "Request timed out", kind: "timeout" },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error: { message: "Request cancelled", kind: "cancelled" },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
70
|
+
return { success: false, error: { message, kind: "network" } };
|
|
18
71
|
}
|
|
19
72
|
}
|
|
20
73
|
|