@danypops/pi-jittor 0.3.1 → 0.5.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/extension/src/index.ts +111 -30
- package/extension/src/jittor-command.ts +110 -0
- package/extension/src/jittor-shell.ts +338 -0
- package/extension/src/observability/cache-economics-view.ts +179 -0
- package/extension/src/observability/status.ts +116 -65
- package/extension/src/optimization/model-selection-panel.ts +61 -32
- package/extension/src/service-client.ts +1 -0
- package/extension/src/settings-tui.ts +112 -34
- package/extension/src/tui-prompts.ts +110 -0
- package/package.json +2 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidates /jittor's four independent overlays (Settings, Status, Benchmarks, Cache) into one
|
|
3
|
+
* Envelope-equivalent shell: a single outer BorderedSelectPanel owns the one shared border/title,
|
|
4
|
+
* a TabbedContainer inside it owns the persistent tab bar and hands render/input straight to
|
|
5
|
+
* whichever tab is active -- the composable primitive that replaces the old ad hoc
|
|
6
|
+
* close-one-overlay-open-a-different-one pattern (see malevich-tui-components' own
|
|
7
|
+
* TabbedContainer doc comment). Modeled on DanyPops/vehicle's own /safety command
|
|
8
|
+
* (vehicle-safety-command.ts), the real precedent for Envelope/TabbedContainer composition in
|
|
9
|
+
* this ecosystem.
|
|
10
|
+
*
|
|
11
|
+
* Each tab's real interactive content and side-effect handling stays owned by its existing
|
|
12
|
+
* module (settings-tui.ts, observability/status.ts, optimization/model-selection-panel.ts,
|
|
13
|
+
* observability/cache-economics-view.ts) via their exported `create*Panel`/`run*Action`/`fetch*`
|
|
14
|
+
* functions -- this module only composes them, so the standalone single-panel entry points and
|
|
15
|
+
* the unified shell can never drift apart on what a keypress actually does.
|
|
16
|
+
*
|
|
17
|
+
* Per-tab data is fetched lazily: only the initially requested tab (plus Settings, which reads
|
|
18
|
+
* already-in-memory persisted state and costs no daemon round trip at all) is fetched before the
|
|
19
|
+
* shell first opens. Switching to a tab that has never been visited fetches it once, on first
|
|
20
|
+
* visit, not before.
|
|
21
|
+
*/
|
|
22
|
+
import type { ModelCandidate, ModelRankingResult, ModelTaskDomain, ModelTaskType, RouterStatus } from "@danypops/jittor";
|
|
23
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
25
|
+
import { BorderedSelectPanel, type MnemonicContext, type TabBarTheme, TabbedContainer, type TextMeasure } from "malevich-tui-components";
|
|
26
|
+
import {
|
|
27
|
+
type CacheEconomicsPanelAction,
|
|
28
|
+
type CacheEconomicsPanelClient,
|
|
29
|
+
createCacheEconomicsPanel,
|
|
30
|
+
fetchCacheEconomicsSummary,
|
|
31
|
+
showCacheEconomicsPanel,
|
|
32
|
+
} from "./observability/cache-economics-view.ts";
|
|
33
|
+
import {
|
|
34
|
+
createStatusPanel,
|
|
35
|
+
fetchStatusSnapshot,
|
|
36
|
+
type JittorPanelClient,
|
|
37
|
+
type PanelAction,
|
|
38
|
+
runStatusAction,
|
|
39
|
+
type StatusPanelSnapshot,
|
|
40
|
+
showJittorPanel,
|
|
41
|
+
} from "./observability/status.ts";
|
|
42
|
+
import {
|
|
43
|
+
type BenchmarkPanelAction,
|
|
44
|
+
type BenchmarkPanelClient,
|
|
45
|
+
createBenchmarkPanel,
|
|
46
|
+
fetchBenchmarkRanking,
|
|
47
|
+
showBenchmarkPanel,
|
|
48
|
+
} from "./optimization/model-selection-panel.ts";
|
|
49
|
+
import type { CodexRecoveryControl, EnforcementControl, UsageBudgetControl } from "./settings.ts";
|
|
50
|
+
import {
|
|
51
|
+
createSettingsPanel,
|
|
52
|
+
runSettingsAction,
|
|
53
|
+
type SettingsAction,
|
|
54
|
+
type SettingsEffects,
|
|
55
|
+
type SettingsSnapshot,
|
|
56
|
+
settingsSnapshot,
|
|
57
|
+
showSettingsPanel,
|
|
58
|
+
} from "./settings-tui.ts";
|
|
59
|
+
|
|
60
|
+
export type JittorTabKey = "settings" | "status" | "benchmarks" | "cache";
|
|
61
|
+
|
|
62
|
+
export interface JittorShellDeps {
|
|
63
|
+
settings: {
|
|
64
|
+
enforcement: EnforcementControl;
|
|
65
|
+
recovery: CodexRecoveryControl;
|
|
66
|
+
budgets: UsageBudgetControl;
|
|
67
|
+
effects: SettingsEffects;
|
|
68
|
+
};
|
|
69
|
+
status: { client: JittorPanelClient };
|
|
70
|
+
benchmarks: {
|
|
71
|
+
client: BenchmarkPanelClient;
|
|
72
|
+
candidates: ModelCandidate[];
|
|
73
|
+
currentIdentity: string;
|
|
74
|
+
domain: ModelTaskDomain;
|
|
75
|
+
type: ModelTaskType;
|
|
76
|
+
};
|
|
77
|
+
cache: { client: CacheEconomicsPanelClient; windowMs: number; now?: () => number };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ShellTheme {
|
|
81
|
+
fg(color: string, text: string): string;
|
|
82
|
+
bold(text: string): string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hostTextMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A real tree describing every key genuinely reachable at once inside the shell -- the shell's
|
|
89
|
+
* own tab-cycling/close bindings at the root, plus each tab's own bindings as a sibling leaf
|
|
90
|
+
* (siblings are never simultaneously active, so two tabs may freely reuse the same letter for
|
|
91
|
+
* different things; only a leaf's own root-to-tab path is checked). Kept in sync by hand with
|
|
92
|
+
* what createStatusPanel/createBenchmarkPanel/createCacheEconomicsPanel/createSettingsPanel and
|
|
93
|
+
* this module's own outer handleInput actually wire -- run through assertNoMnemonicConflicts as a
|
|
94
|
+
* standing test so a newly added keybinding that collides fails loudly, not just live.
|
|
95
|
+
*/
|
|
96
|
+
export function jittorShellMnemonicTree(): MnemonicContext {
|
|
97
|
+
return {
|
|
98
|
+
name: "jittor-shell",
|
|
99
|
+
bindings: [
|
|
100
|
+
{ key: "escape", description: "close shell" },
|
|
101
|
+
{ key: "ctrl+c", description: "close shell" },
|
|
102
|
+
{ key: "tab", description: "next tab" },
|
|
103
|
+
{ key: "shift+tab", description: "previous tab" },
|
|
104
|
+
{ key: "left", description: "previous tab" },
|
|
105
|
+
{ key: "right", description: "next tab" },
|
|
106
|
+
],
|
|
107
|
+
children: [
|
|
108
|
+
{
|
|
109
|
+
name: "settings",
|
|
110
|
+
bindings: [
|
|
111
|
+
{ key: "up", description: "menu up" },
|
|
112
|
+
{ key: "down", description: "menu down" },
|
|
113
|
+
{ key: "enter", description: "menu activate" },
|
|
114
|
+
{ key: "space", description: "menu activate" },
|
|
115
|
+
{ key: "escape", description: "close shell" },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "status",
|
|
120
|
+
bindings: [
|
|
121
|
+
{ key: "r", description: "refresh status" },
|
|
122
|
+
{ key: "p", description: "pause/resume" },
|
|
123
|
+
{ key: "o", description: "override route" },
|
|
124
|
+
{ key: "c", description: "clear override" },
|
|
125
|
+
{ key: "escape", description: "close shell" },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "benchmarks",
|
|
130
|
+
bindings: [
|
|
131
|
+
{ key: "r", description: "refresh benchmarks" },
|
|
132
|
+
{ key: "escape", description: "close shell" },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "cache",
|
|
137
|
+
bindings: [
|
|
138
|
+
{ key: "r", description: "refresh cache" },
|
|
139
|
+
{ key: "escape", description: "close shell" },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface ShellState {
|
|
147
|
+
settings?: SettingsSnapshot;
|
|
148
|
+
status?: StatusPanelSnapshot;
|
|
149
|
+
benchmarks?: ModelRankingResult;
|
|
150
|
+
cache?: Awaited<ReturnType<typeof fetchCacheEconomicsSummary>>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function ensureLoaded(
|
|
154
|
+
activeKey: JittorTabKey,
|
|
155
|
+
state: ShellState,
|
|
156
|
+
deps: JittorShellDeps,
|
|
157
|
+
ctx: ExtensionCommandContext,
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
// Settings reads already-in-memory persisted state -- no daemon round trip, so eagerly keeping
|
|
160
|
+
// it fresh costs nothing and never violates "no network call for a tab never visited".
|
|
161
|
+
state.settings = settingsSnapshot(deps.settings.enforcement, deps.settings.recovery, deps.settings.budgets);
|
|
162
|
+
if (activeKey === "status" && state.status === undefined) {
|
|
163
|
+
state.status = await fetchStatusSnapshot(deps.status.client, ctx.sessionManager.getSessionId());
|
|
164
|
+
}
|
|
165
|
+
if (activeKey === "benchmarks" && state.benchmarks === undefined) {
|
|
166
|
+
state.benchmarks = await fetchBenchmarkRanking(
|
|
167
|
+
ctx,
|
|
168
|
+
deps.benchmarks.client,
|
|
169
|
+
deps.benchmarks.candidates,
|
|
170
|
+
deps.benchmarks.domain,
|
|
171
|
+
deps.benchmarks.type,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (activeKey === "cache" && state.cache === undefined) {
|
|
175
|
+
state.cache = await fetchCacheEconomicsSummary(deps.cache.client, deps.cache.windowMs, deps.cache.now);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function loadingContent(label: string) {
|
|
180
|
+
return {
|
|
181
|
+
invalidate: () => {},
|
|
182
|
+
render: (width: number): string[] => [truncateToWidth(`Loading ${label}\u2026`, width, "\u2026")],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type ShellOutcome =
|
|
187
|
+
| { kind: "close" }
|
|
188
|
+
| { kind: "tab-changed" }
|
|
189
|
+
| { kind: "settings"; action: SettingsAction }
|
|
190
|
+
| { kind: "status"; action: PanelAction }
|
|
191
|
+
| { kind: "benchmarks"; action: BenchmarkPanelAction }
|
|
192
|
+
| { kind: "cache"; action: CacheEconomicsPanelAction };
|
|
193
|
+
|
|
194
|
+
function tabBarTheme(theme: ShellTheme): TabBarTheme {
|
|
195
|
+
return {
|
|
196
|
+
tab: (text) => theme.fg("dim", text),
|
|
197
|
+
activeTab: (text) => theme.bold(theme.fg("accent", text)),
|
|
198
|
+
mnemonic: (text) => theme.bold(text),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Falls back to each subcommand's own already-tested non-TUI notify (outside TUI mode, there is
|
|
204
|
+
* no tab bar to consolidate -- one plain-text notify per subcommand is unchanged and correct).
|
|
205
|
+
*/
|
|
206
|
+
async function showNonTuiFallback(ctx: ExtensionCommandContext, deps: JittorShellDeps, initialTab: JittorTabKey): Promise<void> {
|
|
207
|
+
if (initialTab === "settings")
|
|
208
|
+
return showSettingsPanel(ctx, deps.settings.enforcement, deps.settings.recovery, deps.settings.budgets, deps.settings.effects);
|
|
209
|
+
if (initialTab === "status") return showJittorPanel(ctx, deps.status.client);
|
|
210
|
+
if (initialTab === "benchmarks")
|
|
211
|
+
return showBenchmarkPanel(
|
|
212
|
+
ctx,
|
|
213
|
+
deps.benchmarks.client,
|
|
214
|
+
deps.benchmarks.candidates,
|
|
215
|
+
deps.benchmarks.currentIdentity,
|
|
216
|
+
deps.benchmarks.domain,
|
|
217
|
+
deps.benchmarks.type,
|
|
218
|
+
);
|
|
219
|
+
return showCacheEconomicsPanel(ctx, deps.cache.client, deps.cache.windowMs, deps.cache.now);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function showJittorShell(
|
|
223
|
+
ctx: ExtensionCommandContext,
|
|
224
|
+
deps: JittorShellDeps,
|
|
225
|
+
initialTab: JittorTabKey = "settings",
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
if (ctx.mode !== "tui") return showNonTuiFallback(ctx, deps, initialTab);
|
|
228
|
+
|
|
229
|
+
let activeKey: JittorTabKey = initialTab;
|
|
230
|
+
const state: ShellState = {};
|
|
231
|
+
|
|
232
|
+
for (;;) {
|
|
233
|
+
await ensureLoaded(activeKey, state, deps, ctx);
|
|
234
|
+
const outcome = await ctx.ui.custom<ShellOutcome>((tui, theme, _keybindings, done) => {
|
|
235
|
+
const tabs = [
|
|
236
|
+
{
|
|
237
|
+
key: "settings" as const,
|
|
238
|
+
label: "Settings",
|
|
239
|
+
content: createSettingsPanel(state.settings!, theme, (action) => done({ kind: "settings", action }), 0, false),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
key: "status" as const,
|
|
243
|
+
label: "Status",
|
|
244
|
+
mnemonic: "u",
|
|
245
|
+
content:
|
|
246
|
+
state.status === undefined
|
|
247
|
+
? loadingContent("Status")
|
|
248
|
+
: createStatusPanel(state.status, theme, (action) => done({ kind: "status", action }), false),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
key: "benchmarks" as const,
|
|
252
|
+
label: "Benchmarks",
|
|
253
|
+
content:
|
|
254
|
+
state.benchmarks === undefined
|
|
255
|
+
? loadingContent("Benchmarks")
|
|
256
|
+
: createBenchmarkPanel(
|
|
257
|
+
state.benchmarks,
|
|
258
|
+
deps.benchmarks.currentIdentity,
|
|
259
|
+
theme,
|
|
260
|
+
(action) => done({ kind: "benchmarks", action }),
|
|
261
|
+
false,
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
key: "cache" as const,
|
|
266
|
+
label: "Cache",
|
|
267
|
+
content:
|
|
268
|
+
state.cache === undefined
|
|
269
|
+
? loadingContent("Cache")
|
|
270
|
+
: createCacheEconomicsPanel(state.cache, theme, (action) => done({ kind: "cache", action }), false),
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
const container = new TabbedContainer({
|
|
274
|
+
tabs,
|
|
275
|
+
theme: tabBarTheme(theme),
|
|
276
|
+
initialKey: activeKey,
|
|
277
|
+
onChange: (key) => {
|
|
278
|
+
activeKey = key as JittorTabKey;
|
|
279
|
+
done({ kind: "tab-changed" });
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
const outer = new BorderedSelectPanel({
|
|
283
|
+
title: "Jittor",
|
|
284
|
+
list: container,
|
|
285
|
+
helpText: "Tab/Shift+Tab switch tabs \u00b7 Esc close",
|
|
286
|
+
theme: {
|
|
287
|
+
border: (text) => theme.fg("borderMuted", text),
|
|
288
|
+
title: theme.bold,
|
|
289
|
+
help: (text) => theme.fg("dim", text),
|
|
290
|
+
},
|
|
291
|
+
measure: hostTextMeasure,
|
|
292
|
+
});
|
|
293
|
+
return {
|
|
294
|
+
invalidate: () => outer.invalidate(),
|
|
295
|
+
render: (width: number) => outer.render(width),
|
|
296
|
+
handleInput(data: string): void {
|
|
297
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
298
|
+
done({ kind: "close" });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
outer.handleInput(data);
|
|
302
|
+
tui.requestRender();
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
if (!outcome || outcome.kind === "close") return;
|
|
307
|
+
if (outcome.kind === "tab-changed") continue;
|
|
308
|
+
if (outcome.kind === "settings") {
|
|
309
|
+
await runSettingsAction(
|
|
310
|
+
ctx,
|
|
311
|
+
outcome.action,
|
|
312
|
+
deps.settings.enforcement,
|
|
313
|
+
deps.settings.recovery,
|
|
314
|
+
deps.settings.budgets,
|
|
315
|
+
deps.settings.effects,
|
|
316
|
+
);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (outcome.kind === "status") {
|
|
320
|
+
await runStatusAction(
|
|
321
|
+
ctx,
|
|
322
|
+
deps.status.client,
|
|
323
|
+
outcome.action,
|
|
324
|
+
state.status as StatusPanelSnapshot,
|
|
325
|
+
ctx.sessionManager.getSessionId(),
|
|
326
|
+
);
|
|
327
|
+
state.status = undefined;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (outcome.kind === "benchmarks") {
|
|
331
|
+
if (outcome.action === "refresh") await deps.benchmarks.client.call("benchmark.refresh", { force: true });
|
|
332
|
+
state.benchmarks = undefined;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// "cache": refresh has no separate daemon mutation -- looping back to refetch is the whole effect.
|
|
336
|
+
state.cache = undefined;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CacheEconomicsAggregateTotals,
|
|
3
|
+
CacheEconomicsModelSummary,
|
|
4
|
+
CacheEconomicsSummary,
|
|
5
|
+
CacheEconomicsTaskSummary,
|
|
6
|
+
} from "@danypops/jittor";
|
|
7
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
import { BorderedSelectPanel, type TextMeasure } from "malevich-tui-components";
|
|
10
|
+
|
|
11
|
+
export interface CacheEconomicsPanelTheme {
|
|
12
|
+
fg(color: string, text: string): string;
|
|
13
|
+
bold(text: string): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CacheEconomicsPanelAction = "refresh" | "close";
|
|
17
|
+
|
|
18
|
+
const hostTextMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
19
|
+
|
|
20
|
+
export interface CacheEconomicsPanelClient {
|
|
21
|
+
call(operation: string, input: unknown): Promise<any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatUsd(amount: number): string {
|
|
25
|
+
return `$${amount.toFixed(Math.abs(amount) < 0.01 && amount !== 0 ? 4 : 2)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function costField(amountUsd: number | null, basis: "provider-reported" | "catalog-estimate" | "unknown"): string {
|
|
29
|
+
if (amountUsd === null) return "unknown";
|
|
30
|
+
return basis === "catalog-estimate" ? `${formatUsd(amountUsd)} (est.)` : formatUsd(amountUsd);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function aggregateFields(totals: CacheEconomicsAggregateTotals): string {
|
|
34
|
+
const payback = totals.paybackAchieved === null ? "n/a" : totals.paybackAchieved ? "yes" : "not yet";
|
|
35
|
+
return [
|
|
36
|
+
`read ${totals.cacheReadTokens.toLocaleString()} tok (${costField(totals.cacheReadCostUsd, totals.cacheReadCostBasis)})`,
|
|
37
|
+
`write ${totals.cacheWriteTokens.toLocaleString()} tok (${costField(totals.cacheWriteCostUsd, totals.cacheWriteCostBasis)})`,
|
|
38
|
+
`savings ${totals.savingsUsd === null ? "unknown" : formatUsd(totals.savingsUsd)}`,
|
|
39
|
+
`payback ${payback}`,
|
|
40
|
+
].join(" · ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function freshnessSuffix(catalogFreshness: "fresh" | "stale" | null): string {
|
|
44
|
+
return catalogFreshness === "stale" ? " -- stale catalog snapshot used for the estimate(s) above" : "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function modelLine(model: CacheEconomicsModelSummary): string {
|
|
48
|
+
return `${model.provider}/${model.model}: ${aggregateFields(model)}${freshnessSuffix(model.catalogFreshness)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function taskLine(task: CacheEconomicsTaskSummary): string {
|
|
52
|
+
return `${task.taskId}: ${aggregateFields(task)}${freshnessSuffix(task.catalogFreshness)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Plain multi-line text, shared by TUI notify and non-TUI notify -- a full interactive panel is deferred; this already satisfies "bounded query plus Pi presentation" without a new widget. */
|
|
56
|
+
export function renderCacheEconomicsView(summary: CacheEconomicsSummary): string[] {
|
|
57
|
+
const lines = [
|
|
58
|
+
`Cache economics (${new Date(summary.since).toISOString().slice(0, 10)} .. ${new Date(summary.until).toISOString().slice(0, 10)})${summary.truncated ? " -- query limit reached, totals are a lower bound" : ""}`,
|
|
59
|
+
];
|
|
60
|
+
if (summary.models.length === 0) lines.push("No cache activity recorded in this window.");
|
|
61
|
+
else lines.push(...summary.models.map((model) => `- ${modelLine(model)}`));
|
|
62
|
+
if (summary.tasks.length > 0) {
|
|
63
|
+
lines.push(`By task: ${summary.tasks.length}`);
|
|
64
|
+
lines.push(...summary.tasks.map((task) => `- ${taskLine(task)}`));
|
|
65
|
+
}
|
|
66
|
+
const unattributed = summary.unattributedCacheActivity;
|
|
67
|
+
if (unattributed.cacheReadTokens > 0 || unattributed.cacheWriteTokens > 0) {
|
|
68
|
+
lines.push(
|
|
69
|
+
`Unattributed (no task focused): read ${unattributed.cacheReadTokens.toLocaleString()} tok (${costField(unattributed.cacheReadCostUsd, unattributed.cacheReadCostBasis)}) · write ${unattributed.cacheWriteTokens.toLocaleString()} tok (${costField(unattributed.cacheWriteCostUsd, unattributed.cacheWriteCostBasis)})`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (summary.stablePrefixChurn.length > 0) {
|
|
73
|
+
lines.push(`Stable-prefix churn (${summary.stablePrefixChurn.length} snapshot(s), oldest first):`);
|
|
74
|
+
for (const point of summary.stablePrefixChurn) {
|
|
75
|
+
lines.push(
|
|
76
|
+
`- ${new Date(point.observedAt).toISOString()} session ${point.sessionId}: ${point.stablePrefixTokens.toLocaleString()} tok${point.resetReason === null ? "" : ` (${point.resetReason} reset)`}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (summary.missedOpportunities.length > 0) {
|
|
81
|
+
lines.push(`Candidate missed-cache opportunities: ${summary.missedOpportunities.length}`);
|
|
82
|
+
for (const candidate of summary.missedOpportunities.slice(0, 10)) {
|
|
83
|
+
lines.push(
|
|
84
|
+
`- session ${candidate.sessionId}: ${candidate.resetReason} reset, then ${candidate.cacheWriteTokens.toLocaleString()} cache-write tok${candidate.cacheWriteCostUsd === null ? "" : ` (${formatUsd(candidate.cacheWriteCostUsd)})`}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Shared by both notify/panel entry points below and the unified /jittor shell, so all three fetch identically. */
|
|
92
|
+
export async function fetchCacheEconomicsSummary(
|
|
93
|
+
client: CacheEconomicsPanelClient,
|
|
94
|
+
windowMs: number,
|
|
95
|
+
now: () => number = Date.now,
|
|
96
|
+
): Promise<CacheEconomicsSummary> {
|
|
97
|
+
const until = now();
|
|
98
|
+
const since = Math.max(0, until - windowMs);
|
|
99
|
+
return (await client.call("cache.economics", { since, until })) as CacheEconomicsSummary;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function showCacheEconomicsView(
|
|
103
|
+
ctx: ExtensionCommandContext,
|
|
104
|
+
client: CacheEconomicsPanelClient,
|
|
105
|
+
windowMs: number,
|
|
106
|
+
now: () => number = Date.now,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
const summary = await fetchCacheEconomicsSummary(client, windowMs, now);
|
|
109
|
+
ctx.ui.notify(renderCacheEconomicsView(summary).join("\n"), "info");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The Cache panel's real chrome plus its own r/Esc key handling, wired to `onAction` rather than a
|
|
114
|
+
* `done` callback directly -- reusable by renderCacheEconomicsPanel/showCacheEconomicsPanel below
|
|
115
|
+
* and the unified /jittor shell. Defaults to a full-chrome standalone panel (`framed: true`); pass
|
|
116
|
+
* `framed: false` when nesting this as one tab's content inside another framed container.
|
|
117
|
+
*/
|
|
118
|
+
export function createCacheEconomicsPanel(
|
|
119
|
+
summary: CacheEconomicsSummary,
|
|
120
|
+
theme: CacheEconomicsPanelTheme,
|
|
121
|
+
onAction: (action: CacheEconomicsPanelAction) => void,
|
|
122
|
+
framed = true,
|
|
123
|
+
): BorderedSelectPanel {
|
|
124
|
+
const lines = renderCacheEconomicsView(summary);
|
|
125
|
+
const content = {
|
|
126
|
+
invalidate: () => {},
|
|
127
|
+
render: (availableWidth: number): string[] => lines.map((line) => truncateToWidth(line, availableWidth, "…")),
|
|
128
|
+
handleInput(data: string): void {
|
|
129
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) onAction("close");
|
|
130
|
+
else if (data === "r") onAction("refresh");
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
return new BorderedSelectPanel({
|
|
134
|
+
title: "Jittor Cache Economics",
|
|
135
|
+
list: content,
|
|
136
|
+
helpText: "r refresh · Esc close",
|
|
137
|
+
theme: {
|
|
138
|
+
border: (text) => theme.fg("borderMuted", text),
|
|
139
|
+
title: theme.bold,
|
|
140
|
+
help: (text) => theme.fg("dim", text),
|
|
141
|
+
},
|
|
142
|
+
measure: hostTextMeasure,
|
|
143
|
+
framed,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The same content as renderCacheEconomicsView, wrapped in a titled, bordered, scrollable frame -- mirrors optimization/model-selection-panel.ts's renderBenchmarkView/BorderedSelectPanel pattern. */
|
|
148
|
+
export function renderCacheEconomicsPanel(summary: CacheEconomicsSummary, width: number, theme: CacheEconomicsPanelTheme): string[] {
|
|
149
|
+
return createCacheEconomicsPanel(summary, theme, () => undefined).render(Math.max(1, width));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Interactive scrollable panel for /jittor cache, mirroring showBenchmarkPanel: a plain notify
|
|
154
|
+
* outside TUI mode (unchanged from showCacheEconomicsView's own behavior), an interactive
|
|
155
|
+
* BorderedSelectPanel with an 'r' refresh keybinding inside it.
|
|
156
|
+
*/
|
|
157
|
+
export async function showCacheEconomicsPanel(
|
|
158
|
+
ctx: ExtensionCommandContext,
|
|
159
|
+
client: CacheEconomicsPanelClient,
|
|
160
|
+
windowMs: number,
|
|
161
|
+
now: () => number = Date.now,
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
for (;;) {
|
|
164
|
+
const summary = await fetchCacheEconomicsSummary(client, windowMs, now);
|
|
165
|
+
if (ctx.mode !== "tui") {
|
|
166
|
+
ctx.ui.notify(renderCacheEconomicsView(summary).join("\n"), "info");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const action = await ctx.ui.custom<CacheEconomicsPanelAction>((_tui, theme, _keybindings, done) => {
|
|
170
|
+
const panel = createCacheEconomicsPanel(summary, theme, done);
|
|
171
|
+
return {
|
|
172
|
+
invalidate: () => panel.invalidate(),
|
|
173
|
+
render: (width: number) => panel.render(width),
|
|
174
|
+
handleInput: (data: string) => panel.handleInput(data),
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
if (!action || action === "close") return;
|
|
178
|
+
}
|
|
179
|
+
}
|