opencode-tokenwatch 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/dist/i18n.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type SupportedLanguage = "zh" | "en";
2
+ export declare function detectLanguage(): SupportedLanguage;
3
+ export declare function setLanguage(lang: SupportedLanguage | "auto"): void;
4
+ export declare function getCurrentLanguage(): SupportedLanguage;
5
+ export declare function t(key: string): string;
package/dist/i18n.js ADDED
@@ -0,0 +1,175 @@
1
+ const zh = {
2
+ panelTitle: "TokenWatch",
3
+ collapse: "折叠",
4
+ expand: "展开",
5
+ sessionSummary: "会话累计",
6
+ input: "输入",
7
+ output: "输出",
8
+ cacheRead: "缓存",
9
+ cacheWrite: "缓存写",
10
+ cacheMiss: "未命中",
11
+ hitRate: "命中率",
12
+ requests: "请求",
13
+ cost: "成本",
14
+ trendUp: "↑",
15
+ trendDown: "↓",
16
+ cache: "缓存",
17
+ performance: "性能",
18
+ pricing: "Pricing",
19
+ tokenDistribution: "Token分布",
20
+ modelLabel: "模型",
21
+ provider: "提供商",
22
+ ttft: "TTFT",
23
+ tps: "TPS",
24
+ latency: "延迟",
25
+ avg: "平均",
26
+ max: "最大",
27
+ min: "最小",
28
+ read: "读",
29
+ write: "写",
30
+ sessionAccumulated: "Session累计",
31
+ saving: "节省",
32
+ priceInput: "输入",
33
+ priceCacheRead: "缓存读",
34
+ priceCacheWrite: "缓存写",
35
+ priceOutput: "输出",
36
+ total: "总计",
37
+ system: "系统提示",
38
+ user: "用户",
39
+ agent: "Agent指令",
40
+ toolCall: "Tool调用",
41
+ toolResult: "Tool结果",
42
+ outputTokens: "输出",
43
+ settings: "Settings",
44
+ showCache: "显示缓存统计",
45
+ showPerformance: "显示性能指标",
46
+ showPricing: "显示模型定价",
47
+ showTokenDistribution: "显示Token分布",
48
+ showTrend: "显示趋势指示器",
49
+ language: "语言",
50
+ auto: "自动",
51
+ cmdTitleHtml: "HTML报告",
52
+ cmdDescHtml: "生成交互式HTML仪表盘,展示Token用量、缓存和性能图表",
53
+ cmdTitleJson: "JSON导出",
54
+ cmdDescJson: "导出原始用量数据为JSON文件",
55
+ cmdTitleText: "文本报告",
56
+ cmdDescText: "生成纯文本报告文件",
57
+ cmdTitleSettings: "设置",
58
+ cmdDescSettings: "配置侧边栏显示选项",
59
+ descShowPerformance: "在侧边栏显示TPS、TTFT、延迟等指标",
60
+ descShowPricing: "在侧边栏显示成本估算",
61
+ descShowTokenDistribution: "在侧边栏显示输入/输出/推理Token细分",
62
+ descShowTrend: "在侧边栏显示Token用量趋势",
63
+ settingsLanguage: "语言",
64
+ descSettingsLanguage: "切换显示语言",
65
+ settingsTitle: "TokenWatch设置",
66
+ settingsPlaceholder: "切换设置项...",
67
+ langAuto: "自动",
68
+ done: "完成",
69
+ closeSettings: "关闭设置",
70
+ menuToday: "今天",
71
+ menu7d: "最近 7 天",
72
+ menu30d: "最近 30 天",
73
+ menuAll: "全部时间",
74
+ };
75
+ const en = {
76
+ panelTitle: "TokenWatch",
77
+ collapse: "Collapse",
78
+ expand: "Expand",
79
+ sessionSummary: "Session",
80
+ input: "Input",
81
+ output: "Output",
82
+ cacheRead: "Cache",
83
+ cacheWrite: "C.Write",
84
+ cacheMiss: "Cache Miss",
85
+ hitRate: "Hit Rate",
86
+ requests: "Req",
87
+ cost: "Cost",
88
+ trendUp: "↑",
89
+ trendDown: "↓",
90
+ cache: "Cache",
91
+ performance: "Performance",
92
+ pricing: "Pricing",
93
+ tokenDistribution: "Token Distribution",
94
+ modelLabel: "Model",
95
+ provider: "Provider",
96
+ ttft: "TTFT",
97
+ tps: "TPS",
98
+ latency: "Latency",
99
+ avg: "Avg",
100
+ max: "Max",
101
+ min: "Min",
102
+ read: "Read",
103
+ write: "Write",
104
+ sessionAccumulated: "Session Accumulated",
105
+ saving: "Saving",
106
+ priceInput: "Input",
107
+ priceCacheRead: "Cache Read",
108
+ priceCacheWrite: "Cache Write",
109
+ priceOutput: "Output",
110
+ total: "Total",
111
+ system: "System",
112
+ user: "User",
113
+ agent: "Agent",
114
+ toolCall: "Tool Call",
115
+ toolResult: "Tool Result",
116
+ outputTokens: "Output",
117
+ settings: "Settings",
118
+ showCache: "Show Cache",
119
+ showPerformance: "Show Performance",
120
+ showPricing: "Show Pricing",
121
+ showTokenDistribution: "Show Token Distribution",
122
+ showTrend: "Show Trend",
123
+ language: "Language",
124
+ auto: "Auto",
125
+ cmdTitleHtml: "HTML Report",
126
+ cmdDescHtml: "Generate interactive HTML dashboard with token usage, cache, and performance charts",
127
+ cmdTitleJson: "JSON Export",
128
+ cmdDescJson: "Export raw usage data as JSON file",
129
+ cmdTitleText: "Text Report",
130
+ cmdDescText: "Generate plain text report file",
131
+ cmdTitleSettings: "Settings",
132
+ cmdDescSettings: "Configure sidebar display options",
133
+ descShowPerformance: "Display TPS, TTFT, latency metrics in sidebar",
134
+ descShowPricing: "Display cost estimates in sidebar",
135
+ descShowTokenDistribution: "Display input/output/reasoning token breakdown in sidebar",
136
+ descShowTrend: "Display token usage trend in sidebar",
137
+ settingsLanguage: "Language",
138
+ descSettingsLanguage: "Switch display language",
139
+ settingsTitle: "TokenWatch Settings",
140
+ settingsPlaceholder: "Toggle settings...",
141
+ langAuto: "Auto",
142
+ done: "Done",
143
+ closeSettings: "Close settings",
144
+ menuToday: "Today",
145
+ menu7d: "Last 7 Days",
146
+ menu30d: "Last 30 Days",
147
+ menuAll: "All Time",
148
+ };
149
+ let currentLang = detectLanguage();
150
+ export function detectLanguage() {
151
+ try {
152
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
153
+ if (locale.startsWith("zh"))
154
+ return "zh";
155
+ }
156
+ catch {
157
+ // Intl may not be available in restricted environments
158
+ }
159
+ return "en";
160
+ }
161
+ export function setLanguage(lang) {
162
+ if (lang === "auto") {
163
+ currentLang = detectLanguage();
164
+ }
165
+ else {
166
+ currentLang = lang;
167
+ }
168
+ }
169
+ export function getCurrentLanguage() {
170
+ return currentLang;
171
+ }
172
+ export function t(key) {
173
+ const table = currentLang === "zh" ? zh : en;
174
+ return table[key] ?? key;
175
+ }
@@ -0,0 +1,55 @@
1
+ import type { LogEntry, SessionPerfStats } from "./formatter.js";
2
+ interface PartEvent {
3
+ message_id?: string;
4
+ type?: string;
5
+ text?: string;
6
+ time?: {
7
+ start?: number;
8
+ };
9
+ }
10
+ interface MessageUpdateEvent {
11
+ properties: {
12
+ info: {
13
+ id?: string;
14
+ sessionID?: string;
15
+ role?: string;
16
+ providerID?: string;
17
+ modelID?: string;
18
+ tokens?: {
19
+ input?: number;
20
+ output?: number;
21
+ reasoning?: number;
22
+ cache?: {
23
+ read?: number;
24
+ write?: number;
25
+ };
26
+ };
27
+ cost?: number;
28
+ time?: {
29
+ created?: number;
30
+ completed?: number;
31
+ };
32
+ };
33
+ };
34
+ }
35
+ interface MessageRemoveEvent {
36
+ properties: {
37
+ sessionID?: string;
38
+ messageID?: string;
39
+ };
40
+ }
41
+ declare class PerfTracker {
42
+ private firstPartTimes;
43
+ private statsMap;
44
+ handlePartUpdated(event: PartEvent): void;
45
+ handleMessageUpdated(event: MessageUpdateEvent): void;
46
+ private appendLog;
47
+ handleMessageRemoved(event: MessageRemoveEvent): void;
48
+ private updateStats;
49
+ getSessionStats(): SessionPerfStats;
50
+ readLogs(last?: number): LogEntry[];
51
+ reset(): void;
52
+ }
53
+ export declare function createPerfTracker(): PerfTracker;
54
+ export type { PartEvent, PerfTracker };
55
+ export declare function readLogs(last?: number): LogEntry[];
@@ -0,0 +1,185 @@
1
+ import { appendFileSync } from "node:fs";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { existsSync } from "node:fs";
6
+ const LOG_PATH = join(homedir(), ".opencode", "tokenwatch.jsonl");
7
+ class PerfTracker {
8
+ firstPartTimes = new Map();
9
+ statsMap = new Map();
10
+ handlePartUpdated(event) {
11
+ if (!event.time?.start || !event.message_id)
12
+ return;
13
+ this.firstPartTimes.set(event.message_id, event.time.start);
14
+ }
15
+ handleMessageUpdated(event) {
16
+ const info = event.properties?.info;
17
+ if (!info || info.role !== "assistant")
18
+ return;
19
+ if (!info.time?.completed)
20
+ return;
21
+ const messageID = info.id ?? "";
22
+ const created = info.time.created;
23
+ const completed = info.time.completed;
24
+ if (!created || !completed) {
25
+ this.firstPartTimes.delete(messageID);
26
+ return;
27
+ }
28
+ const sessionID = info.sessionID ?? "";
29
+ const providerID = info.providerID ?? "unknown";
30
+ const modelID = info.modelID ?? "unknown";
31
+ const model = `${providerID}/${modelID}`;
32
+ const tokens = info.tokens;
33
+ const inputTokens = tokens?.input ?? 0;
34
+ const outputTokens = tokens?.output ?? 0;
35
+ const reasoningTokens = tokens?.reasoning ?? 0;
36
+ const cacheRead = tokens?.cache?.read ?? 0;
37
+ const cacheWrite = tokens?.cache?.write ?? 0;
38
+ const cost = info.cost ?? 0;
39
+ const firstPart = this.firstPartTimes.get(messageID) ?? null;
40
+ const latencyMs = completed - created;
41
+ const ttftMs = firstPart !== null ? firstPart - created : null;
42
+ const genMs = firstPart !== null ? completed - firstPart : null;
43
+ const tps = (genMs !== null && genMs > 0 && outputTokens > 0)
44
+ ? (outputTokens / genMs) * 1000
45
+ : null;
46
+ const tpsFallback = (tps === null && latencyMs > 0 && outputTokens > 0)
47
+ ? (outputTokens / latencyMs) * 1000
48
+ : null;
49
+ this.firstPartTimes.delete(messageID);
50
+ const entry = {
51
+ ts: new Date().toISOString(),
52
+ model,
53
+ providerID,
54
+ modelID,
55
+ sessionID,
56
+ ttft_ms: ttftMs,
57
+ tps: tps ?? tpsFallback,
58
+ latency_ms: latencyMs,
59
+ inputTokens,
60
+ outputTokens,
61
+ reasoningTokens,
62
+ cacheReadTokens: cacheRead,
63
+ cacheWriteTokens: cacheWrite,
64
+ cost,
65
+ };
66
+ this.appendLog(entry);
67
+ this.updateStats(model, entry);
68
+ }
69
+ appendLog(entry) {
70
+ try {
71
+ appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n");
72
+ }
73
+ catch {
74
+ // Silently fail — logging is non-critical
75
+ }
76
+ }
77
+ handleMessageRemoved(event) {
78
+ const mid = event.properties?.messageID ?? "";
79
+ if (mid) {
80
+ this.firstPartTimes.delete(mid);
81
+ }
82
+ }
83
+ updateStats(model, entry) {
84
+ let stats = this.statsMap.get(model);
85
+ if (!stats) {
86
+ stats = {
87
+ model,
88
+ providerID: entry.providerID,
89
+ requestCount: 0,
90
+ totalInput: 0,
91
+ totalOutput: 0,
92
+ totalCacheRead: 0,
93
+ totalCacheWrite: 0,
94
+ totalCost: 0,
95
+ avgTTFT: null,
96
+ maxTTFT: null,
97
+ minTTFT: null,
98
+ avgTPS: null,
99
+ maxTPS: null,
100
+ minTPS: null,
101
+ avgLatency: null,
102
+ maxLatency: null,
103
+ minLatency: null,
104
+ };
105
+ this.statsMap.set(model, stats);
106
+ }
107
+ stats.requestCount++;
108
+ stats.totalInput += entry.inputTokens;
109
+ stats.totalOutput += entry.outputTokens;
110
+ stats.totalCacheRead += entry.cacheReadTokens;
111
+ stats.totalCacheWrite += entry.cacheWriteTokens;
112
+ stats.totalCost += entry.cost;
113
+ if (entry.ttft_ms !== null) {
114
+ const c = stats.requestCount;
115
+ const prev = stats.avgTTFT;
116
+ stats.avgTTFT = prev !== null ? prev + (entry.ttft_ms - prev) / c : entry.ttft_ms;
117
+ stats.maxTTFT = stats.maxTTFT !== null ? Math.max(stats.maxTTFT, entry.ttft_ms) : entry.ttft_ms;
118
+ stats.minTTFT = stats.minTTFT !== null ? Math.min(stats.minTTFT, entry.ttft_ms) : entry.ttft_ms;
119
+ }
120
+ if (entry.tps !== null) {
121
+ const c = stats.requestCount;
122
+ const prev = stats.avgTPS;
123
+ stats.avgTPS = prev !== null ? prev + (entry.tps - prev) / c : entry.tps;
124
+ stats.maxTPS = stats.maxTPS !== null ? Math.max(stats.maxTPS, entry.tps) : entry.tps;
125
+ stats.minTPS = stats.minTPS !== null ? Math.min(stats.minTPS, entry.tps) : entry.tps;
126
+ }
127
+ if (entry.latency_ms !== null) {
128
+ const c = stats.requestCount;
129
+ const prev = stats.avgLatency;
130
+ stats.avgLatency = prev !== null ? prev + (entry.latency_ms - prev) / c : entry.latency_ms;
131
+ stats.maxLatency = stats.maxLatency !== null ? Math.max(stats.maxLatency, entry.latency_ms) : entry.latency_ms;
132
+ stats.minLatency = stats.minLatency !== null ? Math.min(stats.minLatency, entry.latency_ms) : entry.latency_ms;
133
+ }
134
+ }
135
+ getSessionStats() {
136
+ let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0;
137
+ let totalRequests = 0, totalCost = 0;
138
+ for (const s of this.statsMap.values()) {
139
+ totalInput += s.totalInput;
140
+ totalOutput += s.totalOutput;
141
+ totalCacheRead += s.totalCacheRead;
142
+ totalCacheWrite += s.totalCacheWrite;
143
+ totalRequests += s.requestCount;
144
+ totalCost += s.totalCost;
145
+ }
146
+ return {
147
+ models: Object.fromEntries(this.statsMap),
148
+ totals: { totalInput, totalOutput, totalCacheRead, totalCacheWrite, totalRequests, totalCost },
149
+ };
150
+ }
151
+ readLogs(last = 50) {
152
+ try {
153
+ if (!existsSync(LOG_PATH))
154
+ return [];
155
+ const content = readFileSync(LOG_PATH, "utf-8").trim();
156
+ if (!content)
157
+ return [];
158
+ const lines = content.split("\n");
159
+ const entries = [];
160
+ for (let i = Math.max(0, lines.length - last); i < lines.length; i++) {
161
+ try {
162
+ entries.push(JSON.parse(lines[i]));
163
+ }
164
+ catch {
165
+ // Skip malformed lines
166
+ }
167
+ }
168
+ return entries;
169
+ }
170
+ catch {
171
+ return [];
172
+ }
173
+ }
174
+ reset() {
175
+ this.firstPartTimes.clear();
176
+ this.statsMap.clear();
177
+ }
178
+ }
179
+ export function createPerfTracker() {
180
+ return new PerfTracker();
181
+ }
182
+ export function readLogs(last = 50) {
183
+ const tracker = new PerfTracker();
184
+ return tracker.readLogs(last);
185
+ }
@@ -0,0 +1,22 @@
1
+ import type { TuiPluginApi, TuiTheme } from "@opencode-ai/plugin/tui";
2
+ import type { PerfTracker } from "./perf-tracker.js";
3
+ import type { TokenMessage } from "./tui.jsx";
4
+ export interface SidebarConfig {
5
+ sidebar: {
6
+ showPerformance: boolean;
7
+ showPricing: boolean;
8
+ showTokenDistribution: boolean;
9
+ showTrend: boolean;
10
+ };
11
+ language: "zh" | "en" | "auto";
12
+ }
13
+ export declare function loadConfig(api: TuiPluginApi): SidebarConfig;
14
+ interface TokenWatchPanelProps {
15
+ api: TuiPluginApi;
16
+ theme: TuiTheme;
17
+ perfTracker: PerfTracker;
18
+ messages: readonly any[];
19
+ allTokenMessages: TokenMessage[];
20
+ }
21
+ export declare function TokenWatchPanel(props: TokenWatchPanelProps): any;
22
+ export {};