@chenlongapps/opencode-token-usage 0.4.7 → 0.4.9
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/README.md +17 -9
- package/README.zh-CN.md +17 -9
- package/ROADMAP.md +12 -0
- package/dist/click.d.ts +9 -0
- package/dist/click.js +20 -0
- package/dist/context-rpc.d.ts +11 -0
- package/dist/context-rpc.js +6 -0
- package/dist/controller.d.ts +16 -1
- package/dist/controller.js +80 -6
- package/dist/index.js +1 -0
- package/dist/performance.d.ts +8 -3
- package/dist/performance.js +77 -23
- package/dist/runtime-monitor.d.ts +34 -0
- package/dist/runtime-monitor.js +155 -0
- package/dist/runtime.d.ts +30 -0
- package/dist/runtime.js +103 -0
- package/dist/tui.js +151 -83
- package/dist/usage.d.ts +11 -1
- package/dist/usage.js +61 -2
- package/docs/verification.md +145 -0
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { memo as _$memo } from "@opentui/solid";
|
|
2
2
|
import { use as _$use } from "@opentui/solid";
|
|
3
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
3
4
|
import { insert as _$insert } from "@opentui/solid";
|
|
4
5
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
5
|
-
import { effect as _$effect } from "@opentui/solid";
|
|
6
6
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
7
7
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
8
|
+
import { spread as _$spread } from "@opentui/solid";
|
|
9
|
+
import { mergeProps as _$mergeProps } from "@opentui/solid";
|
|
8
10
|
import { setProp as _$setProp } from "@opentui/solid";
|
|
9
11
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
10
12
|
import { Plugin } from "@opencode/plugin/tui";
|
|
@@ -12,12 +14,16 @@ import { useTerminalDimensions } from "@opentui/solid";
|
|
|
12
14
|
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
|
|
13
15
|
import { UsageController } from "./controller.js";
|
|
14
16
|
import { PerformanceMonitor } from "./performance.js";
|
|
17
|
+
import { RuntimeMonitor } from "./runtime-monitor.js";
|
|
18
|
+
import { ContextSourceRpc } from "./context-rpc.js";
|
|
15
19
|
import { breakdownRows } from "./context-sources.js";
|
|
20
|
+
import { createClickHandlers } from "./click.js";
|
|
16
21
|
import { createSource } from "./source.js";
|
|
17
|
-
import { bar, countLabel, formatCompact, formatEstimatedCost, formatRateTier, formatTokens, rateRows, requestRows, summaryRows, usageRows } from "./usage.js";
|
|
22
|
+
import { bar, countLabel, fitUsageSummary, formatCompact, formatEstimatedCost, formatRateTier, formatRunTime, formatTokens, rateRows, requestRows, summaryRows, usageRows } from "./usage.js";
|
|
18
23
|
function UsageView(props) {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
24
|
+
const titleClick = createClickHandlers(() => props.openDetails?.(), () => !!props.context.renderer.getSelection()?.getSelectedText());
|
|
25
|
+
const rows = createMemo(() => usageRows(props.state().summary, props.state().context, props.state().performance, props.state().runtime));
|
|
26
|
+
const performanceStart = createMemo(() => rows().findIndex(([label]) => label === "Run Time" || label === "TPS" || label === "TTFT"));
|
|
21
27
|
const status = () => ({
|
|
22
28
|
loading: "Loading…",
|
|
23
29
|
ready: "",
|
|
@@ -36,8 +42,12 @@ function UsageView(props) {
|
|
|
36
42
|
var _el$2 = _$createElement("text"),
|
|
37
43
|
_el$3 = _$createElement("b");
|
|
38
44
|
_$insertNode(_el$2, _el$3);
|
|
45
|
+
_$spread(_el$2, _$mergeProps({
|
|
46
|
+
get fg() {
|
|
47
|
+
return props.context.theme.text.base;
|
|
48
|
+
}
|
|
49
|
+
}, titleClick), true);
|
|
39
50
|
_$insertNode(_el$3, _$createTextNode(`Token Usage`));
|
|
40
|
-
_$effect(_$p => _$setProp(_el$2, "fg", props.context.theme.text.base, _$p));
|
|
41
51
|
return _el$2;
|
|
42
52
|
}
|
|
43
53
|
}), null);
|
|
@@ -92,9 +102,12 @@ function UsagePanel(props) {
|
|
|
92
102
|
});
|
|
93
103
|
const controller = new UsageController(createSource(props.context.client), listener => props.context.data.listen(({
|
|
94
104
|
details
|
|
95
|
-
}) => listener(details)), setState, 80, 3_000, 100, props.performance);
|
|
105
|
+
}) => listener(details)), setState, 80, 3_000, 100, props.performance, false, props.runtime);
|
|
96
106
|
const unregister = props.register(controller);
|
|
97
|
-
createEffect(() =>
|
|
107
|
+
createEffect(() => {
|
|
108
|
+
controller.select(props.sessionID);
|
|
109
|
+
controller.setRunning(props.context.data.session.status(props.sessionID) === "running");
|
|
110
|
+
});
|
|
98
111
|
onCleanup(() => {
|
|
99
112
|
controller.dispose();
|
|
100
113
|
unregister();
|
|
@@ -103,7 +116,8 @@ function UsagePanel(props) {
|
|
|
103
116
|
get context() {
|
|
104
117
|
return props.context;
|
|
105
118
|
},
|
|
106
|
-
state: state
|
|
119
|
+
state: state,
|
|
120
|
+
openDetails: () => props.open(props.sessionID)
|
|
107
121
|
});
|
|
108
122
|
}
|
|
109
123
|
function ChildUsageDialog(props) {
|
|
@@ -200,13 +214,18 @@ function ChildUsageLauncher(props) {
|
|
|
200
214
|
});
|
|
201
215
|
const controller = new UsageController(createSource(props.context.client), listener => props.context.data.listen(({
|
|
202
216
|
details
|
|
203
|
-
}) => listener(details)), setState, 80, 3_000, 100, props.performance);
|
|
217
|
+
}) => listener(details)), setState, 80, 3_000, 100, props.performance, false, props.runtime);
|
|
204
218
|
const unregister = props.register(controller);
|
|
205
|
-
createEffect(() =>
|
|
219
|
+
createEffect(() => {
|
|
220
|
+
controller.select(props.sessionID);
|
|
221
|
+
controller.setRunning(props.context.data.session.status(props.sessionID) === "running");
|
|
222
|
+
});
|
|
206
223
|
onCleanup(() => {
|
|
207
224
|
controller.dispose();
|
|
208
225
|
unregister();
|
|
209
226
|
});
|
|
227
|
+
const dimensions = useTerminalDimensions();
|
|
228
|
+
const runtime = createMemo(() => formatRunTime(state().runtime, true));
|
|
210
229
|
const summary = createMemo(() => {
|
|
211
230
|
const current = state();
|
|
212
231
|
if (!current.summary) return `Token Usage · ${{
|
|
@@ -218,8 +237,7 @@ function ChildUsageLauncher(props) {
|
|
|
218
237
|
const context = current.context ? `${formatTokens(current.context.used)} / ${formatTokens(current.context.limit)} (${current.context.percent.toFixed(1)}%)` : "—";
|
|
219
238
|
const cost = formatEstimatedCost(current.summary.cost, current.summary.costStatus) ?? "—";
|
|
220
239
|
const tps = current.performance?.tps !== undefined && Number.isFinite(current.performance.tps) ? `${current.performance.tpsEstimated ? "~" : ""}${Math.max(0, current.performance.tps).toFixed(1)} tok/s` : "—";
|
|
221
|
-
|
|
222
|
-
return `Token Usage · Context ${context} · Total ${formatTokens(current.summary.total)} · Cost ${cost} · TPS ${tps}${stale}`;
|
|
240
|
+
return fitUsageSummary([["Context", context], ["Total", formatTokens(current.summary.total)], ["Cost", cost], ["Time", runtime()], ["TPS", tps]], Math.max(20, dimensions().width - 8), current.status === "stale");
|
|
223
241
|
});
|
|
224
242
|
return (() => {
|
|
225
243
|
var _el$17 = _$createElement("box"),
|
|
@@ -231,7 +249,7 @@ function ChildUsageLauncher(props) {
|
|
|
231
249
|
_$setProp(_el$17, "flexShrink", 0);
|
|
232
250
|
_$setProp(_el$18, "wrapMode", "none");
|
|
233
251
|
_$setProp(_el$18, "onMouseUp", event => {
|
|
234
|
-
if (event.button === 0) props.open(
|
|
252
|
+
if (event.button === 0) props.open(state);
|
|
235
253
|
});
|
|
236
254
|
_$insert(_el$18, summary);
|
|
237
255
|
_$effect(_$p => _$setProp(_el$18, "fg", props.context.theme.text.muted, _$p));
|
|
@@ -246,9 +264,10 @@ function UsageDialog(props) {
|
|
|
246
264
|
const dimensions = useTerminalDimensions();
|
|
247
265
|
const controller = new UsageController(createSource(props.context.client), listener => props.context.data.listen(({
|
|
248
266
|
details
|
|
249
|
-
}) => listener(details)), setState, 80, 3_000, 100, props.performance, true);
|
|
267
|
+
}) => listener(details)), setState, 80, 3_000, 100, props.performance, true, props.runtime);
|
|
250
268
|
const unregister = props.register(controller);
|
|
251
269
|
controller.select(props.sessionID);
|
|
270
|
+
createEffect(() => controller.setRunning(props.context.data.session.status(props.sessionID) === "running"));
|
|
252
271
|
onCleanup(() => {
|
|
253
272
|
controller.dispose();
|
|
254
273
|
unregister();
|
|
@@ -298,6 +317,7 @@ function UsageDialog(props) {
|
|
|
298
317
|
}));
|
|
299
318
|
const theme = () => props.context.theme.text;
|
|
300
319
|
const context = createMemo(() => state().context);
|
|
320
|
+
const runtime = createMemo(() => formatRunTime(state().runtime));
|
|
301
321
|
const request = createMemo(() => state().details?.request);
|
|
302
322
|
const requestTime = createMemo(() => {
|
|
303
323
|
const time = request()?.time;
|
|
@@ -731,6 +751,38 @@ function UsageDialog(props) {
|
|
|
731
751
|
}
|
|
732
752
|
})
|
|
733
753
|
}), null);
|
|
754
|
+
_$insert(_el$53, _$createComponent(Show, {
|
|
755
|
+
get when() {
|
|
756
|
+
return runtime();
|
|
757
|
+
},
|
|
758
|
+
children: time => (() => {
|
|
759
|
+
var _el$104 = _$createElement("box"),
|
|
760
|
+
_el$105 = _$createElement("text"),
|
|
761
|
+
_el$107 = _$createElement("text");
|
|
762
|
+
_$insertNode(_el$104, _el$105);
|
|
763
|
+
_$insertNode(_el$104, _el$107);
|
|
764
|
+
_$setProp(_el$104, "flexDirection", "row");
|
|
765
|
+
_$setProp(_el$104, "justifyContent", "space-between");
|
|
766
|
+
_$setProp(_el$104, "gap", 2);
|
|
767
|
+
_$insertNode(_el$105, _$createTextNode(`Run Time (this session)`));
|
|
768
|
+
_$setProp(_el$105, "wrapMode", "word");
|
|
769
|
+
_$setProp(_el$105, "minWidth", 0);
|
|
770
|
+
_$setProp(_el$107, "wrapMode", "word");
|
|
771
|
+
_$setProp(_el$107, "minWidth", 0);
|
|
772
|
+
_$insert(_el$107, time);
|
|
773
|
+
_$effect(_p$ => {
|
|
774
|
+
var _v$29 = theme().muted,
|
|
775
|
+
_v$30 = theme().base;
|
|
776
|
+
_v$29 !== _p$.e && (_p$.e = _$setProp(_el$105, "fg", _v$29, _p$.e));
|
|
777
|
+
_v$30 !== _p$.t && (_p$.t = _$setProp(_el$107, "fg", _v$30, _p$.t));
|
|
778
|
+
return _p$;
|
|
779
|
+
}, {
|
|
780
|
+
e: undefined,
|
|
781
|
+
t: undefined
|
|
782
|
+
});
|
|
783
|
+
return _el$104;
|
|
784
|
+
})()
|
|
785
|
+
}), null);
|
|
734
786
|
_$insertNode(_el$57, _el$58);
|
|
735
787
|
_$insertNode(_el$58, _el$59);
|
|
736
788
|
_$insertNode(_el$59, _$createTextNode(`By Model`));
|
|
@@ -752,10 +804,10 @@ function UsageDialog(props) {
|
|
|
752
804
|
},
|
|
753
805
|
get fallback() {
|
|
754
806
|
return (() => {
|
|
755
|
-
var _el$
|
|
756
|
-
_$insertNode(_el$
|
|
757
|
-
_$effect(_$p => _$setProp(_el$
|
|
758
|
-
return _el$
|
|
807
|
+
var _el$108 = _$createElement("text");
|
|
808
|
+
_$insertNode(_el$108, _$createTextNode(`No model usage yet`));
|
|
809
|
+
_$effect(_$p => _$setProp(_el$108, "fg", theme().muted, _$p));
|
|
810
|
+
return _el$108;
|
|
759
811
|
})();
|
|
760
812
|
},
|
|
761
813
|
get children() {
|
|
@@ -764,29 +816,29 @@ function UsageDialog(props) {
|
|
|
764
816
|
return models();
|
|
765
817
|
},
|
|
766
818
|
children: model => (() => {
|
|
767
|
-
var _el$
|
|
768
|
-
_el$
|
|
769
|
-
_el$
|
|
770
|
-
_el$
|
|
771
|
-
_el$
|
|
772
|
-
_el$
|
|
773
|
-
_$insertNode(_el$106, _el$107);
|
|
774
|
-
_$insertNode(_el$106, _el$110);
|
|
775
|
-
_$insertNode(_el$107, _el$108);
|
|
776
|
-
_$insertNode(_el$107, _el$109);
|
|
777
|
-
_$setProp(_el$107, "flexDirection", "row");
|
|
778
|
-
_$setProp(_el$107, "justifyContent", "space-between");
|
|
779
|
-
_$setProp(_el$107, "gap", 2);
|
|
780
|
-
_$setProp(_el$108, "wrapMode", "word");
|
|
781
|
-
_$setProp(_el$108, "minWidth", 0);
|
|
782
|
-
_$setProp(_el$108, "flexGrow", 1);
|
|
783
|
-
_$insert(_el$108, () => model.model);
|
|
784
|
-
_$setProp(_el$109, "flexShrink", 0);
|
|
785
|
-
_$insert(_el$109, () => formatEstimatedCost(model.cost, model.costStatus) ?? "");
|
|
819
|
+
var _el$110 = _$createElement("box"),
|
|
820
|
+
_el$111 = _$createElement("box"),
|
|
821
|
+
_el$112 = _$createElement("text"),
|
|
822
|
+
_el$113 = _$createElement("text"),
|
|
823
|
+
_el$114 = _$createElement("text"),
|
|
824
|
+
_el$115 = _$createTextNode(` tokens · `);
|
|
786
825
|
_$insertNode(_el$110, _el$111);
|
|
787
|
-
_$
|
|
788
|
-
_$
|
|
789
|
-
_$
|
|
826
|
+
_$insertNode(_el$110, _el$114);
|
|
827
|
+
_$insertNode(_el$111, _el$112);
|
|
828
|
+
_$insertNode(_el$111, _el$113);
|
|
829
|
+
_$setProp(_el$111, "flexDirection", "row");
|
|
830
|
+
_$setProp(_el$111, "justifyContent", "space-between");
|
|
831
|
+
_$setProp(_el$111, "gap", 2);
|
|
832
|
+
_$setProp(_el$112, "wrapMode", "word");
|
|
833
|
+
_$setProp(_el$112, "minWidth", 0);
|
|
834
|
+
_$setProp(_el$112, "flexGrow", 1);
|
|
835
|
+
_$insert(_el$112, () => model.model);
|
|
836
|
+
_$setProp(_el$113, "flexShrink", 0);
|
|
837
|
+
_$insert(_el$113, () => formatEstimatedCost(model.cost, model.costStatus) ?? "");
|
|
838
|
+
_$insertNode(_el$114, _el$115);
|
|
839
|
+
_$insert(_el$114, () => number(model.tokens), _el$115);
|
|
840
|
+
_$insert(_el$114, () => countLabel(model.calls, "call"), null);
|
|
841
|
+
_$insert(_el$110, _$createComponent(Show, {
|
|
790
842
|
get when() {
|
|
791
843
|
return detailed();
|
|
792
844
|
},
|
|
@@ -796,52 +848,52 @@ function UsageDialog(props) {
|
|
|
796
848
|
return model.appliedRates;
|
|
797
849
|
},
|
|
798
850
|
children: rates => (() => {
|
|
799
|
-
var _el$
|
|
800
|
-
_el$
|
|
801
|
-
_el$
|
|
802
|
-
_el$
|
|
803
|
-
_$insertNode(_el$
|
|
804
|
-
_$setProp(_el$
|
|
805
|
-
_$insertNode(_el$
|
|
806
|
-
_$insertNode(_el$
|
|
807
|
-
_$setProp(_el$
|
|
808
|
-
_$insert(_el$
|
|
809
|
-
_$insert(_el$
|
|
810
|
-
_$insert(_el$
|
|
811
|
-
_$insert(_el$
|
|
812
|
-
_$effect(_$p => _$setProp(_el$
|
|
813
|
-
return _el$
|
|
851
|
+
var _el$118 = _$createElement("box"),
|
|
852
|
+
_el$119 = _$createElement("text"),
|
|
853
|
+
_el$120 = _$createTextNode(` · `),
|
|
854
|
+
_el$121 = _$createTextNode(` · `);
|
|
855
|
+
_$insertNode(_el$118, _el$119);
|
|
856
|
+
_$setProp(_el$118, "marginTop", 1);
|
|
857
|
+
_$insertNode(_el$119, _el$120);
|
|
858
|
+
_$insertNode(_el$119, _el$121);
|
|
859
|
+
_$setProp(_el$119, "wrapMode", "word");
|
|
860
|
+
_$insert(_el$119, () => rates.source, _el$120);
|
|
861
|
+
_$insert(_el$119, () => formatRateTier(rates), _el$121);
|
|
862
|
+
_$insert(_el$119, () => countLabel(rates.calls, "call"), null);
|
|
863
|
+
_$insert(_el$118, () => requestRowsList(rateRows(rates)), null);
|
|
864
|
+
_$effect(_$p => _$setProp(_el$119, "fg", theme().muted, _$p));
|
|
865
|
+
return _el$118;
|
|
814
866
|
})()
|
|
815
867
|
}), _$createComponent(Show, {
|
|
816
868
|
get when() {
|
|
817
869
|
return model.unpricedCalls > 0;
|
|
818
870
|
},
|
|
819
871
|
get children() {
|
|
820
|
-
var _el$
|
|
821
|
-
_el$
|
|
822
|
-
_$insertNode(_el$
|
|
823
|
-
_$setProp(_el$
|
|
824
|
-
_$insert(_el$
|
|
825
|
-
_$effect(_$p => _$setProp(_el$
|
|
826
|
-
return _el$
|
|
872
|
+
var _el$116 = _$createElement("text"),
|
|
873
|
+
_el$117 = _$createTextNode(`Pricing unavailable for `);
|
|
874
|
+
_$insertNode(_el$116, _el$117);
|
|
875
|
+
_$setProp(_el$116, "wrapMode", "word");
|
|
876
|
+
_$insert(_el$116, () => countLabel(model.unpricedCalls, "call"), null);
|
|
877
|
+
_$effect(_$p => _$setProp(_el$116, "fg", theme().muted, _$p));
|
|
878
|
+
return _el$116;
|
|
827
879
|
}
|
|
828
880
|
})];
|
|
829
881
|
}
|
|
830
882
|
}), null);
|
|
831
883
|
_$effect(_p$ => {
|
|
832
|
-
var _v$
|
|
833
|
-
_v$
|
|
834
|
-
_v$
|
|
835
|
-
_v$
|
|
836
|
-
_v$
|
|
837
|
-
_v$
|
|
884
|
+
var _v$31 = theme().base,
|
|
885
|
+
_v$32 = theme().base,
|
|
886
|
+
_v$33 = theme().muted;
|
|
887
|
+
_v$31 !== _p$.e && (_p$.e = _$setProp(_el$112, "fg", _v$31, _p$.e));
|
|
888
|
+
_v$32 !== _p$.t && (_p$.t = _$setProp(_el$113, "fg", _v$32, _p$.t));
|
|
889
|
+
_v$33 !== _p$.a && (_p$.a = _$setProp(_el$114, "fg", _v$33, _p$.a));
|
|
838
890
|
return _p$;
|
|
839
891
|
}, {
|
|
840
892
|
e: undefined,
|
|
841
893
|
t: undefined,
|
|
842
894
|
a: undefined
|
|
843
895
|
});
|
|
844
|
-
return _el$
|
|
896
|
+
return _el$110;
|
|
845
897
|
})()
|
|
846
898
|
});
|
|
847
899
|
}
|
|
@@ -909,6 +961,15 @@ export default Plugin.define({
|
|
|
909
961
|
const performance = new PerformanceMonitor(listener => context.data.listen(({
|
|
910
962
|
details
|
|
911
963
|
}) => listener(details)));
|
|
964
|
+
const runtime = new RuntimeMonitor(listener => context.data.listen(({
|
|
965
|
+
details
|
|
966
|
+
}) => listener(details)));
|
|
967
|
+
const synchronizeRuntime = () => void runtime.synchronize(signal => context.client.rpc(ContextSourceRpc).clock({}, {
|
|
968
|
+
location: context.location ?? context.data.location.default(),
|
|
969
|
+
signal
|
|
970
|
+
}));
|
|
971
|
+
synchronizeRuntime();
|
|
972
|
+
const stopClockSync = context.data.on("server.connected", synchronizeRuntime);
|
|
912
973
|
const controllers = new Set();
|
|
913
974
|
const register = controller => {
|
|
914
975
|
controllers.add(controller);
|
|
@@ -916,6 +977,19 @@ export default Plugin.define({
|
|
|
916
977
|
controllers.delete(controller);
|
|
917
978
|
};
|
|
918
979
|
};
|
|
980
|
+
const openUsage = sessionID => {
|
|
981
|
+
context.ui.dialog.show(() => _$createComponent(UsageDialog, {
|
|
982
|
+
context: context,
|
|
983
|
+
sessionID: sessionID,
|
|
984
|
+
performance: performance,
|
|
985
|
+
runtime: runtime,
|
|
986
|
+
register: register
|
|
987
|
+
}));
|
|
988
|
+
context.ui.dialog.set({
|
|
989
|
+
size: "large",
|
|
990
|
+
centered: true
|
|
991
|
+
});
|
|
992
|
+
};
|
|
919
993
|
const remove = context.ui.slot({
|
|
920
994
|
append: "sidebar.content",
|
|
921
995
|
render: props => _$createComponent(UsagePanel, {
|
|
@@ -924,10 +998,12 @@ export default Plugin.define({
|
|
|
924
998
|
return props.sessionID;
|
|
925
999
|
},
|
|
926
1000
|
performance: performance,
|
|
927
|
-
|
|
1001
|
+
runtime: runtime,
|
|
1002
|
+
register: register,
|
|
1003
|
+
open: openUsage
|
|
928
1004
|
})
|
|
929
1005
|
});
|
|
930
|
-
const openChildUsage =
|
|
1006
|
+
const openChildUsage = state => {
|
|
931
1007
|
context.ui.dialog.show(() => _$createComponent(ChildUsageDialog, {
|
|
932
1008
|
context: context,
|
|
933
1009
|
state: state
|
|
@@ -951,6 +1027,7 @@ export default Plugin.define({
|
|
|
951
1027
|
return props.sessionID;
|
|
952
1028
|
},
|
|
953
1029
|
performance: performance,
|
|
1030
|
+
runtime: runtime,
|
|
954
1031
|
register: register,
|
|
955
1032
|
open: openChildUsage
|
|
956
1033
|
});
|
|
@@ -974,18 +1051,7 @@ export default Plugin.define({
|
|
|
974
1051
|
run: () => {
|
|
975
1052
|
const route = context.ui.router.current();
|
|
976
1053
|
if (route.type !== "session") return;
|
|
977
|
-
|
|
978
|
-
context: context,
|
|
979
|
-
get sessionID() {
|
|
980
|
-
return route.sessionID;
|
|
981
|
-
},
|
|
982
|
-
performance: performance,
|
|
983
|
-
register: register
|
|
984
|
-
}));
|
|
985
|
-
context.ui.dialog.set({
|
|
986
|
-
size: "large",
|
|
987
|
-
centered: true
|
|
988
|
-
});
|
|
1054
|
+
openUsage(route.sessionID);
|
|
989
1055
|
}
|
|
990
1056
|
}]
|
|
991
1057
|
}));
|
|
@@ -999,6 +1065,8 @@ export default Plugin.define({
|
|
|
999
1065
|
removeChild();
|
|
1000
1066
|
removeCommand();
|
|
1001
1067
|
performance.dispose();
|
|
1068
|
+
runtime.dispose();
|
|
1069
|
+
stopClockSync();
|
|
1002
1070
|
};
|
|
1003
1071
|
}
|
|
1004
1072
|
});
|
package/dist/usage.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PerformanceSummary } from "./performance.js";
|
|
2
|
+
import type { RuntimeSummary } from "./runtime.js";
|
|
2
3
|
export interface Tokens {
|
|
3
4
|
input: number;
|
|
4
5
|
output: number;
|
|
@@ -42,6 +43,9 @@ export interface UsageMessage {
|
|
|
42
43
|
id: string;
|
|
43
44
|
type: string;
|
|
44
45
|
status?: string;
|
|
46
|
+
finish?: string;
|
|
47
|
+
error?: unknown;
|
|
48
|
+
retry?: unknown;
|
|
45
49
|
model?: ModelRef;
|
|
46
50
|
cost?: number;
|
|
47
51
|
tokens?: TokenInput;
|
|
@@ -137,6 +141,12 @@ export interface RequestDetails {
|
|
|
137
141
|
export declare function requestDetails(messages: readonly UsageMessage[], revertMessageID?: string): RequestDetails | undefined;
|
|
138
142
|
export declare function contextUsage(messages: readonly UsageMessage[], limit?: number, revertMessageID?: string): ContextUsage | undefined;
|
|
139
143
|
export declare function formatTokens(value: number): string;
|
|
144
|
+
/** Native sub-minute precision; longer durations keep compact, readable units. */
|
|
145
|
+
export declare function formatDuration(milliseconds: number, compact?: boolean): string;
|
|
146
|
+
/** Hide confirmed zero totals, but preserve unavailable and stale timing. */
|
|
147
|
+
export declare function formatRunTime(runtime?: RuntimeSummary, compact?: boolean): string | undefined;
|
|
148
|
+
/** Preserve session-local time on narrow terminals without adding composer height. */
|
|
149
|
+
export declare function fitUsageSummary(fields: readonly (readonly [string, string | undefined])[], width: number, stale?: boolean): string;
|
|
140
150
|
export declare const formatCost: (value: number) => string;
|
|
141
151
|
export declare function formatEstimatedCost(cost: number, status: CostStatus): string | undefined;
|
|
142
152
|
/** Per-million rates need more precision than a rounded total cost. */
|
|
@@ -159,4 +169,4 @@ export declare function requestRows(details: RequestDetails, compact: boolean):
|
|
|
159
169
|
export declare function summaryRows(summary: Summary): readonly (readonly [string, string])[];
|
|
160
170
|
/** `1 step` reads better than `1 steps` in the dialog's single-line summaries. */
|
|
161
171
|
export declare const countLabel: (value: number, noun: string) => string;
|
|
162
|
-
export declare function usageRows(summary?: Summary, context?: ContextUsage, performance?: PerformanceSummary): readonly (readonly [string, string])[];
|
|
172
|
+
export declare function usageRows(summary?: Summary, context?: ContextUsage, performance?: PerformanceSummary, runtime?: RuntimeSummary): readonly (readonly [string, string])[];
|
package/dist/usage.js
CHANGED
|
@@ -191,7 +191,63 @@ export function contextUsage(messages, limit, revertMessageID) {
|
|
|
191
191
|
export function formatTokens(value) {
|
|
192
192
|
return Math.round(safe(value)).toLocaleString("en-US");
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
/** Native sub-minute precision; longer durations keep compact, readable units. */
|
|
195
|
+
export function formatDuration(milliseconds, compact = false) {
|
|
196
|
+
if (!Number.isFinite(milliseconds) || milliseconds < 0)
|
|
197
|
+
return "—";
|
|
198
|
+
if (milliseconds === 0)
|
|
199
|
+
return "0s";
|
|
200
|
+
if (milliseconds < 1_000)
|
|
201
|
+
return `${milliseconds}ms`;
|
|
202
|
+
if (milliseconds < 60_000)
|
|
203
|
+
return `${(milliseconds / 1_000).toFixed(1)}s`;
|
|
204
|
+
const seconds = Math.floor(milliseconds / 1_000);
|
|
205
|
+
const hours = Math.floor(seconds / 3_600), minutes = Math.floor(seconds / 60) % 60, rest = seconds % 60;
|
|
206
|
+
if (hours > 0)
|
|
207
|
+
return compact ? `${hours}h${minutes}m${rest}s` : `${hours}h ${String(minutes).padStart(2, "0")}m ${String(rest).padStart(2, "0")}s`;
|
|
208
|
+
if (minutes > 0)
|
|
209
|
+
return compact ? `${minutes}m${rest}s` : `${minutes}m ${String(rest).padStart(2, "0")}s`;
|
|
210
|
+
return `${rest}s`;
|
|
211
|
+
}
|
|
212
|
+
/** Hide confirmed zero totals, but preserve unavailable and stale timing. */
|
|
213
|
+
export function formatRunTime(runtime, compact = false) {
|
|
214
|
+
if (runtime?.milliseconds === undefined)
|
|
215
|
+
return "—";
|
|
216
|
+
const estimated = runtime.estimatedMilliseconds !== undefined;
|
|
217
|
+
const milliseconds = runtime.estimatedMilliseconds ?? runtime.milliseconds;
|
|
218
|
+
if (runtime.status === "ready" && milliseconds === 0)
|
|
219
|
+
return undefined;
|
|
220
|
+
const duration = formatDuration(milliseconds, compact);
|
|
221
|
+
const time = estimated && duration !== "—" ? `~${duration}` : duration;
|
|
222
|
+
return runtime.status === "stale" ? `${time} · ${compact ? "stale" : "Not updated"}` : time;
|
|
223
|
+
}
|
|
224
|
+
/** Preserve session-local time on narrow terminals without adding composer height. */
|
|
225
|
+
export function fitUsageSummary(fields, width, stale = false) {
|
|
226
|
+
let visible = fields.filter((field) => field[1] !== undefined);
|
|
227
|
+
const suffix = stale ? " · Not updated" : "";
|
|
228
|
+
const render = (title) => `${[title, ...visible.map(([label, value]) => `${label} ${value}`)].join(" · ")}${suffix}`;
|
|
229
|
+
for (const label of ["TPS", "Context", "Cost", "Total"]) {
|
|
230
|
+
if (render("Token Usage").length <= width)
|
|
231
|
+
return render("Token Usage");
|
|
232
|
+
// Keep a useful metric even when the zero-time field is hidden.
|
|
233
|
+
if (visible.length > 1)
|
|
234
|
+
visible = visible.filter(([name]) => name !== label);
|
|
235
|
+
}
|
|
236
|
+
if (render("Token Usage").length <= width)
|
|
237
|
+
return render("Token Usage");
|
|
238
|
+
if (render("Usage").length <= width)
|
|
239
|
+
return render("Usage");
|
|
240
|
+
// Remove the title before clipping any value; retain time when it is present.
|
|
241
|
+
return `${visible.map(([label, value]) => `${label} ${value}`).join(" · ")}${stale ? " · stale" : ""}`.slice(0, Math.max(0, width));
|
|
242
|
+
}
|
|
243
|
+
export const formatCost = (value) => {
|
|
244
|
+
const amount = safe(value);
|
|
245
|
+
if (amount === 0)
|
|
246
|
+
return "$0.00";
|
|
247
|
+
if (amount < 0.001)
|
|
248
|
+
return "<$0.001";
|
|
249
|
+
return `$${amount.toFixed(amount > 0.01 ? 2 : 3)}`;
|
|
250
|
+
};
|
|
195
251
|
export function formatEstimatedCost(cost, status) {
|
|
196
252
|
if (status === "empty")
|
|
197
253
|
return undefined;
|
|
@@ -268,7 +324,7 @@ export function summaryRows(summary) {
|
|
|
268
324
|
}
|
|
269
325
|
/** `1 step` reads better than `1 steps` in the dialog's single-line summaries. */
|
|
270
326
|
export const countLabel = (value, noun) => `${formatTokens(value)} ${noun}${value === 1 ? "" : "s"}`;
|
|
271
|
-
export function usageRows(summary, context, performance) {
|
|
327
|
+
export function usageRows(summary, context, performance, runtime) {
|
|
272
328
|
const t = summary?.tokens;
|
|
273
329
|
const number = (value) => value === undefined ? "—" : formatTokens(value);
|
|
274
330
|
const rows = [];
|
|
@@ -287,5 +343,8 @@ export function usageRows(summary, context, performance) {
|
|
|
287
343
|
if (performance?.ttft !== undefined && Number.isFinite(performance.ttft)) {
|
|
288
344
|
rows.push(["TTFT", `${(Math.max(0, performance.ttft) / 1_000).toFixed(1)}s`]);
|
|
289
345
|
}
|
|
346
|
+
const time = runtime && formatRunTime(runtime);
|
|
347
|
+
if (time !== undefined)
|
|
348
|
+
rows.push(["Run Time", time]);
|
|
290
349
|
return rows;
|
|
291
350
|
}
|