opencode-tokenwatch 0.4.0 → 0.6.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.en.md +32 -38
- package/README.md +33 -39
- package/dist/host/command-actions.d.ts +20 -0
- package/dist/host/runtime.d.ts +7 -0
- package/dist/host/types.d.ts +148 -0
- package/dist/host/v1/adapter.d.ts +9 -0
- package/dist/host/v1/commands.d.ts +9 -0
- package/dist/{queries.d.ts → host/v1/data-source.d.ts} +1 -6
- package/dist/host/v2/adapter.d.ts +63 -0
- package/dist/host/v2/commands.d.ts +8 -0
- package/dist/host/v2/data-source.d.ts +26 -0
- package/dist/kernel/config.d.ts +33 -0
- package/dist/{formatter.d.ts → kernel/format.d.ts} +9 -10
- package/dist/kernel/model.d.ts +19 -0
- package/dist/kernel/perf-aggregate.d.ts +62 -0
- package/dist/{perf-tracker.d.ts → kernel/perf.d.ts} +13 -7
- package/dist/{generate-usage-html.d.ts → kernel/report-html.d.ts} +1 -1
- package/dist/kernel/report.d.ts +19 -0
- package/dist/{stats-store.d.ts → kernel/store.d.ts} +5 -5
- package/dist/server.d.ts +15 -0
- package/dist/server.js +15 -0
- package/dist/tui.d.ts +11 -12
- package/dist/tui.js +4802 -0
- package/dist/ui/sidebar.d.ts +14 -0
- package/index.ts +11 -0
- package/package.json +39 -12
- package/tui.ts +9 -0
- package/dist/commands.d.ts +0 -2
- package/dist/commands.js +0 -208
- package/dist/commands.jsx +0 -381
- package/dist/formatter.js +0 -289
- package/dist/generate-usage-html.js +0 -962
- package/dist/i18n.js +0 -173
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -7
- package/dist/perf-tracker.js +0 -299
- package/dist/queries.js +0 -393
- package/dist/sidebar.d.ts +0 -22
- package/dist/sidebar.jsx +0 -604
- package/dist/stats-store.js +0 -258
- package/dist/tui.jsx +0 -178
- /package/dist/{i18n.d.ts → kernel/i18n.d.ts} +0 -0
package/dist/commands.jsx
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { getUsageReport, getPresetRange } from "./queries.js";
|
|
2
|
-
import { formatUsageReport } from "./formatter.js";
|
|
3
|
-
import { generateUsageHtml } from "./generate-usage-html.js";
|
|
4
|
-
import { t, setLanguage } from "./i18n.js";
|
|
5
|
-
import { readLogs } from "./perf-tracker.js";
|
|
6
|
-
import { readPersistedStats } from "./stats-store.js";
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
import { execSync } from "node:child_process";
|
|
11
|
-
const DEFAULT_CONFIG = {
|
|
12
|
-
sidebar: { showPerformance: true, showPricing: true, showTokenDistribution: true, showTrend: true },
|
|
13
|
-
language: "auto",
|
|
14
|
-
};
|
|
15
|
-
export async function registerCommands(api) {
|
|
16
|
-
api.command?.register(() => [
|
|
17
|
-
{
|
|
18
|
-
value: "tokenwatch-usage",
|
|
19
|
-
title: "TokenWatch",
|
|
20
|
-
description: "Token usage reports, export, and settings",
|
|
21
|
-
category: "Stats",
|
|
22
|
-
slash: { name: "usage" },
|
|
23
|
-
onSelect: async (dialog) => {
|
|
24
|
-
if (dialog)
|
|
25
|
-
showUsageMenu(api, dialog);
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
]);
|
|
29
|
-
}
|
|
30
|
-
function ensureReportDir() {
|
|
31
|
-
const dir = join(homedir(), ".opencode", "reports");
|
|
32
|
-
if (!existsSync(dir))
|
|
33
|
-
mkdirSync(dir, { recursive: true });
|
|
34
|
-
return dir;
|
|
35
|
-
}
|
|
36
|
-
function openInBrowser(filePath) {
|
|
37
|
-
try {
|
|
38
|
-
const platform = process.platform;
|
|
39
|
-
if (platform === "win32")
|
|
40
|
-
execSync(`start "" "${filePath}"`, { windowsHide: true, timeout: 5000 });
|
|
41
|
-
else if (platform === "darwin")
|
|
42
|
-
execSync(`open "${filePath}"`, { timeout: 5000 });
|
|
43
|
-
else
|
|
44
|
-
execSync(`xdg-open "${filePath}"`, { timeout: 5000 });
|
|
45
|
-
}
|
|
46
|
-
catch { /* silently fail */ }
|
|
47
|
-
}
|
|
48
|
-
/** 线性插值百分位数,输入须为有序数组 */
|
|
49
|
-
function computePercentile(sortedArr, p) {
|
|
50
|
-
if (sortedArr.length === 0)
|
|
51
|
-
return null;
|
|
52
|
-
if (sortedArr.length === 1)
|
|
53
|
-
return sortedArr[0];
|
|
54
|
-
const idx = (p / 100) * (sortedArr.length - 1);
|
|
55
|
-
const lo = Math.floor(idx);
|
|
56
|
-
const hi = Math.ceil(idx);
|
|
57
|
-
if (lo === hi)
|
|
58
|
-
return sortedArr[lo];
|
|
59
|
-
return sortedArr[lo] + (sortedArr[hi] - sortedArr[lo]) * (idx - lo);
|
|
60
|
-
}
|
|
61
|
-
function aggregatePerfStats(logs) {
|
|
62
|
-
const map = new Map();
|
|
63
|
-
for (const entry of logs) {
|
|
64
|
-
// 过滤掉全零无效条目:token 均为 0 表示请求失败或未完成
|
|
65
|
-
if (entry.inputTokens + entry.outputTokens + entry.cacheReadTokens + entry.cacheWriteTokens === 0)
|
|
66
|
-
continue;
|
|
67
|
-
const key = entry.model;
|
|
68
|
-
let s = map.get(key);
|
|
69
|
-
if (!s) {
|
|
70
|
-
s = {
|
|
71
|
-
model: key,
|
|
72
|
-
providerID: entry.providerID,
|
|
73
|
-
requestCount: 0,
|
|
74
|
-
ttftCount: 0, // Bug fix: 独立维护有效样本计数
|
|
75
|
-
tpsCount: 0,
|
|
76
|
-
latencyCount: 0,
|
|
77
|
-
totalInput: 0, totalOutput: 0, totalCacheRead: 0, totalCacheWrite: 0, totalCost: 0,
|
|
78
|
-
avgTTFT: null, maxTTFT: null, minTTFT: null,
|
|
79
|
-
p50TTFT: null, p95TTFT: null, p99TTFT: null,
|
|
80
|
-
avgTPS: null, maxTPS: null, minTPS: null,
|
|
81
|
-
avgLatency: null, maxLatency: null, minLatency: null,
|
|
82
|
-
p50Latency: null, p95Latency: null, p99Latency: null,
|
|
83
|
-
cacheHitRate: null,
|
|
84
|
-
};
|
|
85
|
-
map.set(key, s);
|
|
86
|
-
}
|
|
87
|
-
s.requestCount++;
|
|
88
|
-
s.totalInput += entry.inputTokens;
|
|
89
|
-
s.totalOutput += entry.outputTokens;
|
|
90
|
-
s.totalCacheRead += entry.cacheReadTokens;
|
|
91
|
-
s.totalCacheWrite += entry.cacheWriteTokens;
|
|
92
|
-
s.totalCost += entry.cost;
|
|
93
|
-
if (entry.ttft_ms != null) {
|
|
94
|
-
// Bug fix: 分母使用 ttftCount(有效样本数),而非 requestCount(总请求数)
|
|
95
|
-
s.ttftCount++;
|
|
96
|
-
const c = s.ttftCount;
|
|
97
|
-
s.avgTTFT = s.avgTTFT != null ? s.avgTTFT + (entry.ttft_ms - s.avgTTFT) / c : entry.ttft_ms;
|
|
98
|
-
s.maxTTFT = s.maxTTFT != null ? Math.max(s.maxTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
99
|
-
s.minTTFT = s.minTTFT != null ? Math.min(s.minTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
100
|
-
}
|
|
101
|
-
if (entry.tps != null) {
|
|
102
|
-
// Bug fix: 分母使用 tpsCount(有效样本数)
|
|
103
|
-
s.tpsCount++;
|
|
104
|
-
const c = s.tpsCount;
|
|
105
|
-
s.avgTPS = s.avgTPS != null ? s.avgTPS + (entry.tps - s.avgTPS) / c : entry.tps;
|
|
106
|
-
s.maxTPS = s.maxTPS != null ? Math.max(s.maxTPS, entry.tps) : entry.tps;
|
|
107
|
-
s.minTPS = s.minTPS != null ? Math.min(s.minTPS, entry.tps) : entry.tps;
|
|
108
|
-
}
|
|
109
|
-
if (entry.latency_ms != null) {
|
|
110
|
-
s.latencyCount++;
|
|
111
|
-
const c = s.latencyCount;
|
|
112
|
-
s.avgLatency = s.avgLatency != null ? s.avgLatency + (entry.latency_ms - s.avgLatency) / c : entry.latency_ms;
|
|
113
|
-
s.maxLatency = s.maxLatency != null ? Math.max(s.maxLatency, entry.latency_ms) : entry.latency_ms;
|
|
114
|
-
s.minLatency = s.minLatency != null ? Math.min(s.minLatency, entry.latency_ms) : entry.latency_ms;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// 分位数后处理:需要收集每个模型的所有样本然后计算
|
|
118
|
-
// 注: 此处采用单次遍历日志重新收集分数据,需要两次遍历
|
|
119
|
-
const ttftBuckets = new Map();
|
|
120
|
-
const latBuckets = new Map();
|
|
121
|
-
for (const entry of logs) {
|
|
122
|
-
const key = entry.model;
|
|
123
|
-
if (entry.ttft_ms != null) {
|
|
124
|
-
const arr = ttftBuckets.get(key) ?? [];
|
|
125
|
-
arr.push(entry.ttft_ms);
|
|
126
|
-
ttftBuckets.set(key, arr);
|
|
127
|
-
}
|
|
128
|
-
if (entry.latency_ms != null) {
|
|
129
|
-
const arr = latBuckets.get(key) ?? [];
|
|
130
|
-
arr.push(entry.latency_ms);
|
|
131
|
-
latBuckets.set(key, arr);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
const result = Array.from(map.values());
|
|
135
|
-
for (const s of result) {
|
|
136
|
-
const ttftArr = [...(ttftBuckets.get(s.model) ?? [])].sort((a, b) => a - b);
|
|
137
|
-
s.p50TTFT = computePercentile(ttftArr, 50);
|
|
138
|
-
s.p95TTFT = computePercentile(ttftArr, 95);
|
|
139
|
-
s.p99TTFT = computePercentile(ttftArr, 99);
|
|
140
|
-
const latArr = [...(latBuckets.get(s.model) ?? [])].sort((a, b) => a - b);
|
|
141
|
-
s.p50Latency = computePercentile(latArr, 50);
|
|
142
|
-
s.p95Latency = computePercentile(latArr, 95);
|
|
143
|
-
s.p99Latency = computePercentile(latArr, 99);
|
|
144
|
-
const denom = s.totalInput + s.totalCacheRead;
|
|
145
|
-
s.cacheHitRate = denom > 0 ? (s.totalCacheRead / denom) * 100 : null;
|
|
146
|
-
}
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
|
-
async function buildCombinedData(api, filters = {}) {
|
|
150
|
-
const report = await getUsageReport(filters);
|
|
151
|
-
// perfLogs: 仅用于 JSON 导出参考,保持适当窗口即可
|
|
152
|
-
const logs = readLogs(200);
|
|
153
|
-
// perfSummary: 使用持久化聚合统计,包含自插件安装以来的全量历史数据
|
|
154
|
-
// 不再受 readLogs 窗口限制,即使 JSONL 被轮转,历史指标也不会丢失
|
|
155
|
-
const perfSummary = readPersistedStats();
|
|
156
|
-
const now = new Date();
|
|
157
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
158
|
-
const meta = {
|
|
159
|
-
generatedAt: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
|
|
160
|
-
dateRange: {
|
|
161
|
-
start: report.daily.length > 0 ? report.daily[report.daily.length - 1].day : "—",
|
|
162
|
-
end: report.daily.length > 0 ? report.daily[0].day : "—",
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
return {
|
|
166
|
-
...report,
|
|
167
|
-
perfLogs: logs,
|
|
168
|
-
perfSummary,
|
|
169
|
-
meta,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
async function showHtmlReport(api, filters = {}) {
|
|
173
|
-
try {
|
|
174
|
-
const data = await buildCombinedData(api, filters);
|
|
175
|
-
const html = generateUsageHtml(data);
|
|
176
|
-
const dir = ensureReportDir();
|
|
177
|
-
const dateStr = new Date().toISOString().slice(0, 10);
|
|
178
|
-
const filePath = join(dir, `tokenwatch-${dateStr}.html`);
|
|
179
|
-
writeFileSync(filePath, html, "utf-8");
|
|
180
|
-
api.ui.toast?.({ message: `Report: ${filePath}`, variant: "info" });
|
|
181
|
-
openInBrowser(filePath);
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
185
|
-
api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
function showHtmlReportRangeMenu(api, dialog) {
|
|
189
|
-
dialog.replace(() => (<api.ui.DialogSelect title={t("cmdTitleHtml")} placeholder="Select date range..." options={[
|
|
190
|
-
{
|
|
191
|
-
title: t("menuToday"),
|
|
192
|
-
value: "today",
|
|
193
|
-
onSelect: () => {
|
|
194
|
-
dialog.clear();
|
|
195
|
-
const d = new Date();
|
|
196
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
197
|
-
const s = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
198
|
-
showHtmlReport(api, { startDate: s, endDate: s });
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
title: t("menu7d"),
|
|
203
|
-
value: "7d",
|
|
204
|
-
onSelect: () => { dialog.clear(); showHtmlReport(api, getPresetRange("7d")); },
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
title: t("menu30d"),
|
|
208
|
-
value: "30d",
|
|
209
|
-
onSelect: () => { dialog.clear(); showHtmlReport(api, getPresetRange("30d")); },
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
title: t("menuAll"),
|
|
213
|
-
value: "all",
|
|
214
|
-
onSelect: () => { dialog.clear(); showHtmlReport(api, getPresetRange("all")); },
|
|
215
|
-
},
|
|
216
|
-
]} flat={true}/>));
|
|
217
|
-
}
|
|
218
|
-
function showUsageMenu(api, dialog) {
|
|
219
|
-
try {
|
|
220
|
-
setLanguage(loadConfigFromStore(api).language);
|
|
221
|
-
}
|
|
222
|
-
catch { }
|
|
223
|
-
dialog.replace(() => (<api.ui.DialogSelect title={t("panelTitle")} placeholder="Select an action..." options={[
|
|
224
|
-
{
|
|
225
|
-
title: `${t("cmdTitleHtml")} ▸`,
|
|
226
|
-
value: "html",
|
|
227
|
-
description: t("cmdDescHtml"),
|
|
228
|
-
onSelect: () => showHtmlReportRangeMenu(api, dialog),
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
title: t("cmdTitleJson"),
|
|
232
|
-
value: "json",
|
|
233
|
-
description: t("cmdDescJson"),
|
|
234
|
-
onSelect: () => { dialog.clear(); showJsonExport(api); },
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
title: t("cmdTitleText"),
|
|
238
|
-
value: "text",
|
|
239
|
-
description: t("cmdDescText"),
|
|
240
|
-
onSelect: () => { dialog.clear(); showTextReport(api); },
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
title: `${t("cmdTitleSettings")} ▸`,
|
|
244
|
-
value: "settings",
|
|
245
|
-
description: t("cmdDescSettings"),
|
|
246
|
-
onSelect: () => showSettingsDialog(api, dialog),
|
|
247
|
-
},
|
|
248
|
-
]} flat={true}/>));
|
|
249
|
-
}
|
|
250
|
-
async function showJsonExport(api) {
|
|
251
|
-
try {
|
|
252
|
-
const report = await getUsageReport({});
|
|
253
|
-
const dir = ensureReportDir();
|
|
254
|
-
const dateStr = new Date().toISOString().slice(0, 10);
|
|
255
|
-
const filePath = join(dir, `tokenwatch-${dateStr}.json`);
|
|
256
|
-
writeFileSync(filePath, JSON.stringify(report, null, 2), "utf-8");
|
|
257
|
-
api.ui.toast?.({ message: `JSON: ${filePath}`, variant: "info" });
|
|
258
|
-
}
|
|
259
|
-
catch (err) {
|
|
260
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
261
|
-
api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
async function showTextReport(api) {
|
|
265
|
-
try {
|
|
266
|
-
const report = await getUsageReport({});
|
|
267
|
-
const formatted = formatUsageReport(report);
|
|
268
|
-
const dir = ensureReportDir();
|
|
269
|
-
const dateStr = new Date().toISOString().slice(0, 10);
|
|
270
|
-
const filePath = join(dir, `tokenwatch-${dateStr}.md`);
|
|
271
|
-
writeFileSync(filePath, formatted, "utf-8");
|
|
272
|
-
api.ui.toast?.({ message: `Report saved to ${filePath}`, variant: "info" });
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
276
|
-
api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
function saveConfigToStore(api, cfg) {
|
|
280
|
-
api.kv?.set?.("tokenwatch-config", cfg);
|
|
281
|
-
}
|
|
282
|
-
let lastSelectedSetting;
|
|
283
|
-
function showSettingsDialog(api, dialog) {
|
|
284
|
-
if (!dialog)
|
|
285
|
-
return;
|
|
286
|
-
const reopen = (value) => {
|
|
287
|
-
lastSelectedSetting = value;
|
|
288
|
-
setTimeout(() => showSettingsDialog(api, dialog), 0);
|
|
289
|
-
};
|
|
290
|
-
const cfg = loadConfigFromStore(api).sidebar;
|
|
291
|
-
dialog.replace(() => (<api.ui.DialogSelect title={t("settingsTitle")} placeholder={t("settingsPlaceholder")} options={[
|
|
292
|
-
{
|
|
293
|
-
title: `${cfg.showPerformance ? "✓ " : " "}${t("showPerformance")}`,
|
|
294
|
-
value: "showPerformance",
|
|
295
|
-
description: t("descShowPerformance"),
|
|
296
|
-
onSelect: () => { toggleSidebarSetting(api, "showPerformance"); reopen("showPerformance"); },
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
title: `${cfg.showPricing ? "✓ " : " "}${t("showPricing")}`,
|
|
300
|
-
value: "showPricing",
|
|
301
|
-
description: t("descShowPricing"),
|
|
302
|
-
onSelect: () => { toggleSidebarSetting(api, "showPricing"); reopen("showPricing"); },
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
title: `${cfg.showTokenDistribution ? "✓ " : " "}${t("showTokenDistribution")}`,
|
|
306
|
-
value: "showTokenDistribution",
|
|
307
|
-
description: t("descShowTokenDistribution"),
|
|
308
|
-
onSelect: () => { toggleSidebarSetting(api, "showTokenDistribution"); reopen("showTokenDistribution"); },
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
title: `${cfg.showTrend ? "✓ " : " "}${t("showTrend")}`,
|
|
312
|
-
value: "showTrend",
|
|
313
|
-
description: t("descShowTrend"),
|
|
314
|
-
onSelect: () => { toggleSidebarSetting(api, "showTrend"); reopen("showTrend"); },
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
title: `${t("settingsLanguage")} ▸`,
|
|
318
|
-
value: "language",
|
|
319
|
-
description: t("descSettingsLanguage"),
|
|
320
|
-
onSelect: () => showLanguageMenu(api, dialog),
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
title: t("done"),
|
|
324
|
-
value: "done",
|
|
325
|
-
description: t("closeSettings"),
|
|
326
|
-
onSelect: () => { lastSelectedSetting = undefined; dialog.clear(); },
|
|
327
|
-
},
|
|
328
|
-
]} flat={true} current={lastSelectedSetting}/>));
|
|
329
|
-
}
|
|
330
|
-
function showLanguageMenu(api, dialog) {
|
|
331
|
-
const current = api.kv?.get?.("tokenwatch-config")?.language ?? "auto";
|
|
332
|
-
dialog.replace(() => (<api.ui.DialogSelect title={t("settingsLanguage")} placeholder={t("settingsLanguage")} options={[
|
|
333
|
-
{
|
|
334
|
-
title: `${current === "auto" ? "✓ " : " "}${t("langAuto")}`,
|
|
335
|
-
value: "auto",
|
|
336
|
-
description: "自动检测 / Auto-detect",
|
|
337
|
-
onSelect: () => { setLanguageSetting(api, "auto"); lastSelectedSetting = "language"; dialog.clear(); showSettingsDialog(api, dialog); },
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
title: `${current === "zh" ? "✓ " : " "}中文`,
|
|
341
|
-
value: "zh",
|
|
342
|
-
description: "简体中文",
|
|
343
|
-
onSelect: () => { setLanguageSetting(api, "zh"); lastSelectedSetting = "language"; dialog.clear(); showSettingsDialog(api, dialog); },
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
title: `${current === "en" ? "✓ " : " "}English`,
|
|
347
|
-
value: "en",
|
|
348
|
-
description: "English",
|
|
349
|
-
onSelect: () => { setLanguageSetting(api, "en"); lastSelectedSetting = "language"; dialog.clear(); showSettingsDialog(api, dialog); },
|
|
350
|
-
},
|
|
351
|
-
]} flat={true}/>));
|
|
352
|
-
}
|
|
353
|
-
function setLanguageSetting(api, lang) {
|
|
354
|
-
setLanguage(lang);
|
|
355
|
-
const current = loadConfigFromStore(api);
|
|
356
|
-
current.language = lang;
|
|
357
|
-
saveConfigToStore(api, current);
|
|
358
|
-
const v = (api.kv?.get?.("tokenwatch-config-version") ?? 0) + 1;
|
|
359
|
-
api.kv?.set?.("tokenwatch-config-version", v);
|
|
360
|
-
}
|
|
361
|
-
function toggleSidebarSetting(api, key) {
|
|
362
|
-
const current = loadConfigFromStore(api);
|
|
363
|
-
current.sidebar[key] = !current.sidebar[key];
|
|
364
|
-
saveConfigToStore(api, current);
|
|
365
|
-
const v = (api.kv?.get?.("tokenwatch-config-version") ?? 0) + 1;
|
|
366
|
-
api.kv?.set?.("tokenwatch-config-version", v);
|
|
367
|
-
}
|
|
368
|
-
function loadConfigFromStore(api) {
|
|
369
|
-
const base = { sidebar: { ...DEFAULT_CONFIG.sidebar }, language: DEFAULT_CONFIG.language };
|
|
370
|
-
try {
|
|
371
|
-
const stored = api.kv?.get?.("tokenwatch-config");
|
|
372
|
-
if (stored) {
|
|
373
|
-
if (stored.sidebar)
|
|
374
|
-
Object.assign(base.sidebar, stored.sidebar);
|
|
375
|
-
if (stored.language)
|
|
376
|
-
base.language = stored.language;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
catch { /* defaults */ }
|
|
380
|
-
return base;
|
|
381
|
-
}
|
package/dist/formatter.js
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
export function formatTokens(n) {
|
|
2
|
-
if (n >= 1_000_000_000)
|
|
3
|
-
return `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
4
|
-
if (n >= 1_000_000)
|
|
5
|
-
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
6
|
-
if (n >= 1_000)
|
|
7
|
-
return `${(n / 1_000).toFixed(1)}K`;
|
|
8
|
-
return String(n);
|
|
9
|
-
}
|
|
10
|
-
export function formatCost(n) {
|
|
11
|
-
if (n === 0)
|
|
12
|
-
return "$0.00";
|
|
13
|
-
if (n < 0.01)
|
|
14
|
-
return `$${n.toFixed(4)}`;
|
|
15
|
-
return `$${n.toFixed(2)}`;
|
|
16
|
-
}
|
|
17
|
-
export function formatDuration(ms) {
|
|
18
|
-
if (ms === null)
|
|
19
|
-
return "—";
|
|
20
|
-
if (ms < 1000)
|
|
21
|
-
return `${ms.toFixed(0)}ms`;
|
|
22
|
-
if (ms < 60000)
|
|
23
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
24
|
-
const m = Math.floor(ms / 60000);
|
|
25
|
-
const s = Math.floor((ms % 60000) / 1000);
|
|
26
|
-
return `${m}m ${s}s`;
|
|
27
|
-
}
|
|
28
|
-
export function formatFilters(filters) {
|
|
29
|
-
const parts = [];
|
|
30
|
-
if (filters.sessionId)
|
|
31
|
-
parts.push(`session=${filters.sessionId}`);
|
|
32
|
-
if (filters.provider)
|
|
33
|
-
parts.push(`provider=${filters.provider}`);
|
|
34
|
-
if (filters.model)
|
|
35
|
-
parts.push(`model=${filters.model}`);
|
|
36
|
-
if (filters.startDate || filters.endDate) {
|
|
37
|
-
parts.push(`date=${filters.startDate ?? "..."}..${filters.endDate ?? "..."}`);
|
|
38
|
-
}
|
|
39
|
-
return parts.length ? parts.join(" | ") : "scope=all local sessions";
|
|
40
|
-
}
|
|
41
|
-
function getVisualWidth(str) {
|
|
42
|
-
let width = 0;
|
|
43
|
-
for (let i = 0; i < str.length; i++) {
|
|
44
|
-
width += str.charCodeAt(i) > 255 ? 2 : 1;
|
|
45
|
-
}
|
|
46
|
-
return width;
|
|
47
|
-
}
|
|
48
|
-
function truncateByWidth(str, maxWidth) {
|
|
49
|
-
if (getVisualWidth(str) <= maxWidth)
|
|
50
|
-
return str;
|
|
51
|
-
let width = 0;
|
|
52
|
-
let res = "";
|
|
53
|
-
for (let i = 0; i < str.length; i++) {
|
|
54
|
-
const charWidth = str.charCodeAt(i) > 255 ? 2 : 1;
|
|
55
|
-
if (width + charWidth > maxWidth - 3) {
|
|
56
|
-
return res + "...";
|
|
57
|
-
}
|
|
58
|
-
width += charWidth;
|
|
59
|
-
res += str[i];
|
|
60
|
-
}
|
|
61
|
-
return res;
|
|
62
|
-
}
|
|
63
|
-
function table(columns, rows, totalRow) {
|
|
64
|
-
const widths = columns.map((col, i) => {
|
|
65
|
-
const rowWidths = rows.map((row) => getVisualWidth(row[i] ?? ""));
|
|
66
|
-
const maxRow = rowWidths.length ? Math.max(...rowWidths) : 0;
|
|
67
|
-
const totalWidth = totalRow ? getVisualWidth(totalRow[i] ?? "") : 0;
|
|
68
|
-
return Math.max(getVisualWidth(col.label), maxRow, totalWidth);
|
|
69
|
-
});
|
|
70
|
-
const renderRow = (cells) => "║" + cells.map((cell, i) => {
|
|
71
|
-
const value = cell ?? "";
|
|
72
|
-
const vWidth = getVisualWidth(value);
|
|
73
|
-
const padding = " ".repeat(widths[i] - vWidth);
|
|
74
|
-
const content = columns[i].align === "right"
|
|
75
|
-
? padding + value
|
|
76
|
-
: value + padding;
|
|
77
|
-
return ` ${content} ║`;
|
|
78
|
-
}).join("");
|
|
79
|
-
const sep = (left, mid, right, fill) => left + widths.map((w) => fill.repeat(w + 2)).join(mid) + right;
|
|
80
|
-
const lines = [
|
|
81
|
-
sep("╔", "╦", "╗", "═"),
|
|
82
|
-
renderRow(columns.map((col) => col.label)),
|
|
83
|
-
sep("╠", "╬", "╣", "═"),
|
|
84
|
-
...rows.map(renderRow),
|
|
85
|
-
];
|
|
86
|
-
if (totalRow) {
|
|
87
|
-
lines.push(sep("╠", "╬", "╣", "═"));
|
|
88
|
-
lines.push(renderRow(totalRow));
|
|
89
|
-
}
|
|
90
|
-
lines.push(sep("╚", "╩", "╝", "═"));
|
|
91
|
-
return lines.join("\n");
|
|
92
|
-
}
|
|
93
|
-
export function formatSessionSummary(data, title = "Current Session") {
|
|
94
|
-
const modelLabel = data.modelsUsed.length > 1
|
|
95
|
-
? `${data.modelsUsed.length} models`
|
|
96
|
-
: data.model || "(unknown)";
|
|
97
|
-
return [
|
|
98
|
-
`═══ ${title} ═══`,
|
|
99
|
-
`Models: ${modelLabel}`,
|
|
100
|
-
`Provider: ${data.provider || "(mixed)"}`,
|
|
101
|
-
`Req: ${data.requestCount}`,
|
|
102
|
-
`Total Tokens: ${formatTokens(data.totalTokens)}`,
|
|
103
|
-
` Input: ${formatTokens(data.inputTokens)}`,
|
|
104
|
-
` Output: ${formatTokens(data.outputTokens)}`,
|
|
105
|
-
` Reasoning: ${formatTokens(data.reasoningTokens)}`,
|
|
106
|
-
` C.Read: ${formatTokens(data.cacheRead)}`,
|
|
107
|
-
` C.Write: ${formatTokens(data.cacheWrite)}`,
|
|
108
|
-
` Cost: ${formatCost(data.totalCost)}`,
|
|
109
|
-
].join("\n");
|
|
110
|
-
}
|
|
111
|
-
export function formatStatusBar(data) {
|
|
112
|
-
return `Tok:${formatTokens(data.totalTokens)} Req:${data.requestCount} Cost:${formatCost(data.totalCost)}`;
|
|
113
|
-
}
|
|
114
|
-
export function formatModelBreakdown(items) {
|
|
115
|
-
if (items.length === 0)
|
|
116
|
-
return "═══ Model Breakdown ═══\n(no data)";
|
|
117
|
-
const total = items.reduce((acc, item) => ({
|
|
118
|
-
requests: acc.requests + item.requests,
|
|
119
|
-
totalTokens: acc.totalTokens + item.totalTokens,
|
|
120
|
-
inputTokens: acc.inputTokens + item.inputTokens,
|
|
121
|
-
outputTokens: acc.outputTokens + item.outputTokens,
|
|
122
|
-
cacheRead: acc.cacheRead + item.cacheRead,
|
|
123
|
-
}), {
|
|
124
|
-
requests: 0,
|
|
125
|
-
totalTokens: 0,
|
|
126
|
-
inputTokens: 0,
|
|
127
|
-
outputTokens: 0,
|
|
128
|
-
cacheRead: 0,
|
|
129
|
-
});
|
|
130
|
-
const rows = items.map((item) => [
|
|
131
|
-
item.provider || "-",
|
|
132
|
-
truncateByWidth(item.model, 20),
|
|
133
|
-
String(item.requests),
|
|
134
|
-
formatTokens(item.totalTokens),
|
|
135
|
-
formatTokens(item.inputTokens),
|
|
136
|
-
formatTokens(item.outputTokens),
|
|
137
|
-
formatTokens(item.cacheRead),
|
|
138
|
-
]);
|
|
139
|
-
return [
|
|
140
|
-
"═══ Model Breakdown ═══",
|
|
141
|
-
table([
|
|
142
|
-
{ label: "Provider", align: "left" },
|
|
143
|
-
{ label: "Model", align: "left" },
|
|
144
|
-
{ label: "Req", align: "right" },
|
|
145
|
-
{ label: "Total", align: "right" },
|
|
146
|
-
{ label: "In", align: "right" },
|
|
147
|
-
{ label: "Out", align: "right" },
|
|
148
|
-
{ label: "Cache", align: "right" },
|
|
149
|
-
], rows, [
|
|
150
|
-
"TOTAL",
|
|
151
|
-
"",
|
|
152
|
-
String(total.requests),
|
|
153
|
-
formatTokens(total.totalTokens),
|
|
154
|
-
formatTokens(total.inputTokens),
|
|
155
|
-
formatTokens(total.outputTokens),
|
|
156
|
-
formatTokens(total.cacheRead),
|
|
157
|
-
]),
|
|
158
|
-
].join("\n");
|
|
159
|
-
}
|
|
160
|
-
export function formatProviderBreakdown(items) {
|
|
161
|
-
if (items.length === 0)
|
|
162
|
-
return "═══ Provider Breakdown ═══\n(no data)";
|
|
163
|
-
const total = items.reduce((acc, item) => ({
|
|
164
|
-
requests: acc.requests + item.requests,
|
|
165
|
-
totalTokens: acc.totalTokens + item.totalTokens,
|
|
166
|
-
inputTokens: acc.inputTokens + item.inputTokens,
|
|
167
|
-
outputTokens: acc.outputTokens + item.outputTokens,
|
|
168
|
-
cacheRead: acc.cacheRead + item.cacheRead,
|
|
169
|
-
}), {
|
|
170
|
-
requests: 0,
|
|
171
|
-
totalTokens: 0,
|
|
172
|
-
inputTokens: 0,
|
|
173
|
-
outputTokens: 0,
|
|
174
|
-
cacheRead: 0,
|
|
175
|
-
});
|
|
176
|
-
const rows = items.map((item) => [
|
|
177
|
-
item.provider || "-",
|
|
178
|
-
String(item.requests),
|
|
179
|
-
formatTokens(item.totalTokens),
|
|
180
|
-
formatTokens(item.inputTokens),
|
|
181
|
-
formatTokens(item.outputTokens),
|
|
182
|
-
formatTokens(item.cacheRead),
|
|
183
|
-
]);
|
|
184
|
-
return [
|
|
185
|
-
"═══ Provider Breakdown ═══",
|
|
186
|
-
table([
|
|
187
|
-
{ label: "Provider", align: "left" },
|
|
188
|
-
{ label: "Req", align: "right" },
|
|
189
|
-
{ label: "Total", align: "right" },
|
|
190
|
-
{ label: "In", align: "right" },
|
|
191
|
-
{ label: "Out", align: "right" },
|
|
192
|
-
{ label: "Cache", align: "right" },
|
|
193
|
-
], rows, [
|
|
194
|
-
"TOTAL",
|
|
195
|
-
String(total.requests),
|
|
196
|
-
formatTokens(total.totalTokens),
|
|
197
|
-
formatTokens(total.inputTokens),
|
|
198
|
-
formatTokens(total.outputTokens),
|
|
199
|
-
formatTokens(total.cacheRead),
|
|
200
|
-
]),
|
|
201
|
-
].join("\n");
|
|
202
|
-
}
|
|
203
|
-
export function formatDailyBreakdown(items) {
|
|
204
|
-
if (items.length === 0)
|
|
205
|
-
return "═══ Daily Breakdown ═══\n(no data)";
|
|
206
|
-
const total = items.reduce((acc, item) => ({
|
|
207
|
-
requests: acc.requests + item.requests,
|
|
208
|
-
totalTokens: acc.totalTokens + item.totalTokens,
|
|
209
|
-
inputTokens: acc.inputTokens + item.inputTokens,
|
|
210
|
-
outputTokens: acc.outputTokens + item.outputTokens,
|
|
211
|
-
cacheRead: acc.cacheRead + item.cacheRead,
|
|
212
|
-
}), {
|
|
213
|
-
requests: 0,
|
|
214
|
-
totalTokens: 0,
|
|
215
|
-
inputTokens: 0,
|
|
216
|
-
outputTokens: 0,
|
|
217
|
-
cacheRead: 0,
|
|
218
|
-
});
|
|
219
|
-
const rows = items.map((item) => [
|
|
220
|
-
item.day,
|
|
221
|
-
String(item.requests),
|
|
222
|
-
formatTokens(item.totalTokens),
|
|
223
|
-
formatTokens(item.inputTokens),
|
|
224
|
-
formatTokens(item.outputTokens),
|
|
225
|
-
formatTokens(item.cacheRead),
|
|
226
|
-
]);
|
|
227
|
-
return [
|
|
228
|
-
"═══ Daily Breakdown ═══",
|
|
229
|
-
table([
|
|
230
|
-
{ label: "Day", align: "left" },
|
|
231
|
-
{ label: "Req", align: "right" },
|
|
232
|
-
{ label: "Total", align: "right" },
|
|
233
|
-
{ label: "In", align: "right" },
|
|
234
|
-
{ label: "Out", align: "right" },
|
|
235
|
-
{ label: "Cache", align: "right" },
|
|
236
|
-
], rows, [
|
|
237
|
-
"TOTAL",
|
|
238
|
-
String(total.requests),
|
|
239
|
-
formatTokens(total.totalTokens),
|
|
240
|
-
formatTokens(total.inputTokens),
|
|
241
|
-
formatTokens(total.outputTokens),
|
|
242
|
-
formatTokens(total.cacheRead),
|
|
243
|
-
]),
|
|
244
|
-
].join("\n");
|
|
245
|
-
}
|
|
246
|
-
export function formatSessionBreakdown(items) {
|
|
247
|
-
if (items.length === 0)
|
|
248
|
-
return "═══ Session Breakdown ═══\n(no data)";
|
|
249
|
-
const rows = items.map((item) => {
|
|
250
|
-
// Truncate title by visual width (40 columns)
|
|
251
|
-
const title = truncateByWidth(item.title, 40);
|
|
252
|
-
return [
|
|
253
|
-
item.day,
|
|
254
|
-
item.provider || "-",
|
|
255
|
-
truncateByWidth(item.model, 20),
|
|
256
|
-
String(item.requests),
|
|
257
|
-
formatTokens(item.totalTokens),
|
|
258
|
-
formatTokens(item.cacheRead),
|
|
259
|
-
title,
|
|
260
|
-
];
|
|
261
|
-
});
|
|
262
|
-
return [
|
|
263
|
-
"═══ Session Breakdown ═══",
|
|
264
|
-
table([
|
|
265
|
-
{ label: "Day", align: "left" },
|
|
266
|
-
{ label: "Provider", align: "left" },
|
|
267
|
-
{ label: "Model", align: "left" },
|
|
268
|
-
{ label: "Req", align: "right" },
|
|
269
|
-
{ label: "Total", align: "right" },
|
|
270
|
-
{ label: "Cache", align: "right" },
|
|
271
|
-
{ label: "Title", align: "left" },
|
|
272
|
-
], rows),
|
|
273
|
-
].join("\n");
|
|
274
|
-
}
|
|
275
|
-
export function formatUsageReport(report) {
|
|
276
|
-
return [
|
|
277
|
-
`Filters: ${formatFilters(report.filters)}`,
|
|
278
|
-
"",
|
|
279
|
-
formatSessionSummary(report.summary, "Usage Summary"),
|
|
280
|
-
"",
|
|
281
|
-
formatModelBreakdown(report.models),
|
|
282
|
-
"",
|
|
283
|
-
formatProviderBreakdown(report.providers),
|
|
284
|
-
"",
|
|
285
|
-
formatDailyBreakdown(report.daily),
|
|
286
|
-
"",
|
|
287
|
-
formatSessionBreakdown(report.sessions),
|
|
288
|
-
].join("\n");
|
|
289
|
-
}
|