@mtayfur/opencode-cache-view 0.0.1 → 0.0.3
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 +7 -6
- package/dist/index.js +300 -172
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenCode Cache View
|
|
2
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.
|
|
3
|
+
`@mtayfur/opencode-cache-view` adds a compact sidebar to the OpenCode TUI for cache efficiency, estimated context usage, generation speed, and loaded skills. The latest cache-hit ratio and trend also remain visible in the prompt status row.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
Cache View
|
|
@@ -9,10 +9,8 @@ Session Hit 96.0%
|
|
|
9
9
|
Read 8.0M tok
|
|
10
10
|
Miss 326K tok
|
|
11
11
|
▼ Speed ─────────────────────
|
|
12
|
-
Now —
|
|
13
12
|
TTFT ~1.4 s
|
|
14
|
-
|
|
15
|
-
Avg ~42 tok/s
|
|
13
|
+
TPS 47 tok/s
|
|
16
14
|
Trend ▂▅▄▇▆█
|
|
17
15
|
▼ Estimated Tokens ──────────
|
|
18
16
|
Prompt 186K tok
|
|
@@ -30,11 +28,13 @@ customize-opencode 3.1K tok
|
|
|
30
28
|
|
|
31
29
|
- **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
30
|
- **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:** `
|
|
31
|
+
- **Speed:** `TPS` is calculated when an LLM step finishes as `(output + reasoning) / step duration`, using OpenCode's exact provider token totals. It therefore includes hidden reasoning without estimating from exposed reasoning summaries. While a step is running, the value is shown as `… tok/s`; tool execution between steps is excluded. `TTFT` retains the estimated delay to the first visible reasoning or text block, and `Trend` shows the latest eight completed-step TPS values.
|
|
34
32
|
- **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
33
|
|
|
36
34
|
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
35
|
|
|
36
|
+
The prompt status indicator uses the composable `session_prompt_right` slot, so it does not replace the prompt or conflict with plugins that customize prompt behavior. It shows the same completed-step TPS as the sidebar, followed by a single separator and the cache ratio; only the cache trend is color-coded.
|
|
37
|
+
|
|
38
38
|
## Requirements
|
|
39
39
|
|
|
40
40
|
- OpenCode `1.18.16` or a newer compatible 1.x release
|
|
@@ -71,6 +71,7 @@ Restart OpenCode after either operation.
|
|
|
71
71
|
```text
|
|
72
72
|
src/
|
|
73
73
|
├── index.tsx Plugin entrypoint
|
|
74
|
+
├── cache-status.tsx Prompt status-row cache ratio
|
|
74
75
|
├── cache-view.tsx Solid/OpenTUI view and event subscriptions
|
|
75
76
|
├── format.ts Display formatting
|
|
76
77
|
├── token-estimator.ts Cached character-based token estimation
|
|
@@ -79,7 +80,7 @@ src/
|
|
|
79
80
|
├── context.ts Active context, token, and skill calculations
|
|
80
81
|
├── helpers.ts Shared message and part helpers
|
|
81
82
|
├── read.ts Metric orchestration
|
|
82
|
-
├── speed.ts
|
|
83
|
+
├── speed.ts Completed-step generation speed
|
|
83
84
|
└── types.ts Metric data contracts
|
|
84
85
|
```
|
|
85
86
|
|
package/dist/index.js
CHANGED
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
import { createComponent as _$createComponent2 } from "@opentui/solid";
|
|
4
4
|
import { memo as _$memo2 } from "@opentui/solid";
|
|
5
5
|
|
|
6
|
-
// src/cache-
|
|
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";
|
|
6
|
+
// src/cache-status.tsx
|
|
11
7
|
import { setProp as _$setProp } from "@opentui/solid";
|
|
12
8
|
import { effect as _$effect } from "@opentui/solid";
|
|
9
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
13
10
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
14
11
|
import { insert as _$insert } from "@opentui/solid";
|
|
15
12
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
@@ -155,6 +152,110 @@ function cacheMetrics(sessionTokens, normalUsage, allUsage) {
|
|
|
155
152
|
};
|
|
156
153
|
}
|
|
157
154
|
|
|
155
|
+
// src/cache-status.tsx
|
|
156
|
+
function CacheStatus(props) {
|
|
157
|
+
const [refresh, setRefresh] = createSignal(0);
|
|
158
|
+
const metrics = createMemo(() => {
|
|
159
|
+
refresh();
|
|
160
|
+
const messages = props.api.state.session.messages(props.sessionID);
|
|
161
|
+
const parts = new Map;
|
|
162
|
+
for (const message of messages)
|
|
163
|
+
parts.set(message.id, props.api.state.part(message.id));
|
|
164
|
+
const assistants = assistantMessages(messages, false);
|
|
165
|
+
const allUsage = usageSamples(assistantMessages(messages, true), parts);
|
|
166
|
+
const normalUsage = usageSamples(assistants, parts);
|
|
167
|
+
return {
|
|
168
|
+
cache: cacheMetrics(props.api.state.session.get(props.sessionID)?.tokens, normalUsage, allUsage),
|
|
169
|
+
speed: props.speed.get(props.sessionID)
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
const trendColor = createMemo(() => {
|
|
173
|
+
const trend = metrics().cache.trend;
|
|
174
|
+
if (trend === undefined || Math.abs(trend) < 0.05)
|
|
175
|
+
return props.theme.textMuted;
|
|
176
|
+
return trend > 0 ? props.theme.success : props.theme.error;
|
|
177
|
+
});
|
|
178
|
+
const speedValue = createMemo(() => {
|
|
179
|
+
const speed = metrics().speed;
|
|
180
|
+
return speed.active ? "\u2026 tok/s" : formatSpeed(speed.value);
|
|
181
|
+
});
|
|
182
|
+
onMount(() => {
|
|
183
|
+
let refreshTimer;
|
|
184
|
+
const scheduleRefresh = (event) => {
|
|
185
|
+
if (event.properties.sessionID !== props.sessionID || refreshTimer !== undefined)
|
|
186
|
+
return;
|
|
187
|
+
refreshTimer = setTimeout(() => {
|
|
188
|
+
refreshTimer = undefined;
|
|
189
|
+
setRefresh((value) => value + 1);
|
|
190
|
+
}, 100);
|
|
191
|
+
};
|
|
192
|
+
const message = props.api.event.on("message.updated", scheduleRefresh);
|
|
193
|
+
const messageRemoved = props.api.event.on("message.removed", scheduleRefresh);
|
|
194
|
+
const part = props.api.event.on("message.part.updated", scheduleRefresh);
|
|
195
|
+
const delta = props.api.event.on("message.part.delta", scheduleRefresh);
|
|
196
|
+
const partRemoved = props.api.event.on("message.part.removed", scheduleRefresh);
|
|
197
|
+
const session = props.api.event.on("session.updated", scheduleRefresh);
|
|
198
|
+
const compacted = props.api.event.on("session.compacted", scheduleRefresh);
|
|
199
|
+
onCleanup(() => {
|
|
200
|
+
if (refreshTimer !== undefined)
|
|
201
|
+
clearTimeout(refreshTimer);
|
|
202
|
+
message();
|
|
203
|
+
messageRemoved();
|
|
204
|
+
part();
|
|
205
|
+
delta();
|
|
206
|
+
partRemoved();
|
|
207
|
+
session();
|
|
208
|
+
compacted();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
return (() => {
|
|
212
|
+
var _el$ = _$createElement("text"), _el$2 = _$createElement("span"), _el$3 = _$createElement("span"), _el$5 = _$createElement("span"), _el$6 = _$createElement("span"), _el$7 = _$createTextNode(` `);
|
|
213
|
+
_$insertNode(_el$, _el$2);
|
|
214
|
+
_$insertNode(_el$, _el$3);
|
|
215
|
+
_$insertNode(_el$, _el$5);
|
|
216
|
+
_$insertNode(_el$, _el$6);
|
|
217
|
+
_$insert(_el$2, speedValue);
|
|
218
|
+
_$insertNode(_el$3, _$createTextNode(` \u2502 `));
|
|
219
|
+
_$insert(_el$5, () => formatPercent(metrics().cache.latestHit));
|
|
220
|
+
_$insertNode(_el$6, _el$7);
|
|
221
|
+
_$insert(_el$6, () => formatHitTrend(metrics().cache.trend), null);
|
|
222
|
+
_$effect((_p$) => {
|
|
223
|
+
var _v$ = {
|
|
224
|
+
fg: props.theme.text
|
|
225
|
+
}, _v$2 = {
|
|
226
|
+
fg: props.theme.textMuted
|
|
227
|
+
}, _v$3 = {
|
|
228
|
+
fg: props.theme.text
|
|
229
|
+
}, _v$4 = {
|
|
230
|
+
fg: trendColor()
|
|
231
|
+
};
|
|
232
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "style", _v$, _p$.e));
|
|
233
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "style", _v$2, _p$.t));
|
|
234
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$5, "style", _v$3, _p$.a));
|
|
235
|
+
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$6, "style", _v$4, _p$.o));
|
|
236
|
+
return _p$;
|
|
237
|
+
}, {
|
|
238
|
+
e: undefined,
|
|
239
|
+
t: undefined,
|
|
240
|
+
a: undefined,
|
|
241
|
+
o: undefined
|
|
242
|
+
});
|
|
243
|
+
return _el$;
|
|
244
|
+
})();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/cache-view.tsx
|
|
248
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
249
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
250
|
+
import { createTextNode as _$createTextNode2 } from "@opentui/solid";
|
|
251
|
+
import { use as _$use } from "@opentui/solid";
|
|
252
|
+
import { setProp as _$setProp2 } from "@opentui/solid";
|
|
253
|
+
import { effect as _$effect2 } from "@opentui/solid";
|
|
254
|
+
import { insertNode as _$insertNode2 } from "@opentui/solid";
|
|
255
|
+
import { insert as _$insert2 } from "@opentui/solid";
|
|
256
|
+
import { createElement as _$createElement2 } from "@opentui/solid";
|
|
257
|
+
import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup2, onMount as onMount2 } from "solid-js";
|
|
258
|
+
|
|
158
259
|
// src/metrics/context.ts
|
|
159
260
|
function estimateSessionTokens(messages, parts, activeStart, session, config, estimator) {
|
|
160
261
|
const agentConfig = config;
|
|
@@ -263,72 +364,94 @@ function loadedSkills(messages, parts, activeStart, estimator) {
|
|
|
263
364
|
}
|
|
264
365
|
|
|
265
366
|
// src/metrics/speed.ts
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
367
|
+
function createSpeedTracker(api) {
|
|
368
|
+
const starts = new Map;
|
|
369
|
+
const latest = new Map;
|
|
370
|
+
const trends = new Map;
|
|
371
|
+
const part = api.event.on("message.part.updated", (event) => {
|
|
372
|
+
const value = event.properties.part;
|
|
373
|
+
if (value.type === "step-start") {
|
|
374
|
+
if (!starts.has(value.messageID)) {
|
|
375
|
+
starts.set(value.messageID, {
|
|
376
|
+
sessionID: event.properties.sessionID,
|
|
377
|
+
time: event.properties.time
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (value.type !== "step-finish")
|
|
383
|
+
return;
|
|
384
|
+
const start = starts.get(value.messageID);
|
|
385
|
+
starts.delete(value.messageID);
|
|
386
|
+
if (!start || start.sessionID !== event.properties.sessionID)
|
|
387
|
+
return;
|
|
388
|
+
const duration = event.properties.time - start.time;
|
|
389
|
+
const tokens = value.tokens.output + value.tokens.reasoning;
|
|
390
|
+
if (duration <= 0 || tokens <= 0)
|
|
391
|
+
return;
|
|
392
|
+
const speed = tokens / duration * 1000;
|
|
393
|
+
latest.set(start.sessionID, speed);
|
|
394
|
+
trends.set(start.sessionID, [...trends.get(start.sessionID) ?? [], speed].slice(-8));
|
|
395
|
+
});
|
|
396
|
+
const message = api.event.on("message.updated", (event) => {
|
|
397
|
+
if (event.properties.info.role !== "assistant")
|
|
398
|
+
return;
|
|
399
|
+
if (event.properties.info.time.completed === undefined && event.properties.info.error === undefined)
|
|
400
|
+
return;
|
|
401
|
+
starts.delete(event.properties.info.id);
|
|
402
|
+
});
|
|
403
|
+
const messageRemoved = api.event.on("message.removed", (event) => {
|
|
404
|
+
starts.delete(event.properties.messageID);
|
|
405
|
+
});
|
|
406
|
+
const sessionDeleted = api.event.on("session.deleted", (event) => {
|
|
407
|
+
latest.delete(event.properties.sessionID);
|
|
408
|
+
trends.delete(event.properties.sessionID);
|
|
409
|
+
for (const [messageID, start] of starts) {
|
|
410
|
+
if (start.sessionID === event.properties.sessionID)
|
|
411
|
+
starts.delete(messageID);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
return {
|
|
415
|
+
get(sessionID) {
|
|
416
|
+
let active = false;
|
|
417
|
+
for (const start of starts.values()) {
|
|
418
|
+
if (start.sessionID === sessionID) {
|
|
419
|
+
active = true;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { active, value: latest.get(sessionID), trend: trends.get(sessionID) ?? [] };
|
|
424
|
+
},
|
|
425
|
+
dispose() {
|
|
426
|
+
part();
|
|
427
|
+
message();
|
|
428
|
+
messageRemoved();
|
|
429
|
+
sessionDeleted();
|
|
430
|
+
starts.clear();
|
|
431
|
+
latest.clear();
|
|
432
|
+
trends.clear();
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function isObservablePart(part) {
|
|
437
|
+
if (part.type === "reasoning")
|
|
438
|
+
return part.text.length > 0;
|
|
439
|
+
return part.type === "text" && !part.synthetic && !part.ignored && part.text.length > 0;
|
|
276
440
|
}
|
|
277
441
|
function timeToFirstToken(parts, message) {
|
|
278
442
|
if (!message)
|
|
279
443
|
return;
|
|
280
|
-
|
|
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;
|
|
444
|
+
let first = Number.POSITIVE_INFINITY;
|
|
307
445
|
for (const part of partsFor(parts, message.id)) {
|
|
308
|
-
if (part
|
|
446
|
+
if (!isObservablePart(part))
|
|
309
447
|
continue;
|
|
310
448
|
const start = part.time?.start;
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
tokens += estimator.count(`part:${part.id}:text`, part.text);
|
|
314
|
-
duration += now - start;
|
|
449
|
+
if (typeof start === "number" && start < first)
|
|
450
|
+
first = start;
|
|
315
451
|
}
|
|
316
|
-
if (
|
|
452
|
+
if (!Number.isFinite(first) || first < message.time.created)
|
|
317
453
|
return;
|
|
318
|
-
return
|
|
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
|
-
};
|
|
454
|
+
return first - message.time.created;
|
|
332
455
|
}
|
|
333
456
|
|
|
334
457
|
// src/metrics/read.ts
|
|
@@ -344,13 +467,13 @@ function emptyTokenEstimate() {
|
|
|
344
467
|
total: 0
|
|
345
468
|
};
|
|
346
469
|
}
|
|
347
|
-
function readMetrics(api, sessionID,
|
|
470
|
+
function readMetrics(api, sessionID, estimator, speed) {
|
|
348
471
|
if (!sessionID) {
|
|
349
472
|
return {
|
|
350
473
|
cache: { read: 0, miss: 0 },
|
|
351
474
|
tokens: emptyTokenEstimate(),
|
|
352
475
|
skills: [],
|
|
353
|
-
speed
|
|
476
|
+
speed
|
|
354
477
|
};
|
|
355
478
|
}
|
|
356
479
|
const messages = api.state.session.messages(sessionID);
|
|
@@ -367,7 +490,7 @@ function readMetrics(api, sessionID, now, estimator) {
|
|
|
367
490
|
cache: cacheMetrics(session?.tokens, normalUsage, allUsage),
|
|
368
491
|
tokens: estimateSessionTokens(messages, parts, activeStart, session, api.state.config, estimator),
|
|
369
492
|
skills: loadedSkills(messages, parts, activeStart, estimator),
|
|
370
|
-
speed:
|
|
493
|
+
speed: { ...speed, ttft: timeToFirstToken(parts, assistants.at(-1)) }
|
|
371
494
|
};
|
|
372
495
|
}
|
|
373
496
|
|
|
@@ -415,21 +538,21 @@ var MIN_CONTENT_WIDTH = 20;
|
|
|
415
538
|
function SectionHeader(props) {
|
|
416
539
|
const prefix = `${props.open ? "\u25BC" : "\u25B6"} ${props.title}`;
|
|
417
540
|
return (() => {
|
|
418
|
-
var _el$ = _$
|
|
419
|
-
_$
|
|
420
|
-
_$
|
|
421
|
-
_$
|
|
422
|
-
_$
|
|
423
|
-
_$
|
|
424
|
-
_$
|
|
541
|
+
var _el$ = _$createElement2("text"), _el$2 = _$createElement2("span"), _el$3 = _$createElement2("b"), _el$4 = _$createElement2("span");
|
|
542
|
+
_$insertNode2(_el$, _el$2);
|
|
543
|
+
_$insertNode2(_el$, _el$4);
|
|
544
|
+
_$insertNode2(_el$2, _el$3);
|
|
545
|
+
_$insert2(_el$3, prefix);
|
|
546
|
+
_$insert2(_el$4, () => "\u2500".repeat(Math.max(1, props.width - prefix.length)));
|
|
547
|
+
_$effect2((_p$) => {
|
|
425
548
|
var _v$ = props.toggle, _v$2 = {
|
|
426
549
|
fg: props.theme.text
|
|
427
550
|
}, _v$3 = {
|
|
428
551
|
fg: props.theme.border
|
|
429
552
|
};
|
|
430
|
-
_v$ !== _p$.e && (_p$.e = _$
|
|
431
|
-
_v$2 !== _p$.t && (_p$.t = _$
|
|
432
|
-
_v$3 !== _p$.a && (_p$.a = _$
|
|
553
|
+
_v$ !== _p$.e && (_p$.e = _$setProp2(_el$, "onMouseUp", _v$, _p$.e));
|
|
554
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp2(_el$2, "style", _v$2, _p$.t));
|
|
555
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp2(_el$4, "style", _v$3, _p$.a));
|
|
433
556
|
return _p$;
|
|
434
557
|
}, {
|
|
435
558
|
e: undefined,
|
|
@@ -441,22 +564,22 @@ function SectionHeader(props) {
|
|
|
441
564
|
}
|
|
442
565
|
function CacheView(props) {
|
|
443
566
|
const estimator = new TokenEstimator;
|
|
444
|
-
const [refresh, setRefresh] =
|
|
445
|
-
const [tokensOpen, setTokensOpen] =
|
|
446
|
-
const [skillsOpen, setSkillsOpen] =
|
|
447
|
-
const [speedOpen, setSpeedOpen] =
|
|
448
|
-
const [contentWidth, setContentWidth] =
|
|
567
|
+
const [refresh, setRefresh] = createSignal2(0);
|
|
568
|
+
const [tokensOpen, setTokensOpen] = createSignal2(true);
|
|
569
|
+
const [skillsOpen, setSkillsOpen] = createSignal2(true);
|
|
570
|
+
const [speedOpen, setSpeedOpen] = createSignal2(true);
|
|
571
|
+
const [contentWidth, setContentWidth] = createSignal2(DEFAULT_CONTENT_WIDTH);
|
|
449
572
|
let estimatorSession = "";
|
|
450
573
|
let boxElement;
|
|
451
|
-
const metrics =
|
|
574
|
+
const metrics = createMemo2(() => {
|
|
452
575
|
refresh();
|
|
453
576
|
if (estimatorSession !== props.sessionID) {
|
|
454
577
|
estimator.clear();
|
|
455
578
|
estimatorSession = props.sessionID;
|
|
456
579
|
}
|
|
457
|
-
return readMetrics(props.api, props.sessionID,
|
|
580
|
+
return readMetrics(props.api, props.sessionID, estimator, props.speed.get(props.sessionID));
|
|
458
581
|
});
|
|
459
|
-
const hitColor =
|
|
582
|
+
const hitColor = createMemo2(() => {
|
|
460
583
|
const hit = metrics().cache.latestHit;
|
|
461
584
|
if (hit === undefined)
|
|
462
585
|
return props.theme.textMuted;
|
|
@@ -466,19 +589,19 @@ function CacheView(props) {
|
|
|
466
589
|
return props.theme.warning;
|
|
467
590
|
return props.theme.error;
|
|
468
591
|
});
|
|
469
|
-
const trendColor =
|
|
592
|
+
const trendColor = createMemo2(() => {
|
|
470
593
|
const trend = metrics().cache.trend;
|
|
471
594
|
if (trend === undefined || Math.abs(trend) < 0.05)
|
|
472
595
|
return props.theme.textMuted;
|
|
473
596
|
return trend > 0 ? props.theme.success : props.theme.error;
|
|
474
597
|
});
|
|
475
|
-
const hitBarWidth =
|
|
598
|
+
const hitBarWidth = createMemo2(() => {
|
|
476
599
|
const percent = formatPercent(metrics().cache.latestHit);
|
|
477
600
|
const trend = formatHitTrend(metrics().cache.trend);
|
|
478
601
|
const fixedWidth = "Hit ".length + 2 + 1 + percent.length + 1 + trend.length;
|
|
479
602
|
return Math.max(3, contentWidth() - fixedWidth);
|
|
480
603
|
});
|
|
481
|
-
|
|
604
|
+
onMount2(() => {
|
|
482
605
|
let refreshTimer;
|
|
483
606
|
const matches = (event) => event.properties.sessionID === props.sessionID;
|
|
484
607
|
const bump = () => setRefresh((value) => value + 1);
|
|
@@ -514,7 +637,7 @@ function CacheView(props) {
|
|
|
514
637
|
});
|
|
515
638
|
const session = props.api.event.on("session.updated", scheduleRefresh);
|
|
516
639
|
const compacted = props.api.event.on("session.compacted", scheduleRefresh);
|
|
517
|
-
|
|
640
|
+
onCleanup2(() => {
|
|
518
641
|
if (refreshTimer !== undefined)
|
|
519
642
|
clearTimeout(refreshTimer);
|
|
520
643
|
message();
|
|
@@ -527,41 +650,41 @@ function CacheView(props) {
|
|
|
527
650
|
});
|
|
528
651
|
});
|
|
529
652
|
return (() => {
|
|
530
|
-
var _el$5 = _$
|
|
531
|
-
_$
|
|
532
|
-
_$
|
|
533
|
-
_$
|
|
534
|
-
_$
|
|
535
|
-
_$
|
|
653
|
+
var _el$5 = _$createElement2("box"), _el$6 = _$createElement2("text"), _el$7 = _$createElement2("span"), _el$8 = _$createElement2("b"), _el$0 = _$createElement2("text"), _el$1 = _$createElement2("span"), _el$11 = _$createElement2("span"), _el$12 = _$createTextNode2(`[`), _el$13 = _$createTextNode2(`]`), _el$14 = _$createElement2("span"), _el$15 = _$createTextNode2(` `), _el$16 = _$createElement2("span"), _el$17 = _$createTextNode2(` `), _el$18 = _$createElement2("text"), _el$19 = _$createElement2("text"), _el$20 = _$createElement2("text");
|
|
654
|
+
_$insertNode2(_el$5, _el$6);
|
|
655
|
+
_$insertNode2(_el$5, _el$0);
|
|
656
|
+
_$insertNode2(_el$5, _el$18);
|
|
657
|
+
_$insertNode2(_el$5, _el$19);
|
|
658
|
+
_$insertNode2(_el$5, _el$20);
|
|
536
659
|
_$use((element) => {
|
|
537
660
|
boxElement = element;
|
|
538
661
|
}, _el$5);
|
|
539
|
-
_$
|
|
540
|
-
_$
|
|
662
|
+
_$setProp2(_el$5, "flexDirection", "column");
|
|
663
|
+
_$setProp2(_el$5, "onSizeChange", () => {
|
|
541
664
|
const width = boxElement?.width;
|
|
542
665
|
if (typeof width === "number" && width > 0) {
|
|
543
666
|
setContentWidth(Math.max(MIN_CONTENT_WIDTH, width));
|
|
544
667
|
}
|
|
545
668
|
});
|
|
546
|
-
_$
|
|
547
|
-
_$
|
|
548
|
-
_$
|
|
549
|
-
_$
|
|
550
|
-
_$
|
|
551
|
-
_$
|
|
552
|
-
_$
|
|
553
|
-
_$
|
|
554
|
-
_$
|
|
555
|
-
_$
|
|
556
|
-
_$
|
|
557
|
-
_$
|
|
558
|
-
_$
|
|
559
|
-
_$
|
|
560
|
-
_$
|
|
561
|
-
_$
|
|
562
|
-
_$
|
|
563
|
-
_$
|
|
564
|
-
_$
|
|
669
|
+
_$insertNode2(_el$6, _el$7);
|
|
670
|
+
_$insertNode2(_el$7, _el$8);
|
|
671
|
+
_$insertNode2(_el$8, _$createTextNode2(`Cache View`));
|
|
672
|
+
_$insertNode2(_el$0, _el$1);
|
|
673
|
+
_$insertNode2(_el$0, _el$11);
|
|
674
|
+
_$insertNode2(_el$0, _el$14);
|
|
675
|
+
_$insertNode2(_el$0, _el$16);
|
|
676
|
+
_$insertNode2(_el$1, _$createTextNode2(`Hit `));
|
|
677
|
+
_$insertNode2(_el$11, _el$12);
|
|
678
|
+
_$insertNode2(_el$11, _el$13);
|
|
679
|
+
_$insert2(_el$11, () => progressBar(metrics().cache.latestHit, hitBarWidth()), _el$13);
|
|
680
|
+
_$insertNode2(_el$14, _el$15);
|
|
681
|
+
_$insert2(_el$14, () => formatPercent(metrics().cache.latestHit), null);
|
|
682
|
+
_$insertNode2(_el$16, _el$17);
|
|
683
|
+
_$insert2(_el$16, () => formatHitTrend(metrics().cache.trend), null);
|
|
684
|
+
_$insert2(_el$18, () => row("Session Hit", formatPercent(metrics().cache.totalHit), contentWidth()));
|
|
685
|
+
_$insert2(_el$19, () => row("Read", `${formatCompact(metrics().cache.read)} tok`, contentWidth()));
|
|
686
|
+
_$insert2(_el$20, () => row("Miss", `${formatCompact(metrics().cache.miss)} tok`, contentWidth()));
|
|
687
|
+
_$insert2(_el$5, _$createComponent(SectionHeader, {
|
|
565
688
|
title: "Speed",
|
|
566
689
|
get open() {
|
|
567
690
|
return speedOpen();
|
|
@@ -574,36 +697,26 @@ function CacheView(props) {
|
|
|
574
697
|
},
|
|
575
698
|
toggle: () => setSpeedOpen((value) => !value)
|
|
576
699
|
}), null);
|
|
577
|
-
_$
|
|
700
|
+
_$insert2(_el$5, (() => {
|
|
578
701
|
var _c$ = _$memo(() => !!speedOpen());
|
|
579
702
|
return () => _c$() && [(() => {
|
|
580
|
-
var _el$21 = _$
|
|
581
|
-
_$
|
|
582
|
-
_$
|
|
703
|
+
var _el$21 = _$createElement2("text");
|
|
704
|
+
_$insert2(_el$21, () => row("TTFT", formatDuration(metrics().speed.ttft, true), contentWidth()));
|
|
705
|
+
_$effect2((_$p) => _$setProp2(_el$21, "fg", props.theme.textMuted, _$p));
|
|
583
706
|
return _el$21;
|
|
584
707
|
})(), (() => {
|
|
585
|
-
var _el$22 = _$
|
|
586
|
-
_$
|
|
587
|
-
_$
|
|
708
|
+
var _el$22 = _$createElement2("text");
|
|
709
|
+
_$insert2(_el$22, () => row("TPS", metrics().speed.active ? "\u2026 tok/s" : formatSpeed(metrics().speed.value), contentWidth()));
|
|
710
|
+
_$effect2((_$p) => _$setProp2(_el$22, "fg", props.theme.textMuted, _$p));
|
|
588
711
|
return _el$22;
|
|
589
712
|
})(), (() => {
|
|
590
|
-
var _el$23 = _$
|
|
591
|
-
_$
|
|
592
|
-
_$
|
|
713
|
+
var _el$23 = _$createElement2("text");
|
|
714
|
+
_$insert2(_el$23, () => row("Trend", sparkline(metrics().speed.trend), contentWidth()));
|
|
715
|
+
_$effect2((_$p) => _$setProp2(_el$23, "fg", props.theme.textMuted, _$p));
|
|
593
716
|
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
717
|
})()];
|
|
605
718
|
})(), null);
|
|
606
|
-
_$
|
|
719
|
+
_$insert2(_el$5, _$createComponent(SectionHeader, {
|
|
607
720
|
title: "Estimated Tokens",
|
|
608
721
|
get open() {
|
|
609
722
|
return tokensOpen();
|
|
@@ -616,41 +729,41 @@ function CacheView(props) {
|
|
|
616
729
|
},
|
|
617
730
|
toggle: () => setTokensOpen((value) => !value)
|
|
618
731
|
}), null);
|
|
619
|
-
_$
|
|
732
|
+
_$insert2(_el$5, (() => {
|
|
620
733
|
var _c$2 = _$memo(() => !!tokensOpen());
|
|
621
734
|
return () => _c$2() && [(() => {
|
|
622
|
-
var _el$
|
|
623
|
-
_$
|
|
624
|
-
_$
|
|
735
|
+
var _el$24 = _$createElement2("text");
|
|
736
|
+
_$insert2(_el$24, () => row("Prompt", `${formatCompact(metrics().tokens.system + metrics().tokens.user + metrics().tokens.agent)} tok`, contentWidth()));
|
|
737
|
+
_$effect2((_$p) => _$setProp2(_el$24, "fg", props.theme.textMuted, _$p));
|
|
738
|
+
return _el$24;
|
|
739
|
+
})(), (() => {
|
|
740
|
+
var _el$25 = _$createElement2("text");
|
|
741
|
+
_$insert2(_el$25, () => row("Tool Call", `${formatCompact(metrics().tokens.toolCall)} tok`, contentWidth()));
|
|
742
|
+
_$effect2((_$p) => _$setProp2(_el$25, "fg", props.theme.textMuted, _$p));
|
|
743
|
+
return _el$25;
|
|
744
|
+
})(), (() => {
|
|
745
|
+
var _el$26 = _$createElement2("text");
|
|
746
|
+
_$insert2(_el$26, () => row("Tool Result", `${formatCompact(metrics().tokens.toolResult)} tok`, contentWidth()));
|
|
747
|
+
_$effect2((_$p) => _$setProp2(_el$26, "fg", props.theme.textMuted, _$p));
|
|
625
748
|
return _el$26;
|
|
626
749
|
})(), (() => {
|
|
627
|
-
var _el$27 = _$
|
|
628
|
-
_$
|
|
629
|
-
_$
|
|
750
|
+
var _el$27 = _$createElement2("text");
|
|
751
|
+
_$insert2(_el$27, () => row("Agent Reasoning", `${formatCompact(metrics().tokens.reasoning)} tok`, contentWidth()));
|
|
752
|
+
_$effect2((_$p) => _$setProp2(_el$27, "fg", props.theme.textMuted, _$p));
|
|
630
753
|
return _el$27;
|
|
631
754
|
})(), (() => {
|
|
632
|
-
var _el$28 = _$
|
|
633
|
-
_$
|
|
634
|
-
_$
|
|
755
|
+
var _el$28 = _$createElement2("text");
|
|
756
|
+
_$insert2(_el$28, () => row("Agent Output", `${formatCompact(metrics().tokens.output)} tok`, contentWidth()));
|
|
757
|
+
_$effect2((_$p) => _$setProp2(_el$28, "fg", props.theme.textMuted, _$p));
|
|
635
758
|
return _el$28;
|
|
636
759
|
})(), (() => {
|
|
637
|
-
var _el$29 = _$
|
|
638
|
-
_$
|
|
639
|
-
_$
|
|
760
|
+
var _el$29 = _$createElement2("text");
|
|
761
|
+
_$insert2(_el$29, () => row("Total", `${formatCompact(metrics().tokens.total)} tok`, contentWidth()));
|
|
762
|
+
_$effect2((_$p) => _$setProp2(_el$29, "fg", props.theme.text, _$p));
|
|
640
763
|
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
764
|
})()];
|
|
652
765
|
})(), null);
|
|
653
|
-
_$
|
|
766
|
+
_$insert2(_el$5, (() => {
|
|
654
767
|
var _c$3 = _$memo(() => metrics().skills.length > 0);
|
|
655
768
|
return () => _c$3() && [_$createComponent(SectionHeader, {
|
|
656
769
|
get title() {
|
|
@@ -667,13 +780,13 @@ function CacheView(props) {
|
|
|
667
780
|
},
|
|
668
781
|
toggle: () => setSkillsOpen((value) => !value)
|
|
669
782
|
}), _$memo(() => _$memo(() => !!skillsOpen())() && metrics().skills.map((skill) => (() => {
|
|
670
|
-
var _el$
|
|
671
|
-
_$
|
|
672
|
-
_$
|
|
673
|
-
return _el$
|
|
783
|
+
var _el$30 = _$createElement2("text");
|
|
784
|
+
_$insert2(_el$30, () => skillRow(skill, contentWidth()));
|
|
785
|
+
_$effect2((_$p) => _$setProp2(_el$30, "fg", props.theme.textMuted, _$p));
|
|
786
|
+
return _el$30;
|
|
674
787
|
})()))];
|
|
675
788
|
})(), null);
|
|
676
|
-
_$
|
|
789
|
+
_$effect2((_p$) => {
|
|
677
790
|
var _v$4 = {
|
|
678
791
|
fg: props.theme.text
|
|
679
792
|
}, _v$5 = {
|
|
@@ -685,14 +798,14 @@ function CacheView(props) {
|
|
|
685
798
|
}, _v$8 = {
|
|
686
799
|
fg: trendColor()
|
|
687
800
|
}, _v$9 = props.theme.textMuted, _v$0 = props.theme.textMuted, _v$1 = props.theme.textMuted;
|
|
688
|
-
_v$4 !== _p$.e && (_p$.e = _$
|
|
689
|
-
_v$5 !== _p$.t && (_p$.t = _$
|
|
690
|
-
_v$6 !== _p$.a && (_p$.a = _$
|
|
691
|
-
_v$7 !== _p$.o && (_p$.o = _$
|
|
692
|
-
_v$8 !== _p$.i && (_p$.i = _$
|
|
693
|
-
_v$9 !== _p$.n && (_p$.n = _$
|
|
694
|
-
_v$0 !== _p$.s && (_p$.s = _$
|
|
695
|
-
_v$1 !== _p$.h && (_p$.h = _$
|
|
801
|
+
_v$4 !== _p$.e && (_p$.e = _$setProp2(_el$7, "style", _v$4, _p$.e));
|
|
802
|
+
_v$5 !== _p$.t && (_p$.t = _$setProp2(_el$1, "style", _v$5, _p$.t));
|
|
803
|
+
_v$6 !== _p$.a && (_p$.a = _$setProp2(_el$11, "style", _v$6, _p$.a));
|
|
804
|
+
_v$7 !== _p$.o && (_p$.o = _$setProp2(_el$14, "style", _v$7, _p$.o));
|
|
805
|
+
_v$8 !== _p$.i && (_p$.i = _$setProp2(_el$16, "style", _v$8, _p$.i));
|
|
806
|
+
_v$9 !== _p$.n && (_p$.n = _$setProp2(_el$18, "fg", _v$9, _p$.n));
|
|
807
|
+
_v$0 !== _p$.s && (_p$.s = _$setProp2(_el$19, "fg", _v$0, _p$.s));
|
|
808
|
+
_v$1 !== _p$.h && (_p$.h = _$setProp2(_el$20, "fg", _v$1, _p$.h));
|
|
696
809
|
return _p$;
|
|
697
810
|
}, {
|
|
698
811
|
e: undefined,
|
|
@@ -710,6 +823,8 @@ function CacheView(props) {
|
|
|
710
823
|
|
|
711
824
|
// src/index.tsx
|
|
712
825
|
var tui = async (api) => {
|
|
826
|
+
const speed = createSpeedTracker(api);
|
|
827
|
+
api.lifecycle.onDispose(speed.dispose);
|
|
713
828
|
api.slots.register({
|
|
714
829
|
order: 56,
|
|
715
830
|
slots: {
|
|
@@ -719,6 +834,19 @@ var tui = async (api) => {
|
|
|
719
834
|
get sessionID() {
|
|
720
835
|
return props.session_id ?? "";
|
|
721
836
|
},
|
|
837
|
+
speed,
|
|
838
|
+
get theme() {
|
|
839
|
+
return ctx.theme.current;
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
},
|
|
843
|
+
session_prompt_right(ctx, props) {
|
|
844
|
+
return _$createComponent2(CacheStatus, {
|
|
845
|
+
api,
|
|
846
|
+
get sessionID() {
|
|
847
|
+
return props.session_id;
|
|
848
|
+
},
|
|
849
|
+
speed,
|
|
722
850
|
get theme() {
|
|
723
851
|
return ctx.theme.current;
|
|
724
852
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@mtayfur/opencode-cache-view",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"description": "Minimal OpenCode TUI sidebar for cache hit, estimated tokens, and generation speed.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|