@danypops/pi-jittor 0.1.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.
@@ -0,0 +1,323 @@
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
+ TELEMETRY_STALE_AFTER_MS,
16
+ } from "@danypops/jittor";
17
+
18
+ type FooterColor = "accent" | "dim" | "warning" | "error";
19
+
20
+ interface FooterTheme {
21
+ fg(color: FooterColor, text: string): string;
22
+ bold(text: string): string;
23
+ }
24
+
25
+ interface FooterData {
26
+ getGitBranch(): string | null | undefined;
27
+ getAvailableProviderCount(): number;
28
+ getExtensionStatuses(): ReadonlyMap<string, string>;
29
+ onBranchChange?(callback: () => void): () => void;
30
+ }
31
+
32
+ interface ContextUsage {
33
+ tokens: number | null;
34
+ percent: number | null;
35
+ contextWindow: number;
36
+ }
37
+
38
+ interface FooterContext {
39
+ model?: { provider: string; id: string; reasoning?: boolean; contextWindow?: number };
40
+ modelRegistry: { isUsingOAuth(model: unknown): boolean };
41
+ getContextUsage(): ContextUsage | undefined;
42
+ sessionManager: {
43
+ getCwd(): string;
44
+ getSessionName(): string | undefined;
45
+ getEntries(): Array<{ type: string; message?: any }>;
46
+ };
47
+ }
48
+
49
+ /** A bounded quota is explicitly remaining; unbounded values never receive a fabricated bar. */
50
+ export type ProviderBudget = {
51
+ kind: "bounded";
52
+ label: string;
53
+ remainingFraction: number;
54
+ observedAt?: number;
55
+ resetsAt?: number;
56
+ resetText?: string;
57
+ } | {
58
+ kind: "unbounded";
59
+ label: string;
60
+ valueText: string;
61
+ observedAt?: number;
62
+ };
63
+
64
+ export interface CompactionProgress {
65
+ startedAt: number;
66
+ initialFraction: number;
67
+ /** Learned median duration from jittor-cli's `compaction.estimate`; absent/null means cold-start. */
68
+ estimatedMs?: number | null;
69
+ confidence?: "cold-start" | "learned";
70
+ }
71
+
72
+ interface UsageTotals {
73
+ input: number;
74
+ output: number;
75
+ cacheRead: number;
76
+ cacheWrite: number;
77
+ cost: number;
78
+ cacheHit?: number;
79
+ }
80
+
81
+ function formatTokens(count: number): string {
82
+ if (count < 1_000) return count.toString();
83
+ if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`;
84
+ if (count < 1_000_000) return `${Math.round(count / 1_000)}k`;
85
+ if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
86
+ return `${Math.round(count / 1_000_000)}M`;
87
+ }
88
+
89
+ function footerCwd(cwd: string, home: string | undefined): string {
90
+ if (!home) return cwd;
91
+ const resolvedCwd = resolve(cwd);
92
+ const resolvedHome = resolve(home);
93
+ const relativeToHome = relative(resolvedHome, resolvedCwd);
94
+ const inside = relativeToHome === "" || (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
95
+ if (!inside) return cwd;
96
+ return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
97
+ }
98
+
99
+ function sanitize(value: string): string {
100
+ return value.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
101
+ }
102
+
103
+ function usageTotals(context: FooterContext): UsageTotals {
104
+ let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
105
+ for (const entry of context.sessionManager.getEntries()) {
106
+ if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
107
+ const usage = entry.message.usage;
108
+ input += usage?.input ?? 0;
109
+ output += usage?.output ?? 0;
110
+ cacheRead += usage?.cacheRead ?? 0;
111
+ cacheWrite += usage?.cacheWrite ?? 0;
112
+ cost += usage?.cost?.total ?? 0;
113
+ }
114
+ const prompt = input + cacheRead + cacheWrite;
115
+ return { input, output, cacheRead, cacheWrite, cost, ...(prompt > 0 ? { cacheHit: cacheRead / prompt * 100 } : {}) };
116
+ }
117
+
118
+ function barWidth(width: number): number {
119
+ return width >= FOOTER_WIDE_TERMINAL_WIDTH ? FOOTER_BAR_MAX_WIDTH : FOOTER_BAR_MIN_WIDTH;
120
+ }
121
+
122
+ function progressBar(fraction: number | null, width: number): string {
123
+ if (fraction === null || !Number.isFinite(fraction)) return "░".repeat(width);
124
+ const clamped = Math.min(1, Math.max(0, fraction));
125
+ const filled = Math.round(width * clamped);
126
+ return "█".repeat(filled) + "░".repeat(width - filled);
127
+ }
128
+
129
+ function fillColor(fraction: number | null): FooterColor {
130
+ if (fraction === null || !Number.isFinite(fraction)) return "dim";
131
+ if (fraction > FOOTER_CONTEXT_ERROR_FRACTION) return "error";
132
+ if (fraction > FOOTER_CONTEXT_WARNING_FRACTION) return "warning";
133
+ if (fraction > FOOTER_CONTEXT_ACCENT_FRACTION) return "accent";
134
+ return "dim";
135
+ }
136
+
137
+ function compactionFraction(progress: CompactionProgress, now: number): number {
138
+ const initial = Math.min(1, Math.max(0, progress.initialFraction));
139
+ if (progress.confidence !== "learned" || typeof progress.estimatedMs !== "number" || progress.estimatedMs <= 0) return initial;
140
+ const elapsedFraction = Math.max(0, Math.min(1, (now - progress.startedAt) / progress.estimatedMs));
141
+ return initial * (1 - elapsedFraction);
142
+ }
143
+
144
+ export function compactionBlinkOn(startedAt: number, now: number, halfPeriodMs = FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS): boolean {
145
+ const elapsed = Math.max(0, now - startedAt);
146
+ return Math.floor(elapsed / halfPeriodMs) % 2 === 0;
147
+ }
148
+
149
+ function compactionBarGlyph(progress: CompactionProgress, theme: FooterTheme, width: number, now: number): string {
150
+ if (!compactionBlinkOn(progress.startedAt, now)) return theme.fg("dim", "░".repeat(width));
151
+ return theme.fg("accent", progressBar(compactionFraction(progress, now), width));
152
+ }
153
+
154
+ function contextSegment(
155
+ context: FooterContext,
156
+ theme: FooterTheme,
157
+ width: number,
158
+ compact: boolean,
159
+ now: number,
160
+ compaction?: CompactionProgress,
161
+ ): string {
162
+ const w = barWidth(width);
163
+ if (compaction) return `ctx ${compactionBarGlyph(compaction, theme, w, now)}`;
164
+ const usage = context.getContextUsage();
165
+ const window = usage?.contextWindow ?? context.model?.contextWindow ?? 0;
166
+ const fraction = usage?.percent === null || usage?.percent === undefined ? null : usage.percent / 100;
167
+ const bar = theme.fg(fillColor(fraction), progressBar(fraction, w));
168
+ if (usage?.tokens === null || usage?.tokens === undefined) return `ctx ${bar} ?/${formatTokens(window)}`;
169
+ const value = compact ? `${Math.round((fraction ?? 0) * 100)}%` : `${formatTokens(usage.tokens)}/${formatTokens(window)}`;
170
+ return `ctx ${bar} ${value}`;
171
+ }
172
+
173
+ function minimalContextSegment(
174
+ context: FooterContext,
175
+ theme: FooterTheme,
176
+ width: number,
177
+ now: number,
178
+ compaction?: CompactionProgress,
179
+ ): string {
180
+ const w = barWidth(width);
181
+ if (compaction) {
182
+ return `ctx ${compactionBarGlyph(compaction, theme, w, now)}`;
183
+ }
184
+ const percent = context.getContextUsage()?.percent;
185
+ const fraction = percent === null || percent === undefined ? null : percent / 100;
186
+ return `ctx ${theme.fg(fillColor(fraction), progressBar(fraction, w))}`;
187
+ }
188
+
189
+ function resetLabel(resetsAt: number | undefined, now: number): string | undefined {
190
+ if (resetsAt === undefined) return undefined;
191
+ const remaining = resetsAt - now;
192
+ if (remaining <= 0) return "reset due";
193
+ if (remaining >= MILLISECONDS_PER_DAY) return `resets in ${Math.floor(remaining / MILLISECONDS_PER_DAY)}d`;
194
+ if (remaining >= MILLISECONDS_PER_HOUR) return `resets in ${Math.floor(remaining / MILLISECONDS_PER_HOUR)}h`;
195
+ return `resets in ${Math.max(1, Math.ceil(remaining / MILLISECONDS_PER_MINUTE))}m`;
196
+ }
197
+
198
+ /**
199
+ * `undefined` means no budget signal is possible for this provider at all (see buildFooterBudget);
200
+ * the segment is omitted entirely rather than showing a placeholder that could never resolve.
201
+ * `null` means not known yet but might resolve, which still earns the `?` placeholder.
202
+ */
203
+ function budgetSegment(budget: ProviderBudget | null | undefined, theme: FooterTheme, width: number, compact: boolean, now: number): string | undefined {
204
+ if (budget === undefined) return undefined;
205
+ const w = barWidth(width);
206
+ if (!budget) return `budget ${theme.fg("dim", progressBar(null, w))} ?`;
207
+ const stale = budget.observedAt !== undefined && now - budget.observedAt > TELEMETRY_STALE_AFTER_MS;
208
+ const staleText = stale ? ` ${theme.fg("warning", "stale")}` : "";
209
+ if (budget.kind === "unbounded") return `${budget.label} ${budget.valueText}${staleText}`;
210
+ const remaining = Math.min(1, Math.max(0, budget.remainingFraction));
211
+ const bar = theme.fg(fillColor(1 - remaining), progressBar(remaining, w));
212
+ const value = `${(compact ? Math.round(remaining * 100) : (remaining * 100).toFixed(1))}% left`;
213
+ const reset = compact ? undefined : resetLabel(budget.resetsAt, now) ?? budget.resetText;
214
+ return `${budget.label} ${bar} ${value}${reset ? ` · ${reset}` : ""}${staleText}`;
215
+ }
216
+
217
+ function usageSegment(context: FooterContext): string {
218
+ const totals = usageTotals(context);
219
+ const parts: string[] = [];
220
+ if (totals.input) parts.push(`↑${formatTokens(totals.input)}`);
221
+ if (totals.output) parts.push(`↓${formatTokens(totals.output)}`);
222
+ if (totals.cacheRead) parts.push(`R${formatTokens(totals.cacheRead)}`);
223
+ if (totals.cacheWrite) parts.push(`W${formatTokens(totals.cacheWrite)}`);
224
+ if ((totals.cacheRead || totals.cacheWrite) && totals.cacheHit !== undefined) parts.push(`CH${totals.cacheHit.toFixed(1)}%`);
225
+ if (totals.cost || (context.model && context.modelRegistry.isUsingOAuth(context.model))) {
226
+ parts.push(`$${totals.cost.toFixed(3)}${context.model && context.modelRegistry.isUsingOAuth(context.model) ? " (sub)" : ""}`);
227
+ }
228
+ return parts.join(" ");
229
+ }
230
+
231
+ function repositorySegment(context: FooterContext, footerData: FooterData, theme: FooterTheme): string {
232
+ let cwd = footerCwd(context.sessionManager.getCwd(), process.env.HOME ?? process.env.USERPROFILE);
233
+ const branch = footerData.getGitBranch();
234
+ if (branch) cwd += ` (${branch})`;
235
+ const sessionName = context.sessionManager.getSessionName();
236
+ if (sessionName) cwd += ` · ${sessionName}`;
237
+ return theme.fg("dim", cwd);
238
+ }
239
+
240
+ function modelSegments(context: FooterContext, footerData: FooterData, theme: FooterTheme, thinkingLevel: string): { full: string; compact: string } {
241
+ const model = context.model;
242
+ const modelName = theme.bold(model?.id ?? "no-model");
243
+ const provider = model && footerData.getAvailableProviderCount() > 1 ? `(${model.provider}) ` : "";
244
+ const thinking = model?.reasoning ? ` · ${thinkingLevel === "off" ? "thinking off" : thinkingLevel}` : "";
245
+ return { full: `${provider}${modelName}${thinking}`, compact: modelName };
246
+ }
247
+
248
+ function compactUsageSegment(context: FooterContext): string {
249
+ const totals = usageTotals(context);
250
+ const parts: string[] = [];
251
+ if (totals.input) parts.push(`↑${formatTokens(totals.input)}`);
252
+ if (totals.output) parts.push(`↓${formatTokens(totals.output)}`);
253
+ return parts.join(" ");
254
+ }
255
+
256
+ function joinSegments(segments: Array<string | undefined>): string {
257
+ return segments.filter((segment): segment is string => Boolean(segment)).join(" · ");
258
+ }
259
+
260
+ export function renderFooterLines(
261
+ context: FooterContext,
262
+ footerData: FooterData,
263
+ theme: FooterTheme,
264
+ providerBudget: ProviderBudget | null | undefined,
265
+ thinkingLevel: string,
266
+ width: number,
267
+ now = Date.now(),
268
+ compaction?: CompactionProgress,
269
+ ): string[] {
270
+ const safeWidth = Math.max(1, width);
271
+ const repository = repositorySegment(context, footerData, theme);
272
+ const model = modelSegments(context, footerData, theme, thinkingLevel);
273
+ const usage = usageSegment(context);
274
+ const compactUsage = compactUsageSegment(context);
275
+ const fullContext = contextSegment(context, theme, safeWidth, false, now, compaction);
276
+ const compactContext = contextSegment(context, theme, safeWidth, true, now, compaction);
277
+ const minimalContext = minimalContextSegment(context, theme, safeWidth, now, compaction);
278
+ const fullBudget = budgetSegment(providerBudget, theme, safeWidth, false, now);
279
+ const compactBudget = budgetSegment(providerBudget, theme, safeWidth, true, now);
280
+ const statuses = [...footerData.getExtensionStatuses().entries()]
281
+ .filter(([key]) => key !== "jittor")
282
+ .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
283
+ .map(([, text]) => sanitize(text))
284
+ .join(" ");
285
+
286
+ const candidates = [
287
+ joinSegments([repository, model.full, usage, fullContext, fullBudget, statuses]),
288
+ joinSegments([repository, model.full, usage, fullContext, fullBudget]),
289
+ joinSegments([model.full, usage, compactContext, compactBudget, statuses]),
290
+ joinSegments([model.full, usage, compactContext, compactBudget]),
291
+ joinSegments([model.full, compactUsage, compactContext, compactBudget]),
292
+ joinSegments([model.compact, compactUsage, compactContext, compactBudget]),
293
+ joinSegments([model.compact, compactContext, compactBudget]),
294
+ joinSegments([model.compact, minimalContext, compactBudget]),
295
+ ];
296
+ const line = candidates.find((candidate) => visibleWidth(candidate) <= safeWidth) ?? candidates.at(-1) ?? "";
297
+ return [truncateToWidth(line, safeWidth, "")];
298
+ }
299
+
300
+ export interface IntegratedFooterState {
301
+ providerBudget: ProviderBudget | null | undefined;
302
+ compaction?: CompactionProgress;
303
+ requestRender?: () => void;
304
+ }
305
+
306
+ export function installIntegratedFooter(ctx: ExtensionContext, state: IntegratedFooterState, getThinkingLevel: () => string): void {
307
+ ctx.ui.setStatus("jittor", undefined);
308
+ ctx.ui.setFooter((tui, theme, footerData) => {
309
+ state.requestRender = () => tui.requestRender();
310
+ const unsubscribe = (footerData as FooterData).onBranchChange?.(() => tui.requestRender());
311
+ return {
312
+ invalidate() {},
313
+ render(width: number): string[] {
314
+ return renderFooterLines(ctx as unknown as FooterContext, footerData, theme, state.providerBudget, getThinkingLevel(), width, Date.now(), state.compaction);
315
+ },
316
+ dispose() {
317
+ unsubscribe?.();
318
+ state.requestRender = undefined;
319
+ tui.requestRender();
320
+ },
321
+ };
322
+ });
323
+ }