opencode-visual-cache 1.5.0 → 1.6.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.md +12 -6
- package/README_EN.md +13 -5
- 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 +461 -260
- package/dist/tui.js +1203 -478
- package/package.json +6 -3
- package/src/_version.ts +1 -1
- package/src/i18n.ts +380 -0
- package/src/index.tsx +547 -315
package/src/index.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
TuiPluginModule,
|
|
10
10
|
TuiThemeCurrent,
|
|
11
11
|
TuiDialogStack,
|
|
12
|
+
TuiPromptRef,
|
|
12
13
|
} from "@opencode-ai/plugin/tui"
|
|
13
14
|
import type { UserMessage, AssistantMessage, Message } from "@opencode-ai/sdk"
|
|
14
15
|
import type {
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, untrack } from "solid-js"
|
|
22
23
|
import { PLUGIN_VERSION } from "./_version"
|
|
23
24
|
import { balanceProviders, getBalanceProvider, maskKey, matchBalanceProvider, type BalanceEntry, type BalanceProvider } from "./balance-providers"
|
|
25
|
+
import { LANG_META, createT, detectLang, type LangCode } from "./i18n"
|
|
24
26
|
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Helpers
|
|
@@ -73,97 +75,14 @@ function truncateVisual(s: string, maxCols: number): string {
|
|
|
73
75
|
return result
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
// ── language override (env: CACHE_TUI_LANG) ──
|
|
77
|
-
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined
|
|
78
|
-
|
|
79
78
|
// ── language ──────────────────────────────────────────────────────
|
|
79
|
+
// 语言初始化:环境变量 CACHE_TUI_LANG 覆盖 → 否则按系统 locale 自动检测。
|
|
80
|
+
// 用户通过 /cache-lang 设置的偏好会在 KV 就绪后优先覆盖(见 tui() 内恢复逻辑)。
|
|
80
81
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
catch { return false }
|
|
86
|
-
})()
|
|
87
|
-
|
|
88
|
-
const ZH_T = {
|
|
89
|
-
title: "缓存统计",
|
|
90
|
-
hit: "命中率",
|
|
91
|
-
totalHit: "总命中:",
|
|
92
|
-
read: "缓存读:",
|
|
93
|
-
write: "缓存写:",
|
|
94
|
-
miss: "未命中:",
|
|
95
|
-
out: "输出:",
|
|
96
|
-
cost: "费用:",
|
|
97
|
-
saved: "累计节省:",
|
|
98
|
-
model: "模型:",
|
|
99
|
-
provider: "提供商:",
|
|
100
|
-
rate: "单价:",
|
|
101
|
-
hitFolded: "命中",
|
|
102
|
-
inputRate: "输入",
|
|
103
|
-
cacheRate: "缓存",
|
|
104
|
-
writeRate: "写入",
|
|
105
|
-
noData: "等待缓存数据...",
|
|
106
|
-
tok: "tok",
|
|
107
|
-
distTitle: "估算 Token 分布",
|
|
108
|
-
distSys: "系统提示:",
|
|
109
|
-
distUser: "用户:",
|
|
110
|
-
distAgent: "Agent 指令:",
|
|
111
|
-
distTool: "Tool 调用:",
|
|
112
|
-
distRes: "Tool 结果:",
|
|
113
|
-
distTotal: "总计:",
|
|
114
|
-
distOut: "输出:",
|
|
115
|
-
secDetail: "明细",
|
|
116
|
-
secModel: "模型",
|
|
117
|
-
secSkills: "已加载技能",
|
|
118
|
-
balTotal: "总余额:",
|
|
119
|
-
balNoKey: "未配置 {p} API Key",
|
|
120
|
-
balLoading: "查询中...",
|
|
121
|
-
balError: "查询失败",
|
|
122
|
-
balErr401: "API Key 无效",
|
|
123
|
-
balErr403: "余额查询被拒绝",
|
|
124
|
-
balErrEmpty:"未获取到余额数据",
|
|
125
|
-
balErrTimeout: "查询超时",
|
|
126
|
-
} as const
|
|
127
|
-
|
|
128
|
-
const EN_T = {
|
|
129
|
-
title: "Token Cache",
|
|
130
|
-
hit: "Hit",
|
|
131
|
-
totalHit: "Total Hit:",
|
|
132
|
-
read: "Read:",
|
|
133
|
-
write: "Write:",
|
|
134
|
-
miss: "Miss:",
|
|
135
|
-
out: "Out:",
|
|
136
|
-
cost: "Cost:",
|
|
137
|
-
saved: "Total Saved:",
|
|
138
|
-
model: "Model:",
|
|
139
|
-
provider: "Provider:",
|
|
140
|
-
rate: "Rate:",
|
|
141
|
-
hitFolded: "hit",
|
|
142
|
-
inputRate: "in",
|
|
143
|
-
cacheRate: "cache",
|
|
144
|
-
writeRate: "write",
|
|
145
|
-
noData: "Waiting for cache data...",
|
|
146
|
-
tok: "tok",
|
|
147
|
-
distTitle: "Estimated Token Dist.",
|
|
148
|
-
distSys: "System:",
|
|
149
|
-
distUser: "User:",
|
|
150
|
-
distAgent: "Agent Instr:",
|
|
151
|
-
distTool: "Tool Call:",
|
|
152
|
-
distRes: "Tool Result:",
|
|
153
|
-
distTotal: "Total:",
|
|
154
|
-
distOut: "Output:",
|
|
155
|
-
secDetail: "Detail",
|
|
156
|
-
secModel: "Model",
|
|
157
|
-
secSkills: "Loaded Skills",
|
|
158
|
-
balTotal: "Total:",
|
|
159
|
-
balNoKey: "{p} API Key not set",
|
|
160
|
-
balLoading: "Fetching...",
|
|
161
|
-
balError: "Fetch failed",
|
|
162
|
-
balErr401: "Invalid API Key",
|
|
163
|
-
balErr403: "Balance request rejected",
|
|
164
|
-
balErrEmpty:"No balance data",
|
|
165
|
-
balErrTimeout: "Request timed out",
|
|
166
|
-
} as const
|
|
82
|
+
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined
|
|
83
|
+
const INIT_LANG: LangCode = DEBUG_LANG !== undefined && LANG_META.some((m) => m.code === DEBUG_LANG)
|
|
84
|
+
? (DEBUG_LANG as LangCode)
|
|
85
|
+
: detectLang()
|
|
167
86
|
|
|
168
87
|
// ── color helpers ────────────────────────────────────────────────
|
|
169
88
|
|
|
@@ -224,7 +143,7 @@ function desaturateTo(raw: unknown, maxSat: number, fallback: string): string {
|
|
|
224
143
|
* converges to within a fraction of an 8‑bit step, eliminating
|
|
225
144
|
* colour banding in edge cases.
|
|
226
145
|
*/
|
|
227
|
-
//
|
|
146
|
+
// Bt.601 luma (perceptual brightness used as the grey anchor)
|
|
228
147
|
const luma = c.r * 0.299 + c.g * 0.587 + c.b * 0.114
|
|
229
148
|
let lo = 0, hi = 1
|
|
230
149
|
for (let i = 0; i < 12; i++) {
|
|
@@ -340,13 +259,15 @@ function estimateTokens(text: string): number {
|
|
|
340
259
|
interface TokenDist {
|
|
341
260
|
system: number // UserMessage.system
|
|
342
261
|
user: number // user message text/file parts
|
|
343
|
-
agent: number //
|
|
262
|
+
agent: number // task tool input prompt/description (sub-agent delegation)
|
|
344
263
|
toolCall: number // ToolPart.input (actual tool params)
|
|
345
264
|
toolResult: number // ToolPart completed output / error
|
|
346
|
-
output: number // AssistantMessage.tokens.output (
|
|
265
|
+
output: number // AssistantMessage.tokens.output (API exact, reasoning excluded)
|
|
266
|
+
reasoning: number // AssistantMessage.tokens.reasoning (API exact)
|
|
347
267
|
apiOutput: number // StepFinishPart.tokens.output (API exact, preferred)
|
|
348
|
-
apiInput: number //
|
|
349
|
-
stepCost: number
|
|
268
|
+
apiInput: number // API exact total input context (input + cache read + cache write)
|
|
269
|
+
stepCost: number // last step-finish part cost (USD) in the current round
|
|
270
|
+
stepCount: number // step-finish parts count across the current round (parentID chain)
|
|
350
271
|
}
|
|
351
272
|
|
|
352
273
|
// ---------------------------------------------------------------------------
|
|
@@ -401,6 +322,39 @@ function balanceSymbol(currency: string): string {
|
|
|
401
322
|
return sym ?? currency + " "
|
|
402
323
|
}
|
|
403
324
|
|
|
325
|
+
/** 紧凑数字缩写(底部状态栏用):1234 → "1.2K",1234567 → "1.2M"。 */
|
|
326
|
+
function fmtCompact(n: number): string {
|
|
327
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"
|
|
328
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"
|
|
329
|
+
return String(Math.round(n))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 余额数值格式化:≥1 或 0 显示固定 2 位小数;小额(<1)保留精度(最多 6 位),避免抹成 0.00。 */
|
|
333
|
+
function formatBalanceAmount(total: string): string {
|
|
334
|
+
const n = parseFloat(total)
|
|
335
|
+
if (!Number.isFinite(n)) return total
|
|
336
|
+
if (n === 0 || n >= 1) return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
337
|
+
return n.toLocaleString("en-US", { maximumFractionDigits: 6 })
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 将余额列表格式化为单行文本。
|
|
342
|
+
* 优先直接显示偏好币种(CNY/USD…);偏好币种为换算币种时按汇率折算第一条余额。
|
|
343
|
+
*/
|
|
344
|
+
function formatBalanceText(list: BalanceEntry[], pref: string, rate: number): string {
|
|
345
|
+
const native = pref ? list.find((x) => x.currency === pref) : undefined
|
|
346
|
+
if (native) return balanceSymbol(native.currency) + formatBalanceAmount(native.total)
|
|
347
|
+
const base = list[0]
|
|
348
|
+
const baseAmt = parseFloat(base.total)
|
|
349
|
+
const converted = Number.isFinite(baseAmt)
|
|
350
|
+
? convertBalance(pref || base.currency, rate, baseAmt, base.currency)
|
|
351
|
+
: baseAmt
|
|
352
|
+
const shown = pref && base.currency !== pref
|
|
353
|
+
? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
|
|
354
|
+
: formatBalanceAmount(base.total)
|
|
355
|
+
return balanceSymbol(pref || base.currency) + shown
|
|
356
|
+
}
|
|
357
|
+
|
|
404
358
|
// ---------------------------------------------------------------------------
|
|
405
359
|
// Sidebar component
|
|
406
360
|
// ---------------------------------------------------------------------------
|
|
@@ -413,8 +367,8 @@ interface PanelSignals {
|
|
|
413
367
|
setCurrencySymbol: (v: string) => void
|
|
414
368
|
exchangeRate: () => number
|
|
415
369
|
setExchangeRate: (v: number) => void
|
|
416
|
-
|
|
417
|
-
|
|
370
|
+
langCode: () => LangCode
|
|
371
|
+
setLangCode: (v: LangCode) => void
|
|
418
372
|
sectionDetail: () => boolean
|
|
419
373
|
setSectionDetail: (v: boolean) => void
|
|
420
374
|
sectionModel: () => boolean
|
|
@@ -425,6 +379,9 @@ interface PanelSignals {
|
|
|
425
379
|
setSectionSkills: (v: boolean) => void
|
|
426
380
|
sectionBalance: () => boolean
|
|
427
381
|
setSectionBalance: (v: boolean) => void
|
|
382
|
+
/** Bottom status bar (prompt hint line) visibility. */
|
|
383
|
+
sectionBottom: () => boolean
|
|
384
|
+
setSectionBottom: (v: boolean) => void
|
|
428
385
|
/** Increment to force a balance re-fetch. */
|
|
429
386
|
balanceRefresh: () => number
|
|
430
387
|
setBalanceRefresh: (v: number) => void
|
|
@@ -434,6 +391,11 @@ interface PanelSignals {
|
|
|
434
391
|
/** Auto-switch to the session's provider for balance display. Manual switch disables it. */
|
|
435
392
|
autoBalance: () => boolean
|
|
436
393
|
setAutoBalance: (v: boolean) => void
|
|
394
|
+
/** True when the session's provider has no balance adapter (auto mode). Suppresses balance polling. */
|
|
395
|
+
balanceUnsupported: () => boolean
|
|
396
|
+
setBalanceUnsupported: (v: boolean) => void
|
|
397
|
+
/** Shared balance query state — single source of truth for sidebar and bottom bar. */
|
|
398
|
+
balanceState: () => BalanceState
|
|
437
399
|
/** Preferred currency code for balance display (CNY / USD / …). Empty = first entry. */
|
|
438
400
|
balanceCurrency: () => string
|
|
439
401
|
setBalanceCurrency: (v: string) => void
|
|
@@ -483,7 +445,7 @@ function TokenCachePanel(props: {
|
|
|
483
445
|
const {
|
|
484
446
|
currencySymbol, setCurrencySymbol,
|
|
485
447
|
exchangeRate, setExchangeRate,
|
|
486
|
-
|
|
448
|
+
langCode,
|
|
487
449
|
sectionDetail, setSectionDetail,
|
|
488
450
|
sectionModel, setSectionModel,
|
|
489
451
|
sectionDist, setSectionDist,
|
|
@@ -492,12 +454,14 @@ function TokenCachePanel(props: {
|
|
|
492
454
|
balanceRefresh,
|
|
493
455
|
balanceProviderId, setBalanceProviderId,
|
|
494
456
|
autoBalance, setAutoBalance,
|
|
457
|
+
balanceUnsupported, setBalanceUnsupported,
|
|
458
|
+
balanceState,
|
|
495
459
|
balanceCurrency, setBalanceCurrency,
|
|
496
460
|
borderVisible, setBorderVisible,
|
|
497
461
|
} = props.signals
|
|
498
462
|
|
|
499
|
-
// ── reactive translation (follows
|
|
500
|
-
const t =
|
|
463
|
+
// ── reactive translation (follows langCode signal) ──
|
|
464
|
+
const t = createT(() => langCode())
|
|
501
465
|
|
|
502
466
|
// ── scan session messages reactively ──
|
|
503
467
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
@@ -510,7 +474,7 @@ function TokenCachePanel(props: {
|
|
|
510
474
|
// stable until the next successful computation arrives.
|
|
511
475
|
const [lastDist, setLastDist] = createSignal<TokenDist>({
|
|
512
476
|
system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0,
|
|
513
|
-
output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
|
|
477
|
+
output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0,
|
|
514
478
|
})
|
|
515
479
|
const [lastHasDist, setLastHasDist] = createSignal(false)
|
|
516
480
|
|
|
@@ -519,61 +483,16 @@ function TokenCachePanel(props: {
|
|
|
519
483
|
cost: 0, saved: 0, model: "", inputRate: 0, cacheReadRate: 0, cacheWriteRate: 0,
|
|
520
484
|
hasPricing: false, hasData: false, trend: 0, hasTrendData: false,
|
|
521
485
|
providerName: "", sessionHitRate: 0,
|
|
522
|
-
dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
|
|
486
|
+
dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0 },
|
|
523
487
|
hasDistData: false,
|
|
524
488
|
skills: [] as { name: string; tokens: number }[],
|
|
525
489
|
hasSkills: false,
|
|
526
490
|
})
|
|
527
491
|
const [refreshTick, setRefreshTick] = createSignal(0)
|
|
528
492
|
|
|
529
|
-
//
|
|
530
|
-
const [balanceState, setBalanceState] = createSignal<BalanceState>({
|
|
531
|
-
status: "idle", data: null, lastFetch: 0,
|
|
532
|
-
})
|
|
533
|
-
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
534
|
-
let balanceSeq = 0
|
|
535
|
-
|
|
536
|
-
// 当前 provider 显示名
|
|
493
|
+
// 当前 provider 显示名(余额查询状态为共享信号,见 PanelSignals.balanceState)
|
|
537
494
|
const providerName = createMemo(() => getBalanceProvider(balanceProviderId()).name)
|
|
538
495
|
|
|
539
|
-
const pollBalance = async () => {
|
|
540
|
-
const provider = getBalanceProvider(balanceProviderId())
|
|
541
|
-
// 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
|
|
542
|
-
const key = props.api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
543
|
-
|| findOpencodeKey(props.api, provider)
|
|
544
|
-
if (!key) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
545
|
-
const now = Date.now()
|
|
546
|
-
const prev = balanceState()
|
|
547
|
-
// key 已更换(重新输入)→ 强制重新查询,绕过缓存
|
|
548
|
-
if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS) return // cache still fresh
|
|
549
|
-
const seq = ++balanceSeq
|
|
550
|
-
setBalanceState({ ...prev, status: "loading", error: undefined, key })
|
|
551
|
-
const controller = new AbortController()
|
|
552
|
-
let timedOut = false
|
|
553
|
-
const timer = setTimeout(() => { timedOut = true; controller.abort() }, 10_000)
|
|
554
|
-
try {
|
|
555
|
-
const data = await provider.fetchBalance(key, controller.signal)
|
|
556
|
-
clearTimeout(timer)
|
|
557
|
-
if (seq !== balanceSeq) return // 已被更新的请求取代,丢弃过期结果
|
|
558
|
-
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key })
|
|
559
|
-
} catch (err) {
|
|
560
|
-
clearTimeout(timer)
|
|
561
|
-
if (seq !== balanceSeq) return
|
|
562
|
-
const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "")
|
|
563
|
-
// 失败时清空旧数据,避免显示过期余额
|
|
564
|
-
setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key })
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
569
|
-
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
570
|
-
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
571
|
-
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
572
|
-
createEffect(() => {
|
|
573
|
-
void balanceRefresh()
|
|
574
|
-
untrack(() => { void pollBalance() })
|
|
575
|
-
})
|
|
576
|
-
|
|
577
496
|
// 自动切换当前会话的 provider(前缀匹配)。手动切换会关闭此行为。
|
|
578
497
|
// 直接追踪 messages 取最后一条 assistant 消息的 providerID——
|
|
579
498
|
// 不依赖 session.model 的响应式更新(模型切换时该链路可能不触发重算)。
|
|
@@ -599,9 +518,15 @@ function TokenCachePanel(props: {
|
|
|
599
518
|
}
|
|
600
519
|
if (!pid) return
|
|
601
520
|
const hit = matchBalanceProvider(pid)
|
|
602
|
-
if (hit
|
|
603
|
-
|
|
604
|
-
|
|
521
|
+
if (hit) {
|
|
522
|
+
setBalanceUnsupported(false)
|
|
523
|
+
if (hit.id !== balanceProviderId()) {
|
|
524
|
+
setBalanceProviderId(hit.id)
|
|
525
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1)
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
// 当前提供商没有余额适配器 → 标记不支持,余额显示 N/A 并停止轮询
|
|
529
|
+
setBalanceUnsupported(true)
|
|
605
530
|
}
|
|
606
531
|
})
|
|
607
532
|
|
|
@@ -646,11 +571,11 @@ function TokenCachePanel(props: {
|
|
|
646
571
|
let prevMsgHitRate = -1, lastMsgHitRate = -1
|
|
647
572
|
for (const msg of msgs) {
|
|
648
573
|
if (msg.role !== "assistant") continue
|
|
649
|
-
const
|
|
650
|
-
const mit = num(
|
|
574
|
+
const tok = (msg as AssistantMessage).tokens; if (!tok) continue
|
|
575
|
+
const mit = num(tok.input) + num(tok.cache?.read) + num(tok.cache?.write), mrt = num(tok.cache?.read)
|
|
651
576
|
if (mit > 0) { prevMsgHitRate = lastMsgHitRate; lastMsgHitRate = (mrt / mit) * 100 }
|
|
652
577
|
if (fallbackTokens) {
|
|
653
|
-
input += num(
|
|
578
|
+
input += num(tok.input); read += num(tok.cache?.read); write += num(tok.cache?.write); output += num(tok.output)
|
|
654
579
|
}
|
|
655
580
|
if (fallbackCost) {
|
|
656
581
|
cost += num((msg as AssistantMessage).cost)
|
|
@@ -668,14 +593,15 @@ function TokenCachePanel(props: {
|
|
|
668
593
|
break
|
|
669
594
|
}
|
|
670
595
|
const hitRate = lastMsgHitRate >= 0 ? lastMsgHitRate : 0
|
|
671
|
-
|
|
596
|
+
// 总命中率分母含缓存写(业界口径:read / (input+read+write))
|
|
597
|
+
const freshTotal = input + read + write, sessionHitRate = freshTotal > 0 ? (read / freshTotal) * 100 : 0
|
|
672
598
|
const model = mid.split("/").pop() ?? mid, hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0
|
|
673
599
|
const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0
|
|
674
600
|
const trend = hasTrendData ? lastMsgHitRate - prevMsgHitRate : 0, providerName = pid || ""
|
|
675
601
|
|
|
676
602
|
// untrack 只包裹已知触发死锁的 API
|
|
677
603
|
const distData = untrack(() => {
|
|
678
|
-
let dist: TokenDist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 }
|
|
604
|
+
let dist: TokenDist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0 }
|
|
679
605
|
let hasDistData = false
|
|
680
606
|
const loadedSkills = new Map<string, { name: string; tokens: number }>()
|
|
681
607
|
try {
|
|
@@ -697,12 +623,20 @@ function TokenCachePanel(props: {
|
|
|
697
623
|
} else if (msg.role === "assistant") {
|
|
698
624
|
const am = msg as AssistantMessage
|
|
699
625
|
dist.output += num(am.tokens?.output)
|
|
626
|
+
dist.reasoning += num(am.tokens?.reasoning)
|
|
700
627
|
let parts: readonly Part[] = []; try { parts = props.api.state.part(msg.id) } catch {}
|
|
701
628
|
for (const p of parts) {
|
|
702
629
|
if (p.type === "tool") {
|
|
703
630
|
const tp = p as any; let rawInput = ""
|
|
704
631
|
try { rawInput = tp.state.raw ?? (tp.state.input != null ? JSON.stringify(tp.state.input) : "") } catch {}
|
|
705
632
|
if (rawInput) dist.toolCall += estimateTokens(rawInput)
|
|
633
|
+
// 子代理委托(task 工具):任务描述计入子代理指令(1.15.x 无 subtask part)
|
|
634
|
+
if (tp.tool === "task" && tp.state?.input) {
|
|
635
|
+
const ti = tp.state.input
|
|
636
|
+
const prompt = typeof ti.prompt === "string" ? ti.prompt : ""
|
|
637
|
+
const desc = typeof ti.description === "string" ? ti.description : ""
|
|
638
|
+
dist.agent += estimateTokens(prompt || desc)
|
|
639
|
+
}
|
|
706
640
|
if (tp.state.status === "completed") { const c = tp.state; if (c.output) dist.toolResult += estimateTokens(c.output) }
|
|
707
641
|
else if (tp.state.status === "error") { const e = tp.state; if (e.error) dist.toolResult += estimateTokens(e.error) }
|
|
708
642
|
if (tp.tool === "skill" && tp.state.status === "completed") {
|
|
@@ -723,21 +657,40 @@ function TokenCachePanel(props: {
|
|
|
723
657
|
}
|
|
724
658
|
}
|
|
725
659
|
}
|
|
726
|
-
} else if (p.type === "
|
|
727
|
-
else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
|
|
660
|
+
} else if (p.type === "subtask") { const sub = p as any; dist.agent += estimateTokens(sub.prompt || sub.description || "") }
|
|
728
661
|
}
|
|
729
662
|
}
|
|
730
663
|
}
|
|
731
664
|
// 从后往前找最后一条有 token 数据的 assistant 消息(避免取到 streaming 中未填充的消息)
|
|
732
665
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
733
666
|
if (msgs[i].role !== "assistant") continue
|
|
734
|
-
const
|
|
735
|
-
if (
|
|
667
|
+
const tok = (msgs[i] as AssistantMessage).tokens
|
|
668
|
+
if (tok && ((tok.input ?? 0) > 0 || (tok.cache?.read ?? 0) > 0 || (tok.cache?.write ?? 0) > 0)) { lastAssMsg = msgs[i] as AssistantMessage; break }
|
|
736
669
|
}
|
|
737
|
-
//
|
|
738
|
-
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read)
|
|
670
|
+
// 取最后一条有数据消息的总输入(含缓存读/写)作为当前 context 大小
|
|
671
|
+
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read) + num(lastAssMsg?.tokens?.cache?.write)
|
|
739
672
|
dist.apiOutput = num(lastAssMsg?.tokens?.output)
|
|
740
|
-
|
|
673
|
+
// 本回合(最后一条有数据消息所在的 parentID 链)的 API 调用次数与末次成本。
|
|
674
|
+
// opencode 将回合内每次工具调用循环拆为独立 assistant 消息(各含 1 个 step-finish),
|
|
675
|
+
// 故按 parentID 链聚合统计,而非单条消息。
|
|
676
|
+
if (lastAssMsg) {
|
|
677
|
+
const roundParent = (lastAssMsg as AssistantMessage).parentID
|
|
678
|
+
let lastCost: number | undefined
|
|
679
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
680
|
+
const m = msgs[i]
|
|
681
|
+
if (m.role !== "assistant") continue
|
|
682
|
+
if ((m as AssistantMessage).parentID !== roundParent) break
|
|
683
|
+
let parts: readonly Part[] = []; try { parts = props.api.state.part(m.id) } catch {}
|
|
684
|
+
for (const p of parts) {
|
|
685
|
+
if (p.type !== "step-finish") continue
|
|
686
|
+
dist.stepCount++
|
|
687
|
+
const sc = (p as { cost?: unknown }).cost
|
|
688
|
+
if (lastCost === undefined && typeof sc === "number" && Number.isFinite(sc)) lastCost = sc
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (lastCost !== undefined) dist.stepCost = lastCost
|
|
692
|
+
}
|
|
693
|
+
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0 || dist.reasoning > 0
|
|
741
694
|
} catch {}
|
|
742
695
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist()
|
|
743
696
|
const skills = [...loadedSkills.values()]
|
|
@@ -808,6 +761,7 @@ function TokenCachePanel(props: {
|
|
|
808
761
|
const savedProvider = props.api.kv.get<string>(`${KV_PREFIX}.balance.provider`)
|
|
809
762
|
if (typeof savedProvider === "string" && balanceProviders.some((p) => p.id === savedProvider)) {
|
|
810
763
|
setBalanceProviderId(savedProvider)
|
|
764
|
+
setBalanceUnsupported(false)
|
|
811
765
|
}
|
|
812
766
|
// Restore auto-switch (default on)
|
|
813
767
|
const savedAuto = props.api.kv.get<boolean>(`${KV_PREFIX}.balance.auto`)
|
|
@@ -828,11 +782,6 @@ function TokenCachePanel(props: {
|
|
|
828
782
|
setSectionBalance(Boolean(props.api.kv.get(`${KV_PREFIX}.section.balance`, true)))
|
|
829
783
|
const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
|
|
830
784
|
setBorderVisible(bv !== false)
|
|
831
|
-
// Restore language preference
|
|
832
|
-
const savedLang = props.api.kv.get<string>(`${KV_PREFIX}.lang`)
|
|
833
|
-
if (savedLang === "zh" || savedLang === "en") {
|
|
834
|
-
setLangZH(savedLang === "zh")
|
|
835
|
-
}
|
|
836
785
|
// Restore distribution snapshot so the token distribution block
|
|
837
786
|
// doesn't blank out while api.state.part() re-hydrates.
|
|
838
787
|
const cachedDist = props.api.kv.get<TokenDist>(`${KV_PREFIX}.dist_snapshot`)
|
|
@@ -880,8 +829,7 @@ function TokenCachePanel(props: {
|
|
|
880
829
|
const unsubMsg = props.api.event.on("message.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1) })
|
|
881
830
|
const unsubSession = props.api.event.on("session.updated", () => { setRefreshTick(v => v + 1) })
|
|
882
831
|
setRefreshTick(v => v + 1)
|
|
883
|
-
|
|
884
|
-
onCleanup(() => { clearTimeout(partTimer); clearInterval(balanceTimer); unsubPart(); unsubMsg(); unsubSession() })
|
|
832
|
+
onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg(); unsubSession() })
|
|
885
833
|
})
|
|
886
834
|
|
|
887
835
|
// ── colours ──
|
|
@@ -913,12 +861,14 @@ function TokenCachePanel(props: {
|
|
|
913
861
|
|
|
914
862
|
const sep = createMemo(() => "\u2500".repeat(Math.max(1, panelWidth() - gutter())))
|
|
915
863
|
function trendLabel(t: number): string {
|
|
916
|
-
|
|
864
|
+
// |t| < 0.05 视为无变化:避免显示 "↑0.0%" 的矛盾(箭头存在但数值截断为零)
|
|
865
|
+
if (Math.abs(t) < 0.05) return "-"
|
|
866
|
+
return (t > 0 ? "\u2191" : "\u2193") + Math.abs(t).toFixed(1) + "%"
|
|
917
867
|
}
|
|
918
868
|
|
|
919
869
|
const barW = createMemo(() => {
|
|
920
870
|
const trendSpace = data().hasTrendData ? LABEL_GAP + visualWidth(trendLabel(data().trend)) : 0
|
|
921
|
-
const overhead = visualWidth(t(
|
|
871
|
+
const overhead = visualWidth(t("hit")) + LABEL_GAP + BAR_BRACKETS + BAR_GAP + PCT_FIXED_WIDTH + trendSpace + gutter()
|
|
922
872
|
return Math.max(3, panelWidth() - overhead)
|
|
923
873
|
})
|
|
924
874
|
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
@@ -964,7 +914,7 @@ function TokenCachePanel(props: {
|
|
|
964
914
|
<text onMouseUp={() => setOpen((o) => { const n = !o; persistFold("open", n); return n })}>
|
|
965
915
|
<span style={{ fg: pal().muted }}>{open() ? "\u25bc " : "\u25b6 "}</span>
|
|
966
916
|
<span style={{ fg: pal().primary }}>
|
|
967
|
-
<b>{t()
|
|
917
|
+
<b>{t("title")}</b>
|
|
968
918
|
<Show when={open()}>
|
|
969
919
|
<span style={{ fg: dimColor(pal().muted, 0.75) }}> v{PLUGIN_VERSION}</span>
|
|
970
920
|
</Show>
|
|
@@ -972,18 +922,18 @@ function TokenCachePanel(props: {
|
|
|
972
922
|
<Show when={!open() && data().hasData}>
|
|
973
923
|
<Show when={data().hasTrendData}>
|
|
974
924
|
<span>
|
|
975
|
-
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t(
|
|
925
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t("title")) - visualWidth(pct() + " " + t("hitFolded") + " " + trendLabel(data().trend))))}
|
|
976
926
|
</span>
|
|
977
|
-
<span style={{ fg: hitColor() }}>{pct()} {t()
|
|
978
|
-
<span style={{ fg: data().trend
|
|
927
|
+
<span style={{ fg: hitColor() }}>{pct()} {t("hitFolded")}</span>
|
|
928
|
+
<span style={{ fg: Math.abs(data().trend) >= 0.05 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
979
929
|
{" "}{trendLabel(data().trend)}
|
|
980
930
|
</span>
|
|
981
931
|
</Show>
|
|
982
932
|
<Show when={!data().hasTrendData}>
|
|
983
933
|
<span>
|
|
984
|
-
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t(
|
|
934
|
+
{" ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t("title")) - visualWidth(pct() + " " + t("hitFolded"))))}
|
|
985
935
|
</span>
|
|
986
|
-
<span style={{ fg: hitColor() }}>{pct()} {t()
|
|
936
|
+
<span style={{ fg: hitColor() }}>{pct()} {t("hitFolded")}</span>
|
|
987
937
|
</Show>
|
|
988
938
|
</Show>
|
|
989
939
|
</text>
|
|
@@ -991,7 +941,7 @@ function TokenCachePanel(props: {
|
|
|
991
941
|
<Show when={open()}>
|
|
992
942
|
<Show when={props.signals.overrideSessionId()}>
|
|
993
943
|
{(() => {
|
|
994
|
-
const prefix = " \u21b3 " + (
|
|
944
|
+
const prefix = " \u21b3 " + t("subPrefix")
|
|
995
945
|
const maxSidW = Math.max(6, panelWidth() - visualWidth(prefix))
|
|
996
946
|
return (
|
|
997
947
|
<text>
|
|
@@ -1006,7 +956,7 @@ function TokenCachePanel(props: {
|
|
|
1006
956
|
<text fg={pal().muted}>{sep()}</text>
|
|
1007
957
|
<text>
|
|
1008
958
|
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1009
|
-
<span style={{ fg: pal().muted }}>{t()
|
|
959
|
+
<span style={{ fg: pal().muted }}>{t("noData")}</span>
|
|
1010
960
|
</text>
|
|
1011
961
|
</>
|
|
1012
962
|
}>
|
|
@@ -1014,11 +964,11 @@ function TokenCachePanel(props: {
|
|
|
1014
964
|
|
|
1015
965
|
{/* hit rate + bar — inline to avoid box spacing */}
|
|
1016
966
|
<text>
|
|
1017
|
-
<span style={{ fg: pal().text }}>{t()
|
|
967
|
+
<span style={{ fg: pal().text }}>{t("hit")} </span>
|
|
1018
968
|
<span style={{ fg: hitColor() }}>[{bar()}] </span>
|
|
1019
969
|
<span style={{ fg: pal().text }}>{pct()}</span>
|
|
1020
970
|
<Show when={data().hasTrendData}>
|
|
1021
|
-
<span style={{ fg: data().trend
|
|
971
|
+
<span style={{ fg: Math.abs(data().trend) >= 0.05 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }}>
|
|
1022
972
|
{" "}{trendLabel(data().trend)}
|
|
1023
973
|
</span>
|
|
1024
974
|
</Show>
|
|
@@ -1026,38 +976,45 @@ function TokenCachePanel(props: {
|
|
|
1026
976
|
|
|
1027
977
|
{/* session cumulative hit rate */}
|
|
1028
978
|
<text fg={pal().muted}>
|
|
1029
|
-
{justify(t()
|
|
979
|
+
{justify(t("totalHit"), (Math.floor(data().sessionHitRate * 10) / 10).toFixed(1) + "%")}
|
|
1030
980
|
</text>
|
|
1031
981
|
|
|
1032
982
|
{/* ── detail section (collapsible, default open) ── */}
|
|
1033
983
|
<Show when={sectionDetail()}>
|
|
1034
984
|
<text onMouseUp={() => setDetailOpen((o) => { const n = !o; persistFold("detail", n); return n })}>
|
|
1035
985
|
<span style={{ fg: pal().muted }}>{detailOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
1036
|
-
<span style={{ fg: pal().primary }}><b>{t()
|
|
1037
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + t(
|
|
986
|
+
<span style={{ fg: pal().primary }}><b>{t("secDetail")}</b></span>
|
|
987
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((detailOpen() ? "\u25bc " : "\u25b6 ") + t("secDetail")))}</span>
|
|
1038
988
|
</text>
|
|
1039
989
|
|
|
1040
990
|
<Show when={detailOpen()}>
|
|
1041
991
|
<Show when={data().read > 0}>
|
|
1042
992
|
<text fg={pal().muted}>
|
|
1043
|
-
{justify(t()
|
|
993
|
+
{justify(t("read"), fmt(data().read), t("tok"))}
|
|
1044
994
|
</text>
|
|
1045
995
|
</Show>
|
|
1046
996
|
<Show when={data().write > 0}>
|
|
1047
997
|
<text fg={pal().muted}>
|
|
1048
|
-
{justify(t()
|
|
998
|
+
{justify(t("write"), fmt(data().write), t("tok"))}
|
|
1049
999
|
</text>
|
|
1050
1000
|
</Show>
|
|
1001
|
+
{/* 未命中 = 新鲜输入 + 缓存写(两者都未从缓存命中) */}
|
|
1051
1002
|
<text fg={pal().muted}>
|
|
1052
|
-
{justify(t()
|
|
1003
|
+
{justify(t("miss"), fmt(data().freshInput + data().write), t("tok"))}
|
|
1053
1004
|
</text>
|
|
1054
1005
|
<text fg={pal().muted}>
|
|
1055
|
-
{justify(t()
|
|
1006
|
+
{justify(t("out"), fmt(data().output), t("tok"))}
|
|
1056
1007
|
</text>
|
|
1008
|
+
{/* 本回合多次 API 调用时才显示调用次数与末次成本(单次调用不占行) */}
|
|
1009
|
+
<Show when={data().dist.stepCount >= 2}>
|
|
1010
|
+
<text fg={pal().muted}>
|
|
1011
|
+
{justify(t("stepsCount", { n: data().dist.stepCount }), fmtCost(data().dist.stepCost, currencySymbol(), exchangeRate()))}
|
|
1012
|
+
</text>
|
|
1013
|
+
</Show>
|
|
1057
1014
|
<Show when={data().saved > 0}>
|
|
1058
1015
|
<text>
|
|
1059
|
-
<span style={{ fg: pal().muted }}>{t()
|
|
1060
|
-
<span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(t(
|
|
1016
|
+
<span style={{ fg: pal().muted }}>{t("saved")}</span>
|
|
1017
|
+
<span>{" ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(t("saved")) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate()))))}</span>
|
|
1061
1018
|
<span style={{ fg: pal().success }}>~{fmtCost(data().saved, currencySymbol(), exchangeRate())}</span>
|
|
1062
1019
|
</text>
|
|
1063
1020
|
</Show>
|
|
@@ -1068,34 +1025,34 @@ function TokenCachePanel(props: {
|
|
|
1068
1025
|
<Show when={sectionModel()}>
|
|
1069
1026
|
{<text onMouseUp={() => setModelOpen((o) => { const n = !o; persistFold("model", n); return n })}>
|
|
1070
1027
|
<span style={{ fg: pal().muted }}>{modelOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
1071
|
-
<span style={{ fg: pal().primary }}><b>{t()
|
|
1072
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t(
|
|
1028
|
+
<span style={{ fg: pal().primary }}><b>{t("secModel")}</b></span>
|
|
1029
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t("secModel")))}</span>
|
|
1073
1030
|
</text>}
|
|
1074
1031
|
|
|
1075
1032
|
<Show when={modelOpen()}>
|
|
1076
1033
|
<text fg={pal().text}>
|
|
1077
|
-
{justify(t()
|
|
1034
|
+
{justify(t("cost"), fmtCost(data().cost, currencySymbol(), exchangeRate()))}
|
|
1078
1035
|
</text>
|
|
1079
1036
|
<Show when={data().providerName}>
|
|
1080
1037
|
<text fg={pal().muted}>
|
|
1081
|
-
{justify(t()
|
|
1038
|
+
{justify(t("provider"), data().providerName)}
|
|
1082
1039
|
</text>
|
|
1083
1040
|
</Show>
|
|
1084
1041
|
<text fg={pal().muted}>
|
|
1085
|
-
{justify(t()
|
|
1042
|
+
{justify(t("model"), data().model)}
|
|
1086
1043
|
</text>
|
|
1087
1044
|
<Show when={data().hasPricing}>
|
|
1088
1045
|
<text fg={pal().muted}>
|
|
1089
|
-
{justify(t()
|
|
1046
|
+
{justify(t("rate"), currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t("inputRate"))}
|
|
1090
1047
|
</text>
|
|
1091
1048
|
<Show when={data().cacheReadRate > 0}>
|
|
1092
1049
|
<text fg={pal().muted}>
|
|
1093
|
-
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t(
|
|
1050
|
+
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t("cacheRate"))}
|
|
1094
1051
|
</text>
|
|
1095
1052
|
</Show>
|
|
1096
1053
|
<Show when={data().cacheWriteRate > 0}>
|
|
1097
1054
|
<text fg={pal().muted}>
|
|
1098
|
-
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t(
|
|
1055
|
+
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t("writeRate"))}
|
|
1099
1056
|
</text>
|
|
1100
1057
|
</Show>
|
|
1101
1058
|
</Show>
|
|
@@ -1107,38 +1064,40 @@ function TokenCachePanel(props: {
|
|
|
1107
1064
|
<Show when={data().hasDistData}>
|
|
1108
1065
|
{<text onMouseUp={() => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n })}>
|
|
1109
1066
|
<span style={{ fg: pal().muted }}>{distOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
1110
|
-
<span style={{ fg: pal().primary }}><b>{t()
|
|
1111
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t(
|
|
1067
|
+
<span style={{ fg: pal().primary }}><b>{t("distTitle")}</b></span>
|
|
1068
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t("distTitle")))}</span>
|
|
1112
1069
|
</text>}
|
|
1113
1070
|
<Show when={distOpen()}>
|
|
1114
1071
|
<Show when={data().dist.system > 0}>
|
|
1115
1072
|
<text fg={pal().muted}>
|
|
1116
|
-
{justify(t()
|
|
1073
|
+
{justify(t("distSys"), fmt(data().dist.system), t("tok"))}
|
|
1117
1074
|
</text>
|
|
1118
1075
|
</Show>
|
|
1119
1076
|
<Show when={data().dist.user > 0}>
|
|
1120
1077
|
<text fg={pal().muted}>
|
|
1121
|
-
{justify(t()
|
|
1078
|
+
{justify(t("distUser"), fmt(data().dist.user), t("tok"))}
|
|
1122
1079
|
</text>
|
|
1123
1080
|
</Show>
|
|
1124
1081
|
<Show when={data().dist.agent > 0}>
|
|
1125
1082
|
<text fg={pal().muted}>
|
|
1126
|
-
{justify(t()
|
|
1083
|
+
{justify(t("distAgent"), fmt(data().dist.agent), t("tok"))}
|
|
1127
1084
|
</text>
|
|
1128
1085
|
</Show>
|
|
1129
1086
|
<Show when={data().dist.toolCall > 0}>
|
|
1130
1087
|
<text fg={pal().muted}>
|
|
1131
|
-
{justify(t()
|
|
1088
|
+
{justify(t("distTool"), fmt(data().dist.toolCall), t("tok"))}
|
|
1132
1089
|
</text>
|
|
1133
1090
|
</Show>
|
|
1134
1091
|
<Show when={data().dist.toolResult > 0}>
|
|
1135
1092
|
<text fg={pal().muted}>
|
|
1136
|
-
{justify(t()
|
|
1093
|
+
{justify(t("distRes"), fmt(data().dist.toolResult), t("tok"))}
|
|
1094
|
+
</text>
|
|
1095
|
+
</Show>
|
|
1096
|
+
<Show when={data().dist.reasoning > 0}>
|
|
1097
|
+
<text fg={pal().muted}>
|
|
1098
|
+
{justify(t("distReason"), fmt(data().dist.reasoning), t("tok"))}
|
|
1137
1099
|
</text>
|
|
1138
1100
|
</Show>
|
|
1139
|
-
<text fg={pal().text}>
|
|
1140
|
-
{justify(t().distTotal, fmt(data().dist.apiInput), t().tok)}
|
|
1141
|
-
</text>
|
|
1142
1101
|
</Show>
|
|
1143
1102
|
</Show>
|
|
1144
1103
|
</Show>
|
|
@@ -1148,18 +1107,18 @@ function TokenCachePanel(props: {
|
|
|
1148
1107
|
<Show when={data().hasSkills}>
|
|
1149
1108
|
{<text onMouseUp={() => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n })}>
|
|
1150
1109
|
<span style={{ fg: pal().muted }}>{skillsOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
1151
|
-
<span style={{ fg: pal().primary }}><b>{t()
|
|
1110
|
+
<span style={{ fg: pal().primary }}><b>{t("secSkills")}</b></span>
|
|
1152
1111
|
<span style={{ fg: pal().muted }}> ({data().skills.length})</span>
|
|
1153
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t()
|
|
1112
|
+
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t("secSkills") + ` (${data().skills.length})`))}</span>
|
|
1154
1113
|
</text>}
|
|
1155
1114
|
<Show when={skillsOpen()}>
|
|
1156
1115
|
{data().skills.map((sk: { name: string; tokens: number }) => {
|
|
1157
|
-
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t(
|
|
1116
|
+
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t("tok"))
|
|
1158
1117
|
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1)
|
|
1159
1118
|
const label = truncateVisual(sk.name, maxLabel)
|
|
1160
1119
|
return (
|
|
1161
1120
|
<text fg={pal().muted}>
|
|
1162
|
-
{justify(label, fmt(sk.tokens), t(
|
|
1121
|
+
{justify(label, fmt(sk.tokens), t("tok"))}
|
|
1163
1122
|
</text>
|
|
1164
1123
|
)
|
|
1165
1124
|
})}
|
|
@@ -1167,62 +1126,46 @@ function TokenCachePanel(props: {
|
|
|
1167
1126
|
</Show>
|
|
1168
1127
|
</Show>
|
|
1169
1128
|
|
|
1170
|
-
{/* ──
|
|
1129
|
+
{/* ── provider balance (single line) ── */}
|
|
1171
1130
|
<Show when={sectionBalance()}>
|
|
1172
1131
|
<text fg={pal().muted}>{sep()}</text>
|
|
1173
|
-
<Show when={
|
|
1132
|
+
<Show when={balanceUnsupported()}>
|
|
1174
1133
|
<text fg={pal().muted}>
|
|
1175
1134
|
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1176
|
-
<span>{t(
|
|
1135
|
+
<span>{t("balUnsupported")}</span>
|
|
1177
1136
|
</text>
|
|
1178
1137
|
</Show>
|
|
1179
|
-
<Show when={
|
|
1180
|
-
<
|
|
1181
|
-
<
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
<
|
|
1187
|
-
<
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
// 非原生币种(EUR/JPY/GBP/KRW…)→ 取第一条余额按汇率换算
|
|
1212
|
-
const base = list[0]
|
|
1213
|
-
const baseAmt = parseFloat(base.total)
|
|
1214
|
-
const converted = Number.isFinite(baseAmt)
|
|
1215
|
-
? convertBalance(pref || base.currency, exchangeRate(), baseAmt, base.currency)
|
|
1216
|
-
: baseAmt
|
|
1217
|
-
const shown = pref && base.currency !== pref
|
|
1218
|
-
? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
|
|
1219
|
-
: base.total
|
|
1220
|
-
return (
|
|
1221
|
-
<text fg={pal().text}>
|
|
1222
|
-
{justify(t().balTotal, balanceSymbol(pref || base.currency) + shown)}
|
|
1223
|
-
</text>
|
|
1224
|
-
)
|
|
1225
|
-
})()}
|
|
1138
|
+
<Show when={!balanceUnsupported()}>
|
|
1139
|
+
<Show when={balanceState().status === "idle"}>
|
|
1140
|
+
<text fg={pal().muted}>
|
|
1141
|
+
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1142
|
+
<span>{t("balNoKey", { p: providerName() })}</span>
|
|
1143
|
+
</text>
|
|
1144
|
+
</Show>
|
|
1145
|
+
<Show when={balanceState().status === "loading"}>
|
|
1146
|
+
<text fg={pal().muted}>
|
|
1147
|
+
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1148
|
+
<span>{t("balLoading")}</span>
|
|
1149
|
+
</text>
|
|
1150
|
+
</Show>
|
|
1151
|
+
<Show when={balanceState().status === "error"}>
|
|
1152
|
+
<text fg={pal().error}>
|
|
1153
|
+
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1154
|
+
<span>{(() => {
|
|
1155
|
+
const code = balanceState().error
|
|
1156
|
+
if (code === "401") return t("balErr401")
|
|
1157
|
+
if (code === "403") return t("balErr403")
|
|
1158
|
+
if (code === "EMPTY") return t("balErrEmpty")
|
|
1159
|
+
if (code === "TIMEOUT") return t("balErrTimeout")
|
|
1160
|
+
return t("balError") + (code ? ` (${code})` : "")
|
|
1161
|
+
})()}</span>
|
|
1162
|
+
</text>
|
|
1163
|
+
</Show>
|
|
1164
|
+
<Show when={balanceState().status === "ok" && balanceState().data}>
|
|
1165
|
+
<text fg={pal().text}>
|
|
1166
|
+
{justify(t("balTotal"), formatBalanceText(balanceState().data!, balanceCurrency(), exchangeRate()))}
|
|
1167
|
+
</text>
|
|
1168
|
+
</Show>
|
|
1226
1169
|
</Show>
|
|
1227
1170
|
</Show>
|
|
1228
1171
|
</Show>
|
|
@@ -1235,6 +1178,166 @@ function TokenCachePanel(props: {
|
|
|
1235
1178
|
// Plugin entry
|
|
1236
1179
|
// ---------------------------------------------------------------------------
|
|
1237
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* 输入框 hint 行(session_prompt slot 的 hint):单行显示 路径 · 命中率 · 余额 · Tokens。
|
|
1183
|
+
* 通过 ui.Prompt 的 hint prop 注入——宿主右侧的 token/commands 提示自动保留,
|
|
1184
|
+
* 三合一信息与路径同行显示在中间位置。
|
|
1185
|
+
*/
|
|
1186
|
+
function BottomStatusBar(props: { api: TuiPluginApi; signals: PanelSignals; sessionId: string }): JSX.Element {
|
|
1187
|
+
const KV_PREFIX = "cache_panel"
|
|
1188
|
+
const t = createT(() => props.signals.langCode())
|
|
1189
|
+
|
|
1190
|
+
const sid = props.sessionId
|
|
1191
|
+
|
|
1192
|
+
// ── 命中率(单条口径:最后一条有 token 的 assistant 消息)+ token 汇总 ──
|
|
1193
|
+
const stats = createMemo(() => {
|
|
1194
|
+
const id = sid
|
|
1195
|
+
if (!id) return null
|
|
1196
|
+
const msgs = props.api.state.session.messages(id) as Message[]
|
|
1197
|
+
const session = typeof props.api.state.session.get === "function"
|
|
1198
|
+
? props.api.state.session.get(id)
|
|
1199
|
+
: undefined
|
|
1200
|
+
let input = session?.tokens?.input ?? 0
|
|
1201
|
+
let read = session?.tokens?.cache?.read ?? 0
|
|
1202
|
+
let write = session?.tokens?.cache?.write ?? 0
|
|
1203
|
+
// 旧 SDK 无 session 聚合字段 → 遍历消息累加(与侧边栏 fallback 一致)
|
|
1204
|
+
if (session?.tokens == null) {
|
|
1205
|
+
for (const m of msgs) {
|
|
1206
|
+
if (m.role !== "assistant") continue
|
|
1207
|
+
const tk = (m as AssistantMessage).tokens
|
|
1208
|
+
if (!tk) continue
|
|
1209
|
+
input += num(tk.input)
|
|
1210
|
+
read += num(tk.cache?.read)
|
|
1211
|
+
write += num(tk.cache?.write)
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
// 从后往前取最后两条有 token 数据的 assistant 消息 → 单条命中率 + 趋势
|
|
1215
|
+
// 分母含缓存写(业界口径:read / (input+read+write))
|
|
1216
|
+
let hitRate = -1, prevHitRate = -1
|
|
1217
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1218
|
+
const m = msgs[i]
|
|
1219
|
+
if (m.role !== "assistant") continue
|
|
1220
|
+
const tk = (m as AssistantMessage).tokens
|
|
1221
|
+
if (!tk) continue
|
|
1222
|
+
const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write)
|
|
1223
|
+
const mrt = num(tk.cache?.read)
|
|
1224
|
+
if (mit <= 0) continue
|
|
1225
|
+
const rate = (mrt / mit) * 100
|
|
1226
|
+
if (hitRate < 0) { hitRate = rate; continue }
|
|
1227
|
+
prevHitRate = rate
|
|
1228
|
+
break
|
|
1229
|
+
}
|
|
1230
|
+
return { hitRate, prevHitRate, input, read, write }
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
// 余额查询状态为共享信号(PanelSignals.balanceState),由 tui() 统一轮询
|
|
1234
|
+
|
|
1235
|
+
// 自动切换 provider(跟随当前会话模型;幂等,与侧边栏共享信号)
|
|
1236
|
+
createEffect(() => {
|
|
1237
|
+
if (!props.signals.autoBalance()) return
|
|
1238
|
+
const id = sid
|
|
1239
|
+
if (!id) return
|
|
1240
|
+
const msgs = props.api.state.session.messages(id) as Message[]
|
|
1241
|
+
let pid = ""
|
|
1242
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1243
|
+
const m = msgs[i]
|
|
1244
|
+
if (m.role === "assistant" && (m as AssistantMessage).providerID) { pid = (m as AssistantMessage).providerID; break }
|
|
1245
|
+
}
|
|
1246
|
+
if (!pid) {
|
|
1247
|
+
try { pid = props.api.state.session.get(id)?.model?.providerID ?? "" } catch {}
|
|
1248
|
+
}
|
|
1249
|
+
if (!pid) return
|
|
1250
|
+
const hit = matchBalanceProvider(pid)
|
|
1251
|
+
if (hit) {
|
|
1252
|
+
props.signals.setBalanceUnsupported(false)
|
|
1253
|
+
if (hit.id !== props.signals.balanceProviderId()) {
|
|
1254
|
+
props.signals.setBalanceProviderId(hit.id)
|
|
1255
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1)
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
// 当前提供商没有余额适配器 → 标记不支持,余额显示 N/A 并停止轮询
|
|
1259
|
+
props.signals.setBalanceUnsupported(true)
|
|
1260
|
+
}
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
// ── 主题色(与侧边栏同口径)──
|
|
1264
|
+
const pal = createMemo(() => {
|
|
1265
|
+
const th = props.api.theme.current as Record<string, unknown>
|
|
1266
|
+
const sat = (k: string, fb: string) => desaturateTo(th[k], MAX_SAT, fb)
|
|
1267
|
+
return {
|
|
1268
|
+
text: sat("text", FALLBACK.text),
|
|
1269
|
+
muted: sat("textMuted", FALLBACK.muted),
|
|
1270
|
+
success: sat("success", FALLBACK.success),
|
|
1271
|
+
warning: sat("warning", FALLBACK.warning),
|
|
1272
|
+
error: sat("error", FALLBACK.error),
|
|
1273
|
+
}
|
|
1274
|
+
})
|
|
1275
|
+
|
|
1276
|
+
const hitColor = createMemo(() => {
|
|
1277
|
+
const r = stats()?.hitRate ?? -1
|
|
1278
|
+
if (r >= 85) return pal().success
|
|
1279
|
+
if (r >= 70) return pal().warning
|
|
1280
|
+
return pal().error
|
|
1281
|
+
})
|
|
1282
|
+
|
|
1283
|
+
// 命中率趋势:最后一条与上一条的差值;|Δ| < 0.05 视为无变化(null = 不显示)
|
|
1284
|
+
const trend = createMemo(() => {
|
|
1285
|
+
const s = stats()
|
|
1286
|
+
if (!s || s.prevHitRate < 0 || s.hitRate < 0) return null
|
|
1287
|
+
const d = s.hitRate - s.prevHitRate
|
|
1288
|
+
return Math.abs(d) < 0.05 ? null : d
|
|
1289
|
+
})
|
|
1290
|
+
|
|
1291
|
+
const balanceText = createMemo(() => {
|
|
1292
|
+
const s = props.signals.balanceState()
|
|
1293
|
+
if (s.status === "ok" && s.data) return formatBalanceText(s.data, props.signals.balanceCurrency(), props.signals.exchangeRate())
|
|
1294
|
+
if (s.status === "loading") return "\u2026"
|
|
1295
|
+
if (s.status === "error") return "\u26a0"
|
|
1296
|
+
return "-"
|
|
1297
|
+
})
|
|
1298
|
+
|
|
1299
|
+
// 路径显示(替换宿主默认 hint 左侧的 cwd 文本)
|
|
1300
|
+
const directory = createMemo(() => {
|
|
1301
|
+
try { return props.api.state.path.directory } catch { return "" }
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
// 恢复显隐偏好(默认显示);关闭时回退为仅显示路径,与宿主默认 hint 行一致
|
|
1305
|
+
onMount(() => {
|
|
1306
|
+
try {
|
|
1307
|
+
const v = props.api.kv.get<boolean>(`${KV_PREFIX}.section.bottom`, true)
|
|
1308
|
+
props.signals.setSectionBottom(v !== false)
|
|
1309
|
+
} catch {}
|
|
1310
|
+
})
|
|
1311
|
+
|
|
1312
|
+
return (
|
|
1313
|
+
<Show when={props.signals.sectionBottom()} fallback={<text fg={pal().muted}>{directory()}</text>}>
|
|
1314
|
+
<box marginLeft={1} flexGrow={1} flexShrink={0} flexDirection="row" justifyContent="space-between">
|
|
1315
|
+
<text fg={pal().muted}>{directory()}</text>
|
|
1316
|
+
<box flexDirection="row">
|
|
1317
|
+
<text>
|
|
1318
|
+
<span style={{ fg: pal().muted }}>{t("barHit")} </span>
|
|
1319
|
+
<span style={{ fg: hitColor() }}>{(stats()?.hitRate ?? -1) >= 0 ? (Math.floor(stats()!.hitRate * 10) / 10).toFixed(1) + "%" : "--"}</span>
|
|
1320
|
+
<Show when={trend() !== null}>
|
|
1321
|
+
<span style={{ fg: trend()! > 0 ? pal().success : pal().error }}>
|
|
1322
|
+
{" " + (trend()! > 0 ? "\u2191" : "\u2193") + Math.abs(trend()!).toFixed(1) + "%"}
|
|
1323
|
+
</span>
|
|
1324
|
+
</Show>
|
|
1325
|
+
<span style={{ fg: pal().muted }}>{" \u00b7 " + t("barTok") + " "}</span>
|
|
1326
|
+
<span style={{ fg: pal().text }}>
|
|
1327
|
+
{stats() ? fmtCompact(stats()!.input + stats()!.read + stats()!.write) : "--"}
|
|
1328
|
+
</span>
|
|
1329
|
+
<Show when={!props.signals.balanceUnsupported()}>
|
|
1330
|
+
<span style={{ fg: pal().muted }}>{" \u00b7 " + t("barBal") + " "}</span>
|
|
1331
|
+
<span style={{ fg: pal().text }}>{balanceText()}</span>
|
|
1332
|
+
</Show>
|
|
1333
|
+
</text>
|
|
1334
|
+
<text fg={pal().muted}>{" \u00b7 "}</text>
|
|
1335
|
+
</box>
|
|
1336
|
+
</box>
|
|
1337
|
+
</Show>
|
|
1338
|
+
)
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1238
1341
|
function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin {
|
|
1239
1342
|
let lastSlotSid = ""
|
|
1240
1343
|
return {
|
|
@@ -1271,26 +1374,39 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1271
1374
|
const [sectionDist, setSectionDist] = createSignal(true)
|
|
1272
1375
|
const [sectionSkills, setSectionSkills] = createSignal(true)
|
|
1273
1376
|
const [sectionBalance, setSectionBalance] = createSignal(true)
|
|
1377
|
+
const [sectionBottom, setSectionBottom] = createSignal(true)
|
|
1274
1378
|
const [balanceRefresh, setBalanceRefresh] = createSignal(0)
|
|
1275
1379
|
const [balanceProviderId, setBalanceProviderId] = createSignal("deepseek")
|
|
1276
1380
|
const [autoBalance, setAutoBalance] = createSignal(true)
|
|
1381
|
+
const [balanceUnsupported, setBalanceUnsupported] = createSignal(false)
|
|
1277
1382
|
const [balanceCurrency, setBalanceCurrency] = createSignal("")
|
|
1278
1383
|
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
1279
|
-
const [
|
|
1384
|
+
const [langCode, setLangCode] = createSignal<LangCode>(INIT_LANG)
|
|
1280
1385
|
const [overrideSessionId, setOverrideSessionId] = createSignal<string | undefined>(undefined)
|
|
1281
1386
|
|
|
1387
|
+
// ── 余额查询状态(共享):侧边栏与底部栏读同一份数据,
|
|
1388
|
+
// 避免重复请求导致两处余额不一致 ──
|
|
1389
|
+
const [balanceState, setBalanceState] = createSignal<BalanceState>({
|
|
1390
|
+
status: "idle", data: null, lastFetch: 0,
|
|
1391
|
+
})
|
|
1392
|
+
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
1393
|
+
let balanceSeq = 0
|
|
1394
|
+
|
|
1282
1395
|
const signals: PanelSignals = {
|
|
1283
1396
|
currencySymbol, setCurrencySymbol,
|
|
1284
1397
|
exchangeRate, setExchangeRate,
|
|
1285
|
-
|
|
1398
|
+
langCode, setLangCode,
|
|
1286
1399
|
sectionDetail, setSectionDetail,
|
|
1287
1400
|
sectionModel, setSectionModel,
|
|
1288
1401
|
sectionDist, setSectionDist,
|
|
1289
1402
|
sectionSkills, setSectionSkills,
|
|
1290
1403
|
sectionBalance, setSectionBalance,
|
|
1404
|
+
sectionBottom, setSectionBottom,
|
|
1291
1405
|
balanceRefresh, setBalanceRefresh,
|
|
1292
1406
|
balanceProviderId, setBalanceProviderId,
|
|
1293
1407
|
autoBalance, setAutoBalance,
|
|
1408
|
+
balanceUnsupported, setBalanceUnsupported,
|
|
1409
|
+
balanceState,
|
|
1294
1410
|
balanceCurrency, setBalanceCurrency,
|
|
1295
1411
|
borderVisible, setBorderVisible,
|
|
1296
1412
|
overrideSessionId, setOverrideSessionId,
|
|
@@ -1298,31 +1414,120 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1298
1414
|
|
|
1299
1415
|
api.slots.register(createSidebarSlot(api, signals))
|
|
1300
1416
|
|
|
1417
|
+
// 输入框 hint 行(session_prompt slot,replace 模式):
|
|
1418
|
+
// 用宿主同一 Prompt 组件重渲染输入框,仅替换 hint 行左侧——
|
|
1419
|
+
// 在路径与右侧 token/commands 提示之间插入 命中率 · 余额 · Tokens。
|
|
1420
|
+
api.slots.register({
|
|
1421
|
+
order: 55,
|
|
1422
|
+
slots: {
|
|
1423
|
+
session_prompt(
|
|
1424
|
+
_ctx: TuiSlotContext,
|
|
1425
|
+
input: {
|
|
1426
|
+
session_id: string
|
|
1427
|
+
visible?: boolean
|
|
1428
|
+
disabled?: boolean
|
|
1429
|
+
on_submit?: () => void
|
|
1430
|
+
ref?: (ref: TuiPromptRef | undefined) => void
|
|
1431
|
+
},
|
|
1432
|
+
): JSX.Element {
|
|
1433
|
+
return (
|
|
1434
|
+
<api.ui.Prompt
|
|
1435
|
+
sessionID={input.session_id}
|
|
1436
|
+
visible={input.visible}
|
|
1437
|
+
disabled={input.disabled}
|
|
1438
|
+
onSubmit={input.on_submit}
|
|
1439
|
+
ref={input.ref}
|
|
1440
|
+
hint={<BottomStatusBar api={api} signals={signals} sessionId={input.session_id} />}
|
|
1441
|
+
/>
|
|
1442
|
+
)
|
|
1443
|
+
},
|
|
1444
|
+
},
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1301
1447
|
// ── slash commands for runtime config ──
|
|
1302
1448
|
const KV_PREFIX = "cache_panel"
|
|
1303
1449
|
|
|
1450
|
+
// ── 语言偏好恢复:KV 就绪后优先用户设置(/cache-lang),覆盖自动识别 ──
|
|
1451
|
+
const restoreLang = () => {
|
|
1452
|
+
try {
|
|
1453
|
+
const saved = api.kv.get<string>(`${KV_PREFIX}.lang`)
|
|
1454
|
+
if (saved && LANG_META.some((m) => m.code === saved)) setLangCode(saved as LangCode)
|
|
1455
|
+
} catch {}
|
|
1456
|
+
}
|
|
1457
|
+
if (api.kv.ready) {
|
|
1458
|
+
restoreLang()
|
|
1459
|
+
} else {
|
|
1460
|
+
const langTimer = setInterval(() => {
|
|
1461
|
+
if (api.kv.ready) { clearInterval(langTimer); restoreLang() }
|
|
1462
|
+
}, 10)
|
|
1463
|
+
api.lifecycle.onDispose(() => clearInterval(langTimer))
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const pollBalance = async () => {
|
|
1467
|
+
const provider = getBalanceProvider(balanceProviderId())
|
|
1468
|
+
// 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
|
|
1469
|
+
const key = api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1470
|
+
|| findOpencodeKey(api, provider)
|
|
1471
|
+
if (balanceUnsupported()) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
1472
|
+
if (!key) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
1473
|
+
const now = Date.now()
|
|
1474
|
+
const prev = balanceState()
|
|
1475
|
+
// key 已更换(重新输入)→ 强制重新查询,绕过缓存
|
|
1476
|
+
if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS) return // cache still fresh
|
|
1477
|
+
const seq = ++balanceSeq
|
|
1478
|
+
setBalanceState({ ...prev, status: "loading", error: undefined, key })
|
|
1479
|
+
const controller = new AbortController()
|
|
1480
|
+
let timedOut = false
|
|
1481
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort() }, 10_000)
|
|
1482
|
+
try {
|
|
1483
|
+
const data = await provider.fetchBalance(key, controller.signal)
|
|
1484
|
+
clearTimeout(timer)
|
|
1485
|
+
if (seq !== balanceSeq) return // 已被更新的请求取代,丢弃过期结果
|
|
1486
|
+
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key })
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
clearTimeout(timer)
|
|
1489
|
+
if (seq !== balanceSeq) return
|
|
1490
|
+
const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "")
|
|
1491
|
+
// 失败时清空旧数据,避免显示过期余额
|
|
1492
|
+
setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key })
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
1497
|
+
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
1498
|
+
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
1499
|
+
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
1500
|
+
createEffect(() => {
|
|
1501
|
+
void balanceRefresh()
|
|
1502
|
+
untrack(() => { void pollBalance() })
|
|
1503
|
+
})
|
|
1504
|
+
|
|
1505
|
+
// 定时轮询(5 分钟);随插件生命周期清理
|
|
1506
|
+
const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS)
|
|
1507
|
+
api.lifecycle.onDispose(() => clearInterval(balanceTimer))
|
|
1508
|
+
|
|
1304
1509
|
/** 菜单中 provider 选项标题:标注 key 来源(手动配置 / OpenCode 自动复用 / 未配置)。 */
|
|
1305
1510
|
const providerOptionTitle = (p: BalanceProvider, current?: string) => {
|
|
1306
|
-
const
|
|
1511
|
+
const t = createT(() => langCode())
|
|
1307
1512
|
const hasManual = !!api.kv.get<string>(`${KV_PREFIX}.balance.${p.id}.key`, "")
|
|
1308
1513
|
const hasAuto = !hasManual && !!findOpencodeKey(api, p)
|
|
1309
1514
|
const mark = hasManual
|
|
1310
|
-
? (
|
|
1515
|
+
? t("keyUser")
|
|
1311
1516
|
: hasAuto
|
|
1312
|
-
? (
|
|
1313
|
-
: (
|
|
1517
|
+
? t("keyOpenCode")
|
|
1518
|
+
: t("keyNotSet")
|
|
1314
1519
|
return p.name + mark + (current && p.id === current ? " *" : "")
|
|
1315
1520
|
}
|
|
1316
1521
|
|
|
1317
1522
|
/** 弹出指定 provider 的 API Key 输入框(脱敏预填;空清除 / 含 * 保留原 key / 新 key 实时刷新)。 */
|
|
1318
1523
|
const promptBalanceKey = (dialog: TuiDialogStack | undefined, provider: BalanceProvider) => {
|
|
1319
|
-
const
|
|
1524
|
+
const t = createT(() => langCode())
|
|
1320
1525
|
const current = api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1321
1526
|
const masked = maskKey(current)
|
|
1322
1527
|
dialog?.replace(() => (
|
|
1323
1528
|
<api.ui.DialogPrompt
|
|
1324
1529
|
title={provider.name}
|
|
1325
|
-
description={() => <text>{
|
|
1530
|
+
description={() => <text>{t("balKeyPrompt", { p: provider.name })}</text>}
|
|
1326
1531
|
placeholder={provider.keyPlaceholder ?? "sk-..."}
|
|
1327
1532
|
value={masked}
|
|
1328
1533
|
onConfirm={(val) => {
|
|
@@ -1338,9 +1543,9 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1338
1543
|
api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key)
|
|
1339
1544
|
setBalanceRefresh(v => v + 1)
|
|
1340
1545
|
if (key) {
|
|
1341
|
-
api.ui.toast({ message:
|
|
1546
|
+
api.ui.toast({ message: t("keySaved") })
|
|
1342
1547
|
} else {
|
|
1343
|
-
api.ui.toast({ message:
|
|
1548
|
+
api.ui.toast({ message: t("keyCleared") })
|
|
1344
1549
|
}
|
|
1345
1550
|
dialog?.clear()
|
|
1346
1551
|
}}
|
|
@@ -1364,6 +1569,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1364
1569
|
value: code,
|
|
1365
1570
|
}))}
|
|
1366
1571
|
onSelect={(opt) => {
|
|
1572
|
+
const t = createT(() => langCode())
|
|
1367
1573
|
const sym = CURRENCIES[opt.value] ?? "$"
|
|
1368
1574
|
const defRate = DEFAULT_RATES[opt.value] ?? 1
|
|
1369
1575
|
api.kv.set(`${KV_PREFIX}.currency`, sym)
|
|
@@ -1373,7 +1579,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1373
1579
|
signals.setBalanceCurrency(opt.value)
|
|
1374
1580
|
signals.setCurrencySymbol(sym)
|
|
1375
1581
|
signals.setExchangeRate(defRate)
|
|
1376
|
-
api.ui.toast({ message:
|
|
1582
|
+
api.ui.toast({ message: t("currencySet", { v: opt.value, s: sym, r: defRate }) })
|
|
1377
1583
|
dialog?.clear()
|
|
1378
1584
|
}}
|
|
1379
1585
|
/>
|
|
@@ -1393,11 +1599,12 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1393
1599
|
placeholder="1.0"
|
|
1394
1600
|
value={String(api.kv.get<number>(`${KV_PREFIX}.rate`, 1))}
|
|
1395
1601
|
onConfirm={(val) => {
|
|
1602
|
+
const t = createT(() => langCode())
|
|
1396
1603
|
const n = parseFloat(val)
|
|
1397
1604
|
if (n > 0) {
|
|
1398
1605
|
api.kv.set(`${KV_PREFIX}.rate`, n)
|
|
1399
1606
|
signals.setExchangeRate(n)
|
|
1400
|
-
api.ui.toast({ message:
|
|
1607
|
+
api.ui.toast({ message: t("rateSet", { r: n }) })
|
|
1401
1608
|
}
|
|
1402
1609
|
dialog?.clear()
|
|
1403
1610
|
}}
|
|
@@ -1411,29 +1618,42 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1411
1618
|
description: "Show or hide a sidebar section",
|
|
1412
1619
|
slash: { name: "cache-section" },
|
|
1413
1620
|
onSelect: (dialog) => {
|
|
1621
|
+
const t = createT(() => langCode())
|
|
1414
1622
|
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
1415
1623
|
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
1416
1624
|
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1417
1625
|
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
1418
1626
|
const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
1627
|
+
const bottomOn = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true))
|
|
1419
1628
|
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
1629
|
+
const labels: Record<string, string> = {
|
|
1630
|
+
detail: t("secDetail"),
|
|
1631
|
+
model: t("secModel"),
|
|
1632
|
+
dist: t("distTitle"),
|
|
1633
|
+
skills: t("secSkills"),
|
|
1634
|
+
balance: t("secBalance"),
|
|
1635
|
+
bottom: t("secBottom"),
|
|
1636
|
+
border: t("secBorder"),
|
|
1637
|
+
}
|
|
1638
|
+
const optTitle = (label: string, on: boolean) => `${visualPadEnd(label, 15)}[${on ? "ON" : "OFF"}]`
|
|
1420
1639
|
dialog?.replace(() => (
|
|
1421
1640
|
<api.ui.DialogSelect
|
|
1422
|
-
title="
|
|
1641
|
+
title={t("secToggle")}
|
|
1423
1642
|
options={[
|
|
1424
|
-
{ title:
|
|
1425
|
-
{ title:
|
|
1426
|
-
{ title:
|
|
1427
|
-
{ title:
|
|
1428
|
-
{ title:
|
|
1429
|
-
{ title:
|
|
1643
|
+
{ title: optTitle(labels.detail, detailOn), value: "detail" },
|
|
1644
|
+
{ title: optTitle(labels.model, modelOn), value: "model" },
|
|
1645
|
+
{ title: optTitle(labels.dist, distOn), value: "dist" },
|
|
1646
|
+
{ title: optTitle(labels.skills, skillsOn), value: "skills" },
|
|
1647
|
+
{ title: optTitle(labels.balance, balanceOn), value: "balance" },
|
|
1648
|
+
{ title: optTitle(labels.bottom, bottomOn), value: "bottom" },
|
|
1649
|
+
{ title: optTitle(labels.border, borderOn), value: "border" },
|
|
1430
1650
|
]}
|
|
1431
1651
|
onSelect={(opt) => {
|
|
1432
1652
|
if (opt.value === "border") {
|
|
1433
1653
|
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
1434
1654
|
api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
1435
1655
|
signals.setBorderVisible(!cur)
|
|
1436
|
-
api.ui.toast({ message:
|
|
1656
|
+
api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") })
|
|
1437
1657
|
} else {
|
|
1438
1658
|
const key = `${KV_PREFIX}.section.${opt.value}`
|
|
1439
1659
|
const cur = Boolean(api.kv.get(key, true))
|
|
@@ -1443,7 +1663,9 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1443
1663
|
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
1444
1664
|
if (opt.value === "skills") signals.setSectionSkills(!cur)
|
|
1445
1665
|
if (opt.value === "balance") signals.setSectionBalance(!cur)
|
|
1446
|
-
|
|
1666
|
+
if (opt.value === "bottom") signals.setSectionBottom(!cur)
|
|
1667
|
+
const name = labels[opt.value] ?? opt.value
|
|
1668
|
+
api.ui.toast({ message: t(!cur ? "sectionShown" : "sectionHidden", { s: name }) })
|
|
1447
1669
|
}
|
|
1448
1670
|
dialog?.clear()
|
|
1449
1671
|
}}
|
|
@@ -1457,6 +1679,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1457
1679
|
description: "Display the current plugin configuration",
|
|
1458
1680
|
slash: { name: "cache-config" },
|
|
1459
1681
|
onSelect: (dialog) => {
|
|
1682
|
+
const t = createT(() => langCode())
|
|
1460
1683
|
const sym = api.kv.get<string>(`${KV_PREFIX}.currency`) ?? "$"
|
|
1461
1684
|
const rate = api.kv.get<number>(`${KV_PREFIX}.rate`) ?? 1
|
|
1462
1685
|
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
@@ -1464,9 +1687,16 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1464
1687
|
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1465
1688
|
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
1466
1689
|
const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
1690
|
+
const bottom = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true))
|
|
1691
|
+
const on = (v: boolean) => v ? "ON" : "OFF"
|
|
1467
1692
|
api.ui.toast({
|
|
1468
|
-
title: "
|
|
1469
|
-
message:
|
|
1693
|
+
title: t("panelConfigTitle"),
|
|
1694
|
+
message: t("panelConfigMsg", {
|
|
1695
|
+
c: sym, r: rate,
|
|
1696
|
+
d: on(detail), m: on(model),
|
|
1697
|
+
t: on(dist), k: on(skills),
|
|
1698
|
+
b: on(balance), f: on(bottom),
|
|
1699
|
+
}),
|
|
1470
1700
|
duration: 8000,
|
|
1471
1701
|
})
|
|
1472
1702
|
dialog?.clear()
|
|
@@ -1478,19 +1708,20 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1478
1708
|
description: "Switch between Chinese and English display",
|
|
1479
1709
|
slash: { name: "cache-lang" },
|
|
1480
1710
|
onSelect: (dialog) => {
|
|
1481
|
-
const
|
|
1711
|
+
const t = createT(() => langCode())
|
|
1712
|
+
const cur = langCode()
|
|
1482
1713
|
dialog?.replace(() => (
|
|
1483
1714
|
<api.ui.DialogSelect
|
|
1484
|
-
title="
|
|
1485
|
-
options={
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1715
|
+
title={t("langTitle")}
|
|
1716
|
+
options={LANG_META.map((m) => ({
|
|
1717
|
+
title: `${visualPadEnd(m.label, 9)}${cur === m.code ? "\u2713" : ""}`,
|
|
1718
|
+
value: m.code,
|
|
1719
|
+
}))}
|
|
1489
1720
|
onSelect={(opt) => {
|
|
1490
|
-
const
|
|
1491
|
-
api.kv.set(`${KV_PREFIX}.lang`,
|
|
1492
|
-
|
|
1493
|
-
api.ui.toast({ message:
|
|
1721
|
+
const code = opt.value as LangCode
|
|
1722
|
+
api.kv.set(`${KV_PREFIX}.lang`, code)
|
|
1723
|
+
setLangCode(code)
|
|
1724
|
+
api.ui.toast({ message: t("langSwitched") })
|
|
1494
1725
|
dialog?.clear()
|
|
1495
1726
|
}}
|
|
1496
1727
|
/>
|
|
@@ -1503,15 +1734,13 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1503
1734
|
description: "切换余额提供商 / 自动切换当前会话提供商 | Switch balance provider / auto-switch session provider",
|
|
1504
1735
|
slash: { name: "cache-balance" },
|
|
1505
1736
|
onSelect: (dialog) => {
|
|
1506
|
-
const
|
|
1737
|
+
const t = createT(() => langCode())
|
|
1507
1738
|
const current = signals.balanceProviderId()
|
|
1508
1739
|
const auto = signals.autoBalance()
|
|
1509
|
-
const autoLabel = auto
|
|
1510
|
-
? (zh ? "自动切换提供商 [开]" : "Auto-switch provider [ON]")
|
|
1511
|
-
: (zh ? "自动切换提供商 [关]" : "Auto-switch provider [OFF]")
|
|
1740
|
+
const autoLabel = `${t("autoSwitchOpt")} [${auto ? "ON" : "OFF"}]`
|
|
1512
1741
|
dialog?.replace(() => (
|
|
1513
1742
|
<api.ui.DialogSelect
|
|
1514
|
-
title={
|
|
1743
|
+
title={t("balProvTitle")}
|
|
1515
1744
|
options={[
|
|
1516
1745
|
{
|
|
1517
1746
|
title: autoLabel,
|
|
@@ -1527,7 +1756,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1527
1756
|
const next = !auto
|
|
1528
1757
|
api.kv.set(`${KV_PREFIX}.balance.auto`, next)
|
|
1529
1758
|
signals.setAutoBalance(next)
|
|
1530
|
-
api.ui.toast({ message:
|
|
1759
|
+
api.ui.toast({ message: next ? t("autoSwitchOn") : t("autoSwitchOff") })
|
|
1531
1760
|
dialog?.clear()
|
|
1532
1761
|
} else {
|
|
1533
1762
|
const provider = getBalanceProvider(opt.value)
|
|
@@ -1536,6 +1765,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1536
1765
|
api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
1537
1766
|
signals.setBalanceProviderId(provider.id)
|
|
1538
1767
|
signals.setAutoBalance(false)
|
|
1768
|
+
signals.setBalanceUnsupported(false)
|
|
1539
1769
|
// 切换后立即按新 provider 刷新显示(无 key 时显示 idle,避免残留上一 provider 余额)
|
|
1540
1770
|
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
1541
1771
|
const hasKey = !!api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
@@ -1543,7 +1773,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1543
1773
|
// 未配置 key → 进入设置流程(对话框保持打开等待输入)
|
|
1544
1774
|
promptBalanceKey(dialog, provider)
|
|
1545
1775
|
} else {
|
|
1546
|
-
api.ui.toast({ message:
|
|
1776
|
+
api.ui.toast({ message: t("providerManual", { p: provider.name }) })
|
|
1547
1777
|
dialog?.clear()
|
|
1548
1778
|
}
|
|
1549
1779
|
}
|
|
@@ -1558,11 +1788,11 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1558
1788
|
description: "Select a provider and set its API key for balance display",
|
|
1559
1789
|
slash: { name: "cache-balance-key" },
|
|
1560
1790
|
onSelect: (dialog) => {
|
|
1561
|
-
const
|
|
1791
|
+
const t = createT(() => langCode())
|
|
1562
1792
|
// 步骤 1:选择 provider
|
|
1563
1793
|
dialog?.replace(() => (
|
|
1564
1794
|
<api.ui.DialogSelect
|
|
1565
|
-
title={
|
|
1795
|
+
title={t("balSelectTitle")}
|
|
1566
1796
|
options={balanceProviders.map((p) => ({
|
|
1567
1797
|
title: providerOptionTitle(p),
|
|
1568
1798
|
value: p.id,
|
|
@@ -1589,9 +1819,10 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1589
1819
|
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
1590
1820
|
slash: { name: "cache-debug-skills" },
|
|
1591
1821
|
onSelect: () => {
|
|
1822
|
+
const t = createT(() => langCode())
|
|
1592
1823
|
const rt = api.route.current
|
|
1593
1824
|
if (rt.name !== "session" || !rt.params) {
|
|
1594
|
-
api.ui.toast({ message: "
|
|
1825
|
+
api.ui.toast({ message: t("runInSession"), variant: "warning" })
|
|
1595
1826
|
return
|
|
1596
1827
|
}
|
|
1597
1828
|
const sid = String(rt.params.sessionID)
|
|
@@ -1669,7 +1900,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1669
1900
|
|
|
1670
1901
|
if (unique.length > 0) {
|
|
1671
1902
|
// ── 有子代理 → DialogSelect 列表选择 ──
|
|
1672
|
-
const
|
|
1903
|
+
const t = createT(() => langCode())
|
|
1673
1904
|
const currentSid = signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "")
|
|
1674
1905
|
const options = unique.map((c, i) => ({
|
|
1675
1906
|
title: `${i + 1}. ${c.title}`,
|
|
@@ -1678,24 +1909,24 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1678
1909
|
}))
|
|
1679
1910
|
// 首尾各放一个"回到主会话",长列表时顶部底部均可直达
|
|
1680
1911
|
const backValue = "__main__"
|
|
1681
|
-
const backTitle = `\u2500 ${
|
|
1912
|
+
const backTitle = `\u2500 ${t("backToMainTitle")}`
|
|
1682
1913
|
options.unshift({ title: backTitle, value: backValue, description: "" })
|
|
1683
1914
|
options.push({ title: backTitle, value: backValue, description: "" })
|
|
1684
1915
|
const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1
|
|
1685
1916
|
dialog?.replace(() => (
|
|
1686
1917
|
<api.ui.DialogSelect
|
|
1687
|
-
title={
|
|
1918
|
+
title={t("subSelectTitle")}
|
|
1688
1919
|
options={options}
|
|
1689
1920
|
current={currentIdx >= 0 ? options[currentIdx].value : undefined}
|
|
1690
1921
|
onSelect={(opt) => {
|
|
1691
1922
|
if (opt.value === backValue) {
|
|
1692
1923
|
signals.setOverrideSessionId(undefined)
|
|
1693
1924
|
api.kv.set(`${KV_PREFIX}.session`, "")
|
|
1694
|
-
api.ui.toast({ message:
|
|
1925
|
+
api.ui.toast({ message: t("backToMain") })
|
|
1695
1926
|
} else {
|
|
1696
1927
|
signals.setOverrideSessionId(opt.value)
|
|
1697
1928
|
api.kv.set(`${KV_PREFIX}.session`, opt.value)
|
|
1698
|
-
api.ui.toast({ message: (
|
|
1929
|
+
api.ui.toast({ message: t("subAgentSwitched", { s: opt.value.slice(0, 24) + "\u2026" }) })
|
|
1699
1930
|
}
|
|
1700
1931
|
dialog?.clear()
|
|
1701
1932
|
}}
|
|
@@ -1703,11 +1934,11 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1703
1934
|
))
|
|
1704
1935
|
} else {
|
|
1705
1936
|
// ── 无子代理 → DialogPrompt 手动粘贴 ──
|
|
1706
|
-
const
|
|
1937
|
+
const t = createT(() => langCode())
|
|
1707
1938
|
dialog?.replace(() => (
|
|
1708
1939
|
<api.ui.DialogPrompt
|
|
1709
|
-
title={signals.overrideSessionId() ?
|
|
1710
|
-
description={() => <text>{
|
|
1940
|
+
title={signals.overrideSessionId() ? t("subSwitchTitle") : t("subViewTitle")}
|
|
1941
|
+
description={() => <text>{t("subNoFound")}</text>}
|
|
1711
1942
|
placeholder="ses_..."
|
|
1712
1943
|
value={signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "") ?? ""}
|
|
1713
1944
|
onConfirm={(val) => {
|
|
@@ -1715,7 +1946,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1715
1946
|
if (sid) {
|
|
1716
1947
|
signals.setOverrideSessionId(sid)
|
|
1717
1948
|
api.kv.set(`${KV_PREFIX}.session`, sid)
|
|
1718
|
-
api.ui.toast({ message: (
|
|
1949
|
+
api.ui.toast({ message: t("subAgentSwitched", { s: sid.slice(0, 24) + "\u2026" }) })
|
|
1719
1950
|
}
|
|
1720
1951
|
dialog?.clear()
|
|
1721
1952
|
}}
|
|
@@ -1731,9 +1962,10 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1731
1962
|
description: "Return to main session stats",
|
|
1732
1963
|
slash: { name: "cache-session-back" },
|
|
1733
1964
|
onSelect: (dialog) => {
|
|
1965
|
+
const t = createT(() => langCode())
|
|
1734
1966
|
signals.setOverrideSessionId(undefined)
|
|
1735
1967
|
api.kv.set(`${KV_PREFIX}.session`, "")
|
|
1736
|
-
api.ui.toast({ message:
|
|
1968
|
+
api.ui.toast({ message: t("backToMain") })
|
|
1737
1969
|
dialog?.clear()
|
|
1738
1970
|
},
|
|
1739
1971
|
},
|