@mtayfur/opencode-cache-view 0.0.3 → 1.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 +6 -4
- package/dist/index.js +113 -47
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ customize-opencode 3.1K tok
|
|
|
28
28
|
|
|
29
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.
|
|
30
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.
|
|
31
|
-
- **Speed:** `TPS`
|
|
31
|
+
- **Speed:** During streaming, `TPS` shows a `~`-prefixed estimate from visible text and reasoning deltas over the latest five seconds. Hidden reasoning with no deltas uses the previous completed turn or remains `… tok/s` when none exists. Completed tool-call steps and tool execution time are excluded. The rate remains `~`-prefixed while the turn is incomplete; the final non-tool-call assistant step replaces it with exact `(output + reasoning) / duration` throughput. `TTFT` retains the estimated delay to the first visible reasoning or text block, and `Trend` shows the latest eight completed-turn TPS values.
|
|
32
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.
|
|
33
33
|
|
|
34
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.
|
|
@@ -88,9 +88,11 @@ Metric calculations are isolated from the TUI rendering layer. `read.ts` snapsho
|
|
|
88
88
|
|
|
89
89
|
## Development
|
|
90
90
|
|
|
91
|
+
From the repository root:
|
|
92
|
+
|
|
91
93
|
```sh
|
|
92
94
|
bun install --frozen-lockfile
|
|
93
|
-
bun run typecheck
|
|
94
|
-
bun run build
|
|
95
|
-
npm pack --dry-run
|
|
95
|
+
bun run --filter @mtayfur/opencode-cache-view typecheck
|
|
96
|
+
bun run --filter @mtayfur/opencode-cache-view build
|
|
97
|
+
npm pack ./packages/cache-view --dry-run
|
|
96
98
|
```
|
package/dist/index.js
CHANGED
|
@@ -177,7 +177,9 @@ function CacheStatus(props) {
|
|
|
177
177
|
});
|
|
178
178
|
const speedValue = createMemo(() => {
|
|
179
179
|
const speed = metrics().speed;
|
|
180
|
-
|
|
180
|
+
if (speed.active && speed.value === undefined)
|
|
181
|
+
return "\u2026 tok/s";
|
|
182
|
+
return formatSpeed(speed.value, speed.estimated);
|
|
181
183
|
});
|
|
182
184
|
onMount(() => {
|
|
183
185
|
let refreshTimer;
|
|
@@ -363,11 +365,69 @@ function loadedSkills(messages, parts, activeStart, estimator) {
|
|
|
363
365
|
return [...skills.values()];
|
|
364
366
|
}
|
|
365
367
|
|
|
368
|
+
// src/token-estimator.ts
|
|
369
|
+
function estimateTokenUnits(text) {
|
|
370
|
+
let units = 0;
|
|
371
|
+
for (const char of text) {
|
|
372
|
+
const code = char.codePointAt(0) ?? 0;
|
|
373
|
+
const isWide = code >= 11904 && code <= 40959 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255;
|
|
374
|
+
units += isWide ? 4 : 1;
|
|
375
|
+
}
|
|
376
|
+
return units;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
class TokenEstimator {
|
|
380
|
+
cache = new Map;
|
|
381
|
+
count(key, text) {
|
|
382
|
+
const cached = this.cache.get(key);
|
|
383
|
+
if (cached?.text === text)
|
|
384
|
+
return cached.tokens;
|
|
385
|
+
const units = estimateTokenUnits(text);
|
|
386
|
+
const tokens = Math.ceil(units / 4);
|
|
387
|
+
this.cache.set(key, { text, units, tokens });
|
|
388
|
+
return tokens;
|
|
389
|
+
}
|
|
390
|
+
append(key, delta) {
|
|
391
|
+
const cached = this.cache.get(key);
|
|
392
|
+
if (!cached)
|
|
393
|
+
return;
|
|
394
|
+
const units = cached.units + estimateTokenUnits(delta);
|
|
395
|
+
this.cache.set(key, {
|
|
396
|
+
text: cached.text + delta,
|
|
397
|
+
units,
|
|
398
|
+
tokens: Math.ceil(units / 4)
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
clear() {
|
|
402
|
+
this.cache.clear();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
366
406
|
// src/metrics/speed.ts
|
|
407
|
+
var STREAM_WINDOW = 5000;
|
|
408
|
+
var MIN_STREAM_DURATION = 1000;
|
|
367
409
|
function createSpeedTracker(api) {
|
|
368
410
|
const starts = new Map;
|
|
369
411
|
const latest = new Map;
|
|
370
412
|
const trends = new Map;
|
|
413
|
+
const pendingToolTurns = new Set;
|
|
414
|
+
const streamParts = new Map;
|
|
415
|
+
const streamSamples = new Map;
|
|
416
|
+
const clearStream = (messageID) => {
|
|
417
|
+
streamSamples.delete(messageID);
|
|
418
|
+
for (const [partID, stream] of streamParts) {
|
|
419
|
+
if (stream.messageID === messageID)
|
|
420
|
+
streamParts.delete(partID);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const streamingSpeed = (messageID, now) => {
|
|
424
|
+
const samples = (streamSamples.get(messageID) ?? []).filter((sample) => sample.time >= now - STREAM_WINDOW);
|
|
425
|
+
if (samples.length === 0)
|
|
426
|
+
return;
|
|
427
|
+
const tokens = samples.reduce((sum, sample) => sum + sample.tokens, 0);
|
|
428
|
+
const duration = Math.max(MIN_STREAM_DURATION, samples.at(-1).time - samples[0].time);
|
|
429
|
+
return tokens / duration * 1000;
|
|
430
|
+
};
|
|
371
431
|
const part = api.event.on("message.part.updated", (event) => {
|
|
372
432
|
const value = event.properties.part;
|
|
373
433
|
if (value.type === "step-start") {
|
|
@@ -385,51 +445,95 @@ function createSpeedTracker(api) {
|
|
|
385
445
|
starts.delete(value.messageID);
|
|
386
446
|
if (!start || start.sessionID !== event.properties.sessionID)
|
|
387
447
|
return;
|
|
448
|
+
clearStream(value.messageID);
|
|
388
449
|
const duration = event.properties.time - start.time;
|
|
389
450
|
const tokens = value.tokens.output + value.tokens.reasoning;
|
|
451
|
+
if (value.reason === "tool-calls") {
|
|
452
|
+
pendingToolTurns.add(start.sessionID);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
pendingToolTurns.delete(start.sessionID);
|
|
390
456
|
if (duration <= 0 || tokens <= 0)
|
|
391
457
|
return;
|
|
392
458
|
const speed = tokens / duration * 1000;
|
|
393
459
|
latest.set(start.sessionID, speed);
|
|
394
460
|
trends.set(start.sessionID, [...trends.get(start.sessionID) ?? [], speed].slice(-8));
|
|
395
461
|
});
|
|
462
|
+
const delta = api.event.on("message.part.delta", (event) => {
|
|
463
|
+
if (event.properties.field !== "text" || !starts.has(event.properties.messageID))
|
|
464
|
+
return;
|
|
465
|
+
const now = Date.now();
|
|
466
|
+
const previous = streamParts.get(event.properties.partID) ?? {
|
|
467
|
+
messageID: event.properties.messageID,
|
|
468
|
+
units: 0
|
|
469
|
+
};
|
|
470
|
+
const previousTokens = Math.ceil(previous.units / 4);
|
|
471
|
+
const units = previous.units + estimateTokenUnits(event.properties.delta);
|
|
472
|
+
const tokens = Math.ceil(units / 4) - previousTokens;
|
|
473
|
+
streamParts.set(event.properties.partID, { messageID: event.properties.messageID, units });
|
|
474
|
+
if (tokens <= 0)
|
|
475
|
+
return;
|
|
476
|
+
const samples = streamSamples.get(event.properties.messageID) ?? [];
|
|
477
|
+
streamSamples.set(event.properties.messageID, [...samples, { time: now, tokens }].filter((sample) => sample.time >= now - STREAM_WINDOW));
|
|
478
|
+
});
|
|
396
479
|
const message = api.event.on("message.updated", (event) => {
|
|
397
480
|
if (event.properties.info.role !== "assistant")
|
|
398
481
|
return;
|
|
399
482
|
if (event.properties.info.time.completed === undefined && event.properties.info.error === undefined)
|
|
400
483
|
return;
|
|
401
484
|
starts.delete(event.properties.info.id);
|
|
485
|
+
clearStream(event.properties.info.id);
|
|
486
|
+
if (event.properties.info.error !== undefined)
|
|
487
|
+
pendingToolTurns.delete(event.properties.sessionID);
|
|
402
488
|
});
|
|
403
489
|
const messageRemoved = api.event.on("message.removed", (event) => {
|
|
404
490
|
starts.delete(event.properties.messageID);
|
|
491
|
+
clearStream(event.properties.messageID);
|
|
405
492
|
});
|
|
406
493
|
const sessionDeleted = api.event.on("session.deleted", (event) => {
|
|
407
494
|
latest.delete(event.properties.sessionID);
|
|
408
495
|
trends.delete(event.properties.sessionID);
|
|
496
|
+
pendingToolTurns.delete(event.properties.sessionID);
|
|
409
497
|
for (const [messageID, start] of starts) {
|
|
410
|
-
if (start.sessionID === event.properties.sessionID)
|
|
498
|
+
if (start.sessionID === event.properties.sessionID) {
|
|
411
499
|
starts.delete(messageID);
|
|
500
|
+
clearStream(messageID);
|
|
501
|
+
}
|
|
412
502
|
}
|
|
413
503
|
});
|
|
414
504
|
return {
|
|
415
505
|
get(sessionID) {
|
|
416
|
-
let
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
506
|
+
let activeMessageID;
|
|
507
|
+
let activeTime = Number.NEGATIVE_INFINITY;
|
|
508
|
+
for (const [messageID, start] of starts) {
|
|
509
|
+
if (start.sessionID === sessionID && start.time > activeTime) {
|
|
510
|
+
activeMessageID = messageID;
|
|
511
|
+
activeTime = start.time;
|
|
421
512
|
}
|
|
422
513
|
}
|
|
423
|
-
|
|
514
|
+
const active = activeMessageID !== undefined;
|
|
515
|
+
const live = activeMessageID === undefined ? undefined : streamingSpeed(activeMessageID, Date.now());
|
|
516
|
+
const fallback = latest.get(sessionID);
|
|
517
|
+
const provisional = active || pendingToolTurns.has(sessionID);
|
|
518
|
+
return {
|
|
519
|
+
active,
|
|
520
|
+
value: active ? live ?? fallback : fallback,
|
|
521
|
+
estimated: provisional && (live !== undefined || fallback !== undefined),
|
|
522
|
+
trend: trends.get(sessionID) ?? []
|
|
523
|
+
};
|
|
424
524
|
},
|
|
425
525
|
dispose() {
|
|
426
526
|
part();
|
|
527
|
+
delta();
|
|
427
528
|
message();
|
|
428
529
|
messageRemoved();
|
|
429
530
|
sessionDeleted();
|
|
430
531
|
starts.clear();
|
|
431
532
|
latest.clear();
|
|
432
533
|
trends.clear();
|
|
534
|
+
pendingToolTurns.clear();
|
|
535
|
+
streamParts.clear();
|
|
536
|
+
streamSamples.clear();
|
|
433
537
|
}
|
|
434
538
|
};
|
|
435
539
|
}
|
|
@@ -494,44 +598,6 @@ function readMetrics(api, sessionID, estimator, speed) {
|
|
|
494
598
|
};
|
|
495
599
|
}
|
|
496
600
|
|
|
497
|
-
// src/token-estimator.ts
|
|
498
|
-
function estimateTokenUnits(text) {
|
|
499
|
-
let units = 0;
|
|
500
|
-
for (const char of text) {
|
|
501
|
-
const code = char.codePointAt(0) ?? 0;
|
|
502
|
-
const isWide = code >= 11904 && code <= 40959 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255;
|
|
503
|
-
units += isWide ? 4 : 1;
|
|
504
|
-
}
|
|
505
|
-
return units;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
class TokenEstimator {
|
|
509
|
-
cache = new Map;
|
|
510
|
-
count(key, text) {
|
|
511
|
-
const cached = this.cache.get(key);
|
|
512
|
-
if (cached?.text === text)
|
|
513
|
-
return cached.tokens;
|
|
514
|
-
const units = estimateTokenUnits(text);
|
|
515
|
-
const tokens = Math.ceil(units / 4);
|
|
516
|
-
this.cache.set(key, { text, units, tokens });
|
|
517
|
-
return tokens;
|
|
518
|
-
}
|
|
519
|
-
append(key, delta) {
|
|
520
|
-
const cached = this.cache.get(key);
|
|
521
|
-
if (!cached)
|
|
522
|
-
return;
|
|
523
|
-
const units = cached.units + estimateTokenUnits(delta);
|
|
524
|
-
this.cache.set(key, {
|
|
525
|
-
text: cached.text + delta,
|
|
526
|
-
units,
|
|
527
|
-
tokens: Math.ceil(units / 4)
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
clear() {
|
|
531
|
-
this.cache.clear();
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
601
|
// src/cache-view.tsx
|
|
536
602
|
var DEFAULT_CONTENT_WIDTH = 29;
|
|
537
603
|
var MIN_CONTENT_WIDTH = 20;
|
|
@@ -706,7 +772,7 @@ function CacheView(props) {
|
|
|
706
772
|
return _el$21;
|
|
707
773
|
})(), (() => {
|
|
708
774
|
var _el$22 = _$createElement2("text");
|
|
709
|
-
_$insert2(_el$22, () => row("TPS", metrics().speed.active ? "\u2026 tok/s" : formatSpeed(metrics().speed.value), contentWidth()));
|
|
775
|
+
_$insert2(_el$22, () => row("TPS", metrics().speed.active && metrics().speed.value === undefined ? "\u2026 tok/s" : formatSpeed(metrics().speed.value, metrics().speed.estimated), contentWidth()));
|
|
710
776
|
_$effect2((_$p) => _$setProp2(_el$22, "fg", props.theme.textMuted, _$p));
|
|
711
777
|
return _el$22;
|
|
712
778
|
})(), (() => {
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@mtayfur/opencode-cache-view",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"description": "Minimal OpenCode TUI sidebar for cache hit, estimated tokens, and generation speed.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/mtayfur/opencode-
|
|
8
|
+
"url": "git+https://github.com/mtayfur/opencode-plugins.git",
|
|
9
|
+
"directory": "packages/cache-view"
|
|
9
10
|
},
|
|
10
11
|
"bugs": {
|
|
11
|
-
"url": "https://github.com/mtayfur/opencode-
|
|
12
|
+
"url": "https://github.com/mtayfur/opencode-plugins/issues"
|
|
12
13
|
},
|
|
13
|
-
"homepage": "https://github.com/mtayfur/opencode-cache-view#readme",
|
|
14
|
+
"homepage": "https://github.com/mtayfur/opencode-plugins/tree/main/packages/cache-view#readme",
|
|
14
15
|
"files": [
|
|
15
16
|
"dist"
|
|
16
17
|
],
|
|
@@ -38,8 +39,8 @@
|
|
|
38
39
|
"@opentui/solid": ">=0.2.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@opencode-ai/plugin": "
|
|
42
|
-
"@opencode-ai/sdk": "
|
|
42
|
+
"@opencode-ai/plugin": "1.18.16",
|
|
43
|
+
"@opencode-ai/sdk": "1.18.16",
|
|
43
44
|
"@opentui/core": "^0.5.1",
|
|
44
45
|
"@opentui/solid": "^0.5.1",
|
|
45
46
|
"jsonc-parser": "^3.3.1",
|