@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +119 -48
  3. 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` 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.
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
- return speed.active ? "\u2026 tok/s" : formatSpeed(speed.value);
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 speed = tokens / duration * 1000;
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 active = false;
417
- for (const start of starts.values()) {
418
- if (start.sessionID === sessionID) {
419
- active = true;
420
- break;
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
- return { active, value: latest.get(sessionID), trend: trends.get(sessionID) ?? [] };
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.3",
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",