@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/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 rows = createMemo(() => usageRows(props.state().summary, props.state().context, props.state().performance));
20
- const performanceStart = createMemo(() => rows().findIndex(([label]) => label === "TPS" || label === "TTFT"));
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(() => controller.select(props.sessionID));
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(() => controller.select(props.sessionID));
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
- const stale = current.status === "stale" ? " · Not updated" : "";
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(props.sessionID, state);
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$104 = _$createElement("text");
756
- _$insertNode(_el$104, _$createTextNode(`No model usage yet`));
757
- _$effect(_$p => _$setProp(_el$104, "fg", theme().muted, _$p));
758
- return _el$104;
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$106 = _$createElement("box"),
768
- _el$107 = _$createElement("box"),
769
- _el$108 = _$createElement("text"),
770
- _el$109 = _$createElement("text"),
771
- _el$110 = _$createElement("text"),
772
- _el$111 = _$createTextNode(` tokens · `);
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
- _$insert(_el$110, () => number(model.tokens), _el$111);
788
- _$insert(_el$110, () => countLabel(model.calls, "call"), null);
789
- _$insert(_el$106, _$createComponent(Show, {
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$114 = _$createElement("box"),
800
- _el$115 = _$createElement("text"),
801
- _el$116 = _$createTextNode(` · `),
802
- _el$117 = _$createTextNode(` · `);
803
- _$insertNode(_el$114, _el$115);
804
- _$setProp(_el$114, "marginTop", 1);
805
- _$insertNode(_el$115, _el$116);
806
- _$insertNode(_el$115, _el$117);
807
- _$setProp(_el$115, "wrapMode", "word");
808
- _$insert(_el$115, () => rates.source, _el$116);
809
- _$insert(_el$115, () => formatRateTier(rates), _el$117);
810
- _$insert(_el$115, () => countLabel(rates.calls, "call"), null);
811
- _$insert(_el$114, () => requestRowsList(rateRows(rates)), null);
812
- _$effect(_$p => _$setProp(_el$115, "fg", theme().muted, _$p));
813
- return _el$114;
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$112 = _$createElement("text"),
821
- _el$113 = _$createTextNode(`Pricing unavailable for `);
822
- _$insertNode(_el$112, _el$113);
823
- _$setProp(_el$112, "wrapMode", "word");
824
- _$insert(_el$112, () => countLabel(model.unpricedCalls, "call"), null);
825
- _$effect(_$p => _$setProp(_el$112, "fg", theme().muted, _$p));
826
- return _el$112;
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$29 = theme().base,
833
- _v$30 = theme().base,
834
- _v$31 = theme().muted;
835
- _v$29 !== _p$.e && (_p$.e = _$setProp(_el$108, "fg", _v$29, _p$.e));
836
- _v$30 !== _p$.t && (_p$.t = _$setProp(_el$109, "fg", _v$30, _p$.t));
837
- _v$31 !== _p$.a && (_p$.a = _$setProp(_el$110, "fg", _v$31, _p$.a));
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$106;
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
- register: register
1001
+ runtime: runtime,
1002
+ register: register,
1003
+ open: openUsage
928
1004
  })
929
1005
  });
930
- const openChildUsage = (sessionID, state) => {
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
- context.ui.dialog.show(() => _$createComponent(UsageDialog, {
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
- export const formatCost = (value) => value > 0 && value < 0.01 ? "<$0.01" : `$${safe(value).toFixed(2)}`;
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
  }