@mohndoe/pi-atlas 0.1.1 → 0.1.3
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 +96 -19
- package/bunfig.toml +37 -0
- package/media/screenshot.png +0 -0
- package/package.json +4 -3
- package/src/__tests__/e2e.test.ts +3 -3
- package/src/{__tests__/cache.test.ts → cache.test.ts} +311 -10
- package/src/cache.ts +36 -3
- package/src/components/{__tests__/BarChart.test.ts → BarChart.test.ts} +9 -9
- package/src/components/{__tests__/Dashboard.test.ts → Dashboard.test.ts} +4 -4
- package/src/components/Dashboard.ts +2 -1
- package/src/components/{__tests__/KpiCards.test.ts → KpiCards.test.ts} +5 -5
- package/src/components/KpiCards.ts +1 -1
- package/src/components/LoadingView.test.ts +116 -0
- package/src/components/LoadingView.ts +87 -25
- package/src/components/{__tests__/MarqueeText.test.ts → MarqueeText.test.ts} +2 -2
- package/src/components/{__tests__/RangeSelector.test.ts → RangeSelector.test.ts} +2 -2
- package/src/components/{__tests__/RankedBarList.test.ts → RankedBarList.test.ts} +2 -2
- package/src/components/{__tests__/SortedTable.test.ts → SortedTable.test.ts} +3 -4
- package/src/components/{__tests__/TabBar.test.ts → TabBar.test.ts} +2 -2
- package/src/components/__tests__/SortedTable.integration.test.ts +5 -8
- package/src/components/{__tests__/cells.test.ts → cells.test.ts} +2 -2
- package/src/{__tests__ → components}/components.fixtures.ts +1 -1
- package/src/components/shared/Bar.ts +10 -2
- package/src/{__tests__/compute.fixtures.ts → compute.fixtures.ts} +6 -1
- package/src/{__tests__/compute.test.ts → compute.test.ts} +135 -3
- package/src/compute.ts +24 -4
- package/src/{__tests__/format.test.ts → format.test.ts} +173 -31
- package/src/format.ts +20 -7
- package/src/index.ts +23 -20
- package/src/{__tests__/parser.test.ts → parser.test.ts} +339 -109
- package/src/parser.ts +1 -1
- package/src/tabs/{__tests__/Languages.test.ts → Languages.test.ts} +3 -7
- package/src/tabs/Languages.ts +7 -3
- package/src/tabs/{__tests__/Models.test.ts → Models.test.ts} +3 -6
- package/src/tabs/Models.ts +2 -4
- package/src/tabs/{__tests__/Overview.test.ts → Overview.test.ts} +18 -15
- package/src/tabs/Overview.ts +50 -39
- package/src/tabs/{__tests__/Projects.test.ts → Projects.test.ts} +5 -8
- package/src/tabs/Projects.ts +9 -4
- package/src/tabs/{__tests__/Usage.test.ts → Usage.test.ts} +8 -18
- package/src/tabs/Usage.ts +7 -3
- package/src/types.ts +11 -0
- package/src/components/__tests__/LoadingView.test.ts +0 -26
- /package/src/components/{__tests__ → shared}/Bar.test.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
-
import { makeMockTUI, makeTheme, testPalette } from "
|
|
3
|
-
import { type ModelStat } from "
|
|
4
|
-
import { Models } from "
|
|
2
|
+
import { makeMockTUI, makeTheme, testPalette } from "../components/components.fixtures";
|
|
3
|
+
import { type ModelStat } from "../types";
|
|
4
|
+
import { Models } from "./Models";
|
|
5
5
|
|
|
6
6
|
describe("Models", () => {
|
|
7
7
|
const mockTui = makeMockTUI();
|
|
@@ -19,7 +19,6 @@ describe("Models", () => {
|
|
|
19
19
|
const text = lines.join("\n");
|
|
20
20
|
|
|
21
21
|
expect(lines[0]).toContain("Models");
|
|
22
|
-
expect(lines[0]).toContain(models.length.toString());
|
|
23
22
|
|
|
24
23
|
// formatModelName strips date suffix and capitalizes
|
|
25
24
|
// Model column is 6-char fill at width 80 — truncated name visible
|
|
@@ -41,8 +40,6 @@ describe("Models", () => {
|
|
|
41
40
|
const text = lines.join("\n");
|
|
42
41
|
|
|
43
42
|
expect(lines[0]).toContain("Models");
|
|
44
|
-
// don't display 0 counter
|
|
45
|
-
expect(lines[0]).not.toContain("0");
|
|
46
43
|
expect(text).toContain("No model data for this time range");
|
|
47
44
|
});
|
|
48
45
|
|
package/src/tabs/Models.ts
CHANGED
|
@@ -36,10 +36,8 @@ export class Models extends Container {
|
|
|
36
36
|
const totalCost = this.models.reduce((sum, item) => sum + item.cost, 0);
|
|
37
37
|
const maxCost = Math.max(...this.models.map((m) => m.cost), 0);
|
|
38
38
|
this.rows = this.models.map((m) => {
|
|
39
|
-
let pct = 0;
|
|
40
39
|
let barPct = 0;
|
|
41
40
|
if (totalCost > 0) {
|
|
42
|
-
pct = (m.cost * 100) / totalCost;
|
|
43
41
|
barPct = maxCost > 0 ? (m.cost / maxCost) * 100 : 0;
|
|
44
42
|
}
|
|
45
43
|
return [
|
|
@@ -62,8 +60,8 @@ export class Models extends Container {
|
|
|
62
60
|
const bb = new BorderBox({
|
|
63
61
|
...baseBorderBoxOptions,
|
|
64
62
|
titles: [
|
|
65
|
-
{ text: "Models", align: "left" },
|
|
66
|
-
{ text: this.theme.fg("
|
|
63
|
+
{ text: this.theme.bold("Models"), align: "left" },
|
|
64
|
+
{ text: this.theme.fg("muted", "by cost"), align: "right" },
|
|
67
65
|
],
|
|
68
66
|
});
|
|
69
67
|
if (!this.table) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import { makeTheme } from "../components/components.fixtures";
|
|
4
|
+
import { makeSummary } from "../compute.fixtures";
|
|
5
|
+
import { type StatsSummary } from "../types";
|
|
6
|
+
import { Overview } from "./Overview";
|
|
6
7
|
|
|
7
8
|
describe("Overview", () => {
|
|
8
9
|
const mockSummary: StatsSummary = {
|
|
@@ -24,7 +25,7 @@ describe("Overview", () => {
|
|
|
24
25
|
],
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
it("renders KpiCards followed by
|
|
28
|
+
it("renders KpiCards followed by BarChart", () => {
|
|
28
29
|
const overview = new Overview(mockSummary, "7d", makeTheme(), 15);
|
|
29
30
|
const lines = overview.render(80);
|
|
30
31
|
|
|
@@ -43,14 +44,12 @@ describe("Overview", () => {
|
|
|
43
44
|
const kpiCostIdx = lines.findIndex(
|
|
44
45
|
(l) => l.includes("12.34") || l.includes("$12.34") || l.includes("Total"),
|
|
45
46
|
);
|
|
46
|
-
const spacerIdx = lines.findIndex((l) => l.trim() === "");
|
|
47
|
-
expect(spacerIdx).toBeGreaterThan(kpiCostIdx);
|
|
48
47
|
|
|
49
|
-
// Chart content (█ or label) should appear after the
|
|
48
|
+
// Chart content (█ or label) should appear after the KPIs
|
|
50
49
|
const chartIdx = lines.findIndex(
|
|
51
|
-
(l
|
|
50
|
+
(l) => l.includes("█") || l.includes("No data") || l.includes("Mon"),
|
|
52
51
|
);
|
|
53
|
-
expect(chartIdx).toBeGreaterThan(
|
|
52
|
+
expect(chartIdx).toBeGreaterThan(kpiCostIdx);
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
it("adapts bar chart height to available space after KpiCards", () => {
|
|
@@ -61,8 +60,6 @@ describe("Overview", () => {
|
|
|
61
60
|
// Chart should still render (not zero lines)
|
|
62
61
|
const text = lines.join("\n");
|
|
63
62
|
expect(text).toContain("█");
|
|
64
|
-
// Should have spacer before chart
|
|
65
|
-
expect(lines.some((l) => l.trim() === "")).toBe(true);
|
|
66
63
|
});
|
|
67
64
|
|
|
68
65
|
it("shows 'No data' when daily spend is empty", () => {
|
|
@@ -81,12 +78,18 @@ describe("Overview", () => {
|
|
|
81
78
|
|
|
82
79
|
it("invalidate clears cache and re-renders at new width", () => {
|
|
83
80
|
const overview = new Overview(mockSummary, "7d", makeTheme(), 15);
|
|
84
|
-
|
|
81
|
+
|
|
82
|
+
const linesBefore = overview.render(80);
|
|
83
|
+
for (const line of linesBefore) {
|
|
84
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(80);
|
|
85
|
+
expect(visibleWidth(line)).toBeGreaterThanOrEqual(78);
|
|
86
|
+
}
|
|
85
87
|
overview.invalidate();
|
|
86
88
|
|
|
87
89
|
const lines = overview.render(60);
|
|
88
90
|
for (const line of lines) {
|
|
89
|
-
expect(line
|
|
91
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(60);
|
|
92
|
+
expect(visibleWidth(line)).toBeGreaterThanOrEqual(58);
|
|
90
93
|
}
|
|
91
94
|
});
|
|
92
95
|
});
|
package/src/tabs/Overview.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { BarChart } from "../components/BarChart";
|
|
|
6
6
|
import { KpiCards, type KpiData } from "../components/KpiCards";
|
|
7
7
|
import { GridRow } from "../components/shared/GridRow";
|
|
8
8
|
import { StatCard } from "../components/StatCard";
|
|
9
|
-
import { formatCost, formatNumber } from "../format";
|
|
9
|
+
import { formatCost, formatModelName, formatNumber } from "../format";
|
|
10
10
|
import { type StatsSummary, type TimeRange } from "../types";
|
|
11
11
|
|
|
12
12
|
const SPACER_HEIGHT = 1;
|
|
@@ -39,65 +39,76 @@ export class Overview extends Container {
|
|
|
39
39
|
const topProject = summary.projects[0];
|
|
40
40
|
|
|
41
41
|
const langBox = new BorderBox({
|
|
42
|
+
borderStyle: "heavy",
|
|
42
43
|
titles: [{ text: this.theme.bold("Top Language"), align: "left" }],
|
|
44
|
+
footers: topLanguage
|
|
45
|
+
? [
|
|
46
|
+
{
|
|
47
|
+
text: this.theme.fg("muted", formatNumber(topLanguage.lines) + " ln"),
|
|
48
|
+
align: "right",
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
: [],
|
|
43
52
|
padding: { left: 1, right: 1 },
|
|
44
53
|
borderFn: topLanguage
|
|
45
54
|
? langPalette.getColor(topLanguage.language)
|
|
46
55
|
: (s: string) => this.theme.fg("borderMuted", s),
|
|
47
56
|
});
|
|
48
57
|
langBox.addChild(
|
|
49
|
-
|
|
50
|
-
?
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
text: this.theme.bold(formatNumber(topLanguage.lines) + " lines"),
|
|
55
|
-
color: "text",
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
this.theme,
|
|
59
|
-
)
|
|
60
|
-
: new Text("No data"),
|
|
58
|
+
new Text(
|
|
59
|
+
topLanguage ? this.theme.fg("text", topLanguage.language) : this.theme.fg("dim", "No data"),
|
|
60
|
+
0,
|
|
61
|
+
0,
|
|
62
|
+
),
|
|
61
63
|
);
|
|
62
64
|
|
|
63
65
|
const modelBox = new BorderBox({
|
|
66
|
+
borderStyle: "heavy",
|
|
64
67
|
titles: [{ text: this.theme.bold("Top model"), align: "left" }],
|
|
68
|
+
footers: topModel
|
|
69
|
+
? [
|
|
70
|
+
{
|
|
71
|
+
text: this.theme.fg("muted", formatCost(topModel.cost)),
|
|
72
|
+
align: "right",
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
: [],
|
|
65
76
|
padding: { left: 1, right: 1 },
|
|
66
|
-
borderFn:
|
|
77
|
+
borderFn: topModel
|
|
78
|
+
? modelPalette.getColor(topModel.provider || "")
|
|
79
|
+
: (s: string) => this.theme.fg("borderMuted", s),
|
|
67
80
|
});
|
|
68
81
|
modelBox.addChild(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
this.theme,
|
|
79
|
-
)
|
|
80
|
-
: new Text("No data."),
|
|
82
|
+
new Text(
|
|
83
|
+
topModel
|
|
84
|
+
? this.theme.fg("text", formatModelName(topModel.model))
|
|
85
|
+
: this.theme.fg("dim", "No data"),
|
|
86
|
+
0,
|
|
87
|
+
0,
|
|
88
|
+
),
|
|
81
89
|
);
|
|
82
90
|
|
|
83
91
|
const projectBox = new BorderBox({
|
|
92
|
+
borderStyle: "heavy",
|
|
84
93
|
titles: [{ text: this.theme.bold("Top project"), align: "left" }],
|
|
94
|
+
footers: topProject
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
text: this.theme.fg("muted", formatCost(topProject.cost)),
|
|
98
|
+
align: "right",
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: [],
|
|
85
102
|
padding: { left: 1, right: 1 },
|
|
86
|
-
borderFn: (s: string) => this.theme.fg("
|
|
103
|
+
borderFn: (s: string) => this.theme.fg("text", s),
|
|
87
104
|
});
|
|
105
|
+
|
|
88
106
|
projectBox.addChild(
|
|
89
|
-
|
|
90
|
-
?
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
text: this.theme.bold(formatCost(topProject.cost)),
|
|
95
|
-
color: "text",
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
this.theme,
|
|
99
|
-
)
|
|
100
|
-
: new Text("No data."),
|
|
107
|
+
new Text(
|
|
108
|
+
topProject ? this.theme.fg("text", topProject.project) : this.theme.fg("dim", "No data"),
|
|
109
|
+
0,
|
|
110
|
+
0,
|
|
111
|
+
),
|
|
101
112
|
);
|
|
102
113
|
|
|
103
114
|
this.topCards = new GridRow([langBox, modelBox, projectBox], [33, 33, 34]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
-
import { makeMockTUI, makeTheme } from "
|
|
3
|
-
import { type ProjectStat } from "
|
|
4
|
-
import { Projects } from "
|
|
2
|
+
import { makeMockTUI, makeTheme } from "../components/components.fixtures";
|
|
3
|
+
import { type ProjectStat } from "../types";
|
|
4
|
+
import { Projects } from "./Projects";
|
|
5
5
|
|
|
6
6
|
describe("Projects", () => {
|
|
7
7
|
const mockTui = makeMockTUI();
|
|
@@ -18,7 +18,6 @@ describe("Projects", () => {
|
|
|
18
18
|
const text = lines.join("\n");
|
|
19
19
|
|
|
20
20
|
expect(lines[0]).toContain("Projects");
|
|
21
|
-
expect(lines[0]).toContain(projects.length.toString());
|
|
22
21
|
|
|
23
22
|
// Headers
|
|
24
23
|
expect(text).toContain("Name");
|
|
@@ -37,8 +36,8 @@ describe("Projects", () => {
|
|
|
37
36
|
expect(text).toContain("5");
|
|
38
37
|
|
|
39
38
|
// Costs formatted
|
|
40
|
-
expect(text).toContain("$15.
|
|
41
|
-
expect(text).toContain("$8.
|
|
39
|
+
expect(text).toContain("$15.5");
|
|
40
|
+
expect(text).toContain("$8.2");
|
|
42
41
|
expect(text).toContain("$1.25");
|
|
43
42
|
});
|
|
44
43
|
|
|
@@ -48,8 +47,6 @@ describe("Projects", () => {
|
|
|
48
47
|
const text = lines.join("\n");
|
|
49
48
|
|
|
50
49
|
expect(lines[0]).toContain("Projects");
|
|
51
|
-
// don't display 0 counter
|
|
52
|
-
expect(lines[0]).not.toContain("0");
|
|
53
50
|
expect(text).toContain("No projects data for this time range");
|
|
54
51
|
});
|
|
55
52
|
|
package/src/tabs/Projects.ts
CHANGED
|
@@ -29,9 +29,14 @@ export class Projects extends Container {
|
|
|
29
29
|
/** Build row cells once in constructor. Data is stable per Projects instance. */
|
|
30
30
|
private buildRows(): void {
|
|
31
31
|
if (this.isEmpty) return;
|
|
32
|
+
|
|
33
|
+
const totalCost = this.projects.reduce((sum, item) => sum + item.cost, 0);
|
|
32
34
|
const maxCost = Math.max(...this.projects.map((p) => p.cost), 0);
|
|
33
35
|
this.rows = this.projects.map((p) => {
|
|
34
|
-
|
|
36
|
+
let barPct = 0;
|
|
37
|
+
if (totalCost > 0) {
|
|
38
|
+
barPct = maxCost > 0 ? (p.cost / maxCost) * 100 : 0;
|
|
39
|
+
}
|
|
35
40
|
return [
|
|
36
41
|
cell.marquee(p.project, this.tui),
|
|
37
42
|
cell.bar(barPct, (s) => s, "transparent"),
|
|
@@ -47,13 +52,13 @@ export class Projects extends Container {
|
|
|
47
52
|
const borderBoxOptions: BorderBoxOptions = {
|
|
48
53
|
borderStyle: "singleRounded",
|
|
49
54
|
borderFn: (s) => this.theme.fg("border", s),
|
|
50
|
-
titles: [{ text: "Projects", align: "left" }],
|
|
55
|
+
titles: [{ text: this.theme.bold("Projects"), align: "left" }],
|
|
51
56
|
};
|
|
52
57
|
let borderBox = new BorderBox(borderBoxOptions);
|
|
53
58
|
if (!this.isEmpty) {
|
|
54
59
|
borderBoxOptions.titles = [
|
|
55
60
|
...borderBoxOptions.titles!,
|
|
56
|
-
{ text: this.theme.fg("
|
|
61
|
+
{ text: this.theme.fg("muted", "by cost"), align: "right" },
|
|
57
62
|
];
|
|
58
63
|
if (!this.table) {
|
|
59
64
|
this.table = new SortedTable(
|
|
@@ -62,7 +67,7 @@ export class Projects extends Container {
|
|
|
62
67
|
{ header: cell.header("Name"), width: 20 },
|
|
63
68
|
{ header: cell.header("Share %"), width: "fill" },
|
|
64
69
|
{ header: cell.header("Sessions"), width: 14 },
|
|
65
|
-
{ header: cell.header("Cost"), width:
|
|
70
|
+
{ header: cell.header("Cost"), width: 10 },
|
|
66
71
|
],
|
|
67
72
|
rows: this.rows,
|
|
68
73
|
maxHeight: this.maxHeight,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
-
import { makeMockTUI, makeTheme } from "
|
|
3
|
-
import {
|
|
4
|
-
import { Usage } from "
|
|
2
|
+
import { makeMockTUI, makeTheme } from "../components/components.fixtures";
|
|
3
|
+
import type { ToolStat } from "../types";
|
|
4
|
+
import { Usage } from "./Usage";
|
|
5
5
|
|
|
6
6
|
describe("Usage", () => {
|
|
7
7
|
const mockTui = makeMockTUI();
|
|
@@ -25,11 +25,11 @@ describe("Usage", () => {
|
|
|
25
25
|
const text = tab.render(80).join("\n");
|
|
26
26
|
|
|
27
27
|
expect(text).toContain("Tokens");
|
|
28
|
-
expect(text).toContain("
|
|
28
|
+
expect(text).toContain("10k");
|
|
29
29
|
expect(text).toContain("Input");
|
|
30
|
-
expect(text).toContain("
|
|
30
|
+
expect(text).toContain("5k");
|
|
31
31
|
expect(text).toContain("Output");
|
|
32
|
-
expect(text).toContain("
|
|
32
|
+
expect(text).toContain("4k");
|
|
33
33
|
expect(text).toContain("Cache Read");
|
|
34
34
|
expect(text).toContain("500");
|
|
35
35
|
expect(text).toContain("Cache Write");
|
|
@@ -42,7 +42,6 @@ describe("Usage", () => {
|
|
|
42
42
|
const text = lines.join("\n");
|
|
43
43
|
|
|
44
44
|
expect(lines[0]).toContain("Tools");
|
|
45
|
-
expect(lines[0]).toContain(tools.length.toString());
|
|
46
45
|
|
|
47
46
|
// Headers
|
|
48
47
|
expect(text).toContain("Command");
|
|
@@ -60,21 +59,12 @@ describe("Usage", () => {
|
|
|
60
59
|
expect(text).toContain("45");
|
|
61
60
|
});
|
|
62
61
|
|
|
63
|
-
it("does NOT show 'Tool Calls' title", () => {
|
|
64
|
-
const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10);
|
|
65
|
-
const text = tab.render(80).join("\n");
|
|
66
|
-
|
|
67
|
-
expect(text).not.toContain("Tool Calls");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
62
|
it("shows empty state when tools is empty", () => {
|
|
71
63
|
const tab = new Usage([], tokenUsage, makeTheme(), mockTui, 10);
|
|
72
64
|
const lines = tab.render(80).slice(4);
|
|
73
65
|
const text = lines.join("\n");
|
|
74
66
|
|
|
75
67
|
expect(lines[0]).toContain("Tools");
|
|
76
|
-
// don't display 0 counter
|
|
77
|
-
expect(lines[0]).not.toContain("0");
|
|
78
68
|
expect(text).toContain("No tools data for this time range");
|
|
79
69
|
});
|
|
80
70
|
|
|
@@ -138,8 +128,8 @@ describe("Usage", () => {
|
|
|
138
128
|
expect(text).toContain("Command");
|
|
139
129
|
expect(text).toContain("Calls ▼");
|
|
140
130
|
// Token section still intact
|
|
141
|
-
expect(text).toContain("
|
|
142
|
-
expect(text).toContain("
|
|
131
|
+
expect(text).toContain("5k");
|
|
132
|
+
expect(text).toContain("4k");
|
|
143
133
|
|
|
144
134
|
for (const line of lines2) {
|
|
145
135
|
const visLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
package/src/tabs/Usage.ts
CHANGED
|
@@ -47,9 +47,13 @@ export class Usage extends Container {
|
|
|
47
47
|
/** Build row cells once in constructor. */
|
|
48
48
|
private buildRows(): void {
|
|
49
49
|
if (this.isEmpty) return;
|
|
50
|
+
const totalCount = this.tools.reduce((sum, item) => sum + item.count, 0);
|
|
50
51
|
const maxCount = Math.max(...this.tools.map((t) => t.count), 0);
|
|
51
52
|
this.rows = this.tools.map((t) => {
|
|
52
|
-
|
|
53
|
+
let barPct = 0;
|
|
54
|
+
if (totalCount > 0) {
|
|
55
|
+
barPct = maxCount > 0 ? (t.count / maxCount) * 100 : 0;
|
|
56
|
+
}
|
|
53
57
|
return [
|
|
54
58
|
cell.marquee(stripAnsi(t.name).slice(0, TOOL_NAME_MAX_LENGTH), this.tui),
|
|
55
59
|
cell.bar(barPct, (s) => s, "transparent"),
|
|
@@ -132,7 +136,7 @@ export class Usage extends Container {
|
|
|
132
136
|
const borderBoxOptions: BorderBoxOptions = {
|
|
133
137
|
borderStyle: "singleRounded",
|
|
134
138
|
borderFn: (s) => this.theme.fg("border", s),
|
|
135
|
-
titles: [{ text: "Tools", align: "left" }],
|
|
139
|
+
titles: [{ text: this.theme.bold("Tools"), align: "left" }],
|
|
136
140
|
};
|
|
137
141
|
|
|
138
142
|
let borderBox = new BorderBox(borderBoxOptions);
|
|
@@ -141,7 +145,7 @@ export class Usage extends Container {
|
|
|
141
145
|
if (!this.isEmpty) {
|
|
142
146
|
borderBoxOptions.titles = [
|
|
143
147
|
...borderBoxOptions.titles!,
|
|
144
|
-
{ text: this.theme.fg("
|
|
148
|
+
{ text: this.theme.fg("muted", "by calls"), align: "right" },
|
|
145
149
|
];
|
|
146
150
|
|
|
147
151
|
if (!this.table) {
|
package/src/types.ts
CHANGED
|
@@ -63,6 +63,12 @@ export interface ToolStat {
|
|
|
63
63
|
count: number;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export interface ProviderStat {
|
|
67
|
+
provider: string;
|
|
68
|
+
cost: number;
|
|
69
|
+
calls: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
export interface StatsSummary {
|
|
67
73
|
totalCost: number;
|
|
68
74
|
sessionCount: number;
|
|
@@ -79,6 +85,11 @@ export interface StatsSummary {
|
|
|
79
85
|
models: ModelStat[];
|
|
80
86
|
projects: ProjectStat[];
|
|
81
87
|
tools: ToolStat[];
|
|
88
|
+
providers: ProviderStat[];
|
|
89
|
+
compactionCount: number;
|
|
90
|
+
compactedTokens: number;
|
|
91
|
+
modelChanges: number;
|
|
92
|
+
thinkingLevelCount: Record<string, number>;
|
|
82
93
|
dailySpend: DaySpend[];
|
|
83
94
|
hourlySpend: HourSpend[];
|
|
84
95
|
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { LoadingView } from "../LoadingView";
|
|
3
|
-
|
|
4
|
-
describe("LoadingView", () => {
|
|
5
|
-
it("renders with 0% progress", () => {
|
|
6
|
-
const lv = new LoadingView();
|
|
7
|
-
const lines = lv.render(80);
|
|
8
|
-
expect(lines.join("\n")).toContain("Parsing session logs...");
|
|
9
|
-
expect(lines.join("\n")).toContain("0%");
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("updates progress", () => {
|
|
13
|
-
const lv = new LoadingView();
|
|
14
|
-
lv.setProgress(50);
|
|
15
|
-
const lines = lv.render(80);
|
|
16
|
-
expect(lines.join("\n")).toContain("50%");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("renders progress bar with block chars", () => {
|
|
20
|
-
const lv = new LoadingView();
|
|
21
|
-
lv.setProgress(75);
|
|
22
|
-
const lines = lv.render(80);
|
|
23
|
-
expect(lines.join("\n")).toContain("█");
|
|
24
|
-
expect(lines.join("\n")).toContain("75%");
|
|
25
|
-
});
|
|
26
|
-
});
|
|
File without changes
|