opencode-visual-cache 1.6.0 → 1.6.2
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 +2 -2
- package/README_EN.md +3 -1
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/i18n.d.ts +3 -3
- package/dist/i18n.js +12 -12
- package/dist/index.js +194 -9
- package/dist/tui.js +407 -286
- package/package.json +6 -3
- package/src/_version.ts +1 -1
- package/src/i18n.ts +12 -12
- package/src/index.tsx +209 -32
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
- **模型定价**:显示当前模型的输入 / 缓存读 / 缓存写单价(从 provider 配置动态读取)
|
|
47
47
|
- **折叠面板**:主标题默认折叠,点击展开;明细、模型、分布各自独立折叠
|
|
48
48
|
- **颜色自适应**:命中率 ≥85% 绿 · ≥70% 橙 · <70% 红,颜色从主题色自动去饱和
|
|
49
|
-
- **Token 分布**:按角色(系统提示 / 用户 /
|
|
49
|
+
- **Token 分布**:按角色(系统提示 / 用户 / 子代理指令 / Tool 调用 / Tool 结果)展示估算 Token 占比
|
|
50
50
|
- **折叠记忆**:折叠状态持久化,重启后保持
|
|
51
51
|
- **语言适配**:支持 中文 / English / 日本語 / 한국어,自动检测系统语言,`/cache-lang` 运行时切换,偏好持久化优先
|
|
52
52
|
- **多币种**:通过 `/cache-currency` 切换货币,费用和节省同步换算
|
|
@@ -150,7 +150,7 @@ npm install -g opencode-visual-cache@latest
|
|
|
150
150
|
|
|
151
151
|
通过 `/cache-section` 切换后即时生效,无需重启。此外,该命令还可以开关面板的**外边框**——关闭后内容会顶格显示,释放额外空间。
|
|
152
152
|
|
|
153
|
-
> **关于 Token 分布数值**:分布面板中"
|
|
153
|
+
> **关于 Token 分布数值**:分布面板中"推理"为 API 返回的**精确值**;"系统提示"/"用户"/"子代理指令"/"Tool 调用"/"Tool 结果"为**估算值**——API 仅返回 token 总量,无法拆分各内容类型,插件按内容类型收集文本后基于字符计数近似估算,数值仅供参考。OpenCode 运行时注入的系统提示内容(环境信息、Skill 目录、工具 Schema 定义等,详见 [`system.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/system.ts)、[`tools.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/tools.ts))不在此估算范围内。
|
|
154
154
|
|
|
155
155
|
### 4.4 余额查询
|
|
156
156
|
|
package/README_EN.md
CHANGED
|
@@ -46,7 +46,7 @@ Interested in sub-agent monitoring? Check out [opencode-subagent-magazine](https
|
|
|
46
46
|
- **Model Pricing**: Input / cache-read / cache-write per-million rates (read from provider config dynamically)
|
|
47
47
|
- **Collapsible**: Main title collapsed by default; click to expand. Detail, model, and distribution sections fold independently
|
|
48
48
|
- **Adaptive Colors**: ≥85% green · ≥70% orange · <70% red, auto-desaturated from current theme
|
|
49
|
-
- **Token Distribution**: Per-role (system / user / agent instr / tool call / tool result) estimated token breakdown
|
|
49
|
+
- **Token Distribution**: Per-role (system / user / sub-agent instr / tool call / tool result) estimated token breakdown
|
|
50
50
|
- **Persistent State**: Fold preferences and config remembered across restarts via api.kv
|
|
51
51
|
- **Language**: Chinese / English / 日本語 / 한국어, auto-detects system locale, with `/cache-lang` for runtime switching — user preference takes priority over auto-detection
|
|
52
52
|
- **Multi-currency**: Switch via `/cache-currency` — costs, savings, and per-million rates convert in real time
|
|
@@ -150,6 +150,8 @@ Three sub-sections can be toggled independently to save sidebar space:
|
|
|
150
150
|
|
|
151
151
|
Toggled via `/cache-section` — takes effect instantly, no restart required. The same command also toggles the panel **border**; turning it off removes the outline and padding so content fills the full width.
|
|
152
152
|
|
|
153
|
+
> **About Token Dist. values**: "Reasoning" is an exact value from the API; the other rows (system / user / sub-agent instr / tool call / tool result) are **estimates** — the API only reports total token counts, not how they split across content types, so the plugin collects text per content type and approximates via character counting. Values are indicative only. OpenCode runtime-injected system prompt content (environment info, skill catalog, tool schema definitions — see [`system.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/system.ts), [`tools.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/tools.ts)) is not covered by these estimates.
|
|
154
|
+
|
|
153
155
|
### 4.4 Balance Query
|
|
154
156
|
|
|
155
157
|
The panel can display account balance from multiple AI providers. With **auto-switch** enabled, the balance query follows the model provider of the current session automatically.
|
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.6.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.6.2";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION = "1.6.
|
|
2
|
+
export const PLUGIN_VERSION = "1.6.2";
|
package/dist/i18n.d.ts
CHANGED
|
@@ -21,11 +21,11 @@ declare const ZH_T: {
|
|
|
21
21
|
readonly distTitle: "估算 Token 分布";
|
|
22
22
|
readonly distSys: "系统提示:";
|
|
23
23
|
readonly distUser: "用户:";
|
|
24
|
-
readonly distAgent: "
|
|
24
|
+
readonly distAgent: "子代理指令:";
|
|
25
25
|
readonly distTool: "Tool 调用:";
|
|
26
26
|
readonly distRes: "Tool 结果:";
|
|
27
|
-
readonly
|
|
28
|
-
readonly
|
|
27
|
+
readonly distReason: "推理:";
|
|
28
|
+
readonly stepsCount: "本回合 {n} 次调用:";
|
|
29
29
|
readonly secDetail: "明细";
|
|
30
30
|
readonly secModel: "模型";
|
|
31
31
|
readonly secSkills: "已加载技能";
|
package/dist/i18n.js
CHANGED
|
@@ -24,11 +24,11 @@ const ZH_T = {
|
|
|
24
24
|
distTitle: "估算 Token 分布",
|
|
25
25
|
distSys: "系统提示:",
|
|
26
26
|
distUser: "用户:",
|
|
27
|
-
distAgent: "
|
|
27
|
+
distAgent: "子代理指令:",
|
|
28
28
|
distTool: "Tool 调用:",
|
|
29
29
|
distRes: "Tool 结果:",
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
distReason: "推理:",
|
|
31
|
+
stepsCount: "本回合 {n} 次调用:",
|
|
32
32
|
secDetail: "明细",
|
|
33
33
|
secModel: "模型",
|
|
34
34
|
secSkills: "已加载技能",
|
|
@@ -105,11 +105,11 @@ const EN_T = {
|
|
|
105
105
|
distTitle: "Estimated Token Dist.",
|
|
106
106
|
distSys: "System:",
|
|
107
107
|
distUser: "User:",
|
|
108
|
-
distAgent: "Agent Instr:",
|
|
108
|
+
distAgent: "Sub-Agent Instr:",
|
|
109
109
|
distTool: "Tool Call:",
|
|
110
110
|
distRes: "Tool Result:",
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
distReason: "Reasoning:",
|
|
112
|
+
stepsCount: "Turn #{n} calls:",
|
|
113
113
|
secDetail: "Detail",
|
|
114
114
|
secModel: "Model",
|
|
115
115
|
secSkills: "Loaded Skills",
|
|
@@ -186,11 +186,11 @@ const JA_T = {
|
|
|
186
186
|
distTitle: "Token 分布(推定)",
|
|
187
187
|
distSys: "システム:",
|
|
188
188
|
distUser: "ユーザー:",
|
|
189
|
-
distAgent: "
|
|
189
|
+
distAgent: "サブエージェント指示:",
|
|
190
190
|
distTool: "Tool 呼び出し:",
|
|
191
191
|
distRes: "Tool 結果:",
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
distReason: "推論:",
|
|
193
|
+
stepsCount: "今回 {n} 回呼出:",
|
|
194
194
|
secDetail: "明細",
|
|
195
195
|
secModel: "モデル",
|
|
196
196
|
secSkills: "読み込み済みスキル",
|
|
@@ -267,11 +267,11 @@ const KO_T = {
|
|
|
267
267
|
distTitle: "Token 분포(추정)",
|
|
268
268
|
distSys: "시스템:",
|
|
269
269
|
distUser: "사용자:",
|
|
270
|
-
distAgent: "
|
|
270
|
+
distAgent: "서브 에이전트 지시:",
|
|
271
271
|
distTool: "Tool 호출:",
|
|
272
272
|
distRes: "Tool 결과:",
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
distReason: "추론:",
|
|
274
|
+
stepsCount: "이번 턴 {n}회 호출:",
|
|
275
275
|
secDetail: "상세",
|
|
276
276
|
secModel: "모델",
|
|
277
277
|
secSkills: "로드된 스킬",
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/solid/jsx-runtime";
|
|
2
|
-
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, untrack } from "solid-js";
|
|
2
|
+
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, For, untrack } from "solid-js";
|
|
3
3
|
import { PLUGIN_VERSION } from "./_version";
|
|
4
4
|
import { balanceProviders, getBalanceProvider, maskKey, matchBalanceProvider } from "./balance-providers";
|
|
5
5
|
import { LANG_META, createT, detectLang } from "./i18n";
|
|
@@ -333,6 +333,11 @@ function TokenCachePanel(props) {
|
|
|
333
333
|
const [distOpen, setDistOpen] = createSignal(false);
|
|
334
334
|
const [skillsOpen, setSkillsOpen] = createSignal(true);
|
|
335
335
|
let boxEl;
|
|
336
|
+
// 侧边栏可见性通知:本面板挂载 ⇒ 宿主侧边栏可见(固定占用 42 列输入框宽度)
|
|
337
|
+
createEffect(() => {
|
|
338
|
+
props.signals.setSidebarVisible(true);
|
|
339
|
+
onCleanup(() => props.signals.setSidebarVisible(false));
|
|
340
|
+
});
|
|
336
341
|
// ── shared signals (de-structured so internal code is unchanged) ──
|
|
337
342
|
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
343
|
// ── reactive translation (follows langCode signal) ──
|
|
@@ -347,7 +352,7 @@ function TokenCachePanel(props) {
|
|
|
347
352
|
// stable until the next successful computation arrives.
|
|
348
353
|
const [lastDist, setLastDist] = createSignal({
|
|
349
354
|
system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0,
|
|
350
|
-
output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
|
|
355
|
+
output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0,
|
|
351
356
|
});
|
|
352
357
|
const [lastHasDist, setLastHasDist] = createSignal(false);
|
|
353
358
|
const [dataSignal, setDataSignal] = createSignal({
|
|
@@ -355,7 +360,7 @@ function TokenCachePanel(props) {
|
|
|
355
360
|
cost: 0, saved: 0, model: "", inputRate: 0, cacheReadRate: 0, cacheWriteRate: 0,
|
|
356
361
|
hasPricing: false, hasData: false, trend: 0, hasTrendData: false,
|
|
357
362
|
providerName: "", sessionHitRate: 0,
|
|
358
|
-
dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 },
|
|
363
|
+
dist: { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0 },
|
|
359
364
|
hasDistData: false,
|
|
360
365
|
skills: [],
|
|
361
366
|
hasSkills: false,
|
|
@@ -485,7 +490,7 @@ function TokenCachePanel(props) {
|
|
|
485
490
|
const trend = hasTrendData ? lastMsgHitRate - prevMsgHitRate : 0, providerName = pid || "";
|
|
486
491
|
// untrack 只包裹已知触发死锁的 API
|
|
487
492
|
const distData = untrack(() => {
|
|
488
|
-
let dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, apiOutput: 0, apiInput: 0, stepCost: 0 };
|
|
493
|
+
let dist = { system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0, output: 0, reasoning: 0, apiOutput: 0, apiInput: 0, stepCost: 0, stepCount: 0 };
|
|
489
494
|
let hasDistData = false;
|
|
490
495
|
const loadedSkills = new Map();
|
|
491
496
|
try {
|
|
@@ -520,6 +525,7 @@ function TokenCachePanel(props) {
|
|
|
520
525
|
else if (msg.role === "assistant") {
|
|
521
526
|
const am = msg;
|
|
522
527
|
dist.output += num(am.tokens?.output);
|
|
528
|
+
dist.reasoning += num(am.tokens?.reasoning);
|
|
523
529
|
let parts = [];
|
|
524
530
|
try {
|
|
525
531
|
parts = props.api.state.part(msg.id);
|
|
@@ -535,6 +541,13 @@ function TokenCachePanel(props) {
|
|
|
535
541
|
catch { }
|
|
536
542
|
if (rawInput)
|
|
537
543
|
dist.toolCall += estimateTokens(rawInput);
|
|
544
|
+
// 子代理委托(task 工具):任务描述计入子代理指令(1.15.x 无 subtask part)
|
|
545
|
+
if (tp.tool === "task" && tp.state?.input) {
|
|
546
|
+
const ti = tp.state.input;
|
|
547
|
+
const prompt = typeof ti.prompt === "string" ? ti.prompt : "";
|
|
548
|
+
const desc = typeof ti.description === "string" ? ti.description : "";
|
|
549
|
+
dist.agent += estimateTokens(prompt || desc);
|
|
550
|
+
}
|
|
538
551
|
if (tp.state.status === "completed") {
|
|
539
552
|
const c = tp.state;
|
|
540
553
|
if (c.output)
|
|
@@ -565,8 +578,6 @@ function TokenCachePanel(props) {
|
|
|
565
578
|
}
|
|
566
579
|
}
|
|
567
580
|
}
|
|
568
|
-
else if (p.type === "reasoning")
|
|
569
|
-
dist.agent += estimateTokens(p.text);
|
|
570
581
|
else if (p.type === "subtask") {
|
|
571
582
|
const sub = p;
|
|
572
583
|
dist.agent += estimateTokens(sub.prompt || sub.description || "");
|
|
@@ -587,7 +598,36 @@ function TokenCachePanel(props) {
|
|
|
587
598
|
// 取最后一条有数据消息的总输入(含缓存读/写)作为当前 context 大小
|
|
588
599
|
dist.apiInput = num(lastAssMsg?.tokens?.input) + num(lastAssMsg?.tokens?.cache?.read) + num(lastAssMsg?.tokens?.cache?.write);
|
|
589
600
|
dist.apiOutput = num(lastAssMsg?.tokens?.output);
|
|
590
|
-
|
|
601
|
+
// 本回合(最后一条有数据消息所在的 parentID 链)的 API 调用次数与末次成本。
|
|
602
|
+
// opencode 将回合内每次工具调用循环拆为独立 assistant 消息(各含 1 个 step-finish),
|
|
603
|
+
// 故按 parentID 链聚合统计,而非单条消息。
|
|
604
|
+
if (lastAssMsg) {
|
|
605
|
+
const roundParent = lastAssMsg.parentID;
|
|
606
|
+
let lastCost;
|
|
607
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
608
|
+
const m = msgs[i];
|
|
609
|
+
if (m.role !== "assistant")
|
|
610
|
+
continue;
|
|
611
|
+
if (m.parentID !== roundParent)
|
|
612
|
+
break;
|
|
613
|
+
let parts = [];
|
|
614
|
+
try {
|
|
615
|
+
parts = props.api.state.part(m.id);
|
|
616
|
+
}
|
|
617
|
+
catch { }
|
|
618
|
+
for (const p of parts) {
|
|
619
|
+
if (p.type !== "step-finish")
|
|
620
|
+
continue;
|
|
621
|
+
dist.stepCount++;
|
|
622
|
+
const sc = p.cost;
|
|
623
|
+
if (lastCost === undefined && typeof sc === "number" && Number.isFinite(sc))
|
|
624
|
+
lastCost = sc;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (lastCost !== undefined)
|
|
628
|
+
dist.stepCost = lastCost;
|
|
629
|
+
}
|
|
630
|
+
hasDistData = dist.system + dist.user + dist.agent + dist.toolCall + dist.toolResult > 0 || dist.apiOutput > 0 || dist.apiInput > 0 || dist.reasoning > 0;
|
|
591
631
|
}
|
|
592
632
|
catch { }
|
|
593
633
|
const finalDist = hasDistData ? dist : lastDist(), finalHasDist = hasDistData || lastHasDist();
|
|
@@ -801,7 +841,7 @@ function TokenCachePanel(props) {
|
|
|
801
841
|
const prefix = " \u21b3 " + t("subPrefix");
|
|
802
842
|
const maxSidW = Math.max(6, panelWidth() - visualWidth(prefix));
|
|
803
843
|
return (_jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: prefix }), _jsx("span", { style: { fg: pal().text }, children: truncateVisual(props.signals.overrideSessionId(), maxSidW) })] }));
|
|
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().
|
|
844
|
+
})() }), _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().dist.stepCount >= 2, children: _jsx("text", { fg: pal().muted, children: justify(t("stepsCount", { n: data().dist.stepCount }), fmtCost(data().dist.stepCost, currencySymbol(), exchangeRate())) }) }), _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(Show, { when: data().dist.reasoning > 0, children: _jsx("text", { fg: pal().muted, children: justify(t("distReason"), fmt(data().dist.reasoning), 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
845
|
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t("tok"));
|
|
806
846
|
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
|
|
807
847
|
const label = truncateVisual(sk.name, maxLabel);
|
|
@@ -822,6 +862,32 @@ function TokenCachePanel(props) {
|
|
|
822
862
|
// ---------------------------------------------------------------------------
|
|
823
863
|
// Plugin entry
|
|
824
864
|
// ---------------------------------------------------------------------------
|
|
865
|
+
/**
|
|
866
|
+
* 路径截断的固定开销(列数):仅宿主布局常量,不含任何内容宽度——
|
|
867
|
+
* 统计宽度、宿主 usage、commands 快捷键均为运行时动态计算。
|
|
868
|
+
* - marginLeft=1 + space-between 余量 ≈ 2 列
|
|
869
|
+
*/
|
|
870
|
+
const PATH_CHROME = 2;
|
|
871
|
+
/**
|
|
872
|
+
* 路径可用宽度低于此列数时整体隐藏(极窄终端下路径信息价值太低,
|
|
873
|
+
* 残留的 "E:\Work…" 反而挤压右侧统计与 commands,直接让位更干净)。
|
|
874
|
+
*/
|
|
875
|
+
const HIDE_PATH_BELOW = 14;
|
|
876
|
+
/**
|
|
877
|
+
* 从宿主 keymap 动态读取命令的快捷键显示文本(与宿主 Prompt 同源),
|
|
878
|
+
* 取不到时回退到传入的默认文本。
|
|
879
|
+
*/
|
|
880
|
+
function keyShortcut(api, command, fallback) {
|
|
881
|
+
try {
|
|
882
|
+
const binds = api.tuiConfig.keybinds.get(command);
|
|
883
|
+
const seq = binds?.map((b) => ({ key: b.key }));
|
|
884
|
+
const s = api.keys.formatBindings(seq);
|
|
885
|
+
return s || fallback;
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
return fallback;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
825
891
|
/**
|
|
826
892
|
* 输入框 hint 行(session_prompt slot 的 hint):单行显示 路径 · 命中率 · 余额 · Tokens。
|
|
827
893
|
* 通过 ui.Prompt 的 hint prop 注入——宿主右侧的 token/commands 提示自动保留,
|
|
@@ -965,6 +1031,122 @@ function BottomStatusBar(props) {
|
|
|
965
1031
|
return "";
|
|
966
1032
|
}
|
|
967
1033
|
});
|
|
1034
|
+
const [termW, setTermW] = createSignal(props.api.renderer.terminalWidth);
|
|
1035
|
+
// resize 事件(主通道);事件接口若在插件环境不可用则跳过,由轮询兜底
|
|
1036
|
+
createEffect(() => {
|
|
1037
|
+
const r = props.api.renderer;
|
|
1038
|
+
if (typeof r.on !== "function" || typeof r.off !== "function")
|
|
1039
|
+
return;
|
|
1040
|
+
const onResize = () => setTermW(props.api.renderer.terminalWidth);
|
|
1041
|
+
r.on("resize", onResize);
|
|
1042
|
+
onCleanup(() => r.off("resize", onResize));
|
|
1043
|
+
});
|
|
1044
|
+
// 轮询兜底:事件通道若在插件环境不可用,定期同步终端宽度(值不变时不触发更新)
|
|
1045
|
+
createEffect(() => {
|
|
1046
|
+
const timer = setInterval(() => setTermW(props.api.renderer.terminalWidth), 500);
|
|
1047
|
+
onCleanup(() => clearInterval(timer));
|
|
1048
|
+
});
|
|
1049
|
+
// 统计部分分段(单一数据源):量宽拼接 text,渲染逐段着色,避免双源漂移
|
|
1050
|
+
const statsSegs = createMemo(() => {
|
|
1051
|
+
const s = stats();
|
|
1052
|
+
const hr = s && s.hitRate >= 0 ? (Math.floor(s.hitRate * 10) / 10).toFixed(1) + "%" : "--";
|
|
1053
|
+
const segs = [
|
|
1054
|
+
{ text: t("barHit") + " ", color: pal().muted },
|
|
1055
|
+
{ text: hr, color: hitColor() },
|
|
1056
|
+
];
|
|
1057
|
+
const tr = trend();
|
|
1058
|
+
if (tr !== null) {
|
|
1059
|
+
segs.push({ text: " " + (tr > 0 ? "\u2191" : "\u2193") + Math.abs(tr).toFixed(1) + "%", color: tr > 0 ? pal().success : pal().error });
|
|
1060
|
+
}
|
|
1061
|
+
segs.push({ text: " \u00b7 " + t("barTok") + " ", color: pal().muted });
|
|
1062
|
+
segs.push({ text: s ? fmtCompact(s.input + s.read + s.write) : "--", color: pal().text });
|
|
1063
|
+
if (!props.signals.balanceUnsupported()) {
|
|
1064
|
+
segs.push({ text: " \u00b7 " + t("barBal") + " ", color: pal().muted });
|
|
1065
|
+
segs.push({ text: balanceText(), color: pal().text });
|
|
1066
|
+
}
|
|
1067
|
+
segs.push({ text: " \u00b7 ", color: pal().muted });
|
|
1068
|
+
return segs;
|
|
1069
|
+
});
|
|
1070
|
+
const statsW = createMemo(() => {
|
|
1071
|
+
let w = 0;
|
|
1072
|
+
for (const sg of statsSegs())
|
|
1073
|
+
w += visualWidth(sg.text);
|
|
1074
|
+
return w;
|
|
1075
|
+
});
|
|
1076
|
+
// 宿主右侧 usage 文本复刻(1.18.16 Prompt 口径):
|
|
1077
|
+
// 最后一条 output>0 的 assistant 消息 → tokens 合计格式化 + 模型 context limit 百分比 + session 累计费用
|
|
1078
|
+
const sessionCost = createMemo(() => {
|
|
1079
|
+
try {
|
|
1080
|
+
return num(props.api.state.session.get(sid)?.cost);
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
return 0;
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
const usageText = createMemo(() => {
|
|
1087
|
+
const id = sid;
|
|
1088
|
+
if (!id)
|
|
1089
|
+
return "";
|
|
1090
|
+
const msgs = props.api.state.session.messages(id);
|
|
1091
|
+
let last;
|
|
1092
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1093
|
+
const m = msgs[i];
|
|
1094
|
+
if (m.role !== "assistant")
|
|
1095
|
+
continue;
|
|
1096
|
+
const tk = m.tokens;
|
|
1097
|
+
if (tk && num(tk.output) > 0) {
|
|
1098
|
+
last = m;
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (!last)
|
|
1103
|
+
return "";
|
|
1104
|
+
const tk = last.tokens;
|
|
1105
|
+
if (!tk)
|
|
1106
|
+
return "";
|
|
1107
|
+
const tokens = num(tk.input) + num(tk.output) + num(tk.reasoning) + num(tk.cache?.read) + num(tk.cache?.write);
|
|
1108
|
+
if (tokens <= 0)
|
|
1109
|
+
return "";
|
|
1110
|
+
let pct = "";
|
|
1111
|
+
try {
|
|
1112
|
+
const p = props.api.state.provider.find((x) => x.id === last.providerID);
|
|
1113
|
+
const limit = p?.models?.[last.modelID]?.limit?.context;
|
|
1114
|
+
if (typeof limit === "number" && limit > 0)
|
|
1115
|
+
pct = ` (${Math.round((tokens / limit) * 100)}%)`;
|
|
1116
|
+
}
|
|
1117
|
+
catch { }
|
|
1118
|
+
const context = fmtCompact(tokens) + pct;
|
|
1119
|
+
const cost = sessionCost();
|
|
1120
|
+
return cost > 0 ? context + " \u00b7 " + fmtCost(cost) : context;
|
|
1121
|
+
});
|
|
1122
|
+
// 宿主 1513 行右侧文本:usage(有数据)或 "快捷键 agents"(无数据)+ commands,
|
|
1123
|
+
// 快捷键从宿主 keymap 动态读取
|
|
1124
|
+
const rightText = createMemo(() => {
|
|
1125
|
+
const cmds = keyShortcut(props.api, "command.palette.show", "ctrl+p") + " commands";
|
|
1126
|
+
const u = usageText();
|
|
1127
|
+
if (u)
|
|
1128
|
+
return u + " " + cmds;
|
|
1129
|
+
return keyShortcut(props.api, "agent.cycle", "") + " agents " + cmds;
|
|
1130
|
+
});
|
|
1131
|
+
const rightW = createMemo(() => visualWidth(rightText()));
|
|
1132
|
+
// 输入框实际宽度 = 终端宽度 - 侧边栏(可见时 42) - 边距 4
|
|
1133
|
+
// (与宿主 session 布局 contentWidth 口径一致;侧边栏可见性由本面板挂载状态驱动)
|
|
1134
|
+
const inputW = createMemo(() => termW() - (props.signals.sidebarVisible() ? 42 : 0) - 4);
|
|
1135
|
+
// 路径可用宽度 = 输入框宽度 - 统计宽度(精确) - 宿主右侧宽度(动态) - 布局开销;
|
|
1136
|
+
// 低于 HIDE_PATH_BELOW 时整体隐藏路径(宽度归零),把空间让给统计与 commands
|
|
1137
|
+
const dirDisplay = createMemo(() => {
|
|
1138
|
+
const avail = inputW() - statsW() - rightW() - PATH_CHROME;
|
|
1139
|
+
if (avail < HIDE_PATH_BELOW)
|
|
1140
|
+
return "";
|
|
1141
|
+
return truncateVisual(directory(), avail);
|
|
1142
|
+
});
|
|
1143
|
+
// 状态栏关闭(仅路径)时同样在极窄条件下隐藏路径
|
|
1144
|
+
const dirFallback = createMemo(() => {
|
|
1145
|
+
const avail = inputW() - rightW() - PATH_CHROME;
|
|
1146
|
+
if (avail < HIDE_PATH_BELOW)
|
|
1147
|
+
return "";
|
|
1148
|
+
return truncateVisual(directory(), avail);
|
|
1149
|
+
});
|
|
968
1150
|
// 恢复显隐偏好(默认显示);关闭时回退为仅显示路径,与宿主默认 hint 行一致
|
|
969
1151
|
onMount(() => {
|
|
970
1152
|
try {
|
|
@@ -973,7 +1155,7 @@ function BottomStatusBar(props) {
|
|
|
973
1155
|
}
|
|
974
1156
|
catch { }
|
|
975
1157
|
});
|
|
976
|
-
return (_jsx(Show, { when: props.signals.sectionBottom(), fallback: _jsx("text", { fg: pal().muted, children:
|
|
1158
|
+
return (_jsx(Show, { when: props.signals.sectionBottom(), fallback: _jsx("text", { fg: pal().muted, children: dirFallback() }), children: _jsxs("box", { marginLeft: 1, flexGrow: 1, flexShrink: 0, flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: pal().muted, children: dirDisplay() }), _jsx("box", { flexDirection: "row", children: _jsx("text", { children: _jsx(For, { each: statsSegs(), children: (sg) => _jsx("span", { style: { fg: sg.color }, children: sg.text }) }) }) })] }) }));
|
|
977
1159
|
}
|
|
978
1160
|
function createSidebarSlot(api, signals) {
|
|
979
1161
|
let lastSlotSid = "";
|
|
@@ -1012,6 +1194,8 @@ const tui = async (api) => {
|
|
|
1012
1194
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
1013
1195
|
const [langCode, setLangCode] = createSignal(INIT_LANG);
|
|
1014
1196
|
const [overrideSessionId, setOverrideSessionId] = createSignal(undefined);
|
|
1197
|
+
// 侧边栏可见性(由 TokenCachePanel 挂载状态驱动):可见时宿主输入框宽度 = 终端宽 - 42 - 4
|
|
1198
|
+
const [sidebarVisible, setSidebarVisible] = createSignal(false);
|
|
1015
1199
|
// ── 余额查询状态(共享):侧边栏与底部栏读同一份数据,
|
|
1016
1200
|
// 避免重复请求导致两处余额不一致 ──
|
|
1017
1201
|
const [balanceState, setBalanceState] = createSignal({
|
|
@@ -1037,6 +1221,7 @@ const tui = async (api) => {
|
|
|
1037
1221
|
balanceCurrency, setBalanceCurrency,
|
|
1038
1222
|
borderVisible, setBorderVisible,
|
|
1039
1223
|
overrideSessionId, setOverrideSessionId,
|
|
1224
|
+
sidebarVisible, setSidebarVisible,
|
|
1040
1225
|
};
|
|
1041
1226
|
api.slots.register(createSidebarSlot(api, signals));
|
|
1042
1227
|
// 输入框 hint 行(session_prompt slot,replace 模式):
|