@mtayfur/opencode-cache-view 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +119 -48
- package/package.json +1 -1
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 current turn's cumulative rate when available, otherwise the previous completed turn, or remains `… tok/s` when neither exists. Completed model steps accumulate `(output + reasoning)` tokens and full step duration across tool calls, excluding tool execution time. The cumulative rate remains `~`-prefixed while the turn is incomplete; the final non-tool-call step replaces it with the exact weighted turn 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.
|
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 turnTotals = new Map;
|
|
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,100 @@ 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;
|
|
390
451
|
if (duration <= 0 || tokens <= 0)
|
|
391
452
|
return;
|
|
392
|
-
const
|
|
453
|
+
const previous = turnTotals.get(start.sessionID) ?? { tokens: 0, duration: 0 };
|
|
454
|
+
const turn = {
|
|
455
|
+
tokens: previous.tokens + tokens,
|
|
456
|
+
duration: previous.duration + duration
|
|
457
|
+
};
|
|
458
|
+
const speed = turn.tokens / turn.duration * 1000;
|
|
393
459
|
latest.set(start.sessionID, speed);
|
|
460
|
+
if (value.reason === "tool-calls") {
|
|
461
|
+
turnTotals.set(start.sessionID, turn);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
turnTotals.delete(start.sessionID);
|
|
394
465
|
trends.set(start.sessionID, [...trends.get(start.sessionID) ?? [], speed].slice(-8));
|
|
395
466
|
});
|
|
467
|
+
const delta = api.event.on("message.part.delta", (event) => {
|
|
468
|
+
if (event.properties.field !== "text" || !starts.has(event.properties.messageID))
|
|
469
|
+
return;
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
const previous = streamParts.get(event.properties.partID) ?? {
|
|
472
|
+
messageID: event.properties.messageID,
|
|
473
|
+
units: 0
|
|
474
|
+
};
|
|
475
|
+
const previousTokens = Math.ceil(previous.units / 4);
|
|
476
|
+
const units = previous.units + estimateTokenUnits(event.properties.delta);
|
|
477
|
+
const tokens = Math.ceil(units / 4) - previousTokens;
|
|
478
|
+
streamParts.set(event.properties.partID, { messageID: event.properties.messageID, units });
|
|
479
|
+
if (tokens <= 0)
|
|
480
|
+
return;
|
|
481
|
+
const samples = streamSamples.get(event.properties.messageID) ?? [];
|
|
482
|
+
streamSamples.set(event.properties.messageID, [...samples, { time: now, tokens }].filter((sample) => sample.time >= now - STREAM_WINDOW));
|
|
483
|
+
});
|
|
396
484
|
const message = api.event.on("message.updated", (event) => {
|
|
397
485
|
if (event.properties.info.role !== "assistant")
|
|
398
486
|
return;
|
|
399
487
|
if (event.properties.info.time.completed === undefined && event.properties.info.error === undefined)
|
|
400
488
|
return;
|
|
401
489
|
starts.delete(event.properties.info.id);
|
|
490
|
+
clearStream(event.properties.info.id);
|
|
491
|
+
if (event.properties.info.error !== undefined)
|
|
492
|
+
turnTotals.delete(event.properties.sessionID);
|
|
402
493
|
});
|
|
403
494
|
const messageRemoved = api.event.on("message.removed", (event) => {
|
|
404
495
|
starts.delete(event.properties.messageID);
|
|
496
|
+
clearStream(event.properties.messageID);
|
|
405
497
|
});
|
|
406
498
|
const sessionDeleted = api.event.on("session.deleted", (event) => {
|
|
407
499
|
latest.delete(event.properties.sessionID);
|
|
408
500
|
trends.delete(event.properties.sessionID);
|
|
501
|
+
turnTotals.delete(event.properties.sessionID);
|
|
409
502
|
for (const [messageID, start] of starts) {
|
|
410
|
-
if (start.sessionID === event.properties.sessionID)
|
|
503
|
+
if (start.sessionID === event.properties.sessionID) {
|
|
411
504
|
starts.delete(messageID);
|
|
505
|
+
clearStream(messageID);
|
|
506
|
+
}
|
|
412
507
|
}
|
|
413
508
|
});
|
|
414
509
|
return {
|
|
415
510
|
get(sessionID) {
|
|
416
|
-
let
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
511
|
+
let activeMessageID;
|
|
512
|
+
let activeTime = Number.NEGATIVE_INFINITY;
|
|
513
|
+
for (const [messageID, start] of starts) {
|
|
514
|
+
if (start.sessionID === sessionID && start.time > activeTime) {
|
|
515
|
+
activeMessageID = messageID;
|
|
516
|
+
activeTime = start.time;
|
|
421
517
|
}
|
|
422
518
|
}
|
|
423
|
-
|
|
519
|
+
const active = activeMessageID !== undefined;
|
|
520
|
+
const live = activeMessageID === undefined ? undefined : streamingSpeed(activeMessageID, Date.now());
|
|
521
|
+
const fallback = latest.get(sessionID);
|
|
522
|
+
const provisional = active || turnTotals.has(sessionID);
|
|
523
|
+
return {
|
|
524
|
+
active,
|
|
525
|
+
value: active ? live ?? fallback : fallback,
|
|
526
|
+
estimated: provisional && (live !== undefined || fallback !== undefined),
|
|
527
|
+
trend: trends.get(sessionID) ?? []
|
|
528
|
+
};
|
|
424
529
|
},
|
|
425
530
|
dispose() {
|
|
426
531
|
part();
|
|
532
|
+
delta();
|
|
427
533
|
message();
|
|
428
534
|
messageRemoved();
|
|
429
535
|
sessionDeleted();
|
|
430
536
|
starts.clear();
|
|
431
537
|
latest.clear();
|
|
432
538
|
trends.clear();
|
|
539
|
+
turnTotals.clear();
|
|
540
|
+
streamParts.clear();
|
|
541
|
+
streamSamples.clear();
|
|
433
542
|
}
|
|
434
543
|
};
|
|
435
544
|
}
|
|
@@ -494,44 +603,6 @@ function readMetrics(api, sessionID, estimator, speed) {
|
|
|
494
603
|
};
|
|
495
604
|
}
|
|
496
605
|
|
|
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
606
|
// src/cache-view.tsx
|
|
536
607
|
var DEFAULT_CONTENT_WIDTH = 29;
|
|
537
608
|
var MIN_CONTENT_WIDTH = 20;
|
|
@@ -706,7 +777,7 @@ function CacheView(props) {
|
|
|
706
777
|
return _el$21;
|
|
707
778
|
})(), (() => {
|
|
708
779
|
var _el$22 = _$createElement2("text");
|
|
709
|
-
_$insert2(_el$22, () => row("TPS", metrics().speed.active ? "\u2026 tok/s" : formatSpeed(metrics().speed.value), contentWidth()));
|
|
780
|
+
_$insert2(_el$22, () => row("TPS", metrics().speed.active && metrics().speed.value === undefined ? "\u2026 tok/s" : formatSpeed(metrics().speed.value, metrics().speed.estimated), contentWidth()));
|
|
710
781
|
_$effect2((_$p) => _$setProp2(_el$22, "fg", props.theme.textMuted, _$p));
|
|
711
782
|
return _el$22;
|
|
712
783
|
})(), (() => {
|
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.4",
|
|
5
5
|
"description": "Minimal OpenCode TUI sidebar for cache hit, estimated tokens, and generation speed.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|