opencode-visual-cache 1.6.2 → 1.7.0-beta.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/LICENSE +21 -21
- package/README.md +247 -247
- package/README_EN.md +247 -247
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/core/color.d.ts +37 -0
- package/dist/core/color.js +108 -0
- package/dist/core/currency.d.ts +17 -0
- package/dist/core/currency.js +43 -0
- package/dist/core/estimate.d.ts +1 -0
- package/dist/core/estimate.js +40 -0
- package/dist/core/format.d.ts +15 -0
- package/dist/core/format.js +90 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +5 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.js +1 -0
- package/dist/index.js +12 -818
- package/dist/panel/TokenCachePanel.d.ts +10 -0
- package/dist/panel/TokenCachePanel.js +549 -0
- package/dist/panel/panel-api.d.ts +119 -0
- package/dist/panel/panel-api.js +1 -0
- package/dist/tui.js +223 -209
- package/dist/v2/commands.d.ts +10 -0
- package/dist/v2/commands.js +490 -0
- package/dist/v2/data.d.ts +27 -0
- package/dist/v2/data.js +103 -0
- package/dist/v2/index.d.ts +4 -0
- package/dist/v2/index.js +163 -0
- package/dist/v2/sidebar.d.ts +6 -0
- package/dist/v2/sidebar.js +36 -0
- package/dist/v2/status.d.ts +14 -0
- package/dist/v2/status.js +83 -0
- package/dist/v2/theme.d.ts +8 -0
- package/dist/v2/theme.js +16 -0
- package/dist/v2/types.d.ts +219 -0
- package/dist/v2/types.js +6 -0
- package/dist/v2/v2-panel-api.d.ts +8 -0
- package/dist/v2/v2-panel-api.js +176 -0
- package/dist/v2.js +2941 -0
- package/package.json +72 -67
- package/src/_version.ts +1 -1
- package/src/balance-providers.ts +153 -153
- package/src/core/color.ts +108 -0
- package/src/core/currency.ts +46 -0
- package/src/core/estimate.ts +36 -0
- package/src/core/format.ts +81 -0
- package/src/core/index.ts +5 -0
- package/src/core/types.ts +17 -0
- package/src/i18n.ts +380 -380
- package/src/index.tsx +1035 -2120
- package/src/panel/TokenCachePanel.tsx +776 -0
- package/src/panel/panel-api.ts +104 -0
- package/src/server.ts +10 -10
- package/src/v2/commands.ts +471 -0
- package/src/v2/data.ts +112 -0
- package/src/v2/index.tsx +184 -0
- package/src/v2/sidebar.tsx +69 -0
- package/src/v2/status.tsx +96 -0
- package/src/v2/theme.ts +19 -0
- package/src/v2/types.ts +159 -0
- package/src/v2/v2-panel-api.ts +177 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Message, Part, Session } from "@opencode-ai/sdk"
|
|
2
|
+
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import type { BalanceEntry } from "../balance-providers"
|
|
4
|
+
import type { LangCode } from "../i18n"
|
|
5
|
+
|
|
6
|
+
/** 会话信息(面板消费的字段;SDK Session 类型过严,用宽松接口) */
|
|
7
|
+
export interface PanelSession {
|
|
8
|
+
id: string
|
|
9
|
+
title?: string
|
|
10
|
+
agent?: string
|
|
11
|
+
model?: { providerID?: string; id?: string }
|
|
12
|
+
tokens?: { input?: number; output?: number; reasoning?: number; cache?: { read?: number; write?: number } }
|
|
13
|
+
cost?: number
|
|
14
|
+
[key: string]: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Panel API 契约:TokenCachePanel 消费的宿主 API 子集。
|
|
19
|
+
* V1(TuiPluginApi)天然满足;V2(opencode2 context)由 v2-panel-api 适配实现。
|
|
20
|
+
*/
|
|
21
|
+
export interface PanelApi {
|
|
22
|
+
kv: {
|
|
23
|
+
ready: boolean
|
|
24
|
+
get<T>(key: string, fallback?: T): T | undefined
|
|
25
|
+
set(key: string, value: unknown): void | Promise<void>
|
|
26
|
+
}
|
|
27
|
+
state: {
|
|
28
|
+
session: {
|
|
29
|
+
get(id: string): PanelSession | undefined
|
|
30
|
+
/** V1 返回 sdk/v2 的 Message、V2 返回 SessionMessageInfo——统一放宽 */
|
|
31
|
+
messages(id: string): readonly any[]
|
|
32
|
+
}
|
|
33
|
+
provider: readonly Record<string, any>[]
|
|
34
|
+
config: any
|
|
35
|
+
part(messageID: string): readonly any[]
|
|
36
|
+
path: { directory: string }
|
|
37
|
+
}
|
|
38
|
+
event: {
|
|
39
|
+
on(type: string, handler: (event: unknown) => void): () => void
|
|
40
|
+
}
|
|
41
|
+
renderer: { terminalWidth: number }
|
|
42
|
+
keys: { formatBindings(binding: unknown): string | undefined }
|
|
43
|
+
tuiConfig: { keybinds: { get(command: string): unknown } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BalanceState {
|
|
47
|
+
status: "idle" | "loading" | "ok" | "error"
|
|
48
|
+
data: BalanceEntry[] | null
|
|
49
|
+
lastFetch: number
|
|
50
|
+
error?: string
|
|
51
|
+
key?: string // 上次成功/尝试查询所用的 key,用于检测 key 是否更换
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Signals shared between the TUI component and slash commands.
|
|
55
|
+
* Created in the `tui` function scope so they do not survive module reload —
|
|
56
|
+
* the component re-creates them on mount and restores user config from kv. */
|
|
57
|
+
export interface PanelSignals {
|
|
58
|
+
currencySymbol: () => string
|
|
59
|
+
setCurrencySymbol: (v: string) => void
|
|
60
|
+
exchangeRate: () => number
|
|
61
|
+
setExchangeRate: (v: number) => void
|
|
62
|
+
langCode: () => LangCode
|
|
63
|
+
setLangCode: (v: LangCode) => void
|
|
64
|
+
sectionDetail: () => boolean
|
|
65
|
+
setSectionDetail: (v: boolean) => void
|
|
66
|
+
sectionModel: () => boolean
|
|
67
|
+
setSectionModel: (v: boolean) => void
|
|
68
|
+
sectionDist: () => boolean
|
|
69
|
+
setSectionDist: (v: boolean) => void
|
|
70
|
+
sectionSkills: () => boolean
|
|
71
|
+
setSectionSkills: (v: boolean) => void
|
|
72
|
+
sectionBalance: () => boolean
|
|
73
|
+
setSectionBalance: (v: boolean) => void
|
|
74
|
+
/** Bottom status bar (prompt hint line) visibility. */
|
|
75
|
+
sectionBottom: () => boolean
|
|
76
|
+
setSectionBottom: (v: boolean) => void
|
|
77
|
+
/** Increment to force a balance re-fetch. */
|
|
78
|
+
balanceRefresh: () => number
|
|
79
|
+
setBalanceRefresh: (v: number) => void
|
|
80
|
+
/** Currently selected balance provider id (e.g. "deepseek"). */
|
|
81
|
+
balanceProviderId: () => string
|
|
82
|
+
setBalanceProviderId: (v: string) => void
|
|
83
|
+
/** Auto-switch to the session's provider for balance display. Manual switch disables it. */
|
|
84
|
+
autoBalance: () => boolean
|
|
85
|
+
setAutoBalance: (v: boolean) => void
|
|
86
|
+
/** True when the session's provider has no balance adapter (auto mode). Suppresses balance polling. */
|
|
87
|
+
balanceUnsupported: () => boolean
|
|
88
|
+
setBalanceUnsupported: (v: boolean) => void
|
|
89
|
+
/** Shared balance query state — single source of truth for sidebar and bottom bar. */
|
|
90
|
+
balanceState: () => BalanceState
|
|
91
|
+
/** Preferred currency code for balance display (CNY / USD / …). Empty = first entry. */
|
|
92
|
+
balanceCurrency: () => string
|
|
93
|
+
setBalanceCurrency: (v: string) => void
|
|
94
|
+
borderVisible: () => boolean
|
|
95
|
+
setBorderVisible: (v: boolean) => void
|
|
96
|
+
/** When set, the panel renders stats for this session instead of the main one. */
|
|
97
|
+
overrideSessionId: () => string | undefined
|
|
98
|
+
setOverrideSessionId: (v: string | undefined) => void
|
|
99
|
+
/** True while our sidebar panel is mounted — host sidebar is visible (occupies 42 cols). */
|
|
100
|
+
sidebarVisible: () => boolean
|
|
101
|
+
setSidebarVisible: (v: boolean) => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type { TuiThemeCurrent }
|
package/src/server.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
-
|
|
3
|
-
const server: Plugin = async () => ({})
|
|
4
|
-
|
|
5
|
-
const mod: PluginModule = {
|
|
6
|
-
id: "opencode-visual-cache",
|
|
7
|
-
server,
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export default mod
|
|
1
|
+
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
const server: Plugin = async () => ({})
|
|
4
|
+
|
|
5
|
+
const mod: PluginModule = {
|
|
6
|
+
id: "opencode-visual-cache",
|
|
7
|
+
server,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default mod
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import type { Context, KeymapCommand } from "./types"
|
|
2
|
+
import type { PanelApi, PanelSignals } from "../panel/panel-api"
|
|
3
|
+
import { CURRENCIES, DEFAULT_RATES, visualPadEnd } from "../core"
|
|
4
|
+
import { balanceProviders, getBalanceProvider, maskKey, type BalanceProvider } from "../balance-providers"
|
|
5
|
+
import { LANG_META, createT, type LangCode } from "../i18n"
|
|
6
|
+
|
|
7
|
+
const KV_PREFIX = "cache_panel"
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// V2 helpers(对齐 V1 index.tsx 内嵌辅助;API 形态从 V1 组件式切换为 V2 Promise 式)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** V2 无 api.state.part API——从消息 content 数组提取 tool parts(V2 Tool part 形状)。 */
|
|
14
|
+
function extractToolParts(msg: Record<string, any>): Array<Record<string, any>> {
|
|
15
|
+
if (!Array.isArray(msg?.content)) return []
|
|
16
|
+
return msg.content.filter((p: Record<string, any>) => p?.type === "tool")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** V2 版 findOpencodeKey:从 V2 provider list 找 OpenCode 已认证的 key(对齐 V1 api.state.provider)。
|
|
20
|
+
* 导出供 index.tsx 的余额轮询复用。 */
|
|
21
|
+
export function findOpencodeKeyV2(context: Context, provider: BalanceProvider): string {
|
|
22
|
+
try {
|
|
23
|
+
const provs = context.data.location.provider.list() as Array<{ id?: string; key?: string; options?: { apiKey?: string } }>
|
|
24
|
+
const id = provider.id.toLowerCase()
|
|
25
|
+
const hit = provs.find((p) => String(p.id ?? "").toLowerCase() === id)
|
|
26
|
+
?? provs.find((p) => String(p.id ?? "").toLowerCase().startsWith(id))
|
|
27
|
+
if (!hit) return ""
|
|
28
|
+
const k = typeof hit.key === "string" ? hit.key : ""
|
|
29
|
+
if (k) return k
|
|
30
|
+
return typeof hit.options?.apiKey === "string" ? hit.options.apiKey : ""
|
|
31
|
+
} catch {
|
|
32
|
+
return ""
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 当前路由 sessionID(V2 ui.router.current();Route = { type: "session", sessionID })。 */
|
|
37
|
+
function currentSessionID(context: Context): string {
|
|
38
|
+
try {
|
|
39
|
+
const rt = context.ui.router.current()
|
|
40
|
+
if (rt?.type === "session" && rt.sessionID) return String(rt.sessionID)
|
|
41
|
+
} catch {}
|
|
42
|
+
return ""
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** V2 斜杠命令(对齐 V1 10 个命令的完整行为)。
|
|
46
|
+
* keymap.layer 必须在组件渲染上下文调用,因此本函数只构建命令数组,
|
|
47
|
+
* 由渲染组件内 layer(() => ({ commands })) 注册。 */
|
|
48
|
+
export function makeCommands(context: Context, api: PanelApi, signals: PanelSignals): KeymapCommand[] {
|
|
49
|
+
const t = () => createT(() => signals.langCode())
|
|
50
|
+
|
|
51
|
+
/** 菜单中 provider 选项标题:标注 key 来源(手动配置 / OpenCode 自动复用 / 未配置)。 */
|
|
52
|
+
const providerOptionTitle = (p: BalanceProvider, current?: string) => {
|
|
53
|
+
const hasManual = !!api.kv.get<string>(`${KV_PREFIX}.balance.${p.id}.key`, "")
|
|
54
|
+
const hasAuto = !hasManual && !!findOpencodeKeyV2(context, p)
|
|
55
|
+
const mark = hasManual
|
|
56
|
+
? t()("keyUser")
|
|
57
|
+
: hasAuto
|
|
58
|
+
? t()("keyOpenCode")
|
|
59
|
+
: t()("keyNotSet")
|
|
60
|
+
return p.name + mark + (current && p.id === current ? " *" : "")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 弹出指定 provider 的 API Key 输入框(空清除 / 含 * 保留原 key / 新 key 实时刷新)。 */
|
|
64
|
+
const promptBalanceKey = async (provider: BalanceProvider): Promise<void> => {
|
|
65
|
+
const current = api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "") ?? ""
|
|
66
|
+
const val = await context.ui.dialog.prompt({
|
|
67
|
+
title: provider.name,
|
|
68
|
+
message: t()("balKeyPrompt", { p: provider.name }),
|
|
69
|
+
placeholder: provider.keyPlaceholder ?? "sk-...",
|
|
70
|
+
})
|
|
71
|
+
if (val === undefined) return // 取消
|
|
72
|
+
const input = val.trim()
|
|
73
|
+
let key: string
|
|
74
|
+
if (input === "") {
|
|
75
|
+
key = ""
|
|
76
|
+
} else if (input.includes("*")) {
|
|
77
|
+
key = current
|
|
78
|
+
} else {
|
|
79
|
+
key = input
|
|
80
|
+
}
|
|
81
|
+
await api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key)
|
|
82
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
83
|
+
context.ui.toast.show({ message: key ? t()("keySaved") : t()("keyCleared") })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
// ── /cache-currency 切换货币 ──
|
|
88
|
+
{
|
|
89
|
+
id: "opencode-visual-cache.cache.currency",
|
|
90
|
+
title: "Cache: Set Currency",
|
|
91
|
+
description: "Change the currency unit for cost display",
|
|
92
|
+
group: "Cache",
|
|
93
|
+
palette: true,
|
|
94
|
+
slash: { name: "cache-currency" },
|
|
95
|
+
run: async () => {
|
|
96
|
+
const opt = await context.ui.dialog.select<string>({
|
|
97
|
+
title: "Select Currency",
|
|
98
|
+
options: Object.entries(CURRENCIES).map(([code, sym]) => ({
|
|
99
|
+
title: `${code} (${sym})`,
|
|
100
|
+
value: code,
|
|
101
|
+
})),
|
|
102
|
+
})
|
|
103
|
+
if (!opt) return
|
|
104
|
+
const sym = CURRENCIES[opt] ?? "$"
|
|
105
|
+
const defRate = DEFAULT_RATES[opt] ?? 1
|
|
106
|
+
await api.kv.set(`${KV_PREFIX}.currency`, sym)
|
|
107
|
+
await api.kv.set(`${KV_PREFIX}.rate`, defRate)
|
|
108
|
+
// 同步余额显示币种偏好:CNY/USD 原生直显,其余币种按汇率换算
|
|
109
|
+
await api.kv.set(`${KV_PREFIX}.balance_currency`, opt)
|
|
110
|
+
signals.setBalanceCurrency(opt)
|
|
111
|
+
signals.setCurrencySymbol(sym)
|
|
112
|
+
signals.setExchangeRate(defRate)
|
|
113
|
+
context.ui.toast.show({ message: t()("currencySet", { v: opt, s: sym, r: defRate }) })
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
// ── /cache-rate 调整汇率 ──
|
|
117
|
+
{
|
|
118
|
+
id: "opencode-visual-cache.cache.rate",
|
|
119
|
+
title: "Cache: Set Exchange Rate",
|
|
120
|
+
description: "Set the exchange rate multiplier for the selected currency",
|
|
121
|
+
group: "Cache",
|
|
122
|
+
palette: true,
|
|
123
|
+
slash: { name: "cache-rate" },
|
|
124
|
+
run: async () => {
|
|
125
|
+
const val = await context.ui.dialog.prompt({
|
|
126
|
+
title: "Exchange Rate",
|
|
127
|
+
message: "Enter the exchange rate from USD to your currency (e.g. 7.2 for CNY)",
|
|
128
|
+
placeholder: "1.0",
|
|
129
|
+
})
|
|
130
|
+
if (val === undefined) return
|
|
131
|
+
const n = parseFloat(val)
|
|
132
|
+
if (n > 0) {
|
|
133
|
+
await api.kv.set(`${KV_PREFIX}.rate`, n)
|
|
134
|
+
signals.setExchangeRate(n)
|
|
135
|
+
context.ui.toast.show({ message: t()("rateSet", { r: n }) })
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
// ── /cache-section 开关区块与边框 ──
|
|
140
|
+
{
|
|
141
|
+
id: "opencode-visual-cache.cache.section",
|
|
142
|
+
title: "Cache: Toggle Section",
|
|
143
|
+
description: "Show or hide a sidebar section",
|
|
144
|
+
group: "Cache",
|
|
145
|
+
palette: true,
|
|
146
|
+
slash: { name: "cache-section" },
|
|
147
|
+
run: async () => {
|
|
148
|
+
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
149
|
+
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
150
|
+
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
151
|
+
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
152
|
+
const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
153
|
+
const bottomOn = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true))
|
|
154
|
+
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
155
|
+
const labels: Record<string, string> = {
|
|
156
|
+
detail: t()("secDetail"),
|
|
157
|
+
model: t()("secModel"),
|
|
158
|
+
dist: t()("distTitle"),
|
|
159
|
+
skills: t()("secSkills"),
|
|
160
|
+
balance: t()("secBalance"),
|
|
161
|
+
bottom: t()("secBottom"),
|
|
162
|
+
border: t()("secBorder"),
|
|
163
|
+
}
|
|
164
|
+
const optTitle = (label: string, on: boolean) => `${visualPadEnd(label, 15)}[${on ? "ON" : "OFF"}]`
|
|
165
|
+
const opt = await context.ui.dialog.select<string>({
|
|
166
|
+
title: t()("secToggle"),
|
|
167
|
+
options: [
|
|
168
|
+
{ title: optTitle(labels.detail, detailOn), value: "detail" },
|
|
169
|
+
{ title: optTitle(labels.model, modelOn), value: "model" },
|
|
170
|
+
{ title: optTitle(labels.dist, distOn), value: "dist" },
|
|
171
|
+
{ title: optTitle(labels.skills, skillsOn), value: "skills" },
|
|
172
|
+
{ title: optTitle(labels.balance, balanceOn), value: "balance" },
|
|
173
|
+
{ title: optTitle(labels.bottom, bottomOn), value: "bottom" },
|
|
174
|
+
{ title: optTitle(labels.border, borderOn), value: "border" },
|
|
175
|
+
],
|
|
176
|
+
})
|
|
177
|
+
if (!opt) return
|
|
178
|
+
if (opt === "border") {
|
|
179
|
+
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
180
|
+
await api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
181
|
+
signals.setBorderVisible(!cur)
|
|
182
|
+
context.ui.toast.show({ message: !cur ? t()("borderShown") : t()("borderHidden") })
|
|
183
|
+
} else {
|
|
184
|
+
const key = `${KV_PREFIX}.section.${opt}`
|
|
185
|
+
const cur = Boolean(api.kv.get(key, true))
|
|
186
|
+
await api.kv.set(key, !cur)
|
|
187
|
+
if (opt === "detail") signals.setSectionDetail(!cur)
|
|
188
|
+
if (opt === "model") signals.setSectionModel(!cur)
|
|
189
|
+
if (opt === "dist") signals.setSectionDist(!cur)
|
|
190
|
+
if (opt === "skills") signals.setSectionSkills(!cur)
|
|
191
|
+
if (opt === "balance") signals.setSectionBalance(!cur)
|
|
192
|
+
if (opt === "bottom") signals.setSectionBottom(!cur)
|
|
193
|
+
const name = labels[opt] ?? opt
|
|
194
|
+
context.ui.toast.show({ message: t()(!cur ? "sectionShown" : "sectionHidden", { s: name }) })
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
// ── /cache-config 查看当前配置 ──
|
|
199
|
+
{
|
|
200
|
+
id: "opencode-visual-cache.cache.config",
|
|
201
|
+
title: "Cache: Show Config",
|
|
202
|
+
description: "Display the current plugin configuration",
|
|
203
|
+
group: "Cache",
|
|
204
|
+
palette: true,
|
|
205
|
+
slash: { name: "cache-config" },
|
|
206
|
+
run: () => {
|
|
207
|
+
const sym = api.kv.get<string>(`${KV_PREFIX}.currency`) ?? "$"
|
|
208
|
+
const rate = api.kv.get<number>(`${KV_PREFIX}.rate`) ?? 1
|
|
209
|
+
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
210
|
+
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
211
|
+
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
212
|
+
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
213
|
+
const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
214
|
+
const bottom = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true))
|
|
215
|
+
const on = (v: boolean) => v ? "ON" : "OFF"
|
|
216
|
+
context.ui.toast.show({
|
|
217
|
+
title: t()("panelConfigTitle"),
|
|
218
|
+
message: t()("panelConfigMsg", {
|
|
219
|
+
c: sym, r: rate,
|
|
220
|
+
d: on(detail), m: on(model),
|
|
221
|
+
t: on(dist), k: on(skills),
|
|
222
|
+
b: on(balance), f: on(bottom),
|
|
223
|
+
}),
|
|
224
|
+
})
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
// ── /cache-lang 切换显示语言 ──
|
|
228
|
+
{
|
|
229
|
+
id: "opencode-visual-cache.cache.lang",
|
|
230
|
+
title: "Cache: Switch Language",
|
|
231
|
+
description: "Switch display language (Chinese / English / 日本語 / 한국어)",
|
|
232
|
+
group: "Cache",
|
|
233
|
+
palette: true,
|
|
234
|
+
slash: { name: "cache-lang" },
|
|
235
|
+
run: async () => {
|
|
236
|
+
const cur = signals.langCode()
|
|
237
|
+
const opt = await context.ui.dialog.select<LangCode>({
|
|
238
|
+
title: t()("langTitle"),
|
|
239
|
+
options: LANG_META.map((m) => ({
|
|
240
|
+
title: `${visualPadEnd(m.label, 9)}${cur === m.code ? "\u2713" : ""}`,
|
|
241
|
+
value: m.code,
|
|
242
|
+
})),
|
|
243
|
+
})
|
|
244
|
+
if (!opt) return
|
|
245
|
+
await api.kv.set(`${KV_PREFIX}.lang`, opt)
|
|
246
|
+
signals.setLangCode(opt)
|
|
247
|
+
context.ui.toast.show({ message: t()("langSwitched") })
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
// ── /cache-balance 切换余额提供商 / 自动切换 ──
|
|
251
|
+
{
|
|
252
|
+
id: "opencode-visual-cache.cache.balance",
|
|
253
|
+
title: "Cache: Switch Balance Provider",
|
|
254
|
+
description: "切换余额提供商 / 自动切换当前会话提供商 | Switch balance provider / auto-switch session provider",
|
|
255
|
+
group: "Cache",
|
|
256
|
+
palette: true,
|
|
257
|
+
slash: { name: "cache-balance" },
|
|
258
|
+
run: async () => {
|
|
259
|
+
const current = signals.balanceProviderId()
|
|
260
|
+
const auto = signals.autoBalance()
|
|
261
|
+
const autoLabel = `${t()("autoSwitchOpt")} [${auto ? "ON" : "OFF"}]`
|
|
262
|
+
const opt = await context.ui.dialog.select<string>({
|
|
263
|
+
title: t()("balProvTitle"),
|
|
264
|
+
options: [
|
|
265
|
+
{ title: autoLabel, value: "__auto__" },
|
|
266
|
+
...balanceProviders.map((p) => ({
|
|
267
|
+
title: providerOptionTitle(p, current),
|
|
268
|
+
value: p.id,
|
|
269
|
+
})),
|
|
270
|
+
],
|
|
271
|
+
})
|
|
272
|
+
if (!opt) return
|
|
273
|
+
if (opt === "__auto__") {
|
|
274
|
+
const next = !auto
|
|
275
|
+
await api.kv.set(`${KV_PREFIX}.balance.auto`, next)
|
|
276
|
+
signals.setAutoBalance(next)
|
|
277
|
+
context.ui.toast.show({ message: next ? t()("autoSwitchOn") : t()("autoSwitchOff") })
|
|
278
|
+
} else {
|
|
279
|
+
const provider = getBalanceProvider(opt)
|
|
280
|
+
// 手动切换会关闭自动切换
|
|
281
|
+
await api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id)
|
|
282
|
+
await api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
283
|
+
signals.setBalanceProviderId(provider.id)
|
|
284
|
+
signals.setAutoBalance(false)
|
|
285
|
+
signals.setBalanceUnsupported(false)
|
|
286
|
+
// 切换后立即按新 provider 刷新显示(无 key 时显示 idle,避免残留上一 provider 余额)
|
|
287
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
288
|
+
const hasKey = !!api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
289
|
+
if (!hasKey) {
|
|
290
|
+
// 未配置 key → 进入设置流程(对话框保持打开等待输入)
|
|
291
|
+
await promptBalanceKey(provider)
|
|
292
|
+
} else {
|
|
293
|
+
context.ui.toast.show({ message: t()("providerManual", { p: provider.name }) })
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
// ── /cache-balance-key 设置余额 API Key(两步:选 provider → 输 key)──
|
|
299
|
+
{
|
|
300
|
+
id: "opencode-visual-cache.cache.balance.key",
|
|
301
|
+
title: "Cache: Set Balance API Key",
|
|
302
|
+
description: "Select a provider and set its API key for balance display",
|
|
303
|
+
group: "Cache",
|
|
304
|
+
palette: true,
|
|
305
|
+
slash: { name: "cache-balance-key" },
|
|
306
|
+
run: async () => {
|
|
307
|
+
// 步骤 1:选择 provider
|
|
308
|
+
const opt = await context.ui.dialog.select<string>({
|
|
309
|
+
title: t()("balSelectTitle"),
|
|
310
|
+
options: balanceProviders.map((p) => ({
|
|
311
|
+
title: providerOptionTitle(p),
|
|
312
|
+
value: p.id,
|
|
313
|
+
})),
|
|
314
|
+
})
|
|
315
|
+
if (!opt) return
|
|
316
|
+
const provider = getBalanceProvider(opt)
|
|
317
|
+
// 手动指定 provider 会关闭自动切换
|
|
318
|
+
await api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id)
|
|
319
|
+
await api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
320
|
+
signals.setBalanceProviderId(provider.id)
|
|
321
|
+
signals.setAutoBalance(false)
|
|
322
|
+
// 切换后立即刷新显示(防止取消输入时残留上一 provider 的余额)
|
|
323
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
324
|
+
// 步骤 2:输入 key
|
|
325
|
+
await promptBalanceKey(provider)
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
// ── /cache-debug-skills 技能检测调试 ──
|
|
329
|
+
{
|
|
330
|
+
id: "opencode-visual-cache.cache.debug-skills",
|
|
331
|
+
title: "Cache: Debug Skills Detection",
|
|
332
|
+
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
333
|
+
group: "Cache",
|
|
334
|
+
palette: true,
|
|
335
|
+
slash: { name: "cache-debug-skills" },
|
|
336
|
+
run: () => {
|
|
337
|
+
const sid = currentSessionID(context)
|
|
338
|
+
if (!sid) {
|
|
339
|
+
context.ui.toast.show({ message: t()("runInSession"), variant: "warning" })
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
const msgs = api.state.session.messages(sid)
|
|
343
|
+
const byTool: Record<string, number> = {}
|
|
344
|
+
const skillParts: string[] = []
|
|
345
|
+
for (const msg of msgs) {
|
|
346
|
+
if (msg.role !== "assistant") continue
|
|
347
|
+
for (const p of extractToolParts(msg)) {
|
|
348
|
+
const tool = String(p.tool ?? "?")
|
|
349
|
+
byTool[tool] = (byTool[tool] ?? 0) + 1
|
|
350
|
+
if (tool === "skill") {
|
|
351
|
+
const meta = p.state?.metadata
|
|
352
|
+
const rootMeta = p.metadata
|
|
353
|
+
skillParts.push(`state.metadata=${JSON.stringify(meta)} | root.metadata=${JSON.stringify(rootMeta)} | state.title="${p.state?.title}" | state.output[:80]="${String(p.state?.output ?? "").slice(0, 80)}"`)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const summary = Object.entries(byTool).map(([k, v]) => `${k}: ${v}`).join(" | ")
|
|
358
|
+
const extra = skillParts.length > 0
|
|
359
|
+
? "\n\nSkill parts:\n" + skillParts.join("\n")
|
|
360
|
+
: "\n\n⚠ No skill tool parts found — AI may be reading SKILL.md instead. Try: 'Use the skill tool to load karpathy-guidelines'"
|
|
361
|
+
context.ui.toast.show({
|
|
362
|
+
title: `Tool Summary (${Object.keys(byTool).length} types)`,
|
|
363
|
+
message: summary + extra,
|
|
364
|
+
})
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
// ── /cache-session 子代理缓存统计 ──
|
|
368
|
+
{
|
|
369
|
+
id: "opencode-visual-cache.cache.session",
|
|
370
|
+
title: "Cache: Sub-Agent Stats",
|
|
371
|
+
description: "View token cache statistics for a sub-agent by session ID",
|
|
372
|
+
group: "Cache",
|
|
373
|
+
palette: true,
|
|
374
|
+
slash: { name: "cache-session" },
|
|
375
|
+
run: async () => {
|
|
376
|
+
// ── 扫描当前主 session 的子代理 session ID 列表 ──
|
|
377
|
+
const parentSid = currentSessionID(context)
|
|
378
|
+
const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
|
|
379
|
+
|
|
380
|
+
interface ChildEntry { title: string; value: string; description: string }
|
|
381
|
+
const children: ChildEntry[] = []
|
|
382
|
+
if (parentSid) {
|
|
383
|
+
try {
|
|
384
|
+
const msgs = api.state.session.messages(parentSid)
|
|
385
|
+
for (const msg of msgs) {
|
|
386
|
+
if (msg.role !== "assistant") continue
|
|
387
|
+
for (const p of extractToolParts(msg)) {
|
|
388
|
+
const tool = String(p.tool ?? "")
|
|
389
|
+
if (!SUBAGENT_TOOLS.has(tool)) continue
|
|
390
|
+
const st = p.state as Record<string, unknown> | undefined
|
|
391
|
+
const stMeta = st?.metadata as Record<string, unknown> | undefined
|
|
392
|
+
const subSid = stMeta?.session_id ?? stMeta?.sessionId
|
|
393
|
+
if (!subSid) continue
|
|
394
|
+
const sidStr = String(subSid)
|
|
395
|
+
const input = st?.input as Record<string, unknown> | undefined
|
|
396
|
+
const agent = String(p.subagent_type ?? input?.subagent_type ?? input?.category ?? tool)
|
|
397
|
+
const prompt = String(input?.prompt ?? "")
|
|
398
|
+
const desc = input?.description ? String(input.description) : ""
|
|
399
|
+
const title = desc || prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 40) || agent
|
|
400
|
+
children.push({ title, value: sidStr, description: `${agent} · ${sidStr.slice(0, 24)}…` })
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch {}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 去重
|
|
407
|
+
const seen = new Set<string>()
|
|
408
|
+
const unique = children.filter(c => { if (seen.has(c.value)) return false; seen.add(c.value); return true })
|
|
409
|
+
|
|
410
|
+
if (unique.length > 0) {
|
|
411
|
+
// ── 有子代理 → DialogSelect 列表选择 ──
|
|
412
|
+
const currentSid = signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "")
|
|
413
|
+
const options = unique.map((c, i) => ({
|
|
414
|
+
title: `${i + 1}. ${c.title}`,
|
|
415
|
+
value: c.value,
|
|
416
|
+
description: c.description,
|
|
417
|
+
}))
|
|
418
|
+
// 首尾各放一个"回到主会话",长列表时顶部底部均可直达
|
|
419
|
+
const backValue = "__main__"
|
|
420
|
+
const backTitle = `\u2500 ${t()("backToMainTitle")}`
|
|
421
|
+
options.unshift({ title: backTitle, value: backValue, description: "" })
|
|
422
|
+
options.push({ title: backTitle, value: backValue, description: "" })
|
|
423
|
+
const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1
|
|
424
|
+
const opt = await context.ui.dialog.select<string>({
|
|
425
|
+
title: t()("subSelectTitle"),
|
|
426
|
+
options,
|
|
427
|
+
current: currentIdx >= 0 ? options[currentIdx].value : undefined,
|
|
428
|
+
})
|
|
429
|
+
if (!opt) return
|
|
430
|
+
if (opt === backValue) {
|
|
431
|
+
signals.setOverrideSessionId(undefined)
|
|
432
|
+
await api.kv.set(`${KV_PREFIX}.session`, "")
|
|
433
|
+
context.ui.toast.show({ message: t()("backToMain") })
|
|
434
|
+
} else {
|
|
435
|
+
signals.setOverrideSessionId(opt)
|
|
436
|
+
await api.kv.set(`${KV_PREFIX}.session`, opt)
|
|
437
|
+
context.ui.toast.show({ message: t()("subAgentSwitched", { s: opt.slice(0, 24) + "\u2026" }) })
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
// ── 无子代理 → DialogPrompt 手动粘贴 ──
|
|
441
|
+
const val = await context.ui.dialog.prompt({
|
|
442
|
+
title: signals.overrideSessionId() ? t()("subSwitchTitle") : t()("subViewTitle"),
|
|
443
|
+
message: t()("subNoFound"),
|
|
444
|
+
placeholder: "ses_...",
|
|
445
|
+
})
|
|
446
|
+
if (val === undefined) return
|
|
447
|
+
const sid = val.trim()
|
|
448
|
+
if (sid) {
|
|
449
|
+
signals.setOverrideSessionId(sid)
|
|
450
|
+
await api.kv.set(`${KV_PREFIX}.session`, sid)
|
|
451
|
+
context.ui.toast.show({ message: t()("subAgentSwitched", { s: sid.slice(0, 24) + "\u2026" }) })
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
// ── /cache-session-back 返回主会话 ──
|
|
457
|
+
{
|
|
458
|
+
id: "opencode-visual-cache.cache.session.back",
|
|
459
|
+
title: "Cache: Back to Main",
|
|
460
|
+
description: "Return to main session stats",
|
|
461
|
+
group: "Cache",
|
|
462
|
+
palette: true,
|
|
463
|
+
slash: { name: "cache-session-back" },
|
|
464
|
+
run: async () => {
|
|
465
|
+
signals.setOverrideSessionId(undefined)
|
|
466
|
+
await api.kv.set(`${KV_PREFIX}.session`, "")
|
|
467
|
+
context.ui.toast.show({ message: t()("backToMain") })
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
]
|
|
471
|
+
}
|