@mtayfur/opencode-cache-view 0.0.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/README.md +95 -0
- package/dist/index.js +737 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# OpenCode Cache View
|
|
2
|
+
|
|
3
|
+
`@mtayfur/opencode-cache-view` adds a compact sidebar to the OpenCode TUI for cache efficiency, estimated context usage, generation speed, and loaded skills.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
Cache View
|
|
7
|
+
Hit [████████████████] 98.7% ↑0.8%
|
|
8
|
+
Session Hit 96.0%
|
|
9
|
+
Read 8.0M tok
|
|
10
|
+
Miss 326K tok
|
|
11
|
+
▼ Speed ─────────────────────
|
|
12
|
+
Now —
|
|
13
|
+
TTFT ~1.4 s
|
|
14
|
+
Last ~37 tok/s
|
|
15
|
+
Avg ~42 tok/s
|
|
16
|
+
Trend ▂▅▄▇▆█
|
|
17
|
+
▼ Estimated Tokens ──────────
|
|
18
|
+
Prompt 186K tok
|
|
19
|
+
Tool Call 59K tok
|
|
20
|
+
Tool Result 80K tok
|
|
21
|
+
Agent Reasoning 14K tok
|
|
22
|
+
Agent Output 21K tok
|
|
23
|
+
Total 326K tok
|
|
24
|
+
▼ Loaded Skills (2) ─────────
|
|
25
|
+
playwright-mcp-ops 2.4K tok
|
|
26
|
+
customize-opencode 3.1K tok
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Metrics
|
|
30
|
+
|
|
31
|
+
- **Cache:** `Hit` is `cache.read / (input + cache.read + cache.write)` for the latest regular LLM step. `Session Hit` is the aggregate OpenCode session ratio, including compaction calls. `Miss` combines uncached input and cache writes.
|
|
32
|
+
- **Estimated Tokens:** Uses one character-based estimator for the visible system, user, tool, reasoning, and output content from the latest completed compaction summary, its preserved tail, and subsequent messages loaded in the TUI. Provider system prompts, tool schemas, and media tokens are not included.
|
|
33
|
+
- **Speed:** `TTFT` estimates the time from assistant creation to the first reasoning or text block. `Now`, `Last`, and `Avg` use only visible reasoning and text content and the active duration of those blocks, excluding tool execution time. `Avg` covers the latest eight completed responses.
|
|
34
|
+
- **Loaded Skills:** Shows active, non-compacted skill outputs and their estimated token counts. When a skill is loaded more than once, the latest load is shown.
|
|
35
|
+
|
|
36
|
+
Rows and the cache-hit bar adapt to the available sidebar width. The arrow next to the hit rate compares the latest two LLM steps. Sections can be expanded or collapsed with the mouse.
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- OpenCode `1.18.16` or a newer compatible 1.x release
|
|
41
|
+
- Bun `1.3.14` or newer for local development
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Add the package to `~/.config/opencode/tui.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"plugin": ["@mtayfur/opencode-cache-view"]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
OpenCode installs package plugins automatically. Restart the TUI after changing the configuration.
|
|
54
|
+
|
|
55
|
+
### Local checkout
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
bash ./install.sh
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The installer resolves dependencies, builds `dist/index.js`, and adds its local `file://` URL to the TUI plugin configuration. Remove the local entry with:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
bash ./install.sh --uninstall
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Restart OpenCode after either operation.
|
|
68
|
+
|
|
69
|
+
## Architecture
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
src/
|
|
73
|
+
├── index.tsx Plugin entrypoint
|
|
74
|
+
├── cache-view.tsx Solid/OpenTUI view and event subscriptions
|
|
75
|
+
├── format.ts Display formatting
|
|
76
|
+
├── token-estimator.ts Cached character-based token estimation
|
|
77
|
+
└── metrics/
|
|
78
|
+
├── cache.ts Cache usage and hit ratios
|
|
79
|
+
├── context.ts Active context, token, and skill calculations
|
|
80
|
+
├── helpers.ts Shared message and part helpers
|
|
81
|
+
├── read.ts Metric orchestration
|
|
82
|
+
├── speed.ts TTFT and generation speed
|
|
83
|
+
└── types.ts Metric data contracts
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Metric calculations are isolated from the TUI rendering layer. `read.ts` snapshots OpenCode state and coordinates the dedicated metric modules, while `cache-view.tsx` owns reactive updates and presentation.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
bun install --frozen-lockfile
|
|
92
|
+
bun run typecheck
|
|
93
|
+
bun run build
|
|
94
|
+
npm pack --dry-run
|
|
95
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.tsx
|
|
3
|
+
import { createComponent as _$createComponent2 } from "@opentui/solid";
|
|
4
|
+
import { memo as _$memo2 } from "@opentui/solid";
|
|
5
|
+
|
|
6
|
+
// src/cache-view.tsx
|
|
7
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
8
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
9
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
10
|
+
import { use as _$use } from "@opentui/solid";
|
|
11
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
12
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
13
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
14
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
15
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
16
|
+
import { createMemo, createSignal, onCleanup, onMount } from "solid-js";
|
|
17
|
+
|
|
18
|
+
// src/format.ts
|
|
19
|
+
function formatCompact(value) {
|
|
20
|
+
if (value >= 1e6)
|
|
21
|
+
return `${(value / 1e6).toFixed(1)}M`;
|
|
22
|
+
if (value >= 1e4)
|
|
23
|
+
return `${(value / 1000).toFixed(1)}K`;
|
|
24
|
+
return Math.round(value).toLocaleString("en-US");
|
|
25
|
+
}
|
|
26
|
+
function formatPercent(value) {
|
|
27
|
+
return value === undefined ? "\u2014" : `${value.toFixed(1)}%`;
|
|
28
|
+
}
|
|
29
|
+
function formatSpeed(value, estimated = false) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return "\u2014";
|
|
32
|
+
return `${estimated ? "~" : ""}${Math.round(value)} tok/s`;
|
|
33
|
+
}
|
|
34
|
+
function formatDuration(value, estimated = false) {
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
return "\u2014";
|
|
37
|
+
const prefix = estimated ? "~" : "";
|
|
38
|
+
if (value < 1000)
|
|
39
|
+
return `${prefix}${Math.round(value)} ms`;
|
|
40
|
+
if (value < 1e4)
|
|
41
|
+
return `${prefix}${(value / 1000).toFixed(2)} s`;
|
|
42
|
+
return `${prefix}${(value / 1000).toFixed(1)} s`;
|
|
43
|
+
}
|
|
44
|
+
function formatHitTrend(value) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return "\u2014";
|
|
47
|
+
if (Math.abs(value) < 0.05)
|
|
48
|
+
return "-";
|
|
49
|
+
return `${value > 0 ? "\u2191" : "\u2193"}${Math.abs(value).toFixed(1)}%`;
|
|
50
|
+
}
|
|
51
|
+
function progressBar(percent, width) {
|
|
52
|
+
const filled = percent === undefined ? 0 : Math.round(Math.max(0, Math.min(100, percent)) / 100 * width);
|
|
53
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
54
|
+
}
|
|
55
|
+
function sparkline(values) {
|
|
56
|
+
if (values.length === 0)
|
|
57
|
+
return "\u2014";
|
|
58
|
+
if (values.length === 1)
|
|
59
|
+
return "\u2585";
|
|
60
|
+
const bars = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
61
|
+
const min = Math.min(...values);
|
|
62
|
+
const max = Math.max(...values);
|
|
63
|
+
if (min === max)
|
|
64
|
+
return "\u2585".repeat(values.length);
|
|
65
|
+
return values.map((value) => bars[Math.round((value - min) / (max - min) * (bars.length - 1))]).join("");
|
|
66
|
+
}
|
|
67
|
+
function row(label, value, width) {
|
|
68
|
+
return label + " ".repeat(Math.max(1, width - label.length - value.length)) + value;
|
|
69
|
+
}
|
|
70
|
+
function skillRow(skill, width) {
|
|
71
|
+
const value = `${formatCompact(skill.tokens)} tok`;
|
|
72
|
+
const maxName = Math.max(4, width - value.length - 1);
|
|
73
|
+
const name = skill.name.length > maxName ? `${skill.name.slice(0, maxName - 1)}\u2026` : skill.name;
|
|
74
|
+
return row(name, value, width);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/metrics/helpers.ts
|
|
78
|
+
function finite(value) {
|
|
79
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
80
|
+
}
|
|
81
|
+
function partsFor(parts, messageID) {
|
|
82
|
+
return parts.get(messageID) ?? [];
|
|
83
|
+
}
|
|
84
|
+
function assistantMessages(messages, includeSummaries) {
|
|
85
|
+
return messages.filter((message) => message.role === "assistant" && (includeSummaries || !message.summary));
|
|
86
|
+
}
|
|
87
|
+
function serializedInput(part) {
|
|
88
|
+
if (part.state.status === "pending" && part.state.raw)
|
|
89
|
+
return part.state.raw;
|
|
90
|
+
try {
|
|
91
|
+
return JSON.stringify(part.state.input);
|
|
92
|
+
} catch {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/metrics/cache.ts
|
|
98
|
+
function usageSamples(assistants, parts) {
|
|
99
|
+
const samples = [];
|
|
100
|
+
for (const message of assistants) {
|
|
101
|
+
const finishes = partsFor(parts, message.id).filter((part) => part.type === "step-finish");
|
|
102
|
+
if (finishes.length > 0) {
|
|
103
|
+
for (const finish of finishes)
|
|
104
|
+
samples.push({ tokens: finish.tokens });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const tokens = message.tokens;
|
|
108
|
+
const total = finite(tokens.input) + finite(tokens.output) + finite(tokens.reasoning) + finite(tokens.cache.read) + finite(tokens.cache.write);
|
|
109
|
+
if (message.time.completed && total > 0)
|
|
110
|
+
samples.push({ tokens });
|
|
111
|
+
}
|
|
112
|
+
return samples;
|
|
113
|
+
}
|
|
114
|
+
function sumUsage(samples) {
|
|
115
|
+
const result = {
|
|
116
|
+
input: 0,
|
|
117
|
+
output: 0,
|
|
118
|
+
reasoning: 0,
|
|
119
|
+
cache: { read: 0, write: 0 }
|
|
120
|
+
};
|
|
121
|
+
for (const sample of samples) {
|
|
122
|
+
result.input += finite(sample.tokens.input);
|
|
123
|
+
result.output += finite(sample.tokens.output);
|
|
124
|
+
result.reasoning += finite(sample.tokens.reasoning);
|
|
125
|
+
result.cache.read += finite(sample.tokens.cache.read);
|
|
126
|
+
result.cache.write += finite(sample.tokens.cache.write);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
function promptTokens(tokens) {
|
|
131
|
+
return finite(tokens.input) + finite(tokens.cache.read) + finite(tokens.cache.write);
|
|
132
|
+
}
|
|
133
|
+
function cacheMetrics(sessionTokens, normalUsage, allUsage) {
|
|
134
|
+
const hitRates = [];
|
|
135
|
+
for (const sample of normalUsage) {
|
|
136
|
+
const tokens = sample.tokens;
|
|
137
|
+
const read2 = finite(tokens.cache?.read);
|
|
138
|
+
const prompt2 = promptTokens(tokens);
|
|
139
|
+
if (prompt2 > 0)
|
|
140
|
+
hitRates.push(read2 / prompt2 * 100);
|
|
141
|
+
}
|
|
142
|
+
const latestHit = hitRates.at(-1);
|
|
143
|
+
const previousHit = hitRates.at(-2);
|
|
144
|
+
const aggregate = sessionTokens ?? sumUsage(allUsage);
|
|
145
|
+
const input = finite(aggregate.input);
|
|
146
|
+
const read = finite(aggregate.cache.read);
|
|
147
|
+
const write = finite(aggregate.cache.write);
|
|
148
|
+
const prompt = input + read + write;
|
|
149
|
+
return {
|
|
150
|
+
latestHit,
|
|
151
|
+
trend: latestHit !== undefined && previousHit !== undefined ? latestHit - previousHit : undefined,
|
|
152
|
+
totalHit: prompt > 0 ? read / prompt * 100 : undefined,
|
|
153
|
+
read,
|
|
154
|
+
miss: input + write
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/metrics/context.ts
|
|
159
|
+
function estimateSessionTokens(messages, parts, activeStart, session, config, estimator) {
|
|
160
|
+
const agentConfig = config;
|
|
161
|
+
const agentName = session?.agent ?? agentConfig.default_agent;
|
|
162
|
+
const agentPrompt = agentName ? agentConfig.agent?.[agentName]?.prompt ?? "" : "";
|
|
163
|
+
const result = {
|
|
164
|
+
system: estimator.count(`agent:${agentName ?? "default"}:prompt`, agentPrompt),
|
|
165
|
+
user: 0,
|
|
166
|
+
agent: 0,
|
|
167
|
+
toolCall: 0,
|
|
168
|
+
toolResult: 0,
|
|
169
|
+
output: 0,
|
|
170
|
+
reasoning: 0,
|
|
171
|
+
total: 0
|
|
172
|
+
};
|
|
173
|
+
for (let index = activeStart;index < messages.length; index++) {
|
|
174
|
+
const message = messages[index];
|
|
175
|
+
if (message.role === "user") {
|
|
176
|
+
result.system += estimator.count(`message:${message.id}:system`, message.system ?? "");
|
|
177
|
+
}
|
|
178
|
+
for (const part of partsFor(parts, message.id)) {
|
|
179
|
+
if (message.role === "user" && part.type === "text" && !part.ignored) {
|
|
180
|
+
result.user += estimator.count(`part:${part.id}:text`, part.text);
|
|
181
|
+
} else if (message.role === "user" && part.type === "compaction") {
|
|
182
|
+
result.user += estimator.count(`part:${part.id}:compaction`, "What did we do so far?");
|
|
183
|
+
} else if (message.role === "assistant" && part.type === "text") {
|
|
184
|
+
result.output += estimator.count(`part:${part.id}:text`, part.text);
|
|
185
|
+
} else if (message.role === "assistant" && part.type === "reasoning") {
|
|
186
|
+
result.reasoning += estimator.count(`part:${part.id}:text`, part.text);
|
|
187
|
+
} else if (message.role === "assistant" && part.type === "tool") {
|
|
188
|
+
const input = estimator.count(`part:${part.id}:input`, serializedInput(part));
|
|
189
|
+
if (part.tool === "task")
|
|
190
|
+
result.agent += input;
|
|
191
|
+
else
|
|
192
|
+
result.toolCall += input;
|
|
193
|
+
if (part.state.status === "completed") {
|
|
194
|
+
const output = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output;
|
|
195
|
+
result.toolResult += estimator.count(`part:${part.id}:output`, output);
|
|
196
|
+
} else if (part.state.status === "error") {
|
|
197
|
+
result.toolResult += estimator.count(`part:${part.id}:error`, part.state.error);
|
|
198
|
+
}
|
|
199
|
+
} else if (part.type === "subtask") {
|
|
200
|
+
result.agent += estimator.count(`part:${part.id}:subtask`, part.prompt || part.description);
|
|
201
|
+
} else if (part.type === "agent") {
|
|
202
|
+
result.agent += estimator.count(`part:${part.id}:agent`, part.source?.value ?? "");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
result.total = result.system + result.user + result.agent + result.toolCall + result.toolResult + result.output + result.reasoning;
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
function activeContextStart(messages, parts) {
|
|
210
|
+
const messageIndex = new Map(messages.map((message, index) => [message.id, index]));
|
|
211
|
+
for (let index = messages.length - 1;index >= 0; index--) {
|
|
212
|
+
const summary = messages[index];
|
|
213
|
+
if (summary.role !== "assistant" || !summary.summary || !summary.finish || summary.error !== undefined) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const compactionIndex = messageIndex.get(summary.parentID);
|
|
217
|
+
if (compactionIndex === undefined)
|
|
218
|
+
continue;
|
|
219
|
+
const compaction = partsFor(parts, summary.parentID).find((part) => part.type === "compaction");
|
|
220
|
+
if (!compaction)
|
|
221
|
+
continue;
|
|
222
|
+
const tailIndex = compaction.tail_start_id ? messageIndex.get(compaction.tail_start_id) : undefined;
|
|
223
|
+
return tailIndex !== undefined && tailIndex < compactionIndex ? tailIndex : compactionIndex;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
function skillName(part) {
|
|
228
|
+
const metadataName = "metadata" in part.state ? part.state.metadata?.name : undefined;
|
|
229
|
+
if (typeof metadataName === "string" && metadataName)
|
|
230
|
+
return metadataName;
|
|
231
|
+
const output = part.state.status === "completed" ? part.state.output : undefined;
|
|
232
|
+
const outputMatch = output?.match(/^#{1,2}\s*Skill:\s*(.+)$/m);
|
|
233
|
+
if (outputMatch?.[1])
|
|
234
|
+
return outputMatch[1].trim();
|
|
235
|
+
const input = part.state.input;
|
|
236
|
+
if (typeof input === "object" && input !== null && "name" in input) {
|
|
237
|
+
const inputName = input.name;
|
|
238
|
+
if (typeof inputName === "string" && inputName)
|
|
239
|
+
return inputName;
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
function loadedSkills(messages, parts, activeStart, estimator) {
|
|
244
|
+
const skills = new Map;
|
|
245
|
+
for (let index = activeStart;index < messages.length; index++) {
|
|
246
|
+
const message = messages[index];
|
|
247
|
+
if (message.role !== "assistant")
|
|
248
|
+
continue;
|
|
249
|
+
for (const part of partsFor(parts, message.id)) {
|
|
250
|
+
if (part.type !== "tool" || part.tool !== "skill" || part.state.status !== "completed") {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (part.state.time.compacted)
|
|
254
|
+
continue;
|
|
255
|
+
const name = skillName(part);
|
|
256
|
+
if (!name)
|
|
257
|
+
continue;
|
|
258
|
+
const tokens = estimator.count(`part:${part.id}:output`, part.state.output);
|
|
259
|
+
skills.set(name, { name, tokens });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [...skills.values()];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/metrics/speed.ts
|
|
266
|
+
function firstTokenTime(parts, message) {
|
|
267
|
+
let first = Number.POSITIVE_INFINITY;
|
|
268
|
+
for (const part of partsFor(parts, message.id)) {
|
|
269
|
+
if (part.type !== "text" && part.type !== "reasoning")
|
|
270
|
+
continue;
|
|
271
|
+
const start = part.time?.start;
|
|
272
|
+
if (typeof start === "number" && start < first)
|
|
273
|
+
first = start;
|
|
274
|
+
}
|
|
275
|
+
return Number.isFinite(first) ? first : undefined;
|
|
276
|
+
}
|
|
277
|
+
function timeToFirstToken(parts, message) {
|
|
278
|
+
if (!message)
|
|
279
|
+
return;
|
|
280
|
+
const first = firstTokenTime(parts, message);
|
|
281
|
+
if (first === undefined || first < message.time.created)
|
|
282
|
+
return;
|
|
283
|
+
return first - message.time.created;
|
|
284
|
+
}
|
|
285
|
+
function generationSample(message, parts, estimator) {
|
|
286
|
+
let tokens = 0;
|
|
287
|
+
let duration = 0;
|
|
288
|
+
for (const part of partsFor(parts, message.id)) {
|
|
289
|
+
if (part.type !== "text" && part.type !== "reasoning")
|
|
290
|
+
continue;
|
|
291
|
+
const start = part.time?.start;
|
|
292
|
+
const end = part.time?.end;
|
|
293
|
+
if (typeof start !== "number" || typeof end !== "number" || end <= start)
|
|
294
|
+
continue;
|
|
295
|
+
tokens += estimator.count(`part:${part.id}:text`, part.text);
|
|
296
|
+
duration += end - start;
|
|
297
|
+
}
|
|
298
|
+
if (tokens === 0 || duration < 500)
|
|
299
|
+
return;
|
|
300
|
+
return { speed: tokens / duration * 1000, tokens, duration };
|
|
301
|
+
}
|
|
302
|
+
function streamingSpeed(message, parts, estimator, now) {
|
|
303
|
+
if (!message || message.time.completed)
|
|
304
|
+
return;
|
|
305
|
+
let tokens = 0;
|
|
306
|
+
let duration = 0;
|
|
307
|
+
for (const part of partsFor(parts, message.id)) {
|
|
308
|
+
if (part.type !== "text" && part.type !== "reasoning")
|
|
309
|
+
continue;
|
|
310
|
+
const start = part.time?.start;
|
|
311
|
+
if (part.time?.end !== undefined || typeof start !== "number" || now <= start)
|
|
312
|
+
continue;
|
|
313
|
+
tokens += estimator.count(`part:${part.id}:text`, part.text);
|
|
314
|
+
duration += now - start;
|
|
315
|
+
}
|
|
316
|
+
if (tokens === 0 || duration < 500)
|
|
317
|
+
return;
|
|
318
|
+
return tokens / duration * 1000;
|
|
319
|
+
}
|
|
320
|
+
function speedMetrics(assistants, parts, estimator, now) {
|
|
321
|
+
const samples = assistants.filter((message) => message.time.completed !== undefined).map((message) => generationSample(message, parts, estimator)).filter((sample) => sample !== undefined);
|
|
322
|
+
const recent = samples.slice(-8);
|
|
323
|
+
const totalTokens = recent.reduce((sum, sample) => sum + sample.tokens, 0);
|
|
324
|
+
const totalDuration = recent.reduce((sum, sample) => sum + sample.duration, 0);
|
|
325
|
+
return {
|
|
326
|
+
ttft: timeToFirstToken(parts, assistants.at(-1)),
|
|
327
|
+
now: streamingSpeed(assistants.at(-1), parts, estimator, now),
|
|
328
|
+
last: samples.at(-1)?.speed,
|
|
329
|
+
average: totalDuration > 0 ? totalTokens / totalDuration * 1000 : undefined,
|
|
330
|
+
trend: recent.map((sample) => sample.speed)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/metrics/read.ts
|
|
335
|
+
function emptyTokenEstimate() {
|
|
336
|
+
return {
|
|
337
|
+
system: 0,
|
|
338
|
+
user: 0,
|
|
339
|
+
agent: 0,
|
|
340
|
+
toolCall: 0,
|
|
341
|
+
toolResult: 0,
|
|
342
|
+
output: 0,
|
|
343
|
+
reasoning: 0,
|
|
344
|
+
total: 0
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function readMetrics(api, sessionID, now, estimator) {
|
|
348
|
+
if (!sessionID) {
|
|
349
|
+
return {
|
|
350
|
+
cache: { read: 0, miss: 0 },
|
|
351
|
+
tokens: emptyTokenEstimate(),
|
|
352
|
+
skills: [],
|
|
353
|
+
speed: { trend: [] }
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const messages = api.state.session.messages(sessionID);
|
|
357
|
+
const parts = new Map;
|
|
358
|
+
for (const message of messages)
|
|
359
|
+
parts.set(message.id, api.state.part(message.id));
|
|
360
|
+
const allAssistants = assistantMessages(messages, true);
|
|
361
|
+
const assistants = assistantMessages(messages, false);
|
|
362
|
+
const allUsage = usageSamples(allAssistants, parts);
|
|
363
|
+
const normalUsage = usageSamples(assistants, parts);
|
|
364
|
+
const activeStart = activeContextStart(messages, parts);
|
|
365
|
+
const session = api.state.session.get(sessionID);
|
|
366
|
+
return {
|
|
367
|
+
cache: cacheMetrics(session?.tokens, normalUsage, allUsage),
|
|
368
|
+
tokens: estimateSessionTokens(messages, parts, activeStart, session, api.state.config, estimator),
|
|
369
|
+
skills: loadedSkills(messages, parts, activeStart, estimator),
|
|
370
|
+
speed: speedMetrics(assistants, parts, estimator, now)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/token-estimator.ts
|
|
375
|
+
function estimateTokenUnits(text) {
|
|
376
|
+
let units = 0;
|
|
377
|
+
for (const char of text) {
|
|
378
|
+
const code = char.codePointAt(0) ?? 0;
|
|
379
|
+
const isWide = code >= 11904 && code <= 40959 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255;
|
|
380
|
+
units += isWide ? 4 : 1;
|
|
381
|
+
}
|
|
382
|
+
return units;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
class TokenEstimator {
|
|
386
|
+
cache = new Map;
|
|
387
|
+
count(key, text) {
|
|
388
|
+
const cached = this.cache.get(key);
|
|
389
|
+
if (cached?.text === text)
|
|
390
|
+
return cached.tokens;
|
|
391
|
+
const units = estimateTokenUnits(text);
|
|
392
|
+
const tokens = Math.ceil(units / 4);
|
|
393
|
+
this.cache.set(key, { text, units, tokens });
|
|
394
|
+
return tokens;
|
|
395
|
+
}
|
|
396
|
+
append(key, delta) {
|
|
397
|
+
const cached = this.cache.get(key);
|
|
398
|
+
if (!cached)
|
|
399
|
+
return;
|
|
400
|
+
const units = cached.units + estimateTokenUnits(delta);
|
|
401
|
+
this.cache.set(key, {
|
|
402
|
+
text: cached.text + delta,
|
|
403
|
+
units,
|
|
404
|
+
tokens: Math.ceil(units / 4)
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
clear() {
|
|
408
|
+
this.cache.clear();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/cache-view.tsx
|
|
413
|
+
var DEFAULT_CONTENT_WIDTH = 29;
|
|
414
|
+
var MIN_CONTENT_WIDTH = 20;
|
|
415
|
+
function SectionHeader(props) {
|
|
416
|
+
const prefix = `${props.open ? "\u25BC" : "\u25B6"} ${props.title}`;
|
|
417
|
+
return (() => {
|
|
418
|
+
var _el$ = _$createElement("text"), _el$2 = _$createElement("span"), _el$3 = _$createElement("b"), _el$4 = _$createElement("span");
|
|
419
|
+
_$insertNode(_el$, _el$2);
|
|
420
|
+
_$insertNode(_el$, _el$4);
|
|
421
|
+
_$insertNode(_el$2, _el$3);
|
|
422
|
+
_$insert(_el$3, prefix);
|
|
423
|
+
_$insert(_el$4, () => "\u2500".repeat(Math.max(1, props.width - prefix.length)));
|
|
424
|
+
_$effect((_p$) => {
|
|
425
|
+
var _v$ = props.toggle, _v$2 = {
|
|
426
|
+
fg: props.theme.text
|
|
427
|
+
}, _v$3 = {
|
|
428
|
+
fg: props.theme.border
|
|
429
|
+
};
|
|
430
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "onMouseUp", _v$, _p$.e));
|
|
431
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "style", _v$2, _p$.t));
|
|
432
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$4, "style", _v$3, _p$.a));
|
|
433
|
+
return _p$;
|
|
434
|
+
}, {
|
|
435
|
+
e: undefined,
|
|
436
|
+
t: undefined,
|
|
437
|
+
a: undefined
|
|
438
|
+
});
|
|
439
|
+
return _el$;
|
|
440
|
+
})();
|
|
441
|
+
}
|
|
442
|
+
function CacheView(props) {
|
|
443
|
+
const estimator = new TokenEstimator;
|
|
444
|
+
const [refresh, setRefresh] = createSignal(0);
|
|
445
|
+
const [tokensOpen, setTokensOpen] = createSignal(true);
|
|
446
|
+
const [skillsOpen, setSkillsOpen] = createSignal(true);
|
|
447
|
+
const [speedOpen, setSpeedOpen] = createSignal(true);
|
|
448
|
+
const [contentWidth, setContentWidth] = createSignal(DEFAULT_CONTENT_WIDTH);
|
|
449
|
+
let estimatorSession = "";
|
|
450
|
+
let boxElement;
|
|
451
|
+
const metrics = createMemo(() => {
|
|
452
|
+
refresh();
|
|
453
|
+
if (estimatorSession !== props.sessionID) {
|
|
454
|
+
estimator.clear();
|
|
455
|
+
estimatorSession = props.sessionID;
|
|
456
|
+
}
|
|
457
|
+
return readMetrics(props.api, props.sessionID, Date.now(), estimator);
|
|
458
|
+
});
|
|
459
|
+
const hitColor = createMemo(() => {
|
|
460
|
+
const hit = metrics().cache.latestHit;
|
|
461
|
+
if (hit === undefined)
|
|
462
|
+
return props.theme.textMuted;
|
|
463
|
+
if (hit >= 85)
|
|
464
|
+
return props.theme.success;
|
|
465
|
+
if (hit >= 60)
|
|
466
|
+
return props.theme.warning;
|
|
467
|
+
return props.theme.error;
|
|
468
|
+
});
|
|
469
|
+
const trendColor = createMemo(() => {
|
|
470
|
+
const trend = metrics().cache.trend;
|
|
471
|
+
if (trend === undefined || Math.abs(trend) < 0.05)
|
|
472
|
+
return props.theme.textMuted;
|
|
473
|
+
return trend > 0 ? props.theme.success : props.theme.error;
|
|
474
|
+
});
|
|
475
|
+
const hitBarWidth = createMemo(() => {
|
|
476
|
+
const percent = formatPercent(metrics().cache.latestHit);
|
|
477
|
+
const trend = formatHitTrend(metrics().cache.trend);
|
|
478
|
+
const fixedWidth = "Hit ".length + 2 + 1 + percent.length + 1 + trend.length;
|
|
479
|
+
return Math.max(3, contentWidth() - fixedWidth);
|
|
480
|
+
});
|
|
481
|
+
onMount(() => {
|
|
482
|
+
let refreshTimer;
|
|
483
|
+
const matches = (event) => event.properties.sessionID === props.sessionID;
|
|
484
|
+
const bump = () => setRefresh((value) => value + 1);
|
|
485
|
+
const scheduleRefresh = (event) => {
|
|
486
|
+
if (!matches(event) || refreshTimer !== undefined)
|
|
487
|
+
return;
|
|
488
|
+
refreshTimer = setTimeout(() => {
|
|
489
|
+
refreshTimer = undefined;
|
|
490
|
+
bump();
|
|
491
|
+
}, 100);
|
|
492
|
+
};
|
|
493
|
+
const message = props.api.event.on("message.updated", scheduleRefresh);
|
|
494
|
+
const messageRemoved = props.api.event.on("message.removed", (event) => {
|
|
495
|
+
if (!matches(event))
|
|
496
|
+
return;
|
|
497
|
+
estimator.clear();
|
|
498
|
+
scheduleRefresh(event);
|
|
499
|
+
});
|
|
500
|
+
const part = props.api.event.on("message.part.updated", scheduleRefresh);
|
|
501
|
+
const partRemoved = props.api.event.on("message.part.removed", (event) => {
|
|
502
|
+
if (!matches(event))
|
|
503
|
+
return;
|
|
504
|
+
estimator.clear();
|
|
505
|
+
scheduleRefresh(event);
|
|
506
|
+
});
|
|
507
|
+
const delta = props.api.event.on("message.part.delta", (event) => {
|
|
508
|
+
if (!matches(event))
|
|
509
|
+
return;
|
|
510
|
+
if (event.properties.field === "text") {
|
|
511
|
+
estimator.append(`part:${event.properties.partID}:text`, event.properties.delta);
|
|
512
|
+
}
|
|
513
|
+
scheduleRefresh(event);
|
|
514
|
+
});
|
|
515
|
+
const session = props.api.event.on("session.updated", scheduleRefresh);
|
|
516
|
+
const compacted = props.api.event.on("session.compacted", scheduleRefresh);
|
|
517
|
+
onCleanup(() => {
|
|
518
|
+
if (refreshTimer !== undefined)
|
|
519
|
+
clearTimeout(refreshTimer);
|
|
520
|
+
message();
|
|
521
|
+
messageRemoved();
|
|
522
|
+
part();
|
|
523
|
+
partRemoved();
|
|
524
|
+
delta();
|
|
525
|
+
session();
|
|
526
|
+
compacted();
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
return (() => {
|
|
530
|
+
var _el$5 = _$createElement("box"), _el$6 = _$createElement("text"), _el$7 = _$createElement("span"), _el$8 = _$createElement("b"), _el$0 = _$createElement("text"), _el$1 = _$createElement("span"), _el$11 = _$createElement("span"), _el$12 = _$createTextNode(`[`), _el$13 = _$createTextNode(`]`), _el$14 = _$createElement("span"), _el$15 = _$createTextNode(` `), _el$16 = _$createElement("span"), _el$17 = _$createTextNode(` `), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$20 = _$createElement("text");
|
|
531
|
+
_$insertNode(_el$5, _el$6);
|
|
532
|
+
_$insertNode(_el$5, _el$0);
|
|
533
|
+
_$insertNode(_el$5, _el$18);
|
|
534
|
+
_$insertNode(_el$5, _el$19);
|
|
535
|
+
_$insertNode(_el$5, _el$20);
|
|
536
|
+
_$use((element) => {
|
|
537
|
+
boxElement = element;
|
|
538
|
+
}, _el$5);
|
|
539
|
+
_$setProp(_el$5, "flexDirection", "column");
|
|
540
|
+
_$setProp(_el$5, "onSizeChange", () => {
|
|
541
|
+
const width = boxElement?.width;
|
|
542
|
+
if (typeof width === "number" && width > 0) {
|
|
543
|
+
setContentWidth(Math.max(MIN_CONTENT_WIDTH, width));
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
_$insertNode(_el$6, _el$7);
|
|
547
|
+
_$insertNode(_el$7, _el$8);
|
|
548
|
+
_$insertNode(_el$8, _$createTextNode(`Cache View`));
|
|
549
|
+
_$insertNode(_el$0, _el$1);
|
|
550
|
+
_$insertNode(_el$0, _el$11);
|
|
551
|
+
_$insertNode(_el$0, _el$14);
|
|
552
|
+
_$insertNode(_el$0, _el$16);
|
|
553
|
+
_$insertNode(_el$1, _$createTextNode(`Hit `));
|
|
554
|
+
_$insertNode(_el$11, _el$12);
|
|
555
|
+
_$insertNode(_el$11, _el$13);
|
|
556
|
+
_$insert(_el$11, () => progressBar(metrics().cache.latestHit, hitBarWidth()), _el$13);
|
|
557
|
+
_$insertNode(_el$14, _el$15);
|
|
558
|
+
_$insert(_el$14, () => formatPercent(metrics().cache.latestHit), null);
|
|
559
|
+
_$insertNode(_el$16, _el$17);
|
|
560
|
+
_$insert(_el$16, () => formatHitTrend(metrics().cache.trend), null);
|
|
561
|
+
_$insert(_el$18, () => row("Session Hit", formatPercent(metrics().cache.totalHit), contentWidth()));
|
|
562
|
+
_$insert(_el$19, () => row("Read", `${formatCompact(metrics().cache.read)} tok`, contentWidth()));
|
|
563
|
+
_$insert(_el$20, () => row("Miss", `${formatCompact(metrics().cache.miss)} tok`, contentWidth()));
|
|
564
|
+
_$insert(_el$5, _$createComponent(SectionHeader, {
|
|
565
|
+
title: "Speed",
|
|
566
|
+
get open() {
|
|
567
|
+
return speedOpen();
|
|
568
|
+
},
|
|
569
|
+
get width() {
|
|
570
|
+
return contentWidth();
|
|
571
|
+
},
|
|
572
|
+
get theme() {
|
|
573
|
+
return props.theme;
|
|
574
|
+
},
|
|
575
|
+
toggle: () => setSpeedOpen((value) => !value)
|
|
576
|
+
}), null);
|
|
577
|
+
_$insert(_el$5, (() => {
|
|
578
|
+
var _c$ = _$memo(() => !!speedOpen());
|
|
579
|
+
return () => _c$() && [(() => {
|
|
580
|
+
var _el$21 = _$createElement("text");
|
|
581
|
+
_$insert(_el$21, () => row("Now", formatSpeed(metrics().speed.now, true), contentWidth()));
|
|
582
|
+
_$effect((_$p) => _$setProp(_el$21, "fg", props.theme.textMuted, _$p));
|
|
583
|
+
return _el$21;
|
|
584
|
+
})(), (() => {
|
|
585
|
+
var _el$22 = _$createElement("text");
|
|
586
|
+
_$insert(_el$22, () => row("TTFT", formatDuration(metrics().speed.ttft, true), contentWidth()));
|
|
587
|
+
_$effect((_$p) => _$setProp(_el$22, "fg", props.theme.textMuted, _$p));
|
|
588
|
+
return _el$22;
|
|
589
|
+
})(), (() => {
|
|
590
|
+
var _el$23 = _$createElement("text");
|
|
591
|
+
_$insert(_el$23, () => row("Last", formatSpeed(metrics().speed.last, true), contentWidth()));
|
|
592
|
+
_$effect((_$p) => _$setProp(_el$23, "fg", props.theme.textMuted, _$p));
|
|
593
|
+
return _el$23;
|
|
594
|
+
})(), (() => {
|
|
595
|
+
var _el$24 = _$createElement("text");
|
|
596
|
+
_$insert(_el$24, () => row("Avg", formatSpeed(metrics().speed.average, true), contentWidth()));
|
|
597
|
+
_$effect((_$p) => _$setProp(_el$24, "fg", props.theme.textMuted, _$p));
|
|
598
|
+
return _el$24;
|
|
599
|
+
})(), (() => {
|
|
600
|
+
var _el$25 = _$createElement("text");
|
|
601
|
+
_$insert(_el$25, () => row("Trend", sparkline(metrics().speed.trend), contentWidth()));
|
|
602
|
+
_$effect((_$p) => _$setProp(_el$25, "fg", props.theme.textMuted, _$p));
|
|
603
|
+
return _el$25;
|
|
604
|
+
})()];
|
|
605
|
+
})(), null);
|
|
606
|
+
_$insert(_el$5, _$createComponent(SectionHeader, {
|
|
607
|
+
title: "Estimated Tokens",
|
|
608
|
+
get open() {
|
|
609
|
+
return tokensOpen();
|
|
610
|
+
},
|
|
611
|
+
get width() {
|
|
612
|
+
return contentWidth();
|
|
613
|
+
},
|
|
614
|
+
get theme() {
|
|
615
|
+
return props.theme;
|
|
616
|
+
},
|
|
617
|
+
toggle: () => setTokensOpen((value) => !value)
|
|
618
|
+
}), null);
|
|
619
|
+
_$insert(_el$5, (() => {
|
|
620
|
+
var _c$2 = _$memo(() => !!tokensOpen());
|
|
621
|
+
return () => _c$2() && [(() => {
|
|
622
|
+
var _el$26 = _$createElement("text");
|
|
623
|
+
_$insert(_el$26, () => row("Prompt", `${formatCompact(metrics().tokens.system + metrics().tokens.user + metrics().tokens.agent)} tok`, contentWidth()));
|
|
624
|
+
_$effect((_$p) => _$setProp(_el$26, "fg", props.theme.textMuted, _$p));
|
|
625
|
+
return _el$26;
|
|
626
|
+
})(), (() => {
|
|
627
|
+
var _el$27 = _$createElement("text");
|
|
628
|
+
_$insert(_el$27, () => row("Tool Call", `${formatCompact(metrics().tokens.toolCall)} tok`, contentWidth()));
|
|
629
|
+
_$effect((_$p) => _$setProp(_el$27, "fg", props.theme.textMuted, _$p));
|
|
630
|
+
return _el$27;
|
|
631
|
+
})(), (() => {
|
|
632
|
+
var _el$28 = _$createElement("text");
|
|
633
|
+
_$insert(_el$28, () => row("Tool Result", `${formatCompact(metrics().tokens.toolResult)} tok`, contentWidth()));
|
|
634
|
+
_$effect((_$p) => _$setProp(_el$28, "fg", props.theme.textMuted, _$p));
|
|
635
|
+
return _el$28;
|
|
636
|
+
})(), (() => {
|
|
637
|
+
var _el$29 = _$createElement("text");
|
|
638
|
+
_$insert(_el$29, () => row("Agent Reasoning", `${formatCompact(metrics().tokens.reasoning)} tok`, contentWidth()));
|
|
639
|
+
_$effect((_$p) => _$setProp(_el$29, "fg", props.theme.textMuted, _$p));
|
|
640
|
+
return _el$29;
|
|
641
|
+
})(), (() => {
|
|
642
|
+
var _el$30 = _$createElement("text");
|
|
643
|
+
_$insert(_el$30, () => row("Agent Output", `${formatCompact(metrics().tokens.output)} tok`, contentWidth()));
|
|
644
|
+
_$effect((_$p) => _$setProp(_el$30, "fg", props.theme.textMuted, _$p));
|
|
645
|
+
return _el$30;
|
|
646
|
+
})(), (() => {
|
|
647
|
+
var _el$31 = _$createElement("text");
|
|
648
|
+
_$insert(_el$31, () => row("Total", `${formatCompact(metrics().tokens.total)} tok`, contentWidth()));
|
|
649
|
+
_$effect((_$p) => _$setProp(_el$31, "fg", props.theme.text, _$p));
|
|
650
|
+
return _el$31;
|
|
651
|
+
})()];
|
|
652
|
+
})(), null);
|
|
653
|
+
_$insert(_el$5, (() => {
|
|
654
|
+
var _c$3 = _$memo(() => metrics().skills.length > 0);
|
|
655
|
+
return () => _c$3() && [_$createComponent(SectionHeader, {
|
|
656
|
+
get title() {
|
|
657
|
+
return `Loaded Skills (${metrics().skills.length})`;
|
|
658
|
+
},
|
|
659
|
+
get open() {
|
|
660
|
+
return skillsOpen();
|
|
661
|
+
},
|
|
662
|
+
get width() {
|
|
663
|
+
return contentWidth();
|
|
664
|
+
},
|
|
665
|
+
get theme() {
|
|
666
|
+
return props.theme;
|
|
667
|
+
},
|
|
668
|
+
toggle: () => setSkillsOpen((value) => !value)
|
|
669
|
+
}), _$memo(() => _$memo(() => !!skillsOpen())() && metrics().skills.map((skill) => (() => {
|
|
670
|
+
var _el$32 = _$createElement("text");
|
|
671
|
+
_$insert(_el$32, () => skillRow(skill, contentWidth()));
|
|
672
|
+
_$effect((_$p) => _$setProp(_el$32, "fg", props.theme.textMuted, _$p));
|
|
673
|
+
return _el$32;
|
|
674
|
+
})()))];
|
|
675
|
+
})(), null);
|
|
676
|
+
_$effect((_p$) => {
|
|
677
|
+
var _v$4 = {
|
|
678
|
+
fg: props.theme.text
|
|
679
|
+
}, _v$5 = {
|
|
680
|
+
fg: props.theme.text
|
|
681
|
+
}, _v$6 = {
|
|
682
|
+
fg: hitColor()
|
|
683
|
+
}, _v$7 = {
|
|
684
|
+
fg: props.theme.text
|
|
685
|
+
}, _v$8 = {
|
|
686
|
+
fg: trendColor()
|
|
687
|
+
}, _v$9 = props.theme.textMuted, _v$0 = props.theme.textMuted, _v$1 = props.theme.textMuted;
|
|
688
|
+
_v$4 !== _p$.e && (_p$.e = _$setProp(_el$7, "style", _v$4, _p$.e));
|
|
689
|
+
_v$5 !== _p$.t && (_p$.t = _$setProp(_el$1, "style", _v$5, _p$.t));
|
|
690
|
+
_v$6 !== _p$.a && (_p$.a = _$setProp(_el$11, "style", _v$6, _p$.a));
|
|
691
|
+
_v$7 !== _p$.o && (_p$.o = _$setProp(_el$14, "style", _v$7, _p$.o));
|
|
692
|
+
_v$8 !== _p$.i && (_p$.i = _$setProp(_el$16, "style", _v$8, _p$.i));
|
|
693
|
+
_v$9 !== _p$.n && (_p$.n = _$setProp(_el$18, "fg", _v$9, _p$.n));
|
|
694
|
+
_v$0 !== _p$.s && (_p$.s = _$setProp(_el$19, "fg", _v$0, _p$.s));
|
|
695
|
+
_v$1 !== _p$.h && (_p$.h = _$setProp(_el$20, "fg", _v$1, _p$.h));
|
|
696
|
+
return _p$;
|
|
697
|
+
}, {
|
|
698
|
+
e: undefined,
|
|
699
|
+
t: undefined,
|
|
700
|
+
a: undefined,
|
|
701
|
+
o: undefined,
|
|
702
|
+
i: undefined,
|
|
703
|
+
n: undefined,
|
|
704
|
+
s: undefined,
|
|
705
|
+
h: undefined
|
|
706
|
+
});
|
|
707
|
+
return _el$5;
|
|
708
|
+
})();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/index.tsx
|
|
712
|
+
var tui = async (api) => {
|
|
713
|
+
api.slots.register({
|
|
714
|
+
order: 56,
|
|
715
|
+
slots: {
|
|
716
|
+
sidebar_content(ctx, props) {
|
|
717
|
+
return _$createComponent2(CacheView, {
|
|
718
|
+
api,
|
|
719
|
+
get sessionID() {
|
|
720
|
+
return props.session_id ?? "";
|
|
721
|
+
},
|
|
722
|
+
get theme() {
|
|
723
|
+
return ctx.theme.current;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
var plugin = {
|
|
731
|
+
id: "opencode-cache-view",
|
|
732
|
+
tui
|
|
733
|
+
};
|
|
734
|
+
var src_default = plugin;
|
|
735
|
+
export {
|
|
736
|
+
src_default as default
|
|
737
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@mtayfur/opencode-cache-view",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Minimal OpenCode TUI sidebar for cache hit, estimated tokens, and generation speed.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mtayfur/opencode-cache-view.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/mtayfur/opencode-cache-view/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/mtayfur/opencode-cache-view#readme",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
"./tui": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bun run scripts/build.ts",
|
|
26
|
+
"setup": "bash ./install.sh",
|
|
27
|
+
"setup:uninstall": "bash ./install.sh --uninstall",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepack": "bun run build"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"solid-js": "^1.9.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@opencode-ai/plugin": ">=1.14.0",
|
|
36
|
+
"@opencode-ai/sdk": ">=1.14.0",
|
|
37
|
+
"@opentui/core": ">=0.2.0",
|
|
38
|
+
"@opentui/solid": ">=0.2.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@opencode-ai/plugin": "^1.18.16",
|
|
42
|
+
"@opencode-ai/sdk": "^1.18.16",
|
|
43
|
+
"@opentui/core": "^0.5.1",
|
|
44
|
+
"@opentui/solid": "^0.5.1",
|
|
45
|
+
"jsonc-parser": "^3.3.1",
|
|
46
|
+
"typescript": "^5.8.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"bun": ">=1.3.14",
|
|
50
|
+
"opencode": ">=1.18.16 <2"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"opencode",
|
|
54
|
+
"plugin",
|
|
55
|
+
"cache",
|
|
56
|
+
"tokens",
|
|
57
|
+
"tui"
|
|
58
|
+
],
|
|
59
|
+
"packageManager": "bun@1.3.14"
|
|
60
|
+
}
|