pi-ark-usage 0.1.3 → 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 CHANGED
@@ -10,10 +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
- - **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) 的 `extension_statuses` / customItems 上;百分比按阈值绿/黄/红着色,时间部分紫色,分隔符跟随主题 `thinkingHigh`(编辑器边框色)
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
14
  - **阈值着色**:低用量绿色(<50%)、接近上限黄色(50–79%)、超过红线红色(≥80%,默认值,可用 `/arkset` 调整)
15
- - **单轮消耗**:以每轮对话开始前的快照为基线,结算时标注各周期增量(如 `(+2)`)
16
- - **产品切换感知**:在个人版/团队版、Coding Plan/Agent Plan 之间切换时标注 `(变更)`;套餐周期重置时标注 `(已重置)`
17
15
  - **持久化缓存**:重启 pi 后立即显示上次快照(`~/.pi/agent/pi-ark-usage/cache.json`,0600 权限,临时文件原子写入)
18
16
  - 可选空闲自动刷新(默认关闭)
19
17
 
@@ -99,7 +97,7 @@ pi install npm:pi-ark-usage
99
97
  1. 用官方 `@volcengine/openapi` 的 `Signer` 对 `POST open.volcengineapi.com?Action=GetCodingPlanUsage&Version=2024-01-01`(service `ark`,region `cn-beijing`)做 V4 签名后 fetch。
100
98
  2. 解析 `Result.QuotaUsage[]`(`Level/Percent/ResetTimestamp`,秒级时间戳转 `+08:00` ISO 字符串),做结构与取值校验。
101
99
  3. 快照存入内存 Map,并以原子写(tmp 文件 + rename)持久化到 `~/.pi/agent/pi-ark-usage/`。
102
- 4. Widget 通过 `ctx.ui.setWidget(..., { placement: "belowEditor" })` 渲染在编辑器下方;age(抓取于多久前)右对齐,终端过窄时自动折成两行。
100
+ 4. 摘要通过 `ctx.ui.setStatus("ark-usage", …)` 发布到 extension status,由 pi-powerline-footer 渲染到 powerline 行;每分钟 ticker 在空闲时重发,保持重置倒计时刷新。
103
101
 
104
102
  ## 开发
105
103
 
@@ -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
- * Widget (below editor): 火山用量: 会话 4%(...) / 周 34%(...) / 月 17%(...)
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,11 +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
- buildItems,
31
26
  formatRemaining,
32
27
  getSettings,
33
28
  patchSettings,
@@ -45,10 +40,8 @@ import type {
45
40
  ArkSettings,
46
41
  Language,
47
42
  PlanSnapshot,
48
- RenderItem,
49
43
  } from "./lib/types.js";
50
44
 
51
- const WIDGET_KEY = "pi-ark-usage";
52
45
  /** Extension-status key consumed by pi-powerline-footer (powerline slot). */
53
46
  const STATUS_KEY = "ark-usage";
54
47
  const TICK_MS = 60_000;
@@ -87,10 +80,6 @@ let language: Language = "en";
87
80
  const snapshots = new Map<string, PlanSnapshot>();
88
81
  let activeProduct: ProductId | null = null;
89
82
  let status: Status = "ok";
90
- let deltas: Record<string, number> | null = null;
91
- let note: "changed" | "reset" | null = null;
92
- let baseline: PlanSnapshot | null = null;
93
- let activeWidget: ArkUsageComponent | null = null;
94
83
  let tickTimer: ReturnType<typeof setInterval> | null = null;
95
84
  let lastCtx: ExtensionContext | null = null;
96
85
  let lastAutoAt = 0;
@@ -128,20 +117,12 @@ function activeSnapshot(): PlanSnapshot | null {
128
117
 
129
118
  /* -------------------------------- rendering -------------------------------- */
130
119
 
131
- function renderWidget(ctx: ExtensionContext): void {
120
+ function renderStatus(ctx: ExtensionContext): void {
132
121
  lastCtx = ctx;
133
122
  const snap = activeSnapshot();
134
123
  const widgetStatus = status === "ok" && !snap ? "none" : status;
135
- const items: RenderItem[] = buildItems(
136
- snap,
137
- language,
138
- widgetStatus,
139
- ctx.isIdle(),
140
- deltas,
141
- note,
142
- );
143
124
  // Publish the one-line summary to the extension-status slot (powerline).
144
- // The ticker calls renderWidget every minute while idle, keeping the
125
+ // The ticker calls renderStatus every minute while idle, keeping the
145
126
  // reset countdown fresh without refetching.
146
127
  const compact = buildCompactStatus(snap, widgetStatus, ctx.ui.theme);
147
128
  if (compact) {
@@ -149,23 +130,6 @@ function renderWidget(ctx: ExtensionContext): void {
149
130
  } else {
150
131
  ctx.ui.setStatus(STATUS_KEY, undefined);
151
132
  }
152
- if (activeWidget) {
153
- activeWidget.update(items);
154
- return;
155
- }
156
- ctx.ui.setWidget(
157
- WIDGET_KEY,
158
- (tui, theme) => {
159
- const component = new ArkUsageComponent(
160
- items,
161
- () => theme,
162
- () => tui.requestRender?.(),
163
- );
164
- activeWidget = component;
165
- return component;
166
- },
167
- { placement: "belowEditor" },
168
- );
169
133
  }
170
134
 
171
135
  /* --------------------------------- fetching -------------------------------- */
@@ -193,7 +157,7 @@ async function refresh(
193
157
  activeProduct = product;
194
158
  if (!product) {
195
159
  status = "none";
196
- renderWidget(ctx);
160
+ renderStatus(ctx);
197
161
  return false;
198
162
  }
199
163
 
@@ -203,8 +167,7 @@ async function refresh(
203
167
  }
204
168
 
205
169
  status = "fetching";
206
- note = null;
207
- renderWidget(ctx);
170
+ renderStatus(ctx);
208
171
 
209
172
  const controller = new AbortController();
210
173
  inFlight = controller;
@@ -242,7 +205,7 @@ async function refresh(
242
205
  return false;
243
206
  } finally {
244
207
  if (inFlight === controller) inFlight = null;
245
- renderWidget(ctx);
208
+ renderStatus(ctx);
246
209
  }
247
210
  }
248
211
 
@@ -252,66 +215,21 @@ function persist(): void {
252
215
  saveCache(language, out);
253
216
  }
254
217
 
255
- /* ---------------------------- baseline / delta ----------------------------- */
256
-
257
- function computeDeltas(
258
- before: PlanSnapshot,
259
- after: PlanSnapshot,
260
- ): Record<string, number> | null {
261
- const out: Record<string, number> = {};
262
- for (const p of after.periods) {
263
- const old = before.periods.find((q) => q.label === p.label);
264
- if (old) out[p.label] = Math.round(old.percent - p.percent);
265
- // note: stored as consumed-positive; renderer negates for display
266
- }
267
- // flip sign convention for renderer: it wants negative=consumed
268
- const render: Record<string, number> = {};
269
- for (const [k, v] of Object.entries(out)) render[k] = -v;
270
- return render;
271
- }
272
-
273
- function looksReset(a: PlanSnapshot, b: PlanSnapshot): boolean {
274
- // all periods dropped sharply at once => plan period reset
275
- return b.periods.every((p) => {
276
- const old = a.periods.find((q) => q.label === p.label);
277
- return old !== undefined && p.percent - old.percent < -30;
278
- });
279
- }
218
+ /* ----------------------- session-start / settled --------------------------- */
280
219
 
281
220
  async function handleStart(ctx: ExtensionContext): Promise<void> {
282
221
  const product = resolveProduct(ctx);
283
222
  if (!product) return;
284
223
  const cached = snapshots.get(product) ?? null;
285
- if (cached && Date.now() - cached.fetchedAt <= BASELINE_FRESH_MS) {
286
- baseline = cached;
287
- } else {
288
- const win = await timeoutResult(refresh(ctx, "stale"), AGENT_START_TIMEOUT_MS);
289
- 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);
290
227
  }
291
- deltas = null;
292
- renderWidget(ctx);
228
+ renderStatus(ctx);
293
229
  }
294
230
 
295
231
  async function handleSettled(ctx: ExtensionContext): Promise<void> {
296
- const product = resolveProduct(ctx);
297
- if (!product) return;
298
- const ok = await refresh(ctx, "settled");
299
- if (!ok) return;
300
- const snap = snapshots.get(product);
301
- if (!snap) return;
302
-
303
- if (baseline && baseline.product !== product) {
304
- note = "changed";
305
- deltas = null;
306
- } else if (baseline && looksReset(baseline, snap)) {
307
- note = "reset";
308
- deltas = null;
309
- } else if (baseline) {
310
- deltas = computeDeltas(baseline, snap);
311
- note = null;
312
- }
313
- baseline = null;
314
- renderWidget(ctx);
232
+ await refresh(ctx, "settled");
315
233
  }
316
234
 
317
235
  async function timeoutResult<T>(p: Promise<T>, ms: number): Promise<T | null> {
@@ -342,7 +260,7 @@ function ensureTicker(): void {
342
260
  return;
343
261
  }
344
262
  // age text ticks every minute even without refetch
345
- if (activeWidget && lastCtx.isIdle()) renderWidget(lastCtx);
263
+ if (lastCtx && lastCtx.isIdle()) renderStatus(lastCtx);
346
264
  }, TICK_MS);
347
265
  tickTimer.unref?.();
348
266
  }
@@ -408,7 +326,7 @@ async function cmdSet(
408
326
  const notifySaved = (): void => {
409
327
  persist();
410
328
  ctx.ui.notify(locale.settingsApplied, "info");
411
- if (lastCtx) renderWidget(lastCtx);
329
+ if (lastCtx) renderStatus(lastCtx);
412
330
  };
413
331
 
414
332
  if (parts.length === 0) {
@@ -422,7 +340,7 @@ async function cmdSet(
422
340
  resetSettings();
423
341
  persist();
424
342
  ctx.ui.notify(locale.settingsReset, "info");
425
- if (lastCtx) renderWidget(lastCtx);
343
+ if (lastCtx) renderStatus(lastCtx);
426
344
  return;
427
345
  }
428
346
  case "yellow":
@@ -499,7 +417,7 @@ async function cmdSet(
499
417
  language = raw;
500
418
  persist();
501
419
  ctx.ui.notify(locale.languageChanged(language), "info");
502
- if (lastCtx) renderWidget(lastCtx);
420
+ if (lastCtx) renderStatus(lastCtx);
503
421
  return;
504
422
  }
505
423
  default:
@@ -532,25 +450,21 @@ export default function (pi: ExtensionAPI): void {
532
450
  });
533
451
 
534
452
  pi.on("session_start", (_event, ctx) => {
535
- // Session switches reuse this module instance in-process: pi has disposed
536
- // the old TUI widget and torn down the previous runtime, so drop stale
537
- // handles and revive the shutdown flags before doing anything else.
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.
538
456
  shuttingDown = false;
539
457
  resetShutdownState();
540
- activeWidget = null;
541
458
  activeProduct = resolveProduct(ctx);
542
- deltas = null;
543
- note = null;
544
- renderWidget(ctx);
459
+ renderStatus(ctx);
545
460
  ensureTicker();
546
461
  void refresh(ctx, "session_start");
547
462
  });
548
463
 
549
464
  pi.on("model_select", (_event, ctx) => {
550
465
  const next = resolveProduct(ctx);
551
- if (baseline && next && baseline.product !== next) note = "changed";
552
466
  activeProduct = next;
553
- renderWidget(ctx);
467
+ renderStatus(ctx);
554
468
  void refresh(ctx, "model_select");
555
469
  });
556
470
 
@@ -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 */
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Rendering: locales, threshold settings, percent coloring, remaining-time
3
- * formatting, and the TUI Component for the below-editor widget.
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, RenderItem } from "./types.js";
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"}`,
@@ -196,19 +185,6 @@ export function formatRemaining(ms: number, lang: Language): string {
196
185
  return `${m}m`;
197
186
  }
198
187
 
199
- export function formatAge(fetchedAt: number, lang: Language): string {
200
- const age = Date.now() - fetchedAt;
201
- if (!Number.isFinite(age) || age < 0) return "";
202
- if (age < 60_000) return LOCALES[lang].ageNow;
203
- const totalMin = Math.floor(age / 60_000);
204
- const h = Math.floor(totalMin / 60);
205
- const m = totalMin % 60;
206
- const d = Math.floor(h / 24);
207
- if (d > 0) return LOCALES[lang].ageDaysAgo(d, h % 24);
208
- if (h > 0) return LOCALES[lang].ageHoursAgo(h, m);
209
- return LOCALES[lang].ageMinutesAgo(m);
210
- }
211
-
212
188
  /* ---------------------- compact status (powerline slot) -------------------- */
213
189
 
214
190
  /**
@@ -260,224 +236,5 @@ export function buildCompactStatus(
260
236
  return null;
261
237
  }
262
238
 
263
- /* ----------------------------- snapshot -> items --------------------------- */
264
-
239
+ /** Lifecycle states mirrored in the compact status. */
265
240
  export type WidgetStatus = "ok" | "fetching" | "failed" | "no-binary" | "none";
266
-
267
- export function buildItems(
268
- snapshot: PlanSnapshot | null,
269
- lang: Language,
270
- status: WidgetStatus,
271
- isIdle: boolean,
272
- /** delta annotations keyed by period label; negative = consumed */
273
- deltas: Record<string, number> | null,
274
- note?: "changed" | "reset" | null,
275
- ): RenderItem[] {
276
- const locale = LOCALES[lang];
277
- const items: RenderItem[] = [];
278
-
279
- if (snapshot && snapshot.periods.length > 0) {
280
- items.push({ kind: "text", text: `${locale.title}: ` });
281
- snapshot.periods.forEach((p, i) => {
282
- if (i > 0) items.push({ kind: "text", text: " / " });
283
- const label = locale.labels[p.label] ?? p.label;
284
- items.push({ kind: "text", text: `${label} ` });
285
- items.push({ kind: "pct", pct: clampPct(p.percent), metric: p.label });
286
- const delta = deltas?.[p.label];
287
- if (typeof delta === "number" && delta !== 0) {
288
- const sign = delta > 0 ? "+" : "-";
289
- items.push({ kind: "annotation", text: `(${sign}${Math.abs(Math.round(delta))})` });
290
- }
291
- if (p.reset_at) {
292
- const remaining = new Date(p.reset_at).getTime() - Date.now();
293
- const t = formatRemaining(remaining, lang);
294
- if (t) items.push({ kind: "annotation", text: `(${locale.resetsIn(t)})` });
295
- }
296
- });
297
- } else if (status === "fetching") {
298
- items.push({ kind: "text", text: `${locale.title}: ` });
299
- items.push({ kind: "annotation", text: locale.fetching });
300
- } else if (status === "failed") {
301
- items.push({ kind: "text", text: `${locale.title}: ` });
302
- items.push({ kind: "annotation", text: locale.failed });
303
- } else if (status === "no-binary") {
304
- items.push({ kind: "annotation", text: locale.noBinary });
305
- } else {
306
- items.push({ kind: "annotation", text: locale.noData });
307
- }
308
-
309
- if (!isIdle) {
310
- items.push({ kind: "annotation", text: ` (${locale.using})` });
311
- } else if (note) {
312
- items.push({ kind: "annotation", text: ` (${note === "reset" ? locale.reset : locale.changed})` });
313
- }
314
-
315
- if (snapshot) {
316
- items.push({ kind: "age", text: formatAge(snapshot.fetchedAt, lang) });
317
- }
318
- return items;
319
- }
320
-
321
- export function formatItems(items: RenderItem[], theme: Theme): string {
322
- return items
323
- .map((it) => {
324
- switch (it.kind) {
325
- case "text":
326
- return theme.fg("dim", it.text);
327
- case "pct":
328
- return coloredPct(it.pct);
329
- case "age":
330
- return theme.fg("dim", it.text);
331
- case "annotation":
332
- default:
333
- return hexFg(HEX_COLORS.purple, it.text);
334
- }
335
- })
336
- .join("");
337
- }
338
-
339
- /* ------------------------------ ansi helpers ------------------------------ */
340
-
341
- export const ANSI_RE = /\x1b\[[0-9;]*m/g;
342
-
343
- export function visibleWidth(s: string): number {
344
- let w = 0;
345
- for (const ch of s.replace(ANSI_RE, "")) {
346
- const cp = ch.codePointAt(0) ?? 0;
347
- if (cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f)) continue;
348
- if (cp >= 0x0300 && cp <= 0x036f) continue;
349
- w +=
350
- (cp >= 0x1100 && cp <= 0x115f) ||
351
- (cp >= 0x2e80 && cp <= 0xa4cf) ||
352
- (cp >= 0xac00 && cp <= 0xd7a3) ||
353
- (cp >= 0xf900 && cp <= 0xfaff) ||
354
- (cp >= 0xfe30 && cp <= 0xfe6f) ||
355
- (cp >= 0xff00 && cp <= 0xff60) ||
356
- (cp >= 0xffe0 && cp <= 0xffe6) ||
357
- (cp >= 0x1f300 && cp <= 0x1f9ff)
358
- ? 2
359
- : 1;
360
- }
361
- return w;
362
- }
363
-
364
- export function truncateAnsi(s: string, maxWidth: number): string {
365
- if (maxWidth <= 0) return "";
366
- if (visibleWidth(s) <= maxWidth) return s;
367
- const budget = maxWidth - 1;
368
- let out = "";
369
- let w = 0;
370
- let i = 0;
371
- while (i < s.length) {
372
- ANSI_RE.lastIndex = i;
373
- const m = ANSI_RE.exec(s);
374
- if (m && m.index === i) {
375
- out += m[0];
376
- i = ANSI_RE.lastIndex;
377
- continue;
378
- }
379
- const ch = String.fromCodePoint(s.codePointAt(i) ?? 0);
380
- const cw = visibleWidth(ch);
381
- if (w + cw > budget) break;
382
- out += ch;
383
- w += cw;
384
- i += ch.length;
385
- }
386
- return `${out}…\x1b[0m`;
387
- }
388
-
389
- export function itemsEqual(a: RenderItem[], b: RenderItem[]): boolean {
390
- if (a.length !== b.length) return false;
391
- return a.every((it, i) => {
392
- const o = b[i];
393
- if (it.kind !== o.kind) return false;
394
- switch (it.kind) {
395
- case "pct":
396
- return (
397
- o.kind === "pct" && it.pct === o.pct && it.metric === o.metric
398
- );
399
- case "text":
400
- return o.kind === "text" && it.text === o.text;
401
- case "age":
402
- return o.kind === "age" && it.text === o.text;
403
- case "annotation":
404
- return o.kind === "annotation" && it.text === o.text;
405
- }
406
- });
407
- }
408
-
409
- /* -------------------------------- component -------------------------------- */
410
-
411
- export interface Component {
412
- render(width: number): string[];
413
- invalidate(): void;
414
- dispose?(): void;
415
- }
416
-
417
- /**
418
- * Widget rendered below the editor. The age segment is right-aligned and the
419
- * content wraps to two lines when the terminal is too narrow.
420
- */
421
- export class ArkUsageComponent implements Component {
422
- private items: RenderItem[];
423
- private readonly themeRef: () => Theme;
424
- private readonly requestRender: () => void;
425
- private cache: { width: number; lines: string[] } | null = null;
426
- private disposed = false;
427
-
428
- constructor(
429
- items: RenderItem[],
430
- themeRef: () => Theme,
431
- requestRender: () => void,
432
- ) {
433
- this.items = items;
434
- this.themeRef = themeRef;
435
- this.requestRender = requestRender;
436
- }
437
-
438
- update(items: RenderItem[]): void {
439
- if (this.disposed || itemsEqual(this.items, items)) return;
440
- this.items = items;
441
- this.invalidate();
442
- this.requestRender();
443
- }
444
-
445
- invalidate(): void {
446
- this.cache = null;
447
- }
448
-
449
- render(width: number): string[] {
450
- if (this.cache && this.cache.width === width) return this.cache.lines;
451
- const theme = this.themeRef();
452
-
453
- const ageIdx = this.items.findIndex((it) => it.kind === "age");
454
- if (ageIdx === -1) {
455
- const text = formatItems(this.items, theme);
456
- const lines = text ? [truncateAnsi(text, width)] : [];
457
- this.cache = { width, lines };
458
- return lines;
459
- }
460
-
461
- const leftText = formatItems(this.items.slice(0, ageIdx), theme);
462
- const rightText = formatItems(this.items.slice(ageIdx), theme);
463
- const lw = visibleWidth(leftText);
464
- const rw = visibleWidth(rightText);
465
-
466
- let lines: string[];
467
- if (lw + rw + 1 <= width) {
468
- lines = [leftText + " ".repeat(width - lw - rw) + rightText];
469
- } else {
470
- lines = [
471
- leftText ? truncateAnsi(leftText, width) : "",
472
- rw <= width ? " ".repeat(Math.max(0, width - rw)) + rightText : truncateAnsi(rightText, width),
473
- ].filter((l) => l !== "");
474
- }
475
- this.cache = { width, lines };
476
- return lines;
477
- }
478
-
479
- dispose(): void {
480
- this.disposed = true;
481
- this.cache = null;
482
- }
483
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ark-usage",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Show Volcengine Ark Coding Plan / Agent Plan usage in the pi TUI (AK/SK OpenAPI)",
5
5
  "keywords": [
6
6
  "pi",