opencode-cache-hit 0.2.0 → 0.2.2

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,6 +1,6 @@
1
1
  import type { TimelineConfig } from "../plugin-config.ts"
2
2
  import type { AssistantMessage } from "../types.ts"
3
- import { buildCallRecords, mergeAndSortRecords } from "./records.ts"
3
+ import { assistantMessageToRecord } from "./records.ts"
4
4
  import {
5
5
  appendTimelineRecord,
6
6
  localDateKey,
@@ -10,10 +10,9 @@ import {
10
10
  } from "./writer.ts"
11
11
  import type { LlmCallRecord } from "./types.ts"
12
12
 
13
- export const TIMELINE_DEBOUNCE_MS = 500
14
-
15
13
  export type TimelineCollector = {
16
- schedule: () => void
14
+ /** Process a single message from a message.updated event. */
15
+ handleMessage: (sessionID: string, msg: AssistantMessage) => void
17
16
  resetForRootChange: () => void
18
17
  dispose: () => void
19
18
  memoryRecords: () => readonly LlmCallRecord[]
@@ -23,13 +22,12 @@ export function createTimelineCollector(opts: {
23
22
  config: TimelineConfig
24
23
  getRootSessionId: () => string
25
24
  getChildIds: () => readonly string[]
26
- getMessages: (sessionId: string) => readonly AssistantMessage[]
27
25
  /** Test hook: replace disk append */
28
26
  append?: (logPath: string, record: LlmCallRecord) => Promise<void>
29
27
  }): TimelineCollector {
30
28
  if (!opts.config.enabled) {
31
29
  return {
32
- schedule: () => {},
30
+ handleMessage: () => {},
33
31
  resetForRootChange: () => {},
34
32
  dispose: () => {},
35
33
  memoryRecords: () => [],
@@ -51,106 +49,61 @@ export function createTimelineCollector(opts: {
51
49
  maxLogFiles: opts.config.maxLogFiles,
52
50
  })
53
51
  }
54
- const flushedKeys = new Set<string>()
52
+
55
53
  let activeDateKey = localDateKey()
56
- let debounceTimer: ReturnType<typeof setTimeout> | undefined
57
54
  let memory: LlmCallRecord[] = []
58
- let collectGen = 0
55
+ let disposed = false
59
56
 
60
57
  const ensureDateKey = () => {
61
58
  const today = localDateKey()
62
59
  if (today !== activeDateKey) {
63
- flushedKeys.clear()
64
60
  activeDateKey = today
65
61
  }
66
62
  return today
67
63
  }
68
64
 
69
- const clearDebounce = () => {
70
- if (debounceTimer !== undefined) clearTimeout(debounceTimer)
71
- debounceTimer = undefined
72
- }
73
-
74
- const shouldFlushToDisk = (rec: LlmCallRecord): boolean => {
75
- if (flushedKeys.has(rec.messageKey)) return false
76
- if (rec.isComplete) return true
77
- return opts.config.flushIncomplete
78
- }
79
-
80
- const flushRecords = async (records: LlmCallRecord[], rootId: string, gen: number) => {
81
- const logPath = timelineDailyLogPath(logsDir, ensureDateKey())
82
- for (const rec of records) {
83
- if (gen !== collectGen || opts.getRootSessionId() !== rootId) return
84
- if (!shouldFlushToDisk(rec)) continue
85
- flushedKeys.add(rec.messageKey)
86
- try {
87
- await append(logPath, rec)
88
- } catch {
89
- flushedKeys.delete(rec.messageKey)
90
- }
91
- }
92
- }
93
-
94
- const collectNow = () => {
95
- clearDebounce()
65
+ const handleMessage = (sessionID: string, msg: AssistantMessage) => {
66
+ if (disposed) return
96
67
  const rootId = opts.getRootSessionId()
97
- if (!rootId) {
98
- memory = []
68
+ if (!rootId) return
69
+
70
+ let scope: "main" | "child"
71
+ if (sessionID === rootId) {
72
+ scope = "main"
73
+ } else if (opts.getChildIds().includes(sessionID)) {
74
+ scope = "child"
75
+ } else {
99
76
  return
100
77
  }
101
- const gen = collectGen
102
- const chunks: LlmCallRecord[][] = []
103
- const mainMsgs = opts.getMessages(rootId)
104
- if (mainMsgs.length) {
105
- chunks.push(
106
- buildCallRecords(rootId, rootId, "main", mainMsgs, {
107
- logSummaryMessages: opts.config.logSummaryMessages,
108
- }),
109
- )
110
- }
111
- for (const cid of opts.getChildIds()) {
112
- const msgs = opts.getMessages(cid)
113
- if (msgs.length) {
114
- chunks.push(
115
- buildCallRecords(cid, rootId, "child", msgs, {
116
- logSummaryMessages: opts.config.logSummaryMessages,
117
- }),
118
- )
119
- }
120
- }
121
- if (gen !== collectGen || opts.getRootSessionId() !== rootId) return
122
- memory = mergeAndSortRecords(chunks)
123
- const toFlush = memory.filter(shouldFlushToDisk)
124
- if (toFlush.length > 0) {
125
- queueMicrotask(() => flushRecords(toFlush, rootId, gen))
126
- }
127
- }
128
78
 
129
- const schedule = () => {
130
- clearDebounce()
131
- if (!opts.getRootSessionId()) return
132
- debounceTimer = setTimeout(() => {
133
- debounceTimer = undefined
134
- collectNow()
135
- }, TIMELINE_DEBOUNCE_MS)
136
- }
79
+ if (msg.role !== "assistant") return
80
+ if (!opts.config.logSummaryMessages && msg.summary === true) return
137
81
 
138
- const resetForRootChange = () => {
139
- collectGen++
140
- clearDebounce()
141
- memory = []
142
- }
82
+ const rec = assistantMessageToRecord(msg, sessionID, rootId, scope, Date.now())
83
+ if (!rec) return
84
+ if (!opts.config.flushIncomplete && !rec.isComplete) return
143
85
 
144
- const dispose = () => {
145
- collectGen++
146
- clearDebounce()
147
- }
86
+ const logPath = timelineDailyLogPath(logsDir, ensureDateKey())
87
+ void append(logPath, rec).catch(() => {})
148
88
 
149
- const memoryRecords = () => {
89
+ memory.push(rec)
150
90
  const max = opts.config.maxMemoryRows
151
- if (memory.length <= max) return memory
152
- return memory.slice(-max)
91
+ while (memory.length > max) memory.shift()
153
92
  }
154
93
 
155
- return { schedule, resetForRootChange, dispose, memoryRecords }
94
+ return {
95
+ handleMessage,
96
+ resetForRootChange: () => {
97
+ memory = []
98
+ },
99
+ dispose: () => {
100
+ disposed = true
101
+ memory = []
102
+ },
103
+ memoryRecords: () => {
104
+ const max = opts.config.maxMemoryRows
105
+ if (memory.length <= max) return memory
106
+ return memory.slice(-max)
107
+ },
108
+ }
156
109
  }
@@ -25,18 +25,6 @@ export function messageKeyFor(msg: AssistantMessage, sessionId: string): string
25
25
  return `${sessionId}:${created}:${msg.modelID ?? ""}`
26
26
  }
27
27
 
28
- export function sortKeyForRecord(r: LlmCallRecord): number {
29
- const ts = r.completedAt ?? r.created
30
- return new Date(ts).getTime()
31
- }
32
-
33
- export function mergeAndSortRecords(chunks: readonly LlmCallRecord[][]): LlmCallRecord[] {
34
- const all = chunks.flat()
35
- const keyed = all.map(r => ({ r, k: sortKeyForRecord(r) }))
36
- keyed.sort((a, b) => a.k - b.k)
37
- return keyed.map(x => x.r)
38
- }
39
-
40
28
  export function assistantMessageToRecord(
41
29
  msg: AssistantMessage,
42
30
  sessionId: string,
@@ -72,21 +60,3 @@ export function assistantMessageToRecord(
72
60
  }
73
61
  }
74
62
 
75
- export function buildCallRecords(
76
- sessionId: string,
77
- rootSessionId: string,
78
- scope: "main" | "child",
79
- messages: readonly AssistantMessage[],
80
- opts?: { logSummaryMessages?: boolean; recordedAt?: number },
81
- ): LlmCallRecord[] {
82
- const now = opts?.recordedAt ?? Date.now()
83
- const logSummary = opts?.logSummaryMessages !== false
84
- const out: LlmCallRecord[] = []
85
- for (const msg of messages) {
86
- if (!logSummary && msg.summary === true) continue
87
- const rec = assistantMessageToRecord(msg, sessionId, rootSessionId, scope, now)
88
- if (rec) out.push(rec)
89
- }
90
- return out
91
- }
92
-
@@ -36,7 +36,7 @@ export function MySidebar(props: { theme: Record<string, unknown> }) {
36
36
  open={open()}
37
37
  onToggle={() => setOpen((o) => !o)}
38
38
  title="My Panel"
39
- version="0.1.0"
39
+ version="0.2.0"
40
40
  />
41
41
  <Show when={open()}>
42
42
  <Show
@@ -69,7 +69,7 @@ export function MySidebar(props: { theme: Record<string, unknown> }) {
69
69
  | `TuiPanel` | Outer border + padding |
70
70
  | `TuiPanelTitle` | Foldable title; optional `collapsed` summary |
71
71
  | `TuiSection` | `▼` section header + separator fill |
72
- | `TuiMetricRow` | Label left, value (+ unit) right |
72
+ | `TuiMetricRow` | Label left, value (+ unit) right; optional `labelFg` / `valueFg` for split colors |
73
73
  | `TuiHitRow` | Hit bar + % + trend |
74
74
  | `computeHitBarWidth` | Dynamic bar width |
75
75
 
@@ -36,7 +36,7 @@ export function MySidebar(props: { theme: Record<string, unknown> }) {
36
36
  open={open()}
37
37
  onToggle={() => setOpen((o) => !o)}
38
38
  title="My Panel"
39
- version="0.1.0"
39
+ version="0.2.0"
40
40
  />
41
41
  <Show when={open()}>
42
42
  <Show
@@ -69,7 +69,7 @@ export function MySidebar(props: { theme: Record<string, unknown> }) {
69
69
  | `TuiPanel` | 外框 + padding |
70
70
  | `TuiPanelTitle` | 可折叠标题;可选 `collapsed` 摘要 |
71
71
  | `TuiSection` | `▼` 区块标题 + 分隔线填充 |
72
- | `TuiMetricRow` | 左标签右数值(可选 unit |
72
+ | `TuiMetricRow` | 左标签右数值(可选 unit);可选 `labelFg` / `valueFg` 分段上色 |
73
73
  | `TuiHitRow` | Hit 条 + % + 趋势 |
74
74
  | `computeHitBarWidth` | 动态进度条宽度 |
75
75
 
@@ -1,6 +1,6 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import { Show, type JSX } from "solid-js"
3
- import { padBeforeTitleSummary, sepAfterPrefix, visualWidth } from "./layout.ts"
3
+ import { padBeforeTitleSummary, sepAfterPrefix, UNIT_GAP, visualWidth } from "./layout.ts"
4
4
  import type { PanelLayout } from "./use-panel-layout.ts"
5
5
  import type { PanelPalette } from "./palette.ts"
6
6
 
@@ -126,18 +126,45 @@ export function TuiSection(props: {
126
126
  )
127
127
  }
128
128
 
129
+ function metricRowGap(label: string, value: string, unit: string, gauge: number): number {
130
+ const used =
131
+ visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0)
132
+ return Math.max(1, gauge - used)
133
+ }
134
+
129
135
  export function TuiMetricRow(props: {
130
136
  pal: PanelPalette
131
137
  layout: PanelLayout
132
138
  label: string
133
139
  value: string
134
140
  unit?: string
141
+ /** Whole line (label + value + unit). Ignored when `labelFg` / `valueFg` set. */
135
142
  fg?: string
143
+ /** Label-only color (e.g. sub-agent model); value stays `valueFg` or muted. */
144
+ labelFg?: string
145
+ valueFg?: string
136
146
  }) {
137
- const fg = props.fg ?? props.pal.muted
147
+ const unit = props.unit ?? ""
148
+ const unitSuffix = unit ? " " + unit : ""
149
+ const split = props.labelFg !== undefined || props.valueFg !== undefined
150
+ if (split) {
151
+ const gap = metricRowGap(props.label, props.value, unit, props.layout.gauge())
152
+ const labelColor = props.labelFg ?? props.fg ?? props.pal.muted
153
+ const valueColor = props.valueFg ?? props.fg ?? props.pal.muted
154
+ return (
155
+ <text>
156
+ <span style={{ fg: labelColor }}>{props.label}</span>
157
+ {" ".repeat(gap)}
158
+ <span style={{ fg: valueColor }}>
159
+ {props.value}
160
+ {unitSuffix}
161
+ </span>
162
+ </text>
163
+ )
164
+ }
138
165
  return (
139
- <text fg={fg}>
140
- {props.layout.row(props.label, props.value, props.unit ?? "")}
166
+ <text fg={props.fg ?? props.pal.muted}>
167
+ {props.layout.row(props.label, props.value, unit)}
141
168
  </text>
142
169
  )
143
170
  }
@@ -19,7 +19,12 @@ export {
19
19
  padBeforeTitleSummary,
20
20
  } from "./layout.ts"
21
21
 
22
- export { buildPanelPalette, themeColorToHex, type PanelPalette } from "./palette.ts"
22
+ export {
23
+ buildPanelPalette,
24
+ themeColorToHex,
25
+ toneBrandHex,
26
+ type PanelPalette,
27
+ } from "./palette.ts"
23
28
 
24
29
  export {
25
30
  createPanelLayout,
@@ -79,6 +79,11 @@ export function themeColorToHex(raw: unknown, fallback: string): string {
79
79
  return "#" + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")
80
80
  }
81
81
 
82
+ /** Tone a vendor brand hex for dark TUI panels (same max saturation as theme colors). */
83
+ export function toneBrandHex(hex: string, fallback: string): string {
84
+ return desaturateTo(hex, MAX_SAT, fallback)
85
+ }
86
+
82
87
  export function buildPanelPalette(theme: Record<string, unknown>): PanelPalette {
83
88
  const sat = (k: string, fb: string) => desaturateTo(theme[k], MAX_SAT, fb)
84
89
  return {
package/src/types.ts CHANGED
@@ -53,13 +53,26 @@ export type ProviderInfo = {
53
53
  models: { [key: string]: { cost: ModelCost } }
54
54
  }
55
55
 
56
+ /** Session aggregate from `api.state.session.get()` — DB-level totals, not capped by message limit. */
57
+ export type SessionObject = {
58
+ model?: { id: string; providerID: string }
59
+ cost?: number
60
+ tokens?: {
61
+ input?: number
62
+ output?: number
63
+ reasoning?: number
64
+ cache?: { read?: number; write?: number }
65
+ }
66
+ parentID?: string
67
+ }
68
+
56
69
  export type OpenCodeTuiApi = {
57
70
  state: {
58
71
  path: { directory: string }
59
72
  provider: ReadonlyArray<ProviderInfo>
60
73
  session: {
61
74
  messages: (id: string) => unknown[] | undefined
62
- get: (id: string) => { parentID?: string } | undefined
75
+ get: (id: string) => SessionObject | undefined
63
76
  }
64
77
  }
65
78
  client: {
package/src/version.ts CHANGED
@@ -1 +1,4 @@
1
- export const PLUGIN_VERSION = "0.1.0"
1
+ import packageJson from "../package.json" with { type: "json" }
2
+
3
+ /** Sidebar label; kept in sync with package.json "version". */
4
+ export const PLUGIN_VERSION = packageJson.version
File without changes
Binary file