@danypops/pi-jittor 0.2.1 → 0.3.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 +19 -3
- package/docs/USAGE_PRIOR_ART.md +1 -1
- package/extension/src/index.ts +173 -43
- package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +200 -27
- package/extension/src/observability/context-growth.ts +26 -0
- package/extension/src/{context-report.ts → observability/context-report.ts} +35 -3
- package/extension/src/observability/context-view.ts +264 -0
- package/extension/src/{footer.ts → observability/footer.ts} +7 -0
- package/extension/src/observability/provider-context-snapshot.ts +246 -0
- package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +1 -1
- package/extension/src/{tui.ts → observability/status.ts} +55 -10
- package/extension/src/{usage.ts → observability/usage.ts} +74 -166
- package/extension/src/{benchmark-tui.ts → optimization/model-selection-panel.ts} +61 -29
- package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +2 -2
- package/extension/src/service-client.ts +48 -1
- package/extension/src/settings-tui.ts +57 -32
- package/extension/src/settings.ts +27 -19
- package/package.json +9 -3
- package/extension/src/context-view.ts +0 -140
- /package/extension/src/{capabilities → observability}/context-hub.ts +0 -0
- /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
- /package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +0 -0
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
} from "@danypops/jittor";
|
|
17
17
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
19
|
-
import type
|
|
20
|
-
import type {
|
|
19
|
+
import { HistoryChart, type HistoryChartTheme, ProgressBar, type TextMeasure } from "malevich-tui-components";
|
|
20
|
+
import type { UsageBudgetControl } from "../settings.ts";
|
|
21
|
+
import type { JittorPanelClient } from "./status.ts";
|
|
21
22
|
|
|
22
23
|
type UsageAction = "period-prev" | "period-next" | "view-next" | "refresh" | "close";
|
|
23
24
|
type UsageColor =
|
|
@@ -60,10 +61,9 @@ export interface UsageTheme {
|
|
|
60
61
|
* perceptual separation between adjacent categories.
|
|
61
62
|
*
|
|
62
63
|
* Terminal foreground color is a single channel with a hard ceiling on how many hues stay mutually
|
|
63
|
-
* distinguishable (most qualitative-palette guidance caps around 8–12).
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* on an additional channel rather than silently reusing an indistinguishable color).
|
|
64
|
+
* distinguishable (most qualitative-palette guidance caps around 8–12). seriesStyle derives both a
|
|
65
|
+
* hue and optional bold weight from stable identity bits, using a second visual channel to reduce
|
|
66
|
+
* collisions without allowing ranking or render order to change a model's appearance.
|
|
67
67
|
*/
|
|
68
68
|
const SERIES_HUES: UsageColor[] = [
|
|
69
69
|
"accent",
|
|
@@ -77,15 +77,23 @@ const SERIES_HUES: UsageColor[] = [
|
|
|
77
77
|
"syntaxOperator",
|
|
78
78
|
];
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Stable FNV-1a identity hash. Series ordering changes with totals and selected period, so assigning
|
|
82
|
+
* colors by array index makes a model change color whenever another model overtakes it. Deriving
|
|
83
|
+
* both visual channels from its provider/model key keeps that identity fixed across token and cost
|
|
84
|
+
* graphics, period switches, refreshes, and extension restarts.
|
|
85
|
+
*/
|
|
86
|
+
function seriesStyle(identity: string, theme: UsageTheme): (text: string) => string {
|
|
87
|
+
let hash = 0x811c9dc5;
|
|
88
|
+
for (let index = 0; index < identity.length; index += 1) {
|
|
89
|
+
hash ^= identity.charCodeAt(index);
|
|
90
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
91
|
+
}
|
|
92
|
+
const hue = SERIES_HUES[hash % SERIES_HUES.length]!;
|
|
93
|
+
const useBold = Math.floor(hash / SERIES_HUES.length) % 2 === 1;
|
|
84
94
|
return (text: string) => (useBold ? theme.bold(theme.fg(hue, text)) : theme.fg(hue, text));
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
const PARTIAL_BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
88
|
-
|
|
89
97
|
function compact(value: number): string {
|
|
90
98
|
if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(value >= 10_000_000_000 ? 0 : 1)}B`;
|
|
91
99
|
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1)}M`;
|
|
@@ -93,58 +101,6 @@ function compact(value: number): string {
|
|
|
93
101
|
return String(Math.round(value));
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
interface RenderableSeries {
|
|
97
|
-
key: string;
|
|
98
|
-
provider: string;
|
|
99
|
-
model: string;
|
|
100
|
-
total: number;
|
|
101
|
-
}
|
|
102
|
-
interface RenderableBucket {
|
|
103
|
-
start: number;
|
|
104
|
-
end: number;
|
|
105
|
-
total: number;
|
|
106
|
-
series: Record<string, number>;
|
|
107
|
-
}
|
|
108
|
-
interface RenderableChart {
|
|
109
|
-
period: UsagePeriod;
|
|
110
|
-
start: number;
|
|
111
|
-
end: number;
|
|
112
|
-
buckets: RenderableBucket[];
|
|
113
|
-
series: RenderableSeries[];
|
|
114
|
-
total: number;
|
|
115
|
-
truncated: boolean;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function mergeBuckets(buckets: RenderableBucket[], maximum: number): RenderableBucket[] {
|
|
119
|
-
if (buckets.length <= maximum) return buckets;
|
|
120
|
-
const result: RenderableBucket[] = [];
|
|
121
|
-
for (let index = 0; index < maximum; index += 1) {
|
|
122
|
-
const from = Math.floor((index * buckets.length) / maximum);
|
|
123
|
-
const to = Math.max(from + 1, Math.floor(((index + 1) * buckets.length) / maximum));
|
|
124
|
-
const selected = buckets.slice(from, to);
|
|
125
|
-
const series: Record<string, number> = {};
|
|
126
|
-
for (const bucket of selected) {
|
|
127
|
-
for (const [key, value] of Object.entries(bucket.series)) series[key] = (series[key] ?? 0) + value;
|
|
128
|
-
}
|
|
129
|
-
result.push({
|
|
130
|
-
start: selected[0]!.start,
|
|
131
|
-
end: selected[selected.length - 1]!.end,
|
|
132
|
-
total: selected.reduce((sum, bucket) => sum + bucket.total, 0),
|
|
133
|
-
series,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
return result;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function seriesAt(bucket: RenderableBucket, chart: RenderableChart, valueHeight: number): number {
|
|
140
|
-
let cumulative = 0;
|
|
141
|
-
for (let index = 0; index < chart.series.length; index += 1) {
|
|
142
|
-
cumulative += bucket.series[chart.series[index]!.key] ?? 0;
|
|
143
|
-
if (valueHeight <= cumulative) return index;
|
|
144
|
-
}
|
|
145
|
-
return Math.max(0, chart.series.length - 1);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
104
|
/** Compact USD formatter: full cents below $1k, then the same k/M suffix convention as compact(). */
|
|
149
105
|
function formatUsd(value: number): string {
|
|
150
106
|
const magnitude = Math.abs(value);
|
|
@@ -160,18 +116,6 @@ function formatPeriodPoint(value: number, period: UsagePeriod): string {
|
|
|
160
116
|
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
161
117
|
}
|
|
162
118
|
|
|
163
|
-
function axisLabels(start: number, end: number, period: UsagePeriod, width: number): string {
|
|
164
|
-
const labels = [formatPeriodPoint(start, period), formatPeriodPoint(start + (end - start) / 2, period), formatPeriodPoint(end, period)];
|
|
165
|
-
const positions = [0, Math.max(0, Math.floor((width - labels[1]!.length) / 2)), Math.max(0, width - labels[2]!.length)];
|
|
166
|
-
const characters = Array.from({ length: width }, () => " ");
|
|
167
|
-
for (let labelIndex = 0; labelIndex < labels.length; labelIndex += 1) {
|
|
168
|
-
for (let index = 0; index < labels[labelIndex]!.length && positions[labelIndex]! + index < width; index += 1) {
|
|
169
|
-
characters[positions[labelIndex]! + index] = labels[labelIndex]![index]!;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return characters.join("");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
119
|
function displayIdentity(value: string): string {
|
|
176
120
|
return value
|
|
177
121
|
.replace(/[\r\n\t]/g, " ")
|
|
@@ -184,110 +128,76 @@ function plainTheme(): UsageTheme {
|
|
|
184
128
|
return { fg: (_color, text) => text, bold: (text) => text };
|
|
185
129
|
}
|
|
186
130
|
|
|
131
|
+
const hostTextMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
132
|
+
|
|
133
|
+
function chartTheme(theme: UsageTheme, seriesIdentities: string[]): HistoryChartTheme {
|
|
134
|
+
return {
|
|
135
|
+
title: theme.bold,
|
|
136
|
+
subtitle: (text) => text,
|
|
137
|
+
axis: (text) => theme.fg("borderMuted", text),
|
|
138
|
+
warningLine: (text) => theme.fg("warning", text),
|
|
139
|
+
errorLine: (text) => theme.fg("error", text),
|
|
140
|
+
muted: (text) => theme.fg("muted", text),
|
|
141
|
+
series: (index) => seriesStyle(seriesIdentities[index] ?? `series:${index}`, theme),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
187
145
|
interface ChartRenderOptions {
|
|
188
146
|
title: string;
|
|
189
147
|
formatValue: (value: number) => string;
|
|
190
|
-
/** Appended after the observed/budget amount, e.g. " tokens"; empty when formatValue already carries a unit prefix like "$". */
|
|
191
148
|
unitSuffix: string;
|
|
192
149
|
subtitle?: string;
|
|
193
150
|
budget?: number;
|
|
194
151
|
noDataText: string;
|
|
195
152
|
}
|
|
196
153
|
|
|
197
|
-
/**
|
|
198
|
-
function renderChart(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
154
|
+
/** Adapts Jittor's domain graph to Malevich's reusable cumulative HistoryChart and budget ProgressBar. */
|
|
155
|
+
function renderChart(
|
|
156
|
+
chart: { period: UsagePeriod; buckets: UsageGraph["buckets"]; series: UsageGraph["series"]; total: number; truncated: boolean },
|
|
157
|
+
width: number,
|
|
158
|
+
theme: UsageTheme,
|
|
159
|
+
options: ChartRenderOptions,
|
|
160
|
+
): string[] {
|
|
161
|
+
const component = new HistoryChart({
|
|
162
|
+
title: options.title,
|
|
163
|
+
buckets: chart.buckets,
|
|
164
|
+
series: chart.series.map((series) => ({
|
|
165
|
+
key: series.key,
|
|
166
|
+
label: `${displayIdentity(series.provider)}/${displayIdentity(series.model)}`,
|
|
167
|
+
})),
|
|
168
|
+
formatValue: options.formatValue,
|
|
169
|
+
unitSuffix: options.unitSuffix,
|
|
170
|
+
...(options.subtitle ? { subtitle: options.subtitle } : {}),
|
|
171
|
+
...(options.budget !== undefined ? { budget: options.budget } : {}),
|
|
172
|
+
noDataText: options.noDataText,
|
|
173
|
+
truncated: chart.truncated,
|
|
174
|
+
formatAxisLabel: (value) => formatPeriodPoint(value, chart.period),
|
|
175
|
+
theme: chartTheme(
|
|
176
|
+
theme,
|
|
177
|
+
chart.series.map((series) => series.key),
|
|
178
|
+
),
|
|
179
|
+
measure: hostTextMeasure,
|
|
180
|
+
height: USAGE_CHART_HEIGHT,
|
|
181
|
+
yAxisWidth: USAGE_Y_AXIS_WIDTH,
|
|
182
|
+
maxSeriesShown: USAGE_RENDER_MAX_SERIES,
|
|
209
183
|
});
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
budget
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
? `${observed}${unitSuffix} / ${formatValue(budget)} budget · state unknown · query limit reached`
|
|
222
|
-
: `${observed}${unitSuffix} / ${formatValue(budget)} budget · ${formatValue(budget - chart.total)} remaining`;
|
|
223
|
-
const lines = [
|
|
224
|
-
truncateToWidth(theme.bold(options.title), safeWidth, ""),
|
|
225
|
-
truncateToWidth(budgetState, safeWidth, "…"),
|
|
226
|
-
...(options.subtitle ? [truncateToWidth(options.subtitle, safeWidth, "…")] : []),
|
|
227
|
-
"",
|
|
228
|
-
];
|
|
229
|
-
if (maximum === 0) {
|
|
230
|
-
lines.push(theme.fg("dim", options.noDataText));
|
|
231
|
-
return lines.map((line) => truncateToWidth(line, safeWidth, "…"));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
for (let row = 0; row < USAGE_CHART_HEIGHT; row += 1) {
|
|
235
|
-
const fromBottom = USAGE_CHART_HEIGHT - row - 1;
|
|
236
|
-
const lower = (maximum * fromBottom) / USAGE_CHART_HEIGHT;
|
|
237
|
-
const upper = (maximum * (fromBottom + 1)) / USAGE_CHART_HEIGHT;
|
|
238
|
-
const thresholdRow = budget !== undefined && budget > lower && budget <= upper;
|
|
239
|
-
const label = thresholdRow
|
|
240
|
-
? formatValue(budget)
|
|
241
|
-
: row === 0
|
|
242
|
-
? formatValue(maximum)
|
|
243
|
-
: row === Math.floor(USAGE_CHART_HEIGHT / 2)
|
|
244
|
-
? formatValue(maximum / 2)
|
|
245
|
-
: "";
|
|
246
|
-
if (thresholdRow) {
|
|
247
|
-
const color = chart.total > budget ? "error" : "warning";
|
|
248
|
-
lines.push(`${label.padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", "│")}${theme.fg(color, "┄".repeat(plotWidth))}`);
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
let plot = "";
|
|
252
|
-
for (const bucket of buckets) {
|
|
253
|
-
const scaled = (bucket.total / maximum) * USAGE_CHART_HEIGHT;
|
|
254
|
-
const occupancy = Math.max(0, Math.min(1, scaled - fromBottom));
|
|
255
|
-
if (occupancy <= 0) {
|
|
256
|
-
plot += " ".repeat(barStep);
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const block = PARTIAL_BLOCKS[Math.max(0, Math.ceil(occupancy * PARTIAL_BLOCKS.length) - 1)]!;
|
|
260
|
-
const valueHeight = Math.min(bucket.total, (maximum * (fromBottom + Math.min(occupancy, 0.5))) / USAGE_CHART_HEIGHT);
|
|
261
|
-
plot += seriesStyle(seriesAt(bucket, chart, valueHeight), theme)(block) + (barStep === 2 ? " " : "");
|
|
262
|
-
}
|
|
263
|
-
lines.push(`${label.padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", "│")}${plot}`);
|
|
264
|
-
}
|
|
265
|
-
lines.push(`${"0".padStart(USAGE_Y_AXIS_WIDTH - 2)} ${theme.fg("borderMuted", `└${"─".repeat(plotWidth)}`)}`);
|
|
266
|
-
lines.push(`${" ".repeat(USAGE_Y_AXIS_WIDTH)}${axisLabels(chart.start, chart.end, chart.period, plotWidth)}`);
|
|
267
|
-
lines.push("");
|
|
268
|
-
const displayedSeries = chart.series.slice(0, USAGE_RENDER_MAX_SERIES);
|
|
269
|
-
for (let index = 0; index < displayedSeries.length; index += 1) {
|
|
270
|
-
const series = displayedSeries[index]!;
|
|
271
|
-
const bullet = seriesStyle(index, theme)("■");
|
|
272
|
-
lines.push(
|
|
273
|
-
truncateToWidth(
|
|
274
|
-
`${bullet} ${displayIdentity(series.provider)}/${displayIdentity(series.model)} ${formatValue(series.total)}`,
|
|
275
|
-
safeWidth,
|
|
276
|
-
"…",
|
|
277
|
-
),
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
if (chart.series.length > displayedSeries.length)
|
|
281
|
-
lines.push(truncateToWidth(theme.fg("muted", `… ${chart.series.length - displayedSeries.length} more series omitted`), safeWidth, "…"));
|
|
282
|
-
return lines.map((line) => (visibleWidth(line) <= safeWidth ? line : truncateToWidth(line, safeWidth, "…")));
|
|
184
|
+
const lines = component.render(width);
|
|
185
|
+
if (options.budget === undefined || !Number.isFinite(options.budget) || options.budget <= 0 || chart.total === 0) return lines;
|
|
186
|
+
const meter = new ProgressBar({
|
|
187
|
+
value: chart.total,
|
|
188
|
+
max: options.budget,
|
|
189
|
+
label: "Budget",
|
|
190
|
+
style: chart.total > options.budget ? (text) => theme.fg("error", text) : (text) => theme.fg("accent", text),
|
|
191
|
+
measure: hostTextMeasure,
|
|
192
|
+
});
|
|
193
|
+
lines.splice(options.subtitle ? 3 : 2, 0, ...meter.render(Math.max(20, width)));
|
|
194
|
+
return lines;
|
|
283
195
|
}
|
|
284
196
|
|
|
285
197
|
export function renderUsageGraph(chart: UsageGraph, width: number, theme: UsageTheme, tokenBudget?: number): string[] {
|
|
286
198
|
return renderChart(
|
|
287
199
|
{
|
|
288
200
|
period: chart.period,
|
|
289
|
-
start: chart.start,
|
|
290
|
-
end: chart.end,
|
|
291
201
|
buckets: chart.buckets,
|
|
292
202
|
series: chart.series,
|
|
293
203
|
total: chart.totalTokens,
|
|
@@ -310,8 +220,6 @@ export function renderCostGraph(chart: CostGraph, width: number, theme: UsageThe
|
|
|
310
220
|
return renderChart(
|
|
311
221
|
{
|
|
312
222
|
period: chart.period,
|
|
313
|
-
start: chart.start,
|
|
314
|
-
end: chart.end,
|
|
315
223
|
buckets: chart.buckets,
|
|
316
224
|
series: chart.series,
|
|
317
225
|
total: chart.totalUsd,
|
|
@@ -14,8 +14,9 @@ import {
|
|
|
14
14
|
type UtilityComponentName,
|
|
15
15
|
} from "@danypops/jittor";
|
|
16
16
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
18
|
-
import {
|
|
17
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
18
|
+
import { BorderedSelectPanel, Table, type TextMeasure } from "malevich-tui-components";
|
|
19
|
+
import { sessionSecretField } from "../session-identity.ts";
|
|
19
20
|
|
|
20
21
|
export interface BenchmarkPanelClient {
|
|
21
22
|
call(operation: string, input: unknown): Promise<any>;
|
|
@@ -36,18 +37,25 @@ function componentText(item: RankedModel): string {
|
|
|
36
37
|
.join(" · ");
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
const hostTextMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
41
|
+
|
|
42
|
+
function benchmarkRows(shown: RankedModel[], currentIdentity: string): Record<string, string>[] {
|
|
43
|
+
return shown.flatMap((item, index) => {
|
|
44
|
+
const current = item.identity.startsWith(`${currentIdentity}:`);
|
|
45
|
+
const localSamples = item.components.find((component) => component.name === "reliability")?.evidenceCount ?? 0;
|
|
46
|
+
const provenance = item.provenance
|
|
47
|
+
.slice(0, BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE)
|
|
48
|
+
.map((source) => `${source.sourceId}@${source.revision} ${source.freshness}`)
|
|
49
|
+
.join(" · ");
|
|
50
|
+
return [
|
|
51
|
+
{ rank: `${index + 1}.`, detail: `${item.identity}${index === 0 ? " recommended" : ""}${current ? " current" : ""}` },
|
|
52
|
+
{
|
|
53
|
+
rank: "",
|
|
54
|
+
detail: `utility ${item.utility === null ? "?" : item.utility.toFixed(3)} · confidence ${(item.confidence * 100).toFixed(0)}% · ${componentText(item)}`,
|
|
55
|
+
},
|
|
56
|
+
{ rank: "", detail: `local n=${localSamples}${provenance ? ` · ${provenance}` : " · no external provenance"}` },
|
|
57
|
+
];
|
|
58
|
+
});
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
export function renderBenchmarkView(result: ModelRankingResult, currentIdentity: string, width: number, theme: BenchmarkTheme): string[] {
|
|
@@ -61,21 +69,45 @@ export function renderBenchmarkView(result: ModelRankingResult, currentIdentity:
|
|
|
61
69
|
: recommended && currentIndex === 0
|
|
62
70
|
? "Current model is the top recommendation."
|
|
63
71
|
: "Current model is outside the ranked candidates.";
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
const table = new Table({
|
|
73
|
+
columns: [
|
|
74
|
+
{ header: "#", key: "rank", width: 4 },
|
|
75
|
+
{ header: "Candidate / evidence", key: "detail" },
|
|
76
|
+
],
|
|
77
|
+
rows: benchmarkRows(shown, currentIdentity),
|
|
78
|
+
headerStyle: theme.bold,
|
|
79
|
+
measure: hostTextMeasure,
|
|
80
|
+
});
|
|
81
|
+
const content = {
|
|
82
|
+
invalidate: () => table.invalidate(),
|
|
83
|
+
render: (availableWidth: number): string[] => [
|
|
84
|
+
truncateToWidth(
|
|
85
|
+
result.scopeAuthority === "exact-session"
|
|
86
|
+
? "Scope: exact session"
|
|
87
|
+
: "Scope: available models · ADVISORY (exact session scope unavailable)",
|
|
88
|
+
availableWidth,
|
|
89
|
+
"…",
|
|
90
|
+
),
|
|
91
|
+
truncateToWidth(`Domain: ${result.domain} · Type: ${result.type} · evidence ${result.completeness}`, availableWidth, "…"),
|
|
92
|
+
truncateToWidth(reason, availableWidth, "…"),
|
|
93
|
+
...table.render(availableWidth),
|
|
94
|
+
...(result.ranked.length > shown.length
|
|
95
|
+
? [truncateToWidth(`… ${result.ranked.length - shown.length} more candidates omitted`, availableWidth, "…")]
|
|
96
|
+
: []),
|
|
97
|
+
...(result.scopeWarning ? [truncateToWidth(result.scopeWarning, availableWidth, "…")] : []),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
return new BorderedSelectPanel({
|
|
101
|
+
title: "Jittor Benchmark Recommendations",
|
|
102
|
+
list: content,
|
|
103
|
+
helpText: "r refresh · Esc close",
|
|
104
|
+
theme: {
|
|
105
|
+
border: (text) => theme.fg("borderMuted", text),
|
|
106
|
+
title: theme.bold,
|
|
107
|
+
help: (text) => theme.fg("dim", text),
|
|
108
|
+
},
|
|
109
|
+
measure: hostTextMeasure,
|
|
110
|
+
}).render(safeWidth);
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
export async function showBenchmarkPanel(
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
MILLISECONDS_PER_SECOND,
|
|
13
13
|
} from "@danypops/jittor";
|
|
14
14
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
15
|
+
import { headerValue } from "../../observability/http-headers.ts";
|
|
16
|
+
import type { CodexRecoveryControl } from "../../settings.ts";
|
|
17
17
|
|
|
18
18
|
export interface CodexRecoveryRuntime {
|
|
19
19
|
now(): number;
|
|
@@ -10,11 +10,58 @@ function defaultConnector(): Promise<JittorClient> {
|
|
|
10
10
|
let connector: JittorConnector = defaultConnector;
|
|
11
11
|
let retrying: RetryingClient<JittorClient> = createRetryingClient(() => connector(), { label: "Jittor" });
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Exhaustive transport-retry policy. New daemon operations cannot compile until their mutability
|
|
15
|
+
* is classified: only reads may be transparently invoked twice after a connection-shaped error.
|
|
16
|
+
*/
|
|
17
|
+
const OPERATION_RETRY_MODE = {
|
|
18
|
+
"metrics.record": "once",
|
|
19
|
+
"metrics.record_batch": "once",
|
|
20
|
+
"metrics.query": "retry",
|
|
21
|
+
"metrics.distinct_scopes": "retry",
|
|
22
|
+
"metrics.usage_series": "retry",
|
|
23
|
+
"metrics.cost_by_task": "retry",
|
|
24
|
+
"metrics.prune": "once",
|
|
25
|
+
"benchmark.refresh": "once",
|
|
26
|
+
"benchmark.status": "retry",
|
|
27
|
+
"benchmark.query": "retry",
|
|
28
|
+
"catalog.refresh": "once",
|
|
29
|
+
"catalog.status": "retry",
|
|
30
|
+
"catalog.query": "retry",
|
|
31
|
+
"usage.import": "once",
|
|
32
|
+
"usage.import_status": "retry",
|
|
33
|
+
"usage.import_cancel": "once",
|
|
34
|
+
"export.status": "retry",
|
|
35
|
+
"export.flush": "once",
|
|
36
|
+
"session.register": "once",
|
|
37
|
+
"session.release": "once",
|
|
38
|
+
"models.rank": "once",
|
|
39
|
+
"context.assess": "retry",
|
|
40
|
+
"context.delta": "retry",
|
|
41
|
+
"context.snapshot": "once",
|
|
42
|
+
"compaction.estimate": "retry",
|
|
43
|
+
"service.checkpoint": "once",
|
|
44
|
+
"telemetry.poll": "retry",
|
|
45
|
+
"router.status": "retry",
|
|
46
|
+
"router.decide": "retry",
|
|
47
|
+
"router.pause": "once",
|
|
48
|
+
"router.resume": "once",
|
|
49
|
+
"router.override": "once",
|
|
50
|
+
"router.clear_override": "once",
|
|
51
|
+
"router.current_route": "once",
|
|
52
|
+
"router.available_routes": "once",
|
|
53
|
+
} as const satisfies Record<OperationName, "retry" | "once">;
|
|
54
|
+
|
|
55
|
+
export function operationRetryMode(operation: OperationName): "retry" | "once" {
|
|
56
|
+
return OPERATION_RETRY_MODE[operation];
|
|
57
|
+
}
|
|
58
|
+
|
|
13
59
|
export async function callJittor<Name extends OperationName>(
|
|
14
60
|
operation: Name,
|
|
15
61
|
input: OperationInputs[Name],
|
|
16
62
|
): Promise<OperationOutputs[Name]> {
|
|
17
|
-
|
|
63
|
+
const invoke = (client: JittorClient) => client.call(operation, input);
|
|
64
|
+
return operationRetryMode(operation) === "retry" ? retrying.call(invoke) : retrying.callOnce(invoke);
|
|
18
65
|
}
|
|
19
66
|
|
|
20
67
|
export function setJittorClientConnectorForTests(value: JittorConnector): void {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { USAGE_PERIODS, type UsagePeriod } from "@danypops/jittor";
|
|
2
2
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { BorderedSelectPanel, Menu, type MenuTheme, type TextMeasure } from "malevich-tui-components";
|
|
4
5
|
import type { CodexRecoveryControl, EnforcementControl, UsageBudgetControl } from "./settings.ts";
|
|
5
6
|
|
|
6
7
|
export interface SettingsSnapshot {
|
|
@@ -56,16 +57,53 @@ export function settingsSnapshot(
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
const hostTextMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
61
|
+
|
|
62
|
+
function menuTheme(theme: SettingsTheme): MenuTheme {
|
|
63
|
+
return {
|
|
64
|
+
border: () => "",
|
|
65
|
+
selected: (text) => theme.fg("accent", text),
|
|
66
|
+
normal: (text) => text,
|
|
67
|
+
dim: (text) => theme.fg("dim", text),
|
|
68
|
+
title: theme.bold,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createSettingsPanel(
|
|
73
|
+
snapshot: SettingsSnapshot,
|
|
74
|
+
theme: SettingsTheme,
|
|
75
|
+
onAction: (action: SettingsAction) => void,
|
|
76
|
+
selected = 0,
|
|
77
|
+
): BorderedSelectPanel {
|
|
78
|
+
const menu = new Menu({
|
|
79
|
+
items: SETTINGS_KEYS.map((key) => ({ label: rowText(key, snapshot, theme), action: () => onAction({ kind: "activate", key }) })),
|
|
80
|
+
theme: menuTheme(theme),
|
|
81
|
+
onClose: () => onAction({ kind: "close" }),
|
|
82
|
+
measure: hostTextMeasure,
|
|
83
|
+
matchesKey: (data, key) => {
|
|
84
|
+
if (key === "enter") return matchesKey(data, "enter") || matchesKey(data, "space");
|
|
85
|
+
if (key === "escape") return matchesKey(data, "escape") || matchesKey(data, "ctrl+c");
|
|
86
|
+
if (key === "up") return matchesKey(data, "up");
|
|
87
|
+
if (key === "down") return matchesKey(data, "down");
|
|
88
|
+
return false;
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
for (let index = 0; index < selected; index += 1) menu.handleInput("\x1b[B");
|
|
92
|
+
return new BorderedSelectPanel({
|
|
93
|
+
title: "Jittor Settings",
|
|
94
|
+
list: menu,
|
|
95
|
+
helpText: "Token budgets are user values; provider quotas remain separate. · ↑/↓ select · Enter edit · Esc close",
|
|
96
|
+
theme: {
|
|
97
|
+
border: (text) => theme.fg("borderMuted", text),
|
|
98
|
+
title: theme.bold,
|
|
99
|
+
help: (text) => theme.fg("dim", text),
|
|
100
|
+
},
|
|
101
|
+
measure: hostTextMeasure,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
59
105
|
export function renderSettingsView(snapshot: SettingsSnapshot, selected: number, width: number, theme: SettingsTheme): string[] {
|
|
60
|
-
|
|
61
|
-
const lines = [theme.bold("Jittor Settings"), theme.fg("dim", "Token budgets are user values; provider quotas remain separate."), ""];
|
|
62
|
-
for (let index = 0; index < SETTINGS_KEYS.length; index += 1) {
|
|
63
|
-
const selectedRow = index === selected;
|
|
64
|
-
const prefix = selectedRow ? theme.fg("accent", "› ") : " ";
|
|
65
|
-
lines.push(`${prefix}${rowText(SETTINGS_KEYS[index]!, snapshot, theme)}`);
|
|
66
|
-
}
|
|
67
|
-
lines.push("", theme.fg("dim", "↑/↓ select · Enter edit · Esc close"));
|
|
68
|
-
return lines.map((line) => (visibleWidth(line) <= safeWidth ? line : truncateToWidth(line, safeWidth, "…")));
|
|
106
|
+
return createSettingsPanel(snapshot, theme, () => undefined, Math.max(0, selected)).render(Math.max(20, width));
|
|
69
107
|
}
|
|
70
108
|
|
|
71
109
|
function plainTheme(): SettingsTheme {
|
|
@@ -79,7 +117,7 @@ async function editBudget(ctx: ExtensionCommandContext, budgets: UsageBudgetCont
|
|
|
79
117
|
if (input === undefined) return;
|
|
80
118
|
const normalized = input.trim().toLowerCase();
|
|
81
119
|
if (normalized === "off" || normalized === "clear") {
|
|
82
|
-
budgets.setUsageTokenBudget(period, undefined);
|
|
120
|
+
await budgets.setUsageTokenBudget(period, undefined);
|
|
83
121
|
ctx.ui.notify(`${label} token budget cleared.`, "info");
|
|
84
122
|
return;
|
|
85
123
|
}
|
|
@@ -88,7 +126,7 @@ async function editBudget(ctx: ExtensionCommandContext, budgets: UsageBudgetCont
|
|
|
88
126
|
ctx.ui.notify("Enter a positive token count, or `off` to clear this threshold.", "warning");
|
|
89
127
|
return;
|
|
90
128
|
}
|
|
91
|
-
budgets.setUsageTokenBudget(period, tokens);
|
|
129
|
+
await budgets.setUsageTokenBudget(period, tokens);
|
|
92
130
|
ctx.ui.notify(`${label} token budget set to ${tokens.toLocaleString()} tokens.`, "info");
|
|
93
131
|
}
|
|
94
132
|
|
|
@@ -104,33 +142,20 @@ export async function showSettingsPanel(
|
|
|
104
142
|
},
|
|
105
143
|
): Promise<void> {
|
|
106
144
|
if (ctx.mode !== "tui") {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.slice(0, -2)
|
|
110
|
-
.join("\n"),
|
|
111
|
-
"info",
|
|
112
|
-
);
|
|
145
|
+
const snapshot = settingsSnapshot(enforcement, recovery, budgets);
|
|
146
|
+
ctx.ui.notify(["Jittor Settings", ...SETTINGS_KEYS.map((key) => rowText(key, snapshot, plainTheme()))].join("\n"), "info");
|
|
113
147
|
return;
|
|
114
148
|
}
|
|
115
149
|
for (;;) {
|
|
116
150
|
const snapshot = settingsSnapshot(enforcement, recovery, budgets);
|
|
117
151
|
const action = await ctx.ui.custom<SettingsAction>((tui, theme, _keybindings, done) => {
|
|
118
|
-
|
|
152
|
+
const panel = createSettingsPanel(snapshot, theme, done);
|
|
119
153
|
return {
|
|
120
|
-
invalidate()
|
|
121
|
-
render(width
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || data === "q") done({ kind: "close" });
|
|
126
|
-
else if (matchesKey(data, "up")) {
|
|
127
|
-
selected = (selected - 1 + SETTINGS_KEYS.length) % SETTINGS_KEYS.length;
|
|
128
|
-
tui.requestRender();
|
|
129
|
-
} else if (matchesKey(data, "down")) {
|
|
130
|
-
selected = (selected + 1) % SETTINGS_KEYS.length;
|
|
131
|
-
tui.requestRender();
|
|
132
|
-
} else if (matchesKey(data, "return") || matchesKey(data, "enter") || matchesKey(data, "space"))
|
|
133
|
-
done({ kind: "activate", key: SETTINGS_KEYS[selected]! });
|
|
154
|
+
invalidate: () => panel.invalidate(),
|
|
155
|
+
render: (width) => panel.render(width),
|
|
156
|
+
handleInput(data: string) {
|
|
157
|
+
panel.handleInput(data);
|
|
158
|
+
tui.requestRender();
|
|
134
159
|
},
|
|
135
160
|
};
|
|
136
161
|
});
|