opencode-visual-cache 1.2.16 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,7 +50,8 @@
50
50
  - **折叠记忆**:折叠状态持久化,重启后保持
51
51
  - **语言适配**:自动检测系统语言,支持 `/cache-lang` 运行时切换中/英文,偏好持久化
52
52
  - **多币种**:通过 `/cache-currency` 切换货币,费用和节省同步换算
53
- - **斜杠命令**:`/cache-rate` `/cache-section` `/cache-config` `/cache-lang` 动态配置面板
53
+ - **斜杠命令**:`/cache-session` `/cache-session-back` `/cache-rate` `/cache-section` `/cache-config` `/cache-lang` 动态配置面板
54
+ - **子代理缓存查看**:`/cache-session` 自动扫描并列出子代理,选择一个即可切换面板显示其缓存统计,支持 `/cache-session-back` 返回主会话
54
55
  - **已加载技能**:检测 session 中 LLM 调用 `skill` tool 的记录,展示已加载技能名及估算 Token 占用
55
56
 
56
57
  ---
@@ -100,6 +101,8 @@ npm install -g opencode-visual-cache@latest
100
101
 
101
102
  | 命令 | 功能 | 使用方式 |
102
103
  |------|------|---------|
104
+ | `/cache-session` | 查看子代理缓存统计 | 自动列出子代理供选择,或手动粘贴 Session ID 切换面板数据源 |
105
+ | `/cache-session-back` | 返回主会话统计 | 从子代理缓存视图切回主会话 |
103
106
  | `/cache-currency` | 切换货币单位 | 从列表选择货币(USD / CNY / EUR / JPY / GBP / KRW),自动填入默认汇率 |
104
107
  | `/cache-rate` | 调整汇率乘数 | 输入自定义汇率(如 `7.2`),用于费用换算 |
105
108
  | `/cache-section` | 开关区块与边框 | 独立控制 Token 明细 / 模型与定价 / 估算 Token 分布 / 已加载技能 / 面板边框的显隐 |
package/README_EN.md CHANGED
@@ -50,7 +50,8 @@ Interested in sub-agent monitoring? Check out [opencode-subagent-magazine](https
50
50
  - **Persistent State**: Fold preferences and config remembered across restarts via api.kv
51
51
  - **Language**: Auto-detects system locale, with `/cache-lang` for runtime switching between Chinese and English — preference persisted across restarts
52
52
  - **Multi-currency**: Switch via `/cache-currency` — costs, savings, and per-million rates convert in real time
53
- - **Slash Commands**: `/cache-rate` `/cache-section` `/cache-config` `/cache-lang` for live panel configuration
53
+ - **Slash Commands**: `/cache-session` `/cache-session-back` `/cache-rate` `/cache-section` `/cache-config` `/cache-lang` for live panel configuration
54
+ - **Sub-Agent Cache View**: `/cache-session` auto-scans and lists sub-agents; select one to switch the panel stats. Use `/cache-session-back` to return to the main session
54
55
  - **Loaded Skills**: Detects `skill` tool calls in the session and displays loaded skill names with estimated token footprint
55
56
 
56
57
  ---
@@ -100,6 +101,8 @@ The plugin supports slash commands and command palette (`Ctrl + P`) for runtime
100
101
 
101
102
  | Command | Function | How to use |
102
103
  |---------|----------|------------|
104
+ | `/cache-session` | View sub-agent cache stats | Lists sub-agents automatically, or paste a Session ID to switch the panel data source |
105
+ | `/cache-session-back` | Return to main session | Switch back to main session from sub-agent cache view |
103
106
  | `/cache-currency` | Switch currency | Pick from a list (USD / CNY / EUR / JPY / GBP / KRW); default exchange rate auto-filled |
104
107
  | `/cache-rate` | Adjust exchange rate | Enter a custom rate (e.g. `7.2` for CNY) |
105
108
  | `/cache-section` | Toggle sections & border | Independently show/hide Detail, Model & Pricing, Token Distribution, Loaded Skills, or the panel border |
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.2.16";
1
+ export declare const PLUGIN_VERSION = "1.4.0";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION = "1.2.16";
2
+ export const PLUGIN_VERSION = "1.4.0";
package/dist/index.js CHANGED
@@ -94,6 +94,14 @@ const ZH_T = {
94
94
  secDetail: "明细",
95
95
  secModel: "模型",
96
96
  secSkills: "已加载技能",
97
+ balTotal: "总余额:",
98
+ balNoKey: "未配置 API Key",
99
+ balLoading: "查询中...",
100
+ balError: "查询失败",
101
+ balErr401: "API Key 无效",
102
+ balErr403: "余额查询被拒绝",
103
+ balErrEmpty: "未获取到余额数据",
104
+ balErrTimeout: "查询超时",
97
105
  };
98
106
  const EN_T = {
99
107
  title: "Token Cache",
@@ -125,6 +133,14 @@ const EN_T = {
125
133
  secDetail: "Detail",
126
134
  secModel: "Model",
127
135
  secSkills: "Loaded Skills",
136
+ balTotal: "Total:",
137
+ balNoKey: "No API Key set",
138
+ balLoading: "Fetching...",
139
+ balError: "Fetch failed",
140
+ balErr401: "Invalid API Key",
141
+ balErr403: "Balance request rejected",
142
+ balErrEmpty: "No balance data",
143
+ balErrTimeout: "Request timed out",
128
144
  };
129
145
  // ── color helpers ────────────────────────────────────────────────
130
146
  /** Extract { r, g, b } (0–255) from a hex string or RGBA-like object. */
@@ -202,6 +218,16 @@ function desaturateTo(raw, maxSat, fallback) {
202
218
  const nb = Math.round(c.b + (luma - c.b) * hi);
203
219
  return "#" + [nr, ng, nb].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
204
220
  }
221
+ /** Darken a hex colour by multiplying each channel by `factor` (0–1). */
222
+ function dimColor(hex, factor = 0.5) {
223
+ const c = rgb(hex);
224
+ if (!c)
225
+ return hex;
226
+ const r = Math.round(c.r * factor);
227
+ const g = Math.round(c.g * factor);
228
+ const b = Math.round(c.b * factor);
229
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
230
+ }
205
231
  // Morandi fallbacks — used when a theme colour cannot be resolved
206
232
  const FALLBACK = {
207
233
  primary: "#8B9DAF",
@@ -289,6 +315,44 @@ function estimateTokens(text) {
289
315
  const asciiPerToken = jsonLike ? 3.5 : codeLike ? 3.5 : 4;
290
316
  return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.0));
291
317
  }
318
+ const BALANCE_POLL_MS = 5 * 60 * 1000; // 5 minutes
319
+ async function fetchDeepSeekBalance(apiKey, signal) {
320
+ const res = await fetch("https://api.deepseek.com/user/balance", {
321
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
322
+ signal,
323
+ });
324
+ if (!res.ok) {
325
+ if (res.status === 401)
326
+ throw new Error("401");
327
+ if (res.status === 402 || res.status === 403)
328
+ throw new Error("403");
329
+ throw new Error(String(res.status));
330
+ }
331
+ const json = await res.json();
332
+ const infos = json.balance_infos ?? [];
333
+ if (infos.length === 0)
334
+ throw new Error("EMPTY");
335
+ return infos.map((info) => ({
336
+ currency: info.currency ?? "CNY",
337
+ total: info.total_balance ?? "0",
338
+ }));
339
+ }
340
+ /**
341
+ * 将余额从来源币种换算为目标币种。
342
+ * DEFAULT_RATES 以 USD=1 为基准:先折算为 USD,再换算到目标币种。
343
+ */
344
+ function convertBalance(target, targetRate, amount, from) {
345
+ if (from === target)
346
+ return amount;
347
+ const fromRate = DEFAULT_RATES[from] ?? 1;
348
+ const usd = from === "USD" ? amount : amount / fromRate;
349
+ return target === "USD" ? usd : usd * targetRate;
350
+ }
351
+ /** 货币符号:优先取 /cache-currency 内置映射,未知币种回退为代码。 */
352
+ function balanceSymbol(currency) {
353
+ const sym = CURRENCIES[currency];
354
+ return sym ?? currency + " ";
355
+ }
292
356
  const CURRENCIES = {
293
357
  USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
294
358
  };
@@ -315,7 +379,7 @@ function TokenCachePanel(props) {
315
379
  const [skillsOpen, setSkillsOpen] = createSignal(true);
316
380
  let boxEl;
317
381
  // ── shared signals (de-structured so internal code is unchanged) ──
318
- const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langZH, setLangZH, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, sectionSkills, setSectionSkills, borderVisible, setBorderVisible, } = props.signals;
382
+ const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langZH, setLangZH, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, sectionSkills, setSectionSkills, sectionBalance, setSectionBalance, balanceRefresh, balanceCurrency, setBalanceCurrency, borderVisible, setBorderVisible, } = props.signals;
319
383
  // ── reactive translation (follows langZH signal) ──
320
384
  const t = createMemo(() => langZH() ? ZH_T : EN_T);
321
385
  // ── scan session messages reactively ──
@@ -342,8 +406,66 @@ function TokenCachePanel(props) {
342
406
  hasSkills: false,
343
407
  });
344
408
  const [refreshTick, setRefreshTick] = createSignal(0);
409
+ // ── balance state + polling ──────────────────────────────────
410
+ const [balanceState, setBalanceState] = createSignal({
411
+ status: "idle", data: null, lastFetch: 0,
412
+ });
413
+ // 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
414
+ let balanceSeq = 0;
415
+ const pollBalance = async () => {
416
+ const key = props.api.kv.get(`${KV_PREFIX}.ds_key`, "");
417
+ if (!key) {
418
+ setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined });
419
+ return;
420
+ }
421
+ const now = Date.now();
422
+ const prev = balanceState();
423
+ // key 已更换(重新输入)→ 强制重新查询,绕过缓存
424
+ if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS)
425
+ return; // cache still fresh
426
+ const seq = ++balanceSeq;
427
+ setBalanceState({ ...prev, status: "loading", error: undefined, key });
428
+ const controller = new AbortController();
429
+ let timedOut = false;
430
+ const timer = setTimeout(() => { timedOut = true; controller.abort(); }, 10_000);
431
+ try {
432
+ const data = await fetchDeepSeekBalance(key, controller.signal);
433
+ clearTimeout(timer);
434
+ if (seq !== balanceSeq)
435
+ return; // 已被更新的请求取代,丢弃过期结果
436
+ setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key });
437
+ }
438
+ catch (err) {
439
+ clearTimeout(timer);
440
+ if (seq !== balanceSeq)
441
+ return;
442
+ const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "");
443
+ // 失败时清空旧数据,避免显示过期余额
444
+ setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key });
445
+ }
446
+ };
447
+ // Re-fetch when the API key is (re)configured via /cache-balance-key.
448
+ // 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
449
+ // effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
450
+ // 形成无限循环(每次重跑都发起新的 fetch 请求)。
451
+ createEffect(() => {
452
+ void balanceRefresh();
453
+ untrack(() => { void pollBalance(); });
454
+ });
455
+ // ── auto-clear override when the user navigates to a different main session ──
456
+ let lastMainSid = props.sessionId;
345
457
  createEffect(() => {
346
458
  const sid = props.sessionId;
459
+ if (sid !== lastMainSid) {
460
+ lastMainSid = sid;
461
+ if (props.signals.overrideSessionId()) {
462
+ props.signals.setOverrideSessionId(undefined);
463
+ props.api.kv.set(`${KV_PREFIX}.session`, "");
464
+ }
465
+ }
466
+ });
467
+ createEffect(() => {
468
+ const sid = props.signals.overrideSessionId() ?? props.sessionId;
347
469
  void refreshTick();
348
470
  void partVersion();
349
471
  // 自然追踪 messages 和 provider(SDK 数据就绪时自动重新执行)
@@ -580,10 +702,14 @@ function TokenCachePanel(props) {
580
702
  setCurrencySymbol(sym);
581
703
  if (typeof rate === "number" && rate > 0)
582
704
  setExchangeRate(rate);
705
+ const balCur = props.api.kv.get(`${KV_PREFIX}.balance_currency`);
706
+ if (typeof balCur === "string")
707
+ setBalanceCurrency(balCur);
583
708
  setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
584
709
  setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
585
710
  setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
586
711
  setSectionSkills(Boolean(props.api.kv.get(`${KV_PREFIX}.section.skills`, true)));
712
+ setSectionBalance(Boolean(props.api.kv.get(`${KV_PREFIX}.section.balance`, true)));
587
713
  const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
588
714
  setBorderVisible(bv !== false);
589
715
  // Restore language preference
@@ -641,7 +767,8 @@ function TokenCachePanel(props) {
641
767
  const unsubMsg = props.api.event.on("message.updated", () => { bumpPartVersion(); setRefreshTick(v => v + 1); });
642
768
  const unsubSession = props.api.event.on("session.updated", () => { setRefreshTick(v => v + 1); });
643
769
  setRefreshTick(v => v + 1);
644
- onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg(); unsubSession(); });
770
+ const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS);
771
+ onCleanup(() => { clearTimeout(partTimer); clearInterval(balanceTimer); unsubPart(); unsubMsg(); unsubSession(); });
645
772
  });
646
773
  // ── colours ──
647
774
  // Pull from the current theme, auto-desaturate if too punchy,
@@ -701,21 +828,63 @@ function TokenCachePanel(props) {
701
828
  // boxEl.width may be undefined before the first measurement — guard with 0
702
829
  const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
703
830
  setPanelWidth((prev) => (prev === w ? prev : w));
704
- }, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: t().title }), _jsx(Show, { when: open(), children: _jsxs("span", { style: { fg: pal().muted }, children: [" (v", PLUGIN_VERSION, ")"] }) })] }), _jsxs(Show, { when: !open() && data().hasData, children: [_jsxs(Show, { when: data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t().title) - visualWidth(pct() + " " + t().hitFolded + " " + trendLabel(data().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", t().hitFolded] }), _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] })] }), _jsxs(Show, { when: !data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t().title) - visualWidth(pct() + " " + t().hitFolded))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", t().hitFolded] })] })] })] }), _jsx(Show, { when: open(), children: _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: data().trend !== 0 ? (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), t().tok) }), _jsx("text", { fg: pal().muted, children: justify(t().out, fmt(data().output), t().tok) }), _jsx(Show, { when: data().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: t().saved }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(t().saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate())))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved, currencySymbol(), exchangeRate())] })] }) })] })] }), _jsxs(Show, { when: sectionModel(), children: [_jsxs("text", { onMouseUp: () => setModelOpen((o) => { const n = !o; persistFold("model", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: modelOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().secModel }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t().secModel)) })] }), _jsxs(Show, { when: modelOpen(), children: [_jsx("text", { fg: pal().text, children: justify(t().cost, fmtCost(data().cost, currencySymbol(), exchangeRate())) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(t().provider, data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(t().model, data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(t().rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t().inputRate) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t().cacheRate) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t().writeRate) }) })] })] })] }), _jsx(Show, { when: sectionDist(), children: _jsxs(Show, { when: data().hasDistData, children: [_jsxs("text", { onMouseUp: () => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: distOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().distTitle }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t().distTitle)) })] }), _jsxs(Show, { when: distOpen(), children: [_jsx(Show, { when: data().dist.system > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distSys, fmt(data().dist.system), t().tok) }) }), _jsx(Show, { when: data().dist.user > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distUser, fmt(data().dist.user), t().tok) }) }), _jsx(Show, { when: data().dist.agent > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distAgent, fmt(data().dist.agent), t().tok) }) }), _jsx(Show, { when: data().dist.toolCall > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distTool, fmt(data().dist.toolCall), t().tok) }) }), _jsx(Show, { when: data().dist.toolResult > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distRes, fmt(data().dist.toolResult), t().tok) }) }), _jsx("text", { fg: pal().text, children: justify(t().distTotal, fmt(data().dist.apiInput), t().tok) })] })] }) }), _jsx(Show, { when: sectionSkills(), children: _jsxs(Show, { when: data().hasSkills, children: [_jsxs("text", { onMouseUp: () => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: skillsOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().secSkills }) }), _jsxs("span", { style: { fg: pal().muted }, children: [" (", data().skills.length, ")"] }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t().secSkills + ` (${data().skills.length})`)) })] }), _jsx(Show, { when: skillsOpen(), children: data().skills.map((sk) => {
705
- const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t().tok);
706
- const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
707
- const label = truncateVisual(sk.name, maxLabel);
708
- return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t().tok) }));
709
- }) })] }) })] }) })] }));
831
+ }, children: [_jsxs("text", { onMouseUp: () => setOpen((o) => { const n = !o; persistFold("open", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: open() ? "\u25bc " : "\u25b6 " }), _jsxs("span", { style: { fg: pal().primary }, children: [_jsx("b", { children: t().title }), _jsx(Show, { when: open(), children: _jsxs("span", { style: { fg: dimColor(pal().muted, 0.75) }, children: [" v", PLUGIN_VERSION] }) })] }), _jsxs(Show, { when: !open() && data().hasData, children: [_jsxs(Show, { when: data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t().title) - visualWidth(pct() + " " + t().hitFolded + " " + trendLabel(data().trend)))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", t().hitFolded] }), _jsxs("span", { style: { fg: data().trend !== 0 ? (data().trend > 0 ? pal().success : pal().error) : pal().text }, children: [" ", trendLabel(data().trend)] })] }), _jsxs(Show, { when: !data().hasTrendData, children: [_jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - HEADER_PREFIX - visualWidth(t().title) - visualWidth(pct() + " " + t().hitFolded))) }), _jsxs("span", { style: { fg: hitColor() }, children: [pct(), " ", t().hitFolded] })] })] })] }), _jsxs(Show, { when: open(), children: [_jsx(Show, { when: props.signals.overrideSessionId(), children: (() => {
832
+ const prefix = " \u21b3 " + (langZH() ? "\u5B50\u4EE3\u7406: " : "Sub: ");
833
+ const maxSidW = Math.max(6, panelWidth() - visualWidth(prefix));
834
+ return (_jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: prefix }), _jsx("span", { style: { fg: pal().text }, children: truncateVisual(props.signals.overrideSessionId(), maxSidW) })] }));
835
+ })() }), _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: data().trend !== 0 ? (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), t().tok) }), _jsx("text", { fg: pal().muted, children: justify(t().out, fmt(data().output), t().tok) }), _jsx(Show, { when: data().saved > 0, children: _jsxs("text", { children: [_jsx("span", { style: { fg: pal().muted }, children: t().saved }), _jsx("span", { children: " ".repeat(Math.max(1, panelWidth() - gutter() - visualWidth(t().saved) - visualWidth("~" + fmtCost(data().saved, currencySymbol(), exchangeRate())))) }), _jsxs("span", { style: { fg: pal().success }, children: ["~", fmtCost(data().saved, currencySymbol(), exchangeRate())] })] }) })] })] }), _jsxs(Show, { when: sectionModel(), children: [_jsxs("text", { onMouseUp: () => setModelOpen((o) => { const n = !o; persistFold("model", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: modelOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().secModel }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((modelOpen() ? "\u25bc " : "\u25b6 ") + t().secModel)) })] }), _jsxs(Show, { when: modelOpen(), children: [_jsx("text", { fg: pal().text, children: justify(t().cost, fmtCost(data().cost, currencySymbol(), exchangeRate())) }), _jsx(Show, { when: data().providerName, children: _jsx("text", { fg: pal().muted, children: justify(t().provider, data().providerName) }) }), _jsx("text", { fg: pal().muted, children: justify(t().model, data().model) }), _jsxs(Show, { when: data().hasPricing, children: [_jsx("text", { fg: pal().muted, children: justify(t().rate, currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t().inputRate) }), _jsx(Show, { when: data().cacheReadRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t().cacheRate) }) }), _jsx(Show, { when: data().cacheWriteRate > 0, children: _jsx("text", { fg: pal().muted, children: justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t().writeRate) }) })] })] })] }), _jsx(Show, { when: sectionDist(), children: _jsxs(Show, { when: data().hasDistData, children: [_jsxs("text", { onMouseUp: () => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: distOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().distTitle }) }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t().distTitle)) })] }), _jsxs(Show, { when: distOpen(), children: [_jsx(Show, { when: data().dist.system > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distSys, fmt(data().dist.system), t().tok) }) }), _jsx(Show, { when: data().dist.user > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distUser, fmt(data().dist.user), t().tok) }) }), _jsx(Show, { when: data().dist.agent > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distAgent, fmt(data().dist.agent), t().tok) }) }), _jsx(Show, { when: data().dist.toolCall > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distTool, fmt(data().dist.toolCall), t().tok) }) }), _jsx(Show, { when: data().dist.toolResult > 0, children: _jsx("text", { fg: pal().muted, children: justify(t().distRes, fmt(data().dist.toolResult), t().tok) }) }), _jsx("text", { fg: pal().text, children: justify(t().distTotal, fmt(data().dist.apiInput), t().tok) })] })] }) }), _jsx(Show, { when: sectionSkills(), children: _jsxs(Show, { when: data().hasSkills, children: [_jsxs("text", { onMouseUp: () => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n; }), children: [_jsx("span", { style: { fg: pal().muted }, children: skillsOpen() ? "\u25bc " : "\u25b6 " }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: t().secSkills }) }), _jsxs("span", { style: { fg: pal().muted }, children: [" (", data().skills.length, ")"] }), _jsx("span", { style: { fg: pal().muted }, children: sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t().secSkills + ` (${data().skills.length})`)) })] }), _jsx(Show, { when: skillsOpen(), children: data().skills.map((sk) => {
836
+ const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t().tok);
837
+ const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
838
+ const label = truncateVisual(sk.name, maxLabel);
839
+ return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t().tok) }));
840
+ }) })] }) }), _jsxs(Show, { when: sectionBalance(), children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsx(Show, { when: balanceState().status === "idle", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t().balNoKey })] }) }), _jsx(Show, { when: balanceState().status === "loading", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t().balLoading })] }) }), _jsx(Show, { when: balanceState().status === "error", children: _jsxs("text", { fg: pal().error, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: (() => {
841
+ const code = balanceState().error;
842
+ if (code === "401")
843
+ return t().balErr401;
844
+ if (code === "403")
845
+ return t().balErr403;
846
+ if (code === "EMPTY")
847
+ return t().balErrEmpty;
848
+ if (code === "TIMEOUT")
849
+ return t().balErrTimeout;
850
+ return t().balError + (code ? ` (${code})` : "");
851
+ })() })] }) }), _jsx(Show, { when: balanceState().status === "ok" && balanceState().data, children: (() => {
852
+ const list = balanceState().data;
853
+ const pref = balanceCurrency();
854
+ // 偏好币种是 DeepSeek 原生返回的(CNY/USD)→ 直接显示
855
+ const native = pref ? list.find(x => x.currency === pref) : undefined;
856
+ if (native) {
857
+ return (_jsx("text", { fg: pal().text, children: justify(t().balTotal, balanceSymbol(native.currency) + native.total) }));
858
+ }
859
+ // 非原生币种(EUR/JPY/GBP/KRW…)→ 取第一条余额按汇率换算
860
+ const base = list[0];
861
+ const baseAmt = parseFloat(base.total);
862
+ const converted = Number.isFinite(baseAmt)
863
+ ? convertBalance(pref || base.currency, exchangeRate(), baseAmt, base.currency)
864
+ : baseAmt;
865
+ const shown = pref && base.currency !== pref
866
+ ? converted.toLocaleString("en-US", { maximumFractionDigits: 2 })
867
+ : base.total;
868
+ return (_jsx("text", { fg: pal().text, children: justify(t().balTotal, balanceSymbol(pref || base.currency) + shown) }));
869
+ })() })] })] })] })] }));
710
870
  }
711
871
  // ---------------------------------------------------------------------------
712
872
  // Plugin entry
713
873
  // ---------------------------------------------------------------------------
714
874
  function createSidebarSlot(api, signals) {
875
+ let lastSlotSid = "";
715
876
  return {
716
877
  order: 55,
717
878
  slots: {
718
879
  sidebar_content(ctx, input) {
880
+ // ── auto-clear override when the user navigates to a different main session ──
881
+ if (input.session_id !== lastSlotSid) {
882
+ lastSlotSid = input.session_id;
883
+ if (signals.overrideSessionId()) {
884
+ signals.setOverrideSessionId(undefined);
885
+ api.kv.set("cache_panel.session", "");
886
+ }
887
+ }
719
888
  return (_jsx(TokenCachePanel, { theme: ctx.theme.current, api: api, sessionId: input.session_id, signals: signals }));
720
889
  },
721
890
  },
@@ -729,8 +898,12 @@ const tui = async (api) => {
729
898
  const [sectionModel, setSectionModel] = createSignal(true);
730
899
  const [sectionDist, setSectionDist] = createSignal(true);
731
900
  const [sectionSkills, setSectionSkills] = createSignal(true);
901
+ const [sectionBalance, setSectionBalance] = createSignal(true);
902
+ const [balanceRefresh, setBalanceRefresh] = createSignal(0);
903
+ const [balanceCurrency, setBalanceCurrency] = createSignal("");
732
904
  const [borderVisible, setBorderVisible] = createSignal(true);
733
905
  const [langZH, setLangZH] = createSignal(LANG_ZH);
906
+ const [overrideSessionId, setOverrideSessionId] = createSignal(undefined);
734
907
  const signals = {
735
908
  currencySymbol, setCurrencySymbol,
736
909
  exchangeRate, setExchangeRate,
@@ -739,7 +912,11 @@ const tui = async (api) => {
739
912
  sectionModel, setSectionModel,
740
913
  sectionDist, setSectionDist,
741
914
  sectionSkills, setSectionSkills,
915
+ sectionBalance, setSectionBalance,
916
+ balanceRefresh, setBalanceRefresh,
917
+ balanceCurrency, setBalanceCurrency,
742
918
  borderVisible, setBorderVisible,
919
+ overrideSessionId, setOverrideSessionId,
743
920
  };
744
921
  api.slots.register(createSidebarSlot(api, signals));
745
922
  // ── slash commands for runtime config ──
@@ -759,6 +936,9 @@ const tui = async (api) => {
759
936
  const defRate = DEFAULT_RATES[opt.value] ?? 1;
760
937
  api.kv.set(`${KV_PREFIX}.currency`, sym);
761
938
  api.kv.set(`${KV_PREFIX}.rate`, defRate);
939
+ // 同步余额显示币种偏好:CNY/USD 原生直显,其余币种按汇率换算
940
+ api.kv.set(`${KV_PREFIX}.balance_currency`, opt.value);
941
+ signals.setBalanceCurrency(opt.value);
762
942
  signals.setCurrencySymbol(sym);
763
943
  signals.setExchangeRate(defRate);
764
944
  api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` });
@@ -793,12 +973,14 @@ const tui = async (api) => {
793
973
  const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
794
974
  const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
795
975
  const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
976
+ const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true));
796
977
  const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
797
978
  dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: "Toggle Section", options: [
798
979
  { title: `Token Detail [${detailOn ? "ON" : "OFF"}]`, value: "detail" },
799
980
  { title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
800
981
  { title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
801
982
  { title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
983
+ { title: `DS Balance [${balanceOn ? "ON" : "OFF"}]`, value: "balance" },
802
984
  { title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
803
985
  ], onSelect: (opt) => {
804
986
  if (opt.value === "border") {
@@ -819,6 +1001,8 @@ const tui = async (api) => {
819
1001
  signals.setSectionDist(!cur);
820
1002
  if (opt.value === "skills")
821
1003
  signals.setSectionSkills(!cur);
1004
+ if (opt.value === "balance")
1005
+ signals.setSectionBalance(!cur);
822
1006
  api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
823
1007
  }
824
1008
  dialog?.clear();
@@ -837,9 +1021,10 @@ const tui = async (api) => {
837
1021
  const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true));
838
1022
  const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true));
839
1023
  const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true));
1024
+ const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true));
840
1025
  api.ui.toast({
841
1026
  title: "Cache Panel Config",
842
- message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"}`,
1027
+ message: `Currency: ${sym} | Rate: ${rate} | Detail: ${detail ? "ON" : "OFF"} | Model: ${model ? "ON" : "OFF"} | Dist: ${dist ? "ON" : "OFF"} | Skills: ${skills ? "ON" : "OFF"} | Balance: ${balance ? "ON" : "OFF"}`,
843
1028
  duration: 8000,
844
1029
  });
845
1030
  dialog?.clear();
@@ -864,6 +1049,50 @@ const tui = async (api) => {
864
1049
  } })));
865
1050
  },
866
1051
  },
1052
+ {
1053
+ title: "Cache: Set DeepSeek API Key",
1054
+ value: "cache.balance.key",
1055
+ description: "Set or update the DeepSeek API key for balance display",
1056
+ slash: { name: "cache-balance-key" },
1057
+ onSelect: (dialog) => {
1058
+ const zh = langZH();
1059
+ const current = api.kv.get(`${KV_PREFIX}.ds_key`, "");
1060
+ // 已保存的 key 以脱敏形式预填:保留 "sk-" 前缀 + 头 5 尾 5 字符,中间用 * 填充
1061
+ const maskKey = (k) => {
1062
+ if (!k)
1063
+ return "";
1064
+ const prefix = k.startsWith("sk-") ? "sk-" : "";
1065
+ const body = prefix ? k.slice(3) : k;
1066
+ if (body.length <= 10)
1067
+ return prefix + body.slice(0, 5) + "*".repeat(Math.max(3, body.length - 5));
1068
+ return prefix + body.slice(0, 5) + "*".repeat(Math.max(3, body.length - 10)) + body.slice(-5);
1069
+ };
1070
+ const masked = maskKey(current);
1071
+ dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: zh ? "DeepSeek API Key" : "DeepSeek API Key", description: () => _jsx("text", { children: zh ? "输入 DeepSeek API Key 以显示账户余额(留空清除)" : "Enter your DeepSeek API key to show account balance (leave empty to clear)" }), placeholder: "sk-...", value: masked, onConfirm: (val) => {
1072
+ const input = val.trim();
1073
+ // 空 → 清除;含 * (脱敏占位符残留)→ 视为未修改,保留原 key;否则为新 key
1074
+ let key;
1075
+ if (input === "") {
1076
+ key = "";
1077
+ }
1078
+ else if (input.includes("*")) {
1079
+ key = current;
1080
+ }
1081
+ else {
1082
+ key = input;
1083
+ }
1084
+ api.kv.set(`${KV_PREFIX}.ds_key`, key);
1085
+ setBalanceRefresh(v => v + 1);
1086
+ if (key) {
1087
+ api.ui.toast({ message: zh ? "API Key 已保存,正在查询余额..." : "API Key saved, fetching balance..." });
1088
+ }
1089
+ else {
1090
+ api.ui.toast({ message: zh ? "API Key 已清除" : "API Key cleared" });
1091
+ }
1092
+ dialog?.clear();
1093
+ }, onCancel: () => dialog?.clear() })));
1094
+ },
1095
+ },
867
1096
  {
868
1097
  title: "Cache: Debug Skills Detection",
869
1098
  value: "cache.debug-skills",
@@ -908,6 +1137,111 @@ const tui = async (api) => {
908
1137
  });
909
1138
  },
910
1139
  },
1140
+ {
1141
+ title: "Cache: Sub-Agent Stats",
1142
+ value: "cache.session",
1143
+ description: "View token cache statistics for a sub-agent by session ID",
1144
+ slash: { name: "cache-session" },
1145
+ onSelect: (dialog) => {
1146
+ // ── 扫描当前主 session 的子代理 session ID 列表 ──
1147
+ const rt = api.route.current;
1148
+ const parentSid = rt.name === "session" && rt.params ? String(rt.params.sessionID) : "";
1149
+ const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"]);
1150
+ const children = [];
1151
+ if (parentSid) {
1152
+ try {
1153
+ const msgs = api.state.session.messages(parentSid);
1154
+ for (const msg of msgs) {
1155
+ if (msg.role !== "assistant")
1156
+ continue;
1157
+ let parts = [];
1158
+ try {
1159
+ parts = api.state.part(msg.id);
1160
+ }
1161
+ catch { }
1162
+ for (const p of parts) {
1163
+ if (p.type !== "tool")
1164
+ continue;
1165
+ const tool = String(p.tool ?? "");
1166
+ if (!SUBAGENT_TOOLS.has(tool))
1167
+ continue;
1168
+ const st = p.state;
1169
+ const stMeta = st?.metadata;
1170
+ const subSid = stMeta?.session_id ?? stMeta?.sessionId;
1171
+ if (!subSid)
1172
+ continue;
1173
+ const sidStr = String(subSid);
1174
+ const input = st?.input;
1175
+ const agent = String(p.subagent_type ?? input?.subagent_type ?? input?.category ?? tool);
1176
+ const prompt = String(input?.prompt ?? "");
1177
+ const desc = input?.description ? String(input.description) : "";
1178
+ const title = desc || prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 40) || agent;
1179
+ children.push({ title, value: sidStr, description: `${agent} · ${sidStr.slice(0, 24)}…` });
1180
+ }
1181
+ }
1182
+ }
1183
+ catch { }
1184
+ }
1185
+ // 去重
1186
+ const seen = new Set();
1187
+ const unique = children.filter(c => { if (seen.has(c.value))
1188
+ return false; seen.add(c.value); return true; });
1189
+ if (unique.length > 0) {
1190
+ // ── 有子代理 → DialogSelect 列表选择 ──
1191
+ const zh = langZH();
1192
+ const currentSid = signals.overrideSessionId() ?? api.kv.get(`${KV_PREFIX}.session`, "");
1193
+ const options = unique.map((c, i) => ({
1194
+ title: `${i + 1}. ${c.title}`,
1195
+ value: c.value,
1196
+ description: c.description,
1197
+ }));
1198
+ // 首尾各放一个"回到主会话",长列表时顶部底部均可直达
1199
+ const backValue = "__main__";
1200
+ const backTitle = `\u2500 ${zh ? "\u56DE\u5230\u4E3B\u4F1A\u8BDD" : "Back to Main"}`;
1201
+ options.unshift({ title: backTitle, value: backValue, description: "" });
1202
+ options.push({ title: backTitle, value: backValue, description: "" });
1203
+ const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1;
1204
+ dialog?.replace(() => (_jsx(api.ui.DialogSelect, { title: zh ? "选择子代理" : "Select Sub-Agent", options: options, current: currentIdx >= 0 ? options[currentIdx].value : undefined, onSelect: (opt) => {
1205
+ if (opt.value === backValue) {
1206
+ signals.setOverrideSessionId(undefined);
1207
+ api.kv.set(`${KV_PREFIX}.session`, "");
1208
+ api.ui.toast({ message: zh ? "已切回主会话" : "Switched to main session" });
1209
+ }
1210
+ else {
1211
+ signals.setOverrideSessionId(opt.value);
1212
+ api.kv.set(`${KV_PREFIX}.session`, opt.value);
1213
+ api.ui.toast({ message: (zh ? "已切换至子代理: " : "Showing sub-agent: ") + opt.value.slice(0, 24) + "\u2026" });
1214
+ }
1215
+ dialog?.clear();
1216
+ } })));
1217
+ }
1218
+ else {
1219
+ // ── 无子代理 → DialogPrompt 手动粘贴 ──
1220
+ const zh = langZH();
1221
+ dialog?.replace(() => (_jsx(api.ui.DialogPrompt, { title: signals.overrideSessionId() ? zh ? "切换子代理" : "Switch Sub" : zh ? "查看子代理缓存" : "View Sub Cache", description: () => _jsx("text", { children: zh ? "未找到子代理,请手动粘贴 Session ID" : "No sub-agents found. Paste a Session ID manually" }), placeholder: "ses_...", value: signals.overrideSessionId() ?? api.kv.get(`${KV_PREFIX}.session`, "") ?? "", onConfirm: (val) => {
1222
+ const sid = val.trim();
1223
+ if (sid) {
1224
+ signals.setOverrideSessionId(sid);
1225
+ api.kv.set(`${KV_PREFIX}.session`, sid);
1226
+ api.ui.toast({ message: (langZH() ? "已切换至子代理: " : "Showing sub-agent: ") + sid.slice(0, 24) + "\u2026" });
1227
+ }
1228
+ dialog?.clear();
1229
+ }, onCancel: () => dialog?.clear() })));
1230
+ }
1231
+ },
1232
+ },
1233
+ {
1234
+ title: "Cache: Back to Main",
1235
+ value: "cache.session.back",
1236
+ description: "Return to main session stats",
1237
+ slash: { name: "cache-session-back" },
1238
+ onSelect: (dialog) => {
1239
+ signals.setOverrideSessionId(undefined);
1240
+ api.kv.set(`${KV_PREFIX}.session`, "");
1241
+ api.ui.toast({ message: langZH() ? "已切回主会话" : "Switched to main session" });
1242
+ dialog?.clear();
1243
+ },
1244
+ },
911
1245
  ]);
912
1246
  };
913
1247
  const mod = {