@llblab/pi-kit 0.25.0 → 0.26.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/BACKLOG.md +5 -1
- package/CHANGELOG.md +6 -0
- package/README.md +9 -7
- package/node_modules/@llblab/pi-actors/AGENTS.md +2 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-actors/LICENSE +21 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +4 -3
- package/node_modules/@llblab/pi-claude-usage/AGENTS.md +6 -3
- package/node_modules/@llblab/pi-claude-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +8 -0
- package/node_modules/@llblab/pi-claude-usage/README.md +48 -3
- package/node_modules/@llblab/pi-claude-usage/index.ts +8 -1159
- package/node_modules/@llblab/pi-claude-usage/lib/extension.ts +30 -0
- package/node_modules/@llblab/pi-claude-usage/lib/fast.ts +24 -0
- package/node_modules/@llblab/pi-claude-usage/lib/query.ts +146 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +297 -0
- package/node_modules/@llblab/pi-claude-usage/lib/status.ts +366 -0
- package/node_modules/@llblab/pi-claude-usage/lib/telegram.ts +44 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage-store.ts +221 -0
- package/node_modules/@llblab/pi-claude-usage/lib/usage.ts +128 -0
- package/node_modules/@llblab/pi-claude-usage/package.json +9 -5
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-clean-room/LICENSE +21 -0
- package/node_modules/@llblab/pi-clean-room/README.md +1 -1
- package/node_modules/@llblab/pi-clean-room/package.json +3 -2
- package/node_modules/@llblab/pi-codex-usage/AGENTS.md +9 -6
- package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +2 -1
- package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-codex-usage/README.md +75 -17
- package/node_modules/@llblab/pi-codex-usage/index.ts +8 -1602
- package/node_modules/@llblab/pi-codex-usage/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-codex-usage/lib/fast.ts +23 -0
- package/node_modules/@llblab/pi-codex-usage/lib/query.ts +368 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +347 -0
- package/node_modules/@llblab/pi-codex-usage/lib/status.ts +435 -0
- package/node_modules/@llblab/pi-codex-usage/lib/telegram.ts +45 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage-store.ts +229 -0
- package/node_modules/@llblab/pi-codex-usage/lib/usage.ts +425 -0
- package/node_modules/@llblab/pi-codex-usage/package.json +11 -6
- package/node_modules/@llblab/pi-command-fast/AGENTS.md +7 -0
- package/node_modules/@llblab/pi-command-fast/BACKLOG.md +9 -0
- package/node_modules/@llblab/pi-command-fast/CHANGELOG.md +7 -0
- package/node_modules/@llblab/pi-command-fast/LICENSE +21 -0
- package/node_modules/@llblab/pi-command-fast/README.md +42 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.d.ts +8 -0
- package/node_modules/@llblab/pi-command-fast/dist/command.js +52 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.d.ts +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/index.js +3 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.d.ts +10 -0
- package/node_modules/@llblab/pi-command-fast/dist/models-json.js +81 -0
- package/node_modules/@llblab/pi-command-fast/package.json +49 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-grow-loop/LICENSE +21 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +6 -5
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +7 -1
- package/node_modules/@llblab/pi-state-flow/LICENSE +21 -0
- package/node_modules/@llblab/pi-state-flow/README.md +5 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +238 -46
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +16 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +10 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +60 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +8 -5
- package/node_modules/@llblab/pi-state-flow/dist/package.json +10 -9
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +11 -7
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +4 -2
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +14 -13
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +221 -46
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +14 -5
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +52 -1
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +7 -5
- package/node_modules/@llblab/pi-state-flow/package.json +10 -9
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +7 -7
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Domain: extension composition. Owns: provider registration, Fast request bridge and status wiring. Excludes: quota, persistence and presentation policy. */
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { registerFastProvider } from "@llblab/pi-command-fast";
|
|
4
|
+
import { applyFastToRequest, isFastEnabled, isFastSupported, toggleFast } from "./fast.ts";
|
|
5
|
+
import { ANTHROPIC_PROVIDER_ID, isAnthropicModel } from "./usage.ts";
|
|
6
|
+
import { createClaudeUsageStatus } from "./status.ts";
|
|
7
|
+
|
|
8
|
+
export default function claudeUsage(pi: ExtensionAPI) {
|
|
9
|
+
let releaseFast: (() => void) | undefined;
|
|
10
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
11
|
+
const model = ctx.model;
|
|
12
|
+
if (!model || !isAnthropicModel(model)) return;
|
|
13
|
+
return applyFastToRequest(event.payload, model.id, isFastEnabled(model.id));
|
|
14
|
+
});
|
|
15
|
+
pi.on("session_start", (_event, ctx) => {
|
|
16
|
+
releaseFast = registerFastProvider(pi, ctx, {
|
|
17
|
+
provider: ANTHROPIC_PROVIDER_ID,
|
|
18
|
+
toggle(commandCtx) {
|
|
19
|
+
if (!isFastSupported(commandCtx.model!.id)) {
|
|
20
|
+
commandCtx.ui.notify("Fast mode is supported only for Opus", "warning");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
toggleFast(commandCtx.model!.id);
|
|
24
|
+
status.fastChanged(commandCtx, commandCtx.model!);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
pi.on("session_shutdown", () => { releaseFast?.(); releaseFast = undefined; });
|
|
29
|
+
const status = createClaudeUsageStatus(pi);
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Domain: Claude Fast. Owns: provider semantics and native request beta adaptation. Excludes: quota and command ownership. */
|
|
2
|
+
import { isModelOverrideValue, toggleModelOverrideValue } from "@llblab/pi-command-fast";
|
|
3
|
+
|
|
4
|
+
export const FAST_BETA = "fast-mode-2026-02-01";
|
|
5
|
+
export function isFastSupported(modelId: string): boolean {
|
|
6
|
+
return modelId.startsWith("claude-opus-");
|
|
7
|
+
}
|
|
8
|
+
const target = (id: string) => ({ provider: "anthropic", id, property: "speed", enabledValue: "fast" });
|
|
9
|
+
export function isFastEnabled(modelId: string, path?: string): boolean {
|
|
10
|
+
return isFastSupported(modelId) && isModelOverrideValue(target(modelId), path);
|
|
11
|
+
}
|
|
12
|
+
export function toggleFast(modelId: string, path?: string): boolean {
|
|
13
|
+
return isFastSupported(modelId) && toggleModelOverrideValue(target(modelId), path);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Pi assembles automatic/configured betas before this hook; the Anthropic SDK converts betas to the HTTP header. */
|
|
17
|
+
export function applyFastToRequest(payload: unknown, modelId: string, enabled: boolean): unknown {
|
|
18
|
+
if (!enabled || !isFastSupported(modelId) || !payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
19
|
+
const body = payload as Record<string, unknown>;
|
|
20
|
+
if (body.model !== modelId || ("speed" in body && body.speed !== "fast")) return undefined;
|
|
21
|
+
if ("betas" in body && !Array.isArray(body.betas)) return undefined;
|
|
22
|
+
const betas = Array.isArray(body.betas) ? body.betas : [];
|
|
23
|
+
return { ...body, speed: "fast", betas: [...new Set([...betas, FAST_BETA])] };
|
|
24
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** Domain: quota query. Owns: Pi OAuth selection, guarded HTTP, timeout, Retry-After and error redaction. Excludes: lease mutation, Fast and display. */
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { ANTHROPIC_PROVIDER_ID, parseUsageResponse, type ClaudeUsageReport } from "./usage.ts";
|
|
4
|
+
const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
5
|
+
const ANTHROPIC_OAUTH_BETA = "oauth-2025-04-20";
|
|
6
|
+
const SECOND_MS = 1000;
|
|
7
|
+
const MAX_ERROR_BODY_CHARS = 600;
|
|
8
|
+
type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
9
|
+
|
|
10
|
+
export type QueryUsageResult =
|
|
11
|
+
| { ok: true; report: ClaudeUsageReport }
|
|
12
|
+
| {
|
|
13
|
+
ok: false;
|
|
14
|
+
message: string;
|
|
15
|
+
rateLimited: boolean;
|
|
16
|
+
retryAfterMs?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function queryUsage(
|
|
20
|
+
ctx: ExtensionContext,
|
|
21
|
+
timeoutMs: number,
|
|
22
|
+
mayQuery: () => boolean,
|
|
23
|
+
): Promise<QueryUsageResult | undefined> {
|
|
24
|
+
try {
|
|
25
|
+
if (!mayQuery()) return undefined;
|
|
26
|
+
const auth = await resolvePiAnthropicAuth(ctx);
|
|
27
|
+
if (!auth) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"No Pi Anthropic subscription auth was available. Use a Pi Anthropic model or run /login for Anthropic Claude Pro/Max.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!mayQuery()) return undefined;
|
|
34
|
+
const response = await fetchWithTimeout(
|
|
35
|
+
CLAUDE_USAGE_URL,
|
|
36
|
+
{ headers: auth.headers },
|
|
37
|
+
timeoutMs,
|
|
38
|
+
);
|
|
39
|
+
const text = await response.text();
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: `Claude usage endpoint returned ${response.status} ${response.statusText}: ${redactErrorBody(text)}`,
|
|
44
|
+
rateLimited: response.status === 429,
|
|
45
|
+
retryAfterMs: parseRetryAfterMs(response.headers.get("retry-after")),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { ok: true, report: parseUsageResponse(text) };
|
|
50
|
+
} catch (cause) {
|
|
51
|
+
return { ok: false, message: errorMessage(cause), rateLimited: false };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseRetryAfterMs(value: string | null): number | undefined {
|
|
56
|
+
const seconds = value === null ? NaN : Number(value);
|
|
57
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * SECOND_MS : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function resolvePiAnthropicAuth(
|
|
61
|
+
ctx: ExtensionContext,
|
|
62
|
+
): Promise<{ headers: Record<string, string> } | undefined> {
|
|
63
|
+
const errors: string[] = [];
|
|
64
|
+
|
|
65
|
+
for (const model of anthropicAuthCandidateModels(ctx)) {
|
|
66
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
67
|
+
if (!auth.ok) {
|
|
68
|
+
errors.push(auth.error);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// The usage endpoint only accepts subscription OAuth tokens, not API keys.
|
|
72
|
+
if (auth.apiKey?.includes("sk-ant-oat")) {
|
|
73
|
+
return {
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${auth.apiKey}`,
|
|
76
|
+
"anthropic-beta": ANTHROPIC_OAUTH_BETA,
|
|
77
|
+
Accept: "application/json",
|
|
78
|
+
"User-Agent": "pi-claude-usage",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (errors.length > 0) {
|
|
85
|
+
throw new Error(errors.join("; "));
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function anthropicAuthCandidateModels(ctx: ExtensionContext): PiModel[] {
|
|
91
|
+
const candidates: PiModel[] = [];
|
|
92
|
+
const seen = new Set<string>();
|
|
93
|
+
const add = (model: PiModel | undefined) => {
|
|
94
|
+
if (!model || model.provider !== ANTHROPIC_PROVIDER_ID) return;
|
|
95
|
+
const key = `${model.provider}/${model.id}`;
|
|
96
|
+
if (seen.has(key)) return;
|
|
97
|
+
seen.add(key);
|
|
98
|
+
candidates.push(model);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
add(ctx.model);
|
|
102
|
+
for (const model of ctx.modelRegistry.getAvailable()) add(model);
|
|
103
|
+
for (const model of ctx.modelRegistry.getAll()) add(model);
|
|
104
|
+
return candidates;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function fetchWithTimeout(
|
|
108
|
+
url: string,
|
|
109
|
+
init: RequestInit,
|
|
110
|
+
timeoutMs: number,
|
|
111
|
+
): Promise<Response> {
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
114
|
+
try {
|
|
115
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (controller.signal.aborted) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching Claude usage.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function redactErrorBody(body: string): string {
|
|
129
|
+
return truncateEnd(
|
|
130
|
+
body
|
|
131
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
|
|
132
|
+
.replace(/sk-ant-[A-Za-z0-9_-]+/g, "<redacted>")
|
|
133
|
+
.replace(/"access_token"\s*:\s*"[^"]+"/gi, '"access_token":"<redacted>"')
|
|
134
|
+
.trim(),
|
|
135
|
+
MAX_ERROR_BODY_CHARS,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function truncateEnd(value: string, maxChars: number): string {
|
|
140
|
+
if (value.length <= maxChars) return value;
|
|
141
|
+
return `${value.slice(0, maxChars - 1)}…`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function errorMessage(error: unknown): string {
|
|
145
|
+
return error instanceof Error ? error.message : String(error);
|
|
146
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/** Domain: status formatting. Owns: quota bars, labels, countdown boundaries and loading/problem text. Excludes: lifecycle, I/O and Fast state. */
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { isUsageUnavailable, type ClaudeUsageReport, type NormalizedRateLimitWindow, type UsageQueryError } from "./usage.ts";
|
|
4
|
+
|
|
5
|
+
const SECOND_MS = 1000;
|
|
6
|
+
const MINUTE_MS = 60 * SECOND_MS;
|
|
7
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
8
|
+
const HOUR_TENTH_MS = 6 * MINUTE_MS;
|
|
9
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
10
|
+
const DAY_TENTH_MS = 144 * MINUTE_MS;
|
|
11
|
+
export const STATUS_LABEL_TEXT = "claude";
|
|
12
|
+
export const DUAL_BAR_WIDTH = 10;
|
|
13
|
+
const DUAL_BAR_CHARS = [
|
|
14
|
+
"⠀",
|
|
15
|
+
"▘",
|
|
16
|
+
"▝",
|
|
17
|
+
"▀",
|
|
18
|
+
"▖",
|
|
19
|
+
"▌",
|
|
20
|
+
"▞",
|
|
21
|
+
"▛",
|
|
22
|
+
"▗",
|
|
23
|
+
"▚",
|
|
24
|
+
"▐",
|
|
25
|
+
"▜",
|
|
26
|
+
"▄",
|
|
27
|
+
"▙",
|
|
28
|
+
"▟",
|
|
29
|
+
"█",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function formatClaudeUsageStatusline(
|
|
33
|
+
report: ClaudeUsageReport,
|
|
34
|
+
ctx: ExtensionContext,
|
|
35
|
+
): string {
|
|
36
|
+
const value = formatClaudeUsageStatusValue(report);
|
|
37
|
+
if (!value) return formatStatuslineText(ctx, "n/a");
|
|
38
|
+
if (!report.primary || !report.secondary)
|
|
39
|
+
return formatStatuslineText(ctx, value);
|
|
40
|
+
const [bar, countdown] = value.split(" ", 2);
|
|
41
|
+
const barText = formatStatuslineBarText(
|
|
42
|
+
ctx,
|
|
43
|
+
bar ?? "",
|
|
44
|
+
hasExhaustedQuotaWindow(report) ? "toolErrorBg" : "userMessageBg",
|
|
45
|
+
);
|
|
46
|
+
return countdown
|
|
47
|
+
? `${barText} ${ctx.ui.theme.fg("dim", countdown)}`
|
|
48
|
+
: barText;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatClaudeUsageBar(
|
|
52
|
+
report: ClaudeUsageReport,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
if (!report.primary && !report.secondary) return undefined;
|
|
55
|
+
return formatAdaptiveLimitBar(report.primary, report.secondary);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatClaudeUsageStatusValue(
|
|
59
|
+
report: ClaudeUsageReport,
|
|
60
|
+
now = Date.now(),
|
|
61
|
+
): string | undefined {
|
|
62
|
+
if (!report.primary || !report.secondary) {
|
|
63
|
+
const window = report.primary ?? report.secondary;
|
|
64
|
+
if (!window) return undefined;
|
|
65
|
+
const percentage = `${Math.round(remainingPercent(window))}%`;
|
|
66
|
+
const countdown = window.resetAt
|
|
67
|
+
? formatResetCountdown(window.resetAt, now)
|
|
68
|
+
: undefined;
|
|
69
|
+
return countdown ? `${percentage} ${countdown}` : percentage;
|
|
70
|
+
}
|
|
71
|
+
const bar = formatDualLimitBar(report.primary, report.secondary);
|
|
72
|
+
if (isQuotaWindowExhausted(report.primary) && report.primary.resetAt) {
|
|
73
|
+
const primaryCountdown = formatResetCountdown(report.primary.resetAt, now);
|
|
74
|
+
const weeklyCountdown = report.secondary.resetAt
|
|
75
|
+
? formatResetCountdown(report.secondary.resetAt, now)
|
|
76
|
+
: undefined;
|
|
77
|
+
return weeklyCountdown
|
|
78
|
+
? `${bar} ${primaryCountdown}/${weeklyCountdown}`
|
|
79
|
+
: `${bar} ${primaryCountdown}`;
|
|
80
|
+
}
|
|
81
|
+
const countdown = formatWeeklyResetCountdown(report, now);
|
|
82
|
+
return countdown ? `${bar} ${countdown}` : bar;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatWeeklyResetCountdown(
|
|
86
|
+
report: ClaudeUsageReport,
|
|
87
|
+
now = Date.now(),
|
|
88
|
+
): string | undefined {
|
|
89
|
+
const resetAt = weeklyWindow(report)?.resetAt;
|
|
90
|
+
if (resetAt === undefined) return undefined;
|
|
91
|
+
return formatResetCountdown(resetAt, now);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatResetCountdown(
|
|
95
|
+
resetAt: number,
|
|
96
|
+
now = Date.now(),
|
|
97
|
+
): string {
|
|
98
|
+
const remainingMs = Math.max(0, resetAt - now);
|
|
99
|
+
if (remainingMs > DAY_MS) {
|
|
100
|
+
const dayTenths = Math.max(10, Math.ceil(remainingMs / DAY_TENTH_MS));
|
|
101
|
+
return `${formatTenths(dayTenths)}d`;
|
|
102
|
+
}
|
|
103
|
+
if (remainingMs >= HOUR_MS) {
|
|
104
|
+
const hourTenths = Math.max(10, Math.ceil(remainingMs / HOUR_TENTH_MS));
|
|
105
|
+
return `${formatTenths(hourTenths)}h`;
|
|
106
|
+
}
|
|
107
|
+
if (remainingMs >= MINUTE_MS)
|
|
108
|
+
return `${Math.floor(remainingMs / MINUTE_MS)}m`;
|
|
109
|
+
return `${Math.floor(remainingMs / SECOND_MS)}s`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
export function nextResetCountdownDelayMs(
|
|
114
|
+
report: ClaudeUsageReport,
|
|
115
|
+
now = Date.now(),
|
|
116
|
+
): number | undefined {
|
|
117
|
+
const resetTimes = [weeklyWindow(report)?.resetAt];
|
|
118
|
+
if (report.secondary && isQuotaWindowExhausted(report.primary)) {
|
|
119
|
+
resetTimes.push(report.primary?.resetAt);
|
|
120
|
+
}
|
|
121
|
+
const delays = resetTimes
|
|
122
|
+
.filter((resetAt): resetAt is number => resetAt !== undefined)
|
|
123
|
+
.map((resetAt) => nextResetCountdownDelayForRemainingMs(resetAt - now))
|
|
124
|
+
.filter((delay): delay is number => delay !== undefined);
|
|
125
|
+
return delays.length > 0 ? Math.min(...delays) : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function nextResetCountdownDelayForRemainingMs(
|
|
129
|
+
remainingMs: number,
|
|
130
|
+
): number | undefined {
|
|
131
|
+
if (remainingMs <= 0) return undefined;
|
|
132
|
+
if (remainingMs > DAY_MS) {
|
|
133
|
+
const dayTenths = Math.max(10, Math.ceil(remainingMs / DAY_TENTH_MS));
|
|
134
|
+
return Math.max(1, remainingMs - (dayTenths - 1) * DAY_TENTH_MS);
|
|
135
|
+
}
|
|
136
|
+
if (remainingMs >= HOUR_MS) {
|
|
137
|
+
const hourTenths = Math.max(10, Math.ceil(remainingMs / HOUR_TENTH_MS));
|
|
138
|
+
if (hourTenths === 10) return Math.max(1, remainingMs - HOUR_MS + 1);
|
|
139
|
+
return Math.max(1, remainingMs - (hourTenths - 1) * HOUR_TENTH_MS);
|
|
140
|
+
}
|
|
141
|
+
if (remainingMs >= MINUTE_MS) {
|
|
142
|
+
return Math.max(
|
|
143
|
+
1,
|
|
144
|
+
remainingMs - Math.floor(remainingMs / MINUTE_MS) * MINUTE_MS + 1,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return Math.max(
|
|
148
|
+
1,
|
|
149
|
+
remainingMs - Math.floor(remainingMs / SECOND_MS) * SECOND_MS + 1,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatTenths(value: number): string {
|
|
154
|
+
return value % 10 === 0 ? String(value / 10) : (value / 10).toFixed(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatStatuslineText(
|
|
158
|
+
ctx: ExtensionContext,
|
|
159
|
+
value: string,
|
|
160
|
+
): string {
|
|
161
|
+
const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
|
|
162
|
+
return `${label} ${ctx.ui.theme.fg("dim", value)}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatStatuslineBarText(
|
|
166
|
+
ctx: ExtensionContext,
|
|
167
|
+
bar: string,
|
|
168
|
+
background: "userMessageBg" | "toolErrorBg",
|
|
169
|
+
): string {
|
|
170
|
+
const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
|
|
171
|
+
const value = ctx.ui.theme.bg(background, ctx.ui.theme.fg("dim", bar));
|
|
172
|
+
return `${label} ${value}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function formatClaudeUsageLoadingBar(frame: number): string {
|
|
176
|
+
const totalParts = DUAL_BAR_WIDTH * 2;
|
|
177
|
+
const cycleFrames = (totalParts - 1) * 2;
|
|
178
|
+
const finiteFrame = Number.isFinite(frame) ? frame : 0;
|
|
179
|
+
const cycleFrame = Math.abs(Math.trunc(finiteFrame)) % cycleFrames;
|
|
180
|
+
const primaryPart =
|
|
181
|
+
cycleFrame < totalParts ? cycleFrame : cycleFrames - cycleFrame;
|
|
182
|
+
const secondaryPart = totalParts - primaryPart - 1;
|
|
183
|
+
const masks = Array<number>(DUAL_BAR_WIDTH).fill(0);
|
|
184
|
+
const primaryCell = Math.floor(primaryPart / 2);
|
|
185
|
+
const secondaryCell = Math.floor(secondaryPart / 2);
|
|
186
|
+
masks[primaryCell] |= primaryPart % 2 === 0 ? 1 : 2;
|
|
187
|
+
masks[secondaryCell] |= secondaryPart % 2 === 0 ? 4 : 8;
|
|
188
|
+
return masks.map((mask) => DUAL_BAR_CHARS[mask]).join("");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function formatStatuslineLoading(
|
|
192
|
+
ctx: ExtensionContext,
|
|
193
|
+
frame: number,
|
|
194
|
+
): string {
|
|
195
|
+
return formatStatuslineBarText(
|
|
196
|
+
ctx,
|
|
197
|
+
formatClaudeUsageLoadingBar(frame),
|
|
198
|
+
"userMessageBg",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function formatStatuslineProblem(
|
|
203
|
+
ctx: ExtensionContext,
|
|
204
|
+
errors: UsageQueryError[],
|
|
205
|
+
): string {
|
|
206
|
+
const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
|
|
207
|
+
const value = isUsageUnavailable(errors)
|
|
208
|
+
? ctx.ui.theme.fg("muted", "n/a")
|
|
209
|
+
: ctx.ui.theme.fg("error", "error");
|
|
210
|
+
return `${label} ${value}`;
|
|
211
|
+
}
|
|
212
|
+
function formatAdaptiveLimitBar(
|
|
213
|
+
primary: NormalizedRateLimitWindow | undefined,
|
|
214
|
+
secondary: NormalizedRateLimitWindow | undefined,
|
|
215
|
+
): string {
|
|
216
|
+
if (!primary || !secondary) return formatSingleLimitBar(primary ?? secondary);
|
|
217
|
+
return formatDualLimitBar(primary, secondary);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function formatSingleLimitBar(
|
|
221
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
222
|
+
): string {
|
|
223
|
+
const filled = filledParts(window, DUAL_BAR_WIDTH * 4);
|
|
224
|
+
const partMasks = [1, 4, 2, 8];
|
|
225
|
+
let value = "";
|
|
226
|
+
for (let index = 0; index < DUAL_BAR_WIDTH; index++) {
|
|
227
|
+
const cellParts = Math.min(4, Math.max(0, filled - index * 4));
|
|
228
|
+
let mask = 0;
|
|
229
|
+
for (let part = 0; part < cellParts; part++) mask |= partMasks[part] ?? 0;
|
|
230
|
+
value += DUAL_BAR_CHARS[mask];
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatDualLimitBar(
|
|
236
|
+
primary: NormalizedRateLimitWindow,
|
|
237
|
+
secondary: NormalizedRateLimitWindow,
|
|
238
|
+
): string {
|
|
239
|
+
const primaryParts = filledTwentieths(primary);
|
|
240
|
+
const secondaryParts = filledTwentieths(secondary);
|
|
241
|
+
let value = "";
|
|
242
|
+
for (let index = 0; index < DUAL_BAR_WIDTH; index++) {
|
|
243
|
+
const leftPart = index * 2 + 1;
|
|
244
|
+
const rightPart = leftPart + 1;
|
|
245
|
+
let mask = 0;
|
|
246
|
+
if (primaryParts >= leftPart) mask |= 1;
|
|
247
|
+
if (primaryParts >= rightPart) mask |= 2;
|
|
248
|
+
if (secondaryParts >= leftPart) mask |= 4;
|
|
249
|
+
if (secondaryParts >= rightPart) mask |= 8;
|
|
250
|
+
value += DUAL_BAR_CHARS[mask];
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function filledTwentieths(
|
|
256
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
257
|
+
): number {
|
|
258
|
+
return filledParts(window, 20);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function filledParts(
|
|
262
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
263
|
+
totalParts: number,
|
|
264
|
+
): number {
|
|
265
|
+
if (!window) return 0;
|
|
266
|
+
const remaining = remainingPercent(window);
|
|
267
|
+
if (remaining <= 0) return 0;
|
|
268
|
+
return Math.max(1, Math.round(remaining / (100 / totalParts)));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function remainingPercent(window: NormalizedRateLimitWindow): number {
|
|
272
|
+
return 100 - clampPercent(window.usedPercent);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isQuotaWindowExhausted(
|
|
276
|
+
window: NormalizedRateLimitWindow | undefined,
|
|
277
|
+
): boolean {
|
|
278
|
+
return window !== undefined && remainingPercent(window) <= 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function hasExhaustedQuotaWindow(report: ClaudeUsageReport): boolean {
|
|
282
|
+
return (
|
|
283
|
+
isQuotaWindowExhausted(report.primary) ||
|
|
284
|
+
isQuotaWindowExhausted(report.secondary)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function weeklyWindow(
|
|
289
|
+
report: ClaudeUsageReport,
|
|
290
|
+
): NormalizedRateLimitWindow | undefined {
|
|
291
|
+
return report.secondary ?? report.primary;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function clampPercent(value: number): number {
|
|
295
|
+
if (!Number.isFinite(value)) return 0;
|
|
296
|
+
return Math.min(100, Math.max(0, value));
|
|
297
|
+
}
|