@danypops/pi-jittor 0.2.0 → 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 +379 -118
- package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +251 -47
- package/extension/src/observability/context-growth.ts +26 -0
- package/extension/src/{capabilities → observability}/context-hub.ts +1 -5
- package/extension/src/observability/context-report.ts +92 -0
- package/extension/src/observability/context-view.ts +264 -0
- package/extension/src/{footer.ts → observability/footer.ts} +63 -26
- package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +14 -10
- package/extension/src/observability/provider-context-snapshot.ts +246 -0
- package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +21 -7
- package/extension/src/{tui.ts → observability/status.ts} +202 -72
- package/extension/src/observability/usage.ts +314 -0
- package/extension/src/optimization/model-selection-panel.ts +160 -0
- package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +41 -25
- package/extension/src/service-client.ts +49 -2
- package/extension/src/settings-tui.ts +73 -33
- package/extension/src/settings.ts +40 -29
- package/package.json +11 -5
- package/extension/src/benchmark-tui.ts +0 -113
- package/extension/src/context-report.ts +0 -49
- package/extension/src/context-view.ts +0 -108
- package/extension/src/usage.ts +0 -324
- /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { USAGE_PERIODS, type UsagePeriod } from "@danypops/jittor";
|
|
1
2
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
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 {
|
|
@@ -24,12 +25,7 @@ export interface SettingsEffects {
|
|
|
24
25
|
setRecovery(enabled: boolean): void | Promise<void>;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const SETTINGS_KEYS: SettingsKey[] = [
|
|
28
|
-
"enforcement",
|
|
29
|
-
"footer",
|
|
30
|
-
"recovery",
|
|
31
|
-
...USAGE_PERIODS.map(({ id }) => `budget-${id}` as const),
|
|
32
|
-
];
|
|
28
|
+
const SETTINGS_KEYS: SettingsKey[] = ["enforcement", "footer", "recovery", ...USAGE_PERIODS.map(({ id }) => `budget-${id}` as const)];
|
|
33
29
|
|
|
34
30
|
function state(enabled: boolean, theme: SettingsTheme): string {
|
|
35
31
|
return enabled ? theme.fg("success", "ON") : theme.fg("muted", "OFF");
|
|
@@ -61,20 +57,53 @@ export function settingsSnapshot(
|
|
|
61
57
|
};
|
|
62
58
|
}
|
|
63
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
|
+
|
|
64
105
|
export function renderSettingsView(snapshot: SettingsSnapshot, selected: number, width: number, theme: SettingsTheme): string[] {
|
|
65
|
-
|
|
66
|
-
const lines = [
|
|
67
|
-
theme.bold("Jittor Settings"),
|
|
68
|
-
theme.fg("dim", "Token budgets are user values; provider quotas remain separate."),
|
|
69
|
-
"",
|
|
70
|
-
];
|
|
71
|
-
for (let index = 0; index < SETTINGS_KEYS.length; index += 1) {
|
|
72
|
-
const selectedRow = index === selected;
|
|
73
|
-
const prefix = selectedRow ? theme.fg("accent", "› ") : " ";
|
|
74
|
-
lines.push(`${prefix}${rowText(SETTINGS_KEYS[index]!, snapshot, theme)}`);
|
|
75
|
-
}
|
|
76
|
-
lines.push("", theme.fg("dim", "↑/↓ select · Enter edit · Esc close"));
|
|
77
|
-
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));
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
function plainTheme(): SettingsTheme {
|
|
@@ -88,7 +117,7 @@ async function editBudget(ctx: ExtensionCommandContext, budgets: UsageBudgetCont
|
|
|
88
117
|
if (input === undefined) return;
|
|
89
118
|
const normalized = input.trim().toLowerCase();
|
|
90
119
|
if (normalized === "off" || normalized === "clear") {
|
|
91
|
-
budgets.setUsageTokenBudget(period, undefined);
|
|
120
|
+
await budgets.setUsageTokenBudget(period, undefined);
|
|
92
121
|
ctx.ui.notify(`${label} token budget cleared.`, "info");
|
|
93
122
|
return;
|
|
94
123
|
}
|
|
@@ -97,7 +126,7 @@ async function editBudget(ctx: ExtensionCommandContext, budgets: UsageBudgetCont
|
|
|
97
126
|
ctx.ui.notify("Enter a positive token count, or `off` to clear this threshold.", "warning");
|
|
98
127
|
return;
|
|
99
128
|
}
|
|
100
|
-
budgets.setUsageTokenBudget(period, tokens);
|
|
129
|
+
await budgets.setUsageTokenBudget(period, tokens);
|
|
101
130
|
ctx.ui.notify(`${label} token budget set to ${tokens.toLocaleString()} tokens.`, "info");
|
|
102
131
|
}
|
|
103
132
|
|
|
@@ -113,28 +142,33 @@ export async function showSettingsPanel(
|
|
|
113
142
|
},
|
|
114
143
|
): Promise<void> {
|
|
115
144
|
if (ctx.mode !== "tui") {
|
|
116
|
-
|
|
145
|
+
const snapshot = settingsSnapshot(enforcement, recovery, budgets);
|
|
146
|
+
ctx.ui.notify(["Jittor Settings", ...SETTINGS_KEYS.map((key) => rowText(key, snapshot, plainTheme()))].join("\n"), "info");
|
|
117
147
|
return;
|
|
118
148
|
}
|
|
119
149
|
for (;;) {
|
|
120
150
|
const snapshot = settingsSnapshot(enforcement, recovery, budgets);
|
|
121
151
|
const action = await ctx.ui.custom<SettingsAction>((tui, theme, _keybindings, done) => {
|
|
122
|
-
|
|
152
|
+
const panel = createSettingsPanel(snapshot, theme, done);
|
|
123
153
|
return {
|
|
124
|
-
invalidate()
|
|
125
|
-
render(width
|
|
126
|
-
handleInput(data: string)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
else if (matchesKey(data, "down")) { selected = (selected + 1) % SETTINGS_KEYS.length; tui.requestRender(); }
|
|
130
|
-
else if (matchesKey(data, "return") || matchesKey(data, "enter") || matchesKey(data, "space")) 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();
|
|
131
159
|
},
|
|
132
160
|
};
|
|
133
161
|
});
|
|
134
162
|
if (!action || action.kind === "close") return;
|
|
135
163
|
if (action.key === "enforcement") {
|
|
136
164
|
if (enforcement.isEnabled()) {
|
|
137
|
-
if (
|
|
165
|
+
if (
|
|
166
|
+
await ctx.ui.confirm(
|
|
167
|
+
"Disable routing enforcement?",
|
|
168
|
+
"Jittor will remain monitor-only and will no longer block unsafe provider requests.",
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
await effects.setEnforcement(false);
|
|
138
172
|
} else await effects.setEnforcement(true);
|
|
139
173
|
continue;
|
|
140
174
|
}
|
|
@@ -144,7 +178,13 @@ export async function showSettingsPanel(
|
|
|
144
178
|
}
|
|
145
179
|
if (action.key === "recovery") {
|
|
146
180
|
if (!recovery.isCodexRecoveryEnabled()) {
|
|
147
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
await ctx.ui.confirm(
|
|
183
|
+
"Enable Codex recovery?",
|
|
184
|
+
"Jittor may start bounded hidden retries only after transient Codex failures fully settle.",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
await effects.setRecovery(true);
|
|
148
188
|
} else await effects.setRecovery(false);
|
|
149
189
|
continue;
|
|
150
190
|
}
|
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { JITTOR_EXTENSION_SETTINGS_FILENAME, JITTOR_STATE_DIRECTORY, USAGE_PERIODS, type UsagePeriod } from "@danypops/jittor";
|
|
5
|
+
import { createAtomicJsonWriter } from "@danypops/vehicle-core";
|
|
6
|
+
import { createNodeAtomicJsonFsAdapter } from "@danypops/vehicle-server/atomic-json";
|
|
4
7
|
|
|
8
|
+
const atomicJson = createAtomicJsonWriter({ fs: createNodeAtomicJsonFsAdapter() });
|
|
9
|
+
|
|
10
|
+
// Every setter persists to disk before resolving, so a caller that awaits it (every real call
|
|
11
|
+
// site in index.ts/settings-tui.ts already runs inside an async handler) observes a completed
|
|
12
|
+
// write -- no fire-and-forget persistence that a subsequent synchronous re-read could race. The
|
|
13
|
+
// return type stays `void | Promise<void>` (not a strict `Promise<void>`) so a test/adapter
|
|
14
|
+
// implementation that has no real persistence to await (e.g. the no-op fallback stubs in
|
|
15
|
+
// index.ts's usageBudgetControl()/recoveryControl()) can stay trivially synchronous.
|
|
5
16
|
export interface EnforcementControl {
|
|
6
17
|
isEnabled(): boolean;
|
|
7
|
-
setEnabled(enabled: boolean): void
|
|
18
|
+
setEnabled(enabled: boolean): void | Promise<void>;
|
|
8
19
|
isFooterEnabled(): boolean;
|
|
9
|
-
setFooterEnabled(enabled: boolean): void
|
|
20
|
+
setFooterEnabled(enabled: boolean): void | Promise<void>;
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
export interface CodexRecoveryControl {
|
|
13
24
|
isCodexRecoveryEnabled(): boolean;
|
|
14
|
-
setCodexRecoveryEnabled(enabled: boolean): void
|
|
25
|
+
setCodexRecoveryEnabled(enabled: boolean): void | Promise<void>;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
export interface UsageBudgetControl {
|
|
18
29
|
getUsageTokenBudget(period: UsagePeriod): number | undefined;
|
|
19
|
-
setUsageTokenBudget(period: UsagePeriod, tokens: number | undefined): void
|
|
30
|
+
setUsageTokenBudget(period: UsagePeriod, tokens: number | undefined): void | Promise<void>;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
export interface PersistentExtensionControl extends EnforcementControl, CodexRecoveryControl, UsageBudgetControl {}
|
|
@@ -35,14 +46,16 @@ function defaultSettings(): ExtensionSettings {
|
|
|
35
46
|
function parseUsageTokenBudgets(value: unknown): Partial<Record<UsagePeriod, number>> {
|
|
36
47
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
37
48
|
const record = value as Record<string, unknown>;
|
|
38
|
-
return Object.fromEntries(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
return Object.fromEntries(
|
|
50
|
+
USAGE_PERIODS.flatMap(({ id }) => {
|
|
51
|
+
const tokens = record[id];
|
|
52
|
+
return typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0 ? [[id, tokens]] : [];
|
|
53
|
+
}),
|
|
54
|
+
) as Partial<Record<UsagePeriod, number>>;
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
function settingsPath(env: Record<string, string | undefined> = process.env): string {
|
|
45
|
-
const config = env
|
|
58
|
+
const config = env.XDG_CONFIG_HOME ?? join(env.HOME ?? ".", ".config");
|
|
46
59
|
return join(config, JITTOR_STATE_DIRECTORY, JITTOR_EXTENSION_SETTINGS_FILENAME);
|
|
47
60
|
}
|
|
48
61
|
|
|
@@ -52,22 +65,19 @@ function loadSettings(path: string): ExtensionSettings {
|
|
|
52
65
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return defaultSettings();
|
|
53
66
|
const record = value as Record<string, unknown>;
|
|
54
67
|
return {
|
|
55
|
-
enforcementEnabled: record
|
|
56
|
-
footerEnabled: record
|
|
57
|
-
codexRecoveryEnabled: record
|
|
58
|
-
usageTokenBudgets: parseUsageTokenBudgets(record
|
|
68
|
+
enforcementEnabled: record.enforcementEnabled !== false,
|
|
69
|
+
footerEnabled: record.footerEnabled !== false,
|
|
70
|
+
codexRecoveryEnabled: record.codexRecoveryEnabled === true,
|
|
71
|
+
usageTokenBudgets: parseUsageTokenBudgets(record.usageTokenBudgets),
|
|
59
72
|
};
|
|
60
73
|
} catch {
|
|
61
74
|
return defaultSettings();
|
|
62
75
|
}
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
function persistSettings(path: string, settings: ExtensionSettings): void {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
69
|
-
chmodSync(temporary, 0o600);
|
|
70
|
-
renameSync(temporary, path);
|
|
78
|
+
async function persistSettings(path: string, settings: ExtensionSettings): Promise<void> {
|
|
79
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
80
|
+
await atomicJson.write(path, settings, { mode: 0o600, pretty: true, trailingNewline: true });
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
export function persistentEnforcementControl(env: Record<string, string | undefined> = process.env): PersistentExtensionControl {
|
|
@@ -75,28 +85,29 @@ export function persistentEnforcementControl(env: Record<string, string | undefi
|
|
|
75
85
|
const settings = loadSettings(path);
|
|
76
86
|
return {
|
|
77
87
|
isEnabled: () => settings.enforcementEnabled,
|
|
78
|
-
setEnabled(value: boolean): void {
|
|
88
|
+
async setEnabled(value: boolean): Promise<void> {
|
|
79
89
|
settings.enforcementEnabled = value;
|
|
80
|
-
persistSettings(path, settings);
|
|
90
|
+
await persistSettings(path, settings);
|
|
81
91
|
},
|
|
82
92
|
isFooterEnabled: () => settings.footerEnabled,
|
|
83
|
-
setFooterEnabled(value: boolean): void {
|
|
93
|
+
async setFooterEnabled(value: boolean): Promise<void> {
|
|
84
94
|
settings.footerEnabled = value;
|
|
85
|
-
persistSettings(path, settings);
|
|
95
|
+
await persistSettings(path, settings);
|
|
86
96
|
},
|
|
87
97
|
isCodexRecoveryEnabled: () => settings.codexRecoveryEnabled,
|
|
88
|
-
setCodexRecoveryEnabled(value: boolean): void {
|
|
98
|
+
async setCodexRecoveryEnabled(value: boolean): Promise<void> {
|
|
89
99
|
settings.codexRecoveryEnabled = value;
|
|
90
|
-
persistSettings(path, settings);
|
|
100
|
+
await persistSettings(path, settings);
|
|
91
101
|
},
|
|
92
102
|
getUsageTokenBudget(period): number | undefined {
|
|
93
103
|
return settings.usageTokenBudgets[period];
|
|
94
104
|
},
|
|
95
|
-
setUsageTokenBudget(period, tokens): void {
|
|
96
|
-
if (tokens !== undefined && (!Number.isFinite(tokens) || tokens <= 0))
|
|
105
|
+
async setUsageTokenBudget(period, tokens): Promise<void> {
|
|
106
|
+
if (tokens !== undefined && (!Number.isFinite(tokens) || tokens <= 0))
|
|
107
|
+
throw new Error("usage token budget must be a positive finite number");
|
|
97
108
|
if (tokens === undefined) delete settings.usageTokenBudgets[period];
|
|
98
109
|
else settings.usageTokenBudgets[period] = tokens;
|
|
99
|
-
persistSettings(path, settings);
|
|
110
|
+
await persistSettings(path, settings);
|
|
100
111
|
},
|
|
101
112
|
};
|
|
102
113
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-jittor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension for Jittor
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Pi extension for Jittor token and context observability with optimization controls backed by the @danypops/jittor daemon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package", "llm-router", "token-budget"],
|
|
7
7
|
"scripts": {
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
"extensions": ["extension/src/index.ts"]
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@danypops/vehicle-client": "^0.
|
|
16
|
-
"@danypops/
|
|
17
|
-
"
|
|
15
|
+
"@danypops/vehicle-client": "^0.5.1",
|
|
16
|
+
"@danypops/vehicle-core": "^0.12.1",
|
|
17
|
+
"@danypops/vehicle-server": "^0.17.0",
|
|
18
|
+
"@danypops/jittor": "workspace:^",
|
|
19
|
+
"malevich-tui-components": "^0.16.1"
|
|
18
20
|
},
|
|
19
21
|
"peerDependencies": {
|
|
20
22
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -22,6 +24,10 @@
|
|
|
22
24
|
"typebox": "*"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
27
|
+
"@danypops/pi-extension-harness": "^0.6.2",
|
|
28
|
+
"@danypops/pi-process-harness": "^0.1.2",
|
|
29
|
+
"@danypops/pi-tui-harness": "^0.0.2",
|
|
30
|
+
"@earendil-works/pi-ai": "*",
|
|
25
31
|
"bun-types": "latest"
|
|
26
32
|
},
|
|
27
33
|
"repository": {
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
4
|
-
BENCHMARK_TUI_MAX_CANDIDATES,
|
|
5
|
-
BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE,
|
|
6
|
-
MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
|
|
7
|
-
MODEL_RANKING_DEFAULT_COST_WEIGHT,
|
|
8
|
-
MODEL_RANKING_DEFAULT_LATENCY_WEIGHT,
|
|
9
|
-
MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
|
|
10
|
-
MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
|
|
11
|
-
type ModelCandidate,
|
|
12
|
-
type ModelRankingResult,
|
|
13
|
-
type ModelTaskDomain,
|
|
14
|
-
type ModelTaskType,
|
|
15
|
-
type RankedModel,
|
|
16
|
-
type UtilityComponentName,
|
|
17
|
-
} from "@danypops/jittor";
|
|
18
|
-
import { sessionSecretField } from "./session-identity.ts";
|
|
19
|
-
|
|
20
|
-
export interface BenchmarkPanelClient {
|
|
21
|
-
call(operation: string, input: unknown): Promise<any>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface BenchmarkTheme {
|
|
25
|
-
fg(color: string, text: string): string;
|
|
26
|
-
bold(text: string): string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type BenchmarkPanelAction = "refresh" | "close";
|
|
30
|
-
|
|
31
|
-
const COMPONENT_LABELS: Record<UtilityComponentName, string> = { quality: "Q", cost: "$", latency: "L", context: "C", reliability: "R" };
|
|
32
|
-
|
|
33
|
-
function componentText(item: RankedModel): string {
|
|
34
|
-
return item.components.map((component) => `${COMPONENT_LABELS[component.name]} ${component.score === null ? "?" : component.score.toFixed(3)}`).join(" · ");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function candidateLines(item: RankedModel, index: number, currentIdentity: string): string[] {
|
|
38
|
-
const current = item.identity.startsWith(`${currentIdentity}:`);
|
|
39
|
-
const localSamples = item.components.find((component) => component.name === "reliability")?.evidenceCount ?? 0;
|
|
40
|
-
const provenance = item.provenance.slice(0, BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE).map((source) => `${source.sourceId}@${source.revision} ${source.freshness}`).join(" · ");
|
|
41
|
-
return [
|
|
42
|
-
` ${index + 1}. ${item.identity}${index === 0 ? " recommended" : ""}${current ? " current" : ""}`,
|
|
43
|
-
` utility ${item.utility === null ? "?" : item.utility.toFixed(3)} · confidence ${(item.confidence * 100).toFixed(0)}% · ${componentText(item)}`,
|
|
44
|
-
` local n=${localSamples}${provenance ? ` · ${provenance}` : " · no external provenance"}`,
|
|
45
|
-
];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function renderBenchmarkView(result: ModelRankingResult, currentIdentity: string, width: number, theme: BenchmarkTheme): string[] {
|
|
49
|
-
const safeWidth = Math.max(1, width);
|
|
50
|
-
const shown = result.ranked.slice(0, BENCHMARK_TUI_MAX_CANDIDATES);
|
|
51
|
-
const currentIndex = result.ranked.findIndex((item) => item.identity.startsWith(`${currentIdentity}:`));
|
|
52
|
-
const recommended = result.ranked[0];
|
|
53
|
-
const reason = recommended && currentIndex > 0
|
|
54
|
-
? `Recommendation differs from current: ${recommended.identity} ranks #1; current ranks #${currentIndex + 1}.`
|
|
55
|
-
: recommended && currentIndex === 0 ? "Current model is the top recommendation." : "Current model is outside the ranked candidates.";
|
|
56
|
-
const lines = [
|
|
57
|
-
theme.fg("borderMuted", "─".repeat(safeWidth)),
|
|
58
|
-
theme.bold("Jittor Benchmark Recommendations"),
|
|
59
|
-
result.scopeAuthority === "exact-session" ? "Scope: exact session" : "Scope: available models · ADVISORY (exact session scope unavailable)",
|
|
60
|
-
`Domain: ${result.domain} · Type: ${result.type} · evidence ${result.completeness}`,
|
|
61
|
-
reason,
|
|
62
|
-
...shown.flatMap((item, index) => candidateLines(item, index, currentIdentity)),
|
|
63
|
-
...(result.ranked.length > shown.length ? [` … ${result.ranked.length - shown.length} more candidates omitted`] : []),
|
|
64
|
-
...(result.scopeWarning ? [result.scopeWarning] : []),
|
|
65
|
-
theme.fg("dim", "r refresh · Esc close"),
|
|
66
|
-
theme.fg("borderMuted", "─".repeat(safeWidth)),
|
|
67
|
-
];
|
|
68
|
-
return lines.map((line) => truncateToWidth(line, safeWidth, "…"));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function showBenchmarkPanel(
|
|
72
|
-
ctx: ExtensionCommandContext,
|
|
73
|
-
client: BenchmarkPanelClient,
|
|
74
|
-
candidates: ModelCandidate[],
|
|
75
|
-
currentIdentity: string,
|
|
76
|
-
domain: ModelTaskDomain,
|
|
77
|
-
type: ModelTaskType,
|
|
78
|
-
): Promise<void> {
|
|
79
|
-
for (;;) {
|
|
80
|
-
const session_id = ctx.sessionManager.getSessionId();
|
|
81
|
-
const result = await client.call("models.rank", {
|
|
82
|
-
candidates,
|
|
83
|
-
session_id,
|
|
84
|
-
...sessionSecretField(session_id),
|
|
85
|
-
scopeAuthority: "available-models",
|
|
86
|
-
domain,
|
|
87
|
-
type,
|
|
88
|
-
budgetPressure: 0,
|
|
89
|
-
weights: {
|
|
90
|
-
quality: MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
|
|
91
|
-
cost: MODEL_RANKING_DEFAULT_COST_WEIGHT,
|
|
92
|
-
latency: MODEL_RANKING_DEFAULT_LATENCY_WEIGHT,
|
|
93
|
-
context: MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
|
|
94
|
-
reliability: MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
|
|
95
|
-
},
|
|
96
|
-
sourceIds: ["openrouter-models", "lmarena-hf", "artificial-analysis-direct", "openrouter-design-arena"],
|
|
97
|
-
}) as ModelRankingResult;
|
|
98
|
-
if (ctx.mode !== "tui") {
|
|
99
|
-
ctx.ui.notify(renderBenchmarkView(result, currentIdentity, 100, { fg: (_color, text) => text, bold: (text) => text }).join("\n"), "info");
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const action = await ctx.ui.custom<BenchmarkPanelAction>((_tui, theme, _keybindings, done) => ({
|
|
103
|
-
invalidate() {},
|
|
104
|
-
render(width: number): string[] { return renderBenchmarkView(result, currentIdentity, width, theme); },
|
|
105
|
-
handleInput(data: string): void {
|
|
106
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) done("close");
|
|
107
|
-
else if (data === "r") done("refresh");
|
|
108
|
-
},
|
|
109
|
-
}));
|
|
110
|
-
if (!action || action === "close") return;
|
|
111
|
-
await client.call("benchmark.refresh", { force: true });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { buildContextRows, type ContextSegment as MalevichContextSegment } from "malevich-tui-components";
|
|
2
|
-
import type { ContextBreakdown } from "./context-breakdown.ts";
|
|
3
|
-
import type { ContextSegment } from "@danypops/jittor";
|
|
4
|
-
|
|
5
|
-
/** Bounds how many items render per segment in the plain-text fallback -- a notify-mode report is a scan-at-a-glance summary, not a full dump (the interactive TUI view has no such cap, since it scrolls). */
|
|
6
|
-
const MAX_ITEMS_PER_SEGMENT_LINE = 5;
|
|
7
|
-
|
|
8
|
-
function formatTokens(tokens: number): string {
|
|
9
|
-
return tokens >= 1_000 ? `${(tokens / 1_000).toFixed(1)}k` : String(tokens);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function percentOf(part: number, whole: number): string {
|
|
13
|
-
return whole > 0 ? `${((part / whole) * 100).toFixed(1)}%` : "—";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Malevich's row builder is confidence-unaware (it's a generic segment/item shape); folding the tier into the label is how it survives into the rendered row text, e.g. "Active Rules [exact-cooperative]". */
|
|
17
|
-
function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
|
|
18
|
-
const items = [...(segment.items ?? [])].sort((left, right) => right.estimatedTokens - left.estimatedTokens).slice(0, MAX_ITEMS_PER_SEGMENT_LINE);
|
|
19
|
-
return { key: segment.key, label: `${segment.label} [${segment.confidence}]`, estimatedTokens: segment.estimatedTokens, items, unknown: segment.unknown };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Plain-text Context Hub report (non-TUI fallback): real usage against the model's effective
|
|
24
|
-
* (reserve-adjusted) budget first -- matching Papyrus's own real-vs-estimate honesty accounting
|
|
25
|
-
* -- an explicit overshoot warning when the known segments' estimates exceed the real total, then
|
|
26
|
-
* every segment heaviest-first. Malevich's buildContextRows renders segments in the order given,
|
|
27
|
-
* so sorting by weight is this function's own policy, not Malevich's.
|
|
28
|
-
*/
|
|
29
|
-
export function buildContextReport(breakdown: ContextBreakdown): string {
|
|
30
|
-
const lines: string[] = [];
|
|
31
|
-
if (breakdown.totalTokens !== null && breakdown.effectiveBudget !== null) {
|
|
32
|
-
lines.push(`Real usage: ${formatTokens(breakdown.totalTokens)} / ${formatTokens(breakdown.effectiveBudget)} tokens (${percentOf(breakdown.totalTokens, breakdown.effectiveBudget)} of usable budget)`);
|
|
33
|
-
} else if (breakdown.totalTokens !== null) {
|
|
34
|
-
lines.push(`Real usage: ${formatTokens(breakdown.totalTokens)} tokens (model context window unknown)`);
|
|
35
|
-
} else {
|
|
36
|
-
lines.push("Real usage: not yet reported -- sizes below are estimates only");
|
|
37
|
-
}
|
|
38
|
-
if (breakdown.overshootTokens > 0) lines.push(`Estimates exceed real total by ~${breakdown.overshootTokens} tok -- sizes below are approximate, not exact`);
|
|
39
|
-
|
|
40
|
-
const sorted = [...breakdown.segments].sort((left, right) => right.estimatedTokens - left.estimatedTokens).map(withConfidenceLabel);
|
|
41
|
-
const rows = buildContextRows(sorted, breakdown.totalTokens ?? undefined);
|
|
42
|
-
if (rows.length === 0) {
|
|
43
|
-
lines.push("", "(no segments observed yet)");
|
|
44
|
-
return lines.join("\n");
|
|
45
|
-
}
|
|
46
|
-
lines.push("");
|
|
47
|
-
for (const row of rows) lines.push(row.isHeader ? row.text : `${" ".repeat(row.depth)}${row.text}`);
|
|
48
|
-
return lines.join("\n");
|
|
49
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type { ExtensionCommandContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import { buildContextRows, renderContextRowLines, renderContextUsageBar, type ContextBarTheme, type ContextRow, type ContextRowsTheme, type ContextSegment as MalevichContextSegment } from "malevich-tui-components";
|
|
4
|
-
import type { ContextSegment } from "@danypops/jittor";
|
|
5
|
-
import type { ContextBreakdown } from "./context-breakdown.ts";
|
|
6
|
-
import { buildContextReport } from "./context-report.ts";
|
|
7
|
-
|
|
8
|
-
const VISIBLE_ROWS = 24;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A dynamically-contributed segment set (any string key from any extension, not a fixed enum)
|
|
12
|
-
* can't use a hardcoded per-key color map the way Papyrus's own ContextViewport did for its
|
|
13
|
-
* fixed seven segments -- this cycles a small categorical palette keyed by a stable hash of the
|
|
14
|
-
* segment key, so the same key always renders the same color within one process without needing
|
|
15
|
-
* every possible contributor's key to be known in advance.
|
|
16
|
-
*/
|
|
17
|
-
const PALETTE: ThemeColor[] = ["accent", "success", "syntaxFunction", "warning", "syntaxKeyword", "syntaxType", "muted"];
|
|
18
|
-
|
|
19
|
-
function paletteColor(key: string): ThemeColor {
|
|
20
|
-
let hash = 0;
|
|
21
|
-
for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
|
|
22
|
-
return PALETTE[hash % PALETTE.length]!;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function formatTokenCount(tokens: number): string {
|
|
26
|
-
return tokens >= 1_000 ? `${(tokens / 1_000).toFixed(1)}k` : String(tokens);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function percentOf(part: number, whole: number): string {
|
|
30
|
-
return whole > 0 ? `${((part / whole) * 100).toFixed(1)}%` : "—";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Folds each segment's confidence tier into its label so it survives Malevich's confidence-unaware row builder. */
|
|
34
|
-
function withConfidenceLabel(segment: ContextSegment): MalevichContextSegment {
|
|
35
|
-
return { key: segment.key, label: `${segment.label} [${segment.confidence}]`, estimatedTokens: segment.estimatedTokens, items: segment.items, unknown: segment.unknown };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
class ContextViewport {
|
|
39
|
-
private offsetY = 0;
|
|
40
|
-
private readonly rows: ContextRow[];
|
|
41
|
-
private readonly segments: readonly MalevichContextSegment[];
|
|
42
|
-
|
|
43
|
-
constructor(
|
|
44
|
-
private readonly tui: TUI,
|
|
45
|
-
private readonly theme: Theme,
|
|
46
|
-
private readonly breakdown: ContextBreakdown,
|
|
47
|
-
private readonly close: () => void,
|
|
48
|
-
) {
|
|
49
|
-
// Heaviest-first: Malevich renders segments in the order given, so sorting by weight for a
|
|
50
|
-
// merged multi-producer view is this viewport's own policy, matching context-report.ts.
|
|
51
|
-
this.segments = [...breakdown.segments].sort((left, right) => right.estimatedTokens - left.estimatedTokens).map(withConfidenceLabel);
|
|
52
|
-
this.rows = buildContextRows(this.segments, breakdown.totalTokens ?? undefined);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
invalidate(): void {}
|
|
56
|
-
|
|
57
|
-
render(width: number): string[] {
|
|
58
|
-
const theme = this.theme;
|
|
59
|
-
const contentWidth = Math.max(1, width);
|
|
60
|
-
const border = theme.fg("borderMuted", "─".repeat(contentWidth));
|
|
61
|
-
const lines: string[] = [border, truncateToWidth(theme.fg("accent", theme.bold("Context")), contentWidth, "")];
|
|
62
|
-
|
|
63
|
-
const { totalTokens, effectiveBudget } = this.breakdown;
|
|
64
|
-
if (totalTokens !== null && effectiveBudget !== null) {
|
|
65
|
-
lines.push(truncateToWidth(`${formatTokenCount(totalTokens)} / ${formatTokenCount(effectiveBudget)} tokens (${percentOf(totalTokens, effectiveBudget)} of usable budget)`, contentWidth, ""));
|
|
66
|
-
} else if (totalTokens !== null) {
|
|
67
|
-
lines.push(truncateToWidth(`${formatTokenCount(totalTokens)} tokens (model context window unknown)`, contentWidth, ""));
|
|
68
|
-
} else {
|
|
69
|
-
lines.push(theme.fg("dim", "No real usage reported yet — sizes below are estimates only"));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const colorFor = (key: string) => (s: string) => theme.fg(paletteColor(key), s);
|
|
73
|
-
const barTheme: ContextBarTheme = { colorFor, empty: (s) => theme.fg("dim", s) };
|
|
74
|
-
lines.push(renderContextUsageBar(barTheme, this.segments, contentWidth, effectiveBudget ?? undefined, totalTokens ?? undefined));
|
|
75
|
-
if (this.breakdown.overshootTokens > 0) {
|
|
76
|
-
lines.push(truncateToWidth(theme.fg("warning", `Estimates exceed real total by ~${this.breakdown.overshootTokens} tok — sizes below are approximate, not exact`), contentWidth, ""));
|
|
77
|
-
}
|
|
78
|
-
lines.push("");
|
|
79
|
-
|
|
80
|
-
const rowsTheme: ContextRowsTheme = { colorFor, header: (s) => theme.bold(s) };
|
|
81
|
-
const visible = this.rows.slice(this.offsetY, this.offsetY + VISIBLE_ROWS);
|
|
82
|
-
lines.push(...renderContextRowLines(visible, contentWidth, rowsTheme));
|
|
83
|
-
if (this.rows.length === 0) lines.push(theme.fg("dim", " (nothing observed yet)"));
|
|
84
|
-
else lines.push(theme.fg("muted", ` ${Math.min(this.offsetY + VISIBLE_ROWS, this.rows.length)}/${this.rows.length}`));
|
|
85
|
-
|
|
86
|
-
lines.push("");
|
|
87
|
-
lines.push(theme.fg("dim", "↑↓ scroll · esc close"));
|
|
88
|
-
lines.push(border);
|
|
89
|
-
return lines;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
handleInput(data: string): void {
|
|
93
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.close(); return; }
|
|
94
|
-
if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
|
|
95
|
-
else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.rows.length - VISIBLE_ROWS), this.offsetY + 1);
|
|
96
|
-
else return;
|
|
97
|
-
this.tui.requestRender();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Interactive scrollable Context Hub view in TUI mode; the same plain-text report as /context's non-interactive path otherwise. */
|
|
102
|
-
export async function showContextView(ctx: ExtensionCommandContext, breakdown: ContextBreakdown): Promise<void> {
|
|
103
|
-
if (ctx.mode !== "tui") {
|
|
104
|
-
ctx.ui.notify(buildContextReport(breakdown), "info");
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => new ContextViewport(tui, theme, breakdown, done));
|
|
108
|
-
}
|