opencode-visual-cache 1.2.7 → 1.2.9-beta.0
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 +137 -47
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/index.tsx +162 -41
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.2.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.2.9-beta.0";
|
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.9-beta.0";
|
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,29 +538,71 @@ 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();
|
|
531
575
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
576
|
+
else {
|
|
577
|
+
// Poll kv.ready with a 1-second timeout to avoid infinite busy-wait
|
|
578
|
+
// on platforms where kv initialisation may be delayed (Linux single-thread
|
|
579
|
+
// mode, session switch storms, etc.).
|
|
580
|
+
const MAX_POLL = 100;
|
|
581
|
+
let tries = 0;
|
|
582
|
+
const pollRestore = () => {
|
|
583
|
+
if (!props.api.kv.ready) {
|
|
584
|
+
if (++tries > MAX_POLL) {
|
|
585
|
+
doRestore();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
setTimeout(pollRestore, 10);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
doRestore();
|
|
592
|
+
};
|
|
593
|
+
pollRestore();
|
|
594
|
+
}
|
|
595
|
+
// Debounce partVersion updates so that event bursts during session
|
|
596
|
+
// switching / streaming don't cause data() to re-compute on every
|
|
597
|
+
// single event (up to hundreds per second on Linux single-thread).
|
|
598
|
+
let partTimer;
|
|
599
|
+
const bumpPartVersion = () => {
|
|
600
|
+
clearTimeout(partTimer);
|
|
601
|
+
partTimer = setTimeout(() => setPartVersion((v) => v + 1), 100);
|
|
602
|
+
};
|
|
603
|
+
const unsubPart = props.api.event.on("message.part.updated", bumpPartVersion);
|
|
604
|
+
const unsubMsg = props.api.event.on("message.updated", bumpPartVersion);
|
|
605
|
+
onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg(); });
|
|
541
606
|
});
|
|
542
607
|
// ── colours ──
|
|
543
608
|
// Pull from the current theme, auto-desaturate if too punchy,
|
|
@@ -576,6 +641,16 @@ function TokenCachePanel(props) {
|
|
|
576
641
|
});
|
|
577
642
|
const bar = createMemo(() => progressBar(data().hitRate, barW()));
|
|
578
643
|
const pct = createMemo(() => (Math.floor(data().hitRate * 10) / 10).toFixed(1) + "%");
|
|
644
|
+
// When border visibility changes the box dimensions shift, which
|
|
645
|
+
// may not reliably trigger onSizeChange across (re)mount cycles.
|
|
646
|
+
// Force panelWidth to resync with the live box after every change.
|
|
647
|
+
createEffect(() => {
|
|
648
|
+
borderVisible();
|
|
649
|
+
if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
|
|
650
|
+
const w = Math.max(MIN_PANEL_WIDTH, boxEl.width);
|
|
651
|
+
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
652
|
+
}
|
|
653
|
+
});
|
|
579
654
|
// left-align label, right-align value — auto-fill space between
|
|
580
655
|
const justify = (label, value, unit = "") => {
|
|
581
656
|
const gauge = panelWidth() - gutter();
|
|
@@ -583,7 +658,7 @@ function TokenCachePanel(props) {
|
|
|
583
658
|
const gap = Math.max(1, gauge - used);
|
|
584
659
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "");
|
|
585
660
|
};
|
|
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: () => {
|
|
661
|
+
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
662
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
588
663
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
589
664
|
setPanelWidth((prev) => (prev === w ? prev : w));
|
|
@@ -592,18 +667,33 @@ function TokenCachePanel(props) {
|
|
|
592
667
|
// ---------------------------------------------------------------------------
|
|
593
668
|
// Plugin entry
|
|
594
669
|
// ---------------------------------------------------------------------------
|
|
595
|
-
function createSidebarSlot(api) {
|
|
670
|
+
function createSidebarSlot(api, signals) {
|
|
596
671
|
return {
|
|
597
672
|
order: 55,
|
|
598
673
|
slots: {
|
|
599
674
|
sidebar_content(ctx, input) {
|
|
600
|
-
return (_jsx(TokenCachePanel, { theme: ctx.theme.current, api: api, sessionId: input.session_id }));
|
|
675
|
+
return (_jsx(TokenCachePanel, { theme: ctx.theme.current, api: api, sessionId: input.session_id, signals: signals }));
|
|
601
676
|
},
|
|
602
677
|
},
|
|
603
678
|
};
|
|
604
679
|
}
|
|
605
680
|
const tui = async (api) => {
|
|
606
|
-
|
|
681
|
+
// ── shared panel signals ──────────────────────────────────────
|
|
682
|
+
const [currencySymbol, setCurrencySymbol] = createSignal("$");
|
|
683
|
+
const [exchangeRate, setExchangeRate] = createSignal(1);
|
|
684
|
+
const [sectionDetail, setSectionDetail] = createSignal(true);
|
|
685
|
+
const [sectionModel, setSectionModel] = createSignal(true);
|
|
686
|
+
const [sectionDist, setSectionDist] = createSignal(true);
|
|
687
|
+
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
688
|
+
const signals = {
|
|
689
|
+
currencySymbol, setCurrencySymbol,
|
|
690
|
+
exchangeRate, setExchangeRate,
|
|
691
|
+
sectionDetail, setSectionDetail,
|
|
692
|
+
sectionModel, setSectionModel,
|
|
693
|
+
sectionDist, setSectionDist,
|
|
694
|
+
borderVisible, setBorderVisible,
|
|
695
|
+
};
|
|
696
|
+
api.slots.register(createSidebarSlot(api, signals));
|
|
607
697
|
// ── slash commands for runtime config ──
|
|
608
698
|
const KV_PREFIX = "cache_panel";
|
|
609
699
|
api.command?.register(() => [
|
|
@@ -621,8 +711,8 @@ const tui = async (api) => {
|
|
|
621
711
|
const defRate = DEFAULT_RATES[opt.value] ?? 1;
|
|
622
712
|
api.kv.set(`${KV_PREFIX}.currency`, sym);
|
|
623
713
|
api.kv.set(`${KV_PREFIX}.rate`, defRate);
|
|
624
|
-
setCurrencySymbol(sym);
|
|
625
|
-
setExchangeRate(defRate);
|
|
714
|
+
signals.setCurrencySymbol(sym);
|
|
715
|
+
signals.setExchangeRate(defRate);
|
|
626
716
|
api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` });
|
|
627
717
|
dialog?.clear();
|
|
628
718
|
} })));
|
|
@@ -638,7 +728,7 @@ const tui = async (api) => {
|
|
|
638
728
|
const n = parseFloat(val);
|
|
639
729
|
if (n > 0) {
|
|
640
730
|
api.kv.set(`${KV_PREFIX}.rate`, n);
|
|
641
|
-
setExchangeRate(n);
|
|
731
|
+
signals.setExchangeRate(n);
|
|
642
732
|
api.ui.toast({ message: `Exchange rate set to ${n}` });
|
|
643
733
|
}
|
|
644
734
|
dialog?.clear();
|
|
@@ -664,7 +754,7 @@ const tui = async (api) => {
|
|
|
664
754
|
if (opt.value === "border") {
|
|
665
755
|
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true));
|
|
666
756
|
api.kv.set(`${KV_PREFIX}.border`, !cur);
|
|
667
|
-
setBorderVisible(!cur);
|
|
757
|
+
signals.setBorderVisible(!cur);
|
|
668
758
|
api.ui.toast({ message: `Panel border ${!cur ? "shown" : "hidden"}` });
|
|
669
759
|
}
|
|
670
760
|
else {
|
|
@@ -672,11 +762,11 @@ const tui = async (api) => {
|
|
|
672
762
|
const cur = Boolean(api.kv.get(key, true));
|
|
673
763
|
api.kv.set(key, !cur);
|
|
674
764
|
if (opt.value === "detail")
|
|
675
|
-
setSectionDetail(!cur);
|
|
765
|
+
signals.setSectionDetail(!cur);
|
|
676
766
|
if (opt.value === "model")
|
|
677
|
-
setSectionModel(!cur);
|
|
767
|
+
signals.setSectionModel(!cur);
|
|
678
768
|
if (opt.value === "dist")
|
|
679
|
-
setSectionDist(!cur);
|
|
769
|
+
signals.setSectionDist(!cur);
|
|
680
770
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` });
|
|
681
771
|
}
|
|
682
772
|
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.9-beta.0";
|
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,28 +600,66 @@ 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
|
+
// Poll kv.ready with a 1-second timeout to avoid infinite busy-wait
|
|
637
|
+
// on platforms where kv initialisation may be delayed (Linux single-thread
|
|
638
|
+
// mode, session switch storms, etc.).
|
|
639
|
+
const MAX_POLL = 100
|
|
640
|
+
let tries = 0
|
|
641
|
+
const pollRestore = () => {
|
|
642
|
+
if (!props.api.kv.ready) {
|
|
643
|
+
if (++tries > MAX_POLL) { doRestore(); return }
|
|
644
|
+
setTimeout(pollRestore, 10)
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
doRestore()
|
|
648
|
+
}
|
|
649
|
+
pollRestore()
|
|
650
|
+
}
|
|
563
651
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
652
|
+
// Debounce partVersion updates so that event bursts during session
|
|
653
|
+
// switching / streaming don't cause data() to re-compute on every
|
|
654
|
+
// single event (up to hundreds per second on Linux single-thread).
|
|
655
|
+
let partTimer: ReturnType<typeof setTimeout> | undefined
|
|
656
|
+
const bumpPartVersion = () => {
|
|
657
|
+
clearTimeout(partTimer)
|
|
658
|
+
partTimer = setTimeout(() => setPartVersion((v) => v + 1), 100)
|
|
659
|
+
}
|
|
660
|
+
const unsubPart = props.api.event.on("message.part.updated", bumpPartVersion)
|
|
661
|
+
const unsubMsg = props.api.event.on("message.updated", bumpPartVersion)
|
|
662
|
+
onCleanup(() => { clearTimeout(partTimer); unsubPart(); unsubMsg() })
|
|
571
663
|
})
|
|
572
664
|
|
|
573
665
|
// ── colours ──
|
|
@@ -610,6 +702,17 @@ function TokenCachePanel(props: {
|
|
|
610
702
|
const bar = createMemo(() => progressBar(data().hitRate, barW()))
|
|
611
703
|
const pct = createMemo(() => (Math.floor(data().hitRate * 10) / 10).toFixed(1) + "%")
|
|
612
704
|
|
|
705
|
+
// When border visibility changes the box dimensions shift, which
|
|
706
|
+
// may not reliably trigger onSizeChange across (re)mount cycles.
|
|
707
|
+
// Force panelWidth to resync with the live box after every change.
|
|
708
|
+
createEffect(() => {
|
|
709
|
+
borderVisible()
|
|
710
|
+
if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
|
|
711
|
+
const w = Math.max(MIN_PANEL_WIDTH, boxEl.width)
|
|
712
|
+
setPanelWidth((prev) => (prev === w ? prev : w))
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
|
|
613
716
|
// left-align label, right-align value — auto-fill space between
|
|
614
717
|
const justify = (label: string, value: string, unit = ""): string => {
|
|
615
718
|
const gauge = panelWidth() - gutter()
|
|
@@ -621,7 +724,7 @@ function TokenCachePanel(props: {
|
|
|
621
724
|
return (
|
|
622
725
|
<box
|
|
623
726
|
border={borderVisible()}
|
|
624
|
-
borderColor
|
|
727
|
+
{...(borderVisible() ? { borderColor: pal().border } : {})}
|
|
625
728
|
paddingTop={0}
|
|
626
729
|
paddingBottom={0}
|
|
627
730
|
paddingLeft={borderVisible() ? 2 : 0}
|
|
@@ -815,7 +918,7 @@ function TokenCachePanel(props: {
|
|
|
815
918
|
// Plugin entry
|
|
816
919
|
// ---------------------------------------------------------------------------
|
|
817
920
|
|
|
818
|
-
function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
|
|
921
|
+
function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin {
|
|
819
922
|
return {
|
|
820
923
|
order: 55,
|
|
821
924
|
slots: {
|
|
@@ -825,6 +928,7 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
|
|
|
825
928
|
theme={ctx.theme.current}
|
|
826
929
|
api={api}
|
|
827
930
|
sessionId={input.session_id}
|
|
931
|
+
signals={signals}
|
|
828
932
|
/>
|
|
829
933
|
)
|
|
830
934
|
},
|
|
@@ -833,7 +937,24 @@ function createSidebarSlot(api: TuiPluginApi): TuiSlotPlugin {
|
|
|
833
937
|
}
|
|
834
938
|
|
|
835
939
|
const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
836
|
-
|
|
940
|
+
// ── shared panel signals ──────────────────────────────────────
|
|
941
|
+
const [currencySymbol, setCurrencySymbol] = createSignal("$")
|
|
942
|
+
const [exchangeRate, setExchangeRate] = createSignal(1)
|
|
943
|
+
const [sectionDetail, setSectionDetail] = createSignal(true)
|
|
944
|
+
const [sectionModel, setSectionModel] = createSignal(true)
|
|
945
|
+
const [sectionDist, setSectionDist] = createSignal(true)
|
|
946
|
+
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
947
|
+
|
|
948
|
+
const signals: PanelSignals = {
|
|
949
|
+
currencySymbol, setCurrencySymbol,
|
|
950
|
+
exchangeRate, setExchangeRate,
|
|
951
|
+
sectionDetail, setSectionDetail,
|
|
952
|
+
sectionModel, setSectionModel,
|
|
953
|
+
sectionDist, setSectionDist,
|
|
954
|
+
borderVisible, setBorderVisible,
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
api.slots.register(createSidebarSlot(api, signals))
|
|
837
958
|
|
|
838
959
|
// ── slash commands for runtime config ──
|
|
839
960
|
const KV_PREFIX = "cache_panel"
|
|
@@ -856,8 +977,8 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
856
977
|
const defRate = DEFAULT_RATES[opt.value] ?? 1
|
|
857
978
|
api.kv.set(`${KV_PREFIX}.currency`, sym)
|
|
858
979
|
api.kv.set(`${KV_PREFIX}.rate`, defRate)
|
|
859
|
-
setCurrencySymbol(sym)
|
|
860
|
-
setExchangeRate(defRate)
|
|
980
|
+
signals.setCurrencySymbol(sym)
|
|
981
|
+
signals.setExchangeRate(defRate)
|
|
861
982
|
api.ui.toast({ message: `Currency: ${opt.value} (${sym}), rate: ${defRate}` })
|
|
862
983
|
dialog?.clear()
|
|
863
984
|
}}
|
|
@@ -881,7 +1002,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
881
1002
|
const n = parseFloat(val)
|
|
882
1003
|
if (n > 0) {
|
|
883
1004
|
api.kv.set(`${KV_PREFIX}.rate`, n)
|
|
884
|
-
setExchangeRate(n)
|
|
1005
|
+
signals.setExchangeRate(n)
|
|
885
1006
|
api.ui.toast({ message: `Exchange rate set to ${n}` })
|
|
886
1007
|
}
|
|
887
1008
|
dialog?.clear()
|
|
@@ -913,15 +1034,15 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
913
1034
|
if (opt.value === "border") {
|
|
914
1035
|
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
915
1036
|
api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
916
|
-
setBorderVisible(!cur)
|
|
1037
|
+
signals.setBorderVisible(!cur)
|
|
917
1038
|
api.ui.toast({ message: `Panel border ${!cur ? "shown" : "hidden"}` })
|
|
918
1039
|
} else {
|
|
919
1040
|
const key = `${KV_PREFIX}.section.${opt.value}`
|
|
920
1041
|
const cur = Boolean(api.kv.get(key, true))
|
|
921
1042
|
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)
|
|
1043
|
+
if (opt.value === "detail") signals.setSectionDetail(!cur)
|
|
1044
|
+
if (opt.value === "model") signals.setSectionModel(!cur)
|
|
1045
|
+
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
925
1046
|
api.ui.toast({ message: `${opt.value} section ${!cur ? "shown" : "hidden"}` })
|
|
926
1047
|
}
|
|
927
1048
|
dialog?.clear()
|