@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.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/dist/index.js +113 -47
  3. 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` 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 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
- 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 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 active = false;
417
- for (const start of starts.values()) {
418
- if (start.sessionID === sessionID) {
419
- active = true;
420
- break;
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
- return { active, value: latest.get(sessionID), trend: trends.get(sessionID) ?? [] };
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": "0.0.3",
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-cache-view.git"
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-cache-view/issues"
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": "^1.18.16",
42
- "@opencode-ai/sdk": "^1.18.16",
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",