opencode-visual-cache 1.2.16 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-visual-cache",
3
- "version": "1.2.16",
3
+ "version": "1.3.0",
4
4
  "description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.2.16";
2
+ export const PLUGIN_VERSION="1.3.0";
package/src/index.tsx CHANGED
@@ -223,6 +223,16 @@ function desaturateTo(raw: unknown, maxSat: number, fallback: string): string {
223
223
  return "#" + [nr, ng, nb].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
224
224
  }
225
225
 
226
+ /** Darken a hex colour by multiplying each channel by `factor` (0–1). */
227
+ function dimColor(hex: string, factor = 0.5): string {
228
+ const c = rgb(hex)
229
+ if (!c) return hex
230
+ const r = Math.round(c.r * factor)
231
+ const g = Math.round(c.g * factor)
232
+ const b = Math.round(c.b * factor)
233
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")
234
+ }
235
+
226
236
  // Morandi fallbacks — used when a theme colour cannot be resolved
227
237
  const FALLBACK = {
228
238
  primary: "#8B9DAF",
@@ -345,6 +355,9 @@ interface PanelSignals {
345
355
  setSectionSkills: (v: boolean) => void
346
356
  borderVisible: () => boolean
347
357
  setBorderVisible: (v: boolean) => void
358
+ /** When set, the panel renders stats for this session instead of the main one. */
359
+ overrideSessionId: () => string | undefined
360
+ setOverrideSessionId: (v: string | undefined) => void
348
361
  }
349
362
 
350
363
  const CURRENCIES: Record<string, string> = {
@@ -424,8 +437,21 @@ function TokenCachePanel(props: {
424
437
  })
425
438
  const [refreshTick, setRefreshTick] = createSignal(0)
426
439
 
440
+ // ── auto-clear override when the user navigates to a different main session ──
441
+ let lastMainSid = props.sessionId
427
442
  createEffect(() => {
428
443
  const sid = props.sessionId
444
+ if (sid !== lastMainSid) {
445
+ lastMainSid = sid
446
+ if (props.signals.overrideSessionId()) {
447
+ props.signals.setOverrideSessionId(undefined)
448
+ props.api.kv.set(`${KV_PREFIX}.session`, "")
449
+ }
450
+ }
451
+ })
452
+
453
+ createEffect(() => {
454
+ const sid = props.signals.overrideSessionId() ?? props.sessionId
429
455
  void refreshTick()
430
456
  void partVersion()
431
457
 
@@ -751,7 +777,7 @@ function TokenCachePanel(props: {
751
777
  <span style={{ fg: pal().primary }}>
752
778
  <b>{t().title}</b>
753
779
  <Show when={open()}>
754
- <span style={{ fg: pal().muted }}> (v{PLUGIN_VERSION})</span>
780
+ <span style={{ fg: dimColor(pal().muted, 0.75) }}> v{PLUGIN_VERSION}</span>
755
781
  </Show>
756
782
  </span>
757
783
  <Show when={!open() && data().hasData}>
@@ -774,6 +800,18 @@ function TokenCachePanel(props: {
774
800
  </text>
775
801
 
776
802
  <Show when={open()}>
803
+ <Show when={props.signals.overrideSessionId()}>
804
+ {(() => {
805
+ const prefix = " \u21b3 " + (langZH() ? "\u5B50\u4EE3\u7406: " : "Sub: ")
806
+ const maxSidW = Math.max(6, panelWidth() - visualWidth(prefix))
807
+ return (
808
+ <text>
809
+ <span style={{ fg: pal().muted }}>{prefix}</span>
810
+ <span style={{ fg: pal().text }}>{truncateVisual(props.signals.overrideSessionId()!, maxSidW)}</span>
811
+ </text>
812
+ )
813
+ })()}
814
+ </Show>
777
815
  <Show when={data().hasData} fallback={
778
816
  <>
779
817
  <text fg={pal().muted}>{sep()}</text>
@@ -950,10 +988,19 @@ function TokenCachePanel(props: {
950
988
  // ---------------------------------------------------------------------------
951
989
 
952
990
  function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin {
991
+ let lastSlotSid = ""
953
992
  return {
954
993
  order: 55,
955
994
  slots: {
956
995
  sidebar_content(ctx: TuiSlotContext, input: { session_id: string }): JSX.Element {
996
+ // ── auto-clear override when the user navigates to a different main session ──
997
+ if (input.session_id !== lastSlotSid) {
998
+ lastSlotSid = input.session_id
999
+ if (signals.overrideSessionId()) {
1000
+ signals.setOverrideSessionId(undefined)
1001
+ api.kv.set("cache_panel.session", "")
1002
+ }
1003
+ }
957
1004
  return (
958
1005
  <TokenCachePanel
959
1006
  theme={ctx.theme.current}
@@ -977,6 +1024,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
977
1024
  const [sectionSkills, setSectionSkills] = createSignal(true)
978
1025
  const [borderVisible, setBorderVisible] = createSignal(true)
979
1026
  const [langZH, setLangZH] = createSignal(LANG_ZH)
1027
+ const [overrideSessionId, setOverrideSessionId] = createSignal<string | undefined>(undefined)
980
1028
 
981
1029
  const signals: PanelSignals = {
982
1030
  currencySymbol, setCurrencySymbol,
@@ -987,6 +1035,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
987
1035
  sectionDist, setSectionDist,
988
1036
  sectionSkills, setSectionSkills,
989
1037
  borderVisible, setBorderVisible,
1038
+ overrideSessionId, setOverrideSessionId,
990
1039
  }
991
1040
 
992
1041
  api.slots.register(createSidebarSlot(api, signals))
@@ -1174,6 +1223,120 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1174
1223
  })
1175
1224
  },
1176
1225
  },
1226
+ {
1227
+ title: "Cache: Sub-Agent Stats",
1228
+ value: "cache.session",
1229
+ description: "View token cache statistics for a sub-agent by session ID",
1230
+ slash: { name: "cache-session" },
1231
+ onSelect: (dialog) => {
1232
+ // ── 扫描当前主 session 的子代理 session ID 列表 ──
1233
+ const rt = api.route.current
1234
+ const parentSid = rt.name === "session" && rt.params ? String(rt.params.sessionID) : ""
1235
+ const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
1236
+
1237
+ interface ChildEntry { title: string; value: string; description: string }
1238
+ const children: ChildEntry[] = []
1239
+ if (parentSid) {
1240
+ try {
1241
+ const msgs = api.state.session.messages(parentSid)
1242
+ for (const msg of msgs) {
1243
+ if (msg.role !== "assistant") continue
1244
+ let parts: readonly Part[] = []
1245
+ try { parts = api.state.part(msg.id) } catch {}
1246
+ for (const p of parts) {
1247
+ if (p.type !== "tool") continue
1248
+ const tool = String((p as ToolPart).tool ?? "")
1249
+ if (!SUBAGENT_TOOLS.has(tool)) continue
1250
+ const st = (p as any).state as Record<string, unknown> | undefined
1251
+ const stMeta = st?.metadata as Record<string, unknown> | undefined
1252
+ const subSid = stMeta?.session_id ?? stMeta?.sessionId
1253
+ if (!subSid) continue
1254
+ const sidStr = String(subSid)
1255
+ const input = st?.input as Record<string, unknown> | undefined
1256
+ const agent = String((p as any).subagent_type ?? input?.subagent_type ?? input?.category ?? tool)
1257
+ const prompt = String(input?.prompt ?? "")
1258
+ const desc = input?.description ? String(input.description) : ""
1259
+ const title = desc || prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 40) || agent
1260
+ children.push({ title, value: sidStr, description: `${agent} · ${sidStr.slice(0, 24)}…` })
1261
+ }
1262
+ }
1263
+ } catch {}
1264
+ }
1265
+
1266
+ // 去重
1267
+ const seen = new Set<string>()
1268
+ const unique = children.filter(c => { if (seen.has(c.value)) return false; seen.add(c.value); return true })
1269
+
1270
+ if (unique.length > 0) {
1271
+ // ── 有子代理 → DialogSelect 列表选择 ──
1272
+ const zh = langZH()
1273
+ const currentSid = signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "")
1274
+ const options = unique.map((c, i) => ({
1275
+ title: `${i + 1}. ${c.title}`,
1276
+ value: c.value,
1277
+ description: c.description,
1278
+ }))
1279
+ // 首尾各放一个"回到主会话",长列表时顶部底部均可直达
1280
+ const backValue = "__main__"
1281
+ const backTitle = `\u2500 ${zh ? "\u56DE\u5230\u4E3B\u4F1A\u8BDD" : "Back to Main"}`
1282
+ options.unshift({ title: backTitle, value: backValue, description: "" })
1283
+ options.push({ title: backTitle, value: backValue, description: "" })
1284
+ const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1
1285
+ dialog?.replace(() => (
1286
+ <api.ui.DialogSelect
1287
+ title={zh ? "选择子代理" : "Select Sub-Agent"}
1288
+ options={options}
1289
+ current={currentIdx >= 0 ? options[currentIdx].value : undefined}
1290
+ onSelect={(opt) => {
1291
+ if (opt.value === backValue) {
1292
+ signals.setOverrideSessionId(undefined)
1293
+ api.kv.set(`${KV_PREFIX}.session`, "")
1294
+ api.ui.toast({ message: zh ? "已切回主会话" : "Switched to main session" })
1295
+ } else {
1296
+ signals.setOverrideSessionId(opt.value)
1297
+ api.kv.set(`${KV_PREFIX}.session`, opt.value)
1298
+ api.ui.toast({ message: (zh ? "已切换至子代理: " : "Showing sub-agent: ") + opt.value.slice(0, 24) + "\u2026" })
1299
+ }
1300
+ dialog?.clear()
1301
+ }}
1302
+ />
1303
+ ))
1304
+ } else {
1305
+ // ── 无子代理 → DialogPrompt 手动粘贴 ──
1306
+ const zh = langZH()
1307
+ dialog?.replace(() => (
1308
+ <api.ui.DialogPrompt
1309
+ title={signals.overrideSessionId() ? zh ? "切换子代理" : "Switch Sub" : zh ? "查看子代理缓存" : "View Sub Cache"}
1310
+ description={() => <text>{zh ? "未找到子代理,请手动粘贴 Session ID" : "No sub-agents found. Paste a Session ID manually"}</text>}
1311
+ placeholder="ses_..."
1312
+ value={signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "") ?? ""}
1313
+ onConfirm={(val) => {
1314
+ const sid = val.trim()
1315
+ if (sid) {
1316
+ signals.setOverrideSessionId(sid)
1317
+ api.kv.set(`${KV_PREFIX}.session`, sid)
1318
+ api.ui.toast({ message: (langZH() ? "已切换至子代理: " : "Showing sub-agent: ") + sid.slice(0, 24) + "\u2026" })
1319
+ }
1320
+ dialog?.clear()
1321
+ }}
1322
+ onCancel={() => dialog?.clear()}
1323
+ />
1324
+ ))
1325
+ }
1326
+ },
1327
+ },
1328
+ {
1329
+ title: "Cache: Back to Main",
1330
+ value: "cache.session.back",
1331
+ description: "Return to main session stats",
1332
+ slash: { name: "cache-session-back" },
1333
+ onSelect: (dialog) => {
1334
+ signals.setOverrideSessionId(undefined)
1335
+ api.kv.set(`${KV_PREFIX}.session`, "")
1336
+ api.ui.toast({ message: langZH() ? "已切回主会话" : "Switched to main session" })
1337
+ dialog?.clear()
1338
+ },
1339
+ },
1177
1340
  ])
1178
1341
  }
1179
1342