@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.
Files changed (25) hide show
  1. package/README.md +19 -3
  2. package/docs/USAGE_PRIOR_ART.md +1 -1
  3. package/extension/src/index.ts +379 -118
  4. package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +251 -47
  5. package/extension/src/observability/context-growth.ts +26 -0
  6. package/extension/src/{capabilities → observability}/context-hub.ts +1 -5
  7. package/extension/src/observability/context-report.ts +92 -0
  8. package/extension/src/observability/context-view.ts +264 -0
  9. package/extension/src/{footer.ts → observability/footer.ts} +63 -26
  10. package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +14 -10
  11. package/extension/src/observability/provider-context-snapshot.ts +246 -0
  12. package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +21 -7
  13. package/extension/src/{tui.ts → observability/status.ts} +202 -72
  14. package/extension/src/observability/usage.ts +314 -0
  15. package/extension/src/optimization/model-selection-panel.ts +160 -0
  16. package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +41 -25
  17. package/extension/src/service-client.ts +49 -2
  18. package/extension/src/settings-tui.ts +73 -33
  19. package/extension/src/settings.ts +40 -29
  20. package/package.json +11 -5
  21. package/extension/src/benchmark-tui.ts +0 -113
  22. package/extension/src/context-report.ts +0 -49
  23. package/extension/src/context-view.ts +0 -108
  24. package/extension/src/usage.ts +0 -324
  25. /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 { USAGE_PERIODS, type UsagePeriod } from "@danypops/jittor";
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
- const safeWidth = Math.max(20, width);
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
- ctx.ui.notify(renderSettingsView(settingsSnapshot(enforcement, recovery, budgets), -1, 100, plainTheme()).slice(0, -2).join("\n"), "info");
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
- let selected = 0;
152
+ const panel = createSettingsPanel(snapshot, theme, done);
123
153
  return {
124
- invalidate() {},
125
- render(width: number): string[] { return renderSettingsView(snapshot, selected, width, theme); },
126
- handleInput(data: string): void {
127
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || data === "q") done({ kind: "close" });
128
- else if (matchesKey(data, "up")) { selected = (selected - 1 + SETTINGS_KEYS.length) % SETTINGS_KEYS.length; tui.requestRender(); }
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 (await ctx.ui.confirm("Disable routing enforcement?", "Jittor will remain monitor-only and will no longer block unsafe provider requests.")) await effects.setEnforcement(false);
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 (await ctx.ui.confirm("Enable Codex recovery?", "Jittor may start bounded hidden retries only after transient Codex failures fully settle.")) await effects.setRecovery(true);
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 { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
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(USAGE_PERIODS.flatMap(({ id }) => {
39
- const tokens = record[id];
40
- return typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0 ? [[id, tokens]] : [];
41
- })) as Partial<Record<UsagePeriod, number>>;
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["XDG_CONFIG_HOME"] ?? join(env["HOME"] ?? ".", ".config");
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["enforcementEnabled"] !== false,
56
- footerEnabled: record["footerEnabled"] !== false,
57
- codexRecoveryEnabled: record["codexRecoveryEnabled"] === true,
58
- usageTokenBudgets: parseUsageTokenBudgets(record["usageTokenBudgets"]),
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
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
67
- const temporary = `${path}.${process.pid}.tmp`;
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)) throw new Error("usage token budget must be a positive finite number");
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.2.0",
4
- "description": "Pi extension for Jittor: native routing enforcement, footer, settings, usage graphs, and benchmark panels backed by the @danypops/jittor daemon",
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.1.1",
16
- "@danypops/jittor": "^0.14.0",
17
- "malevich-tui-components": "^0.6.0"
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
- }