@danypops/jittor 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +28 -77
  2. package/package.json +11 -14
  3. package/src/adapters/sqlite-metric-store.ts +11 -2
  4. package/src/adapters/sqlite-session-identity-store.ts +45 -0
  5. package/src/cli-commands/benchmarks.ts +140 -0
  6. package/src/cli-commands/compaction.ts +17 -0
  7. package/src/cli-commands/context.ts +49 -0
  8. package/src/cli-commands/metrics.ts +296 -0
  9. package/src/cli-commands/op.ts +40 -0
  10. package/src/cli-commands/route-args.ts +15 -0
  11. package/src/cli-commands/router.ts +207 -0
  12. package/src/cli-commands/service-daemon.ts +72 -0
  13. package/src/cli-commands/session.ts +42 -0
  14. package/src/cli-commands/support.ts +33 -0
  15. package/src/cli.ts +42 -769
  16. package/src/constants.ts +7 -0
  17. package/src/daemon.ts +13 -3
  18. package/src/db.ts +15 -1
  19. package/src/index.ts +137 -0
  20. package/src/operations/benchmark-operations.ts +12 -0
  21. package/src/operations/context-operations.ts +30 -0
  22. package/src/operations/metrics-operations.ts +77 -0
  23. package/src/operations/model-ranking-operations.ts +16 -0
  24. package/src/operations/router-operations.ts +19 -0
  25. package/src/operations/session-identity-operations.ts +15 -0
  26. package/src/operations/session-scope.ts +31 -0
  27. package/src/operations/types.ts +3 -0
  28. package/src/ports/metric-store.ts +2 -0
  29. package/src/ports/router-controller.ts +9 -9
  30. package/src/ports/session-identity-store.ts +5 -0
  31. package/src/providers/telemetry-sources.ts +2 -1
  32. package/src/router.ts +124 -67
  33. package/src/service.ts +60 -118
  34. package/src/session-identity-service.ts +55 -0
  35. package/docs/USAGE_PRIOR_ART.md +0 -64
  36. package/extension/src/benchmark-tui.ts +0 -105
  37. package/extension/src/footer.ts +0 -366
  38. package/extension/src/index.ts +0 -828
  39. package/extension/src/service-client.ts +0 -26
  40. package/extension/src/settings-tui.ts +0 -153
  41. package/extension/src/settings.ts +0 -103
  42. package/extension/src/tui.ts +0 -270
  43. package/extension/src/usage.ts +0 -320
@@ -1,105 +0,0 @@
1
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
3
- import {
4
- BENCHMARK_TUI_MAX_CANDIDATES,
5
- BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE,
6
- MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
7
- MODEL_RANKING_DEFAULT_COST_WEIGHT,
8
- MODEL_RANKING_DEFAULT_LATENCY_WEIGHT,
9
- MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
10
- MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
11
- } from "../../src/constants.ts";
12
- import type { ModelTaskDomain, ModelTaskType } from "../../src/domain/model-observation.ts";
13
- import type { ModelCandidate, ModelRankingResult, RankedModel, UtilityComponentName } from "../../src/domain/model-ranking.ts";
14
-
15
- export interface BenchmarkPanelClient {
16
- call(operation: string, input: unknown): Promise<any>;
17
- }
18
-
19
- interface BenchmarkTheme {
20
- fg(color: string, text: string): string;
21
- bold(text: string): string;
22
- }
23
-
24
- type BenchmarkPanelAction = "refresh" | "close";
25
-
26
- const COMPONENT_LABELS: Record<UtilityComponentName, string> = { quality: "Q", cost: "$", latency: "L", context: "C", reliability: "R" };
27
-
28
- function componentText(item: RankedModel): string {
29
- return item.components.map((component) => `${COMPONENT_LABELS[component.name]} ${component.score === null ? "?" : component.score.toFixed(3)}`).join(" · ");
30
- }
31
-
32
- function candidateLines(item: RankedModel, index: number, currentIdentity: string): string[] {
33
- const current = item.identity.startsWith(`${currentIdentity}:`);
34
- const localSamples = item.components.find((component) => component.name === "reliability")?.evidenceCount ?? 0;
35
- const provenance = item.provenance.slice(0, BENCHMARK_TUI_MAX_PROVENANCE_PER_CANDIDATE).map((source) => `${source.sourceId}@${source.revision} ${source.freshness}`).join(" · ");
36
- return [
37
- ` ${index + 1}. ${item.identity}${index === 0 ? " recommended" : ""}${current ? " current" : ""}`,
38
- ` utility ${item.utility === null ? "?" : item.utility.toFixed(3)} · confidence ${(item.confidence * 100).toFixed(0)}% · ${componentText(item)}`,
39
- ` local n=${localSamples}${provenance ? ` · ${provenance}` : " · no external provenance"}`,
40
- ];
41
- }
42
-
43
- export function renderBenchmarkView(result: ModelRankingResult, currentIdentity: string, width: number, theme: BenchmarkTheme): string[] {
44
- const safeWidth = Math.max(1, width);
45
- const shown = result.ranked.slice(0, BENCHMARK_TUI_MAX_CANDIDATES);
46
- const currentIndex = result.ranked.findIndex((item) => item.identity.startsWith(`${currentIdentity}:`));
47
- const recommended = result.ranked[0];
48
- const reason = recommended && currentIndex > 0
49
- ? `Recommendation differs from current: ${recommended.identity} ranks #1; current ranks #${currentIndex + 1}.`
50
- : recommended && currentIndex === 0 ? "Current model is the top recommendation." : "Current model is outside the ranked candidates.";
51
- const lines = [
52
- theme.fg("borderMuted", "─".repeat(safeWidth)),
53
- theme.bold("Jittor Benchmark Recommendations"),
54
- result.scopeAuthority === "exact-session" ? "Scope: exact session" : "Scope: available models · ADVISORY (exact session scope unavailable)",
55
- `Domain: ${result.domain} · Type: ${result.type} · evidence ${result.completeness}`,
56
- reason,
57
- ...shown.flatMap((item, index) => candidateLines(item, index, currentIdentity)),
58
- ...(result.ranked.length > shown.length ? [` … ${result.ranked.length - shown.length} more candidates omitted`] : []),
59
- ...(result.scopeWarning ? [result.scopeWarning] : []),
60
- theme.fg("dim", "r refresh · Esc close"),
61
- theme.fg("borderMuted", "─".repeat(safeWidth)),
62
- ];
63
- return lines.map((line) => truncateToWidth(line, safeWidth, "…"));
64
- }
65
-
66
- export async function showBenchmarkPanel(
67
- ctx: ExtensionCommandContext,
68
- client: BenchmarkPanelClient,
69
- candidates: ModelCandidate[],
70
- currentIdentity: string,
71
- domain: ModelTaskDomain,
72
- type: ModelTaskType,
73
- ): Promise<void> {
74
- for (;;) {
75
- const result = await client.call("models.rank", {
76
- candidates,
77
- scopeAuthority: "available-models",
78
- domain,
79
- type,
80
- budgetPressure: 0,
81
- weights: {
82
- quality: MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
83
- cost: MODEL_RANKING_DEFAULT_COST_WEIGHT,
84
- latency: MODEL_RANKING_DEFAULT_LATENCY_WEIGHT,
85
- context: MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
86
- reliability: MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
87
- },
88
- sourceIds: ["openrouter-models", "lmarena-hf", "artificial-analysis-direct", "openrouter-design-arena"],
89
- }) as ModelRankingResult;
90
- if (ctx.mode !== "tui") {
91
- ctx.ui.notify(renderBenchmarkView(result, currentIdentity, 100, { fg: (_color, text) => text, bold: (text) => text }).join("\n"), "info");
92
- return;
93
- }
94
- const action = await ctx.ui.custom<BenchmarkPanelAction>((_tui, theme, _keybindings, done) => ({
95
- invalidate() {},
96
- render(width: number): string[] { return renderBenchmarkView(result, currentIdentity, width, theme); },
97
- handleInput(data: string): void {
98
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) done("close");
99
- else if (data === "r") done("refresh");
100
- },
101
- }));
102
- if (!action || action === "close") return;
103
- await client.call("benchmark.refresh", { force: true });
104
- }
105
- }
@@ -1,366 +0,0 @@
1
- import { isAbsolute, relative, resolve, sep } from "node:path";
2
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
- import {
5
- FOOTER_BAR_MAX_WIDTH,
6
- FOOTER_BAR_MIN_WIDTH,
7
- FOOTER_CONTEXT_ACCENT_FRACTION,
8
- FOOTER_CONTEXT_ERROR_FRACTION,
9
- FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS,
10
- FOOTER_CONTEXT_WARNING_FRACTION,
11
- FOOTER_WIDE_TERMINAL_WIDTH,
12
- MILLISECONDS_PER_DAY,
13
- MILLISECONDS_PER_HOUR,
14
- MILLISECONDS_PER_MINUTE,
15
- MILLISECONDS_PER_SECOND,
16
- TELEMETRY_STALE_AFTER_MS,
17
- } from "../../src/constants.ts";
18
-
19
- type FooterColor = "accent" | "dim" | "warning" | "error";
20
-
21
- interface FooterTheme {
22
- fg(color: FooterColor, text: string): string;
23
- bold(text: string): string;
24
- }
25
-
26
- interface FooterData {
27
- getGitBranch(): string | null | undefined;
28
- getAvailableProviderCount(): number;
29
- getExtensionStatuses(): ReadonlyMap<string, string>;
30
- onBranchChange?(callback: () => void): () => void;
31
- }
32
-
33
- interface ContextUsage {
34
- tokens: number | null;
35
- percent: number | null;
36
- contextWindow: number;
37
- }
38
-
39
- interface FooterContext {
40
- model?: { provider: string; id: string; reasoning?: boolean; contextWindow?: number };
41
- modelRegistry: { isUsingOAuth(model: unknown): boolean };
42
- getContextUsage(): ContextUsage | undefined;
43
- sessionManager: {
44
- getCwd(): string;
45
- getSessionName(): string | undefined;
46
- getEntries(): Array<{ type: string; message?: any }>;
47
- };
48
- }
49
-
50
- /** A bounded quota is explicitly remaining; unbounded values never receive a fabricated bar. */
51
- export type ProviderBudget = {
52
- kind: "bounded";
53
- label: string;
54
- remainingFraction: number;
55
- observedAt?: number;
56
- resetsAt?: number;
57
- resetText?: string;
58
- } | {
59
- kind: "unbounded";
60
- label: string;
61
- valueText: string;
62
- observedAt?: number;
63
- };
64
-
65
- export interface CompactionProgress {
66
- startedAt: number;
67
- initialFraction: number;
68
- /** Learned median duration from jittor-cli's `compaction.estimate`; absent/null means cold-start. */
69
- estimatedMs?: number | null;
70
- confidence?: "cold-start" | "learned";
71
- }
72
-
73
- interface UsageTotals {
74
- input: number;
75
- output: number;
76
- cacheRead: number;
77
- cacheWrite: number;
78
- cost: number;
79
- cacheHit?: number;
80
- }
81
-
82
- function formatTokens(count: number): string {
83
- if (count < 1_000) return count.toString();
84
- if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`;
85
- if (count < 1_000_000) return `${Math.round(count / 1_000)}k`;
86
- if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
87
- return `${Math.round(count / 1_000_000)}M`;
88
- }
89
-
90
- function footerCwd(cwd: string, home: string | undefined): string {
91
- if (!home) return cwd;
92
- const resolvedCwd = resolve(cwd);
93
- const resolvedHome = resolve(home);
94
- const relativeToHome = relative(resolvedHome, resolvedCwd);
95
- const inside = relativeToHome === "" || (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
96
- if (!inside) return cwd;
97
- return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
98
- }
99
-
100
- function sanitize(value: string): string {
101
- return value.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
102
- }
103
-
104
- function usageTotals(context: FooterContext): UsageTotals {
105
- let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0, cacheHit: number | undefined;
106
- for (const entry of context.sessionManager.getEntries()) {
107
- if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
108
- const usage = entry.message.usage;
109
- input += usage?.input ?? 0;
110
- output += usage?.output ?? 0;
111
- cacheRead += usage?.cacheRead ?? 0;
112
- cacheWrite += usage?.cacheWrite ?? 0;
113
- cost += usage?.cost?.total ?? 0;
114
- const prompt = (usage?.input ?? 0) + (usage?.cacheRead ?? 0) + (usage?.cacheWrite ?? 0);
115
- if (prompt > 0) cacheHit = (usage.cacheRead ?? 0) / prompt * 100;
116
- }
117
- return { input, output, cacheRead, cacheWrite, cost, ...(cacheHit === undefined ? {} : { cacheHit }) };
118
- }
119
-
120
- function barWidth(width: number): number {
121
- return width >= FOOTER_WIDE_TERMINAL_WIDTH ? FOOTER_BAR_MAX_WIDTH : FOOTER_BAR_MIN_WIDTH;
122
- }
123
-
124
- function progressBar(fraction: number | null, width: number): string {
125
- if (fraction === null || !Number.isFinite(fraction)) return "░".repeat(width);
126
- const clamped = Math.min(1, Math.max(0, fraction));
127
- const filled = Math.round(width * clamped);
128
- return "█".repeat(filled) + "░".repeat(width - filled);
129
- }
130
-
131
- function fillColor(fraction: number | null): FooterColor {
132
- if (fraction === null || !Number.isFinite(fraction)) return "dim";
133
- if (fraction > FOOTER_CONTEXT_ERROR_FRACTION) return "error";
134
- if (fraction > FOOTER_CONTEXT_WARNING_FRACTION) return "warning";
135
- if (fraction > FOOTER_CONTEXT_ACCENT_FRACTION) return "accent";
136
- return "dim";
137
- }
138
-
139
- /**
140
- * Once a learned median duration is available (see estimateCompactionDuration / the
141
- * `compaction.estimate` daemon operation), the bar drains against that real estimate: fraction
142
- * counts down linearly from 1 to 0 over estimatedMs, exactly in step with the countdown shown in
143
- * compactionStatusText — same elapsed/estimatedMs ratio drives both. Until then — cold start, or
144
- * the estimate fetch has not resolved yet — there is no real duration to drain against, so the
145
- * fill holds steady at the fraction observed when compaction started; the blink alone (see
146
- * compactionBarGlyph) communicates liveness without fabricating a rate.
147
- */
148
- function compactionFraction(progress: CompactionProgress, width: number, now: number): number {
149
- if (progress.confidence === "learned" && typeof progress.estimatedMs === "number" && progress.estimatedMs > 0) {
150
- const elapsed = Math.max(0, now - progress.startedAt);
151
- return Math.max(0, Math.min(1, 1 - (elapsed / progress.estimatedMs)));
152
- }
153
- return Math.min(1, Math.max(0, progress.initialFraction));
154
- }
155
-
156
- /**
157
- * Liveness blink independent of whether the drain bar reflects a learned estimate or the
158
- * fixed-rate cold-start fallback: it does not claim to know how long compaction will take, only
159
- * that it has not stalled. It toggles once per render tick so a single owned interval (installed
160
- * in beginCompactionUi) drives both the drain and the blink — no extra timer is created here.
161
- */
162
- export function compactionBlinkOn(startedAt: number, now: number, halfPeriodMs = FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS): boolean {
163
- const elapsed = Math.max(0, now - startedAt);
164
- return Math.floor(elapsed / halfPeriodMs) % 2 === 0;
165
- }
166
-
167
- /**
168
- * The compaction signal lives in the bar itself: it blinks between its normal draining fill and a
169
- * blank track of the same width, rather than a separate indicator glyph next to it. Off-phase
170
- * intentionally renders identically to the "no data" empty track (dim, all "░") so the bar reads
171
- * as a single blinking element, not a bar plus a decoration.
172
- */
173
- function compactionBarGlyph(progress: CompactionProgress, theme: FooterTheme, width: number, now: number): string {
174
- if (!compactionBlinkOn(progress.startedAt, now)) return theme.fg("dim", "░".repeat(width));
175
- const fraction = compactionFraction(progress, width, now);
176
- return theme.fg("accent", progressBar(fraction, width));
177
- }
178
-
179
- /**
180
- * A countdown, never a count-up: once a learned estimate exists it reports seconds remaining,
181
- * ticking down toward zero in step with the draining bar. Before that (cold start, no estimate
182
- * yet) there is nothing true to count down from, so this reports nothing at all rather than a
183
- * fabricated elapsed count or a guessed total — the blinking, non-draining bar is the only signal.
184
- */
185
- function compactionStatusText(progress: CompactionProgress, now: number): string | undefined {
186
- if (progress.confidence === "learned" && typeof progress.estimatedMs === "number" && progress.estimatedMs > 0) {
187
- const remainingSeconds = Math.max(0, Math.ceil((progress.estimatedMs - (now - progress.startedAt)) / MILLISECONDS_PER_SECOND));
188
- return `compact ~${remainingSeconds}s left`;
189
- }
190
- return undefined;
191
- }
192
-
193
- function contextSegment(
194
- context: FooterContext,
195
- theme: FooterTheme,
196
- width: number,
197
- compact: boolean,
198
- now: number,
199
- compaction?: CompactionProgress,
200
- ): string {
201
- const w = barWidth(width);
202
- if (compaction) {
203
- const bar = compactionBarGlyph(compaction, theme, w, now);
204
- const statusText = compactionStatusText(compaction, now);
205
- return statusText === undefined ? `ctx ${bar}` : `ctx ${bar} ${statusText}`;
206
- }
207
- const usage = context.getContextUsage();
208
- const window = usage?.contextWindow ?? context.model?.contextWindow ?? 0;
209
- const fraction = usage?.percent === null || usage?.percent === undefined ? null : usage.percent / 100;
210
- const bar = theme.fg(fillColor(fraction), progressBar(fraction, w));
211
- if (usage?.tokens === null || usage?.tokens === undefined) return `ctx ${bar} ?/${formatTokens(window)}`;
212
- const value = compact ? `${Math.round((fraction ?? 0) * 100)}%` : `${formatTokens(usage.tokens)}/${formatTokens(window)}`;
213
- return `ctx ${bar} ${value}`;
214
- }
215
-
216
- function minimalContextSegment(
217
- context: FooterContext,
218
- theme: FooterTheme,
219
- width: number,
220
- now: number,
221
- compaction?: CompactionProgress,
222
- ): string {
223
- const w = barWidth(width);
224
- if (compaction) {
225
- return `ctx ${compactionBarGlyph(compaction, theme, w, now)}`;
226
- }
227
- const percent = context.getContextUsage()?.percent;
228
- const fraction = percent === null || percent === undefined ? null : percent / 100;
229
- return `ctx ${theme.fg(fillColor(fraction), progressBar(fraction, w))}`;
230
- }
231
-
232
- function resetLabel(resetsAt: number | undefined, now: number): string | undefined {
233
- if (resetsAt === undefined) return undefined;
234
- const remaining = resetsAt - now;
235
- if (remaining <= 0) return "reset due";
236
- if (remaining >= MILLISECONDS_PER_DAY) return `resets in ${Math.floor(remaining / MILLISECONDS_PER_DAY)}d`;
237
- if (remaining >= MILLISECONDS_PER_HOUR) return `resets in ${Math.floor(remaining / MILLISECONDS_PER_HOUR)}h`;
238
- return `resets in ${Math.max(1, Math.ceil(remaining / MILLISECONDS_PER_MINUTE))}m`;
239
- }
240
-
241
- /**
242
- * `undefined` means no budget signal is possible for this provider at all (see buildFooterBudget);
243
- * the segment is omitted entirely rather than showing a placeholder that could never resolve.
244
- * `null` means not known yet but might resolve, which still earns the `?` placeholder.
245
- */
246
- function budgetSegment(budget: ProviderBudget | null | undefined, theme: FooterTheme, width: number, compact: boolean, now: number): string | undefined {
247
- if (budget === undefined) return undefined;
248
- const w = barWidth(width);
249
- if (!budget) return `budget ${theme.fg("dim", progressBar(null, w))} ?`;
250
- const stale = budget.observedAt !== undefined && now - budget.observedAt > TELEMETRY_STALE_AFTER_MS;
251
- const staleText = stale ? ` ${theme.fg("warning", "stale")}` : "";
252
- if (budget.kind === "unbounded") return `${budget.label} ${budget.valueText}${staleText}`;
253
- const remaining = Math.min(1, Math.max(0, budget.remainingFraction));
254
- const bar = theme.fg(fillColor(1 - remaining), progressBar(remaining, w));
255
- const value = `${(compact ? Math.round(remaining * 100) : (remaining * 100).toFixed(1))}% left`;
256
- const reset = compact ? undefined : resetLabel(budget.resetsAt, now) ?? budget.resetText;
257
- return `${budget.label} ${bar} ${value}${reset ? ` · ${reset}` : ""}${staleText}`;
258
- }
259
-
260
- function usageSegment(context: FooterContext): string {
261
- const totals = usageTotals(context);
262
- const parts: string[] = [];
263
- if (totals.input) parts.push(`↑${formatTokens(totals.input)}`);
264
- if (totals.output) parts.push(`↓${formatTokens(totals.output)}`);
265
- if (totals.cacheRead) parts.push(`R${formatTokens(totals.cacheRead)}`);
266
- if (totals.cacheWrite) parts.push(`W${formatTokens(totals.cacheWrite)}`);
267
- if ((totals.cacheRead || totals.cacheWrite) && totals.cacheHit !== undefined) parts.push(`CH${totals.cacheHit.toFixed(1)}%`);
268
- if (totals.cost || (context.model && context.modelRegistry.isUsingOAuth(context.model))) {
269
- parts.push(`$${totals.cost.toFixed(3)}${context.model && context.modelRegistry.isUsingOAuth(context.model) ? " (sub)" : ""}`);
270
- }
271
- return parts.join(" ");
272
- }
273
-
274
- function repositorySegment(context: FooterContext, footerData: FooterData, theme: FooterTheme): string {
275
- let cwd = footerCwd(context.sessionManager.getCwd(), process.env.HOME ?? process.env.USERPROFILE);
276
- const branch = footerData.getGitBranch();
277
- if (branch) cwd += ` (${branch})`;
278
- const sessionName = context.sessionManager.getSessionName();
279
- if (sessionName) cwd += ` · ${sessionName}`;
280
- return theme.fg("dim", cwd);
281
- }
282
-
283
- function modelSegments(context: FooterContext, footerData: FooterData, theme: FooterTheme, thinkingLevel: string): { full: string; compact: string } {
284
- const model = context.model;
285
- const modelName = theme.bold(model?.id ?? "no-model");
286
- const provider = model && footerData.getAvailableProviderCount() > 1 ? `(${model.provider}) ` : "";
287
- const thinking = model?.reasoning ? ` · ${thinkingLevel === "off" ? "thinking off" : thinkingLevel}` : "";
288
- return { full: `${provider}${modelName}${thinking}`, compact: modelName };
289
- }
290
-
291
- function compactUsageSegment(context: FooterContext): string {
292
- const totals = usageTotals(context);
293
- const parts: string[] = [];
294
- if (totals.input) parts.push(`↑${formatTokens(totals.input)}`);
295
- if (totals.output) parts.push(`↓${formatTokens(totals.output)}`);
296
- return parts.join(" ");
297
- }
298
-
299
- function joinSegments(segments: Array<string | undefined>): string {
300
- return segments.filter((segment): segment is string => Boolean(segment)).join(" · ");
301
- }
302
-
303
- export function renderFooterLines(
304
- context: FooterContext,
305
- footerData: FooterData,
306
- theme: FooterTheme,
307
- providerBudget: ProviderBudget | null | undefined,
308
- thinkingLevel: string,
309
- width: number,
310
- now = Date.now(),
311
- compaction?: CompactionProgress,
312
- ): string[] {
313
- const safeWidth = Math.max(1, width);
314
- const repository = repositorySegment(context, footerData, theme);
315
- const model = modelSegments(context, footerData, theme, thinkingLevel);
316
- const usage = usageSegment(context);
317
- const compactUsage = compactUsageSegment(context);
318
- const fullContext = contextSegment(context, theme, safeWidth, false, now, compaction);
319
- const compactContext = contextSegment(context, theme, safeWidth, true, now, compaction);
320
- const minimalContext = minimalContextSegment(context, theme, safeWidth, now, compaction);
321
- const fullBudget = budgetSegment(providerBudget, theme, safeWidth, false, now);
322
- const compactBudget = budgetSegment(providerBudget, theme, safeWidth, true, now);
323
- const statuses = [...footerData.getExtensionStatuses().entries()]
324
- .filter(([key]) => key !== "jittor")
325
- .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
326
- .map(([, text]) => sanitize(text))
327
- .join(" ");
328
-
329
- const candidates = [
330
- joinSegments([repository, model.full, usage, fullContext, fullBudget, statuses]),
331
- joinSegments([repository, model.full, usage, fullContext, fullBudget]),
332
- joinSegments([model.full, usage, compactContext, compactBudget, statuses]),
333
- joinSegments([model.full, usage, compactContext, compactBudget]),
334
- joinSegments([model.full, compactUsage, compactContext, compactBudget]),
335
- joinSegments([model.compact, compactUsage, compactContext, compactBudget]),
336
- joinSegments([model.compact, compactContext, compactBudget]),
337
- joinSegments([model.compact, minimalContext, compactBudget]),
338
- ];
339
- const line = candidates.find((candidate) => visibleWidth(candidate) <= safeWidth) ?? candidates.at(-1) ?? "";
340
- return [truncateToWidth(line, safeWidth, "")];
341
- }
342
-
343
- export interface IntegratedFooterState {
344
- providerBudget: ProviderBudget | null | undefined;
345
- compaction?: CompactionProgress;
346
- requestRender?: () => void;
347
- }
348
-
349
- export function installIntegratedFooter(ctx: ExtensionContext, state: IntegratedFooterState, getThinkingLevel: () => string): void {
350
- ctx.ui.setStatus("jittor", undefined);
351
- ctx.ui.setFooter((tui, theme, footerData) => {
352
- state.requestRender = () => tui.requestRender();
353
- const unsubscribe = (footerData as FooterData).onBranchChange?.(() => tui.requestRender());
354
- return {
355
- invalidate() {},
356
- render(width: number): string[] {
357
- return renderFooterLines(ctx as unknown as FooterContext, footerData, theme, state.providerBudget, getThinkingLevel(), width, Date.now(), state.compaction);
358
- },
359
- dispose() {
360
- unsubscribe?.();
361
- state.requestRender = undefined;
362
- tui.requestRender();
363
- },
364
- };
365
- });
366
- }