pi-ark-usage 0.1.1 → 0.1.3

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,7 +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
- - **阈值着色**:低用量绿色、接近上限黄色、超过红线红色
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`(编辑器边框色)
14
+ - **阈值着色**:低用量绿色(<50%)、接近上限黄色(50–79%)、超过红线红色(≥80%,默认值,可用 `/arkset` 调整)
14
15
  - **单轮消耗**:以每轮对话开始前的快照为基线,结算时标注各周期增量(如 `(+2)`)
15
16
  - **产品切换感知**:在个人版/团队版、Coding Plan/Agent Plan 之间切换时标注 `(变更)`;套餐周期重置时标注 `(已重置)`
16
17
  - **持久化缓存**:重启 pi 后立即显示上次快照(`~/.pi/agent/pi-ark-usage/cache.json`,0600 权限,临时文件原子写入)
@@ -67,7 +68,7 @@ pi install npm:pi-ark-usage
67
68
  |---|---|
68
69
  | `/arkset yellow <n>` | 黄色阈值(百分比,默认 40),须低于红线 |
69
70
  | `/arkset red <n>` | 红色阈值(百分比,默认 80),须高于黄线 |
70
- | `/arkset auto <分钟>` | 空闲自动刷新间隔,0–30 的整数,`0` 关闭(默认关闭) |
71
+ | `/arkset auto <分钟>` | 空闲自动刷新间隔,0–30 的整数,`0` 关闭(默认 5 分钟) |
71
72
  | `/arkset product <id>` | 展示映射的产品:`auto`(默认)/ `coding-plan` / `agent-plan` / `coding-plan-team` / `agent-plan-team` |
72
73
  | `/arkset seat <id>` | 团队版指定 SeatID;`/arkset seat none` 清除 |
73
74
  | `/arkset lang <zh\|en>` | UI language, default English |
@@ -89,7 +90,9 @@ pi install npm:pi-ark-usage
89
90
  | `agent_start` | 建立本轮基线(缓存 1 小时内有效;否则限时 3 秒补抓,超时沿用缓存) |
90
91
  | `agent_settled` | 本轮结算抓取,并与基线对比标注消耗 |
91
92
  | `/arkcheck` | 手动强制刷新 |
92
- | 可选自动刷新 | 仅当 `/arkset auto` 开启时按间隔执行 |
93
+ | 可选自动刷新 | 默认每 5 分钟(`/arkset auto` 可调 0–30,`0` 关闭) |
94
+
95
+ > 注意:`auto` 设置会持久化到磁盘 cache;旧版本用户若曾显式设过 `0`,升级后仍是关闭,需手动 `/arkset auto 5` 开启。
93
96
 
94
97
  ## 工作原理
95
98
 
@@ -26,6 +26,7 @@ import {
26
26
  ArkUsageComponent,
27
27
  AUTO_REFRESH_MAX_MINUTES,
28
28
  LOCALES,
29
+ buildCompactStatus,
29
30
  buildItems,
30
31
  formatRemaining,
31
32
  getSettings,
@@ -36,6 +37,7 @@ import {
36
37
  flushWrites,
37
38
  loadCache,
38
39
  markShuttingDown,
40
+ resetShutdownState,
39
41
  saveCache,
40
42
  } from "./lib/cache.js";
41
43
  import type {
@@ -47,6 +49,8 @@ import type {
47
49
  } from "./lib/types.js";
48
50
 
49
51
  const WIDGET_KEY = "pi-ark-usage";
52
+ /** Extension-status key consumed by pi-powerline-footer (powerline slot). */
53
+ const STATUS_KEY = "ark-usage";
50
54
  const TICK_MS = 60_000;
51
55
  const AGENT_START_TIMEOUT_MS = 3_000;
52
56
  const BASELINE_FRESH_MS = 60 * 60_000;
@@ -127,14 +131,24 @@ function activeSnapshot(): PlanSnapshot | null {
127
131
  function renderWidget(ctx: ExtensionContext): void {
128
132
  lastCtx = ctx;
129
133
  const snap = activeSnapshot();
134
+ const widgetStatus = status === "ok" && !snap ? "none" : status;
130
135
  const items: RenderItem[] = buildItems(
131
136
  snap,
132
137
  language,
133
- status === "ok" && !snap ? "none" : status,
138
+ widgetStatus,
134
139
  ctx.isIdle(),
135
140
  deltas,
136
141
  note,
137
142
  );
143
+ // Publish the one-line summary to the extension-status slot (powerline).
144
+ // The ticker calls renderWidget every minute while idle, keeping the
145
+ // reset countdown fresh without refetching.
146
+ const compact = buildCompactStatus(snap, widgetStatus, ctx.ui.theme);
147
+ if (compact) {
148
+ ctx.ui.setStatus(STATUS_KEY, compact);
149
+ } else {
150
+ ctx.ui.setStatus(STATUS_KEY, undefined);
151
+ }
138
152
  if (activeWidget) {
139
153
  activeWidget.update(items);
140
154
  return;
@@ -504,15 +518,26 @@ export default function (pi: ExtensionAPI): void {
504
518
  }
505
519
  }
506
520
 
507
- pi.on("session_shutdown", async () => {
508
- shuttingDown = true;
509
- markShuttingDown();
521
+ pi.on("session_shutdown", async (event) => {
510
522
  stopTicker();
511
523
  inFlight?.abort();
524
+ // Only a real quit is terminal. Session switches (/new, /resume, /fork,
525
+ // /reload) tear down and then re-enter session_start in the same process;
526
+ // marking terminal shutdown there would block every future refresh.
527
+ if (event.reason === "quit") {
528
+ shuttingDown = true;
529
+ markShuttingDown();
530
+ }
512
531
  await flushWrites();
513
532
  });
514
533
 
515
534
  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.
538
+ shuttingDown = false;
539
+ resetShutdownState();
540
+ activeWidget = null;
516
541
  activeProduct = resolveProduct(ctx);
517
542
  deltas = null;
518
543
  note = null;
@@ -172,6 +172,11 @@ export function markShuttingDown(): void {
172
172
  shuttingDown = true;
173
173
  }
174
174
 
175
+ /** Revive cache writes after an in-process session switch (see index.ts). */
176
+ export function resetShutdownState(): void {
177
+ shuttingDown = false;
178
+ }
179
+
175
180
  async function writeFile(data: string): Promise<void> {
176
181
  await mkdir(CACHE_DIR, { recursive: true, mode: DIR_MODE });
177
182
  const dir = await lstat(CACHE_DIR);
@@ -122,9 +122,9 @@ export const LOCALES: Record<Language, Locale> = { zh: ZH, en: EN };
122
122
  export const AUTO_REFRESH_MAX_MINUTES = 30;
123
123
 
124
124
  export const DEFAULT_SETTINGS: ArkSettings = {
125
- pctYellow: 40,
125
+ pctYellow: 50,
126
126
  pctRed: 80,
127
- autoRefreshMinutes: 0,
127
+ autoRefreshMinutes: 5,
128
128
  product: "auto",
129
129
  seat: "",
130
130
  };
@@ -209,6 +209,57 @@ export function formatAge(fetchedAt: number, lang: Language): string {
209
209
  return LOCALES[lang].ageMinutesAgo(m);
210
210
  }
211
211
 
212
+ /* ---------------------- compact status (powerline slot) -------------------- */
213
+
214
+ /**
215
+ * Compact reset countdown for the powerline status: `38m`, `4h38m`, `5d4h`.
216
+ * Lower-precision units are dropped to keep the line short and stable.
217
+ */
218
+ export function formatCompactRemaining(ms: number): string {
219
+ if (!Number.isFinite(ms) || ms <= 0) return "";
220
+ const totalMin = Math.floor(ms / 60_000);
221
+ const d = Math.floor(totalMin / 1440);
222
+ const h = Math.floor((totalMin % 1440) / 60);
223
+ const m = totalMin % 60;
224
+ if (d > 0) return `${d}d${h}h`;
225
+ if (h > 0) return `${h}h${m}m`;
226
+ return `${m}m`;
227
+ }
228
+
229
+ /**
230
+ * One-line colored summary for the powerline extension-status slot:
231
+ * `1% 4h38m | 15% 5d4h | 54% 23d4h`
232
+ *
233
+ * Periods are shown in API order (session / weekly / monthly) without
234
+ * labels — the order itself is the convention. Percent follows the
235
+ * yellow/red thresholds (green below yellow, yellow below red, red at/above
236
+ * red); the time part is always purple; the ` | ` separator uses the
237
+ * editor border color (`thinkingHigh`, pink in the dark theme).
238
+ *
239
+ * Returns null when nothing should be shown (the powerline item hides).
240
+ */
241
+ export function buildCompactStatus(
242
+ snapshot: PlanSnapshot | null,
243
+ status: WidgetStatus,
244
+ theme: Theme,
245
+ ): string | null {
246
+ if (snapshot && snapshot.periods.length > 0) {
247
+ const sep = theme.fg("thinkingHigh", " | ");
248
+ return snapshot.periods
249
+ .map((p) => {
250
+ const pct = coloredPct(clampPct(p.percent));
251
+ const reset = p.reset_at
252
+ ? formatCompactRemaining(new Date(p.reset_at).getTime() - Date.now())
253
+ : "";
254
+ return reset ? `${pct} ${hexFg(HEX_COLORS.purple, reset)}` : pct;
255
+ })
256
+ .join(sep);
257
+ }
258
+ if (status === "fetching") return hexFg(HEX_COLORS.purple, "…");
259
+ if (status === "failed") return hexFg(HEX_COLORS.red, "failed");
260
+ return null;
261
+ }
262
+
212
263
  /* ----------------------------- snapshot -> items --------------------------- */
213
264
 
214
265
  export type WidgetStatus = "ok" | "fetching" | "failed" | "no-binary" | "none";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ark-usage",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Show Volcengine Ark Coding Plan / Agent Plan usage in the pi TUI (AK/SK OpenAPI)",
5
5
  "keywords": [
6
6
  "pi",
@@ -33,7 +33,8 @@
33
33
  },
34
34
  "main": "extensions/index.ts",
35
35
  "scripts": {
36
- "typecheck": "tsc --noEmit"
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "node --import ./test/ts-resolve-hook.mjs --test --experimental-strip-types test/*.test.ts"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/node": "^22.19.19",