pi-ark-usage 0.1.2 → 0.1.4
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 +3 -4
- package/extensions/index.ts +32 -105
- package/extensions/lib/types.ts +0 -7
- package/extensions/lib/widget.ts +49 -241
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,9 +10,8 @@ Volc Usage: sess 30%(reset after 4h18m) / wk 37%(reset after 4d) / mo 19%(reset
|
|
|
10
10
|
|
|
11
11
|
- **AK/SK 直连 OpenAPI**:不走子进程、不依赖 SSO 登录态;凭据只从环境变量或配置文件读取,本扩展不保存、不上传任何 Key
|
|
12
12
|
- **三个周期同屏**:session(会话)/ weekly(周)/ monthly(月),并显示各周期重置倒计时
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- **产品切换感知**:在个人版/团队版、Coding Plan/Agent Plan 之间切换时标注 `(变更)`;套餐周期重置时标注 `(已重置)`
|
|
13
|
+
- **Powerline 状态段(唯一展示位)**:通过 `ctx.ui.setStatus("ark-usage", …)` 发布单行摘要(`1% 4h38m | 15% 5d4h | 54% 23d4h`,固定按 session/weekly/monthly 顺序),显示在 [pi-powerline-footer](https://www.npmjs.com/package/pi-powerline-footer) 上;百分比按阈值绿/黄/红着色,时间部分紫色,分隔符跟随主题 `thinkingHigh`(编辑器边框色)。不安装 powerline 时也可通过内置 footer 的 extension statuses 区域或 `/arkusage` 查看
|
|
14
|
+
- **阈值着色**:低用量绿色(<50%)、接近上限黄色(50–79%)、超过红线红色(≥80%,默认值,可用 `/arkset` 调整)
|
|
16
15
|
- **持久化缓存**:重启 pi 后立即显示上次快照(`~/.pi/agent/pi-ark-usage/cache.json`,0600 权限,临时文件原子写入)
|
|
17
16
|
- 可选空闲自动刷新(默认关闭)
|
|
18
17
|
|
|
@@ -98,7 +97,7 @@ pi install npm:pi-ark-usage
|
|
|
98
97
|
1. 用官方 `@volcengine/openapi` 的 `Signer` 对 `POST open.volcengineapi.com?Action=GetCodingPlanUsage&Version=2024-01-01`(service `ark`,region `cn-beijing`)做 V4 签名后 fetch。
|
|
99
98
|
2. 解析 `Result.QuotaUsage[]`(`Level/Percent/ResetTimestamp`,秒级时间戳转 `+08:00` ISO 字符串),做结构与取值校验。
|
|
100
99
|
3. 快照存入内存 Map,并以原子写(tmp 文件 + rename)持久化到 `~/.pi/agent/pi-ark-usage/`。
|
|
101
|
-
4.
|
|
100
|
+
4. 摘要通过 `ctx.ui.setStatus("ark-usage", …)` 发布到 extension status,由 pi-powerline-footer 渲染到 powerline 行;每分钟 ticker 在空闲时重发,保持重置倒计时刷新。
|
|
102
101
|
|
|
103
102
|
## 开发
|
|
104
103
|
|
package/extensions/index.ts
CHANGED
|
@@ -6,14 +6,11 @@
|
|
|
6
6
|
* the official `@volcengine/openapi` Signer. This extension never stores keys:
|
|
7
7
|
* credentials come from VOLC_ACCESSKEY/VOLC_SECRETKEY or ~/.volc/config.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Powerline status (via `ctx.ui.setStatus`): 1% 4h38m | 15% 5d4h | 54% 23d4h
|
|
10
10
|
* Commands:
|
|
11
11
|
* /arkcheck force refresh
|
|
12
12
|
* /arkusage show snapshot in a notification
|
|
13
13
|
* /arkset ... view/change settings
|
|
14
|
-
*
|
|
15
|
-
* Design modeled after pi-check-agent-quota: baseline snapshot taken at
|
|
16
|
-
* agent_start, compared at agent_settled to annotate per-round consumption.
|
|
17
14
|
*/
|
|
18
15
|
|
|
19
16
|
import type {
|
|
@@ -23,10 +20,9 @@ import type {
|
|
|
23
20
|
} from "@earendil-works/pi-coding-agent";
|
|
24
21
|
import { fetchPlan } from "./lib/arkcli.js";
|
|
25
22
|
import {
|
|
26
|
-
ArkUsageComponent,
|
|
27
23
|
AUTO_REFRESH_MAX_MINUTES,
|
|
28
24
|
LOCALES,
|
|
29
|
-
|
|
25
|
+
buildCompactStatus,
|
|
30
26
|
formatRemaining,
|
|
31
27
|
getSettings,
|
|
32
28
|
patchSettings,
|
|
@@ -44,10 +40,10 @@ import type {
|
|
|
44
40
|
ArkSettings,
|
|
45
41
|
Language,
|
|
46
42
|
PlanSnapshot,
|
|
47
|
-
RenderItem,
|
|
48
43
|
} from "./lib/types.js";
|
|
49
44
|
|
|
50
|
-
|
|
45
|
+
/** Extension-status key consumed by pi-powerline-footer (powerline slot). */
|
|
46
|
+
const STATUS_KEY = "ark-usage";
|
|
51
47
|
const TICK_MS = 60_000;
|
|
52
48
|
const AGENT_START_TIMEOUT_MS = 3_000;
|
|
53
49
|
const BASELINE_FRESH_MS = 60 * 60_000;
|
|
@@ -84,10 +80,6 @@ let language: Language = "en";
|
|
|
84
80
|
const snapshots = new Map<string, PlanSnapshot>();
|
|
85
81
|
let activeProduct: ProductId | null = null;
|
|
86
82
|
let status: Status = "ok";
|
|
87
|
-
let deltas: Record<string, number> | null = null;
|
|
88
|
-
let note: "changed" | "reset" | null = null;
|
|
89
|
-
let baseline: PlanSnapshot | null = null;
|
|
90
|
-
let activeWidget: ArkUsageComponent | null = null;
|
|
91
83
|
let tickTimer: ReturnType<typeof setInterval> | null = null;
|
|
92
84
|
let lastCtx: ExtensionContext | null = null;
|
|
93
85
|
let lastAutoAt = 0;
|
|
@@ -125,34 +117,19 @@ function activeSnapshot(): PlanSnapshot | null {
|
|
|
125
117
|
|
|
126
118
|
/* -------------------------------- rendering -------------------------------- */
|
|
127
119
|
|
|
128
|
-
function
|
|
120
|
+
function renderStatus(ctx: ExtensionContext): void {
|
|
129
121
|
lastCtx = ctx;
|
|
130
122
|
const snap = activeSnapshot();
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
activeWidget.update(items);
|
|
141
|
-
return;
|
|
123
|
+
const widgetStatus = status === "ok" && !snap ? "none" : status;
|
|
124
|
+
// Publish the one-line summary to the extension-status slot (powerline).
|
|
125
|
+
// The ticker calls renderStatus every minute while idle, keeping the
|
|
126
|
+
// reset countdown fresh without refetching.
|
|
127
|
+
const compact = buildCompactStatus(snap, widgetStatus, ctx.ui.theme);
|
|
128
|
+
if (compact) {
|
|
129
|
+
ctx.ui.setStatus(STATUS_KEY, compact);
|
|
130
|
+
} else {
|
|
131
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
142
132
|
}
|
|
143
|
-
ctx.ui.setWidget(
|
|
144
|
-
WIDGET_KEY,
|
|
145
|
-
(tui, theme) => {
|
|
146
|
-
const component = new ArkUsageComponent(
|
|
147
|
-
items,
|
|
148
|
-
() => theme,
|
|
149
|
-
() => tui.requestRender?.(),
|
|
150
|
-
);
|
|
151
|
-
activeWidget = component;
|
|
152
|
-
return component;
|
|
153
|
-
},
|
|
154
|
-
{ placement: "belowEditor" },
|
|
155
|
-
);
|
|
156
133
|
}
|
|
157
134
|
|
|
158
135
|
/* --------------------------------- fetching -------------------------------- */
|
|
@@ -180,7 +157,7 @@ async function refresh(
|
|
|
180
157
|
activeProduct = product;
|
|
181
158
|
if (!product) {
|
|
182
159
|
status = "none";
|
|
183
|
-
|
|
160
|
+
renderStatus(ctx);
|
|
184
161
|
return false;
|
|
185
162
|
}
|
|
186
163
|
|
|
@@ -190,8 +167,7 @@ async function refresh(
|
|
|
190
167
|
}
|
|
191
168
|
|
|
192
169
|
status = "fetching";
|
|
193
|
-
|
|
194
|
-
renderWidget(ctx);
|
|
170
|
+
renderStatus(ctx);
|
|
195
171
|
|
|
196
172
|
const controller = new AbortController();
|
|
197
173
|
inFlight = controller;
|
|
@@ -229,7 +205,7 @@ async function refresh(
|
|
|
229
205
|
return false;
|
|
230
206
|
} finally {
|
|
231
207
|
if (inFlight === controller) inFlight = null;
|
|
232
|
-
|
|
208
|
+
renderStatus(ctx);
|
|
233
209
|
}
|
|
234
210
|
}
|
|
235
211
|
|
|
@@ -239,66 +215,21 @@ function persist(): void {
|
|
|
239
215
|
saveCache(language, out);
|
|
240
216
|
}
|
|
241
217
|
|
|
242
|
-
/*
|
|
243
|
-
|
|
244
|
-
function computeDeltas(
|
|
245
|
-
before: PlanSnapshot,
|
|
246
|
-
after: PlanSnapshot,
|
|
247
|
-
): Record<string, number> | null {
|
|
248
|
-
const out: Record<string, number> = {};
|
|
249
|
-
for (const p of after.periods) {
|
|
250
|
-
const old = before.periods.find((q) => q.label === p.label);
|
|
251
|
-
if (old) out[p.label] = Math.round(old.percent - p.percent);
|
|
252
|
-
// note: stored as consumed-positive; renderer negates for display
|
|
253
|
-
}
|
|
254
|
-
// flip sign convention for renderer: it wants negative=consumed
|
|
255
|
-
const render: Record<string, number> = {};
|
|
256
|
-
for (const [k, v] of Object.entries(out)) render[k] = -v;
|
|
257
|
-
return render;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function looksReset(a: PlanSnapshot, b: PlanSnapshot): boolean {
|
|
261
|
-
// all periods dropped sharply at once => plan period reset
|
|
262
|
-
return b.periods.every((p) => {
|
|
263
|
-
const old = a.periods.find((q) => q.label === p.label);
|
|
264
|
-
return old !== undefined && p.percent - old.percent < -30;
|
|
265
|
-
});
|
|
266
|
-
}
|
|
218
|
+
/* ----------------------- session-start / settled --------------------------- */
|
|
267
219
|
|
|
268
220
|
async function handleStart(ctx: ExtensionContext): Promise<void> {
|
|
269
221
|
const product = resolveProduct(ctx);
|
|
270
222
|
if (!product) return;
|
|
271
223
|
const cached = snapshots.get(product) ?? null;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const win = await timeoutResult(refresh(ctx, "stale"), AGENT_START_TIMEOUT_MS);
|
|
276
|
-
baseline = win ? snapshots.get(product) ?? null : cached;
|
|
224
|
+
// Only refetch at agent_start when the cached snapshot went stale.
|
|
225
|
+
if (!cached || Date.now() - cached.fetchedAt > BASELINE_FRESH_MS) {
|
|
226
|
+
await timeoutResult(refresh(ctx, "stale"), AGENT_START_TIMEOUT_MS);
|
|
277
227
|
}
|
|
278
|
-
|
|
279
|
-
renderWidget(ctx);
|
|
228
|
+
renderStatus(ctx);
|
|
280
229
|
}
|
|
281
230
|
|
|
282
231
|
async function handleSettled(ctx: ExtensionContext): Promise<void> {
|
|
283
|
-
|
|
284
|
-
if (!product) return;
|
|
285
|
-
const ok = await refresh(ctx, "settled");
|
|
286
|
-
if (!ok) return;
|
|
287
|
-
const snap = snapshots.get(product);
|
|
288
|
-
if (!snap) return;
|
|
289
|
-
|
|
290
|
-
if (baseline && baseline.product !== product) {
|
|
291
|
-
note = "changed";
|
|
292
|
-
deltas = null;
|
|
293
|
-
} else if (baseline && looksReset(baseline, snap)) {
|
|
294
|
-
note = "reset";
|
|
295
|
-
deltas = null;
|
|
296
|
-
} else if (baseline) {
|
|
297
|
-
deltas = computeDeltas(baseline, snap);
|
|
298
|
-
note = null;
|
|
299
|
-
}
|
|
300
|
-
baseline = null;
|
|
301
|
-
renderWidget(ctx);
|
|
232
|
+
await refresh(ctx, "settled");
|
|
302
233
|
}
|
|
303
234
|
|
|
304
235
|
async function timeoutResult<T>(p: Promise<T>, ms: number): Promise<T | null> {
|
|
@@ -329,7 +260,7 @@ function ensureTicker(): void {
|
|
|
329
260
|
return;
|
|
330
261
|
}
|
|
331
262
|
// age text ticks every minute even without refetch
|
|
332
|
-
if (
|
|
263
|
+
if (lastCtx && lastCtx.isIdle()) renderStatus(lastCtx);
|
|
333
264
|
}, TICK_MS);
|
|
334
265
|
tickTimer.unref?.();
|
|
335
266
|
}
|
|
@@ -395,7 +326,7 @@ async function cmdSet(
|
|
|
395
326
|
const notifySaved = (): void => {
|
|
396
327
|
persist();
|
|
397
328
|
ctx.ui.notify(locale.settingsApplied, "info");
|
|
398
|
-
if (lastCtx)
|
|
329
|
+
if (lastCtx) renderStatus(lastCtx);
|
|
399
330
|
};
|
|
400
331
|
|
|
401
332
|
if (parts.length === 0) {
|
|
@@ -409,7 +340,7 @@ async function cmdSet(
|
|
|
409
340
|
resetSettings();
|
|
410
341
|
persist();
|
|
411
342
|
ctx.ui.notify(locale.settingsReset, "info");
|
|
412
|
-
if (lastCtx)
|
|
343
|
+
if (lastCtx) renderStatus(lastCtx);
|
|
413
344
|
return;
|
|
414
345
|
}
|
|
415
346
|
case "yellow":
|
|
@@ -486,7 +417,7 @@ async function cmdSet(
|
|
|
486
417
|
language = raw;
|
|
487
418
|
persist();
|
|
488
419
|
ctx.ui.notify(locale.languageChanged(language), "info");
|
|
489
|
-
if (lastCtx)
|
|
420
|
+
if (lastCtx) renderStatus(lastCtx);
|
|
490
421
|
return;
|
|
491
422
|
}
|
|
492
423
|
default:
|
|
@@ -519,25 +450,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
519
450
|
});
|
|
520
451
|
|
|
521
452
|
pi.on("session_start", (_event, ctx) => {
|
|
522
|
-
// Session switches reuse this module instance in-process: pi has
|
|
523
|
-
//
|
|
524
|
-
//
|
|
453
|
+
// Session switches reuse this module instance in-process: pi has torn
|
|
454
|
+
// down the previous runtime, so revive the shutdown flags before doing
|
|
455
|
+
// anything else.
|
|
525
456
|
shuttingDown = false;
|
|
526
457
|
resetShutdownState();
|
|
527
|
-
activeWidget = null;
|
|
528
458
|
activeProduct = resolveProduct(ctx);
|
|
529
|
-
|
|
530
|
-
note = null;
|
|
531
|
-
renderWidget(ctx);
|
|
459
|
+
renderStatus(ctx);
|
|
532
460
|
ensureTicker();
|
|
533
461
|
void refresh(ctx, "session_start");
|
|
534
462
|
});
|
|
535
463
|
|
|
536
464
|
pi.on("model_select", (_event, ctx) => {
|
|
537
465
|
const next = resolveProduct(ctx);
|
|
538
|
-
if (baseline && next && baseline.product !== next) note = "changed";
|
|
539
466
|
activeProduct = next;
|
|
540
|
-
|
|
467
|
+
renderStatus(ctx);
|
|
541
468
|
void refresh(ctx, "model_select");
|
|
542
469
|
});
|
|
543
470
|
|
package/extensions/lib/types.ts
CHANGED
|
@@ -4,13 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
export type Language = "zh" | "en";
|
|
6
6
|
|
|
7
|
-
/** Render model, modeled after pi-check-agent-quota. */
|
|
8
|
-
export type RenderItem =
|
|
9
|
-
| { kind: "text"; text: string }
|
|
10
|
-
| { kind: "pct"; pct: number; metric: string }
|
|
11
|
-
| { kind: "annotation"; text: string }
|
|
12
|
-
| { kind: "age"; text: string };
|
|
13
|
-
|
|
14
7
|
/** User-tunable settings (persisted in the disk cache). */
|
|
15
8
|
export type ArkSettings = {
|
|
16
9
|
/** yellow threshold, percent */
|
package/extensions/lib/widget.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rendering
|
|
3
|
-
*
|
|
2
|
+
* Rendering for the powerline extension-status slot: locales, threshold
|
|
3
|
+
* settings, percent coloring, remaining-time formatting, and the compact
|
|
4
|
+
* colored one-line summary published via `ctx.ui.setStatus`.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import type { ArkSettings, Language, PlanSnapshot
|
|
8
|
+
import type { ArkSettings, Language, PlanSnapshot } from "./types.js";
|
|
8
9
|
|
|
9
10
|
/* ---------------------------------- i18n ---------------------------------- */
|
|
10
11
|
|
|
@@ -20,10 +21,6 @@ type Locale = {
|
|
|
20
21
|
changed: string;
|
|
21
22
|
reset: string;
|
|
22
23
|
resetsIn: (t: string) => string;
|
|
23
|
-
ageNow: string;
|
|
24
|
-
ageMinutesAgo: (m: number) => string;
|
|
25
|
-
ageHoursAgo: (h: number, m: number) => string;
|
|
26
|
-
ageDaysAgo: (d: number, h: number) => string;
|
|
27
24
|
noActiveProduct: string;
|
|
28
25
|
settingsShow: (s: ArkSettings) => string;
|
|
29
26
|
settingsApplied: string;
|
|
@@ -56,10 +53,6 @@ const ZH: Locale = {
|
|
|
56
53
|
changed: "变更",
|
|
57
54
|
reset: "已重置",
|
|
58
55
|
resetsIn: (t) => `${t}后重置`,
|
|
59
|
-
ageNow: "刚刚",
|
|
60
|
-
ageMinutesAgo: (m) => `${m}分钟前`,
|
|
61
|
-
ageHoursAgo: (h, m) => (m > 0 ? `${h}小时${m}分前` : `${h}小时前`),
|
|
62
|
-
ageDaysAgo: (d, h) => (h > 0 ? `${d}天${h}小时前` : `${d}天前`),
|
|
63
56
|
noActiveProduct: "当前没有可查询的火山套餐",
|
|
64
57
|
settingsShow: (s) =>
|
|
65
58
|
`当前设置:产品=${s.product} 黄线=${s.pctYellow} 红线=${s.pctRed} 自动刷新=${s.autoRefreshMinutes}分钟 seat=${s.seat || "无"}`,
|
|
@@ -94,10 +87,6 @@ const EN: Locale = {
|
|
|
94
87
|
changed: "changed",
|
|
95
88
|
reset: "reset",
|
|
96
89
|
resetsIn: (t) => `reset after ${t}`,
|
|
97
|
-
ageNow: "now",
|
|
98
|
-
ageMinutesAgo: (m) => `${m}m ago`,
|
|
99
|
-
ageHoursAgo: (h, m) => (m > 0 ? `${h}h${m}m ago` : `${h}h ago`),
|
|
100
|
-
ageDaysAgo: (d, h) => (h > 0 ? `${d}d${h}h ago` : `${d}d ago`),
|
|
101
90
|
noActiveProduct: "No active Volcengine plan",
|
|
102
91
|
settingsShow: (s) =>
|
|
103
92
|
`Settings: product=${s.product} yellow=${s.pctYellow} red=${s.pctRed} autoRefresh=${s.autoRefreshMinutes}m seat=${s.seat || "none"}`,
|
|
@@ -122,7 +111,7 @@ export const LOCALES: Record<Language, Locale> = { zh: ZH, en: EN };
|
|
|
122
111
|
export const AUTO_REFRESH_MAX_MINUTES = 30;
|
|
123
112
|
|
|
124
113
|
export const DEFAULT_SETTINGS: ArkSettings = {
|
|
125
|
-
pctYellow:
|
|
114
|
+
pctYellow: 50,
|
|
126
115
|
pctRed: 80,
|
|
127
116
|
autoRefreshMinutes: 5,
|
|
128
117
|
product: "auto",
|
|
@@ -196,237 +185,56 @@ export function formatRemaining(ms: number, lang: Language): string {
|
|
|
196
185
|
return `${m}m`;
|
|
197
186
|
}
|
|
198
187
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
188
|
+
/* ---------------------- compact status (powerline slot) -------------------- */
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Compact reset countdown for the powerline status: `38m`, `4h38m`, `5d4h`.
|
|
192
|
+
* Lower-precision units are dropped to keep the line short and stable.
|
|
193
|
+
*/
|
|
194
|
+
export function formatCompactRemaining(ms: number): string {
|
|
195
|
+
if (!Number.isFinite(ms) || ms <= 0) return "";
|
|
196
|
+
const totalMin = Math.floor(ms / 60_000);
|
|
197
|
+
const d = Math.floor(totalMin / 1440);
|
|
198
|
+
const h = Math.floor((totalMin % 1440) / 60);
|
|
205
199
|
const m = totalMin % 60;
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
return LOCALES[lang].ageMinutesAgo(m);
|
|
200
|
+
if (d > 0) return `${d}d${h}h`;
|
|
201
|
+
if (h > 0) return `${h}h${m}m`;
|
|
202
|
+
return `${m}m`;
|
|
210
203
|
}
|
|
211
204
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
205
|
+
/**
|
|
206
|
+
* One-line colored summary for the powerline extension-status slot:
|
|
207
|
+
* `1% 4h38m | 15% 5d4h | 54% 23d4h`
|
|
208
|
+
*
|
|
209
|
+
* Periods are shown in API order (session / weekly / monthly) without
|
|
210
|
+
* labels — the order itself is the convention. Percent follows the
|
|
211
|
+
* yellow/red thresholds (green below yellow, yellow below red, red at/above
|
|
212
|
+
* red); the time part is always purple; the ` | ` separator uses the
|
|
213
|
+
* editor border color (`thinkingHigh`, pink in the dark theme).
|
|
214
|
+
*
|
|
215
|
+
* Returns null when nothing should be shown (the powerline item hides).
|
|
216
|
+
*/
|
|
217
|
+
export function buildCompactStatus(
|
|
217
218
|
snapshot: PlanSnapshot | null,
|
|
218
|
-
lang: Language,
|
|
219
219
|
status: WidgetStatus,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
deltas: Record<string, number> | null,
|
|
223
|
-
note?: "changed" | "reset" | null,
|
|
224
|
-
): RenderItem[] {
|
|
225
|
-
const locale = LOCALES[lang];
|
|
226
|
-
const items: RenderItem[] = [];
|
|
227
|
-
|
|
220
|
+
theme: Theme,
|
|
221
|
+
): string | null {
|
|
228
222
|
if (snapshot && snapshot.periods.length > 0) {
|
|
229
|
-
|
|
230
|
-
snapshot.periods
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
if (p.reset_at) {
|
|
241
|
-
const remaining = new Date(p.reset_at).getTime() - Date.now();
|
|
242
|
-
const t = formatRemaining(remaining, lang);
|
|
243
|
-
if (t) items.push({ kind: "annotation", text: `(${locale.resetsIn(t)})` });
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
} else if (status === "fetching") {
|
|
247
|
-
items.push({ kind: "text", text: `${locale.title}: ` });
|
|
248
|
-
items.push({ kind: "annotation", text: locale.fetching });
|
|
249
|
-
} else if (status === "failed") {
|
|
250
|
-
items.push({ kind: "text", text: `${locale.title}: ` });
|
|
251
|
-
items.push({ kind: "annotation", text: locale.failed });
|
|
252
|
-
} else if (status === "no-binary") {
|
|
253
|
-
items.push({ kind: "annotation", text: locale.noBinary });
|
|
254
|
-
} else {
|
|
255
|
-
items.push({ kind: "annotation", text: locale.noData });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (!isIdle) {
|
|
259
|
-
items.push({ kind: "annotation", text: ` (${locale.using})` });
|
|
260
|
-
} else if (note) {
|
|
261
|
-
items.push({ kind: "annotation", text: ` (${note === "reset" ? locale.reset : locale.changed})` });
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (snapshot) {
|
|
265
|
-
items.push({ kind: "age", text: formatAge(snapshot.fetchedAt, lang) });
|
|
266
|
-
}
|
|
267
|
-
return items;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function formatItems(items: RenderItem[], theme: Theme): string {
|
|
271
|
-
return items
|
|
272
|
-
.map((it) => {
|
|
273
|
-
switch (it.kind) {
|
|
274
|
-
case "text":
|
|
275
|
-
return theme.fg("dim", it.text);
|
|
276
|
-
case "pct":
|
|
277
|
-
return coloredPct(it.pct);
|
|
278
|
-
case "age":
|
|
279
|
-
return theme.fg("dim", it.text);
|
|
280
|
-
case "annotation":
|
|
281
|
-
default:
|
|
282
|
-
return hexFg(HEX_COLORS.purple, it.text);
|
|
283
|
-
}
|
|
284
|
-
})
|
|
285
|
-
.join("");
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/* ------------------------------ ansi helpers ------------------------------ */
|
|
289
|
-
|
|
290
|
-
export const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
291
|
-
|
|
292
|
-
export function visibleWidth(s: string): number {
|
|
293
|
-
let w = 0;
|
|
294
|
-
for (const ch of s.replace(ANSI_RE, "")) {
|
|
295
|
-
const cp = ch.codePointAt(0) ?? 0;
|
|
296
|
-
if (cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f)) continue;
|
|
297
|
-
if (cp >= 0x0300 && cp <= 0x036f) continue;
|
|
298
|
-
w +=
|
|
299
|
-
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
300
|
-
(cp >= 0x2e80 && cp <= 0xa4cf) ||
|
|
301
|
-
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
302
|
-
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
303
|
-
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
304
|
-
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
305
|
-
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
306
|
-
(cp >= 0x1f300 && cp <= 0x1f9ff)
|
|
307
|
-
? 2
|
|
308
|
-
: 1;
|
|
223
|
+
const sep = theme.fg("thinkingHigh", " | ");
|
|
224
|
+
return snapshot.periods
|
|
225
|
+
.map((p) => {
|
|
226
|
+
const pct = coloredPct(clampPct(p.percent));
|
|
227
|
+
const reset = p.reset_at
|
|
228
|
+
? formatCompactRemaining(new Date(p.reset_at).getTime() - Date.now())
|
|
229
|
+
: "";
|
|
230
|
+
return reset ? `${pct} ${hexFg(HEX_COLORS.purple, reset)}` : pct;
|
|
231
|
+
})
|
|
232
|
+
.join(sep);
|
|
309
233
|
}
|
|
310
|
-
return
|
|
234
|
+
if (status === "fetching") return hexFg(HEX_COLORS.purple, "…");
|
|
235
|
+
if (status === "failed") return hexFg(HEX_COLORS.red, "failed");
|
|
236
|
+
return null;
|
|
311
237
|
}
|
|
312
238
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (visibleWidth(s) <= maxWidth) return s;
|
|
316
|
-
const budget = maxWidth - 1;
|
|
317
|
-
let out = "";
|
|
318
|
-
let w = 0;
|
|
319
|
-
let i = 0;
|
|
320
|
-
while (i < s.length) {
|
|
321
|
-
ANSI_RE.lastIndex = i;
|
|
322
|
-
const m = ANSI_RE.exec(s);
|
|
323
|
-
if (m && m.index === i) {
|
|
324
|
-
out += m[0];
|
|
325
|
-
i = ANSI_RE.lastIndex;
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
const ch = String.fromCodePoint(s.codePointAt(i) ?? 0);
|
|
329
|
-
const cw = visibleWidth(ch);
|
|
330
|
-
if (w + cw > budget) break;
|
|
331
|
-
out += ch;
|
|
332
|
-
w += cw;
|
|
333
|
-
i += ch.length;
|
|
334
|
-
}
|
|
335
|
-
return `${out}…\x1b[0m`;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function itemsEqual(a: RenderItem[], b: RenderItem[]): boolean {
|
|
339
|
-
if (a.length !== b.length) return false;
|
|
340
|
-
return a.every((it, i) => {
|
|
341
|
-
const o = b[i];
|
|
342
|
-
if (it.kind !== o.kind) return false;
|
|
343
|
-
switch (it.kind) {
|
|
344
|
-
case "pct":
|
|
345
|
-
return (
|
|
346
|
-
o.kind === "pct" && it.pct === o.pct && it.metric === o.metric
|
|
347
|
-
);
|
|
348
|
-
case "text":
|
|
349
|
-
return o.kind === "text" && it.text === o.text;
|
|
350
|
-
case "age":
|
|
351
|
-
return o.kind === "age" && it.text === o.text;
|
|
352
|
-
case "annotation":
|
|
353
|
-
return o.kind === "annotation" && it.text === o.text;
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/* -------------------------------- component -------------------------------- */
|
|
359
|
-
|
|
360
|
-
export interface Component {
|
|
361
|
-
render(width: number): string[];
|
|
362
|
-
invalidate(): void;
|
|
363
|
-
dispose?(): void;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Widget rendered below the editor. The age segment is right-aligned and the
|
|
368
|
-
* content wraps to two lines when the terminal is too narrow.
|
|
369
|
-
*/
|
|
370
|
-
export class ArkUsageComponent implements Component {
|
|
371
|
-
private items: RenderItem[];
|
|
372
|
-
private readonly themeRef: () => Theme;
|
|
373
|
-
private readonly requestRender: () => void;
|
|
374
|
-
private cache: { width: number; lines: string[] } | null = null;
|
|
375
|
-
private disposed = false;
|
|
376
|
-
|
|
377
|
-
constructor(
|
|
378
|
-
items: RenderItem[],
|
|
379
|
-
themeRef: () => Theme,
|
|
380
|
-
requestRender: () => void,
|
|
381
|
-
) {
|
|
382
|
-
this.items = items;
|
|
383
|
-
this.themeRef = themeRef;
|
|
384
|
-
this.requestRender = requestRender;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
update(items: RenderItem[]): void {
|
|
388
|
-
if (this.disposed || itemsEqual(this.items, items)) return;
|
|
389
|
-
this.items = items;
|
|
390
|
-
this.invalidate();
|
|
391
|
-
this.requestRender();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
invalidate(): void {
|
|
395
|
-
this.cache = null;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
render(width: number): string[] {
|
|
399
|
-
if (this.cache && this.cache.width === width) return this.cache.lines;
|
|
400
|
-
const theme = this.themeRef();
|
|
401
|
-
|
|
402
|
-
const ageIdx = this.items.findIndex((it) => it.kind === "age");
|
|
403
|
-
if (ageIdx === -1) {
|
|
404
|
-
const text = formatItems(this.items, theme);
|
|
405
|
-
const lines = text ? [truncateAnsi(text, width)] : [];
|
|
406
|
-
this.cache = { width, lines };
|
|
407
|
-
return lines;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const leftText = formatItems(this.items.slice(0, ageIdx), theme);
|
|
411
|
-
const rightText = formatItems(this.items.slice(ageIdx), theme);
|
|
412
|
-
const lw = visibleWidth(leftText);
|
|
413
|
-
const rw = visibleWidth(rightText);
|
|
414
|
-
|
|
415
|
-
let lines: string[];
|
|
416
|
-
if (lw + rw + 1 <= width) {
|
|
417
|
-
lines = [leftText + " ".repeat(width - lw - rw) + rightText];
|
|
418
|
-
} else {
|
|
419
|
-
lines = [
|
|
420
|
-
leftText ? truncateAnsi(leftText, width) : "",
|
|
421
|
-
rw <= width ? " ".repeat(Math.max(0, width - rw)) + rightText : truncateAnsi(rightText, width),
|
|
422
|
-
].filter((l) => l !== "");
|
|
423
|
-
}
|
|
424
|
-
this.cache = { width, lines };
|
|
425
|
-
return lines;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
dispose(): void {
|
|
429
|
-
this.disposed = true;
|
|
430
|
-
this.cache = null;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
239
|
+
/** Lifecycle states mirrored in the compact status. */
|
|
240
|
+
export type WidgetStatus = "ok" | "fetching" | "failed" | "no-binary" | "none";
|