@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.
Files changed (3) hide show
  1. package/README.md +7 -6
  2. package/dist/index.js +300 -172
  3. 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
- Last ~37 tok/s
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:** `TTFT` estimates the time from assistant creation to the first reasoning or text block. `Now`, `Last`, and `Avg` use only visible reasoning and text content and the active duration of those blocks, excluding tool execution time. `Avg` covers the latest eight completed responses.
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 TTFT and generation speed
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-view.tsx
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 firstTokenTime(parts, message) {
267
- let first = Number.POSITIVE_INFINITY;
268
- for (const part of partsFor(parts, message.id)) {
269
- if (part.type !== "text" && part.type !== "reasoning")
270
- continue;
271
- const start = part.time?.start;
272
- if (typeof start === "number" && start < first)
273
- first = start;
274
- }
275
- return Number.isFinite(first) ? first : undefined;
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
- const first = firstTokenTime(parts, message);
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.type !== "text" && part.type !== "reasoning")
446
+ if (!isObservablePart(part))
309
447
  continue;
310
448
  const start = part.time?.start;
311
- if (part.time?.end !== undefined || typeof start !== "number" || now <= start)
312
- continue;
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 (tokens === 0 || duration < 500)
452
+ if (!Number.isFinite(first) || first < message.time.created)
317
453
  return;
318
- return tokens / duration * 1000;
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, now, estimator) {
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: { trend: [] }
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: speedMetrics(assistants, parts, estimator, now)
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$ = _$createElement("text"), _el$2 = _$createElement("span"), _el$3 = _$createElement("b"), _el$4 = _$createElement("span");
419
- _$insertNode(_el$, _el$2);
420
- _$insertNode(_el$, _el$4);
421
- _$insertNode(_el$2, _el$3);
422
- _$insert(_el$3, prefix);
423
- _$insert(_el$4, () => "\u2500".repeat(Math.max(1, props.width - prefix.length)));
424
- _$effect((_p$) => {
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 = _$setProp(_el$, "onMouseUp", _v$, _p$.e));
431
- _v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "style", _v$2, _p$.t));
432
- _v$3 !== _p$.a && (_p$.a = _$setProp(_el$4, "style", _v$3, _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] = createSignal(0);
445
- const [tokensOpen, setTokensOpen] = createSignal(true);
446
- const [skillsOpen, setSkillsOpen] = createSignal(true);
447
- const [speedOpen, setSpeedOpen] = createSignal(true);
448
- const [contentWidth, setContentWidth] = createSignal(DEFAULT_CONTENT_WIDTH);
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 = createMemo(() => {
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, Date.now(), estimator);
580
+ return readMetrics(props.api, props.sessionID, estimator, props.speed.get(props.sessionID));
458
581
  });
459
- const hitColor = createMemo(() => {
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 = createMemo(() => {
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 = createMemo(() => {
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
- onMount(() => {
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
- onCleanup(() => {
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 = _$createElement("box"), _el$6 = _$createElement("text"), _el$7 = _$createElement("span"), _el$8 = _$createElement("b"), _el$0 = _$createElement("text"), _el$1 = _$createElement("span"), _el$11 = _$createElement("span"), _el$12 = _$createTextNode(`[`), _el$13 = _$createTextNode(`]`), _el$14 = _$createElement("span"), _el$15 = _$createTextNode(` `), _el$16 = _$createElement("span"), _el$17 = _$createTextNode(` `), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$20 = _$createElement("text");
531
- _$insertNode(_el$5, _el$6);
532
- _$insertNode(_el$5, _el$0);
533
- _$insertNode(_el$5, _el$18);
534
- _$insertNode(_el$5, _el$19);
535
- _$insertNode(_el$5, _el$20);
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
- _$setProp(_el$5, "flexDirection", "column");
540
- _$setProp(_el$5, "onSizeChange", () => {
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
- _$insertNode(_el$6, _el$7);
547
- _$insertNode(_el$7, _el$8);
548
- _$insertNode(_el$8, _$createTextNode(`Cache View`));
549
- _$insertNode(_el$0, _el$1);
550
- _$insertNode(_el$0, _el$11);
551
- _$insertNode(_el$0, _el$14);
552
- _$insertNode(_el$0, _el$16);
553
- _$insertNode(_el$1, _$createTextNode(`Hit `));
554
- _$insertNode(_el$11, _el$12);
555
- _$insertNode(_el$11, _el$13);
556
- _$insert(_el$11, () => progressBar(metrics().cache.latestHit, hitBarWidth()), _el$13);
557
- _$insertNode(_el$14, _el$15);
558
- _$insert(_el$14, () => formatPercent(metrics().cache.latestHit), null);
559
- _$insertNode(_el$16, _el$17);
560
- _$insert(_el$16, () => formatHitTrend(metrics().cache.trend), null);
561
- _$insert(_el$18, () => row("Session Hit", formatPercent(metrics().cache.totalHit), contentWidth()));
562
- _$insert(_el$19, () => row("Read", `${formatCompact(metrics().cache.read)} tok`, contentWidth()));
563
- _$insert(_el$20, () => row("Miss", `${formatCompact(metrics().cache.miss)} tok`, contentWidth()));
564
- _$insert(_el$5, _$createComponent(SectionHeader, {
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
- _$insert(_el$5, (() => {
700
+ _$insert2(_el$5, (() => {
578
701
  var _c$ = _$memo(() => !!speedOpen());
579
702
  return () => _c$() && [(() => {
580
- var _el$21 = _$createElement("text");
581
- _$insert(_el$21, () => row("Now", formatSpeed(metrics().speed.now, true), contentWidth()));
582
- _$effect((_$p) => _$setProp(_el$21, "fg", props.theme.textMuted, _$p));
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 = _$createElement("text");
586
- _$insert(_el$22, () => row("TTFT", formatDuration(metrics().speed.ttft, true), contentWidth()));
587
- _$effect((_$p) => _$setProp(_el$22, "fg", props.theme.textMuted, _$p));
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 = _$createElement("text");
591
- _$insert(_el$23, () => row("Last", formatSpeed(metrics().speed.last, true), contentWidth()));
592
- _$effect((_$p) => _$setProp(_el$23, "fg", props.theme.textMuted, _$p));
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
- _$insert(_el$5, _$createComponent(SectionHeader, {
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
- _$insert(_el$5, (() => {
732
+ _$insert2(_el$5, (() => {
620
733
  var _c$2 = _$memo(() => !!tokensOpen());
621
734
  return () => _c$2() && [(() => {
622
- var _el$26 = _$createElement("text");
623
- _$insert(_el$26, () => row("Prompt", `${formatCompact(metrics().tokens.system + metrics().tokens.user + metrics().tokens.agent)} tok`, contentWidth()));
624
- _$effect((_$p) => _$setProp(_el$26, "fg", props.theme.textMuted, _$p));
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 = _$createElement("text");
628
- _$insert(_el$27, () => row("Tool Call", `${formatCompact(metrics().tokens.toolCall)} tok`, contentWidth()));
629
- _$effect((_$p) => _$setProp(_el$27, "fg", props.theme.textMuted, _$p));
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 = _$createElement("text");
633
- _$insert(_el$28, () => row("Tool Result", `${formatCompact(metrics().tokens.toolResult)} tok`, contentWidth()));
634
- _$effect((_$p) => _$setProp(_el$28, "fg", props.theme.textMuted, _$p));
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 = _$createElement("text");
638
- _$insert(_el$29, () => row("Agent Reasoning", `${formatCompact(metrics().tokens.reasoning)} tok`, contentWidth()));
639
- _$effect((_$p) => _$setProp(_el$29, "fg", props.theme.textMuted, _$p));
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
- _$insert(_el$5, (() => {
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$32 = _$createElement("text");
671
- _$insert(_el$32, () => skillRow(skill, contentWidth()));
672
- _$effect((_$p) => _$setProp(_el$32, "fg", props.theme.textMuted, _$p));
673
- return _el$32;
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
- _$effect((_p$) => {
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 = _$setProp(_el$7, "style", _v$4, _p$.e));
689
- _v$5 !== _p$.t && (_p$.t = _$setProp(_el$1, "style", _v$5, _p$.t));
690
- _v$6 !== _p$.a && (_p$.a = _$setProp(_el$11, "style", _v$6, _p$.a));
691
- _v$7 !== _p$.o && (_p$.o = _$setProp(_el$14, "style", _v$7, _p$.o));
692
- _v$8 !== _p$.i && (_p$.i = _$setProp(_el$16, "style", _v$8, _p$.i));
693
- _v$9 !== _p$.n && (_p$.n = _$setProp(_el$18, "fg", _v$9, _p$.n));
694
- _v$0 !== _p$.s && (_p$.s = _$setProp(_el$19, "fg", _v$0, _p$.s));
695
- _v$1 !== _p$.h && (_p$.h = _$setProp(_el$20, "fg", _v$1, _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.1",
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",