@danypops/pi-jittor 0.4.0 → 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.
@@ -13,7 +13,7 @@ export interface CacheEconomicsPanelTheme {
13
13
  bold(text: string): string;
14
14
  }
15
15
 
16
- type CacheEconomicsPanelAction = "refresh" | "close";
16
+ export type CacheEconomicsPanelAction = "refresh" | "close";
17
17
 
18
18
  const hostTextMeasure: TextMeasure = { visibleWidth, truncateToWidth };
19
19
 
@@ -88,25 +88,47 @@ export function renderCacheEconomicsView(summary: CacheEconomicsSummary): string
88
88
  return lines;
89
89
  }
90
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
+
91
102
  export async function showCacheEconomicsView(
92
103
  ctx: ExtensionCommandContext,
93
104
  client: CacheEconomicsPanelClient,
94
105
  windowMs: number,
95
106
  now: () => number = Date.now,
96
107
  ): Promise<void> {
97
- const until = now();
98
- const since = Math.max(0, until - windowMs);
99
- const summary = (await client.call("cache.economics", { since, until })) as CacheEconomicsSummary;
108
+ const summary = await fetchCacheEconomicsSummary(client, windowMs, now);
100
109
  ctx.ui.notify(renderCacheEconomicsView(summary).join("\n"), "info");
101
110
  }
102
111
 
103
- /** The same content as renderCacheEconomicsView, wrapped in a titled, bordered, scrollable frame -- mirrors optimization/model-selection-panel.ts's renderBenchmarkView/BorderedSelectPanel pattern. */
104
- export function renderCacheEconomicsPanel(summary: CacheEconomicsSummary, width: number, theme: CacheEconomicsPanelTheme): string[] {
105
- const safeWidth = Math.max(1, width);
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 {
106
124
  const lines = renderCacheEconomicsView(summary);
107
125
  const content = {
108
126
  invalidate: () => {},
109
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
+ },
110
132
  };
111
133
  return new BorderedSelectPanel({
112
134
  title: "Jittor Cache Economics",
@@ -118,7 +140,13 @@ export function renderCacheEconomicsPanel(summary: CacheEconomicsSummary, width:
118
140
  help: (text) => theme.fg("dim", text),
119
141
  },
120
142
  measure: hostTextMeasure,
121
- }).render(safeWidth);
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));
122
150
  }
123
151
 
124
152
  /**
@@ -133,23 +161,19 @@ export async function showCacheEconomicsPanel(
133
161
  now: () => number = Date.now,
134
162
  ): Promise<void> {
135
163
  for (;;) {
136
- const until = now();
137
- const since = Math.max(0, until - windowMs);
138
- const summary = (await client.call("cache.economics", { since, until })) as CacheEconomicsSummary;
164
+ const summary = await fetchCacheEconomicsSummary(client, windowMs, now);
139
165
  if (ctx.mode !== "tui") {
140
166
  ctx.ui.notify(renderCacheEconomicsView(summary).join("\n"), "info");
141
167
  return;
142
168
  }
143
- const action = await ctx.ui.custom<CacheEconomicsPanelAction>((_tui, theme, _keybindings, done) => ({
144
- invalidate() {},
145
- render(width: number): string[] {
146
- return renderCacheEconomicsPanel(summary, width, theme);
147
- },
148
- handleInput(data: string): void {
149
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) done("close");
150
- else if (data === "r") done("refresh");
151
- },
152
- }));
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
+ });
153
177
  if (!action || action === "close") return;
154
178
  }
155
179
  }
@@ -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
- async function snapshot(
330
- client: JittorPanelClient,
331
- sessionId: string,
332
- ): Promise<{ status: RouterStatus; metrics: StoredMetricObservation[] }> {
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
- const labels = routes.map(routeText);
345
- const selected = await ctx.ui.select("Override route", labels);
346
- const index = selected ? labels.indexOf(selected) : -1;
347
- return index >= 0 ? routes[index] : undefined;
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 snapshot(client, session_id);
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
- invalidate() {},
360
- render(width: number): string[] {
361
- const border = theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
362
- const controls = current.status.paused
363
- ? "r refresh · p release emergency halt · o override · c clear override · Esc close"
364
- : "r refresh · p emergency halt · o override · c clear override · Esc close";
365
- return [
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
- if (action === "refresh") {
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
- export function renderBenchmarkView(result: ModelRankingResult, currentIdentity: string, width: number, theme: BenchmarkTheme): string[] {
62
- const safeWidth = Math.max(1, width);
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
- }).render(safeWidth);
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 session_id = ctx.sessionManager.getSessionId();
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
- invalidate() {},
149
- render(width: number): string[] {
150
- return renderBenchmarkView(result, currentIdentity, width, theme);
151
- },
152
- handleInput(data: string): void {
153
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) done("close");
154
- else if (data === "r") done("refresh");
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
  }
@@ -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
- const SETTINGS_KEYS: SettingsKey[] = ["enforcement", "footer", "recovery", ...USAGE_PERIODS.map(({ id }) => `budget-${id}` as const)];
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
- function createSettingsPanel(
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
- if (action.key === "enforcement") {
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
  }