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.
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/index.js +117 -40
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +145 -34
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.2.
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.2.
|
|
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
|
-
/**
|
|
310
|
-
*
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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()
|