@aliou/pi-synthetic 0.8.3 → 0.8.5
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 +4 -2
- package/src/extensions/command-quotas/command.ts +53 -0
- package/src/extensions/command-quotas/components/quotas-display.ts +314 -0
- package/src/extensions/command-quotas/index.ts +13 -0
- package/src/{hooks → extensions/command-quotas}/sub-integration.ts +3 -3
- package/src/{providers → extensions/provider}/index.ts +4 -0
- package/src/{providers → extensions/provider}/models.ts +19 -0
- package/src/{hooks/search-tool-availability.ts → extensions/web-search/hooks.ts} +1 -1
- package/src/extensions/web-search/index.ts +13 -0
- package/src/lib/env.ts +7 -0
- package/src/commands/quotas.ts +0 -68
- package/src/components/quotas-display.ts +0 -240
- package/src/components/quotas-error.ts +0 -32
- package/src/components/quotas-loading.ts +0 -30
- package/src/components/tabbed-panel.ts +0 -161
- package/src/index.ts +0 -17
- /package/src/{providers → extensions/provider}/models.test.ts +0 -0
- /package/src/{tools/search.ts → extensions/web-search/tool.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
},
|
|
16
16
|
"pi": {
|
|
17
17
|
"extensions": [
|
|
18
|
-
"./src/index.ts"
|
|
18
|
+
"./src/extensions/provider/index.ts",
|
|
19
|
+
"./src/extensions/web-search/index.ts",
|
|
20
|
+
"./src/extensions/command-quotas/index.ts"
|
|
19
21
|
],
|
|
20
22
|
"video": "https://assets.aliou.me/pi-extensions/demos/pi-synthetic.mp4"
|
|
21
23
|
},
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { fetchQuotas } from "../../utils/quotas";
|
|
3
|
+
import { QuotasComponent } from "./components/quotas-display";
|
|
4
|
+
|
|
5
|
+
export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
6
|
+
pi.registerCommand("synthetic:quotas", {
|
|
7
|
+
description: "Display Synthetic API usage quotas",
|
|
8
|
+
handler: async (_args, ctx) => {
|
|
9
|
+
const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
|
|
10
|
+
const component = new QuotasComponent(theme, () => done(null));
|
|
11
|
+
|
|
12
|
+
fetchQuotas()
|
|
13
|
+
.then((quotas) => {
|
|
14
|
+
if (!quotas) {
|
|
15
|
+
component.setState({
|
|
16
|
+
type: "error",
|
|
17
|
+
message: "Failed to fetch quotas",
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
component.setState({ type: "loaded", quotas });
|
|
21
|
+
}
|
|
22
|
+
tui.requestRender();
|
|
23
|
+
})
|
|
24
|
+
.catch(() => {
|
|
25
|
+
component.setState({
|
|
26
|
+
type: "error",
|
|
27
|
+
message: "Failed to fetch quotas",
|
|
28
|
+
});
|
|
29
|
+
tui.requestRender();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
render: (width: number) => component.render(width),
|
|
34
|
+
invalidate: () => component.invalidate(),
|
|
35
|
+
handleInput: (data: string) => component.handleInput(data),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// RPC fallback: return JSON
|
|
40
|
+
if (result === undefined) {
|
|
41
|
+
const quotas = await fetchQuotas();
|
|
42
|
+
if (!quotas) {
|
|
43
|
+
ctx.ui.notify(
|
|
44
|
+
JSON.stringify({ error: "Failed to fetch quotas" }),
|
|
45
|
+
"error",
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
ctx.ui.notify(JSON.stringify(quotas, null, 2), "info");
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
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";
|
|
9
|
+
import type { QuotasResponse } from "../../../types/quotas";
|
|
10
|
+
|
|
11
|
+
type QuotasState =
|
|
12
|
+
| { type: "loading" }
|
|
13
|
+
| { type: "error"; message: string }
|
|
14
|
+
| { type: "loaded"; quotas: QuotasResponse };
|
|
15
|
+
|
|
16
|
+
interface QuotaWindow {
|
|
17
|
+
label: string;
|
|
18
|
+
usedPercent: number;
|
|
19
|
+
resetsAt: Date;
|
|
20
|
+
windowSeconds: number;
|
|
21
|
+
usedValue: number;
|
|
22
|
+
limitValue: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toWindows(quotas: QuotasResponse): QuotaWindow[] {
|
|
26
|
+
const windows: QuotaWindow[] = [];
|
|
27
|
+
|
|
28
|
+
if (quotas.subscription.limit > 0) {
|
|
29
|
+
windows.push({
|
|
30
|
+
label: "Completions",
|
|
31
|
+
usedPercent:
|
|
32
|
+
(quotas.subscription.requests / quotas.subscription.limit) * 100,
|
|
33
|
+
resetsAt: new Date(quotas.subscription.renewsAt),
|
|
34
|
+
windowSeconds: 5 * 60 * 60,
|
|
35
|
+
usedValue: quotas.subscription.requests,
|
|
36
|
+
limitValue: quotas.subscription.limit,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (quotas.search.hourly.limit > 0) {
|
|
41
|
+
windows.push({
|
|
42
|
+
label: "Search",
|
|
43
|
+
usedPercent:
|
|
44
|
+
(quotas.search.hourly.requests / quotas.search.hourly.limit) * 100,
|
|
45
|
+
resetsAt: new Date(quotas.search.hourly.renewsAt),
|
|
46
|
+
windowSeconds: 60 * 60,
|
|
47
|
+
usedValue: quotas.search.hourly.requests,
|
|
48
|
+
limitValue: quotas.search.hourly.limit,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (quotas.freeToolCalls.limit > 0) {
|
|
53
|
+
windows.push({
|
|
54
|
+
label: "Free Tool Calls",
|
|
55
|
+
usedPercent:
|
|
56
|
+
(quotas.freeToolCalls.requests / quotas.freeToolCalls.limit) * 100,
|
|
57
|
+
resetsAt: new Date(quotas.freeToolCalls.renewsAt),
|
|
58
|
+
windowSeconds: 24 * 60 * 60,
|
|
59
|
+
usedValue: quotas.freeToolCalls.requests,
|
|
60
|
+
limitValue: quotas.freeToolCalls.limit,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return windows;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getPacePercent(window: QuotaWindow): number | null {
|
|
68
|
+
const totalMs = window.windowSeconds * 1000;
|
|
69
|
+
if (totalMs <= 0) return null;
|
|
70
|
+
const remainingMs = window.resetsAt.getTime() - Date.now();
|
|
71
|
+
const elapsedMs = totalMs - remainingMs;
|
|
72
|
+
return Math.max(0, Math.min(100, (elapsedMs / totalMs) * 100));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getProjectedPercent(
|
|
76
|
+
usedPercent: number,
|
|
77
|
+
pacePercent: number | null,
|
|
78
|
+
): number {
|
|
79
|
+
if (pacePercent === null) return usedPercent;
|
|
80
|
+
const effectivePace = Math.max(5, pacePercent);
|
|
81
|
+
return Math.max(0, (usedPercent / effectivePace) * 100);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getSeverity(
|
|
85
|
+
projectedPercent: number,
|
|
86
|
+
pacePercent: number | null,
|
|
87
|
+
): "success" | "warning" | "error" {
|
|
88
|
+
if (pacePercent === null) {
|
|
89
|
+
if (projectedPercent >= 100) return "error";
|
|
90
|
+
if (projectedPercent >= 90) return "warning";
|
|
91
|
+
return "success";
|
|
92
|
+
}
|
|
93
|
+
// Dynamic thresholds based on window progress
|
|
94
|
+
const progress = pacePercent / 100;
|
|
95
|
+
const warnThreshold = 260 - (260 - 120) * progress;
|
|
96
|
+
const highThreshold = 320 - (320 - 145) * progress;
|
|
97
|
+
const criticalThreshold = 400 - (400 - 170) * progress;
|
|
98
|
+
|
|
99
|
+
if (projectedPercent >= criticalThreshold) return "error";
|
|
100
|
+
if (projectedPercent >= highThreshold) return "error";
|
|
101
|
+
if (projectedPercent >= warnThreshold) return "warning";
|
|
102
|
+
return "success";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatResetDateTime(date: Date): string {
|
|
106
|
+
const now = new Date();
|
|
107
|
+
const isToday =
|
|
108
|
+
date.getDate() === now.getDate() &&
|
|
109
|
+
date.getMonth() === now.getMonth() &&
|
|
110
|
+
date.getFullYear() === now.getFullYear();
|
|
111
|
+
|
|
112
|
+
const timeStr = date.toLocaleTimeString("en-US", {
|
|
113
|
+
hour: "numeric",
|
|
114
|
+
minute: "2-digit",
|
|
115
|
+
hour12: true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (isToday) {
|
|
119
|
+
return `today ${timeStr}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const dateStr = date.toLocaleDateString("en-US", {
|
|
123
|
+
month: "short",
|
|
124
|
+
day: "numeric",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return `${dateStr} ${timeStr}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderProgressBar(
|
|
131
|
+
percent: number,
|
|
132
|
+
width: number,
|
|
133
|
+
theme: Theme,
|
|
134
|
+
fillColor: "success" | "warning" | "error",
|
|
135
|
+
pacePercent?: number | null,
|
|
136
|
+
): string {
|
|
137
|
+
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
138
|
+
const filled = Math.round((clamped / 100) * width);
|
|
139
|
+
const paceIndex =
|
|
140
|
+
pacePercent === null || pacePercent === undefined || pacePercent <= percent
|
|
141
|
+
? null
|
|
142
|
+
: Math.round((Math.max(0, Math.min(100, pacePercent)) / 100) * width);
|
|
143
|
+
|
|
144
|
+
const parts: string[] = [];
|
|
145
|
+
for (let idx = 0; idx < width; idx++) {
|
|
146
|
+
if (idx < filled) {
|
|
147
|
+
parts.push(theme.fg(fillColor, "█"));
|
|
148
|
+
} else if (paceIndex !== null && idx < paceIndex) {
|
|
149
|
+
parts.push(theme.fg(fillColor, "▓"));
|
|
150
|
+
} else {
|
|
151
|
+
parts.push(theme.fg("dim", "░"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return parts.join("");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class QuotasComponent implements Component {
|
|
159
|
+
private state: QuotasState = { type: "loading" };
|
|
160
|
+
private theme: Theme;
|
|
161
|
+
private onClose: () => void;
|
|
162
|
+
|
|
163
|
+
constructor(theme: Theme, onClose: () => void) {
|
|
164
|
+
this.theme = theme;
|
|
165
|
+
this.onClose = onClose;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setState(state: QuotasState): void {
|
|
169
|
+
this.state = state;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
handleInput(data: string): boolean {
|
|
173
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
174
|
+
this.onClose();
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render(width: number): string[] {
|
|
181
|
+
const lines: string[] = [];
|
|
182
|
+
const border = new DynamicBorder((s: string) => this.theme.fg("border", s));
|
|
183
|
+
const contentWidth = Math.max(1, width - 4);
|
|
184
|
+
|
|
185
|
+
lines.push(...border.render(width));
|
|
186
|
+
lines.push(
|
|
187
|
+
truncateToWidth(
|
|
188
|
+
` ${this.theme.fg("accent", this.theme.bold("Synthetic API Quotas"))}`,
|
|
189
|
+
width,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
lines.push("");
|
|
193
|
+
|
|
194
|
+
switch (this.state.type) {
|
|
195
|
+
case "loading":
|
|
196
|
+
lines.push(this.theme.fg("muted", " Loading..."));
|
|
197
|
+
break;
|
|
198
|
+
case "error":
|
|
199
|
+
lines.push(this.theme.fg("error", ` ${this.state.message}`));
|
|
200
|
+
break;
|
|
201
|
+
case "loaded":
|
|
202
|
+
lines.push(
|
|
203
|
+
...this.renderLoaded(this.state.quotas, contentWidth, width),
|
|
204
|
+
);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push(this.theme.fg("dim", " q/Esc to close"));
|
|
210
|
+
lines.push(...border.render(width));
|
|
211
|
+
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private renderLoaded(
|
|
216
|
+
quotas: QuotasResponse,
|
|
217
|
+
contentWidth: number,
|
|
218
|
+
maxWidth: number,
|
|
219
|
+
): string[] {
|
|
220
|
+
const lines: string[] = [];
|
|
221
|
+
const windows = toWindows(quotas);
|
|
222
|
+
const barWidth = Math.min(50, Math.max(20, contentWidth - 20));
|
|
223
|
+
|
|
224
|
+
for (const window of windows) {
|
|
225
|
+
lines.push(...this.renderWindow(window, barWidth, maxWidth));
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Remove trailing empty line
|
|
230
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
231
|
+
lines.pop();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private renderWindow(
|
|
238
|
+
window: QuotaWindow,
|
|
239
|
+
barWidth: number,
|
|
240
|
+
maxWidth: number,
|
|
241
|
+
): string[] {
|
|
242
|
+
const lines: string[] = [];
|
|
243
|
+
const theme = this.theme;
|
|
244
|
+
|
|
245
|
+
const pacePercent = getPacePercent(window);
|
|
246
|
+
const projectedPercent = getProjectedPercent(
|
|
247
|
+
window.usedPercent,
|
|
248
|
+
pacePercent,
|
|
249
|
+
);
|
|
250
|
+
const severity = getSeverity(projectedPercent, pacePercent);
|
|
251
|
+
|
|
252
|
+
// Label
|
|
253
|
+
lines.push(
|
|
254
|
+
truncateToWidth(` ${theme.fg("accent", window.label)}`, maxWidth),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Progress bar + usage
|
|
258
|
+
const bar = renderProgressBar(
|
|
259
|
+
window.usedPercent,
|
|
260
|
+
barWidth,
|
|
261
|
+
theme,
|
|
262
|
+
severity,
|
|
263
|
+
pacePercent,
|
|
264
|
+
);
|
|
265
|
+
const usedStr = `${window.usedValue.toLocaleString()}/${window.limitValue.toLocaleString()} (${Math.round(window.usedPercent)}%)`;
|
|
266
|
+
lines.push(
|
|
267
|
+
truncateToWidth(` ${bar} ${theme.fg(severity, usedStr)}`, maxWidth),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Metadata: estimated + pace left, reset time right
|
|
271
|
+
const leftParts: string[] = [];
|
|
272
|
+
if (projectedPercent > 0) {
|
|
273
|
+
const estStr = `est ${Math.round(projectedPercent)}%`;
|
|
274
|
+
leftParts.push(
|
|
275
|
+
severity !== "success"
|
|
276
|
+
? theme.fg(severity, estStr)
|
|
277
|
+
: theme.fg("dim", estStr),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (pacePercent !== null) {
|
|
282
|
+
const paceDiff = window.usedPercent - pacePercent;
|
|
283
|
+
if (Math.abs(paceDiff) > 5) {
|
|
284
|
+
if (paceDiff > 0) {
|
|
285
|
+
leftParts.push(
|
|
286
|
+
theme.fg("warning", `${Math.round(Math.abs(paceDiff))}% ahead`),
|
|
287
|
+
);
|
|
288
|
+
} else {
|
|
289
|
+
leftParts.push(
|
|
290
|
+
theme.fg("success", `${Math.round(Math.abs(paceDiff))}% behind`),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const leftStr = leftParts.join(" ");
|
|
297
|
+
const resetStr = formatResetDateTime(window.resetsAt);
|
|
298
|
+
const rightStr = theme.fg("dim", resetStr);
|
|
299
|
+
|
|
300
|
+
const leftW = visibleWidth(leftStr);
|
|
301
|
+
const rightW = visibleWidth(rightStr);
|
|
302
|
+
const gap = Math.max(2, barWidth - leftW - rightW);
|
|
303
|
+
|
|
304
|
+
lines.push(
|
|
305
|
+
truncateToWidth(` ${leftStr}${" ".repeat(gap)}${rightStr}`, maxWidth),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
return lines;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
invalidate(): void {
|
|
312
|
+
// No internal cached state to invalidate
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { hasSyntheticApiKey } from "../../lib/env";
|
|
3
|
+
import { registerQuotasCommand } from "./command";
|
|
4
|
+
import { registerSubIntegration } from "./sub-integration";
|
|
5
|
+
|
|
6
|
+
export default async function (pi: ExtensionAPI) {
|
|
7
|
+
if (!hasSyntheticApiKey()) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
registerQuotasCommand(pi);
|
|
12
|
+
registerSubIntegration(pi);
|
|
13
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { QuotasResponse } from "
|
|
3
|
-
import { fetchQuotas, formatResetTime } from "
|
|
2
|
+
import type { QuotasResponse } from "../../types/quotas";
|
|
3
|
+
import { fetchQuotas, formatResetTime } from "../../utils/quotas";
|
|
4
4
|
|
|
5
5
|
interface RateWindow {
|
|
6
6
|
label: string;
|
|
@@ -53,7 +53,7 @@ function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
|
53
53
|
const pct =
|
|
54
54
|
(quotas.freeToolCalls.requests / quotas.freeToolCalls.limit) * 100;
|
|
55
55
|
windows.push({
|
|
56
|
-
label: "
|
|
56
|
+
label: "Tools",
|
|
57
57
|
usedPercent: Math.round(pct),
|
|
58
58
|
resetDescription: formatResetTime(quotas.freeToolCalls.renewsAt),
|
|
59
59
|
resetAt: quotas.freeToolCalls.renewsAt,
|
|
@@ -55,6 +55,25 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
55
55
|
contextWindow: 202752,
|
|
56
56
|
maxTokens: 65536,
|
|
57
57
|
},
|
|
58
|
+
// API: hf:zai-org/GLM-5 → ctx=196608, out=65536
|
|
59
|
+
{
|
|
60
|
+
id: "hf:zai-org/GLM-5",
|
|
61
|
+
name: "zai-org/GLM-5",
|
|
62
|
+
reasoning: true,
|
|
63
|
+
compat: {
|
|
64
|
+
supportsReasoningEffort: true,
|
|
65
|
+
reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
|
|
66
|
+
},
|
|
67
|
+
input: ["text"],
|
|
68
|
+
cost: {
|
|
69
|
+
input: 1,
|
|
70
|
+
output: 6,
|
|
71
|
+
cacheRead: 1,
|
|
72
|
+
cacheWrite: 0,
|
|
73
|
+
},
|
|
74
|
+
contextWindow: 196608,
|
|
75
|
+
maxTokens: 65536,
|
|
76
|
+
},
|
|
58
77
|
// API: hf:zai-org/GLM-4.7-Flash → ctx=196608
|
|
59
78
|
{
|
|
60
79
|
id: "hf:zai-org/GLM-4.7-Flash",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { hasSyntheticApiKey } from "../../lib/env";
|
|
3
|
+
import { registerSyntheticWebSearchHooks } from "./hooks";
|
|
4
|
+
import { registerSyntheticWebSearchTool } from "./tool";
|
|
5
|
+
|
|
6
|
+
export default async function (pi: ExtensionAPI) {
|
|
7
|
+
if (!hasSyntheticApiKey()) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
registerSyntheticWebSearchTool(pi);
|
|
12
|
+
registerSyntheticWebSearchHooks(pi);
|
|
13
|
+
}
|
package/src/lib/env.ts
ADDED
package/src/commands/quotas.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { Component } from "@mariozechner/pi-tui";
|
|
3
|
-
import { QuotasDisplayComponent } from "../components/quotas-display";
|
|
4
|
-
import { QuotasErrorComponent } from "../components/quotas-error";
|
|
5
|
-
import { QuotasLoadingComponent } from "../components/quotas-loading";
|
|
6
|
-
import { fetchQuotas } from "../utils/quotas";
|
|
7
|
-
|
|
8
|
-
export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
9
|
-
pi.registerCommand("synthetic:quotas", {
|
|
10
|
-
description: "Display Synthetic API usage quotas",
|
|
11
|
-
handler: async (_args, ctx) => {
|
|
12
|
-
const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
|
|
13
|
-
let currentComponent: Component = new QuotasLoadingComponent(theme);
|
|
14
|
-
|
|
15
|
-
fetchQuotas()
|
|
16
|
-
.then((quotas) => {
|
|
17
|
-
if (!quotas) {
|
|
18
|
-
currentComponent = new QuotasErrorComponent(
|
|
19
|
-
theme,
|
|
20
|
-
"Failed to fetch quotas",
|
|
21
|
-
);
|
|
22
|
-
} else {
|
|
23
|
-
currentComponent = new QuotasDisplayComponent(
|
|
24
|
-
theme,
|
|
25
|
-
quotas,
|
|
26
|
-
() => {
|
|
27
|
-
done(null);
|
|
28
|
-
},
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
tui.requestRender();
|
|
32
|
-
})
|
|
33
|
-
.catch(() => {
|
|
34
|
-
currentComponent = new QuotasErrorComponent(
|
|
35
|
-
theme,
|
|
36
|
-
"Failed to fetch quotas",
|
|
37
|
-
);
|
|
38
|
-
tui.requestRender();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
render: (width: number) => currentComponent.render(width),
|
|
43
|
-
invalidate: () => currentComponent.invalidate(),
|
|
44
|
-
handleInput: (data: string) => {
|
|
45
|
-
if (currentComponent.handleInput) {
|
|
46
|
-
return currentComponent.handleInput(data);
|
|
47
|
-
}
|
|
48
|
-
done(null);
|
|
49
|
-
return true;
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// RPC fallback: return JSON
|
|
55
|
-
if (result === undefined) {
|
|
56
|
-
const quotas = await fetchQuotas();
|
|
57
|
-
if (!quotas) {
|
|
58
|
-
ctx.ui.notify(
|
|
59
|
-
JSON.stringify({ error: "Failed to fetch quotas" }),
|
|
60
|
-
"error",
|
|
61
|
-
);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
ctx.ui.notify(JSON.stringify(quotas, null, 2), "info");
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
3
|
-
import { matchesKey } from "@mariozechner/pi-tui";
|
|
4
|
-
import type { QuotasResponse } from "../types/quotas";
|
|
5
|
-
import { TabbedScrollablePanel } from "./tabbed-panel";
|
|
6
|
-
|
|
7
|
-
export class QuotasDisplayComponent implements Component {
|
|
8
|
-
private panel: TabbedScrollablePanel;
|
|
9
|
-
private onClose: () => void;
|
|
10
|
-
|
|
11
|
-
constructor(theme: Theme, quotas: QuotasResponse, onClose: () => void) {
|
|
12
|
-
this.onClose = onClose;
|
|
13
|
-
|
|
14
|
-
this.panel = new TabbedScrollablePanel(
|
|
15
|
-
{
|
|
16
|
-
title: "Synthetic API Quotas",
|
|
17
|
-
tabs: [
|
|
18
|
-
{
|
|
19
|
-
label: "Completions",
|
|
20
|
-
buildContent: () =>
|
|
21
|
-
buildHybridLayout(theme, quotas.subscription, 5), // 5-hour window
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
label: "Search",
|
|
25
|
-
buildContent: () =>
|
|
26
|
-
buildHybridLayout(theme, quotas.search.hourly, 1), // 1 hour
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
label: "Free tool call",
|
|
30
|
-
buildContent: () =>
|
|
31
|
-
buildToolCallsLayout(theme, quotas.freeToolCalls, 24), // 24 hours (daily)
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
onClose: onClose,
|
|
35
|
-
},
|
|
36
|
-
null as unknown as TUI,
|
|
37
|
-
theme,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
handleInput(data: string): boolean {
|
|
42
|
-
if (matchesKey(data, "escape") || data === "q") {
|
|
43
|
-
this.onClose();
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
return this.panel.handleInput(data);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
render(width: number): string[] {
|
|
50
|
-
return this.panel.render(width);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
invalidate(): void {
|
|
54
|
-
this.panel.invalidate();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Layout: bar + pct/cur-total + pace/reset
|
|
59
|
-
function buildHybridLayout(
|
|
60
|
-
theme: Theme,
|
|
61
|
-
quota: { limit: number; requests: number; renewsAt: string },
|
|
62
|
-
periodHours: number,
|
|
63
|
-
): string[] {
|
|
64
|
-
const percentUsed = Math.round((quota.requests / quota.limit) * 100);
|
|
65
|
-
const renewsAt = new Date(quota.renewsAt);
|
|
66
|
-
const now = new Date();
|
|
67
|
-
|
|
68
|
-
const lines: string[] = [];
|
|
69
|
-
const barWidth = 50;
|
|
70
|
-
|
|
71
|
-
const usedStr = quota.requests.toLocaleString();
|
|
72
|
-
const limitStr = quota.limit.toLocaleString();
|
|
73
|
-
|
|
74
|
-
// Color based on usage
|
|
75
|
-
const usedColor =
|
|
76
|
-
percentUsed >= 100 ? "error" : percentUsed > 75 ? "warning" : "success";
|
|
77
|
-
|
|
78
|
-
// Calculate pace with known period
|
|
79
|
-
const totalPeriod = periodHours * 60 * 60 * 1000;
|
|
80
|
-
const timeUntilReset = Math.max(0, renewsAt.getTime() - now.getTime());
|
|
81
|
-
const timeElapsed = totalPeriod - timeUntilReset;
|
|
82
|
-
const percentTimeElapsed = (timeElapsed / totalPeriod) * 100;
|
|
83
|
-
const paceDiff = percentUsed - percentTimeElapsed;
|
|
84
|
-
|
|
85
|
-
let paceStr: string;
|
|
86
|
-
let paceColor: "success" | "dim" | "error";
|
|
87
|
-
if (paceDiff < -10) {
|
|
88
|
-
paceStr = `${Math.round(Math.abs(paceDiff))}% behind pace`;
|
|
89
|
-
paceColor = "success";
|
|
90
|
-
} else if (paceDiff > 10) {
|
|
91
|
-
paceStr = `${Math.round(paceDiff)}% ahead of pace`;
|
|
92
|
-
paceColor = "error";
|
|
93
|
-
} else {
|
|
94
|
-
paceStr = "within pace";
|
|
95
|
-
paceColor = "dim";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Row above bar: pct left, cur/total right
|
|
99
|
-
const pctStr = `${percentUsed}% used`;
|
|
100
|
-
const totalDisplay = `${usedStr}/${limitStr}`;
|
|
101
|
-
const spacing = " ".repeat(
|
|
102
|
-
Math.max(1, barWidth - pctStr.length - totalDisplay.length),
|
|
103
|
-
);
|
|
104
|
-
lines.push(
|
|
105
|
-
` ${theme.fg(usedColor, pctStr)}${spacing}${theme.fg("dim", totalDisplay)}`,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// Bar
|
|
109
|
-
const usedWidth = Math.round((percentUsed / 100) * barWidth);
|
|
110
|
-
let bar: string;
|
|
111
|
-
if (usedWidth >= barWidth) {
|
|
112
|
-
bar = theme.fg("error", "█".repeat(barWidth));
|
|
113
|
-
} else if (percentUsed > 75) {
|
|
114
|
-
bar =
|
|
115
|
-
theme.fg("warning", "█".repeat(usedWidth)) +
|
|
116
|
-
theme.fg("dim", "█".repeat(barWidth - usedWidth));
|
|
117
|
-
} else {
|
|
118
|
-
bar =
|
|
119
|
-
theme.fg("success", "█".repeat(usedWidth)) +
|
|
120
|
-
theme.fg("dim", "█".repeat(barWidth - usedWidth));
|
|
121
|
-
}
|
|
122
|
-
lines.push(` ${bar}`);
|
|
123
|
-
|
|
124
|
-
// Row below bar: pace left, reset right
|
|
125
|
-
const resetStr = formatShortTime(renewsAt);
|
|
126
|
-
const paceSpacing = " ".repeat(
|
|
127
|
-
Math.max(1, barWidth - paceStr.length - resetStr.length),
|
|
128
|
-
);
|
|
129
|
-
lines.push(
|
|
130
|
-
` ${theme.fg(paceColor, paceStr)}${paceSpacing}${theme.fg("dim", resetStr)}`,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
return lines;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Layout for free tool calls - shows remaining, not usage
|
|
137
|
-
function buildToolCallsLayout(
|
|
138
|
-
theme: Theme,
|
|
139
|
-
quota: { limit: number; requests: number; renewsAt: string },
|
|
140
|
-
periodHours: number,
|
|
141
|
-
): string[] {
|
|
142
|
-
const remaining = quota.limit - quota.requests;
|
|
143
|
-
const percentRemaining = Math.round((remaining / quota.limit) * 100);
|
|
144
|
-
const renewsAt = new Date(quota.renewsAt);
|
|
145
|
-
const now = new Date();
|
|
146
|
-
|
|
147
|
-
const lines: string[] = [];
|
|
148
|
-
const barWidth = 50;
|
|
149
|
-
|
|
150
|
-
const remainingStr = remaining.toLocaleString();
|
|
151
|
-
|
|
152
|
-
// Color based on remaining (inverse of usage)
|
|
153
|
-
const remainingColor =
|
|
154
|
-
remaining <= 0 ? "error" : percentRemaining < 25 ? "warning" : "success";
|
|
155
|
-
|
|
156
|
-
// Calculate pace (how fast you're consuming free calls)
|
|
157
|
-
const totalPeriod = periodHours * 60 * 60 * 1000;
|
|
158
|
-
const timeUntilReset = Math.max(0, renewsAt.getTime() - now.getTime());
|
|
159
|
-
const timeElapsed = totalPeriod - timeUntilReset;
|
|
160
|
-
const percentTimeElapsed = (timeElapsed / totalPeriod) * 100;
|
|
161
|
-
const expectedRemaining = Math.round(
|
|
162
|
-
quota.limit * (1 - percentTimeElapsed / 100),
|
|
163
|
-
);
|
|
164
|
-
const remainingDiff = remaining - expectedRemaining;
|
|
165
|
-
|
|
166
|
-
let paceStr: string;
|
|
167
|
-
let paceColor: "success" | "dim" | "error";
|
|
168
|
-
if (remainingDiff > quota.limit * 0.1) {
|
|
169
|
-
paceStr = `${remainingDiff.toLocaleString()} more than expected`;
|
|
170
|
-
paceColor = "success";
|
|
171
|
-
} else if (remainingDiff < -quota.limit * 0.1) {
|
|
172
|
-
paceStr = `${Math.abs(remainingDiff).toLocaleString()} fewer than expected`;
|
|
173
|
-
paceColor = "error";
|
|
174
|
-
} else {
|
|
175
|
-
paceStr = "on track";
|
|
176
|
-
paceColor = "dim";
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Row above bar: pct remaining left, ratio right (like other tabs)
|
|
180
|
-
const pctStr = `${percentRemaining}% remaining`;
|
|
181
|
-
const ratioStr = `${remainingStr}/${quota.limit.toLocaleString()}`;
|
|
182
|
-
const spacing = " ".repeat(
|
|
183
|
-
Math.max(1, barWidth - pctStr.length - ratioStr.length),
|
|
184
|
-
);
|
|
185
|
-
lines.push(
|
|
186
|
-
` ${theme.fg(remainingColor, pctStr)}${spacing}${theme.fg("dim", ratioStr)}`,
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
// Bar (shows remaining, not used - so full bar = all remaining)
|
|
190
|
-
const remainingWidth = Math.round((percentRemaining / 100) * barWidth);
|
|
191
|
-
let bar: string;
|
|
192
|
-
if (remaining <= 0) {
|
|
193
|
-
bar = theme.fg("dim", "█".repeat(barWidth));
|
|
194
|
-
} else if (percentRemaining < 25) {
|
|
195
|
-
bar =
|
|
196
|
-
theme.fg("warning", "█".repeat(remainingWidth)) +
|
|
197
|
-
theme.fg("dim", "█".repeat(barWidth - remainingWidth));
|
|
198
|
-
} else {
|
|
199
|
-
bar =
|
|
200
|
-
theme.fg("success", "█".repeat(remainingWidth)) +
|
|
201
|
-
theme.fg("dim", "█".repeat(barWidth - remainingWidth));
|
|
202
|
-
}
|
|
203
|
-
lines.push(` ${bar}`);
|
|
204
|
-
|
|
205
|
-
// Row below bar: pace left, reset right
|
|
206
|
-
const resetStr = formatShortTime(renewsAt);
|
|
207
|
-
const paceSpacing = " ".repeat(
|
|
208
|
-
Math.max(1, barWidth - paceStr.length - resetStr.length),
|
|
209
|
-
);
|
|
210
|
-
lines.push(
|
|
211
|
-
` ${theme.fg(paceColor, paceStr)}${paceSpacing}${theme.fg("dim", resetStr)}`,
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// If depleted, show note about paid calls
|
|
215
|
-
if (remaining <= 0) {
|
|
216
|
-
lines.push(` ${theme.fg("dim", "Additional calls will be charged")}`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return lines;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function formatShortTime(date: Date): string {
|
|
223
|
-
const now = new Date();
|
|
224
|
-
const diffMs = date.getTime() - now.getTime();
|
|
225
|
-
|
|
226
|
-
if (diffMs <= 0) {
|
|
227
|
-
return "soon";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
|
231
|
-
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
|
232
|
-
|
|
233
|
-
if (diffHours < 24) {
|
|
234
|
-
return `in ${diffHours}h`;
|
|
235
|
-
} else if (diffDays < 7) {
|
|
236
|
-
return `in ${diffDays}d`;
|
|
237
|
-
} else {
|
|
238
|
-
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { type Component, Container, Text } from "@mariozechner/pi-tui";
|
|
4
|
-
|
|
5
|
-
export class QuotasErrorComponent implements Component {
|
|
6
|
-
private container: Container;
|
|
7
|
-
|
|
8
|
-
constructor(theme: Theme, message: string) {
|
|
9
|
-
this.container = new Container();
|
|
10
|
-
const border = new DynamicBorder((s: string) => theme.fg("accent", s));
|
|
11
|
-
|
|
12
|
-
this.container.addChild(border);
|
|
13
|
-
this.container.addChild(
|
|
14
|
-
new Text(theme.fg("accent", theme.bold(" Synthetic API Quotas ")), 1, 0),
|
|
15
|
-
);
|
|
16
|
-
this.container.addChild(new Text("", 0, 0));
|
|
17
|
-
this.container.addChild(new Text(theme.fg("error", ` ${message}`), 1, 0));
|
|
18
|
-
this.container.addChild(new Text("", 0, 0));
|
|
19
|
-
this.container.addChild(
|
|
20
|
-
new Text(theme.fg("dim", " Press any key to close"), 1, 0),
|
|
21
|
-
);
|
|
22
|
-
this.container.addChild(border);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
render(width: number): string[] {
|
|
26
|
-
return this.container.render(width);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
invalidate(): void {
|
|
30
|
-
this.container.invalidate();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { type Component, Container, Text } from "@mariozechner/pi-tui";
|
|
4
|
-
|
|
5
|
-
export class QuotasLoadingComponent implements Component {
|
|
6
|
-
private container: Container;
|
|
7
|
-
|
|
8
|
-
constructor(theme: Theme) {
|
|
9
|
-
this.container = new Container();
|
|
10
|
-
const border = new DynamicBorder((s: string) => theme.fg("accent", s));
|
|
11
|
-
|
|
12
|
-
this.container.addChild(border);
|
|
13
|
-
this.container.addChild(
|
|
14
|
-
new Text(theme.fg("accent", theme.bold(" Synthetic API Quotas ")), 1, 0),
|
|
15
|
-
);
|
|
16
|
-
this.container.addChild(new Text("", 0, 0));
|
|
17
|
-
this.container.addChild(
|
|
18
|
-
new Text(theme.fg("dim", " Loading quotas..."), 1, 0),
|
|
19
|
-
);
|
|
20
|
-
this.container.addChild(border);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
render(width: number): string[] {
|
|
24
|
-
return this.container.render(width);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
invalidate(): void {
|
|
28
|
-
this.container.invalidate();
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
4
|
-
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
5
|
-
|
|
6
|
-
export interface PanelTab {
|
|
7
|
-
label: string;
|
|
8
|
-
buildContent: () => string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface TabbedScrollablePanelOptions {
|
|
12
|
-
title: string;
|
|
13
|
-
tabs: PanelTab[];
|
|
14
|
-
onClose: () => void;
|
|
15
|
-
maxVisible?: number;
|
|
16
|
-
keymap?: "vim" | "default";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class TabbedScrollablePanel implements Component {
|
|
20
|
-
private activeTab = 0;
|
|
21
|
-
private scrollOffset = 0;
|
|
22
|
-
private cachedLines: string[] | null = null;
|
|
23
|
-
private cachedWidth = 0;
|
|
24
|
-
private border: DynamicBorder;
|
|
25
|
-
private options: TabbedScrollablePanelOptions;
|
|
26
|
-
private theme: Theme;
|
|
27
|
-
|
|
28
|
-
constructor(options: TabbedScrollablePanelOptions, _tui: TUI, theme: Theme) {
|
|
29
|
-
this.options = options;
|
|
30
|
-
this.theme = theme;
|
|
31
|
-
this.border = new DynamicBorder((segment) => theme.fg("border", segment));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
handleInput(data: string): boolean {
|
|
35
|
-
if (matchesKey(data, "escape") || data === "q") {
|
|
36
|
-
this.options.onClose();
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (matchesKey(data, "tab")) {
|
|
41
|
-
this.activeTab = (this.activeTab + 1) % this.options.tabs.length;
|
|
42
|
-
this.scrollOffset = 0;
|
|
43
|
-
this.invalidate();
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (matchesKey(data, "shift+tab")) {
|
|
48
|
-
this.activeTab =
|
|
49
|
-
(this.activeTab - 1 + this.options.tabs.length) %
|
|
50
|
-
this.options.tabs.length;
|
|
51
|
-
this.scrollOffset = 0;
|
|
52
|
-
this.invalidate();
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const maxVisible = this.options.maxVisible ?? 16;
|
|
57
|
-
const totalLines = this.cachedLines?.length ?? 0;
|
|
58
|
-
const maxScroll = Math.max(0, totalLines - maxVisible);
|
|
59
|
-
|
|
60
|
-
if (data === "j" || matchesKey(data, "down")) {
|
|
61
|
-
if (this.scrollOffset < maxScroll) {
|
|
62
|
-
this.scrollOffset++;
|
|
63
|
-
}
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (data === "k" || matchesKey(data, "up")) {
|
|
68
|
-
if (this.scrollOffset > 0) {
|
|
69
|
-
this.scrollOffset--;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (data === " " || matchesKey(data, "pageDown")) {
|
|
75
|
-
this.scrollOffset = Math.min(this.scrollOffset + maxVisible, maxScroll);
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (matchesKey(data, "pageUp")) {
|
|
80
|
-
this.scrollOffset = Math.max(0, this.scrollOffset - maxVisible);
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (matchesKey(data, "home")) {
|
|
85
|
-
this.scrollOffset = 0;
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (matchesKey(data, "end")) {
|
|
90
|
-
this.scrollOffset = maxScroll;
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
invalidate(): void {
|
|
98
|
-
this.cachedLines = null;
|
|
99
|
-
this.cachedWidth = 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
render(width: number): string[] {
|
|
103
|
-
const tab = this.options.tabs[this.activeTab];
|
|
104
|
-
|
|
105
|
-
if (!this.cachedLines || this.cachedWidth !== width) {
|
|
106
|
-
this.cachedLines = tab ? tab.buildContent() : [];
|
|
107
|
-
this.cachedWidth = width;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const lines: string[] = [];
|
|
111
|
-
|
|
112
|
-
lines.push(...this.border.render(width));
|
|
113
|
-
lines.push(
|
|
114
|
-
truncateToWidth(
|
|
115
|
-
` ${this.theme.fg("accent", this.theme.bold(this.options.title))}`,
|
|
116
|
-
width,
|
|
117
|
-
),
|
|
118
|
-
);
|
|
119
|
-
lines.push(this.renderTabBar(width));
|
|
120
|
-
lines.push("");
|
|
121
|
-
|
|
122
|
-
// Content - no forced padding, just render what we have
|
|
123
|
-
for (const line of this.cachedLines) {
|
|
124
|
-
lines.push(truncateToWidth(` ${line}`, width));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Footer directly after content
|
|
128
|
-
lines.push("");
|
|
129
|
-
lines.push(
|
|
130
|
-
truncateToWidth(
|
|
131
|
-
this.theme.fg("dim", " Tab/S-Tab switch tabs q/Esc close"),
|
|
132
|
-
width,
|
|
133
|
-
),
|
|
134
|
-
);
|
|
135
|
-
lines.push(...this.border.render(width));
|
|
136
|
-
|
|
137
|
-
return lines;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private renderTabBar(width: number): string {
|
|
141
|
-
const parts: string[] = [];
|
|
142
|
-
|
|
143
|
-
for (let i = 0; i < this.options.tabs.length; i++) {
|
|
144
|
-
const tab = this.options.tabs[i];
|
|
145
|
-
if (!tab) continue;
|
|
146
|
-
const active = i === this.activeTab;
|
|
147
|
-
|
|
148
|
-
if (active) {
|
|
149
|
-
parts.push(this.theme.fg("accent", this.theme.bold(` ${tab.label} `)));
|
|
150
|
-
} else {
|
|
151
|
-
parts.push(this.theme.fg("dim", ` ${tab.label} `));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (i < this.options.tabs.length - 1) {
|
|
155
|
-
parts.push(this.theme.fg("borderMuted", "│"));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return truncateToWidth(` ${parts.join("")}`, width);
|
|
160
|
-
}
|
|
161
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { registerQuotasCommand } from "./commands/quotas";
|
|
3
|
-
import { registerSyntheticWebSearchHooks } from "./hooks/search-tool-availability";
|
|
4
|
-
import { registerSubIntegration } from "./hooks/sub-integration";
|
|
5
|
-
import { registerSyntheticProvider } from "./providers/index";
|
|
6
|
-
import { registerSyntheticWebSearchTool } from "./tools/search";
|
|
7
|
-
|
|
8
|
-
export default async function (pi: ExtensionAPI) {
|
|
9
|
-
registerSyntheticProvider(pi);
|
|
10
|
-
registerSyntheticWebSearchTool(pi);
|
|
11
|
-
registerSyntheticWebSearchHooks(pi);
|
|
12
|
-
|
|
13
|
-
if (process.env.SYNTHETIC_API_KEY) {
|
|
14
|
-
registerQuotasCommand(pi);
|
|
15
|
-
registerSubIntegration(pi);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
File without changes
|
|
File without changes
|