@danypops/jittor 0.11.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.
@@ -1,127 +0,0 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import {
3
- CODEX_RECOVERY_ATTEMPT_WINDOW_MS,
4
- CODEX_RECOVERY_BASE_DELAY_MS,
5
- CODEX_RECOVERY_JITTER_RATIO,
6
- CODEX_RECOVERY_MAX_ATTEMPTS,
7
- CODEX_RECOVERY_MAX_DELAY_MS,
8
- MILLISECONDS_PER_MINUTE,
9
- MILLISECONDS_PER_SECOND,
10
- } from "../../../src/constants.ts";
11
- import { CodexRecoveryPolicy, classifyCodexFailure, type CodexFailureKind, type CodexFailureMetadata } from "../../../src/domain/codex-recovery.ts";
12
- import type { CodexRecoveryControl } from "../settings.ts";
13
- import { headerValue } from "./http-headers.ts";
14
-
15
- export interface CodexRecoveryRuntime {
16
- now(): number;
17
- random(): number;
18
- setTimeout(callback: () => void | Promise<void>, delayMs: number): unknown;
19
- clearTimeout(handle: unknown): void;
20
- }
21
-
22
- export const SYSTEM_RECOVERY_RUNTIME: CodexRecoveryRuntime = {
23
- now: Date.now,
24
- random: Math.random,
25
- setTimeout(callback, delayMs) { return setTimeout(() => { void callback(); }, delayMs); },
26
- clearTimeout(handle) { clearTimeout(handle as ReturnType<typeof setTimeout>); },
27
- };
28
-
29
- /**
30
- * Codex's settled-turn hidden-retry recovery, as its own capability: tracks the most recent
31
- * Codex response (status/retry-after) across the current turn, classifies a finalized failure,
32
- * and schedules at most one bounded, jittered follow-up once Pi's own turn has genuinely
33
- * settled. Fully self-contained -- the only external dependencies are Pi's own message-send API,
34
- * the persisted on/off control, and a runtime the tests can fake (timers, randomness, clock).
35
- */
36
- export class CodexRecoveryCapability {
37
- private readonly policy: CodexRecoveryPolicy;
38
- private lastResponse: CodexFailureMetadata = {};
39
- private timer: unknown;
40
- private cooldown: { until: number; attempt: number; failureKind: CodexFailureKind } | undefined;
41
-
42
- constructor(
43
- private readonly pi: ExtensionAPI,
44
- private readonly control: CodexRecoveryControl,
45
- private readonly runtime: CodexRecoveryRuntime,
46
- ) {
47
- this.policy = new CodexRecoveryPolicy({
48
- baseDelayMs: CODEX_RECOVERY_BASE_DELAY_MS,
49
- maxDelayMs: CODEX_RECOVERY_MAX_DELAY_MS,
50
- maxAttempts: CODEX_RECOVERY_MAX_ATTEMPTS,
51
- attemptWindowMs: CODEX_RECOVERY_ATTEMPT_WINDOW_MS,
52
- jitterRatio: CODEX_RECOVERY_JITTER_RATIO,
53
- }, runtime.random);
54
- }
55
-
56
- /** Clears the tracked response at the start of every new turn, before any Codex response for it has arrived. */
57
- resetTurn(): void {
58
- this.lastResponse = {};
59
- }
60
-
61
- notifyResponse(status: number, headers: Record<string, string>): void {
62
- this.lastResponse = { status, ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}) };
63
- }
64
-
65
- notifyMessageEnd(stopReason: string, errorMessage: string | undefined): void {
66
- if (stopReason === "error") {
67
- const failure = classifyCodexFailure(errorMessage, this.lastResponse);
68
- if (this.control.isCodexRecoveryEnabled() && failure.transient) this.policy.observeFailure(failure, this.runtime.now());
69
- else this.cancel(true);
70
- } else if (stopReason !== "aborted") {
71
- this.cancel(true);
72
- }
73
- this.lastResponse = {};
74
- }
75
-
76
- cancel(resetPolicy: boolean): void {
77
- if (this.timer !== undefined) this.runtime.clearTimeout(this.timer);
78
- this.timer = undefined;
79
- this.cooldown = undefined;
80
- if (resetPolicy) this.policy.cancel();
81
- }
82
-
83
- statusText(): string {
84
- const now = this.runtime.now();
85
- const state = this.policy.state(now);
86
- const enabled = this.control.isCodexRecoveryEnabled();
87
- const attempt = this.cooldown?.attempt ?? (state.pending ? state.attempts + 1 : state.attempts);
88
- const phase = this.cooldown
89
- ? `cooldown ${Math.ceil(Math.max(0, this.cooldown.until - now) / MILLISECONDS_PER_SECOND)}s`
90
- : state.pending ? "pending"
91
- : state.attempts >= CODEX_RECOVERY_MAX_ATTEMPTS ? "exhausted"
92
- : state.attempts > 0 ? "waiting" : "idle";
93
- const failureKind = this.cooldown?.failureKind ?? state.lastFailureKind;
94
- return [
95
- `Codex recovery: ${enabled ? "on" : "off"}`,
96
- phase,
97
- `attempt ${attempt}/${CODEX_RECOVERY_MAX_ATTEMPTS}`,
98
- `window ${CODEX_RECOVERY_ATTEMPT_WINDOW_MS / MILLISECONDS_PER_MINUTE}m`,
99
- ...(failureKind ? [failureKind] : []),
100
- ].join(" · ");
101
- }
102
-
103
- scheduleIfIdle(ctx: ExtensionContext): void {
104
- if (!this.control.isCodexRecoveryEnabled() || this.timer !== undefined || !ctx.isIdle() || ctx.hasPendingMessages()) return;
105
- const plan = this.policy.plan(this.runtime.now());
106
- if (plan.action === "exhausted") {
107
- this.policy.abandonFailure();
108
- if (ctx.hasUI) ctx.ui.notify(`Jittor Codex recovery stopped: ${plan.reason}.`, "warning");
109
- return;
110
- }
111
- if (plan.action !== "schedule") return;
112
- this.cooldown = { until: this.runtime.now() + plan.delayMs, attempt: plan.attempt, failureKind: plan.failureKind };
113
- this.timer = this.runtime.setTimeout(async () => {
114
- this.timer = undefined;
115
- this.cooldown = undefined;
116
- if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
117
- const attempt = this.policy.recordAttempt(this.runtime.now());
118
- if (!attempt) return;
119
- this.pi.sendMessage({
120
- customType: "jittor-codex-recovery",
121
- content: `Retry the previous Codex request after a transient ${attempt.failureKind} failure. Automatic recovery attempt ${attempt.attempt} of ${CODEX_RECOVERY_MAX_ATTEMPTS}.`,
122
- display: false,
123
- details: { attempt: attempt.attempt, failureKind: attempt.failureKind },
124
- }, { triggerTurn: true, deliverAs: "followUp" });
125
- }, plan.delayMs);
126
- }
127
- }
@@ -1,5 +0,0 @@
1
- /** Case-insensitive header lookup -- Pi's provider-response event headers are a plain Record, not a Headers instance. */
2
- export function headerValue(headers: Record<string, string>, name: string): string | undefined {
3
- const expected = name.toLowerCase();
4
- return Object.entries(headers).find(([key]) => key.toLowerCase() === expected)?.[1];
5
- }
@@ -1,104 +0,0 @@
1
- import { classifyTaskFromTools, modelRunMetrics, type ModelRunObservation } from "../../../src/domain/model-observation.ts";
2
- import type { MetricObservation } from "../../../src/domain/metric.ts";
3
-
4
- export interface ActiveLocalModelRun {
5
- runId: string;
6
- startedAt: number;
7
- firstTokenAt: number | null;
8
- providerResponses: number;
9
- toolNames: string[];
10
- toolCalls: number;
11
- toolFailures: number;
12
- }
13
-
14
- /**
15
- * Content-free local model observations derived only from Pi's public lifecycle: TTFT, wall
16
- * latency, tool-loop counts and failures, and a bounded tool-name list used only to derive
17
- * domain/type classification -- never prompts, responses, tool arguments/results, or project
18
- * paths. Owns the one in-flight run plus the most recently completed one (for `/jittor outcome`).
19
- */
20
- export class LocalRunTelemetry {
21
- private active: ActiveLocalModelRun | undefined;
22
- private lastCompleted: ModelRunObservation | undefined;
23
- private sequence = 0;
24
-
25
- beginTurn(timestamp: number): void {
26
- this.active = {
27
- runId: `local-${timestamp}-${++this.sequence}`,
28
- startedAt: timestamp,
29
- firstTokenAt: null,
30
- providerResponses: 0,
31
- toolNames: [],
32
- toolCalls: 0,
33
- toolFailures: 0,
34
- };
35
- }
36
-
37
- discardTurn(): void {
38
- this.active = undefined;
39
- }
40
-
41
- onMessageUpdate(assistantMessageEventType: string): void {
42
- if (!this.active || this.active.firstTokenAt !== null) return;
43
- if (["text_delta", "thinking_delta", "toolcall_delta"].includes(assistantMessageEventType)) this.active.firstTokenAt = Date.now();
44
- }
45
-
46
- onToolExecutionEnd(toolName: string, isError: boolean): void {
47
- if (!this.active) return;
48
- this.active.toolCalls += 1;
49
- if (isError) this.active.toolFailures += 1;
50
- if (this.active.toolNames.length < 100) this.active.toolNames.push(toolName);
51
- }
52
-
53
- onProviderResponse(): void {
54
- if (this.active) this.active.providerResponses += 1;
55
- }
56
-
57
- /** Finalizes the active run against a completed assistant turn_end message, returning metrics to record (empty if the message shape doesn't match a completed assistant turn). */
58
- completeTurn(message: unknown, thinkingLevel: string): MetricObservation[] {
59
- const active = this.active;
60
- this.active = undefined;
61
- if (!active || typeof message !== "object" || message === null || Array.isArray(message)) return [];
62
- const value = message as Record<string, unknown>;
63
- if (value["role"] !== "assistant" || typeof value["provider"] !== "string" || typeof value["model"] !== "string") return [];
64
- const usage = typeof value["usage"] === "object" && value["usage"] !== null ? value["usage"] as Record<string, unknown> : {};
65
- const amount = (name: string): number => typeof usage[name] === "number" && Number.isFinite(usage[name]) ? usage[name] as number : 0;
66
- const cost = typeof usage["cost"] === "object" && usage["cost"] !== null && typeof (usage["cost"] as Record<string, unknown>)["total"] === "number"
67
- ? (usage["cost"] as Record<string, number>)["total"] ?? 0 : 0;
68
- const stopReason = ["stop", "length", "toolUse", "error", "aborted"].includes(String(value["stopReason"]))
69
- ? value["stopReason"] as ModelRunObservation["stopReason"] : "unknown";
70
- const completedAt = Math.max(Date.now(), active.firstTokenAt ?? active.startedAt, active.startedAt);
71
- this.lastCompleted = {
72
- runId: active.runId,
73
- provider: value["provider"],
74
- model: value["model"],
75
- thinking: thinkingLevel,
76
- ...classifyTaskFromTools(active.toolNames),
77
- startedAt: active.startedAt,
78
- firstTokenAt: active.firstTokenAt,
79
- completedAt,
80
- inputTokens: amount("input"),
81
- outputTokens: amount("output"),
82
- cacheReadTokens: amount("cacheRead"),
83
- cacheWriteTokens: amount("cacheWrite"),
84
- costUsd: Number.isFinite(cost) && cost >= 0 ? cost : 0,
85
- providerResponses: Math.max(1, active.providerResponses),
86
- toolCalls: active.toolCalls,
87
- toolFailures: active.toolFailures,
88
- stopReason,
89
- explicitOutcome: "unknown",
90
- };
91
- return modelRunMetrics(this.lastCompleted);
92
- }
93
-
94
- /** Builds the single outcome-accepted/outcome-accepted=0 metric for the most recently completed run, or null if none exists yet. */
95
- explicitOutcomeMetric(explicitOutcome: "accepted" | "rejected"): MetricObservation | null {
96
- if (!this.lastCompleted) return null;
97
- return modelRunMetrics({ ...this.lastCompleted, explicitOutcome }).find((metric) => metric.metric === "outcome-accepted") ?? null;
98
- }
99
-
100
- reset(): void {
101
- this.active = undefined;
102
- this.lastCompleted = undefined;
103
- }
104
- }
@@ -1,96 +0,0 @@
1
- import type { MetricObservation } from "../../../src/domain/metric.ts";
2
- import { hasAnthropicRateLimitHeaders, parseAnthropicRateLimitHeaders } from "../../../src/providers/anthropic-contracts.ts";
3
- import { parseCodexRateLimitHeaders } from "../../../src/providers/codex.ts";
4
- import { classifyGoogleVertexFailure, googleVertexFailureMetrics, type GoogleVertexFailureMetadata } from "../../../src/providers/google-vertex-contracts.ts";
5
- import { headerValue } from "./http-headers.ts";
6
-
7
- export interface ProviderTelemetryClient {
8
- call(operation: string, input: unknown): Promise<any>;
9
- }
10
-
11
- async function recordMetrics(client: ProviderTelemetryClient, metrics: MetricObservation[]): Promise<void> {
12
- if (metrics.length === 0) return;
13
- await client.call("metrics.record_batch", { observations: metrics });
14
- }
15
-
16
- /**
17
- * Bounded telemetry derived directly from provider HTTP responses and finalized messages, for
18
- * every provider except Codex (whose response tracking is settled-turn-recovery's own concern --
19
- * see codex-recovery.ts). Anthropic and anthropic-vertex official rate-limit headers become
20
- * budget metrics; Google Vertex and anthropic-vertex failures become bounded, content-free
21
- * failure-count metrics classified from GCP's own `google.rpc.Status` shape. anthropic-vertex is
22
- * tracked distinctly from both google-vertex (different code path, different quota pool) and
23
- * direct anthropic (different transport), never conflated with either.
24
- */
25
- export class ProviderResponseTelemetry {
26
- private lastGoogleVertexResponse: GoogleVertexFailureMetadata = {};
27
- private lastAnthropicVertexResponse: GoogleVertexFailureMetadata = {};
28
-
29
- resetTurn(): void {
30
- this.lastGoogleVertexResponse = {};
31
- this.lastAnthropicVertexResponse = {};
32
- }
33
-
34
- async handleProviderResponse(
35
- client: ProviderTelemetryClient,
36
- provider: string | undefined,
37
- status: number,
38
- headers: Record<string, string>,
39
- notifySchemaDrift: (message: string) => void,
40
- ): Promise<void> {
41
- if (provider === "anthropic") {
42
- const parsedHeaders = new Headers(headers);
43
- if (hasAnthropicRateLimitHeaders(parsedHeaders)) {
44
- try {
45
- await recordMetrics(client, parseAnthropicRateLimitHeaders(parsedHeaders, Date.now()).metrics);
46
- } catch {
47
- notifySchemaDrift("Anthropic telemetry schema drift");
48
- }
49
- }
50
- }
51
- if (provider === "anthropic-vertex") {
52
- // Best-effort only: unverified whether this passthrough ever forwards Anthropic's own
53
- // rate-limit headers. If it doesn't, hasAnthropicRateLimitHeaders is false and nothing is
54
- // recorded -- the same honest default as every other unconfirmed signal in this module.
55
- const parsedHeaders = new Headers(headers);
56
- if (hasAnthropicRateLimitHeaders(parsedHeaders)) {
57
- try {
58
- await recordMetrics(client, parseAnthropicRateLimitHeaders(parsedHeaders, Date.now(), "anthropic-vertex").metrics);
59
- } catch {
60
- notifySchemaDrift("Anthropic-on-Vertex telemetry schema drift");
61
- }
62
- }
63
- // Well-evidenced regardless of headers: GCP's own quota system fronts this transport, so the
64
- // same failure classification as google-vertex applies below.
65
- this.lastAnthropicVertexResponse = { status, ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}) };
66
- }
67
- if (provider === "google-vertex") {
68
- this.lastGoogleVertexResponse = { status, ...(headerValue(headers, "retry-after") ? { retryAfter: headerValue(headers, "retry-after") } : {}) };
69
- }
70
- if (Object.keys(headers).some((name) => name.toLowerCase().startsWith("x-codex-"))) {
71
- try {
72
- const updates = parseCodexRateLimitHeaders(new Headers(headers), Date.now());
73
- await recordMetrics(client, updates.flatMap((update) => update.metrics));
74
- } catch {
75
- notifySchemaDrift("Codex telemetry schema drift");
76
- }
77
- }
78
- }
79
-
80
- async handleMessageEnd(client: ProviderTelemetryClient, provider: string | undefined, stopReason: string | undefined, errorMessage: string | undefined): Promise<void> {
81
- if (provider === "google-vertex") {
82
- if (stopReason === "error") {
83
- const failure = classifyGoogleVertexFailure(errorMessage, this.lastGoogleVertexResponse);
84
- await recordMetrics(client, googleVertexFailureMetrics(failure, Date.now())).catch(() => undefined);
85
- }
86
- this.lastGoogleVertexResponse = {};
87
- }
88
- if (provider === "anthropic-vertex") {
89
- if (stopReason === "error") {
90
- const failure = classifyGoogleVertexFailure(errorMessage, this.lastAnthropicVertexResponse);
91
- await recordMetrics(client, googleVertexFailureMetrics(failure, Date.now(), "anthropic-vertex")).catch(() => undefined);
92
- }
93
- this.lastAnthropicVertexResponse = {};
94
- }
95
- }
96
- }
@@ -1,323 +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
- TELEMETRY_STALE_AFTER_MS,
16
- } from "../../src/constants.ts";
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
- }