@aaroncarry/pi-usage 0.1.0 → 0.2.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 +30 -6
- package/README.zh-CN.md +28 -5
- package/package.json +10 -3
- package/src/config.ts +3 -0
- package/src/index.ts +144 -7
- package/src/trends/aggregate.ts +822 -0
- package/src/trends/dashboard.ts +364 -0
- package/src/trends/insights.ts +152 -0
- package/src/trends/render.ts +393 -0
- package/src/ui/card.ts +39 -1
- package/src/ui/statusline.ts +5 -2
package/README.md
CHANGED
|
@@ -6,31 +6,54 @@ A [pi](https://github.com/earendil-works/pi) extension that shows the balances a
|
|
|
6
6
|
|
|
7
7
|
## What you get
|
|
8
8
|
|
|
9
|
-
The footer status line (default mode) follows the model you are using and shows its quota windows
|
|
9
|
+
The footer status line (default mode) follows the model you are using and shows its quota windows, this session's consumption, and a 7-day token sparkline:
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
Codex 5h 13% · weekly 2% · session 10.0k tok $0.020
|
|
12
|
+
Codex 5h 13% used · weekly 2% used · session 10.0k tok $0.020 · 7d ▁▁▁▁▁█▂ 66k
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`/usage` prints a card into the conversation (a custom entry — no popup, no focus steal):
|
|
15
|
+
`/usage` prints a card into the conversation (a custom entry — no popup, no focus steal) with a 30-day usage summary at the bottom:
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
Usage · 02:15
|
|
19
19
|
|
|
20
20
|
● Codex (Plus)
|
|
21
|
-
5h ░░░░░░░░░░ 0% · resets in 4h 54m
|
|
22
|
-
weekly ░░░░░░░░░░ 2% · resets in 6d 17h
|
|
21
|
+
5h ░░░░░░░░░░ 0% used · resets in 4h 54m
|
|
22
|
+
weekly ░░░░░░░░░░ 2% used · resets in 6d 17h
|
|
23
23
|
○ GLM
|
|
24
24
|
Balance ¥21.46 recharged ¥118.00 · spent ¥96.54
|
|
25
25
|
○ DeepSeek
|
|
26
26
|
Balance ¥38.48
|
|
27
|
+
Last 30 days ────────────────────────────
|
|
28
|
+
tokens ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▆███ 66k
|
|
29
|
+
gpt-5.6-luna █████████████████░░░ 70% 34k
|
|
30
|
+
gpt-5.6-terra █████░░░░░░░░░░░░░░░ 21% 10k
|
|
27
31
|
```
|
|
28
32
|
|
|
33
|
+
The dashboard opens on the **Charts** view (braille time series); `v` cycles Charts → Heatmap → Insights → Table. **Insights** is a tmustier-style cost analysis: where the spend went (top model share, cache leverage, reasoning share) and worth-attention alarms (likely cache misses with their cost, daily burn vs the prior 4 weeks, single-session concentration).
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Usage trends [Charts] Heatmap Table
|
|
37
|
+
Total 66k tok · $0.04 · peak 39k (9/13)
|
|
38
|
+
39k ┤ ⢸⡄
|
|
39
|
+
│ ⣿⠘⡄
|
|
40
|
+
0 └⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣸⣔⣱⠑⢼
|
|
41
|
+
08-28 09-13
|
|
42
|
+
GLM ████░░░░ 70%
|
|
43
|
+
Provider / Model Sessions Msgs Cost Tokens ↑In ↓Out Cache
|
|
44
|
+
▾ openai-codex - 16 $0.04 44k 39k 5.3k 150k
|
|
45
|
+
gpt-5.6-luna 1 14 $0.01 34k 29k 5.2k 140k
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Trends are aggregated from pi's session files (`<agentDir>/sessions/**/*.jsonl`) with an incremental disk cache; forked session copies are deduplicated. Token metric = input + output + cache write.
|
|
49
|
+
|
|
29
50
|
## Commands
|
|
30
51
|
|
|
31
52
|
| Command | Effect |
|
|
32
53
|
|---|---|
|
|
33
|
-
| `/
|
|
54
|
+
| `/trends` | Open the interactive trends dashboard (charts, heatmap, table, insights). Same as `/usage trends`. |
|
|
55
|
+
| `/usage` | Print the usage card (balances + 30-day summary) into the session. Re-running refreshes: served from cache within the 5-minute TTL, refetched afterwards (15 s timeout). Cards persist in the session and are re-rendered on `/reload` and session restore. |
|
|
56
|
+
| `/usage trends` | Open the interactive trends dashboard (Table / Charts / Heatmap / Insights; `m` metric, `←→` period, `v` view, `↑↓`+`enter` table expand). |
|
|
34
57
|
| `/usage active\|all\|off` | Switch the footer status line mode immediately and persist it to `usage.json` (tab-completed). |
|
|
35
58
|
| `pi --usage-status all` | Set the footer mode for this run only (overrides `usage.json`, not written back). |
|
|
36
59
|
|
|
@@ -126,6 +149,7 @@ Optional config file `<agentDir>/usage.json`:
|
|
|
126
149
|
|
|
127
150
|
- `intervalMinutes`: background refresh interval (default 5, minimum 1).
|
|
128
151
|
- `status`: `active` (default) | `all` | `off`.
|
|
152
|
+
- `sparkline`: set `false` to drop the 7-day sparkline from the footer status line.
|
|
129
153
|
- `autoDetect`: set `false` to disable endpoint auto-detection for unknown providers.
|
|
130
154
|
- `providers.<id>.enabled: false`: hide an account from the status line and card.
|
|
131
155
|
- `providers.<id>.label`: display name override.
|
package/README.zh-CN.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
## 效果
|
|
10
10
|
|
|
11
|
-
footer
|
|
11
|
+
footer 状态行(默认模式)跟随当前模型,显示其额度窗口、本次会话消耗和 7 天迷你趋势:
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
Codex 5h 13% · weekly 2% · session 10.0k tok $0.020
|
|
14
|
+
Codex 5h 13% used · weekly 2% used · session 10.0k tok $0.020 · 7d ▁▁▁▁▁█▂ 66k
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
`/usage` 向会话流打印一张卡片(自定义条目渲染——非弹窗、不抢焦点):
|
|
@@ -20,19 +20,41 @@ Codex 5h 13% · weekly 2% · session 10.0k tok $0.020
|
|
|
20
20
|
Usage · 02:15
|
|
21
21
|
|
|
22
22
|
● Codex (Plus)
|
|
23
|
-
5h ░░░░░░░░░░ 0% · resets in 4h 54m
|
|
24
|
-
weekly ░░░░░░░░░░ 2% · resets in 6d 17h
|
|
23
|
+
5h ░░░░░░░░░░ 0% used · resets in 4h 54m
|
|
24
|
+
weekly ░░░░░░░░░░ 2% used · resets in 6d 17h
|
|
25
25
|
○ GLM
|
|
26
26
|
Balance ¥21.46 recharged ¥118.00 · spent ¥96.54
|
|
27
27
|
○ DeepSeek
|
|
28
28
|
Balance ¥38.48
|
|
29
|
+
Last 30 days ────────────────────────────
|
|
30
|
+
tokens ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▆███ 66k
|
|
31
|
+
gpt-5.6-luna █████████████████░░░ 70% 34k
|
|
32
|
+
gpt-5.6-terra █████░░░░░░░░░░░░░░░ 21% 10k
|
|
29
33
|
```
|
|
30
34
|
|
|
35
|
+
仪表盘默认打开 **Charts** 视图(盲文时间序列);`v` 循环切换 Charts → Heatmap → **Insights** → Table。Insights 是成本洞察:Where it went(top 模型占比、缓存覆盖率、思考 token 占比)+ Worth attention(疑似缓存未命中及代价、日消耗相对前 4 周的增速、单会话消费集中度)。
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Usage trends [Charts] Heatmap Table
|
|
39
|
+
Total 66k tok · $0.04 · peak 39k (9/13)
|
|
40
|
+
39k ┤ ⢸⡄
|
|
41
|
+
│ ⣿⠘⡄
|
|
42
|
+
0 └⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣸⣔⣱⠑⢼
|
|
43
|
+
08-28 09-13
|
|
44
|
+
Provider / Model Sessions Msgs Cost Tokens ↑In ↓Out Cache
|
|
45
|
+
▾ openai-codex - 16 $0.04 44k 39k 5.3k 150k
|
|
46
|
+
gpt-5.6-luna 1 14 $0.01 34k 29k 5.2k 140k
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
趋势数据聚合自 pi 的会话文件(`<agentDir>/sessions/**/*.jsonl`),带增量磁盘缓存;分叉会话副本自动去重。Token 口径 = input + output + cache 写入。
|
|
50
|
+
|
|
31
51
|
## 命令
|
|
32
52
|
|
|
33
53
|
| 命令 | 作用 |
|
|
34
54
|
|---|---|
|
|
35
|
-
| `/
|
|
55
|
+
| `/trends` | 打开交互式趋势仪表盘(图表/热力图/明细表/洞察)。等价于 `/usage trends` |
|
|
56
|
+
| `/usage` | 向会话流打印用量卡片(余额 + 30 天摘要)。重复执行即刷新:5 分钟 TTL 内秒回,过期则重新拉取(15 秒超时)。卡片留存在会话里,`/reload`、恢复旧会话时自动重放 |
|
|
57
|
+
| `/usage trends` | 打开交互式趋势仪表盘(Table / Charts / Heatmap / Insights;`m` 切指标,`←→` 切周期,`v` 切视图,`↑↓`+`enter` 展开表格) |
|
|
36
58
|
| `/usage active\|all\|off` | 立即切换 footer 状态行模式,并持久化到 `usage.json`(输入时有补全) |
|
|
37
59
|
| `pi --usage-status all` | 指定本次运行的 footer 模式(覆盖 `usage.json`,不写回文件) |
|
|
38
60
|
|
|
@@ -127,6 +149,7 @@ pi install npm:@aaroncarry/pi-usage
|
|
|
127
149
|
|
|
128
150
|
- `intervalMinutes`:后台刷新间隔(默认 5,最小 1)。
|
|
129
151
|
- `status`:`active`(默认)/ `all` / `off`。
|
|
152
|
+
- `sparkline`:设为 `false` 移除 footer 状态行的 7 天迷你趋势。
|
|
130
153
|
- `autoDetect`:设为 `false` 关闭对未知厂商的余额端点自动探测。
|
|
131
154
|
- `providers.<id>.enabled: false`:从状态行和卡片隐藏某账号。
|
|
132
155
|
- `providers.<id>.label`:显示名覆盖。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaroncarry/pi-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Usage and balance viewer for pi: subscription quota windows, prepaid balances, and session consumption in a footer status line and a /usage card",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
"url": "https://github.com/aaroncarry/pi-usage/issues"
|
|
13
13
|
},
|
|
14
14
|
"homepage": "https://github.com/aaroncarry/pi-usage#readme",
|
|
15
|
-
"files": [
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"README.zh-CN.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
16
21
|
"keywords": [
|
|
17
22
|
"pi",
|
|
18
23
|
"pi-extension",
|
|
@@ -33,7 +38,9 @@
|
|
|
33
38
|
"access": "public"
|
|
34
39
|
},
|
|
35
40
|
"pi": {
|
|
36
|
-
"extensions": [
|
|
41
|
+
"extensions": [
|
|
42
|
+
"./src/index.ts"
|
|
43
|
+
]
|
|
37
44
|
},
|
|
38
45
|
"scripts": {
|
|
39
46
|
"typecheck": "tsc --noEmit",
|
package/src/config.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface BalanceConfig {
|
|
|
57
57
|
* (New API relays, Sub2API, MiniMax, Zhipu). Default true.
|
|
58
58
|
*/
|
|
59
59
|
autoDetect?: boolean;
|
|
60
|
+
/** 7-day token sparkline in the footer status line. Default true. */
|
|
61
|
+
sparkline?: boolean;
|
|
60
62
|
/** Per-provider options; entries with `custom` define generic adapters. */
|
|
61
63
|
providers?: Record<string, ProviderConfig>;
|
|
62
64
|
}
|
|
@@ -111,6 +113,7 @@ function sanitizeBalanceConfig(raw: Record<string, unknown>): BalanceConfig {
|
|
|
111
113
|
config.status = raw.status;
|
|
112
114
|
}
|
|
113
115
|
if (typeof raw.autoDetect === "boolean") config.autoDetect = raw.autoDetect;
|
|
116
|
+
if (typeof raw.sparkline === "boolean") config.sparkline = raw.sparkline;
|
|
114
117
|
if (isRecord(raw.providers)) {
|
|
115
118
|
const providers: Record<string, ProviderConfig> = {};
|
|
116
119
|
for (const [id, value] of Object.entries(raw.providers)) {
|
package/src/index.ts
CHANGED
|
@@ -10,15 +10,26 @@ import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { isRecord, loadBalanceConfig, saveStatusMode, type StatusMode } from "./config.ts";
|
|
12
12
|
import { getAgentDir, tokenFromRegistryAuth } from "./credentials.ts";
|
|
13
|
-
import { sumSessionUsage, type SessionUsageTotals } from "./session-usage.ts";
|
|
13
|
+
import { sumSessionUsage, formatTokens, type SessionUsageTotals } from "./session-usage.ts";
|
|
14
|
+
import {
|
|
15
|
+
annotateModelLabels,
|
|
16
|
+
collectTrends,
|
|
17
|
+
dailyTotals,
|
|
18
|
+
distributionRows,
|
|
19
|
+
type TrendsData,
|
|
20
|
+
} from "./trends/aggregate.ts";
|
|
21
|
+
import { TrendsDashboard } from "./trends/dashboard.ts";
|
|
22
|
+
import { sparklineString } from "./trends/render.ts";
|
|
14
23
|
import { BalanceService } from "./service.ts";
|
|
15
|
-
import { buildUsageCard, type UsageCardData } from "./ui/card.ts";
|
|
24
|
+
import { buildUsageCard, type CardTrendsSummary, type UsageCardData } from "./ui/card.ts";
|
|
16
25
|
import { formatStatusLine } from "./ui/statusline.ts";
|
|
17
26
|
import type { CredentialResolver } from "./types.ts";
|
|
18
27
|
|
|
19
28
|
const USAGE_ENTRY_TYPE = "usage-report";
|
|
20
29
|
/** Bound /usage fetches so a hanging provider API cannot stall the command. */
|
|
21
30
|
const USAGE_FETCH_TIMEOUT_MS = 15_000;
|
|
31
|
+
/** Trends scans are cached in memory briefly so card/dashboard/sparkline share one scan. */
|
|
32
|
+
const TRENDS_TTL_MS = 60_000;
|
|
22
33
|
|
|
23
34
|
const STATUS_MODES = ["active", "all", "off"] as const;
|
|
24
35
|
|
|
@@ -31,6 +42,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
31
42
|
let ui: ExtensionUIContext | undefined;
|
|
32
43
|
let activeProviderId: string | undefined;
|
|
33
44
|
let consumption: SessionUsageTotals | undefined;
|
|
45
|
+
let trendsCache: { data: TrendsData; at: number } | undefined;
|
|
46
|
+
let sparkline: string | undefined;
|
|
34
47
|
/** `/usage <mode>` override for this session; wins over flag and usage.json. */
|
|
35
48
|
let statusOverride: StatusMode | undefined;
|
|
36
49
|
/** `--usage-status` CLI flag; wins over usage.json. */
|
|
@@ -74,6 +87,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
74
87
|
activeProviderId,
|
|
75
88
|
theme: ui.theme,
|
|
76
89
|
consumption,
|
|
90
|
+
sparkline,
|
|
77
91
|
});
|
|
78
92
|
ui.setStatus("usage", text);
|
|
79
93
|
}
|
|
@@ -96,29 +110,139 @@ export default function (pi: ExtensionAPI) {
|
|
|
96
110
|
activeProviderId: typeof entry.data.activeProviderId === "string" ? entry.data.activeProviderId : undefined,
|
|
97
111
|
generatedAt: typeof entry.data.generatedAt === "number" ? entry.data.generatedAt : Date.now(),
|
|
98
112
|
};
|
|
113
|
+
if (isRecord(entry.data.trends)) {
|
|
114
|
+
const raw = entry.data.trends;
|
|
115
|
+
const rawDays = raw.days;
|
|
116
|
+
const rawModels = raw.models;
|
|
117
|
+
if (
|
|
118
|
+
Array.isArray(rawDays) &&
|
|
119
|
+
typeof raw.endsAt === "number" &&
|
|
120
|
+
typeof raw.total === "number" &&
|
|
121
|
+
typeof raw.cost === "number" &&
|
|
122
|
+
Array.isArray(rawModels)
|
|
123
|
+
) {
|
|
124
|
+
data.trends = {
|
|
125
|
+
days: rawDays.filter((value): value is number => typeof value === "number"),
|
|
126
|
+
endsAt: raw.endsAt,
|
|
127
|
+
total: raw.total,
|
|
128
|
+
cost: raw.cost,
|
|
129
|
+
models: rawModels.filter(
|
|
130
|
+
(model): model is { label: string; tokens: number } =>
|
|
131
|
+
isRecord(model) && typeof model.label === "string" && typeof model.tokens === "number",
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
99
136
|
return buildUsageCard(data, theme);
|
|
100
137
|
});
|
|
101
138
|
|
|
139
|
+
/** Session trends with a small TTL so card, dashboard and sparkline share one scan. */
|
|
140
|
+
async function getTrends(signal?: AbortSignal): Promise<TrendsData> {
|
|
141
|
+
if (trendsCache && Date.now() - trendsCache.at < TRENDS_TTL_MS) return trendsCache.data;
|
|
142
|
+
const data = await collectTrends(getAgentDir(), { signal });
|
|
143
|
+
trendsCache = { data, at: Date.now() };
|
|
144
|
+
return data;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function costTotal(data: TrendsData, fromMs?: number): number {
|
|
148
|
+
return distributionRows(data, fromMs).reduce((total, row) => total + row.cost, 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 30-day summary embedded in the /usage card. */
|
|
152
|
+
async function cardTrendsSummary(): Promise<CardTrendsSummary | undefined> {
|
|
153
|
+
try {
|
|
154
|
+
const data = await getTrends();
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const from = now - 30 * 86_400_000;
|
|
157
|
+
const days = dailyTotals(data, "tokens", from);
|
|
158
|
+
const todayStart = new Date(now).setHours(0, 0, 0, 0);
|
|
159
|
+
const byDay = new Map(days.map((entry) => [entry.dayStart, entry.value]));
|
|
160
|
+
const aligned: number[] = [];
|
|
161
|
+
for (let index = 29; index >= 0; index--) {
|
|
162
|
+
aligned.push(byDay.get(todayStart - index * 86_400_000) ?? 0);
|
|
163
|
+
}
|
|
164
|
+
const models = annotateModelLabels(distributionRows(data, from))
|
|
165
|
+
.filter((row) => row.model !== "summaries")
|
|
166
|
+
.slice(0, 3)
|
|
167
|
+
.map((row) => ({ label: row.label, tokens: row.tokens }));
|
|
168
|
+
return {
|
|
169
|
+
days: aligned,
|
|
170
|
+
endsAt: todayStart,
|
|
171
|
+
total: aligned.reduce((total, value) => total + value, 0),
|
|
172
|
+
cost: costTotal(data, from),
|
|
173
|
+
models,
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Refresh the 7-day status-line sparkline once trends data is available. */
|
|
181
|
+
function refreshSparkline(): void {
|
|
182
|
+
if (!service || service.config.sparkline === false) return;
|
|
183
|
+
void getTrends()
|
|
184
|
+
.then((data) => {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
const days = dailyTotals(data, "tokens", now - 7 * 86_400_000);
|
|
187
|
+
const todayStart = new Date(now).setHours(0, 0, 0, 0);
|
|
188
|
+
const byDay = new Map(days.map((entry) => [entry.dayStart, entry.value]));
|
|
189
|
+
const aligned: number[] = [];
|
|
190
|
+
for (let index = 6; index >= 0; index--) {
|
|
191
|
+
aligned.push(byDay.get(todayStart - index * 86_400_000) ?? 0);
|
|
192
|
+
}
|
|
193
|
+
sparkline = sparklineString(aligned);
|
|
194
|
+
updateStatus();
|
|
195
|
+
})
|
|
196
|
+
.catch(() => undefined);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Open the interactive trends dashboard. */
|
|
200
|
+
async function openTrendsDashboard(ctx: ExtensionContext): Promise<void> {
|
|
201
|
+
if (ctx.mode !== "tui") {
|
|
202
|
+
ctx.ui.notify("/trends requires interactive mode", "warning");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
ensureService(ctx);
|
|
206
|
+
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => {
|
|
207
|
+
return new TrendsDashboard({ theme, done, data: getTrends() });
|
|
208
|
+
}, {
|
|
209
|
+
overlay: true,
|
|
210
|
+
overlayOptions: { width: "85%", maxHeight: "92%", anchor: "center" },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
102
214
|
pi.registerCommand("usage", {
|
|
103
|
-
description: "Show usage card, or set the footer status line mode",
|
|
215
|
+
description: "Show usage card, open the trends dashboard, or set the footer status line mode",
|
|
104
216
|
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
105
|
-
const
|
|
106
|
-
|
|
217
|
+
const options: AutocompleteItem[] = [
|
|
218
|
+
{ value: "trends", label: "trends", description: "Open the interactive trends dashboard" },
|
|
219
|
+
{ value: "active", label: "active", description: "Footer: current account + session usage (default)" },
|
|
220
|
+
{ value: "all", label: "all", description: "Footer: all accounts on one line" },
|
|
221
|
+
{ value: "off", label: "off", description: "Footer: hide the status line" },
|
|
222
|
+
];
|
|
223
|
+
const matches = options.filter((option) => option.value.startsWith(prefix.toLowerCase()));
|
|
224
|
+
return matches.length > 0 ? matches : null;
|
|
107
225
|
},
|
|
108
226
|
handler: async (args, ctx) => {
|
|
109
227
|
const argument = args?.trim().toLowerCase();
|
|
110
228
|
if (!argument) {
|
|
111
229
|
const current = ensureService(ctx);
|
|
112
230
|
await current.refreshAll({ signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS) });
|
|
231
|
+
const trends = await cardTrendsSummary();
|
|
113
232
|
pi.appendEntry(USAGE_ENTRY_TYPE, {
|
|
114
233
|
balances: current.getAll(),
|
|
115
234
|
activeProviderId,
|
|
116
235
|
generatedAt: Date.now(),
|
|
236
|
+
trends,
|
|
117
237
|
});
|
|
118
238
|
return;
|
|
119
239
|
}
|
|
240
|
+
if (argument === "trends") {
|
|
241
|
+
await openTrendsDashboard(ctx);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
120
244
|
if (!isStatusMode(argument)) {
|
|
121
|
-
ctx.ui.notify(`/usage: unknown
|
|
245
|
+
ctx.ui.notify(`/usage: unknown argument "${argument}" — use trends, active, all, or off`, "warning");
|
|
122
246
|
return;
|
|
123
247
|
}
|
|
124
248
|
statusOverride = argument;
|
|
@@ -128,6 +252,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
128
252
|
},
|
|
129
253
|
});
|
|
130
254
|
|
|
255
|
+
// First-class command so new users discover the dashboard without knowing
|
|
256
|
+
// about the two-stage "/usage <space> trends" argument completion.
|
|
257
|
+
pi.registerCommand("trends", {
|
|
258
|
+
description: "Open the usage trends dashboard (charts, heatmap, table, insights)",
|
|
259
|
+
handler: async (_args, ctx) => {
|
|
260
|
+
await openTrendsDashboard(ctx);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
131
264
|
pi.on("session_start", async (_event, ctx) => {
|
|
132
265
|
// Session replacement tears the old service down via session_shutdown;
|
|
133
266
|
// build a fresh one bound to the new session context.
|
|
@@ -145,13 +278,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
145
278
|
consumption = sumSessionUsage(ctx.sessionManager.getEntries());
|
|
146
279
|
next.start();
|
|
147
280
|
updateStatus();
|
|
281
|
+
refreshSparkline();
|
|
148
282
|
});
|
|
149
283
|
|
|
150
284
|
// Session tokens/cost are local data: refresh immediately after every turn
|
|
151
|
-
// instead of waiting for the provider quota polling cycle.
|
|
285
|
+
// instead of waiting for the provider quota polling cycle. The sparkline's
|
|
286
|
+
// today-bucket moves too; getTrends' TTL keeps this cheap.
|
|
152
287
|
pi.on("turn_end", async (_event, ctx) => {
|
|
153
288
|
consumption = sumSessionUsage(ctx.sessionManager.getEntries());
|
|
154
289
|
updateStatus();
|
|
290
|
+
refreshSparkline();
|
|
155
291
|
});
|
|
156
292
|
|
|
157
293
|
pi.on("model_select", async (event) => {
|
|
@@ -164,5 +300,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
164
300
|
service = undefined;
|
|
165
301
|
ui = undefined;
|
|
166
302
|
consumption = undefined;
|
|
303
|
+
sparkline = undefined;
|
|
167
304
|
});
|
|
168
305
|
}
|