@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.
- package/README.md +21 -73
- package/package.json +11 -14
- package/src/cli.ts +0 -0
- package/src/index.ts +137 -0
- package/docs/USAGE_PRIOR_ART.md +0 -64
- package/extension/src/benchmark-tui.ts +0 -109
- package/extension/src/capabilities/codex-recovery.ts +0 -127
- package/extension/src/capabilities/http-headers.ts +0 -5
- package/extension/src/capabilities/local-run-telemetry.ts +0 -104
- package/extension/src/capabilities/provider-response-telemetry.ts +0 -96
- package/extension/src/footer.ts +0 -323
- package/extension/src/index.ts +0 -648
- package/extension/src/service-client.ts +0 -26
- package/extension/src/session-identity.ts +0 -20
- package/extension/src/settings-tui.ts +0 -153
- package/extension/src/settings.ts +0 -103
- package/extension/src/tui.ts +0 -279
- package/extension/src/usage.ts +0 -320
|
@@ -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
|
-
}
|
package/extension/src/footer.ts
DELETED
|
@@ -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
|
-
}
|