opencode-tokenwatch 0.2.0 → 0.3.1
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 +19 -11
- package/README.md +20 -12
- package/dist/commands.jsx +69 -4
- package/dist/formatter.d.ts +30 -0
- package/dist/generate-usage-html.js +210 -71
- package/dist/i18n.js +2 -4
- package/dist/perf-tracker.d.ts +6 -0
- package/dist/perf-tracker.js +122 -13
- package/dist/queries.d.ts +3 -1
- package/dist/queries.js +65 -6
- package/dist/sidebar.d.ts +2 -2
- package/dist/sidebar.jsx +321 -92
- package/dist/stats-store.d.ts +23 -0
- package/dist/stats-store.js +258 -0
- package/dist/tui.jsx +84 -27
- package/package.json +1 -1
package/dist/perf-tracker.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { appendFileSync } from "node:fs";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
4
|
+
import { existsSync, statSync } from "node:fs";
|
|
5
|
+
import { updatePersistedStats } from "./stats-store.js";
|
|
6
6
|
const LOG_PATH = join(homedir(), ".opencode", "tokenwatch.jsonl");
|
|
7
7
|
class PerfTracker {
|
|
8
8
|
firstPartTimes = new Map();
|
|
9
9
|
statsMap = new Map();
|
|
10
|
+
/** 原始样本串,用于分位数计算,不持久化 */
|
|
11
|
+
ttftSamples = new Map();
|
|
12
|
+
latencySamples = new Map();
|
|
10
13
|
handlePartUpdated(event) {
|
|
11
14
|
if (!event.time?.start || !event.message_id)
|
|
12
15
|
return;
|
|
13
|
-
|
|
16
|
+
// Bug fix: 取最早 part 时间而非最后一个,避免 TTFT 被高估
|
|
17
|
+
const cur = this.firstPartTimes.get(event.message_id) ?? Number.POSITIVE_INFINITY;
|
|
18
|
+
if (event.time.start < cur) {
|
|
19
|
+
this.firstPartTimes.set(event.message_id, event.time.start);
|
|
20
|
+
}
|
|
14
21
|
}
|
|
15
22
|
handleMessageUpdated(event) {
|
|
16
23
|
const info = event.properties?.info;
|
|
@@ -43,9 +50,9 @@ class PerfTracker {
|
|
|
43
50
|
const tps = (genMs !== null && genMs > 0 && outputTokens > 0)
|
|
44
51
|
? (outputTokens / genMs) * 1000
|
|
45
52
|
: null;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
// Bug fix: 移除 TPS fallback。
|
|
54
|
+
// 原 fallback 用 latencyMs(completed-created,含排队+TTFT)计算 TPS,
|
|
55
|
+
// 会使结果严重低估(约 40%+)。null 表示"无可靠数据"比虚假数字更好。
|
|
49
56
|
this.firstPartTimes.delete(messageID);
|
|
50
57
|
const entry = {
|
|
51
58
|
ts: new Date().toISOString(),
|
|
@@ -54,7 +61,7 @@ class PerfTracker {
|
|
|
54
61
|
modelID,
|
|
55
62
|
sessionID,
|
|
56
63
|
ttft_ms: ttftMs,
|
|
57
|
-
tps: tps
|
|
64
|
+
tps: tps, // 只在有可靠 genMs 时才有值
|
|
58
65
|
latency_ms: latencyMs,
|
|
59
66
|
inputTokens,
|
|
60
67
|
outputTokens,
|
|
@@ -68,11 +75,23 @@ class PerfTracker {
|
|
|
68
75
|
}
|
|
69
76
|
appendLog(entry) {
|
|
70
77
|
try {
|
|
78
|
+
// Risk fix: JSONL 日志轮转保护,防止长期使用后文件无限增长
|
|
79
|
+
// 超过 5MB 时截断,保留最新 2000 行
|
|
80
|
+
// 注意:轮转前先调用 updatePersistedStats,确保被轮转行的数据已持久化
|
|
81
|
+
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
82
|
+
const KEEP_LINES = 2000;
|
|
83
|
+
if (existsSync(LOG_PATH) && statSync(LOG_PATH).size > MAX_SIZE) {
|
|
84
|
+
const lines = readFileSync(LOG_PATH, "utf-8").trim().split("\n");
|
|
85
|
+
writeFileSync(LOG_PATH, lines.slice(-KEEP_LINES).join("\n") + "\n");
|
|
86
|
+
}
|
|
71
87
|
appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n");
|
|
72
88
|
}
|
|
73
89
|
catch {
|
|
74
90
|
// Silently fail — logging is non-critical
|
|
75
91
|
}
|
|
92
|
+
// 无论 JSONL 写入是否成功,都尝试更新持久化聚合统计
|
|
93
|
+
// 这样即使日志被轮转,历史统计数据也永不丢失
|
|
94
|
+
updatePersistedStats(entry);
|
|
76
95
|
}
|
|
77
96
|
handleMessageRemoved(event) {
|
|
78
97
|
const mid = event.properties?.messageID ?? "";
|
|
@@ -87,6 +106,9 @@ class PerfTracker {
|
|
|
87
106
|
model,
|
|
88
107
|
providerID: entry.providerID,
|
|
89
108
|
requestCount: 0,
|
|
109
|
+
ttftCount: 0, // Bug fix: 独立维护有效样本计数
|
|
110
|
+
tpsCount: 0,
|
|
111
|
+
latencyCount: 0,
|
|
90
112
|
totalInput: 0,
|
|
91
113
|
totalOutput: 0,
|
|
92
114
|
totalCacheRead: 0,
|
|
@@ -95,12 +117,19 @@ class PerfTracker {
|
|
|
95
117
|
avgTTFT: null,
|
|
96
118
|
maxTTFT: null,
|
|
97
119
|
minTTFT: null,
|
|
120
|
+
p50TTFT: null,
|
|
121
|
+
p95TTFT: null,
|
|
122
|
+
p99TTFT: null,
|
|
98
123
|
avgTPS: null,
|
|
99
124
|
maxTPS: null,
|
|
100
125
|
minTPS: null,
|
|
101
126
|
avgLatency: null,
|
|
102
127
|
maxLatency: null,
|
|
103
128
|
minLatency: null,
|
|
129
|
+
p50Latency: null,
|
|
130
|
+
p95Latency: null,
|
|
131
|
+
p99Latency: null,
|
|
132
|
+
cacheHitRate: null,
|
|
104
133
|
};
|
|
105
134
|
this.statsMap.set(model, stats);
|
|
106
135
|
}
|
|
@@ -111,41 +140,87 @@ class PerfTracker {
|
|
|
111
140
|
stats.totalCacheWrite += entry.cacheWriteTokens;
|
|
112
141
|
stats.totalCost += entry.cost;
|
|
113
142
|
if (entry.ttft_ms !== null) {
|
|
114
|
-
|
|
143
|
+
// Bug fix: 分母使用 ttftCount(有效样本数),而非 requestCount(总请求数)
|
|
144
|
+
stats.ttftCount++;
|
|
145
|
+
const c = stats.ttftCount;
|
|
115
146
|
const prev = stats.avgTTFT;
|
|
116
147
|
stats.avgTTFT = prev !== null ? prev + (entry.ttft_ms - prev) / c : entry.ttft_ms;
|
|
117
148
|
stats.maxTTFT = stats.maxTTFT !== null ? Math.max(stats.maxTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
118
149
|
stats.minTTFT = stats.minTTFT !== null ? Math.min(stats.minTTFT, entry.ttft_ms) : entry.ttft_ms;
|
|
150
|
+
// 收集原始样本用于分位数计算
|
|
151
|
+
const ttftArr = this.ttftSamples.get(model) ?? [];
|
|
152
|
+
ttftArr.push(entry.ttft_ms);
|
|
153
|
+
this.ttftSamples.set(model, ttftArr);
|
|
119
154
|
}
|
|
120
155
|
if (entry.tps !== null) {
|
|
121
|
-
|
|
156
|
+
// Bug fix: 分母使用 tpsCount(有效样本数)
|
|
157
|
+
stats.tpsCount++;
|
|
158
|
+
const c = stats.tpsCount;
|
|
122
159
|
const prev = stats.avgTPS;
|
|
123
160
|
stats.avgTPS = prev !== null ? prev + (entry.tps - prev) / c : entry.tps;
|
|
124
161
|
stats.maxTPS = stats.maxTPS !== null ? Math.max(stats.maxTPS, entry.tps) : entry.tps;
|
|
125
162
|
stats.minTPS = stats.minTPS !== null ? Math.min(stats.minTPS, entry.tps) : entry.tps;
|
|
126
163
|
}
|
|
127
164
|
if (entry.latency_ms !== null) {
|
|
128
|
-
|
|
165
|
+
// latency 每条消息都有,但保持一致使用专用计数
|
|
166
|
+
stats.latencyCount++;
|
|
167
|
+
const c = stats.latencyCount;
|
|
129
168
|
const prev = stats.avgLatency;
|
|
130
169
|
stats.avgLatency = prev !== null ? prev + (entry.latency_ms - prev) / c : entry.latency_ms;
|
|
131
170
|
stats.maxLatency = stats.maxLatency !== null ? Math.max(stats.maxLatency, entry.latency_ms) : entry.latency_ms;
|
|
132
171
|
stats.minLatency = stats.minLatency !== null ? Math.min(stats.minLatency, entry.latency_ms) : entry.latency_ms;
|
|
172
|
+
// 收集原始样本用于分位数计算
|
|
173
|
+
const latArr = this.latencySamples.get(model) ?? [];
|
|
174
|
+
latArr.push(entry.latency_ms);
|
|
175
|
+
this.latencySamples.set(model, latArr);
|
|
133
176
|
}
|
|
134
177
|
}
|
|
178
|
+
/** 计算有序数组的指定百分位数(线性插值法) */
|
|
179
|
+
percentile(sortedArr, p) {
|
|
180
|
+
if (sortedArr.length === 0)
|
|
181
|
+
return null;
|
|
182
|
+
if (sortedArr.length === 1)
|
|
183
|
+
return sortedArr[0];
|
|
184
|
+
const idx = (p / 100) * (sortedArr.length - 1);
|
|
185
|
+
const lo = Math.floor(idx);
|
|
186
|
+
const hi = Math.ceil(idx);
|
|
187
|
+
if (lo === hi)
|
|
188
|
+
return sortedArr[lo];
|
|
189
|
+
return sortedArr[lo] + (sortedArr[hi] - sortedArr[lo]) * (idx - lo);
|
|
190
|
+
}
|
|
135
191
|
getSessionStats() {
|
|
136
192
|
let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0;
|
|
137
193
|
let totalRequests = 0, totalCost = 0;
|
|
138
|
-
|
|
194
|
+
let weightedHitSum = 0, totalReqForHit = 0;
|
|
195
|
+
for (const [model, s] of this.statsMap) {
|
|
139
196
|
totalInput += s.totalInput;
|
|
140
197
|
totalOutput += s.totalOutput;
|
|
141
198
|
totalCacheRead += s.totalCacheRead;
|
|
142
199
|
totalCacheWrite += s.totalCacheWrite;
|
|
143
200
|
totalRequests += s.requestCount;
|
|
144
201
|
totalCost += s.totalCost;
|
|
202
|
+
// 计算每个模型的分位数(需先排序)
|
|
203
|
+
const ttftArr = [...(this.ttftSamples.get(model) ?? [])].sort((a, b) => a - b);
|
|
204
|
+
s.p50TTFT = this.percentile(ttftArr, 50);
|
|
205
|
+
s.p95TTFT = this.percentile(ttftArr, 95);
|
|
206
|
+
s.p99TTFT = this.percentile(ttftArr, 99);
|
|
207
|
+
const latArr = [...(this.latencySamples.get(model) ?? [])].sort((a, b) => a - b);
|
|
208
|
+
s.p50Latency = this.percentile(latArr, 50);
|
|
209
|
+
s.p95Latency = this.percentile(latArr, 95);
|
|
210
|
+
s.p99Latency = this.percentile(latArr, 99);
|
|
211
|
+
// 模型级缓存命中率
|
|
212
|
+
const denom = s.totalInput + s.totalCacheRead;
|
|
213
|
+
s.cacheHitRate = denom > 0 ? (s.totalCacheRead / denom) * 100 : null;
|
|
214
|
+
// 累加加权命中率(按请求数加权)
|
|
215
|
+
if (s.cacheHitRate !== null) {
|
|
216
|
+
weightedHitSum += s.cacheHitRate * s.requestCount;
|
|
217
|
+
totalReqForHit += s.requestCount;
|
|
218
|
+
}
|
|
145
219
|
}
|
|
220
|
+
const weightedCacheHitRate = totalReqForHit > 0 ? weightedHitSum / totalReqForHit : null;
|
|
146
221
|
return {
|
|
147
222
|
models: Object.fromEntries(this.statsMap),
|
|
148
|
-
totals: { totalInput, totalOutput, totalCacheRead, totalCacheWrite, totalRequests, totalCost },
|
|
223
|
+
totals: { totalInput, totalOutput, totalCacheRead, totalCacheWrite, totalRequests, totalCost, weightedCacheHitRate },
|
|
149
224
|
};
|
|
150
225
|
}
|
|
151
226
|
readLogs(last = 50) {
|
|
@@ -174,6 +249,40 @@ class PerfTracker {
|
|
|
174
249
|
reset() {
|
|
175
250
|
this.firstPartTimes.clear();
|
|
176
251
|
this.statsMap.clear();
|
|
252
|
+
this.ttftSamples.clear();
|
|
253
|
+
this.latencySamples.clear();
|
|
254
|
+
}
|
|
255
|
+
loadSession(sessionID) {
|
|
256
|
+
this.firstPartTimes.clear();
|
|
257
|
+
this.statsMap.clear();
|
|
258
|
+
this.ttftSamples.clear();
|
|
259
|
+
this.latencySamples.clear();
|
|
260
|
+
if (!sessionID)
|
|
261
|
+
return;
|
|
262
|
+
try {
|
|
263
|
+
if (!existsSync(LOG_PATH))
|
|
264
|
+
return;
|
|
265
|
+
const content = readFileSync(LOG_PATH, "utf-8").trim();
|
|
266
|
+
if (!content)
|
|
267
|
+
return;
|
|
268
|
+
const lines = content.split("\n");
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
if (!line)
|
|
271
|
+
continue;
|
|
272
|
+
try {
|
|
273
|
+
const entry = JSON.parse(line);
|
|
274
|
+
if (entry.sessionID === sessionID) {
|
|
275
|
+
this.updateStats(entry.model, entry);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Skip malformed lines
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Non-critical loading failure
|
|
285
|
+
}
|
|
177
286
|
}
|
|
178
287
|
}
|
|
179
288
|
export function createPerfTracker() {
|
package/dist/queries.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DailyBreakdownItem, ModelBreakdownItem, ProviderBreakdownItem, SessionBreakdownItem, SessionTokenData, UsageFilters, UsageReport } from "./formatter.js";
|
|
1
|
+
import type { DailyBreakdownItem, ErrorStats, ModelBreakdownItem, ProviderBreakdownItem, SessionBreakdownItem, SessionTokenData, UsageFilters, UsageReport } from "./formatter.js";
|
|
2
2
|
export declare function getPresetRange(preset: "all" | "7d" | "30d" | "month"): Pick<UsageFilters, "startDate" | "endDate">;
|
|
3
3
|
export declare function getCurrentSessionStats(sessionId?: string): Promise<SessionTokenData>;
|
|
4
4
|
export declare function getSummary(filters?: UsageFilters): Promise<SessionTokenData>;
|
|
@@ -8,5 +8,7 @@ export declare function getDailyBreakdown(filters?: UsageFilters): Promise<Daily
|
|
|
8
8
|
export declare function getSessionBreakdown(filters?: UsageFilters): Promise<SessionBreakdownItem[]>;
|
|
9
9
|
export declare function getAvailableModels(): Promise<string[]>;
|
|
10
10
|
export declare function getAvailableProviders(): Promise<string[]>;
|
|
11
|
+
/** 失败请求计数 SQL。1次运行获取成功数+失败数+按模型细分 */
|
|
12
|
+
export declare function getErrorStats(filters?: UsageFilters): Promise<ErrorStats>;
|
|
11
13
|
export declare function getUsageReport(filters?: UsageFilters): Promise<UsageReport>;
|
|
12
14
|
export declare function exportReportAsCsv(report: UsageReport, section: "models" | "providers" | "daily" | "sessions"): string;
|
package/dist/queries.js
CHANGED
|
@@ -20,6 +20,10 @@ async function queryDb(sql) {
|
|
|
20
20
|
function escapeSql(value) {
|
|
21
21
|
return value.replace(/'/g, "''");
|
|
22
22
|
}
|
|
23
|
+
/** 校验日期格式必须为 YYYY-MM-DD,防止格式异常字符串进入 SQL */
|
|
24
|
+
function isValidDate(s) {
|
|
25
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(s);
|
|
26
|
+
}
|
|
23
27
|
function messageWhere(filters) {
|
|
24
28
|
const where = [
|
|
25
29
|
"json_extract(m.data, '$.role') = 'assistant'",
|
|
@@ -31,10 +35,13 @@ function messageWhere(filters) {
|
|
|
31
35
|
where.push(`coalesce(json_extract(m.data, '$.providerID'), '') = '${escapeSql(filters.provider)}'`);
|
|
32
36
|
if (filters.model)
|
|
33
37
|
where.push(`coalesce(json_extract(m.data, '$.modelID'), '') = '${escapeSql(filters.model)}'`);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// Risk fix: 日期参数先验证格式(YYYY-MM-DD),格式不符则忽略该过滤条件
|
|
39
|
+
if (filters.startDate && isValidDate(filters.startDate)) {
|
|
40
|
+
where.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') >= '${filters.startDate}'`);
|
|
41
|
+
}
|
|
42
|
+
if (filters.endDate && isValidDate(filters.endDate)) {
|
|
43
|
+
where.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') <= '${filters.endDate}'`);
|
|
44
|
+
}
|
|
38
45
|
return where.join(" AND ");
|
|
39
46
|
}
|
|
40
47
|
function parseList(value) {
|
|
@@ -254,15 +261,67 @@ ORDER BY value ASC
|
|
|
254
261
|
const rows = await queryDb(sql);
|
|
255
262
|
return rows.map((row) => row.value ?? "unknown");
|
|
256
263
|
}
|
|
264
|
+
/** 失败请求计数 SQL。1次运行获取成功数+失败数+按模型细分 */
|
|
265
|
+
export async function getErrorStats(filters = {}) {
|
|
266
|
+
// 构建日期/Session/Provider/Model 过滤条件(不包含 tokens.total > 0 过滤)
|
|
267
|
+
const baseConds = [
|
|
268
|
+
"json_extract(m.data, '$.role') = 'assistant'",
|
|
269
|
+
];
|
|
270
|
+
if (filters.sessionId)
|
|
271
|
+
baseConds.push(`m.session_id = '${escapeSql(filters.sessionId)}'`);
|
|
272
|
+
if (filters.provider)
|
|
273
|
+
baseConds.push(`coalesce(json_extract(m.data, '$.providerID'), '') = '${escapeSql(filters.provider)}'`);
|
|
274
|
+
if (filters.model)
|
|
275
|
+
baseConds.push(`coalesce(json_extract(m.data, '$.modelID'), '') = '${escapeSql(filters.model)}'`);
|
|
276
|
+
if (filters.startDate && isValidDate(filters.startDate)) {
|
|
277
|
+
baseConds.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') >= '${filters.startDate}'`);
|
|
278
|
+
}
|
|
279
|
+
if (filters.endDate && isValidDate(filters.endDate)) {
|
|
280
|
+
baseConds.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') <= '${filters.endDate}'`);
|
|
281
|
+
}
|
|
282
|
+
const baseWhere = baseConds.join(" AND ");
|
|
283
|
+
// 按模型细化:同时统计成功和失败请求
|
|
284
|
+
const sql = `
|
|
285
|
+
SELECT
|
|
286
|
+
coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
|
|
287
|
+
coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
|
|
288
|
+
count(*) as total,
|
|
289
|
+
sum(CASE WHEN coalesce(json_extract(m.data, '$.tokens.total'), 0) = 0 THEN 1 ELSE 0 END) as failed
|
|
290
|
+
FROM message m
|
|
291
|
+
WHERE ${baseWhere}
|
|
292
|
+
GROUP BY provider, model
|
|
293
|
+
ORDER BY failed DESC
|
|
294
|
+
`.trim();
|
|
295
|
+
try {
|
|
296
|
+
const rows = await queryDb(sql);
|
|
297
|
+
let successCount = 0, failedCount = 0;
|
|
298
|
+
const byModel = rows.map(r => {
|
|
299
|
+
const total = r.total ?? 0;
|
|
300
|
+
const failed = r.failed ?? 0;
|
|
301
|
+
const success = total - failed;
|
|
302
|
+
successCount += success;
|
|
303
|
+
failedCount += failed;
|
|
304
|
+
return { provider: r.provider ?? 'unknown', model: r.model ?? 'unknown', failed, total };
|
|
305
|
+
});
|
|
306
|
+
const errorRate = (successCount + failedCount) > 0
|
|
307
|
+
? failedCount / (successCount + failedCount)
|
|
308
|
+
: 0;
|
|
309
|
+
return { successCount, failedCount, errorRate, byModel };
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return { successCount: 0, failedCount: 0, errorRate: 0, byModel: [] };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
257
315
|
export async function getUsageReport(filters = {}) {
|
|
258
|
-
const [summary, models, providers, daily, sessions] = await Promise.all([
|
|
316
|
+
const [summary, models, providers, daily, sessions, errors] = await Promise.all([
|
|
259
317
|
getSummary(filters),
|
|
260
318
|
getModelBreakdown(filters),
|
|
261
319
|
getProviderBreakdown(filters),
|
|
262
320
|
getDailyBreakdown(filters),
|
|
263
321
|
getSessionBreakdown(filters),
|
|
322
|
+
getErrorStats(filters),
|
|
264
323
|
]);
|
|
265
|
-
return { filters, summary, models, providers, daily, sessions };
|
|
324
|
+
return { filters, summary, models, providers, daily, sessions, errors };
|
|
266
325
|
}
|
|
267
326
|
function csvEscape(value) {
|
|
268
327
|
const text = String(value);
|
package/dist/sidebar.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ interface TokenWatchPanelProps {
|
|
|
15
15
|
api: TuiPluginApi;
|
|
16
16
|
theme: TuiTheme;
|
|
17
17
|
perfTracker: PerfTracker;
|
|
18
|
-
messages: readonly any[];
|
|
19
|
-
allTokenMessages: TokenMessage[];
|
|
18
|
+
messages: () => readonly any[];
|
|
19
|
+
allTokenMessages: () => TokenMessage[];
|
|
20
20
|
}
|
|
21
21
|
export declare function TokenWatchPanel(props: TokenWatchPanelProps): any;
|
|
22
22
|
export {};
|