opencode-visual-cache 1.2.7 → 1.2.8

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.
@@ -1 +1 @@
1
- export declare const PLUGIN_VERSION = "1.2.7";
1
+ export declare const PLUGIN_VERSION = "1.2.8";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION = "1.2.7";
2
+ export const PLUGIN_VERSION = "1.2.8";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/solid/jsx-runtime";
2
- import { createMemo, createSignal, onMount, onCleanup, Show } from "solid-js";
2
+ import { createMemo, createSignal, createEffect, onMount, onCleanup, Show } from "solid-js";
3
3
  import { PLUGIN_VERSION } from "./_version";
4
4
  // ── terminal-width helpers ────────────────────────────────────────
5
5
  // CJK characters occupy 2 terminal columns; padEnd/padStart count
@@ -267,17 +267,6 @@ function estimateTokens(text) {
267
267
  const asciiPerToken = jsonLike ? 2 : codeLike ? 2.5 : 4;
268
268
  return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1.5));
269
269
  }
270
- // ---------------------------------------------------------------------------
271
- // Sidebar component
272
- // ---------------------------------------------------------------------------
273
- /** Module-level config signals — shared between the TUI component and slash
274
- * commands so that runtime changes take effect immediately without restart. */
275
- const [currencySymbol, setCurrencySymbol] = createSignal("$");
276
- const [exchangeRate, setExchangeRate] = createSignal(1);
277
- const [sectionDetail, setSectionDetail] = createSignal(true);
278
- const [sectionModel, setSectionModel] = createSignal(true);
279
- const [sectionDist, setSectionDist] = createSignal(true);
280
- const [borderVisible, setBorderVisible] = createSignal(true);
281
270
  const CURRENCIES = {
282
271
  USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
283
272
  };
@@ -302,9 +291,21 @@ function TokenCachePanel(props) {
302
291
  const [modelOpen, setModelOpen] = createSignal(true);
303
292
  const [distOpen, setDistOpen] = createSignal(false);
304
293
  let boxEl;
294
+ // ── shared signals (de-structured so internal code is unchanged) ──
295
+ const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, borderVisible, setBorderVisible, } = props.signals;
305
296
  // ── scan session messages reactively ──
306
297
  // SolidJS createMemo re-evaluates whenever the underlying
307
298
  // api.state.session state changes — no event listener needed.
299
+ // ── distribution cache ────────────────────────────────────────
300
+ // When data() re-computes before api.state.part() is warm (e.g. after
301
+ // a view switch), hasDistData flips to false and the distribution
302
+ // block disappears. Keep the last valid snapshot so the UI stays
303
+ // stable until the next successful computation arrives.
304
+ const [lastDist, setLastDist] = createSignal({
305
+ system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0,
306
+ output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
307
+ });
308
+ const [lastHasDist, setLastHasDist] = createSignal(false);
308
309
  const data = createMemo(() => {
309
310
  const msgs = props.api.state.session.messages(props.sessionId);
310
311
  let input = 0;
@@ -486,6 +487,10 @@ function TokenCachePanel(props) {
486
487
  catch {
487
488
  // Graceful degradation — dist stays at zeroes
488
489
  }
490
+ // Fall back to last known-good distribution while api.state.part()
491
+ // is re-hydrating after a view switch.
492
+ const finalDist = hasDistData ? dist : lastDist();
493
+ const finalHasDist = hasDistData || lastHasDist();
489
494
  return {
490
495
  hitRate, read, write, freshInput: input, output,
491
496
  cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
@@ -493,9 +498,24 @@ function TokenCachePanel(props) {
493
498
  trend, hasTrendData,
494
499
  providerName,
495
500
  sessionHitRate,
496
- dist, hasDistData,
501
+ dist: finalDist,
502
+ hasDistData: finalHasDist,
497
503
  };
498
504
  });
505
+ // Persist the last valid distribution so that data() can fall back
506
+ // to it while api.state.part() is re-hydrating after a view switch.
507
+ createEffect(() => {
508
+ const d = data();
509
+ if (d.hasDistData) {
510
+ setLastDist({ ...d.dist });
511
+ setLastHasDist(true);
512
+ // Also persist across component remounts (view switches)
513
+ try {
514
+ props.api.kv.set(`${KV_PREFIX}.dist_snapshot`, { ...d.dist });
515
+ }
516
+ catch { }
517
+ }
518
+ });
499
519
  // ── token distribution (in-process via api.state.part) ──
500
520
  const [partVersion, setPartVersion] = createSignal(0);
501
521
  // Persist fold state to api.kv
@@ -507,7 +527,10 @@ function TokenCachePanel(props) {
507
527
  catch { }
508
528
  };
509
529
  onMount(() => {
510
- // Restore fold state from persisted storage
530
+ // Reset panelWidth on (re)mount so the layout uses a clean
531
+ // default until onSizeChange measures the live box dimensions.
532
+ setPanelWidth(DEFAULT_PANEL_WIDTH);
533
+ // Restore fold state from persisted storage (non-critical — fire and forget)
511
534
  try {
512
535
  setOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.open`, false)));
513
536
  setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)));
@@ -515,22 +538,51 @@ function TokenCachePanel(props) {
515
538
  setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)));
516
539
  }
517
540
  catch { }
518
- // Restore user config (currency, rate, section visibility)
519
- try {
520
- const sym = props.api.kv.get(`${KV_PREFIX}.currency`);
521
- const rate = props.api.kv.get(`${KV_PREFIX}.rate`);
522
- if (typeof sym === "string")
523
- setCurrencySymbol(sym);
524
- if (typeof rate === "number" && rate > 0)
525
- setExchangeRate(rate);
526
- setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
527
- setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
528
- setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
529
- const borderVal = props.api.kv.get(`${KV_PREFIX}.border`, true);
530
- setBorderVisible(borderVal !== false);
541
+ // Restore user config (currency, rate, section visibility).
542
+ // Try synchronously first (kv is usually ready on mount), fall back to
543
+ // polling if the module was reloaded and kv hasn't initialised yet.
544
+ const doRestore = () => {
545
+ try {
546
+ const sym = props.api.kv.get(`${KV_PREFIX}.currency`);
547
+ const rate = props.api.kv.get(`${KV_PREFIX}.rate`);
548
+ if (typeof sym === "string")
549
+ setCurrencySymbol(sym);
550
+ if (typeof rate === "number" && rate > 0)
551
+ setExchangeRate(rate);
552
+ setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
553
+ setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
554
+ setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
555
+ const bv = props.api.kv.get(`${KV_PREFIX}.border`, true);
556
+ setBorderVisible(bv !== false);
557
+ // Restore distribution snapshot so the token distribution block
558
+ // doesn't blank out while api.state.part() re-hydrates.
559
+ const cachedDist = props.api.kv.get(`${KV_PREFIX}.dist_snapshot`);
560
+ if (cachedDist) {
561
+ setLastDist(cachedDist);
562
+ setLastHasDist(true);
563
+ }
564
+ }
565
+ catch {
566
+ // kv read failed — signals stay at defaults
567
+ }
568
+ // Re-measure panel width after config signals have settled
569
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
570
+ setPanelWidth(Math.max(MIN_PANEL_WIDTH, boxEl.width));
571
+ }
572
+ };
573
+ if (props.api.kv.ready) {
574
+ doRestore();
575
+ }
576
+ else {
577
+ const pollRestore = () => {
578
+ if (!props.api.kv.ready) {
579
+ setTimeout(pollRestore, 10);
580
+ return;
581
+ }
582
+ doRestore();
583
+ };
584
+ pollRestore();
531
585
  }
532
- catch { }
533
- // (border visibility was restored above together with section config)
534
586
  const unsubPart = props.api.event.on("message.part.updated", () => {
535
587
  setPartVersion((v) => v + 1);
536
588
  });
@@ -576,6 +628,16 @@ function TokenCachePanel(props) {
576
628
  });
577
629
  const bar = createMemo(() => progressBar(data().hitRate, barW()));
578
630
  const pct = createMemo(() => (Math.floor(data().hitRate * 10) / 10).toFixed(1) + "%");
631
+ // When border visibility changes the box dimensions shift, which
632
+ // may not reliably trigger onSizeChange across (re)mount cycles.
633
+ // Force panelWidth to resync with the live box after every change.
634
+ createEffect(() => {
635
+ borderVisible();
636
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
637
+ const w = Math.max(MIN_PANEL_WIDTH, boxEl.width);
638
+ setPanelWidth((prev) => (prev === w ? prev : w));
639
+ }
640
+ });
579
641
  // left-align label, right-align value — auto-fill space between
580
642
  const justify = (label, value, unit = "") => {
581
643
  const gauge = panelWidth() - gutter();
@@ -583,7 +645,7 @@ function TokenCachePanel(props) {
583
645
  const gap = Math.max(1, gauge - used);
584
646
  return label + " ".repeat(gap) + value + (unit ? " " + unit : "");
585
647
  };
586
- return (_jsxs("box", { border: borderVisible(), borderColor: pal().border, paddingTop: 0, paddingBottom: 0, paddingLeft: borderVisible() ? 2 : 0, paddingRight: borderVisible() ? 2 : 0, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
648
+ return (_jsxs("box", { border: borderVisible(), ...(borderVisible() ? { borderColor: pal().border } : {}), paddingTop: 0, paddingBottom: 0, paddingLeft: borderVisible() ? 2 : 0, paddingRight: borderVisible() ? 2 : 0, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
587
649
  // boxEl.width may be undefined before the first measurement — guard with 0
588
650
  const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
589
651
  setPanelWidth((prev) => (prev === w ? prev : w));
@@ -592,18 +654,33 @@ function TokenCachePanel(props) {
592
654
  // ---------------------------------------------------------------------------
593
655
  // Plugin entry
594
656
  // ---------------------------------------------------------------------------
595
- function createSidebarSlot(api) {
657
+ function createSidebarSlot(api, signals) {
596
658
  return {
597
659
  order: 55,
598
660
  slots: {
599
661
  sidebar_content(ctx, input) {
600
- return (_jsx(TokenCachePanel, { theme: ctx.theme.current, api: api, sessionId: input.session_id }));
662
+ return (_jsx(TokenCachePanel, { theme: ctx.theme.current, api: api, sessionId: input.session_id, signals: signals }));
601
663
  },
602
664
  },
603
665
  };
604
666
  }
605
667
  const tui = async (api) => {
606
- api.slots.register(createSidebarSlot(api));
668
+ // ── shared panel signals ──────────────────────────────────────
669
+ const [currencySymbol, setCurrencySymbol] = createSignal("$");
670
+ const [exchangeRate, setExchangeRate] = createSignal(1);
671
+ const [sectionDetail, setSectionDetail] = createSignal(true);
672
+ const [sectionModel, setSectionModel] = createSignal(true);
673
+ const [sectionDist, setSectionDist] = createSignal(true);
674
+ const [borderVisible, setBorderVisible] = createSignal(true);
675
+ const signals = {
676
+ currencySymbol, setCurrencySymbol,
677
+ exchangeRate, setExchangeRate,
678
+ sectionDetail, setSectionDetail,
679
+ sectionModel, setSectionModel,
680
+ sectionDist, setSectionDist,
681
+ borderVisible, setBorderVisible,
682
+ };
683
+ api.slots.register(createSidebarSlot(api, signals));
607
684
  // ── slash commands for runtime config ──
608
685
  const KV_PREFIX = "cache_panel";
609
686
  api.command?.register(() => [
@@ -621,8 +698,8 @@ const tui = async (api) => {
621
698
  const defRate = DEFAULT_RATES[opt.value] ?? 1;
622
699
  api.kv.set(`${KV_PREFIX}.currency`, sym);
623
700
  api.kv.set(`${KV_PREFIX}.rate`, defRate);
624
- setCurrencySymbol(sym);
625
- setExchangeRate(defRate);
701
+ signals.setCurrencySymbol(sym);
702
+ signals.setExchangeRate(defRate);
626
703
  api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` });
627
704
  dialog?.clear();
628
705
  } })));
@@ -638,7 +715,7 @@ const tui = async (api) => {
638
715
  const n = parseFloat(val);
639
716
  if (n > 0) {
640
717
  api.kv.set(`${KV_PREFIX}.rate`, n);
641
- setExchangeRate(n);
718
+ signals.setExchangeRate(n);
642
719
  api.ui.toast({ message: `Exchange rate set to ${n}` });
643
720
  }
644
721
  dialog?.clear();
@@ -664,7 +741,7 @@ const tui = async (api) => {
664
741
  if (opt.value === "border") {
665
742
  const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
666
743
  api.kv.set(`${KV_PREFIX}.border`, !cur);
667
- setBorderVisible(!cur);
744
+ signals.setBorderVisible(!cur);
668
745
  api.ui.toast({ message: `Panel border ${!cur ? "shown" : "hidden"}` });
669
746
  }
670
747
  else {
@@ -672,11 +749,11 @@ const tui = async (api) => {
672
749
  const cur = Boolean(api.kv.get(key, true));
673
750
  api.kv.set(key, !cur);
674
751
  if (opt.value === "detail")
675
- setSectionDetail(!cur);
752
+ signals.setSectionDetail(!cur);
676
753
  if (opt.value === "model")
677
- setSectionModel(!cur);
754
+ signals.setSectionModel(!cur);
678
755
  if (opt.value === "dist")
679
- setSectionDist(!cur);
756
+ signals.setSectionDist(!cur);
680
757
  api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
681
758
  }
682
759
  dialog?.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-visual-cache",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "OpenCode TUI plugin displaying real-time token cache hit rate in the sidebar",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/_version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // auto-generated
2
- export const PLUGIN_VERSION="1.2.7";
2
+ export const PLUGIN_VERSION="1.2.8";
package/src/index.tsx CHANGED
@@ -17,7 +17,7 @@ import type {
17
17
  FilePart,
18
18
  ReasoningPart,
19
19
  } from "@opencode-ai/sdk/v2"
20
- import { createMemo, createSignal, onMount, onCleanup, Show } from "solid-js"
20
+ import { createMemo, createSignal, createEffect, onMount, onCleanup, Show } from "solid-js"
21
21
  import { PLUGIN_VERSION } from "./_version"
22
22
 
23
23
  // ---------------------------------------------------------------------------
@@ -306,14 +306,23 @@ interface TokenDist {
306
306
  // Sidebar component
307
307
  // ---------------------------------------------------------------------------
308
308
 
309
- /** Module-level config signals — shared between the TUI component and slash
310
- * commands so that runtime changes take effect immediately without restart. */
311
- const [currencySymbol, setCurrencySymbol] = createSignal("$")
312
- const [exchangeRate, setExchangeRate] = createSignal(1)
313
- const [sectionDetail, setSectionDetail] = createSignal(true)
314
- const [sectionModel, setSectionModel] = createSignal(true)
315
- const [sectionDist, setSectionDist] = createSignal(true)
316
- const [borderVisible, setBorderVisible] = createSignal(true)
309
+ /** Signals shared between the TUI component and slash commands.
310
+ * Created in the `tui` function scope so they do not survive module reload —
311
+ * the component re-creates them on mount and restores user config from kv. */
312
+ interface PanelSignals {
313
+ currencySymbol: () => string
314
+ setCurrencySymbol: (v: string) => void
315
+ exchangeRate: () => number
316
+ setExchangeRate: (v: number) => void
317
+ sectionDetail: () => boolean
318
+ setSectionDetail: (v: boolean) => void
319
+ sectionModel: () => boolean
320
+ setSectionModel: (v: boolean) => void
321
+ sectionDist: () => boolean
322
+ setSectionDist: (v: boolean) => void
323
+ borderVisible: () => boolean
324
+ setBorderVisible: (v: boolean) => void
325
+ }
317
326
 
318
327
  const CURRENCIES: Record<string, string> = {
319
328
  USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
@@ -340,6 +349,7 @@ function TokenCachePanel(props: {
340
349
  theme: TuiThemeCurrent
341
350
  api: TuiPluginApi
342
351
  sessionId: string
352
+ signals: PanelSignals
343
353
  }): JSX.Element {
344
354
  const [panelWidth, setPanelWidth] = createSignal(DEFAULT_PANEL_WIDTH)
345
355
  const [open, setOpen] = createSignal(true)
@@ -348,9 +358,31 @@ function TokenCachePanel(props: {
348
358
  const [distOpen, setDistOpen] = createSignal(false)
349
359
  let boxEl: any
350
360
 
361
+ // ── shared signals (de-structured so internal code is unchanged) ──
362
+ const {
363
+ currencySymbol, setCurrencySymbol,
364
+ exchangeRate, setExchangeRate,
365
+ sectionDetail, setSectionDetail,
366
+ sectionModel, setSectionModel,
367
+ sectionDist, setSectionDist,
368
+ borderVisible, setBorderVisible,
369
+ } = props.signals
370
+
351
371
  // ── scan session messages reactively ──
352
372
  // SolidJS createMemo re-evaluates whenever the underlying
353
373
  // api.state.session state changes — no event listener needed.
374
+
375
+ // ── distribution cache ────────────────────────────────────────
376
+ // When data() re-computes before api.state.part() is warm (e.g. after
377
+ // a view switch), hasDistData flips to false and the distribution
378
+ // block disappears. Keep the last valid snapshot so the UI stays
379
+ // stable until the next successful computation arrives.
380
+ const [lastDist, setLastDist] = createSignal<TokenDist>({
381
+ system: 0, user: 0, agent: 0, toolCall: 0, toolResult: 0,
382
+ output: 0, apiOutput: 0, apiInput: 0, stepCost: 0,
383
+ })
384
+ const [lastHasDist, setLastHasDist] = createSignal(false)
385
+
354
386
  const data = createMemo(() => {
355
387
  const msgs = props.api.state.session.messages(props.sessionId) as Message[]
356
388
 
@@ -517,6 +549,11 @@ function TokenCachePanel(props: {
517
549
  // Graceful degradation — dist stays at zeroes
518
550
  }
519
551
 
552
+ // Fall back to last known-good distribution while api.state.part()
553
+ // is re-hydrating after a view switch.
554
+ const finalDist = hasDistData ? dist : lastDist()
555
+ const finalHasDist = hasDistData || lastHasDist()
556
+
520
557
  return {
521
558
  hitRate, read, write, freshInput: input, output,
522
559
  cost, saved, model, inputRate, cacheReadRate, cacheWriteRate, hasPricing,
@@ -524,7 +561,20 @@ function TokenCachePanel(props: {
524
561
  trend, hasTrendData,
525
562
  providerName,
526
563
  sessionHitRate,
527
- dist, hasDistData,
564
+ dist: finalDist,
565
+ hasDistData: finalHasDist,
566
+ }
567
+ })
568
+
569
+ // Persist the last valid distribution so that data() can fall back
570
+ // to it while api.state.part() is re-hydrating after a view switch.
571
+ createEffect(() => {
572
+ const d = data()
573
+ if (d.hasDistData) {
574
+ setLastDist({ ...d.dist })
575
+ setLastHasDist(true)
576
+ // Also persist across component remounts (view switches)
577
+ try { props.api.kv.set(`${KV_PREFIX}.dist_snapshot`, { ...d.dist }) } catch {}
528
578
  }
529
579
  })
530
580
 
@@ -538,7 +588,11 @@ function TokenCachePanel(props: {
538
588
  }
539
589
 
540
590
  onMount(() => {
541
- // Restore fold state from persisted storage
591
+ // Reset panelWidth on (re)mount so the layout uses a clean
592
+ // default until onSizeChange measures the live box dimensions.
593
+ setPanelWidth(DEFAULT_PANEL_WIDTH)
594
+
595
+ // Restore fold state from persisted storage (non-critical — fire and forget)
542
596
  try {
543
597
  setOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.open`, false)))
544
598
  setDetailOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.detail`, true)))
@@ -546,20 +600,48 @@ function TokenCachePanel(props: {
546
600
  setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)))
547
601
  } catch {}
548
602
 
549
- // Restore user config (currency, rate, section visibility)
550
- try {
551
- const sym = props.api.kv.get<string>(`${KV_PREFIX}.currency`)
552
- const rate = props.api.kv.get<number>(`${KV_PREFIX}.rate`)
553
- if (typeof sym === "string") setCurrencySymbol(sym)
554
- if (typeof rate === "number" && rate > 0) setExchangeRate(rate)
555
- setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
556
- setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
557
- setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
558
- const borderVal = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
559
- setBorderVisible(borderVal !== false)
560
- } catch {}
603
+ // Restore user config (currency, rate, section visibility).
604
+ // Try synchronously first (kv is usually ready on mount), fall back to
605
+ // polling if the module was reloaded and kv hasn't initialised yet.
606
+ const doRestore = () => {
607
+ try {
608
+ const sym = props.api.kv.get<string>(`${KV_PREFIX}.currency`)
609
+ const rate = props.api.kv.get<number>(`${KV_PREFIX}.rate`)
610
+ if (typeof sym === "string") setCurrencySymbol(sym)
611
+ if (typeof rate === "number" && rate > 0) setExchangeRate(rate)
612
+ setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
613
+ setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
614
+ setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
615
+ const bv = props.api.kv.get<boolean>(`${KV_PREFIX}.border`, true)
616
+ setBorderVisible(bv !== false)
617
+ // Restore distribution snapshot so the token distribution block
618
+ // doesn't blank out while api.state.part() re-hydrates.
619
+ const cachedDist = props.api.kv.get<TokenDist>(`${KV_PREFIX}.dist_snapshot`)
620
+ if (cachedDist) {
621
+ setLastDist(cachedDist)
622
+ setLastHasDist(true)
623
+ }
624
+ } catch {
625
+ // kv read failed — signals stay at defaults
626
+ }
627
+ // Re-measure panel width after config signals have settled
628
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
629
+ setPanelWidth(Math.max(MIN_PANEL_WIDTH, boxEl.width))
630
+ }
631
+ }
561
632
 
562
- // (border visibility was restored above together with section config)
633
+ if (props.api.kv.ready) {
634
+ doRestore()
635
+ } else {
636
+ const pollRestore = () => {
637
+ if (!props.api.kv.ready) {
638
+ setTimeout(pollRestore, 10)
639
+ return
640
+ }
641
+ doRestore()
642
+ }
643
+ pollRestore()
644
+ }
563
645
 
564
646
  const unsubPart = props.api.event.on("message.part.updated", () => {
565
647
  setPartVersion((v) => v + 1)
@@ -610,6 +692,17 @@ function TokenCachePanel(props: {
610
692
  const bar = createMemo(() => progressBar(data().hitRate, barW()))
611
693
  const pct = createMemo(() => (Math.floor(data().hitRate * 10) / 10).toFixed(1) + "%")
612
694
 
695
+ // When border visibility changes the box dimensions shift, which
696
+ // may not reliably trigger onSizeChange across (re)mount cycles.
697
+ // Force panelWidth to resync with the live box after every change.
698
+ createEffect(() => {
699
+ borderVisible()
700
+ if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
701
+ const w = Math.max(MIN_PANEL_WIDTH, boxEl.width)
702
+ setPanelWidth((prev) => (prev === w ? prev : w))
703
+ }
704
+ })
705
+
613
706
  // left-align label, right-align value — auto-fill space between
614
707
  const justify = (label: string, value: string, unit = ""): string => {
615
708
  const gauge = panelWidth() - gutter()
@@ -621,7 +714,7 @@ function TokenCachePanel(props: {
621
714
  return (
622
715
  <box
623
716
  border={borderVisible()}
624
- borderColor={pal().border}
717
+ {...(borderVisible() ? { borderColor: pal().border } : {})}
625
718
  paddingTop={0}
626
719
  paddingBottom={0}
627
720
  paddingLeft={borderVisible() ? 2 : 0}
@@ -815,7 +908,7 @@ function TokenCachePanel(props: {
815
908
  // Plugin entry
816
909
  // ---------------------------------------------------------------------------
817
910
 
818
- function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
911
+ function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin {
819
912
  return {
820
913
  order: 55,
821
914
  slots: {
@@ -825,6 +918,7 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
825
918
  theme={ctx.theme.current}
826
919
  api={api}
827
920
  sessionId={input.session_id}
921
+ signals={signals}
828
922
  />
829
923
  )
830
924
  },
@@ -833,7 +927,24 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
833
927
  }
834
928
 
835
929
  const tui: TuiPlugin = async (api: TuiPluginApi) => {
836
- api.slots.register(createSidebarSlot(api))
930
+ // ── shared panel signals ──────────────────────────────────────
931
+ const [currencySymbol, setCurrencySymbol] = createSignal("$")
932
+ const [exchangeRate, setExchangeRate] = createSignal(1)
933
+ const [sectionDetail, setSectionDetail] = createSignal(true)
934
+ const [sectionModel, setSectionModel] = createSignal(true)
935
+ const [sectionDist, setSectionDist] = createSignal(true)
936
+ const [borderVisible, setBorderVisible] = createSignal(true)
937
+
938
+ const signals: PanelSignals = {
939
+ currencySymbol, setCurrencySymbol,
940
+ exchangeRate, setExchangeRate,
941
+ sectionDetail, setSectionDetail,
942
+ sectionModel, setSectionModel,
943
+ sectionDist, setSectionDist,
944
+ borderVisible, setBorderVisible,
945
+ }
946
+
947
+ api.slots.register(createSidebarSlot(api, signals))
837
948
 
838
949
  // ── slash commands for runtime config ──
839
950
  const KV_PREFIX = "cache_panel"
@@ -856,8 +967,8 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
856
967
  const defRate = DEFAULT_RATES[opt.value] ?? 1
857
968
  api.kv.set(`${KV_PREFIX}.currency`, sym)
858
969
  api.kv.set(`${KV_PREFIX}.rate`, defRate)
859
- setCurrencySymbol(sym)
860
- setExchangeRate(defRate)
970
+ signals.setCurrencySymbol(sym)
971
+ signals.setExchangeRate(defRate)
861
972
  api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` })
862
973
  dialog?.clear()
863
974
  }}
@@ -881,7 +992,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
881
992
  const n = parseFloat(val)
882
993
  if (n > 0) {
883
994
  api.kv.set(`${KV_PREFIX}.rate`, n)
884
- setExchangeRate(n)
995
+ signals.setExchangeRate(n)
885
996
  api.ui.toast({ message: `Exchange rate set to ${n}` })
886
997
  }
887
998
  dialog?.clear()
@@ -913,15 +1024,15 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
913
1024
  if (opt.value === "border") {
914
1025
  const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
915
1026
  api.kv.set(`${KV_PREFIX}.border`, !cur)
916
- setBorderVisible(!cur)
1027
+ signals.setBorderVisible(!cur)
917
1028
  api.ui.toast({ message: `Panel border ${!cur ? "shown" : "hidden"}` })
918
1029
  } else {
919
1030
  const key = `${KV_PREFIX}.section.${opt.value}`
920
1031
  const cur = Boolean(api.kv.get(key, true))
921
1032
  api.kv.set(key, !cur)
922
- if (opt.value === "detail") setSectionDetail(!cur)
923
- if (opt.value === "model") setSectionModel(!cur)
924
- if (opt.value === "dist") setSectionDist(!cur)
1033
+ if (opt.value === "detail") signals.setSectionDetail(!cur)
1034
+ if (opt.value === "model") signals.setSectionModel(!cur)
1035
+ if (opt.value === "dist") signals.setSectionDist(!cur)
925
1036
  api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
926
1037
  }
927
1038
  dialog?.clear()