@danypops/pi-jittor 0.1.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/README.md +63 -0
- package/docs/USAGE_PRIOR_ART.md +64 -0
- package/extension/src/benchmark-tui.ts +113 -0
- package/extension/src/capabilities/codex-recovery.ts +130 -0
- package/extension/src/capabilities/http-headers.ts +5 -0
- package/extension/src/capabilities/local-run-telemetry.ts +103 -0
- package/extension/src/capabilities/provider-response-telemetry.ts +101 -0
- package/extension/src/footer.ts +323 -0
- package/extension/src/index.ts +658 -0
- package/extension/src/service-client.ts +28 -0
- package/extension/src/session-identity.ts +20 -0
- package/extension/src/settings-tui.ts +153 -0
- package/extension/src/settings.ts +102 -0
- package/extension/src/tui.ts +284 -0
- package/extension/src/usage.ts +324 -0
- package/package.json +35 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
HUMAN_TEXT_FIELD_MAX_CHARACTERS,
|
|
5
|
+
USAGE_CHART_HEIGHT,
|
|
6
|
+
USAGE_MAX_DISTINCT_SCOPES,
|
|
7
|
+
USAGE_RENDER_MAX_SERIES,
|
|
8
|
+
USAGE_Y_AXIS_WIDTH,
|
|
9
|
+
buildCostGraph,
|
|
10
|
+
buildUsageGraph,
|
|
11
|
+
resolveUsageWindow,
|
|
12
|
+
USAGE_PERIODS,
|
|
13
|
+
usagePeriod,
|
|
14
|
+
type CostGraph,
|
|
15
|
+
type UsageAggregateRow,
|
|
16
|
+
type UsageGraph,
|
|
17
|
+
type UsagePeriod,
|
|
18
|
+
} from "@danypops/jittor";
|
|
19
|
+
import type { UsageBudgetControl } from "./settings.ts";
|
|
20
|
+
import type { JittorPanelClient } from "./tui.ts";
|
|
21
|
+
|
|
22
|
+
type UsageAction = "period-prev" | "period-next" | "view-next" | "refresh" | "close";
|
|
23
|
+
type UsageColor =
|
|
24
|
+
| "accent" | "success" | "warning" | "error" | "thinkingText" | "muted" | "dim" | "borderMuted"
|
|
25
|
+
| "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator";
|
|
26
|
+
|
|
27
|
+
export interface UsageTheme {
|
|
28
|
+
fg(color: UsageColor, text: string): string;
|
|
29
|
+
bold(text: string): string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Categorical palette for per-provider/model series. Deliberately excludes "success"/"warning"/
|
|
34
|
+
* "error": those already carry a fixed status meaning elsewhere in this same panel (the budget
|
|
35
|
+
* threshold line, freshness state), so reusing them for arbitrary model identity would make a
|
|
36
|
+
* model's bar segment look like a warning or a failure to a pre-attentive reader — a real
|
|
37
|
+
* categorical-color-design pitfall (see e.g. ColorBrewer/Okabe-Ito guidance on qualitative
|
|
38
|
+
* palettes: colors for nominal categories should not imply an order, magnitude, or judgment).
|
|
39
|
+
*
|
|
40
|
+
* Instead this reuses the syntax-highlighting color roles, because theme authors already tune
|
|
41
|
+
* those specifically to be simultaneously distinguishable on screen — that is the same design
|
|
42
|
+
* problem as a categorical data palette (many hues coexisting in one view that all need to read
|
|
43
|
+
* as different from each other). The order below interleaves the hue families syntax themes
|
|
44
|
+
* conventionally assign to keyword/function/string/number/type/variable/operator (violet, blue,
|
|
45
|
+
* green, orange, teal, cyan, neutral) so the first colors used are spread around the hue wheel
|
|
46
|
+
* rather than clustered, which is the standard categorical-palette heuristic for maximizing
|
|
47
|
+
* perceptual separation between adjacent categories.
|
|
48
|
+
*
|
|
49
|
+
* Terminal foreground color is a single channel with a hard ceiling on how many hues stay mutually
|
|
50
|
+
* distinguishable (most qualitative-palette guidance caps around 8–12). Once the hue palette is
|
|
51
|
+
* exhausted, seriesStyle adds bold as a second, independent visual channel before any exact
|
|
52
|
+
* color+weight combination repeats — a standard visualization technique (encode extra categories
|
|
53
|
+
* on an additional channel rather than silently reusing an indistinguishable color).
|
|
54
|
+
*/
|
|
55
|
+
const SERIES_HUES: UsageColor[] = [
|
|
56
|
+
"accent", "syntaxFunction", "syntaxString", "syntaxNumber",
|
|
57
|
+
"syntaxKeyword", "syntaxType", "thinkingText", "syntaxVariable", "syntaxOperator",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Returns a style function for the Nth series: cycles hue, then adds bold once the palette wraps. */
|
|
61
|
+
function seriesStyle(index: number, theme: UsageTheme): (text: string) => string {
|
|
62
|
+
const hue = SERIES_HUES[index % SERIES_HUES.length]!;
|
|
63
|
+
const useBold = Math.floor(index / SERIES_HUES.length) % 2 === 1;
|
|
64
|
+
return (text: string) => useBold ? theme.bold(theme.fg(hue, text)) : theme.fg(hue, text);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const PARTIAL_BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
68
|
+
|
|
69
|
+
function compact(value: number): string {
|
|
70
|
+
if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(value >= 10_000_000_000 ? 0 : 1)}B`;
|
|
71
|
+
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1)}M`;
|
|
72
|
+
if (value >= 1_000) return `${(value / 1_000).toFixed(value >= 10_000 || value % 1_000 === 0 ? 0 : 1)}k`;
|
|
73
|
+
return String(Math.round(value));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface RenderableSeries { key: string; provider: string; model: string; total: number }
|
|
77
|
+
interface RenderableBucket { start: number; end: number; total: number; series: Record<string, number> }
|
|
78
|
+
interface RenderableChart { period: UsagePeriod; start: number; end: number; buckets: RenderableBucket[]; series: RenderableSeries[]; total: number; truncated: boolean }
|
|
79
|
+
|
|
80
|
+
function mergeBuckets(buckets: RenderableBucket[], maximum: number): RenderableBucket[] {
|
|
81
|
+
if (buckets.length <= maximum) return buckets;
|
|
82
|
+
const result: RenderableBucket[] = [];
|
|
83
|
+
for (let index = 0; index < maximum; index += 1) {
|
|
84
|
+
const from = Math.floor(index * buckets.length / maximum);
|
|
85
|
+
const to = Math.max(from + 1, Math.floor((index + 1) * buckets.length / maximum));
|
|
86
|
+
const selected = buckets.slice(from, to);
|
|
87
|
+
const series: Record<string, number> = {};
|
|
88
|
+
for (const bucket of selected) {
|
|
89
|
+
for (const [key, value] of Object.entries(bucket.series)) series[key] = (series[key] ?? 0) + value;
|
|
90
|
+
}
|
|
91
|
+
result.push({
|
|
92
|
+
start: selected[0]!.start,
|
|
93
|
+
end: selected[selected.length - 1]!.end,
|
|
94
|
+
total: selected.reduce((sum, bucket) => sum + bucket.total, 0),
|
|
95
|
+
series,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function seriesAt(bucket: RenderableBucket, chart: RenderableChart, valueHeight: number): number {
|
|
102
|
+
let cumulative = 0;
|
|
103
|
+
for (let index = 0; index < chart.series.length; index += 1) {
|
|
104
|
+
cumulative += bucket.series[chart.series[index]!.key] ?? 0;
|
|
105
|
+
if (valueHeight <= cumulative) return index;
|
|
106
|
+
}
|
|
107
|
+
return Math.max(0, chart.series.length - 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Compact USD formatter: full cents below $1k, then the same k/M suffix convention as compact(). */
|
|
111
|
+
function formatUsd(value: number): string {
|
|
112
|
+
const magnitude = Math.abs(value);
|
|
113
|
+
if (magnitude === 0) return "$0";
|
|
114
|
+
if (magnitude < 1_000) return `$${value.toFixed(magnitude < 0.01 ? 4 : 2)}`;
|
|
115
|
+
if (magnitude < 1_000_000) return `$${(value / 1_000).toFixed(1)}k`;
|
|
116
|
+
return `$${(value / 1_000_000).toFixed(1)}M`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatPeriodPoint(value: number, period: UsagePeriod): string {
|
|
120
|
+
const date = new Date(value);
|
|
121
|
+
if (period === "hourly" || period === "daily") return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
122
|
+
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function axisLabels(start: number, end: number, period: UsagePeriod, width: number): string {
|
|
126
|
+
const labels = [formatPeriodPoint(start, period), formatPeriodPoint(start + (end - start) / 2, period), formatPeriodPoint(end, period)];
|
|
127
|
+
const positions = [0, Math.max(0, Math.floor((width - labels[1]!.length) / 2)), Math.max(0, width - labels[2]!.length)];
|
|
128
|
+
const characters = Array.from({ length: width }, () => " ");
|
|
129
|
+
for (let labelIndex = 0; labelIndex < labels.length; labelIndex += 1) {
|
|
130
|
+
for (let index = 0; index < labels[labelIndex]!.length && positions[labelIndex]! + index < width; index += 1) {
|
|
131
|
+
characters[positions[labelIndex]! + index] = labels[labelIndex]![index]!;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return characters.join("");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function displayIdentity(value: string): string {
|
|
138
|
+
return value.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim().slice(0, HUMAN_TEXT_FIELD_MAX_CHARACTERS);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function plainTheme(): UsageTheme {
|
|
142
|
+
return { fg: (_color, text) => text, bold: (text) => text };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface ChartRenderOptions {
|
|
146
|
+
title: string;
|
|
147
|
+
formatValue: (value: number) => string;
|
|
148
|
+
/** Appended after the observed/budget amount, e.g. " tokens"; empty when formatValue already carries a unit prefix like "$". */
|
|
149
|
+
unitSuffix: string;
|
|
150
|
+
subtitle?: string;
|
|
151
|
+
budget?: number;
|
|
152
|
+
noDataText: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Shared cumulative bar-chart renderer behind renderUsageGraph and renderCostGraph. */
|
|
156
|
+
function renderChart(chart: RenderableChart, width: number, theme: UsageTheme, options: ChartRenderOptions): string[] {
|
|
157
|
+
const { formatValue, unitSuffix } = options;
|
|
158
|
+
const safeWidth = Math.max(20, width);
|
|
159
|
+
const chartColumns = Math.max(1, Math.floor((safeWidth - USAGE_Y_AXIS_WIDTH - 1) / 2));
|
|
160
|
+
const increments = mergeBuckets(chart.buckets, chartColumns);
|
|
161
|
+
const runningSeries: Record<string, number> = {};
|
|
162
|
+
let runningTotal = 0;
|
|
163
|
+
const buckets = increments.map((bucket) => {
|
|
164
|
+
runningTotal += bucket.total;
|
|
165
|
+
for (const [key, value] of Object.entries(bucket.series)) runningSeries[key] = (runningSeries[key] ?? 0) + value;
|
|
166
|
+
return { ...bucket, total: runningTotal, series: { ...runningSeries } };
|
|
167
|
+
});
|
|
168
|
+
const barStep = buckets.length * 2 <= safeWidth - USAGE_Y_AXIS_WIDTH ? 2 : 1;
|
|
169
|
+
const plotWidth = buckets.length * barStep;
|
|
170
|
+
const budget = typeof options.budget === "number" && Number.isFinite(options.budget) && options.budget > 0 ? options.budget : undefined;
|
|
171
|
+
const maximum = Math.max(chart.total, budget ?? 0);
|
|
172
|
+
const observed = chart.truncated ? `at least ${formatValue(chart.total)}` : formatValue(chart.total);
|
|
173
|
+
const budgetState = budget === undefined
|
|
174
|
+
? `${observed}${unitSuffix} · budget not configured${chart.truncated ? " · query limit reached" : ""}`
|
|
175
|
+
: chart.total > budget
|
|
176
|
+
? `${observed}${unitSuffix} / ${formatValue(budget)} budget · OVER BUDGET by ${chart.truncated ? "at least " : ""}${formatValue(chart.total - budget)}`
|
|
177
|
+
: chart.truncated
|
|
178
|
+
? `${observed}${unitSuffix} / ${formatValue(budget)} budget · state unknown · query limit reached`
|
|
179
|
+
: `${observed}${unitSuffix} / ${formatValue(budget)} budget · ${formatValue(budget - chart.total)} remaining`;
|
|
180
|
+
const lines = [
|
|
181
|
+
truncateToWidth(theme.bold(options.title), safeWidth, ""),
|
|
182
|
+
truncateToWidth(budgetState, safeWidth, "…"),
|
|
183
|
+
...(options.subtitle ? [truncateToWidth(options.subtitle, safeWidth, "…")] : []),
|
|
184
|
+
"",
|
|
185
|
+
];
|
|
186
|
+
if (maximum === 0) {
|
|
187
|
+
lines.push(theme.fg("dim", options.noDataText));
|
|
188
|
+
return lines.map((line) => truncateToWidth(line, safeWidth, "…"));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (let row = 0; row < USAGE_CHART_HEIGHT; row += 1) {
|
|
192
|
+
const fromBottom = USAGE_CHART_HEIGHT - row - 1;
|
|
193
|
+
const lower = maximum * fromBottom / USAGE_CHART_HEIGHT;
|
|
194
|
+
const upper = maximum * (fromBottom + 1) / USAGE_CHART_HEIGHT;
|
|
195
|
+
const thresholdRow = budget !== undefined && budget > lower && budget <= upper;
|
|
196
|
+
const label = thresholdRow ? formatValue(budget) : row === 0 ? formatValue(maximum) : row === Math.floor(USAGE_CHART_HEIGHT / 2) ? formatValue(maximum / 2) : "";
|
|
197
|
+
if (thresholdRow) {
|
|
198
|
+
const color = chart.total > budget ? "error" : "warning";
|
|
199
|
+
lines.push(`${label.padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", "│")}${theme.fg(color, "┄".repeat(plotWidth))}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
let plot = "";
|
|
203
|
+
for (const bucket of buckets) {
|
|
204
|
+
const scaled = bucket.total / maximum * USAGE_CHART_HEIGHT;
|
|
205
|
+
const occupancy = Math.max(0, Math.min(1, scaled - fromBottom));
|
|
206
|
+
if (occupancy <= 0) {
|
|
207
|
+
plot += " ".repeat(barStep);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const block = PARTIAL_BLOCKS[Math.max(0, Math.ceil(occupancy * PARTIAL_BLOCKS.length) - 1)]!;
|
|
211
|
+
const valueHeight = Math.min(bucket.total, maximum * (fromBottom + Math.min(occupancy, 0.5)) / USAGE_CHART_HEIGHT);
|
|
212
|
+
plot += seriesStyle(seriesAt(bucket, chart, valueHeight), theme)(block) + (barStep === 2 ? " " : "");
|
|
213
|
+
}
|
|
214
|
+
lines.push(`${label.padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", "│")}${plot}`);
|
|
215
|
+
}
|
|
216
|
+
lines.push(`${"0".padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", `└${"─".repeat(plotWidth)}`)}`);
|
|
217
|
+
lines.push(`${" ".repeat(USAGE_Y_AXIS_WIDTH)}${axisLabels(chart.start, chart.end, chart.period, plotWidth)}`);
|
|
218
|
+
lines.push("");
|
|
219
|
+
const displayedSeries = chart.series.slice(0, USAGE_RENDER_MAX_SERIES);
|
|
220
|
+
for (let index = 0; index < displayedSeries.length; index += 1) {
|
|
221
|
+
const series = displayedSeries[index]!;
|
|
222
|
+
const bullet = seriesStyle(index, theme)("■");
|
|
223
|
+
lines.push(truncateToWidth(`${bullet} ${displayIdentity(series.provider)}/${displayIdentity(series.model)} ${formatValue(series.total)}`, safeWidth, "…"));
|
|
224
|
+
}
|
|
225
|
+
if (chart.series.length > displayedSeries.length) lines.push(truncateToWidth(theme.fg("muted", `… ${chart.series.length - displayedSeries.length} more series omitted`), safeWidth, "…"));
|
|
226
|
+
return lines.map((line) => visibleWidth(line) <= safeWidth ? line : truncateToWidth(line, safeWidth, "…"));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function renderUsageGraph(chart: UsageGraph, width: number, theme: UsageTheme, tokenBudget?: number): string[] {
|
|
230
|
+
return renderChart(
|
|
231
|
+
{ period: chart.period, start: chart.start, end: chart.end, buckets: chart.buckets, series: chart.series, total: chart.totalTokens, truncated: chart.truncated },
|
|
232
|
+
width, theme,
|
|
233
|
+
{
|
|
234
|
+
title: `${usagePeriod(chart.period).label} token usage`,
|
|
235
|
+
formatValue: compact,
|
|
236
|
+
unitSuffix: " tokens",
|
|
237
|
+
subtitle: `input ${compact(chart.breakdown.input)} · output ${compact(chart.breakdown.output)} · cache ${compact(chart.breakdown.cacheRead + chart.breakdown.cacheWrite)}`,
|
|
238
|
+
budget: tokenBudget,
|
|
239
|
+
noDataText: "No recorded Pi token usage in this period.",
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function renderCostGraph(chart: CostGraph, width: number, theme: UsageTheme, costBudget?: number): string[] {
|
|
245
|
+
return renderChart(
|
|
246
|
+
{ period: chart.period, start: chart.start, end: chart.end, buckets: chart.buckets, series: chart.series, total: chart.totalUsd, truncated: chart.truncated },
|
|
247
|
+
width, theme,
|
|
248
|
+
{
|
|
249
|
+
title: `${usagePeriod(chart.period).label} cost`,
|
|
250
|
+
formatValue: formatUsd,
|
|
251
|
+
unitSuffix: "",
|
|
252
|
+
budget: costBudget,
|
|
253
|
+
noDataText: "No recorded Pi cost in this period.",
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export type UsageViewKind = "tokens" | "cost";
|
|
259
|
+
const USAGE_VIEWS: UsageViewKind[] = ["tokens", "cost"];
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* One bounded round trip: the daemon discovers distinct scopes (still capped at
|
|
263
|
+
* USAGE_MAX_DISTINCT_SCOPES -- more scopes than that is still a real, honestly-reported
|
|
264
|
+
* truncation) and SQL-side aggregates every matching observation into (scope, metric, bucket)
|
|
265
|
+
* sums for the exact window this panel renders. Replaces a per-scope fetch of up to
|
|
266
|
+
* USAGE_PER_SCOPE_QUERY_LIMIT raw rows each, which fixed a *different* problem (one heavy scope
|
|
267
|
+
* starving *other* scopes out of a shared row budget) but could still silently truncate a single
|
|
268
|
+
* heavy scope's *own* older history within the same window -- a real incident: a scope logging
|
|
269
|
+
* tens of thousands of rows a week had its "weekly" chart built from a few minutes of its most
|
|
270
|
+
* recent rows alone. Aggregation has no such failure mode: result size scales with (scopes x
|
|
271
|
+
* metrics x buckets), never with raw event count.
|
|
272
|
+
*/
|
|
273
|
+
async function loadPiMetrics(client: JittorPanelClient, window: ReturnType<typeof resolveUsageWindow>): Promise<{ rows: UsageAggregateRow[]; truncated: boolean }> {
|
|
274
|
+
const result = await client.call("metrics.usage_series", {
|
|
275
|
+
source: "pi", since: window.start, until: window.end, bucketSizeMs: window.bucketSizeMs, bucketCount: window.bucketCount, scopeLimit: USAGE_MAX_DISTINCT_SCOPES,
|
|
276
|
+
}) as { rows: UsageAggregateRow[]; truncated: boolean };
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function showUsagePanel(
|
|
281
|
+
ctx: ExtensionCommandContext,
|
|
282
|
+
client: JittorPanelClient,
|
|
283
|
+
budgets: Pick<UsageBudgetControl, "getUsageTokenBudget">,
|
|
284
|
+
now = Date.now(),
|
|
285
|
+
initialView: UsageViewKind = "tokens",
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
let periodIndex = 0;
|
|
288
|
+
let viewIndex = Math.max(0, USAGE_VIEWS.indexOf(initialView));
|
|
289
|
+
for (;;) {
|
|
290
|
+
const period = USAGE_PERIODS[periodIndex]!.id;
|
|
291
|
+
const window = resolveUsageWindow(period, now);
|
|
292
|
+
// One bounded query serves both views: token and cost metrics share the same "pi" source rows.
|
|
293
|
+
const { rows, truncated } = await loadPiMetrics(client, window);
|
|
294
|
+
const view = USAGE_VIEWS[viewIndex]!;
|
|
295
|
+
const tokenChart = buildUsageGraph(rows, window, { period, truncated });
|
|
296
|
+
const costChart = buildCostGraph(rows, window, { period, truncated });
|
|
297
|
+
const tokenBudget = budgets.getUsageTokenBudget(period);
|
|
298
|
+
const renderActive = (width: number, theme: UsageTheme): string[] =>
|
|
299
|
+
view === "tokens" ? renderUsageGraph(tokenChart, width, theme, tokenBudget) : renderCostGraph(costChart, width, theme);
|
|
300
|
+
if (ctx.mode !== "tui") {
|
|
301
|
+
ctx.ui.notify(renderActive(80, plainTheme()).join("\n"), "info");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const action = await ctx.ui.custom<UsageAction>((_tui, theme, _keybindings, done) => ({
|
|
305
|
+
invalidate() {},
|
|
306
|
+
render(width: number): string[] {
|
|
307
|
+
const lines = renderActive(width, theme);
|
|
308
|
+
const controls = theme.fg("dim", "←/→/Tab period · v view · r refresh · Esc close");
|
|
309
|
+
return [...lines, "", truncateToWidth(controls, width, "…")];
|
|
310
|
+
},
|
|
311
|
+
handleInput(data: string): void {
|
|
312
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || data === "q") done("close");
|
|
313
|
+
else if (matchesKey(data, "left") || matchesKey(data, "shift+tab")) done("period-prev");
|
|
314
|
+
else if (matchesKey(data, "right") || matchesKey(data, "tab")) done("period-next");
|
|
315
|
+
else if (data === "v") done("view-next");
|
|
316
|
+
else if (data === "r") done("refresh");
|
|
317
|
+
},
|
|
318
|
+
}));
|
|
319
|
+
if (!action || action === "close") return;
|
|
320
|
+
if (action === "period-prev") periodIndex = (periodIndex - 1 + USAGE_PERIODS.length) % USAGE_PERIODS.length;
|
|
321
|
+
if (action === "period-next") periodIndex = (periodIndex + 1) % USAGE_PERIODS.length;
|
|
322
|
+
if (action === "view-next") viewIndex = (viewIndex + 1) % USAGE_VIEWS.length;
|
|
323
|
+
}
|
|
324
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@danypops/pi-jittor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension for Jittor: native routing enforcement, footer, settings, usage graphs, and benchmark panels backed by the @danypops/jittor daemon",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": ["pi-package", "llm-router", "token-budget"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "bun test",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"pi": {
|
|
12
|
+
"extensions": ["extension/src/index.ts"]
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@danypops/daemon-kit": "^0.4.0",
|
|
16
|
+
"@danypops/jittor": "^0.12.0"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
20
|
+
"@earendil-works/pi-tui": "*",
|
|
21
|
+
"typebox": "*"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"bun-types": "latest"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/DanyPops/jittor.git",
|
|
29
|
+
"directory": "packages/pi-jittor"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"files": ["extension", "docs", "README.md"]
|
|
35
|
+
}
|