opencode-visual-cache 1.7.0-beta.2 → 1.7.0-beta.4

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.
@@ -1,69 +1,69 @@
1
- /** @jsxImportSource @opentui/solid */
2
-
3
- import { createMemo, For, Show } from "solid-js"
4
- import type { Context } from "./types"
5
- import { collectSessionStats, hitRate } from "./data"
6
- import { fmt, fmtCost, progressBar, visualPadEnd } from "../core"
7
-
8
- const LABEL_GAP = 1
9
- const BAR_BRACKETS = 2
10
- const BAR_GAP = 1
11
- const PCT_FIXED_WIDTH = 5
12
- const MIN_PANEL_WIDTH = 20
13
- const DEFAULT_PANEL_WIDTH = 26
14
-
15
- export function SidebarView(props: { context: Context; sessionID: string }) {
16
- const stats = createMemo(() => collectSessionStats(props.context, props.sessionID))
17
- const rate = createMemo(() => hitRate(stats()))
18
- const theme = props.context.theme
19
-
20
- const rateColor = () => {
21
- const r = rate()
22
- if (r >= 85) return theme.text.feedback.success.default
23
- if (r >= 70) return theme.text.feedback.warning.default
24
- return theme.text.feedback.error.default
25
- }
26
- const barWidth = () =>
27
- Math.max(MIN_PANEL_WIDTH, Math.min(DEFAULT_PANEL_WIDTH, props.context.app.version ? DEFAULT_PANEL_WIDTH : DEFAULT_PANEL_WIDTH)) -
28
- LABEL_GAP - BAR_BRACKETS - BAR_GAP - PCT_FIXED_WIDTH - 1
29
-
30
- const rows = () => {
31
- const s = stats()
32
- return [
33
- { label: "Hit", value: fmt(s.cacheRead) + " tok", color: rateColor() },
34
- { label: "Miss", value: fmt(s.input + s.cacheWrite) + " tok", color: theme.text.default },
35
- { label: "Write", value: fmt(s.cacheWrite) + " tok", color: theme.text.subdued },
36
- { label: "Out", value: fmt(s.output) + " tok", color: theme.text.subdued },
37
- ]
38
- }
39
-
40
- return (
41
- <Show when={stats().hasData}>
42
- <box>
43
- <box flexDirection="row" gap={LABEL_GAP}>
44
- <text fg={theme.text.default}>Cache</text>
45
- <text fg={rateColor()}>
46
- {"["}
47
- {progressBar(rate(), barWidth())}
48
- {"]"}
49
- </text>
50
- <text fg={rateColor()}>{rate().toFixed(1) + "%"}</text>
51
- </box>
52
- <For each={rows()}>
53
- {(row) => (
54
- <box flexDirection="row" gap={LABEL_GAP}>
55
- <text fg={theme.text.default}>{visualPadEnd(row.label, 5)}</text>
56
- <text fg={row.color}>{row.value}</text>
57
- </box>
58
- )}
59
- </For>
60
- <Show when={stats().cost > 0}>
61
- <box flexDirection="row" gap={LABEL_GAP}>
62
- <text fg={theme.text.default}>{"Cost"}</text>
63
- <text fg={theme.text.default}>{fmtCost(stats().cost)}</text>
64
- </box>
65
- </Show>
66
- </box>
67
- </Show>
68
- )
69
- }
1
+ /** @jsxImportSource @opentui/solid */
2
+
3
+ import { createMemo, For, Show } from "solid-js"
4
+ import type { Context } from "./types"
5
+ import { collectSessionStats, hitRate } from "./data"
6
+ import { fmt, fmtCost, progressBar, visualPadEnd } from "../core"
7
+
8
+ const LABEL_GAP = 1
9
+ const BAR_BRACKETS = 2
10
+ const BAR_GAP = 1
11
+ const PCT_FIXED_WIDTH = 5
12
+ const MIN_PANEL_WIDTH = 20
13
+ const DEFAULT_PANEL_WIDTH = 26
14
+
15
+ export function SidebarView(props: { context: Context; sessionID: string }) {
16
+ const stats = createMemo(() => collectSessionStats(props.context, props.sessionID))
17
+ const rate = createMemo(() => hitRate(stats()))
18
+ const theme = props.context.theme
19
+
20
+ const rateColor = () => {
21
+ const r = rate()
22
+ if (r >= 85) return theme.text.feedback.success.default
23
+ if (r >= 70) return theme.text.feedback.warning.default
24
+ return theme.text.feedback.error.default
25
+ }
26
+ const barWidth = () =>
27
+ Math.max(MIN_PANEL_WIDTH, Math.min(DEFAULT_PANEL_WIDTH, props.context.app.version ? DEFAULT_PANEL_WIDTH : DEFAULT_PANEL_WIDTH)) -
28
+ LABEL_GAP - BAR_BRACKETS - BAR_GAP - PCT_FIXED_WIDTH - 1
29
+
30
+ const rows = () => {
31
+ const s = stats()
32
+ return [
33
+ { label: "Hit", value: fmt(s.cacheRead) + " tok", color: rateColor() },
34
+ { label: "Miss", value: fmt(s.input + s.cacheWrite) + " tok", color: theme.text.default },
35
+ { label: "Write", value: fmt(s.cacheWrite) + " tok", color: theme.text.subdued },
36
+ { label: "Out", value: fmt(s.output) + " tok", color: theme.text.subdued },
37
+ ]
38
+ }
39
+
40
+ return (
41
+ <Show when={stats().hasData}>
42
+ <box>
43
+ <box flexDirection="row" gap={LABEL_GAP}>
44
+ <text fg={theme.text.default}>Cache</text>
45
+ <text fg={rateColor()}>
46
+ {"["}
47
+ {progressBar(rate(), barWidth())}
48
+ {"]"}
49
+ </text>
50
+ <text fg={rateColor()}>{rate().toFixed(1) + "%"}</text>
51
+ </box>
52
+ <For each={rows()}>
53
+ {(row) => (
54
+ <box flexDirection="row" gap={LABEL_GAP}>
55
+ <text fg={theme.text.default}>{visualPadEnd(row.label, 5)}</text>
56
+ <text fg={row.color}>{row.value}</text>
57
+ </box>
58
+ )}
59
+ </For>
60
+ <Show when={stats().cost > 0}>
61
+ <box flexDirection="row" gap={LABEL_GAP}>
62
+ <text fg={theme.text.default}>{"Cost"}</text>
63
+ <text fg={theme.text.default}>{fmtCost(stats().cost)}</text>
64
+ </box>
65
+ </Show>
66
+ </box>
67
+ </Show>
68
+ )
69
+ }
package/src/v2/status.tsx CHANGED
@@ -1,96 +1,96 @@
1
- /** @jsxImportSource @opentui/solid */
2
-
3
- import { createMemo, For, Show } from "solid-js"
4
- import type { Context } from "./types"
5
- import type { PanelSignals } from "../panel/panel-api"
6
- import { collectLastHitRate } from "./data"
7
- import { desaturateTo, fmtCompact, formatBalanceText, MAX_SAT, FALLBACK, visualWidth } from "../core"
8
- import { createT } from "../i18n"
9
- import { mapTheme } from "./theme"
10
-
11
- /**
12
- * 底部状态栏(prompt.footer.status)——对齐 V1 BottomStatusBar 统计段口径:
13
- * 单条命中率(最后一条有 token 的 assistant 消息)+ 趋势 + Tokens 总量 + 余额。
14
- * 余额读共享 signals.balanceState(PluginRoot 驱动轮询);provider 不支持余额时隐藏余额段(对齐 V1)。
15
- * 颜色经 mapTheme(V1 形状)——与侧边栏命中率颜色同源,保证两处一致。
16
- */
17
- export function StatusView(props: {
18
- context: Context
19
- signals: PanelSignals
20
- sessionID: string
21
- }) {
22
- const t = createT(() => props.signals.langCode())
23
-
24
- // 与侧边栏 TokenCachePanel 同一颜色来源(mapTheme → desaturateTo)
25
- const pal = createMemo(() => {
26
- const th = mapTheme(props.context.theme) as unknown as Record<string, string>
27
- const sat = (k: string, fb: string) => desaturateTo(th[k], MAX_SAT, fb)
28
- return {
29
- text: sat("text", FALLBACK.text),
30
- muted: sat("textMuted", FALLBACK.muted),
31
- success: sat("success", FALLBACK.success),
32
- warning: sat("warning", FALLBACK.warning),
33
- error: sat("error", FALLBACK.error),
34
- }
35
- })
36
-
37
- const stats = createMemo(() => collectLastHitRate(props.context, props.sessionID))
38
-
39
- const hitColor = createMemo(() => {
40
- const r = stats().hitRate
41
- if (r >= 85) return pal().success
42
- if (r >= 70) return pal().warning
43
- return pal().error
44
- })
45
-
46
- // 命中率趋势:最后一条与上一条的差值;|Δ| < 0.05 视为无变化(null = 不显示)
47
- const trend = createMemo(() => {
48
- const s = stats()
49
- if (s.prevHitRate < 0 || s.hitRate < 0) return null
50
- const d = s.hitRate - s.prevHitRate
51
- return Math.abs(d) < 0.05 ? null : d
52
- })
53
-
54
- // 余额文本(对齐 V1):ok → 数值;loading → …;error → ⚠;idle → -
55
- const balanceText = createMemo(() => {
56
- const s = props.signals.balanceState()
57
- if (s.status === "ok" && s.data) return formatBalanceText(s.data, props.signals.balanceCurrency(), props.signals.exchangeRate())
58
- if (s.status === "loading") return "\u2026"
59
- if (s.status === "error") return "\u26a0"
60
- return "-"
61
- })
62
-
63
- const segs = createMemo<{ text: string; color: string | undefined }[]>(() => {
64
- const s = stats()
65
- const hr = s.hitRate >= 0 ? (Math.floor(s.hitRate * 10) / 10).toFixed(1) + "%" : "--"
66
- const out: { text: string; color: string | undefined }[] = [
67
- { text: t("barHit") + " ", color: pal().muted },
68
- { text: hr, color: hitColor() },
69
- ]
70
- const tr = trend()
71
- if (tr !== null) {
72
- out.push({ text: " " + (tr > 0 ? "\u2191" : "\u2193") + Math.abs(tr).toFixed(1) + "%", color: tr > 0 ? pal().success : pal().error })
73
- }
74
- out.push({ text: " \u00b7 " + t("barTok") + " ", color: pal().muted })
75
- out.push({ text: s ? fmtCompact(s.input + s.read + s.write) : "--", color: pal().text })
76
- if (!props.signals.balanceUnsupported()) {
77
- out.push({ text: " \u00b7 " + t("barBal") + " ", color: pal().muted })
78
- out.push({ text: balanceText(), color: pal().text })
79
- }
80
- out.push({ text: " \u00b7 ", color: pal().muted })
81
- return out
82
- })
83
- const segsW = createMemo(() => {
84
- let w = 0
85
- for (const sg of segs()) w += visualWidth(sg.text)
86
- return w
87
- })
88
-
89
- return (
90
- <Show when={segsW() > 0}>
91
- <text>
92
- <For each={segs()}>{(sg) => <span style={{ fg: sg.color }}>{sg.text}</span>}</For>
93
- </text>
94
- </Show>
95
- )
96
- }
1
+ /** @jsxImportSource @opentui/solid */
2
+
3
+ import { createMemo, For, Show } from "solid-js"
4
+ import type { Context } from "./types"
5
+ import type { PanelSignals } from "../panel/panel-api"
6
+ import { collectLastHitRate } from "./data"
7
+ import { desaturateTo, fmtCompact, formatBalanceText, MAX_SAT, FALLBACK, visualWidth } from "../core"
8
+ import { createT } from "../i18n"
9
+ import { mapTheme } from "./theme"
10
+
11
+ /**
12
+ * 底部状态栏(prompt.footer.status)——对齐 V1 BottomStatusBar 统计段口径:
13
+ * 单条命中率(最后一条有 token 的 assistant 消息)+ 趋势 + Tokens 总量 + 余额。
14
+ * 余额读共享 signals.balanceState(PluginRoot 驱动轮询);provider 不支持余额时隐藏余额段(对齐 V1)。
15
+ * 颜色经 mapTheme(V1 形状)——与侧边栏命中率颜色同源,保证两处一致。
16
+ */
17
+ export function StatusView(props: {
18
+ context: Context
19
+ signals: PanelSignals
20
+ sessionID: string
21
+ }) {
22
+ const t = createT(() => props.signals.langCode())
23
+
24
+ // 与侧边栏 TokenCachePanel 同一颜色来源(mapTheme → desaturateTo)
25
+ const pal = createMemo(() => {
26
+ const th = mapTheme(props.context.theme) as unknown as Record<string, string>
27
+ const sat = (k: string, fb: string) => desaturateTo(th[k], MAX_SAT, fb)
28
+ return {
29
+ text: sat("text", FALLBACK.text),
30
+ muted: sat("textMuted", FALLBACK.muted),
31
+ success: sat("success", FALLBACK.success),
32
+ warning: sat("warning", FALLBACK.warning),
33
+ error: sat("error", FALLBACK.error),
34
+ }
35
+ })
36
+
37
+ const stats = createMemo(() => collectLastHitRate(props.context, props.sessionID))
38
+
39
+ const hitColor = createMemo(() => {
40
+ const r = stats().hitRate
41
+ if (r >= 85) return pal().success
42
+ if (r >= 70) return pal().warning
43
+ return pal().error
44
+ })
45
+
46
+ // 命中率趋势:最后一条与上一条的差值;|Δ| < 0.05 视为无变化(null = 不显示)
47
+ const trend = createMemo(() => {
48
+ const s = stats()
49
+ if (s.prevHitRate < 0 || s.hitRate < 0) return null
50
+ const d = s.hitRate - s.prevHitRate
51
+ return Math.abs(d) < 0.05 ? null : d
52
+ })
53
+
54
+ // 余额文本(对齐 V1):ok → 数值;loading → …;error → ⚠;idle → -
55
+ const balanceText = createMemo(() => {
56
+ const s = props.signals.balanceState()
57
+ if (s.status === "ok" && s.data) return formatBalanceText(s.data, props.signals.balanceCurrency(), props.signals.exchangeRate())
58
+ if (s.status === "loading") return "\u2026"
59
+ if (s.status === "error") return "\u26a0"
60
+ return "-"
61
+ })
62
+
63
+ const segs = createMemo<{ text: string; color: string | undefined }[]>(() => {
64
+ const s = stats()
65
+ const hr = s.hitRate >= 0 ? (Math.floor(s.hitRate * 10) / 10).toFixed(1) + "%" : "--"
66
+ const out: { text: string; color: string | undefined }[] = [
67
+ { text: t("barHit") + " ", color: pal().muted },
68
+ { text: hr, color: hitColor() },
69
+ ]
70
+ const tr = trend()
71
+ if (tr !== null) {
72
+ out.push({ text: " " + (tr > 0 ? "\u2191" : "\u2193") + Math.abs(tr).toFixed(1) + "%", color: tr > 0 ? pal().success : pal().error })
73
+ }
74
+ out.push({ text: " \u00b7 " + t("barTok") + " ", color: pal().muted })
75
+ out.push({ text: s ? fmtCompact(s.input + s.read + s.write) : "--", color: pal().text })
76
+ if (!props.signals.balanceUnsupported()) {
77
+ out.push({ text: " \u00b7 " + t("barBal") + " ", color: pal().muted })
78
+ out.push({ text: balanceText(), color: pal().text })
79
+ }
80
+ out.push({ text: " \u00b7 ", color: pal().muted })
81
+ return out
82
+ })
83
+ const segsW = createMemo(() => {
84
+ let w = 0
85
+ for (const sg of segs()) w += visualWidth(sg.text)
86
+ return w
87
+ })
88
+
89
+ return (
90
+ <Show when={segsW() > 0}>
91
+ <text>
92
+ <For each={segs()}>{(sg) => <span style={{ fg: sg.color }}>{sg.text}</span>}</For>
93
+ </text>
94
+ </Show>
95
+ )
96
+ }
package/src/v2/theme.ts CHANGED
@@ -1,19 +1,19 @@
1
- import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
2
- import type { Context } from "./types"
3
-
4
- /** V2 theme → V1 形状映射(组件按 primary/text/textMuted/… 字段消费)。
5
- * 侧边栏与底部栏共用——保证命中率颜色等两处一致。
6
- * - V1 primary → V2 interactive(v1-migrate.ts 官方映射:interactive = hues.byToken.primary)
7
- * - step 取 300:更亮(对齐 V2 暗色强调惯例的亮端),500/400 在暗背景下偏深
8
- */
9
- export function mapTheme(theme: Context["theme"]): TuiThemeCurrent {
10
- return {
11
- primary: theme.hue.interactive[300],
12
- text: theme.text.default,
13
- textMuted: theme.text.subdued,
14
- success: theme.text.feedback.success.default,
15
- warning: theme.text.feedback.warning.default,
16
- error: theme.text.feedback.error.default,
17
- border: theme.text.subdued,
18
- } as unknown as TuiThemeCurrent
19
- }
1
+ import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui"
2
+ import type { Context } from "./types"
3
+
4
+ /** V2 theme → V1 形状映射(组件按 primary/text/textMuted/… 字段消费)。
5
+ * 侧边栏与底部栏共用——保证命中率颜色等两处一致。
6
+ * - V1 primary → V2 interactive(v1-migrate.ts 官方映射:interactive = hues.byToken.primary)
7
+ * - step 取 300:更亮(对齐 V2 暗色强调惯例的亮端),500/400 在暗背景下偏深
8
+ */
9
+ export function mapTheme(theme: Context["theme"]): TuiThemeCurrent {
10
+ return {
11
+ primary: theme.hue.interactive[300],
12
+ text: theme.text.default,
13
+ textMuted: theme.text.subdued,
14
+ success: theme.text.feedback.success.default,
15
+ warning: theme.text.feedback.warning.default,
16
+ error: theme.text.feedback.error.default,
17
+ border: theme.text.subdued,
18
+ } as unknown as TuiThemeCurrent
19
+ }