@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
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
TELEMETRY_STALE_AFTER_MS,
|
|
10
10
|
} from "@danypops/jittor";
|
|
11
11
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
12
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
import { BorderedSelectPanel, type TextMeasure } from "malevich-tui-components";
|
|
13
14
|
import { sessionSecretField } from "../session-identity.ts";
|
|
15
|
+
import { showConfirmDialog, showRouteOverrideMenu } from "../tui-prompts.ts";
|
|
14
16
|
import type { ProviderBudget } from "./footer.ts";
|
|
15
17
|
|
|
16
18
|
export interface JittorPanelClient {
|
|
@@ -32,7 +34,14 @@ export function providerBudgetMetricQuery(status: RouterStatus): MetricQuery | n
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
type PanelAction = "pause" | "resume" | "refresh" | "override" | "clear-override" | "close";
|
|
37
|
+
export type PanelAction = "pause" | "resume" | "refresh" | "override" | "clear-override" | "close";
|
|
38
|
+
|
|
39
|
+
export interface StatusPanelTheme {
|
|
40
|
+
fg(color: string, text: string): string;
|
|
41
|
+
bold(text: string): string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hostTextMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
36
45
|
|
|
37
46
|
function latest(
|
|
38
47
|
rows: StoredMetricObservation[],
|
|
@@ -326,10 +335,13 @@ export function buildStatusView(status: RouterStatus, metrics: StoredMetricObser
|
|
|
326
335
|
return lines;
|
|
327
336
|
}
|
|
328
337
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
338
|
+
export interface StatusPanelSnapshot {
|
|
339
|
+
status: RouterStatus;
|
|
340
|
+
metrics: StoredMetricObservation[];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Shared by the standalone status panel below and the unified /jittor shell, so both fetch identically. */
|
|
344
|
+
export async function fetchStatusSnapshot(client: JittorPanelClient, sessionId: string): Promise<StatusPanelSnapshot> {
|
|
333
345
|
const status = (await client.call("router.status", { session_id: sessionId })) as RouterStatus;
|
|
334
346
|
const query = providerBudgetMetricQuery(status);
|
|
335
347
|
const metrics = query ? ((await client.call("metrics.query", query)) as StoredMetricObservation[]) : [];
|
|
@@ -341,74 +353,113 @@ async function chooseOverride(ctx: ExtensionCommandContext, routes: Route[]): Pr
|
|
|
341
353
|
ctx.ui.notify("Pi reports no authenticated routes for the current provider.", "warning");
|
|
342
354
|
return undefined;
|
|
343
355
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
356
|
+
return showRouteOverrideMenu(ctx, routes, routeText);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The Status panel's real chrome (a BorderedSelectPanel, replacing the hand-rolled border lines
|
|
361
|
+
* this used to draw directly) plus its own r/p/o/c/Esc key handling, wired to `onAction` rather
|
|
362
|
+
* than a `done` callback directly -- reusable by the standalone panel below and the unified
|
|
363
|
+
* /jittor shell. Defaults to a full-chrome standalone panel (`framed: true`); pass `framed: false`
|
|
364
|
+
* when nesting this as one tab's content inside another framed container.
|
|
365
|
+
*/
|
|
366
|
+
export function createStatusPanel(
|
|
367
|
+
current: StatusPanelSnapshot,
|
|
368
|
+
theme: StatusPanelTheme,
|
|
369
|
+
onAction: (action: PanelAction) => void,
|
|
370
|
+
framed = true,
|
|
371
|
+
): BorderedSelectPanel {
|
|
372
|
+
const controls = current.status.paused
|
|
373
|
+
? "r refresh · p release emergency halt · o override · c clear override · Esc close"
|
|
374
|
+
: "r refresh · p emergency halt · o override · c clear override · Esc close";
|
|
375
|
+
const content = {
|
|
376
|
+
invalidate: () => {},
|
|
377
|
+
render: (width: number): string[] =>
|
|
378
|
+
buildStatusView(current.status, current.metrics).map((line) => truncateToWidth(` ${line}`, width, "…")),
|
|
379
|
+
handleInput(data: string): void {
|
|
380
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) onAction("close");
|
|
381
|
+
else if (data === "r") onAction("refresh");
|
|
382
|
+
else if (data === "p") onAction(current.status.paused ? "resume" : "pause");
|
|
383
|
+
else if (data === "o") onAction("override");
|
|
384
|
+
else if (data === "c") onAction("clear-override");
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
return new BorderedSelectPanel({
|
|
388
|
+
title: "Jittor",
|
|
389
|
+
list: content,
|
|
390
|
+
helpText: controls,
|
|
391
|
+
theme: {
|
|
392
|
+
border: (text) => theme.fg("borderMuted", text),
|
|
393
|
+
title: theme.bold,
|
|
394
|
+
help: (text) => theme.fg("dim", text),
|
|
395
|
+
},
|
|
396
|
+
measure: hostTextMeasure,
|
|
397
|
+
framed,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Performs the real side effect for one resolved status action -- confirmations and daemon calls.
|
|
403
|
+
* A no-op for "close" and "refresh" is handled by the caller (refresh is a plain re-poll with no
|
|
404
|
+
* confirmation). Shared by the standalone status panel below and the unified /jittor shell.
|
|
405
|
+
*/
|
|
406
|
+
export async function runStatusAction(
|
|
407
|
+
ctx: ExtensionCommandContext,
|
|
408
|
+
client: JittorPanelClient,
|
|
409
|
+
action: PanelAction,
|
|
410
|
+
current: StatusPanelSnapshot,
|
|
411
|
+
sessionId: string,
|
|
412
|
+
): Promise<void> {
|
|
413
|
+
if (action === "close") return;
|
|
414
|
+
if (action === "refresh") {
|
|
415
|
+
await client.call("telemetry.poll", {});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (action === "pause" || action === "resume") {
|
|
419
|
+
if (
|
|
420
|
+
await showConfirmDialog(
|
|
421
|
+
ctx,
|
|
422
|
+
action === "pause" ? "Emergency-halt provider requests?" : "Release emergency halt?",
|
|
423
|
+
"This changes provider-request enforcement. Use /jittor off to disable blocking entirely.",
|
|
424
|
+
)
|
|
425
|
+
) {
|
|
426
|
+
await client.call(action === "pause" ? "router.pause" : "router.resume", { session_id: sessionId, ...sessionSecretField(sessionId) });
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (action === "clear-override") {
|
|
431
|
+
if (await showConfirmDialog(ctx, "Clear route override?", "Policy-controlled routing will resume."))
|
|
432
|
+
await client.call("router.clear_override", { session_id: sessionId, ...sessionSecretField(sessionId) });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const route = await chooseOverride(ctx, current.status.availableRoutes);
|
|
436
|
+
if (route && (await showConfirmDialog(ctx, "Apply route override?", `${routeText(route)} for one hour`))) {
|
|
437
|
+
await client.call("router.override", {
|
|
438
|
+
route,
|
|
439
|
+
expiresAt: Date.now() + 60 * 60 * 1_000,
|
|
440
|
+
session_id: sessionId,
|
|
441
|
+
...sessionSecretField(sessionId),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
348
444
|
}
|
|
349
445
|
|
|
350
446
|
export async function showJittorPanel(ctx: ExtensionCommandContext, client: JittorPanelClient): Promise<void> {
|
|
351
447
|
const session_id = ctx.sessionManager.getSessionId();
|
|
352
448
|
for (;;) {
|
|
353
|
-
const current = await
|
|
449
|
+
const current = await fetchStatusSnapshot(client, session_id);
|
|
354
450
|
if (ctx.mode !== "tui") {
|
|
355
451
|
ctx.ui.notify(buildStatusView(current.status, current.metrics).join("\n"), "info");
|
|
356
452
|
return;
|
|
357
453
|
}
|
|
358
|
-
const action = await ctx.ui.custom<PanelAction>((_tui, theme, _keybindings, done) =>
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
border,
|
|
367
|
-
truncateToWidth(theme.bold("Jittor"), width, ""),
|
|
368
|
-
border,
|
|
369
|
-
...buildStatusView(current.status, current.metrics).map((line) => truncateToWidth(` ${line}`, width, "…")),
|
|
370
|
-
border,
|
|
371
|
-
truncateToWidth(theme.fg("dim", controls), width, "…"),
|
|
372
|
-
border,
|
|
373
|
-
];
|
|
374
|
-
},
|
|
375
|
-
handleInput(data: string): void {
|
|
376
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) done("close");
|
|
377
|
-
else if (data === "r") done("refresh");
|
|
378
|
-
else if (data === "p") done(current.status.paused ? "resume" : "pause");
|
|
379
|
-
else if (data === "o") done("override");
|
|
380
|
-
else if (data === "c") done("clear-override");
|
|
381
|
-
},
|
|
382
|
-
}));
|
|
454
|
+
const action = await ctx.ui.custom<PanelAction>((_tui, theme, _keybindings, done) => {
|
|
455
|
+
const panel = createStatusPanel(current, theme, done);
|
|
456
|
+
return {
|
|
457
|
+
invalidate: () => panel.invalidate(),
|
|
458
|
+
render: (width: number) => panel.render(width),
|
|
459
|
+
handleInput: (data: string) => panel.handleInput(data),
|
|
460
|
+
};
|
|
461
|
+
});
|
|
383
462
|
if (!action || action === "close") return;
|
|
384
|
-
|
|
385
|
-
await client.call("telemetry.poll", {});
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
if (action === "pause" || action === "resume") {
|
|
389
|
-
if (
|
|
390
|
-
await ctx.ui.confirm(
|
|
391
|
-
action === "pause" ? "Emergency-halt provider requests?" : "Release emergency halt?",
|
|
392
|
-
"This changes provider-request enforcement. Use /jittor off to disable blocking entirely.",
|
|
393
|
-
)
|
|
394
|
-
) {
|
|
395
|
-
await client.call(action === "pause" ? "router.pause" : "router.resume", { session_id, ...sessionSecretField(session_id) });
|
|
396
|
-
}
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
if (action === "clear-override") {
|
|
400
|
-
if (await ctx.ui.confirm("Clear route override?", "Policy-controlled routing will resume."))
|
|
401
|
-
await client.call("router.clear_override", { session_id, ...sessionSecretField(session_id) });
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
const route = await chooseOverride(ctx, current.status.availableRoutes);
|
|
405
|
-
if (route && (await ctx.ui.confirm("Apply route override?", `${routeText(route)} for one hour`))) {
|
|
406
|
-
await client.call("router.override", {
|
|
407
|
-
route,
|
|
408
|
-
expiresAt: Date.now() + 60 * 60 * 1_000,
|
|
409
|
-
session_id,
|
|
410
|
-
...sessionSecretField(session_id),
|
|
411
|
-
});
|
|
412
|
-
}
|
|
463
|
+
await runStatusAction(ctx, client, action, current, session_id);
|
|
413
464
|
}
|
|
414
465
|
}
|
|
@@ -27,7 +27,7 @@ interface BenchmarkTheme {
|
|
|
27
27
|
bold(text: string): string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
type BenchmarkPanelAction = "refresh" | "close";
|
|
30
|
+
export type BenchmarkPanelAction = "refresh" | "close";
|
|
31
31
|
|
|
32
32
|
const COMPONENT_LABELS: Record<UtilityComponentName, string> = { quality: "Q", cost: "$", latency: "L", context: "C", reliability: "R" };
|
|
33
33
|
|
|
@@ -58,8 +58,19 @@ function benchmarkRows(shown: RankedModel[], currentIdentity: string): Record<st
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The Benchmarks panel's real chrome plus its own r/Esc key handling, wired to `onAction` rather
|
|
63
|
+
* than a `done` callback directly -- reusable by renderBenchmarkView/showBenchmarkPanel below and
|
|
64
|
+
* the unified /jittor shell. Defaults to a full-chrome standalone panel (`framed: true`); pass
|
|
65
|
+
* `framed: false` when nesting this as one tab's content inside another framed container.
|
|
66
|
+
*/
|
|
67
|
+
export function createBenchmarkPanel(
|
|
68
|
+
result: ModelRankingResult,
|
|
69
|
+
currentIdentity: string,
|
|
70
|
+
theme: BenchmarkTheme,
|
|
71
|
+
onAction: (action: BenchmarkPanelAction) => void,
|
|
72
|
+
framed = true,
|
|
73
|
+
): BorderedSelectPanel {
|
|
63
74
|
const shown = result.ranked.slice(0, BENCHMARK_TUI_MAX_CANDIDATES);
|
|
64
75
|
const currentIndex = result.ranked.findIndex((item) => item.identity.startsWith(`${currentIdentity}:`));
|
|
65
76
|
const recommended = result.ranked[0];
|
|
@@ -96,6 +107,10 @@ export function renderBenchmarkView(result: ModelRankingResult, currentIdentity:
|
|
|
96
107
|
: []),
|
|
97
108
|
...(result.scopeWarning ? [truncateToWidth(result.scopeWarning, availableWidth, "…")] : []),
|
|
98
109
|
],
|
|
110
|
+
handleInput(data: string): void {
|
|
111
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) onAction("close");
|
|
112
|
+
else if (data === "r") onAction("refresh");
|
|
113
|
+
},
|
|
99
114
|
};
|
|
100
115
|
return new BorderedSelectPanel({
|
|
101
116
|
title: "Jittor Benchmark Recommendations",
|
|
@@ -107,7 +122,40 @@ export function renderBenchmarkView(result: ModelRankingResult, currentIdentity:
|
|
|
107
122
|
help: (text) => theme.fg("dim", text),
|
|
108
123
|
},
|
|
109
124
|
measure: hostTextMeasure,
|
|
110
|
-
|
|
125
|
+
framed,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function renderBenchmarkView(result: ModelRankingResult, currentIdentity: string, width: number, theme: BenchmarkTheme): string[] {
|
|
130
|
+
return createBenchmarkPanel(result, currentIdentity, theme, () => undefined).render(Math.max(1, width));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Shared by the standalone benchmark panel below and the unified /jittor shell, so both fetch identically. */
|
|
134
|
+
export async function fetchBenchmarkRanking(
|
|
135
|
+
ctx: ExtensionCommandContext,
|
|
136
|
+
client: BenchmarkPanelClient,
|
|
137
|
+
candidates: ModelCandidate[],
|
|
138
|
+
domain: ModelTaskDomain,
|
|
139
|
+
type: ModelTaskType,
|
|
140
|
+
): Promise<ModelRankingResult> {
|
|
141
|
+
const session_id = ctx.sessionManager.getSessionId();
|
|
142
|
+
return (await client.call("models.rank", {
|
|
143
|
+
candidates,
|
|
144
|
+
session_id,
|
|
145
|
+
...sessionSecretField(session_id),
|
|
146
|
+
scopeAuthority: "available-models",
|
|
147
|
+
domain,
|
|
148
|
+
type,
|
|
149
|
+
budgetPressure: 0,
|
|
150
|
+
weights: {
|
|
151
|
+
quality: MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
|
|
152
|
+
cost: MODEL_RANKING_DEFAULT_COST_WEIGHT,
|
|
153
|
+
latency: MODEL_RANKING_DEFAULT_LATENCY_WEIGHT,
|
|
154
|
+
context: MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
|
|
155
|
+
reliability: MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
|
|
156
|
+
},
|
|
157
|
+
sourceIds: ["openrouter-models", "lmarena-hf", "artificial-analysis-direct", "openrouter-design-arena"],
|
|
158
|
+
})) as ModelRankingResult;
|
|
111
159
|
}
|
|
112
160
|
|
|
113
161
|
export async function showBenchmarkPanel(
|
|
@@ -119,24 +167,7 @@ export async function showBenchmarkPanel(
|
|
|
119
167
|
type: ModelTaskType,
|
|
120
168
|
): Promise<void> {
|
|
121
169
|
for (;;) {
|
|
122
|
-
const
|
|
123
|
-
const result = (await client.call("models.rank", {
|
|
124
|
-
candidates,
|
|
125
|
-
session_id,
|
|
126
|
-
...sessionSecretField(session_id),
|
|
127
|
-
scopeAuthority: "available-models",
|
|
128
|
-
domain,
|
|
129
|
-
type,
|
|
130
|
-
budgetPressure: 0,
|
|
131
|
-
weights: {
|
|
132
|
-
quality: MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
|
|
133
|
-
cost: MODEL_RANKING_DEFAULT_COST_WEIGHT,
|
|
134
|
-
latency: MODEL_RANKING_DEFAULT_LATENCY_WEIGHT,
|
|
135
|
-
context: MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
|
|
136
|
-
reliability: MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
|
|
137
|
-
},
|
|
138
|
-
sourceIds: ["openrouter-models", "lmarena-hf", "artificial-analysis-direct", "openrouter-design-arena"],
|
|
139
|
-
})) as ModelRankingResult;
|
|
170
|
+
const result = await fetchBenchmarkRanking(ctx, client, candidates, domain, type);
|
|
140
171
|
if (ctx.mode !== "tui") {
|
|
141
172
|
ctx.ui.notify(
|
|
142
173
|
renderBenchmarkView(result, currentIdentity, 100, { fg: (_color, text) => text, bold: (text) => text }).join("\n"),
|
|
@@ -144,16 +175,14 @@ export async function showBenchmarkPanel(
|
|
|
144
175
|
);
|
|
145
176
|
return;
|
|
146
177
|
}
|
|
147
|
-
const action = await ctx.ui.custom<BenchmarkPanelAction>((_tui, theme, _keybindings, done) =>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
},
|
|
156
|
-
}));
|
|
178
|
+
const action = await ctx.ui.custom<BenchmarkPanelAction>((_tui, theme, _keybindings, done) => {
|
|
179
|
+
const panel = createBenchmarkPanel(result, currentIdentity, theme, done);
|
|
180
|
+
return {
|
|
181
|
+
invalidate: () => panel.invalidate(),
|
|
182
|
+
render: (width: number) => panel.render(width),
|
|
183
|
+
handleInput: (data: string) => panel.handleInput(data),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
157
186
|
if (!action || action === "close") return;
|
|
158
187
|
await client.call("benchmark.refresh", { force: true });
|
|
159
188
|
}
|
|
@@ -50,6 +50,7 @@ const OPERATION_RETRY_MODE = {
|
|
|
50
50
|
"router.clear_override": "once",
|
|
51
51
|
"router.current_route": "once",
|
|
52
52
|
"router.available_routes": "once",
|
|
53
|
+
"cache.economics": "retry",
|
|
53
54
|
} as const satisfies Record<OperationName, "retry" | "once">;
|
|
54
55
|
|
|
55
56
|
export function operationRetryMode(operation: OperationName): "retry" | "once" {
|
|
@@ -3,6 +3,7 @@ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { BorderedSelectPanel, Menu, type MenuTheme, type TextMeasure } from "malevich-tui-components";
|
|
5
5
|
import type { CodexRecoveryControl, EnforcementControl, UsageBudgetControl } from "./settings.ts";
|
|
6
|
+
import { showConfirmDialog } from "./tui-prompts.ts";
|
|
6
7
|
|
|
7
8
|
export interface SettingsSnapshot {
|
|
8
9
|
enforcementEnabled: boolean;
|
|
@@ -16,8 +17,8 @@ interface SettingsTheme {
|
|
|
16
17
|
bold(text: string): string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
type SettingsKey = "enforcement" | "footer" | "recovery" | `budget-${UsagePeriod}`;
|
|
20
|
-
type SettingsAction = { kind: "activate"; key: SettingsKey } | { kind: "close" };
|
|
20
|
+
export type SettingsKey = "enforcement" | "footer" | "recovery" | `budget-${UsagePeriod}`;
|
|
21
|
+
export type SettingsAction = { kind: "activate"; key: SettingsKey } | { kind: "close" };
|
|
21
22
|
|
|
22
23
|
export interface SettingsEffects {
|
|
23
24
|
setEnforcement(enabled: boolean): void | Promise<void>;
|
|
@@ -25,7 +26,21 @@ export interface SettingsEffects {
|
|
|
25
26
|
setRecovery(enabled: boolean): void | Promise<void>;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
// Enforcement -> Budget -> Providers -> UI: safety posture (the global kill-switch everything
|
|
30
|
+
// else is downstream of) leads, followed by spend limits, then provider-specific quirks (today,
|
|
31
|
+
// only Codex recovery -- grouped under its own header instead of sitting flat next to global
|
|
32
|
+
// switches, which read as "too Codex-oriented" with nothing to signal its narrower scope), then
|
|
33
|
+
// display preferences last.
|
|
34
|
+
const SETTINGS_KEYS: SettingsKey[] = ["enforcement", ...USAGE_PERIODS.map(({ id }) => `budget-${id}` as const), "recovery", "footer"];
|
|
35
|
+
|
|
36
|
+
type SettingsCategory = "Enforcement" | "Budget" | "Providers" | "UI";
|
|
37
|
+
|
|
38
|
+
function categoryOf(key: SettingsKey): SettingsCategory {
|
|
39
|
+
if (key === "enforcement") return "Enforcement";
|
|
40
|
+
if (key === "recovery") return "Providers";
|
|
41
|
+
if (key === "footer") return "UI";
|
|
42
|
+
return "Budget";
|
|
43
|
+
}
|
|
29
44
|
|
|
30
45
|
function state(enabled: boolean, theme: SettingsTheme): string {
|
|
31
46
|
return enabled ? theme.fg("success", "ON") : theme.fg("muted", "OFF");
|
|
@@ -69,11 +84,54 @@ function menuTheme(theme: SettingsTheme): MenuTheme {
|
|
|
69
84
|
};
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Wraps `menu` (already built over `SETTINGS_KEYS` in order, with no `title` of its own) so its
|
|
89
|
+
* rendered output gets a non-selectable category header line inserted before each contiguous run
|
|
90
|
+
* of same-category rows. `Menu` has no native header/divider item type, and keeps its selected
|
|
91
|
+
* index private with no getter, so headers are inserted into the *rendered lines*, never built as
|
|
92
|
+
* extra selectable items -- `menu`'s own input handling, selection state, and every existing
|
|
93
|
+
* keybinding stay completely untouched; this only changes what gets displayed.
|
|
94
|
+
*
|
|
95
|
+
* With no `title` set, `Menu.render` always returns exactly `[rule, ...itemLines, rule]` (see
|
|
96
|
+
* malevich-tui-components' Menu/renderFramedPanel). If that ever stops holding -- a future
|
|
97
|
+
* malevich release changing Menu's own frame shape -- this falls back to Menu's unmodified
|
|
98
|
+
* output rather than slicing the wrong lines into a garbled view.
|
|
99
|
+
*/
|
|
100
|
+
function groupedSettingsMenu(
|
|
101
|
+
menu: Menu,
|
|
102
|
+
theme: SettingsTheme,
|
|
103
|
+
): { invalidate(): void; handleInput(data: string): void; render(width: number): string[] } {
|
|
104
|
+
return {
|
|
105
|
+
invalidate: () => menu.invalidate(),
|
|
106
|
+
handleInput: (data: string) => menu.handleInput(data),
|
|
107
|
+
render(width: number): string[] {
|
|
108
|
+
const rendered = menu.render(width);
|
|
109
|
+
if (rendered.length !== SETTINGS_KEYS.length + 2) return rendered;
|
|
110
|
+
const itemLines = rendered.slice(1, 1 + SETTINGS_KEYS.length);
|
|
111
|
+
const out: string[] = [rendered[0]!];
|
|
112
|
+
let lastCategory: SettingsCategory | undefined;
|
|
113
|
+
for (const [index, key] of SETTINGS_KEYS.entries()) {
|
|
114
|
+
const category = categoryOf(key);
|
|
115
|
+
if (category !== lastCategory) {
|
|
116
|
+
if (lastCategory !== undefined) out.push("");
|
|
117
|
+
out.push(theme.bold(theme.fg("dim", category)));
|
|
118
|
+
lastCategory = category;
|
|
119
|
+
}
|
|
120
|
+
out.push(itemLines[index]!);
|
|
121
|
+
}
|
|
122
|
+
out.push(rendered.at(-1)!);
|
|
123
|
+
return out;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Defaults to a full-chrome standalone panel (`framed: true`); pass `framed: false` when nesting this as one tab's content inside another framed container (e.g. the unified /jittor shell's own outer border). */
|
|
129
|
+
export function createSettingsPanel(
|
|
73
130
|
snapshot: SettingsSnapshot,
|
|
74
131
|
theme: SettingsTheme,
|
|
75
132
|
onAction: (action: SettingsAction) => void,
|
|
76
133
|
selected = 0,
|
|
134
|
+
framed = true,
|
|
77
135
|
): BorderedSelectPanel {
|
|
78
136
|
const menu = new Menu({
|
|
79
137
|
items: SETTINGS_KEYS.map((key) => ({ label: rowText(key, snapshot, theme), action: () => onAction({ kind: "activate", key }) })),
|
|
@@ -91,7 +149,7 @@ function createSettingsPanel(
|
|
|
91
149
|
for (let index = 0; index < selected; index += 1) menu.handleInput("\x1b[B");
|
|
92
150
|
return new BorderedSelectPanel({
|
|
93
151
|
title: "Jittor Settings",
|
|
94
|
-
list: menu,
|
|
152
|
+
list: groupedSettingsMenu(menu, theme),
|
|
95
153
|
helpText: "Token budgets are user values; provider quotas remain separate. · ↑/↓ select · Enter edit · Esc close",
|
|
96
154
|
theme: {
|
|
97
155
|
border: (text) => theme.fg("borderMuted", text),
|
|
@@ -99,6 +157,7 @@ function createSettingsPanel(
|
|
|
99
157
|
help: (text) => theme.fg("dim", text),
|
|
100
158
|
},
|
|
101
159
|
measure: hostTextMeasure,
|
|
160
|
+
framed,
|
|
102
161
|
});
|
|
103
162
|
}
|
|
104
163
|
|
|
@@ -130,6 +189,53 @@ async function editBudget(ctx: ExtensionCommandContext, budgets: UsageBudgetCont
|
|
|
130
189
|
ctx.ui.notify(`${label} token budget set to ${tokens.toLocaleString()} tokens.`, "info");
|
|
131
190
|
}
|
|
132
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Performs the real side effect for one resolved settings action -- confirmations, effects calls,
|
|
194
|
+
* budget prompts. A no-op for "close". Shared by the standalone settings panel below and the
|
|
195
|
+
* unified /jittor shell, so the two interactive surfaces can never drift apart.
|
|
196
|
+
*/
|
|
197
|
+
export async function runSettingsAction(
|
|
198
|
+
ctx: ExtensionCommandContext,
|
|
199
|
+
action: SettingsAction,
|
|
200
|
+
enforcement: EnforcementControl,
|
|
201
|
+
recovery: CodexRecoveryControl,
|
|
202
|
+
budgets: UsageBudgetControl,
|
|
203
|
+
effects: SettingsEffects,
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
if (action.kind === "close") return;
|
|
206
|
+
if (action.key === "enforcement") {
|
|
207
|
+
if (enforcement.isEnabled()) {
|
|
208
|
+
if (
|
|
209
|
+
await showConfirmDialog(
|
|
210
|
+
ctx,
|
|
211
|
+
"Disable routing enforcement?",
|
|
212
|
+
"Jittor will remain monitor-only and will no longer block unsafe provider requests.",
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
await effects.setEnforcement(false);
|
|
216
|
+
} else await effects.setEnforcement(true);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (action.key === "footer") {
|
|
220
|
+
await effects.setFooter(!enforcement.isFooterEnabled());
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (action.key === "recovery") {
|
|
224
|
+
if (!recovery.isCodexRecoveryEnabled()) {
|
|
225
|
+
if (
|
|
226
|
+
await showConfirmDialog(
|
|
227
|
+
ctx,
|
|
228
|
+
"Enable Codex recovery?",
|
|
229
|
+
"Jittor may start bounded hidden retries only after transient Codex failures fully settle.",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
await effects.setRecovery(true);
|
|
233
|
+
} else await effects.setRecovery(false);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await editBudget(ctx, budgets, action.key.slice("budget-".length) as UsagePeriod);
|
|
237
|
+
}
|
|
238
|
+
|
|
133
239
|
export async function showSettingsPanel(
|
|
134
240
|
ctx: ExtensionCommandContext,
|
|
135
241
|
enforcement: EnforcementControl,
|
|
@@ -160,34 +266,6 @@ export async function showSettingsPanel(
|
|
|
160
266
|
};
|
|
161
267
|
});
|
|
162
268
|
if (!action || action.kind === "close") return;
|
|
163
|
-
|
|
164
|
-
if (enforcement.isEnabled()) {
|
|
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);
|
|
172
|
-
} else await effects.setEnforcement(true);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (action.key === "footer") {
|
|
176
|
-
await effects.setFooter(!enforcement.isFooterEnabled());
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (action.key === "recovery") {
|
|
180
|
-
if (!recovery.isCodexRecoveryEnabled()) {
|
|
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);
|
|
188
|
-
} else await effects.setRecovery(false);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
await editBudget(ctx, budgets, action.key.slice("budget-".length) as UsagePeriod);
|
|
269
|
+
await runSettingsAction(ctx, action, enforcement, recovery, budgets, effects);
|
|
192
270
|
}
|
|
193
271
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirm/select prompts rendered with malevich-tui-components' own Dialog/TabMenu, inside
|
|
3
|
+
* jittor's own theme and frame -- replacing Pi's host-native `ctx.ui.confirm`/`ctx.ui.select`
|
|
4
|
+
* (a visually separate native prompt) for every jittor-owned confirmation and selection.
|
|
5
|
+
*
|
|
6
|
+
* TUI-only: every real caller (settings-tui.ts's runSettingsAction, observability/status.ts's
|
|
7
|
+
* runStatusAction/chooseOverride) is itself only ever invoked from within an already-`ctx.mode
|
|
8
|
+
* === "tui"` code path -- the non-TUI notify fallback returns before ever reaching one of these
|
|
9
|
+
* calls -- so there is no non-TUI branch to fall back to here.
|
|
10
|
+
*
|
|
11
|
+
* Budget entry (settings-tui.ts's editBudget) deliberately keeps Pi's native `ctx.ui.input`:
|
|
12
|
+
* malevich has no plain visible text-input component today -- only `MaskedInput` (renders mask
|
|
13
|
+
* glyphs only, built for secrets; wrong semantics for a token count a user wants to see as they
|
|
14
|
+
* type) and `Form` (owns focus/validation/layout but requires a host-supplied `FormFieldInput`,
|
|
15
|
+
* and `MaskedInput` is the only one malevich ships). Building a new visible-text-input component
|
|
16
|
+
* is out of scope here; this is a deliberate, documented decision, not an oversight.
|
|
17
|
+
*/
|
|
18
|
+
import type { Route } from "@danypops/jittor";
|
|
19
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
21
|
+
import { Dialog, TabMenu, type TextMeasure } from "malevich-tui-components";
|
|
22
|
+
|
|
23
|
+
interface PromptTheme {
|
|
24
|
+
fg(color: string, text: string): string;
|
|
25
|
+
bold(text: string): string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hostTextMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A bordered Dialog with exactly a Yes/No choice, resolving `true` for confirm, `false` for
|
|
32
|
+
* cancel -- including Escape, Ctrl+C, or the host dismissing the panel outright (`custom()`
|
|
33
|
+
* resolving `undefined`). A drop-in replacement for `ctx.ui.confirm(title, body)`.
|
|
34
|
+
*/
|
|
35
|
+
export async function showConfirmDialog(
|
|
36
|
+
ctx: ExtensionCommandContext,
|
|
37
|
+
title: string,
|
|
38
|
+
body: string,
|
|
39
|
+
options: { confirmLabel?: string; cancelLabel?: string } = {},
|
|
40
|
+
): Promise<boolean> {
|
|
41
|
+
const confirmLabel = options.confirmLabel ?? "Yes";
|
|
42
|
+
const cancelLabel = options.cancelLabel ?? "No";
|
|
43
|
+
const confirmed = await ctx.ui.custom<boolean>((_tui, theme: PromptTheme, _keybindings, done) => {
|
|
44
|
+
const dialog = new Dialog({
|
|
45
|
+
title,
|
|
46
|
+
body,
|
|
47
|
+
actions: [
|
|
48
|
+
{ label: confirmLabel, key: "y", action: () => done(true) },
|
|
49
|
+
{ label: cancelLabel, key: "n", action: () => done(false) },
|
|
50
|
+
],
|
|
51
|
+
theme: {
|
|
52
|
+
border: (text) => theme.fg("borderMuted", text),
|
|
53
|
+
title: theme.bold,
|
|
54
|
+
body: (text) => text,
|
|
55
|
+
dim: (text) => theme.fg("dim", text),
|
|
56
|
+
},
|
|
57
|
+
measure: hostTextMeasure,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
invalidate: () => dialog.invalidate(),
|
|
61
|
+
render: (width: number) => dialog.render(width),
|
|
62
|
+
handleInput(data: string): void {
|
|
63
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
64
|
+
done(false);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
dialog.handleInput(data);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
return confirmed ?? false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A TabMenu over one flat level -- every route is a leaf node, no drill-down -- resolving the
|
|
76
|
+
* selected route, or `undefined` on cancel (Escape at the root, Ctrl+C, or the host dismissing
|
|
77
|
+
* the panel outright). A drop-in replacement for `ctx.ui.select("Override route", labels)`
|
|
78
|
+
* followed by mapping the chosen label back to its route.
|
|
79
|
+
*/
|
|
80
|
+
export async function showRouteOverrideMenu(
|
|
81
|
+
ctx: ExtensionCommandContext,
|
|
82
|
+
routes: Route[],
|
|
83
|
+
routeLabel: (route: Route) => string,
|
|
84
|
+
): Promise<Route | undefined> {
|
|
85
|
+
return ctx.ui.custom<Route | undefined>((_tui, theme: PromptTheme, _keybindings, done) => {
|
|
86
|
+
const tabMenu = new TabMenu<Route>({
|
|
87
|
+
nodes: routes.map((route) => ({ label: routeLabel(route), value: route })),
|
|
88
|
+
theme: {
|
|
89
|
+
tab: (text) => theme.fg("dim", text),
|
|
90
|
+
activeTab: (text) => theme.bold(theme.fg("accent", text)),
|
|
91
|
+
breadcrumb: (text) => theme.fg("dim", text),
|
|
92
|
+
description: (text) => theme.fg("dim", text),
|
|
93
|
+
help: (text) => theme.fg("dim", text),
|
|
94
|
+
},
|
|
95
|
+
onSelect: (route) => done(route),
|
|
96
|
+
onCancel: () => done(undefined),
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
invalidate: () => tabMenu.invalidate(),
|
|
100
|
+
render: (width: number) => tabMenu.render(width),
|
|
101
|
+
handleInput(data: string): void {
|
|
102
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
103
|
+
done(undefined);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
tabMenu.handleInput(data);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|