@danypops/pi-jittor 0.4.0 → 0.5.1
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 +54 -27
- package/extension/src/jittor-command.ts +110 -0
- package/extension/src/jittor-shell.ts +339 -0
- package/extension/src/observability/cache-economics-view.ts +45 -21
- package/extension/src/observability/status.ts +116 -65
- package/extension/src/optimization/model-selection-panel.ts +61 -32
- package/extension/src/settings-tui.ts +112 -34
- package/extension/src/tui-prompts.ts +110 -0
- package/package.json +2 -2
package/extension/src/index.ts
CHANGED
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
validateTaskFocusEvent,
|
|
35
35
|
} from "@danypops/jittor";
|
|
36
36
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
37
|
-
import {
|
|
37
|
+
import { jittorArgumentCompletions, jittorUsageError } from "./jittor-command.ts";
|
|
38
|
+
import { showJittorShell } from "./jittor-shell.ts";
|
|
38
39
|
import {
|
|
39
40
|
basePromptSegment,
|
|
40
41
|
buildBasePromptItems,
|
|
@@ -51,14 +52,12 @@ import { type CompactionProgress, type IntegratedFooterState, installIntegratedF
|
|
|
51
52
|
import { LocalRunTelemetry } from "./observability/model-run.ts";
|
|
52
53
|
import { captureProviderContextSnapshot } from "./observability/provider-context-snapshot.ts";
|
|
53
54
|
import { ProviderResponseTelemetry } from "./observability/provider-response.ts";
|
|
54
|
-
import { buildFooterBudget, providerBudgetMetricQuery
|
|
55
|
+
import { buildFooterBudget, providerBudgetMetricQuery } from "./observability/status.ts";
|
|
55
56
|
import { showUsagePanel } from "./observability/usage.ts";
|
|
56
|
-
import { showBenchmarkPanel } from "./optimization/model-selection-panel.ts";
|
|
57
57
|
import { CodexRecoveryCapability, type CodexRecoveryRuntime, SYSTEM_RECOVERY_RUNTIME } from "./optimization/recovery/codex.ts";
|
|
58
58
|
import { callJittor } from "./service-client.ts";
|
|
59
59
|
import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
|
|
60
60
|
import { type CodexRecoveryControl, type EnforcementControl, persistentEnforcementControl, type UsageBudgetControl } from "./settings.ts";
|
|
61
|
-
import { showSettingsPanel } from "./settings-tui.ts";
|
|
62
61
|
|
|
63
62
|
export { formatFooterStatus } from "./observability/status.ts";
|
|
64
63
|
export type { CodexRecoveryRuntime } from "./optimization/recovery/codex.ts";
|
|
@@ -574,21 +573,53 @@ export function registerJittorExtension(
|
|
|
574
573
|
|
|
575
574
|
pi.registerCommand("jittor", {
|
|
576
575
|
description: "Jittor settings, routing status, benchmarks, cache economics, and Codex recovery controls",
|
|
576
|
+
getArgumentCompletions: jittorArgumentCompletions,
|
|
577
577
|
handler: async (args, ctx) => {
|
|
578
578
|
const action = args.trim().toLowerCase();
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
579
|
+
const usageError = jittorUsageError(action);
|
|
580
|
+
if (usageError) {
|
|
581
|
+
ctx.ui.notify(usageError, "warning");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// One shared JittorShellDeps builder for every /jittor entry point below -- all four tabs
|
|
585
|
+
// (Settings/Status/Benchmarks/Cache) are reachable from a single opened shell regardless of
|
|
586
|
+
// which subcommand opened it, so every entry point needs the full dependency set, not just
|
|
587
|
+
// its own tab's. `benchmarksTask` overrides the domain/type only for an explicit
|
|
588
|
+
// "/jittor benchmarks ..." invocation; every other entry point defaults to general/general
|
|
589
|
+
// (only meaningfully exercised if the user tab-cycles into Benchmarks from elsewhere).
|
|
590
|
+
const buildShellDeps = (benchmarksTask?: {
|
|
591
|
+
domain: ModelTaskDomain;
|
|
592
|
+
type: ModelTaskType;
|
|
593
|
+
}): Parameters<typeof showJittorShell>[1] => ({
|
|
594
|
+
settings: {
|
|
595
|
+
enforcement,
|
|
596
|
+
recovery: codexRecovery,
|
|
597
|
+
budgets: usageBudgets,
|
|
598
|
+
effects: {
|
|
599
|
+
setEnforcement: async (enabled) => (enabled ? enable(ctx) : disable(ctx)),
|
|
600
|
+
setFooter: async (enabled) => {
|
|
601
|
+
await enforcement.setFooterEnabled(enabled);
|
|
602
|
+
showFooter(ctx);
|
|
603
|
+
if (enabled) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
|
|
604
|
+
},
|
|
605
|
+
setRecovery: async (enabled) => {
|
|
606
|
+
if (!enabled) cancelRecovery(true);
|
|
607
|
+
await codexRecovery.setCodexRecoveryEnabled(enabled);
|
|
608
|
+
},
|
|
590
609
|
},
|
|
591
|
-
}
|
|
610
|
+
},
|
|
611
|
+
status: { client },
|
|
612
|
+
benchmarks: {
|
|
613
|
+
client,
|
|
614
|
+
candidates: benchmarkCandidatesFromPi(scopedOrAvailableModels(ctx), pi.getThinkingLevel()),
|
|
615
|
+
currentIdentity: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "",
|
|
616
|
+
domain: benchmarksTask?.domain ?? "general",
|
|
617
|
+
type: benchmarksTask?.type ?? "general",
|
|
618
|
+
},
|
|
619
|
+
cache: { client, windowMs: 7 * MILLISECONDS_PER_DAY },
|
|
620
|
+
});
|
|
621
|
+
if (action === "" || action === "settings") {
|
|
622
|
+
await showJittorShell(ctx, buildShellDeps(), "settings");
|
|
592
623
|
return;
|
|
593
624
|
}
|
|
594
625
|
if (action === "benchmarks" || action.startsWith("benchmarks ")) {
|
|
@@ -613,19 +644,15 @@ export function registerJittorExtension(
|
|
|
613
644
|
ctx.ui.notify("Usage: /jittor benchmarks [coding|general] [research|planning|general]", "warning");
|
|
614
645
|
return;
|
|
615
646
|
}
|
|
616
|
-
|
|
617
|
-
await showBenchmarkPanel(
|
|
647
|
+
await showJittorShell(
|
|
618
648
|
ctx,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
`${ctx.model.provider}/${ctx.model.id}`,
|
|
622
|
-
requestedDomain ?? "general",
|
|
623
|
-
requestedType ?? "general",
|
|
649
|
+
buildShellDeps({ domain: requestedDomain ?? "general", type: requestedType ?? "general" }),
|
|
650
|
+
"benchmarks",
|
|
624
651
|
);
|
|
625
652
|
return;
|
|
626
653
|
}
|
|
627
654
|
if (action === "cache") {
|
|
628
|
-
await
|
|
655
|
+
await showJittorShell(ctx, buildShellDeps(), "cache");
|
|
629
656
|
return;
|
|
630
657
|
}
|
|
631
658
|
if (action === "outcome accepted" || action === "outcome rejected") {
|
|
@@ -701,13 +728,13 @@ export function registerJittorExtension(
|
|
|
701
728
|
);
|
|
702
729
|
return;
|
|
703
730
|
}
|
|
704
|
-
// Reached only for the explicit "status" keyword
|
|
705
|
-
//
|
|
731
|
+
// Reached only for the explicit "status" keyword -- jittorUsageError above already rejected
|
|
732
|
+
// anything else this handler doesn't recognize, so this is never an unrecognized fallback.
|
|
706
733
|
if (!enforcement.isEnabled()) {
|
|
707
734
|
ctx.ui.notify("Jittor is monitor-only. Run /jittor on to re-enable blocking.", "info");
|
|
708
735
|
return;
|
|
709
736
|
}
|
|
710
|
-
await
|
|
737
|
+
await showJittorShell(ctx, buildShellDeps(), "status");
|
|
711
738
|
},
|
|
712
739
|
});
|
|
713
740
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full argument grammar for /jittor, as one small data table -- the single source of truth
|
|
3
|
+
* both jittorUsageError (validation) and jittorArgumentCompletions (tab-completion) read from, so
|
|
4
|
+
* the two can never drift apart (a phrase added to one is automatically valid/completable in the
|
|
5
|
+
* other). /jittor's own registerCommand handler in index.ts owns dispatching a validated phrase to
|
|
6
|
+
* its real behavior; this module only ever answers "is this a real phrase" and "what could this
|
|
7
|
+
* become".
|
|
8
|
+
*
|
|
9
|
+
* `benchmarks` is deliberately excluded from LEAF_PHRASES: its own domain/type combinatorics
|
|
10
|
+
* (TASK_DOMAINS x TASK_TYPES, either order) already have a dedicated, tested malformed-usage
|
|
11
|
+
* branch in index.ts with its own specific error message -- this module defers to it entirely
|
|
12
|
+
* for validation, and only mirrors its real phrase space for completions.
|
|
13
|
+
*/
|
|
14
|
+
import { TASK_DOMAINS, TASK_TYPES } from "@danypops/jittor";
|
|
15
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
16
|
+
|
|
17
|
+
export const JITTOR_TOP_LEVEL_COMMANDS = [
|
|
18
|
+
"settings",
|
|
19
|
+
"status",
|
|
20
|
+
"benchmarks",
|
|
21
|
+
"cache",
|
|
22
|
+
"outcome",
|
|
23
|
+
"recovery",
|
|
24
|
+
"on",
|
|
25
|
+
"off",
|
|
26
|
+
"footer",
|
|
27
|
+
"context",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
/** Every real, currently-supported phrase EXCEPT benchmarks (see module doc comment). */
|
|
31
|
+
const LEAF_PHRASES = [
|
|
32
|
+
"",
|
|
33
|
+
"settings",
|
|
34
|
+
"status",
|
|
35
|
+
"cache",
|
|
36
|
+
"context",
|
|
37
|
+
"on",
|
|
38
|
+
"enable",
|
|
39
|
+
"off",
|
|
40
|
+
"disable",
|
|
41
|
+
"outcome accepted",
|
|
42
|
+
"outcome rejected",
|
|
43
|
+
"recovery",
|
|
44
|
+
"recovery status",
|
|
45
|
+
"recovery on",
|
|
46
|
+
"recovery enable",
|
|
47
|
+
"recovery off",
|
|
48
|
+
"recovery disable",
|
|
49
|
+
"recovery cancel",
|
|
50
|
+
"footer on",
|
|
51
|
+
"footer enable",
|
|
52
|
+
"footer off",
|
|
53
|
+
"footer disable",
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
/** Real benchmarks phrases: bare, one axis alone, or both axes in either accepted word order -- mirrors index.ts's own lenient two-word-any-order parsing, for completions only. */
|
|
57
|
+
function benchmarksPhrases(): string[] {
|
|
58
|
+
const phrases: string[] = ["benchmarks"];
|
|
59
|
+
for (const domain of TASK_DOMAINS) phrases.push(`benchmarks ${domain}`);
|
|
60
|
+
for (const type of TASK_TYPES) phrases.push(`benchmarks ${type}`);
|
|
61
|
+
for (const domain of TASK_DOMAINS) {
|
|
62
|
+
for (const type of TASK_TYPES) {
|
|
63
|
+
phrases.push(`benchmarks ${domain} ${type}`, `benchmarks ${type} ${domain}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return phrases;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* null when `action` (already trimmed/lowercased by the caller, matching index.ts's own
|
|
71
|
+
* convention) is a real, currently-supported phrase; otherwise a human-readable message naming
|
|
72
|
+
* exactly what was wrong and what the real alternatives are -- never a silent fallback to
|
|
73
|
+
* unrelated behavior (the bug class this replaces: an unrecognized word, or a valid command with
|
|
74
|
+
* an unrecognized trailing argument, used to silently open the status panel instead).
|
|
75
|
+
*/
|
|
76
|
+
export function jittorUsageError(action: string): string | null {
|
|
77
|
+
if ((LEAF_PHRASES as readonly string[]).includes(action)) return null;
|
|
78
|
+
if (action === "benchmarks" || action.startsWith("benchmarks ")) return null;
|
|
79
|
+
|
|
80
|
+
const words = action.split(/\s+/).filter(Boolean);
|
|
81
|
+
const firstWord = words[0]!;
|
|
82
|
+
if (!(JITTOR_TOP_LEVEL_COMMANDS as readonly string[]).includes(firstWord)) {
|
|
83
|
+
return `Unknown /jittor command "${firstWord}". Allowed: ${JITTOR_TOP_LEVEL_COMMANDS.join(", ")}.`;
|
|
84
|
+
}
|
|
85
|
+
const subPhrases = LEAF_PHRASES.filter((phrase) => phrase.startsWith(`${firstWord} `));
|
|
86
|
+
if (subPhrases.length === 0) return `/jittor ${firstWord} does not take any arguments.`;
|
|
87
|
+
const subArguments = subPhrases.map((phrase) => phrase.slice(firstWord.length + 1));
|
|
88
|
+
return `Unknown /jittor ${firstWord} argument "${words.slice(1).join(" ")}". Allowed: ${subArguments.join(", ")}.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let cachedCompletionPhrases: string[] | undefined;
|
|
92
|
+
function completionPhrases(): string[] {
|
|
93
|
+
if (!cachedCompletionPhrases) cachedCompletionPhrases = [...LEAF_PHRASES.filter((phrase) => phrase.length > 0), ...benchmarksPhrases()];
|
|
94
|
+
return cachedCompletionPhrases;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Every real phrase (see completionPhrases) that could still result from typing more after
|
|
99
|
+
* `argumentPrefix`, matching pi-tui's own SlashCommand.getArgumentCompletions contract: `value` is
|
|
100
|
+
* the FULL replacement for the entire argument text typed so far (pi-tui's own applyCompletion
|
|
101
|
+
* replaces the whole prefix span with it, not just the trailing word), so every candidate here is a
|
|
102
|
+
* complete phrase, not a per-word delta. Matching is case-insensitive on the typed prefix (a human
|
|
103
|
+
* may capitalize while typing); returned values are always canonical lowercase.
|
|
104
|
+
*/
|
|
105
|
+
export function jittorArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
106
|
+
const needle = argumentPrefix.toLowerCase();
|
|
107
|
+
const matches = completionPhrases().filter((phrase) => phrase.startsWith(needle));
|
|
108
|
+
if (matches.length === 0) return null;
|
|
109
|
+
return matches.map((value) => ({ value, label: value }));
|
|
110
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
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
|
+
measure: hostTextMeasure,
|
|
278
|
+
onChange: (key) => {
|
|
279
|
+
activeKey = key as JittorTabKey;
|
|
280
|
+
done({ kind: "tab-changed" });
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
const outer = new BorderedSelectPanel({
|
|
284
|
+
title: "Jittor",
|
|
285
|
+
list: container,
|
|
286
|
+
helpText: "Tab/Shift+Tab switch tabs \u00b7 Esc close",
|
|
287
|
+
theme: {
|
|
288
|
+
border: (text) => theme.fg("borderMuted", text),
|
|
289
|
+
title: theme.bold,
|
|
290
|
+
help: (text) => theme.fg("dim", text),
|
|
291
|
+
},
|
|
292
|
+
measure: hostTextMeasure,
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
invalidate: () => outer.invalidate(),
|
|
296
|
+
render: (width: number) => outer.render(width),
|
|
297
|
+
handleInput(data: string): void {
|
|
298
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
299
|
+
done({ kind: "close" });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
outer.handleInput(data);
|
|
303
|
+
tui.requestRender();
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
if (!outcome || outcome.kind === "close") return;
|
|
308
|
+
if (outcome.kind === "tab-changed") continue;
|
|
309
|
+
if (outcome.kind === "settings") {
|
|
310
|
+
await runSettingsAction(
|
|
311
|
+
ctx,
|
|
312
|
+
outcome.action,
|
|
313
|
+
deps.settings.enforcement,
|
|
314
|
+
deps.settings.recovery,
|
|
315
|
+
deps.settings.budgets,
|
|
316
|
+
deps.settings.effects,
|
|
317
|
+
);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (outcome.kind === "status") {
|
|
321
|
+
await runStatusAction(
|
|
322
|
+
ctx,
|
|
323
|
+
deps.status.client,
|
|
324
|
+
outcome.action,
|
|
325
|
+
state.status as StatusPanelSnapshot,
|
|
326
|
+
ctx.sessionManager.getSessionId(),
|
|
327
|
+
);
|
|
328
|
+
state.status = undefined;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (outcome.kind === "benchmarks") {
|
|
332
|
+
if (outcome.action === "refresh") await deps.benchmarks.client.call("benchmark.refresh", { force: true });
|
|
333
|
+
state.benchmarks = undefined;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
// "cache": refresh has no separate daemon mutation -- looping back to refetch is the whole effect.
|
|
337
|
+
state.cache = undefined;
|
|
338
|
+
}
|
|
339
|
+
}
|