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,81 +1,81 @@
1
- /** CJK characters occupy 2 terminal columns; padEnd/padStart count
2
- * string length (=1 per char), which breaks alignment with mixed text. */
3
-
4
- export function charColumns(c: string): number {
5
- const code = c.codePointAt(0) ?? 0
6
- if (code < 0x20) return 0 // control
7
- if (code < 0x7F) return 1 // ASCII
8
- if (code < 0xA0) return 0 // C1 controls
9
- // East-Asian wide / fullwidth ranges
10
- if ((code >= 0x1100 && code <= 0x115F) || // Hangul Jamo
11
- (code >= 0x2E80 && code <= 0xA4CF) || // CJK Radicals … Yi
12
- (code >= 0xAC00 && code <= 0xD7A3) || // Hangul
13
- (code >= 0xF900 && code <= 0xFAFF) || // CJK Compat
14
- (code >= 0xFE10 && code <= 0xFE6F) || // Vertical / Compat
15
- (code >= 0xFF01 && code <= 0xFF60) || // Fullwidth
16
- (code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth signs
17
- (code >= 0x1F300 && code <= 0x1F64F) || // Misc Symbols (emoji)
18
- (code >= 0x20000 && code <= 0x3FFFD)) // SIP / TIP
19
- return 2
20
- return 1
21
- }
22
-
23
- export function visualWidth(s: string): number {
24
- let w = 0; for (const c of s) w += charColumns(c); return w
25
- }
26
-
27
- export function visualPadEnd(s: string, cols: number): string {
28
- const pad = cols - visualWidth(s)
29
- return pad > 0 ? s + " ".repeat(pad) : s
30
- }
31
-
32
- /** Truncate `s` to fit within `maxCols` visual columns, appending "…" when cut. */
33
- export function truncateVisual(s: string, maxCols: number): string {
34
- if (visualWidth(s) <= maxCols) return s
35
- let result = "", w = 0
36
- for (const c of s) {
37
- const cw = charColumns(c)
38
- if (w + cw > maxCols - 1) { result += "\u2026"; break }
39
- result += c; w += cw
40
- }
41
- return result
42
- }
43
-
44
- export function progressBar(percent: number, width: number): string {
45
- const clamped = Math.max(0, Math.min(100, percent))
46
- const filled = Math.round((clamped / 100) * width)
47
- const empty = Math.max(0, width - filled)
48
- return "\u2588".repeat(filled) + "\u2591".repeat(empty)
49
- }
50
-
51
- export function fmt(n: number): string {
52
- if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"
53
- if (n >= 10_000) return (n / 1_000).toFixed(1) + "K"
54
- return n.toLocaleString("en-US")
55
- }
56
-
57
- export function num(v: unknown): number {
58
- return typeof v === "number" && Number.isFinite(v) ? v : 0
59
- }
60
-
61
- export function fmtCost(n: number, symbol = "$", rate = 1): string {
62
- const v = n * rate
63
- if (v >= 1) return symbol + v.toFixed(2)
64
- if (v >= 0.01) return symbol + v.toFixed(3)
65
- return symbol + v.toFixed(4)
66
- }
67
-
68
- /** 紧凑数字缩写(底部状态栏用):1234 → "1.2K",1234567 → "1.2M"。 */
69
- export function fmtCompact(n: number): string {
70
- if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"
71
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"
72
- return String(Math.round(n))
73
- }
74
-
75
- /** 余额数值格式化:≥1 或 0 显示固定 2 位小数;小额(<1)保留精度(最多 6 位),避免抹成 0.00。 */
76
- export function formatBalanceAmount(total: string): string {
77
- const n = parseFloat(total)
78
- if (!Number.isFinite(n)) return total
79
- if (n === 0 || n >= 1) return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
80
- return n.toLocaleString("en-US", { maximumFractionDigits: 6 })
81
- }
1
+ /** CJK characters occupy 2 terminal columns; padEnd/padStart count
2
+ * string length (=1 per char), which breaks alignment with mixed text. */
3
+
4
+ export function charColumns(c: string): number {
5
+ const code = c.codePointAt(0) ?? 0
6
+ if (code < 0x20) return 0 // control
7
+ if (code < 0x7F) return 1 // ASCII
8
+ if (code < 0xA0) return 0 // C1 controls
9
+ // East-Asian wide / fullwidth ranges
10
+ if ((code >= 0x1100 && code <= 0x115F) || // Hangul Jamo
11
+ (code >= 0x2E80 && code <= 0xA4CF) || // CJK Radicals … Yi
12
+ (code >= 0xAC00 && code <= 0xD7A3) || // Hangul
13
+ (code >= 0xF900 && code <= 0xFAFF) || // CJK Compat
14
+ (code >= 0xFE10 && code <= 0xFE6F) || // Vertical / Compat
15
+ (code >= 0xFF01 && code <= 0xFF60) || // Fullwidth
16
+ (code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth signs
17
+ (code >= 0x1F300 && code <= 0x1F64F) || // Misc Symbols (emoji)
18
+ (code >= 0x20000 && code <= 0x3FFFD)) // SIP / TIP
19
+ return 2
20
+ return 1
21
+ }
22
+
23
+ export function visualWidth(s: string): number {
24
+ let w = 0; for (const c of s) w += charColumns(c); return w
25
+ }
26
+
27
+ export function visualPadEnd(s: string, cols: number): string {
28
+ const pad = cols - visualWidth(s)
29
+ return pad > 0 ? s + " ".repeat(pad) : s
30
+ }
31
+
32
+ /** Truncate `s` to fit within `maxCols` visual columns, appending "…" when cut. */
33
+ export function truncateVisual(s: string, maxCols: number): string {
34
+ if (visualWidth(s) <= maxCols) return s
35
+ let result = "", w = 0
36
+ for (const c of s) {
37
+ const cw = charColumns(c)
38
+ if (w + cw > maxCols - 1) { result += "\u2026"; break }
39
+ result += c; w += cw
40
+ }
41
+ return result
42
+ }
43
+
44
+ export function progressBar(percent: number, width: number): string {
45
+ const clamped = Math.max(0, Math.min(100, percent))
46
+ const filled = Math.round((clamped / 100) * width)
47
+ const empty = Math.max(0, width - filled)
48
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty)
49
+ }
50
+
51
+ export function fmt(n: number): string {
52
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"
53
+ if (n >= 10_000) return (n / 1_000).toFixed(1) + "K"
54
+ return n.toLocaleString("en-US")
55
+ }
56
+
57
+ export function num(v: unknown): number {
58
+ return typeof v === "number" && Number.isFinite(v) ? v : 0
59
+ }
60
+
61
+ export function fmtCost(n: number, symbol = "$", rate = 1): string {
62
+ const v = n * rate
63
+ if (v >= 1) return symbol + v.toFixed(2)
64
+ if (v >= 0.01) return symbol + v.toFixed(3)
65
+ return symbol + v.toFixed(4)
66
+ }
67
+
68
+ /** 紧凑数字缩写(底部状态栏用):1234 → "1.2K",1234567 → "1.2M"。 */
69
+ export function fmtCompact(n: number): string {
70
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"
71
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"
72
+ return String(Math.round(n))
73
+ }
74
+
75
+ /** 余额数值格式化:≥1 或 0 显示固定 2 位小数;小额(<1)保留精度(最多 6 位),避免抹成 0.00。 */
76
+ export function formatBalanceAmount(total: string): string {
77
+ const n = parseFloat(total)
78
+ if (!Number.isFinite(n)) return total
79
+ if (n === 0 || n >= 1) return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
80
+ return n.toLocaleString("en-US", { maximumFractionDigits: 6 })
81
+ }
package/src/core/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- export * from "./color"
2
- export * from "./currency"
3
- export * from "./estimate"
4
- export * from "./format"
5
- export * from "./types"
1
+ export * from "./color"
2
+ export * from "./currency"
3
+ export * from "./estimate"
4
+ export * from "./format"
5
+ export * from "./types"
package/src/core/types.ts CHANGED
@@ -1,17 +1,17 @@
1
- /**
2
- * Token distribution breakdown for a session round.
3
- * Pure data model shared by V1/V2 shells.
4
- */
5
- export interface TokenDist {
6
- system: number // UserMessage.system
7
- user: number // user message text/file parts
8
- agent: number // task tool input prompt/description (sub-agent delegation)
9
- toolCall: number // ToolPart.input (actual tool params)
10
- toolResult: number // ToolPart completed output / error
11
- output: number // AssistantMessage.tokens.output (API exact, reasoning excluded)
12
- reasoning: number // AssistantMessage.tokens.reasoning (API exact)
13
- apiOutput: number // StepFinishPart.tokens.output (API exact, preferred)
14
- apiInput: number // API exact total input context (input + cache read + cache write)
15
- stepCost: number // last step-finish part cost (USD) in the current round
16
- stepCount: number // step-finish parts count across the current round (parentID chain)
17
- }
1
+ /**
2
+ * Token distribution breakdown for a session round.
3
+ * Pure data model shared by V1/V2 shells.
4
+ */
5
+ export interface TokenDist {
6
+ system: number // UserMessage.system
7
+ user: number // user message text/file parts
8
+ agent: number // task tool input prompt/description (sub-agent delegation)
9
+ toolCall: number // ToolPart.input (actual tool params)
10
+ toolResult: number // ToolPart completed output / error
11
+ output: number // AssistantMessage.tokens.output (API exact, reasoning excluded)
12
+ reasoning: number // AssistantMessage.tokens.reasoning (API exact)
13
+ apiOutput: number // StepFinishPart.tokens.output (API exact, preferred)
14
+ apiInput: number // API exact total input context (input + cache read + cache write)
15
+ stepCost: number // last step-finish part cost (USD) in the current round
16
+ stepCount: number // step-finish parts count across the current round (parentID chain)
17
+ }
package/src/index.tsx CHANGED
@@ -493,6 +493,9 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
493
493
  onSubmit={input.on_submit}
494
494
  ref={input.ref}
495
495
  hint={<BottomStatusBar api={api} signals={signals} sessionId={input.session_id} />}
496
+ // 接管 session_prompt 后需透传宿主的 session_prompt_right 插槽,
497
+ // 否则 oc-tps 等依赖该插槽的插件无法显示;无注册时 Slot 为 null。
498
+ right={<api.ui.Slot name="session_prompt_right" session_id={input.session_id} />}
496
499
  />
497
500
  )
498
501
  },
@@ -1,104 +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 }
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,14 +1,10 @@
1
1
  import type { Plugin, PluginModule } from "@opencode-ai/plugin"
2
- import v2Mod from "./v2/index"
3
2
 
4
- // V1 server 插件(空实现,保持原行为);V2 经 exports["./server"] 加载此入口,
5
- // 需要 default.setup——此处复用 v2 的 setup,一个入口同时满足 V1(server)与 V2(setup)。
6
3
  const server: Plugin = async () => ({})
7
4
 
8
- const mod: PluginModule & { setup: typeof v2Mod.setup } = {
5
+ const mod: PluginModule = {
9
6
  id: "opencode-visual-cache",
10
7
  server,
11
- setup: v2Mod.setup,
12
8
  }
13
9
 
14
10
  export default mod