opencode-visual-cache 1.5.0 → 1.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.md +10 -4
- package/README_EN.md +10 -4
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/i18n.d.ts +102 -0
- package/dist/i18n.js +368 -0
- package/dist/index.js +420 -254
- package/dist/tui.js +1086 -411
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/i18n.ts +380 -0
- package/src/index.tsx +499 -304
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/soli
|
|
|
2
2
|
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, untrack } from "solid-js";
|
|
3
3
|
import { PLUGIN_VERSION } from "./_version";
|
|
4
4
|
import { balanceProviders, getBalanceProvider, maskKey, matchBalanceProvider } from "./balance-providers";
|
|
5
|
+
import { LANG_META, createT, detectLang } from "./i18n";
|
|
5
6
|
// ── terminal-width helpers ────────────────────────────────────────
|
|
6
7
|
// CJK characters occupy 2 terminal columns; padEnd/padStart count
|
|
7
8
|
// string length (=1 per char), which breaks alignment with mixed text.
|
|
@@ -52,97 +53,13 @@ function truncateVisual(s, maxCols) {
|
|
|
52
53
|
}
|
|
53
54
|
return result;
|
|
54
55
|
}
|
|
55
|
-
// ── language override (env: CACHE_TUI_LANG) ──
|
|
56
|
-
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined;
|
|
57
56
|
// ── language ──────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
})();
|
|
68
|
-
const ZH_T = {
|
|
69
|
-
title: "缓存统计",
|
|
70
|
-
hit: "命中率",
|
|
71
|
-
totalHit: "总命中:",
|
|
72
|
-
read: "缓存读:",
|
|
73
|
-
write: "缓存写:",
|
|
74
|
-
miss: "未命中:",
|
|
75
|
-
out: "输出:",
|
|
76
|
-
cost: "费用:",
|
|
77
|
-
saved: "累计节省:",
|
|
78
|
-
model: "模型:",
|
|
79
|
-
provider: "提供商:",
|
|
80
|
-
rate: "单价:",
|
|
81
|
-
hitFolded: "命中",
|
|
82
|
-
inputRate: "输入",
|
|
83
|
-
cacheRate: "缓存",
|
|
84
|
-
writeRate: "写入",
|
|
85
|
-
noData: "等待缓存数据...",
|
|
86
|
-
tok: "tok",
|
|
87
|
-
distTitle: "估算 Token 分布",
|
|
88
|
-
distSys: "系统提示:",
|
|
89
|
-
distUser: "用户:",
|
|
90
|
-
distAgent: "Agent 指令:",
|
|
91
|
-
distTool: "Tool 调用:",
|
|
92
|
-
distRes: "Tool 结果:",
|
|
93
|
-
distTotal: "总计:",
|
|
94
|
-
distOut: "输出:",
|
|
95
|
-
secDetail: "明细",
|
|
96
|
-
secModel: "模型",
|
|
97
|
-
secSkills: "已加载技能",
|
|
98
|
-
balTotal: "总余额:",
|
|
99
|
-
balNoKey: "未配置 {p} API Key",
|
|
100
|
-
balLoading: "查询中...",
|
|
101
|
-
balError: "查询失败",
|
|
102
|
-
balErr401: "API Key 无效",
|
|
103
|
-
balErr403: "余额查询被拒绝",
|
|
104
|
-
balErrEmpty: "未获取到余额数据",
|
|
105
|
-
balErrTimeout: "查询超时",
|
|
106
|
-
};
|
|
107
|
-
const EN_T = {
|
|
108
|
-
title: "Token Cache",
|
|
109
|
-
hit: "Hit",
|
|
110
|
-
totalHit: "Total Hit:",
|
|
111
|
-
read: "Read:",
|
|
112
|
-
write: "Write:",
|
|
113
|
-
miss: "Miss:",
|
|
114
|
-
out: "Out:",
|
|
115
|
-
cost: "Cost:",
|
|
116
|
-
saved: "Total Saved:",
|
|
117
|
-
model: "Model:",
|
|
118
|
-
provider: "Provider:",
|
|
119
|
-
rate: "Rate:",
|
|
120
|
-
hitFolded: "hit",
|
|
121
|
-
inputRate: "in",
|
|
122
|
-
cacheRate: "cache",
|
|
123
|
-
writeRate: "write",
|
|
124
|
-
noData: "Waiting for cache data...",
|
|
125
|
-
tok: "tok",
|
|
126
|
-
distTitle: "Estimated Token Dist.",
|
|
127
|
-
distSys: "System:",
|
|
128
|
-
distUser: "User:",
|
|
129
|
-
distAgent: "Agent Instr:",
|
|
130
|
-
distTool: "Tool Call:",
|
|
131
|
-
distRes: "Tool Result:",
|
|
132
|
-
distTotal: "Total:",
|
|
133
|
-
distOut: "Output:",
|
|
134
|
-
secDetail: "Detail",
|
|
135
|
-
secModel: "Model",
|
|
136
|
-
secSkills: "Loaded Skills",
|
|
137
|
-
balTotal: "Total:",
|
|
138
|
-
balNoKey: "{p} API Key not set",
|
|
139
|
-
balLoading: "Fetching...",
|
|
140
|
-
balError: "Fetch failed",
|
|
141
|
-
balErr401: "Invalid API Key",
|
|
142
|
-
balErr403: "Balance request rejected",
|
|
143
|
-
balErrEmpty: "No balance data",
|
|
144
|
-
balErrTimeout: "Request timed out",
|
|
145
|
-
};
|
|
57
|
+
// 语言初始化:环境变量 CACHE_TUI_LANG 覆盖 → 否则按系统 locale 自动检测。
|
|
58
|
+
// 用户通过 /cache-lang 设置的偏好会在 KV 就绪后优先覆盖(见 tui() 内恢复逻辑)。
|
|
59
|
+
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined;
|
|
60
|
+
const INIT_LANG = DEBUG_LANG !== undefined && LANG_META.some((m) => m.code === DEBUG_LANG)
|
|
61
|
+
? DEBUG_LANG
|
|
62
|
+
: detectLang();
|
|
146
63
|
// ── color helpers ────────────────────────────────────────────────
|
|
147
64
|
/** Extract { r, g, b } (0–255) from a hex string or RGBA-like object. */
|
|
148
65
|
function rgb(raw) {
|
|
@@ -201,7 +118,7 @@ function desaturateTo(raw, maxSat, fallback) {
|
|
|
201
118
|
* converges to within a fraction of an 8‑bit step, eliminating
|
|
202
119
|
* colour banding in edge cases.
|
|
203
120
|
*/
|
|
204
|
-
//
|
|
121
|
+
// Bt.601 luma (perceptual brightness used as the grey anchor)
|
|
205
122
|
const luma = c.r * 0.299 + c.g * 0.587 + c.b * 0.114;
|
|
206
123
|
let lo = 0, hi = 1;
|
|
207
124
|
for (let i = 0; i < 12; i++) {
|
|
@@ -356,6 +273,41 @@ function balanceSymbol(currency) {
|
|
|
356
273
|
const sym = CURRENCIES[currency];
|
|
357
274
|
return sym ?? currency + " ";
|
|
358
275
|
}
|
|
276
|
+
/** 紧凑数字缩写(底部状态栏用):1234 → "1.2K",1234567 → "1.2M"。 */
|
|
277
|
+
function fmtCompact(n) {
|
|
278
|
+
if (n >= 1e6)
|
|
279
|
+
return (n / 1e6).toFixed(1) + "M";
|
|
280
|
+
if (n >= 1e3)
|
|
281
|
+
return (n / 1e3).toFixed(1) + "K";
|
|
282
|
+
return String(Math.round(n));
|
|
283
|
+
}
|
|
284
|
+
/** 余额数值格式化:≥1 或 0 显示固定 2 位小数;小额(<1)保留精度(最多 6 位),避免抹成 0.00。 */
|
|
285
|
+
function formatBalanceAmount(total) {
|
|
286
|
+
const n = parseFloat(total);
|
|
287
|
+
if (!Number.isFinite(n))
|
|
288
|
+
return total;
|
|
289
|
+
if (n === 0 || n >= 1)
|
|
290
|
+
return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
291
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 });
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 将余额列表格式化为单行文本。
|
|
295
|
+
* 优先直接显示偏好币种(CNY/USD…);偏好币种为换算币种时按汇率折算第一条余额。
|
|
296
|
+
*/
|
|
297
|
+
function formatBalanceText(list, pref, rate) {
|
|
298
|
+
const native = pref ? list.find((x) => x.currency === pref) : undefined;
|
|
299
|
+
if (native)
|
|
300
|
+
return balanceSymbol(native.currency) + formatBalanceAmount(native.total);
|
|
301
|
+
const base = list[0];
|
|
302
|
+
const baseAmt = parseFloat(base.total);
|
|
303
|
+
const converted = Number.isFinite(baseAmt)
|
|
304
|
+
? convertBalance(pref || base.currency, rate, baseAmt, base.currency)
|
|
305
|
+
: baseAmt;
|
|
306
|
+
const shown = pref && base.currency !== pref
|
|
307
|
+
? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
|
|
308
|
+
: formatBalanceAmount(base.total);
|
|
309
|
+
return balanceSymbol(pref || base.currency) + shown;
|
|
310
|
+
}
|
|
359
311
|
const CURRENCIES = {
|
|
360
312
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
361
313
|
};
|
|
@@ -382,9 +334,9 @@ function TokenCachePanel(props) {
|
|
|
382
334
|
const [skillsOpen, setSkillsOpen] = createSignal(true);
|
|
383
335
|
let boxEl;
|
|
384
336
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
385
|
-
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate,
|
|
386
|
-
// ── reactive translation (follows
|
|
387
|
-
const t =
|
|
337
|
+
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langCode, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, sectionSkills, setSectionSkills, sectionBalance, setSectionBalance, balanceRefresh, balanceProviderId, setBalanceProviderId, autoBalance, setAutoBalance, balanceUnsupported, setBalanceUnsupported, balanceState, balanceCurrency, setBalanceCurrency, borderVisible, setBorderVisible, } = props.signals;
|
|
338
|
+
// ── reactive translation (follows langCode signal) ──
|
|
339
|
+
const t = createT(() => langCode());
|
|
388
340
|
// ── scan session messages reactively ──
|
|
389
341
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
390
342
|
// api.state.session state changes — no event listener needed.
|
|
@@ -409,57 +361,8 @@ function TokenCachePanel(props) {
|
|
|
409
361
|
hasSkills: false,
|
|
410
362
|
});
|
|
411
363
|
const [refreshTick, setRefreshTick] = createSignal(0);
|
|
412
|
-
//
|
|
413
|
-
const [balanceState, setBalanceState] = createSignal({
|
|
414
|
-
status: "idle", data: null, lastFetch: 0,
|
|
415
|
-
});
|
|
416
|
-
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
417
|
-
let balanceSeq = 0;
|
|
418
|
-
// 当前 provider 显示名
|
|
364
|
+
// 当前 provider 显示名(余额查询状态为共享信号,见 PanelSignals.balanceState)
|
|
419
365
|
const providerName = createMemo(() => getBalanceProvider(balanceProviderId()).name);
|
|
420
|
-
const pollBalance = async () => {
|
|
421
|
-
const provider = getBalanceProvider(balanceProviderId());
|
|
422
|
-
// 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
|
|
423
|
-
const key = props.api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
424
|
-
|| findOpencodeKey(props.api, provider);
|
|
425
|
-
if (!key) {
|
|
426
|
-
setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
const now = Date.now();
|
|
430
|
-
const prev = balanceState();
|
|
431
|
-
// key 已更换(重新输入)→ 强制重新查询,绕过缓存
|
|
432
|
-
if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS)
|
|
433
|
-
return; // cache still fresh
|
|
434
|
-
const seq = ++balanceSeq;
|
|
435
|
-
setBalanceState({ ...prev, status: "loading", error: undefined, key });
|
|
436
|
-
const controller = new AbortController();
|
|
437
|
-
let timedOut = false;
|
|
438
|
-
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, 10_000);
|
|
439
|
-
try {
|
|
440
|
-
const data = await provider.fetchBalance(key, controller.signal);
|
|
441
|
-
clearTimeout(timer);
|
|
442
|
-
if (seq !== balanceSeq)
|
|
443
|
-
return; // 已被更新的请求取代,丢弃过期结果
|
|
444
|
-
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key });
|
|
445
|
-
}
|
|
446
|
-
catch (err) {
|
|
447
|
-
clearTimeout(timer);
|
|
448
|
-
if (seq !== balanceSeq)
|
|
449
|
-
return;
|
|
450
|
-
const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "");
|
|
451
|
-
// 失败时清空旧数据,避免显示过期余额
|
|
452
|
-
setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key });
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
456
|
-
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
457
|
-
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
458
|
-
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
459
|
-
createEffect(() => {
|
|
460
|
-
void balanceRefresh();
|
|
461
|
-
untrack(() => { void pollBalance(); });
|
|
462
|
-
});
|
|
463
366
|
// 自动切换当前会话的 provider(前缀匹配)。手动切换会关闭此行为。
|
|
464
367
|
// 直接追踪 messages 取最后一条 assistant 消息的 providerID——
|
|
465
368
|
// 不依赖 session.model 的响应式更新(模型切换时该链路可能不触发重算)。
|
|
@@ -488,9 +391,16 @@ function TokenCachePanel(props) {
|
|
|
488
391
|
if (!pid)
|
|
489
392
|
return;
|
|
490
393
|
const hit = matchBalanceProvider(pid);
|
|
491
|
-
if (hit
|
|
492
|
-
|
|
493
|
-
|
|
394
|
+
if (hit) {
|
|
395
|
+
setBalanceUnsupported(false);
|
|
396
|
+
if (hit.id !== balanceProviderId()) {
|
|
397
|
+
setBalanceProviderId(hit.id);
|
|
398
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// 当前提供商没有余额适配器 → 标记不支持,余额显示 N/A 并停止轮询
|
|
403
|
+
setBalanceUnsupported(true);
|
|
494
404
|
}
|
|
495
405
|
});
|
|
496
406
|
// ── auto-clear override when the user navigates to a different main session ──
|
|
@@ -530,19 +440,19 @@ function TokenCachePanel(props) {
|
|
|
530
440
|
for (const msg of msgs) {
|
|
531
441
|
if (msg.role !== "assistant")
|
|
532
442
|
continue;
|
|
533
|
-
const
|
|
534
|
-
if (!
|
|
443
|
+
const tok = msg.tokens;
|
|
444
|
+
if (!tok)
|
|
535
445
|
continue;
|
|
536
|
-
const mit = num(
|
|
446
|
+
const mit = num(tok.input) + num(tok.cache?.read) + num(tok.cache?.write), mrt = num(tok.cache?.read);
|
|
537
447
|
if (mit > 0) {
|
|
538
448
|
prevMsgHitRate = lastMsgHitRate;
|
|
539
449
|
lastMsgHitRate = (mrt / mit) * 100;
|
|
540
450
|
}
|
|
541
451
|
if (fallbackTokens) {
|
|
542
|
-
input += num(
|
|
543
|
-
read += num(
|
|
544
|
-
write += num(
|
|
545
|
-
output += num(
|
|
452
|
+
input += num(tok.input);
|
|
453
|
+
read += num(tok.cache?.read);
|
|
454
|
+
write += num(tok.cache?.write);
|
|
455
|
+
output += num(tok.output);
|
|
546
456
|
}
|
|
547
457
|
if (fallbackCost) {
|
|
548
458
|
cost += num(msg.cost);
|
|
@@ -568,7 +478,8 @@ function TokenCachePanel(props) {
|
|
|
568
478
|
break;
|
|
569
479
|
}
|
|
570
480
|
const hitRate = lastMsgHitRate >= 0 ? lastMsgHitRate : 0;
|
|
571
|
-
|
|
481
|
+
// 总命中率分母含缓存写(业界口径:read / (input+read+write))
|
|
482
|
+
const freshTotal = input + read + write, sessionHitRate = freshTotal > 0 ? (read / freshTotal) * 100 : 0;
|
|
572
483
|
const model = mid.split("/").pop() ?? mid, hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0;
|
|
573
484
|
const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0;
|
|
574
485
|
const trend = hasTrendData ? lastMsgHitRate - prevMsgHitRate : 0, providerName = pid || "";
|
|
@@ -667,14 +578,14 @@ function TokenCachePanel(props) {
|
|
|
667
578
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
668
579
|
if (msgs[i].role !== "assistant")
|
|
669
580
|
continue;
|
|
670
|
-
const
|
|
671
|
-
if (
|
|
581
|
+
const tok = msgs[i].tokens;
|
|
582
|
+
if (tok && ((tok.input ?? 0) > 0 || (tok.cache?.read ?? 0) > 0 || (tok.cache?.write ?? 0) > 0)) {
|
|
672
583
|
lastAssMsg = msgs[i];
|
|
673
584
|
break;
|
|
674
585
|
}
|
|
675
586
|
}
|
|
676
|
-
//
|
|
677
|
-
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read);
|
|
587
|
+
// 取最后一条有数据消息的总输入(含缓存读/写)作为当前 context 大小
|
|
588
|
+
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read) + num(lastAssMsg?.tokens?.cache?.write);
|
|
678
589
|
dist.apiOutput = num(lastAssMsg?.tokens?.output);
|
|
679
590
|
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0;
|
|
680
591
|
}
|
|
@@ -750,6 +661,7 @@ function TokenCachePanel(props) {
|
|
|
750
661
|
const savedProvider = props.api.kv.get(`${KV_PREFIX}.balance.provider`);
|
|
751
662
|
if (typeof savedProvider === "string" && balanceProviders.some((p) => p.id === savedProvider)) {
|
|
752
663
|
setBalanceProviderId(savedProvider);
|
|
664
|
+
setBalanceUnsupported(false);
|
|
753
665
|
}
|
|
754
666
|
// Restore auto-switch (default on)
|
|
755
667
|
const savedAuto = props.api.kv.get(`${KV_PREFIX}.balance.auto`);
|
|
@@ -772,11 +684,6 @@ function TokenCachePanel(props) {
|
|
|
772
684
|
setSectionBalance(Boolean(props.api.kv.get(`${KV_PREFIX}.section.balance`, true)));
|
|
773
685
|
const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
|
|
774
686
|
setBorderVisible(bv !== false);
|
|
775
|
-
// Restore language preference
|
|
776
|
-
const savedLang = props.api.kv.get(`${KV_PREFIX}.lang`);
|
|
777
|
-
if (savedLang === "zh" || savedLang === "en") {
|
|
778
|
-
setLangZH(savedLang === "zh");
|
|
779
|
-
}
|
|
780
687
|
// Restore distribution snapshot so the token distribution block
|
|
781
688
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
782
689
|
const cachedDist = props.api.kv.get(`${KV_PREFIX}.dist_snapshot`);
|
|
@@ -827,8 +734,7 @@ function TokenCachePanel(props) {
|
|
|
827
734
|
const unsubMsg = props.api.event.on("message.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1); });
|
|
828
735
|
const unsubSession = props.api.event.on("session.updated", () => { setRefreshTick(v => v + 1); });
|
|
829
736
|
setRefreshTick(v => v + 1);
|
|
830
|
-
|
|
831
|
-
onCleanup(() => { clearTimeout(partTimer); clearInterval(balanceTimer); unsubPart(); unsubMsg(); unsubSession(); });
|
|
737
|
+
onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg(); unsubSession(); });
|
|
832
738
|
});
|
|
833
739
|
// ── colours ──
|
|
834
740
|
// Pull from the current theme, auto-desaturate if too punchy,
|
|
@@ -858,11 +764,14 @@ function TokenCachePanel(props) {
|
|
|
858
764
|
const gutter = createMemo(() => borderVisible() ? 6 : 0);
|
|
859
765
|
const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - gutter())));
|
|
860
766
|
function trendLabel(t) {
|
|
861
|
-
|
|
767
|
+
// |t| < 0.05 视为无变化:避免显示 "↑0.0%" 的矛盾(箭头存在但数值截断为零)
|
|
768
|
+
if (Math.abs(t) < 0.05)
|
|
769
|
+
return "-";
|
|
770
|
+
return (t > 0 ? "\u2191" : "\u2193") + Math.abs(t).toFixed(1) + "%";
|
|
862
771
|
}
|
|
863
772
|
const barW = createMemo(() => {
|
|
864
773
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0;
|
|
865
|
-
const overhead = visualWidth(t(
|
|
774
|
+
const overhead = visualWidth(t("hit")) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter();
|
|
866
775
|
return Math.max(3, panelWidth() - overhead);
|
|
867
776
|
});
|
|
868
777
|
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
@@ -888,49 +797,184 @@ function TokenCachePanel(props) {
|
|
|
888
797
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
889
798
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
890
799
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
891
|
-
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: t()
|
|
892
|
-
const prefix = " \u21b3 " + (
|
|
800
|
+
}, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: t("title") }), _jsx(Show, { when: open(), children: _jsxs("span", { style: { fg: dimColor(pal().muted, 0.75) }, children: [" v", PLUGIN_VERSION] }) })] }), _jsxs(Show, { when: !open() && data().hasData, children: [_jsxs(Show, { when: data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t("title")) - visualWidth(pct() + " " + t("hitFolded") + " " + trendLabel(data().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", t("hitFolded")] }), _jsxs("span", { style: { fg: Math.abs(data().trend) >= 0.05 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] })] }), _jsxs(Show, { when: !data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t("title")) - visualWidth(pct() + " " + t("hitFolded")))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", t("hitFolded")] })] })] })] }), _jsxs(Show, { when: open(), children: [_jsx(Show, { when: props.signals.overrideSessionId(), children: (() => {
|
|
801
|
+
const prefix = " \u21b3 " + t("subPrefix");
|
|
893
802
|
const maxSidW = Math.max(6, panelWidth() - visualWidth(prefix));
|
|
894
803
|
return (_jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: prefix }), _jsx("span", { style: { fg: pal().text }, children: truncateVisual(props.signals.overrideSessionId(), maxSidW) })] }));
|
|
895
|
-
})() }), _jsxs(Show, { when: data().hasData, fallback: _jsxs(_Fragment, { children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { style: { fg: pal().muted }, children: t()
|
|
896
|
-
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t(
|
|
804
|
+
})() }), _jsxs(Show, { when: data().hasData, fallback: _jsxs(_Fragment, { children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { style: { fg: pal().muted }, children: t("noData") })] })] }), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsxs("text", { children: [_jsxs("span", { style: { fg: pal().text }, children: [t("hit"), " "] }), _jsxs("span", { style: { fg: hitColor() }, children: ["[", bar(), "] "] }), _jsx("span", { style: { fg: pal().text }, children: pct() }), _jsx(Show, { when: data().hasTrendData, children: _jsxs("span", { style: { fg: Math.abs(data().trend) >= 0.05 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] }) })] }), _jsx("text", { fg: pal().muted, children: justify(t("totalHit"), (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%") }), _jsxs(Show, { when: sectionDetail(), children: [_jsxs("text", { onMouseUp: () => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: detailOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t("secDetail") }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + t("secDetail"))) })] }), _jsxs(Show, { when: detailOpen(), children: [_jsx(Show, { when: data().read > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("read"), fmt(data().read), t("tok")) }) }), _jsx(Show, { when: data().write > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("write"), fmt(data().write), t("tok")) }) }), _jsx("text", { fg: pal().muted, children: justify(t("miss"), fmt(data().freshInput + data().write), t("tok")) }), _jsx("text", { fg: pal().muted, children: justify(t("out"), fmt(data().output), t("tok")) }), _jsx(Show, { when: data().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: t("saved") }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(t("saved")) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate())))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved, currencySymbol(), exchangeRate())] })] }) })] })] }), _jsxs(Show, { when: sectionModel(), children: [_jsxs("text", { onMouseUp: () => setModelOpen((o) => { const n = !o; persistFold("model", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: modelOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t("secModel") }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t("secModel"))) })] }), _jsxs(Show, { when: modelOpen(), children: [_jsx("text", { fg: pal().text, children: justify(t("cost"), fmtCost(data().cost, currencySymbol(), exchangeRate())) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(t("provider"), data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(t("model"), data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(t("rate"), currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t("inputRate")) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t("cacheRate")) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t("writeRate")) }) })] })] })] }), _jsx(Show, { when: sectionDist(), children: _jsxs(Show, { when: data().hasDistData, children: [_jsxs("text", { onMouseUp: () => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: distOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t("distTitle") }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t("distTitle"))) })] }), _jsxs(Show, { when: distOpen(), children: [_jsx(Show, { when: data().dist.system > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distSys"), fmt(data().dist.system), t("tok")) }) }), _jsx(Show, { when: data().dist.user > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distUser"), fmt(data().dist.user), t("tok")) }) }), _jsx(Show, { when: data().dist.agent > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distAgent"), fmt(data().dist.agent), t("tok")) }) }), _jsx(Show, { when: data().dist.toolCall > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distTool"), fmt(data().dist.toolCall), t("tok")) }) }), _jsx(Show, { when: data().dist.toolResult > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distRes"), fmt(data().dist.toolResult), t("tok")) }) }), _jsx("text", { fg: pal().text, children: justify(t("distTotal"), fmt(data().dist.apiInput), t("tok")) })] })] }) }), _jsx(Show, { when: sectionSkills(), children: _jsxs(Show, { when: data().hasSkills, children: [_jsxs("text", { onMouseUp: () => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: skillsOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t("secSkills") }) }), _jsxs("span", { style: { fg: pal().muted }, children: [" (", data().skills.length, ")"] }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t("secSkills") + ` (${data().skills.length})`)) })] }), _jsx(Show, { when: skillsOpen(), children: data().skills.map((sk) => {
|
|
805
|
+
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t("tok"));
|
|
897
806
|
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
|
|
898
807
|
const label = truncateVisual(sk.name, maxLabel);
|
|
899
|
-
return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t(
|
|
900
|
-
}) })] }) }), _jsxs(Show, { when: sectionBalance(), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsx(Show, { when: balanceState().status === "idle", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t(
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const list = balanceState().data;
|
|
913
|
-
const pref = balanceCurrency();
|
|
914
|
-
// 偏好币种是 DeepSeek 原生返回的(CNY/USD)→ 直接显示
|
|
915
|
-
const native = pref ? list.find(x => x.currency === pref) : undefined;
|
|
916
|
-
if (native) {
|
|
917
|
-
return (_jsx("text", { fg: pal().text, children: justify(t().balTotal, balanceSymbol(native.currency) + native.total) }));
|
|
918
|
-
}
|
|
919
|
-
// 非原生币种(EUR/JPY/GBP/KRW…)→ 取第一条余额按汇率换算
|
|
920
|
-
const base = list[0];
|
|
921
|
-
const baseAmt = parseFloat(base.total);
|
|
922
|
-
const converted = Number.isFinite(baseAmt)
|
|
923
|
-
? convertBalance(pref || base.currency, exchangeRate(), baseAmt, base.currency)
|
|
924
|
-
: baseAmt;
|
|
925
|
-
const shown = pref && base.currency !== pref
|
|
926
|
-
? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
|
|
927
|
-
: base.total;
|
|
928
|
-
return (_jsx("text", { fg: pal().text, children: justify(t().balTotal, balanceSymbol(pref || base.currency) + shown) }));
|
|
929
|
-
})() })] })] })] })] }));
|
|
808
|
+
return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t("tok")) }));
|
|
809
|
+
}) })] }) }), _jsxs(Show, { when: sectionBalance(), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsx(Show, { when: balanceUnsupported(), children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balUnsupported") })] }) }), _jsxs(Show, { when: !balanceUnsupported(), children: [_jsx(Show, { when: balanceState().status === "idle", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balNoKey", { p: providerName() }) })] }) }), _jsx(Show, { when: balanceState().status === "loading", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balLoading") })] }) }), _jsx(Show, { when: balanceState().status === "error", children: _jsxs("text", { fg: pal().error, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: (() => {
|
|
810
|
+
const code = balanceState().error;
|
|
811
|
+
if (code === "401")
|
|
812
|
+
return t("balErr401");
|
|
813
|
+
if (code === "403")
|
|
814
|
+
return t("balErr403");
|
|
815
|
+
if (code === "EMPTY")
|
|
816
|
+
return t("balErrEmpty");
|
|
817
|
+
if (code === "TIMEOUT")
|
|
818
|
+
return t("balErrTimeout");
|
|
819
|
+
return t("balError") + (code ? ` (${code})` : "");
|
|
820
|
+
})() })] }) }), _jsx(Show, { when: balanceState().status === "ok" && balanceState().data, children: _jsx("text", { fg: pal().text, children: justify(t("balTotal"), formatBalanceText(balanceState().data, balanceCurrency(), exchangeRate())) }) })] })] })] })] })] }));
|
|
930
821
|
}
|
|
931
822
|
// ---------------------------------------------------------------------------
|
|
932
823
|
// Plugin entry
|
|
933
824
|
// ---------------------------------------------------------------------------
|
|
825
|
+
/**
|
|
826
|
+
* 输入框 hint 行(session_prompt slot 的 hint):单行显示 路径 · 命中率 · 余额 · Tokens。
|
|
827
|
+
* 通过 ui.Prompt 的 hint prop 注入——宿主右侧的 token/commands 提示自动保留,
|
|
828
|
+
* 三合一信息与路径同行显示在中间位置。
|
|
829
|
+
*/
|
|
830
|
+
function BottomStatusBar(props) {
|
|
831
|
+
const KV_PREFIX = "cache_panel";
|
|
832
|
+
const t = createT(() => props.signals.langCode());
|
|
833
|
+
const sid = props.sessionId;
|
|
834
|
+
// ── 命中率(单条口径:最后一条有 token 的 assistant 消息)+ token 汇总 ──
|
|
835
|
+
const stats = createMemo(() => {
|
|
836
|
+
const id = sid;
|
|
837
|
+
if (!id)
|
|
838
|
+
return null;
|
|
839
|
+
const msgs = props.api.state.session.messages(id);
|
|
840
|
+
const session = typeof props.api.state.session.get === "function"
|
|
841
|
+
? props.api.state.session.get(id)
|
|
842
|
+
: undefined;
|
|
843
|
+
let input = session?.tokens?.input ?? 0;
|
|
844
|
+
let read = session?.tokens?.cache?.read ?? 0;
|
|
845
|
+
let write = session?.tokens?.cache?.write ?? 0;
|
|
846
|
+
// 旧 SDK 无 session 聚合字段 → 遍历消息累加(与侧边栏 fallback 一致)
|
|
847
|
+
if (session?.tokens == null) {
|
|
848
|
+
for (const m of msgs) {
|
|
849
|
+
if (m.role !== "assistant")
|
|
850
|
+
continue;
|
|
851
|
+
const tk = m.tokens;
|
|
852
|
+
if (!tk)
|
|
853
|
+
continue;
|
|
854
|
+
input += num(tk.input);
|
|
855
|
+
read += num(tk.cache?.read);
|
|
856
|
+
write += num(tk.cache?.write);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// 从后往前取最后两条有 token 数据的 assistant 消息 → 单条命中率 + 趋势
|
|
860
|
+
// 分母含缓存写(业界口径:read / (input+read+write))
|
|
861
|
+
let hitRate = -1, prevHitRate = -1;
|
|
862
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
863
|
+
const m = msgs[i];
|
|
864
|
+
if (m.role !== "assistant")
|
|
865
|
+
continue;
|
|
866
|
+
const tk = m.tokens;
|
|
867
|
+
if (!tk)
|
|
868
|
+
continue;
|
|
869
|
+
const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write);
|
|
870
|
+
const mrt = num(tk.cache?.read);
|
|
871
|
+
if (mit <= 0)
|
|
872
|
+
continue;
|
|
873
|
+
const rate = (mrt / mit) * 100;
|
|
874
|
+
if (hitRate < 0) {
|
|
875
|
+
hitRate = rate;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
prevHitRate = rate;
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
return { hitRate, prevHitRate, input, read, write };
|
|
882
|
+
});
|
|
883
|
+
// 余额查询状态为共享信号(PanelSignals.balanceState),由 tui() 统一轮询
|
|
884
|
+
// 自动切换 provider(跟随当前会话模型;幂等,与侧边栏共享信号)
|
|
885
|
+
createEffect(() => {
|
|
886
|
+
if (!props.signals.autoBalance())
|
|
887
|
+
return;
|
|
888
|
+
const id = sid;
|
|
889
|
+
if (!id)
|
|
890
|
+
return;
|
|
891
|
+
const msgs = props.api.state.session.messages(id);
|
|
892
|
+
let pid = "";
|
|
893
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
894
|
+
const m = msgs[i];
|
|
895
|
+
if (m.role === "assistant" && m.providerID) {
|
|
896
|
+
pid = m.providerID;
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (!pid) {
|
|
901
|
+
try {
|
|
902
|
+
pid = props.api.state.session.get(id)?.model?.providerID ?? "";
|
|
903
|
+
}
|
|
904
|
+
catch { }
|
|
905
|
+
}
|
|
906
|
+
if (!pid)
|
|
907
|
+
return;
|
|
908
|
+
const hit = matchBalanceProvider(pid);
|
|
909
|
+
if (hit) {
|
|
910
|
+
props.signals.setBalanceUnsupported(false);
|
|
911
|
+
if (hit.id !== props.signals.balanceProviderId()) {
|
|
912
|
+
props.signals.setBalanceProviderId(hit.id);
|
|
913
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
// 当前提供商没有余额适配器 → 标记不支持,余额显示 N/A 并停止轮询
|
|
918
|
+
props.signals.setBalanceUnsupported(true);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
// ── 主题色(与侧边栏同口径)──
|
|
922
|
+
const pal = createMemo(() => {
|
|
923
|
+
const th = props.api.theme.current;
|
|
924
|
+
const sat = (k, fb) => desaturateTo(th[k], MAX_SAT, fb);
|
|
925
|
+
return {
|
|
926
|
+
text: sat("text", FALLBACK.text),
|
|
927
|
+
muted: sat("textMuted", FALLBACK.muted),
|
|
928
|
+
success: sat("success", FALLBACK.success),
|
|
929
|
+
warning: sat("warning", FALLBACK.warning),
|
|
930
|
+
error: sat("error", FALLBACK.error),
|
|
931
|
+
};
|
|
932
|
+
});
|
|
933
|
+
const hitColor = createMemo(() => {
|
|
934
|
+
const r = stats()?.hitRate ?? -1;
|
|
935
|
+
if (r >= 85)
|
|
936
|
+
return pal().success;
|
|
937
|
+
if (r >= 70)
|
|
938
|
+
return pal().warning;
|
|
939
|
+
return pal().error;
|
|
940
|
+
});
|
|
941
|
+
// 命中率趋势:最后一条与上一条的差值;|Δ| < 0.05 视为无变化(null = 不显示)
|
|
942
|
+
const trend = createMemo(() => {
|
|
943
|
+
const s = stats();
|
|
944
|
+
if (!s || s.prevHitRate < 0 || s.hitRate < 0)
|
|
945
|
+
return null;
|
|
946
|
+
const d = s.hitRate - s.prevHitRate;
|
|
947
|
+
return Math.abs(d) < 0.05 ? null : d;
|
|
948
|
+
});
|
|
949
|
+
const balanceText = createMemo(() => {
|
|
950
|
+
const s = props.signals.balanceState();
|
|
951
|
+
if (s.status === "ok" && s.data)
|
|
952
|
+
return formatBalanceText(s.data, props.signals.balanceCurrency(), props.signals.exchangeRate());
|
|
953
|
+
if (s.status === "loading")
|
|
954
|
+
return "\u2026";
|
|
955
|
+
if (s.status === "error")
|
|
956
|
+
return "\u26a0";
|
|
957
|
+
return "-";
|
|
958
|
+
});
|
|
959
|
+
// 路径显示(替换宿主默认 hint 左侧的 cwd 文本)
|
|
960
|
+
const directory = createMemo(() => {
|
|
961
|
+
try {
|
|
962
|
+
return props.api.state.path.directory;
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
return "";
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
// 恢复显隐偏好(默认显示);关闭时回退为仅显示路径,与宿主默认 hint 行一致
|
|
969
|
+
onMount(() => {
|
|
970
|
+
try {
|
|
971
|
+
const v = props.api.kv.get(`${KV_PREFIX}.section.bottom`, true);
|
|
972
|
+
props.signals.setSectionBottom(v !== false);
|
|
973
|
+
}
|
|
974
|
+
catch { }
|
|
975
|
+
});
|
|
976
|
+
return (_jsx(Show, { when: props.signals.sectionBottom(), fallback: _jsx("text", { fg: pal().muted, children: directory() }), children: _jsxs("box", { marginLeft: 1, flexGrow: 1, flexShrink: 0, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: pal().muted, children: directory() }), _jsxs("box", { flexDirection: "row", children: [_jsxs("text", { children: [_jsxs("span", { style: { fg: pal().muted }, children: [t("barHit"), " "] }), _jsx("span", { style: { fg: hitColor() }, children: (stats()?.hitRate ?? -1) >= 0 ? (Math.floor(stats().hitRate * 10) / 10).toFixed(1) + "%" : "--" }), _jsx(Show, { when: trend() !== null, children: _jsx("span", { style: { fg: trend() > 0 ? pal().success : pal().error }, children: " " + (trend() > 0 ? "\u2191" : "\u2193") + Math.abs(trend()).toFixed(1) + "%" }) }), _jsx("span", { style: { fg: pal().muted }, children: " \u00b7 " + t("barTok") + " " }), _jsx("span", { style: { fg: pal().text }, children: stats() ? fmtCompact(stats().input + stats().read + stats().write) : "--" }), _jsxs(Show, { when: !props.signals.balanceUnsupported(), children: [_jsx("span", { style: { fg: pal().muted }, children: " \u00b7 " + t("barBal") + " " }), _jsx("span", { style: { fg: pal().text }, children: balanceText() })] })] }), _jsx("text", { fg: pal().muted, children: " \u00b7 " })] })] }) }));
|
|
977
|
+
}
|
|
934
978
|
function createSidebarSlot(api, signals) {
|
|
935
979
|
let lastSlotSid = "";
|
|
936
980
|
return {
|
|
@@ -959,50 +1003,144 @@ const tui = async (api) => {
|
|
|
959
1003
|
const [sectionDist, setSectionDist] = createSignal(true);
|
|
960
1004
|
const [sectionSkills, setSectionSkills] = createSignal(true);
|
|
961
1005
|
const [sectionBalance, setSectionBalance] = createSignal(true);
|
|
1006
|
+
const [sectionBottom, setSectionBottom] = createSignal(true);
|
|
962
1007
|
const [balanceRefresh, setBalanceRefresh] = createSignal(0);
|
|
963
1008
|
const [balanceProviderId, setBalanceProviderId] = createSignal("deepseek");
|
|
964
1009
|
const [autoBalance, setAutoBalance] = createSignal(true);
|
|
1010
|
+
const [balanceUnsupported, setBalanceUnsupported] = createSignal(false);
|
|
965
1011
|
const [balanceCurrency, setBalanceCurrency] = createSignal("");
|
|
966
1012
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
967
|
-
const [
|
|
1013
|
+
const [langCode, setLangCode] = createSignal(INIT_LANG);
|
|
968
1014
|
const [overrideSessionId, setOverrideSessionId] = createSignal(undefined);
|
|
1015
|
+
// ── 余额查询状态(共享):侧边栏与底部栏读同一份数据,
|
|
1016
|
+
// 避免重复请求导致两处余额不一致 ──
|
|
1017
|
+
const [balanceState, setBalanceState] = createSignal({
|
|
1018
|
+
status: "idle", data: null, lastFetch: 0,
|
|
1019
|
+
});
|
|
1020
|
+
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
1021
|
+
let balanceSeq = 0;
|
|
969
1022
|
const signals = {
|
|
970
1023
|
currencySymbol, setCurrencySymbol,
|
|
971
1024
|
exchangeRate, setExchangeRate,
|
|
972
|
-
|
|
1025
|
+
langCode, setLangCode,
|
|
973
1026
|
sectionDetail, setSectionDetail,
|
|
974
1027
|
sectionModel, setSectionModel,
|
|
975
1028
|
sectionDist, setSectionDist,
|
|
976
1029
|
sectionSkills, setSectionSkills,
|
|
977
1030
|
sectionBalance, setSectionBalance,
|
|
1031
|
+
sectionBottom, setSectionBottom,
|
|
978
1032
|
balanceRefresh, setBalanceRefresh,
|
|
979
1033
|
balanceProviderId, setBalanceProviderId,
|
|
980
1034
|
autoBalance, setAutoBalance,
|
|
1035
|
+
balanceUnsupported, setBalanceUnsupported,
|
|
1036
|
+
balanceState,
|
|
981
1037
|
balanceCurrency, setBalanceCurrency,
|
|
982
1038
|
borderVisible, setBorderVisible,
|
|
983
1039
|
overrideSessionId, setOverrideSessionId,
|
|
984
1040
|
};
|
|
985
1041
|
api.slots.register(createSidebarSlot(api, signals));
|
|
1042
|
+
// 输入框 hint 行(session_prompt slot,replace 模式):
|
|
1043
|
+
// 用宿主同一 Prompt 组件重渲染输入框,仅替换 hint 行左侧——
|
|
1044
|
+
// 在路径与右侧 token/commands 提示之间插入 命中率 · 余额 · Tokens。
|
|
1045
|
+
api.slots.register({
|
|
1046
|
+
order: 55,
|
|
1047
|
+
slots: {
|
|
1048
|
+
session_prompt(_ctx, input) {
|
|
1049
|
+
return (_jsx(api.ui.Prompt, { sessionID: input.session_id, visible: input.visible, disabled: input.disabled, onSubmit: input.on_submit, ref: input.ref, hint: _jsx(BottomStatusBar, { api: api, signals: signals, sessionId: input.session_id }) }));
|
|
1050
|
+
},
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
986
1053
|
// ── slash commands for runtime config ──
|
|
987
1054
|
const KV_PREFIX = "cache_panel";
|
|
1055
|
+
// ── 语言偏好恢复:KV 就绪后优先用户设置(/cache-lang),覆盖自动识别 ──
|
|
1056
|
+
const restoreLang = () => {
|
|
1057
|
+
try {
|
|
1058
|
+
const saved = api.kv.get(`${KV_PREFIX}.lang`);
|
|
1059
|
+
if (saved && LANG_META.some((m) => m.code === saved))
|
|
1060
|
+
setLangCode(saved);
|
|
1061
|
+
}
|
|
1062
|
+
catch { }
|
|
1063
|
+
};
|
|
1064
|
+
if (api.kv.ready) {
|
|
1065
|
+
restoreLang();
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
const langTimer = setInterval(() => {
|
|
1069
|
+
if (api.kv.ready) {
|
|
1070
|
+
clearInterval(langTimer);
|
|
1071
|
+
restoreLang();
|
|
1072
|
+
}
|
|
1073
|
+
}, 10);
|
|
1074
|
+
api.lifecycle.onDispose(() => clearInterval(langTimer));
|
|
1075
|
+
}
|
|
1076
|
+
const pollBalance = async () => {
|
|
1077
|
+
const provider = getBalanceProvider(balanceProviderId());
|
|
1078
|
+
// 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
|
|
1079
|
+
const key = api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1080
|
+
|| findOpencodeKey(api, provider);
|
|
1081
|
+
if (balanceUnsupported()) {
|
|
1082
|
+
setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (!key) {
|
|
1086
|
+
setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const now = Date.now();
|
|
1090
|
+
const prev = balanceState();
|
|
1091
|
+
// key 已更换(重新输入)→ 强制重新查询,绕过缓存
|
|
1092
|
+
if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS)
|
|
1093
|
+
return; // cache still fresh
|
|
1094
|
+
const seq = ++balanceSeq;
|
|
1095
|
+
setBalanceState({ ...prev, status: "loading", error: undefined, key });
|
|
1096
|
+
const controller = new AbortController();
|
|
1097
|
+
let timedOut = false;
|
|
1098
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, 10_000);
|
|
1099
|
+
try {
|
|
1100
|
+
const data = await provider.fetchBalance(key, controller.signal);
|
|
1101
|
+
clearTimeout(timer);
|
|
1102
|
+
if (seq !== balanceSeq)
|
|
1103
|
+
return; // 已被更新的请求取代,丢弃过期结果
|
|
1104
|
+
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key });
|
|
1105
|
+
}
|
|
1106
|
+
catch (err) {
|
|
1107
|
+
clearTimeout(timer);
|
|
1108
|
+
if (seq !== balanceSeq)
|
|
1109
|
+
return;
|
|
1110
|
+
const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "");
|
|
1111
|
+
// 失败时清空旧数据,避免显示过期余额
|
|
1112
|
+
setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key });
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
1116
|
+
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
1117
|
+
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
1118
|
+
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
1119
|
+
createEffect(() => {
|
|
1120
|
+
void balanceRefresh();
|
|
1121
|
+
untrack(() => { void pollBalance(); });
|
|
1122
|
+
});
|
|
1123
|
+
// 定时轮询(5 分钟);随插件生命周期清理
|
|
1124
|
+
const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS);
|
|
1125
|
+
api.lifecycle.onDispose(() => clearInterval(balanceTimer));
|
|
988
1126
|
/** 菜单中 provider 选项标题:标注 key 来源(手动配置 / OpenCode 自动复用 / 未配置)。 */
|
|
989
1127
|
const providerOptionTitle = (p, current) => {
|
|
990
|
-
const
|
|
1128
|
+
const t = createT(() => langCode());
|
|
991
1129
|
const hasManual = !!api.kv.get(`${KV_PREFIX}.balance.${p.id}.key`, "");
|
|
992
1130
|
const hasAuto = !hasManual && !!findOpencodeKey(api, p);
|
|
993
1131
|
const mark = hasManual
|
|
994
|
-
? (
|
|
1132
|
+
? t("keyUser")
|
|
995
1133
|
: hasAuto
|
|
996
|
-
? (
|
|
997
|
-
: (
|
|
1134
|
+
? t("keyOpenCode")
|
|
1135
|
+
: t("keyNotSet");
|
|
998
1136
|
return p.name + mark + (current && p.id === current ? " *" : "");
|
|
999
1137
|
};
|
|
1000
1138
|
/** 弹出指定 provider 的 API Key 输入框(脱敏预填;空清除 / 含 * 保留原 key / 新 key 实时刷新)。 */
|
|
1001
1139
|
const promptBalanceKey = (dialog, provider) => {
|
|
1002
|
-
const
|
|
1140
|
+
const t = createT(() => langCode());
|
|
1003
1141
|
const current = api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "");
|
|
1004
1142
|
const masked = maskKey(current);
|
|
1005
|
-
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: provider.name, description: () => _jsx("text", { children:
|
|
1143
|
+
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: provider.name, description: () => _jsx("text", { children: t("balKeyPrompt", { p: provider.name }) }), placeholder: provider.keyPlaceholder ?? "sk-...", value: masked, onConfirm: (val) => {
|
|
1006
1144
|
const input = val.trim();
|
|
1007
1145
|
let key;
|
|
1008
1146
|
if (input === "") {
|
|
@@ -1017,10 +1155,10 @@ const tui = async (api) => {
|
|
|
1017
1155
|
api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key);
|
|
1018
1156
|
setBalanceRefresh(v => v + 1);
|
|
1019
1157
|
if (key) {
|
|
1020
|
-
api.ui.toast({ message:
|
|
1158
|
+
api.ui.toast({ message: t("keySaved") });
|
|
1021
1159
|
}
|
|
1022
1160
|
else {
|
|
1023
|
-
api.ui.toast({ message:
|
|
1161
|
+
api.ui.toast({ message: t("keyCleared") });
|
|
1024
1162
|
}
|
|
1025
1163
|
dialog?.clear();
|
|
1026
1164
|
}, onCancel: () => dialog?.clear() })));
|
|
@@ -1036,6 +1174,7 @@ const tui = async (api) => {
|
|
|
1036
1174
|
title: `${code} (${sym})`,
|
|
1037
1175
|
value: code,
|
|
1038
1176
|
})), onSelect: (opt) => {
|
|
1177
|
+
const t = createT(() => langCode());
|
|
1039
1178
|
const sym = CURRENCIES[opt.value] ?? "$";
|
|
1040
1179
|
const defRate = DEFAULT_RATES[opt.value] ?? 1;
|
|
1041
1180
|
api.kv.set(`${KV_PREFIX}.currency`, sym);
|
|
@@ -1045,7 +1184,7 @@ const tui = async (api) => {
|
|
|
1045
1184
|
signals.setBalanceCurrency(opt.value);
|
|
1046
1185
|
signals.setCurrencySymbol(sym);
|
|
1047
1186
|
signals.setExchangeRate(defRate);
|
|
1048
|
-
api.ui.toast({ message:
|
|
1187
|
+
api.ui.toast({ message: t("currencySet", { v: opt.value, s: sym, r: defRate }) });
|
|
1049
1188
|
dialog?.clear();
|
|
1050
1189
|
} })));
|
|
1051
1190
|
},
|
|
@@ -1057,11 +1196,12 @@ const tui = async (api) => {
|
|
|
1057
1196
|
slash: { name: "cache-rate" },
|
|
1058
1197
|
onSelect: (dialog) => {
|
|
1059
1198
|
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: "Exchange Rate", description: () => _jsx("text", { children: "Enter the exchange rate from USD to your currency (e.g. 7.2 for CNY)" }), placeholder: "1.0", value: String(api.kv.get(`${KV_PREFIX}.rate`, 1)), onConfirm: (val) => {
|
|
1199
|
+
const t = createT(() => langCode());
|
|
1060
1200
|
const n = parseFloat(val);
|
|
1061
1201
|
if (n > 0) {
|
|
1062
1202
|
api.kv.set(`${KV_PREFIX}.rate`, n);
|
|
1063
1203
|
signals.setExchangeRate(n);
|
|
1064
|
-
api.ui.toast({ message:
|
|
1204
|
+
api.ui.toast({ message: t("rateSet", { r: n }) });
|
|
1065
1205
|
}
|
|
1066
1206
|
dialog?.clear();
|
|
1067
1207
|
} })));
|
|
@@ -1073,25 +1213,38 @@ const tui = async (api) => {
|
|
|
1073
1213
|
description: "Show or hide a sidebar section",
|
|
1074
1214
|
slash: { name: "cache-section" },
|
|
1075
1215
|
onSelect: (dialog) => {
|
|
1216
|
+
const t = createT(() => langCode());
|
|
1076
1217
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
1077
1218
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
|
|
1078
1219
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
1079
1220
|
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
1080
1221
|
const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true));
|
|
1222
|
+
const bottomOn = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true));
|
|
1081
1223
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1224
|
+
const labels = {
|
|
1225
|
+
detail: t("secDetail"),
|
|
1226
|
+
model: t("secModel"),
|
|
1227
|
+
dist: t("distTitle"),
|
|
1228
|
+
skills: t("secSkills"),
|
|
1229
|
+
balance: t("secBalance"),
|
|
1230
|
+
bottom: t("secBottom"),
|
|
1231
|
+
border: t("secBorder"),
|
|
1232
|
+
};
|
|
1233
|
+
const optTitle = (label, on) => `${visualPadEnd(label, 15)}[${on ? "ON" : "OFF"}]`;
|
|
1234
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("secToggle"), options: [
|
|
1235
|
+
{ title: optTitle(labels.detail, detailOn), value: "detail" },
|
|
1236
|
+
{ title: optTitle(labels.model, modelOn), value: "model" },
|
|
1237
|
+
{ title: optTitle(labels.dist, distOn), value: "dist" },
|
|
1238
|
+
{ title: optTitle(labels.skills, skillsOn), value: "skills" },
|
|
1239
|
+
{ title: optTitle(labels.balance, balanceOn), value: "balance" },
|
|
1240
|
+
{ title: optTitle(labels.bottom, bottomOn), value: "bottom" },
|
|
1241
|
+
{ title: optTitle(labels.border, borderOn), value: "border" },
|
|
1089
1242
|
], onSelect: (opt) => {
|
|
1090
1243
|
if (opt.value === "border") {
|
|
1091
1244
|
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
1092
1245
|
api.kv.set(`${KV_PREFIX}.border`, !cur);
|
|
1093
1246
|
signals.setBorderVisible(!cur);
|
|
1094
|
-
api.ui.toast({ message:
|
|
1247
|
+
api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") });
|
|
1095
1248
|
}
|
|
1096
1249
|
else {
|
|
1097
1250
|
const key = `${KV_PREFIX}.section.${opt.value}`;
|
|
@@ -1107,7 +1260,10 @@ const tui = async (api) => {
|
|
|
1107
1260
|
signals.setSectionSkills(!cur);
|
|
1108
1261
|
if (opt.value === "balance")
|
|
1109
1262
|
signals.setSectionBalance(!cur);
|
|
1110
|
-
|
|
1263
|
+
if (opt.value === "bottom")
|
|
1264
|
+
signals.setSectionBottom(!cur);
|
|
1265
|
+
const name = labels[opt.value] ?? opt.value;
|
|
1266
|
+
api.ui.toast({ message: t(!cur ? "sectionShown" : "sectionHidden", { s: name }) });
|
|
1111
1267
|
}
|
|
1112
1268
|
dialog?.clear();
|
|
1113
1269
|
} })));
|
|
@@ -1119,6 +1275,7 @@ const tui = async (api) => {
|
|
|
1119
1275
|
description: "Display the current plugin configuration",
|
|
1120
1276
|
slash: { name: "cache-config" },
|
|
1121
1277
|
onSelect: (dialog) => {
|
|
1278
|
+
const t = createT(() => langCode());
|
|
1122
1279
|
const sym = api.kv.get(`${KV_PREFIX}.currency`) ?? "$";
|
|
1123
1280
|
const rate = api.kv.get(`${KV_PREFIX}.rate`) ?? 1;
|
|
1124
1281
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true));
|
|
@@ -1126,9 +1283,16 @@ const tui = async (api) => {
|
|
|
1126
1283
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
|
|
1127
1284
|
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
|
|
1128
1285
|
const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true));
|
|
1286
|
+
const bottom = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true));
|
|
1287
|
+
const on = (v) => v ? "ON" : "OFF";
|
|
1129
1288
|
api.ui.toast({
|
|
1130
|
-
title: "
|
|
1131
|
-
message:
|
|
1289
|
+
title: t("panelConfigTitle"),
|
|
1290
|
+
message: t("panelConfigMsg", {
|
|
1291
|
+
c: sym, r: rate,
|
|
1292
|
+
d: on(detail), m: on(model),
|
|
1293
|
+
t: on(dist), k: on(skills),
|
|
1294
|
+
b: on(balance), f: on(bottom),
|
|
1295
|
+
}),
|
|
1132
1296
|
duration: 8000,
|
|
1133
1297
|
});
|
|
1134
1298
|
dialog?.clear();
|
|
@@ -1140,15 +1304,16 @@ const tui = async (api) => {
|
|
|
1140
1304
|
description: "Switch between Chinese and English display",
|
|
1141
1305
|
slash: { name: "cache-lang" },
|
|
1142
1306
|
onSelect: (dialog) => {
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1307
|
+
const t = createT(() => langCode());
|
|
1308
|
+
const cur = langCode();
|
|
1309
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("langTitle"), options: LANG_META.map((m) => ({
|
|
1310
|
+
title: `${visualPadEnd(m.label, 9)}${cur === m.code ? "\u2713" : ""}`,
|
|
1311
|
+
value: m.code,
|
|
1312
|
+
})), onSelect: (opt) => {
|
|
1313
|
+
const code = opt.value;
|
|
1314
|
+
api.kv.set(`${KV_PREFIX}.lang`, code);
|
|
1315
|
+
setLangCode(code);
|
|
1316
|
+
api.ui.toast({ message: t("langSwitched") });
|
|
1152
1317
|
dialog?.clear();
|
|
1153
1318
|
} })));
|
|
1154
1319
|
},
|
|
@@ -1159,13 +1324,11 @@ const tui = async (api) => {
|
|
|
1159
1324
|
description: "切换余额提供商 / 自动切换当前会话提供商 | Switch balance provider / auto-switch session provider",
|
|
1160
1325
|
slash: { name: "cache-balance" },
|
|
1161
1326
|
onSelect: (dialog) => {
|
|
1162
|
-
const
|
|
1327
|
+
const t = createT(() => langCode());
|
|
1163
1328
|
const current = signals.balanceProviderId();
|
|
1164
1329
|
const auto = signals.autoBalance();
|
|
1165
|
-
const autoLabel = auto
|
|
1166
|
-
|
|
1167
|
-
: (zh ? "自动切换提供商 [关]" : "Auto-switch provider [OFF]");
|
|
1168
|
-
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: zh ? "余额提供商 / 自动切换" : "Balance Provider / Auto-switch", options: [
|
|
1330
|
+
const autoLabel = `${t("autoSwitchOpt")} [${auto ? "ON" : "OFF"}]`;
|
|
1331
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("balProvTitle"), options: [
|
|
1169
1332
|
{
|
|
1170
1333
|
title: autoLabel,
|
|
1171
1334
|
value: "__auto__",
|
|
@@ -1179,7 +1342,7 @@ const tui = async (api) => {
|
|
|
1179
1342
|
const next = !auto;
|
|
1180
1343
|
api.kv.set(`${KV_PREFIX}.balance.auto`, next);
|
|
1181
1344
|
signals.setAutoBalance(next);
|
|
1182
|
-
api.ui.toast({ message:
|
|
1345
|
+
api.ui.toast({ message: next ? t("autoSwitchOn") : t("autoSwitchOff") });
|
|
1183
1346
|
dialog?.clear();
|
|
1184
1347
|
}
|
|
1185
1348
|
else {
|
|
@@ -1189,6 +1352,7 @@ const tui = async (api) => {
|
|
|
1189
1352
|
api.kv.set(`${KV_PREFIX}.balance.auto`, false);
|
|
1190
1353
|
signals.setBalanceProviderId(provider.id);
|
|
1191
1354
|
signals.setAutoBalance(false);
|
|
1355
|
+
signals.setBalanceUnsupported(false);
|
|
1192
1356
|
// 切换后立即按新 provider 刷新显示(无 key 时显示 idle,避免残留上一 provider 余额)
|
|
1193
1357
|
signals.setBalanceRefresh(signals.balanceRefresh() + 1);
|
|
1194
1358
|
const hasKey = !!api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "");
|
|
@@ -1197,7 +1361,7 @@ const tui = async (api) => {
|
|
|
1197
1361
|
promptBalanceKey(dialog, provider);
|
|
1198
1362
|
}
|
|
1199
1363
|
else {
|
|
1200
|
-
api.ui.toast({ message:
|
|
1364
|
+
api.ui.toast({ message: t("providerManual", { p: provider.name }) });
|
|
1201
1365
|
dialog?.clear();
|
|
1202
1366
|
}
|
|
1203
1367
|
}
|
|
@@ -1210,9 +1374,9 @@ const tui = async (api) => {
|
|
|
1210
1374
|
description: "Select a provider and set its API key for balance display",
|
|
1211
1375
|
slash: { name: "cache-balance-key" },
|
|
1212
1376
|
onSelect: (dialog) => {
|
|
1213
|
-
const
|
|
1377
|
+
const t = createT(() => langCode());
|
|
1214
1378
|
// 步骤 1:选择 provider
|
|
1215
|
-
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title:
|
|
1379
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("balSelectTitle"), options: balanceProviders.map((p) => ({
|
|
1216
1380
|
title: providerOptionTitle(p),
|
|
1217
1381
|
value: p.id,
|
|
1218
1382
|
})), onSelect: (opt) => {
|
|
@@ -1235,9 +1399,10 @@ const tui = async (api) => {
|
|
|
1235
1399
|
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
1236
1400
|
slash: { name: "cache-debug-skills" },
|
|
1237
1401
|
onSelect: () => {
|
|
1402
|
+
const t = createT(() => langCode());
|
|
1238
1403
|
const rt = api.route.current;
|
|
1239
1404
|
if (rt.name !== "session" || !rt.params) {
|
|
1240
|
-
api.ui.toast({ message: "
|
|
1405
|
+
api.ui.toast({ message: t("runInSession"), variant: "warning" });
|
|
1241
1406
|
return;
|
|
1242
1407
|
}
|
|
1243
1408
|
const sid = String(rt.params.sessionID);
|
|
@@ -1324,7 +1489,7 @@ const tui = async (api) => {
|
|
|
1324
1489
|
return false; seen.add(c.value); return true; });
|
|
1325
1490
|
if (unique.length > 0) {
|
|
1326
1491
|
// ── 有子代理 → DialogSelect 列表选择 ──
|
|
1327
|
-
const
|
|
1492
|
+
const t = createT(() => langCode());
|
|
1328
1493
|
const currentSid = signals.overrideSessionId() ?? api.kv.get(`${KV_PREFIX}.session`, "");
|
|
1329
1494
|
const options = unique.map((c, i) => ({
|
|
1330
1495
|
title: `${i + 1}. ${c.title}`,
|
|
@@ -1333,33 +1498,33 @@ const tui = async (api) => {
|
|
|
1333
1498
|
}));
|
|
1334
1499
|
// 首尾各放一个"回到主会话",长列表时顶部底部均可直达
|
|
1335
1500
|
const backValue = "__main__";
|
|
1336
|
-
const backTitle = `\u2500 ${
|
|
1501
|
+
const backTitle = `\u2500 ${t("backToMainTitle")}`;
|
|
1337
1502
|
options.unshift({ title: backTitle, value: backValue, description: "" });
|
|
1338
1503
|
options.push({ title: backTitle, value: backValue, description: "" });
|
|
1339
1504
|
const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1;
|
|
1340
|
-
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title:
|
|
1505
|
+
dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: t("subSelectTitle"), options: options, current: currentIdx >= 0 ? options[currentIdx].value : undefined, onSelect: (opt) => {
|
|
1341
1506
|
if (opt.value === backValue) {
|
|
1342
1507
|
signals.setOverrideSessionId(undefined);
|
|
1343
1508
|
api.kv.set(`${KV_PREFIX}.session`, "");
|
|
1344
|
-
api.ui.toast({ message:
|
|
1509
|
+
api.ui.toast({ message: t("backToMain") });
|
|
1345
1510
|
}
|
|
1346
1511
|
else {
|
|
1347
1512
|
signals.setOverrideSessionId(opt.value);
|
|
1348
1513
|
api.kv.set(`${KV_PREFIX}.session`, opt.value);
|
|
1349
|
-
api.ui.toast({ message: (
|
|
1514
|
+
api.ui.toast({ message: t("subAgentSwitched", { s: opt.value.slice(0, 24) + "\u2026" }) });
|
|
1350
1515
|
}
|
|
1351
1516
|
dialog?.clear();
|
|
1352
1517
|
} })));
|
|
1353
1518
|
}
|
|
1354
1519
|
else {
|
|
1355
1520
|
// ── 无子代理 → DialogPrompt 手动粘贴 ──
|
|
1356
|
-
const
|
|
1357
|
-
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: signals.overrideSessionId() ?
|
|
1521
|
+
const t = createT(() => langCode());
|
|
1522
|
+
dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: signals.overrideSessionId() ? t("subSwitchTitle") : t("subViewTitle"), description: () => _jsx("text", { children: t("subNoFound") }), placeholder: "ses_...", value: signals.overrideSessionId() ?? api.kv.get(`${KV_PREFIX}.session`, "") ?? "", onConfirm: (val) => {
|
|
1358
1523
|
const sid = val.trim();
|
|
1359
1524
|
if (sid) {
|
|
1360
1525
|
signals.setOverrideSessionId(sid);
|
|
1361
1526
|
api.kv.set(`${KV_PREFIX}.session`, sid);
|
|
1362
|
-
api.ui.toast({ message: (
|
|
1527
|
+
api.ui.toast({ message: t("subAgentSwitched", { s: sid.slice(0, 24) + "\u2026" }) });
|
|
1363
1528
|
}
|
|
1364
1529
|
dialog?.clear();
|
|
1365
1530
|
}, onCancel: () => dialog?.clear() })));
|
|
@@ -1372,9 +1537,10 @@ const tui = async (api) => {
|
|
|
1372
1537
|
description: "Return to main session stats",
|
|
1373
1538
|
slash: { name: "cache-session-back" },
|
|
1374
1539
|
onSelect: (dialog) => {
|
|
1540
|
+
const t = createT(() => langCode());
|
|
1375
1541
|
signals.setOverrideSessionId(undefined);
|
|
1376
1542
|
api.kv.set(`${KV_PREFIX}.session`, "");
|
|
1377
|
-
api.ui.toast({ message:
|
|
1543
|
+
api.ui.toast({ message: t("backToMain") });
|
|
1378
1544
|
dialog?.clear();
|
|
1379
1545
|
},
|
|
1380
1546
|
},
|