@mohndoe/pi-atlas 0.1.1

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.
Files changed (66) hide show
  1. package/.pi/extensions/guardrails.json +10 -0
  2. package/.pi/extensions/guardrails.v0.json +8 -0
  3. package/AGENTS.md +13 -0
  4. package/CONTEXT.md +119 -0
  5. package/LICENSE +21 -0
  6. package/README.md +40 -0
  7. package/bun.lock +325 -0
  8. package/docs/ARCHITECTURE.md +66 -0
  9. package/docs/adr/0001-global-session-project-map.md +9 -0
  10. package/docs/adr/0002-precomputed-summaries.md +9 -0
  11. package/docs/agents/domain.md +42 -0
  12. package/docs/agents/issue-tracker.md +22 -0
  13. package/docs/agents/triage-labels.md +14 -0
  14. package/package.json +49 -0
  15. package/src/__tests__/cache.test.ts +388 -0
  16. package/src/__tests__/components.fixtures.ts +54 -0
  17. package/src/__tests__/compute.fixtures.ts +49 -0
  18. package/src/__tests__/compute.test.ts +336 -0
  19. package/src/__tests__/e2e.test.ts +182 -0
  20. package/src/__tests__/format.test.ts +232 -0
  21. package/src/__tests__/parser.test.ts +1396 -0
  22. package/src/cache.ts +178 -0
  23. package/src/colorPalette.ts +119 -0
  24. package/src/components/BarChart.ts +288 -0
  25. package/src/components/Dashboard.ts +222 -0
  26. package/src/components/Header.ts +40 -0
  27. package/src/components/KpiCards.ts +104 -0
  28. package/src/components/LoadingView.ts +38 -0
  29. package/src/components/MarqueeText.ts +79 -0
  30. package/src/components/RangeSelector.ts +63 -0
  31. package/src/components/RankedBarList.ts +71 -0
  32. package/src/components/SortedTable.ts +221 -0
  33. package/src/components/StatCard.ts +64 -0
  34. package/src/components/TabBar.ts +59 -0
  35. package/src/components/UsageRow.ts +55 -0
  36. package/src/components/__tests__/Bar.test.ts +66 -0
  37. package/src/components/__tests__/BarChart.test.ts +224 -0
  38. package/src/components/__tests__/Dashboard.test.ts +452 -0
  39. package/src/components/__tests__/KpiCards.test.ts +83 -0
  40. package/src/components/__tests__/LoadingView.test.ts +26 -0
  41. package/src/components/__tests__/MarqueeText.test.ts +75 -0
  42. package/src/components/__tests__/RangeSelector.test.ts +34 -0
  43. package/src/components/__tests__/RankedBarList.test.ts +110 -0
  44. package/src/components/__tests__/SortedTable.integration.test.ts +228 -0
  45. package/src/components/__tests__/SortedTable.test.ts +723 -0
  46. package/src/components/__tests__/TabBar.test.ts +62 -0
  47. package/src/components/__tests__/cells.test.ts +193 -0
  48. package/src/components/cells.ts +108 -0
  49. package/src/components/shared/Bar.ts +22 -0
  50. package/src/components/shared/GridRow.ts +22 -0
  51. package/src/compute.ts +210 -0
  52. package/src/format.ts +219 -0
  53. package/src/index.ts +88 -0
  54. package/src/parser.ts +363 -0
  55. package/src/tabs/Languages.ts +102 -0
  56. package/src/tabs/Models.ts +108 -0
  57. package/src/tabs/Overview.ts +152 -0
  58. package/src/tabs/Projects.ts +92 -0
  59. package/src/tabs/Usage.ts +181 -0
  60. package/src/tabs/__tests__/Languages.test.ts +158 -0
  61. package/src/tabs/__tests__/Models.test.ts +143 -0
  62. package/src/tabs/__tests__/Overview.test.ts +92 -0
  63. package/src/tabs/__tests__/Projects.test.ts +142 -0
  64. package/src/tabs/__tests__/Usage.test.ts +174 -0
  65. package/src/types.ts +99 -0
  66. package/tsconfig.json +30 -0
@@ -0,0 +1,102 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Container, Text, type TUI } from "@earendil-works/pi-tui";
3
+ import { ColorPalette } from "../colorPalette";
4
+ import { cell, type CellComponent } from "../components/cells";
5
+ import { SortedTable } from "../components/SortedTable";
6
+ import { formatNumber } from "../format";
7
+ import type { LangStat } from "../types";
8
+ import { BorderBox } from "@mohndoe/pi-tui-extras";
9
+
10
+ const EMPTY_MESSAGE = "No language data for this time range";
11
+
12
+ export class Languages extends Container {
13
+ private isEmpty: boolean;
14
+ private theme: Theme;
15
+ private table: SortedTable | null = null;
16
+ private rows: CellComponent[][] = [];
17
+
18
+ constructor(
19
+ private languages: LangStat[],
20
+ theme: Theme,
21
+ private palette: ColorPalette,
22
+ private tui: TUI,
23
+ private maxHeight: number,
24
+ ) {
25
+ super();
26
+ this.theme = theme;
27
+ this.isEmpty = languages.length === 0;
28
+ this.buildRows();
29
+ }
30
+
31
+ /** Build row cells once in constructor. Data is stable per Languages instance —
32
+ * a new Languages is created whenever Dashboard.buildTabs() runs (range switch). */
33
+ private buildRows(): void {
34
+ if (this.isEmpty) return;
35
+ const maxLines = Math.max(...this.languages.map((l) => l.lines), 0);
36
+ this.rows = this.languages.map((l) => {
37
+ const barPct = maxLines > 0 ? (l.lines / maxLines) * 100 : 0;
38
+ return [
39
+ cell.marquee(l.language, this.tui),
40
+ cell.bar(barPct, this.palette.getColor(l.language), "transparent"),
41
+ cell.text(this.theme.fg("muted", formatNumber(l.edits))),
42
+ cell.text(this.theme.bold(formatNumber(l.lines))),
43
+ ];
44
+ });
45
+ }
46
+
47
+ override render(width: number): string[] {
48
+ this.clear();
49
+
50
+ const baseBorderBoxOptions = {
51
+ borderStyle: "singleRounded" as const,
52
+ borderFn: (s: string) => this.theme.fg("border", s),
53
+ };
54
+
55
+ if (!this.isEmpty) {
56
+ const bb = new BorderBox({
57
+ ...baseBorderBoxOptions,
58
+ titles: [
59
+ { text: "Languages", align: "left" },
60
+ { text: this.theme.fg("dim", formatNumber(this.languages.length)), align: "right" },
61
+ ],
62
+ });
63
+
64
+ if (!this.table) {
65
+ this.table = new SortedTable(
66
+ {
67
+ columns: [
68
+ { header: cell.header("Name"), width: 12 },
69
+ { header: cell.header("Share %"), width: "fill" },
70
+ { header: cell.header("Edits"), width: 8 },
71
+ { header: cell.header("Lines"), width: 14 },
72
+ ],
73
+ rows: this.rows,
74
+ maxHeight: this.maxHeight,
75
+ sort: { column: 3, direction: "desc" },
76
+ tui: this.tui,
77
+ },
78
+ this.theme,
79
+ );
80
+ }
81
+ bb.addChild(this.table);
82
+ this.addChild(bb);
83
+ } else {
84
+ const bb = new BorderBox({
85
+ ...baseBorderBoxOptions,
86
+ titles: [{ text: "Languages", align: "left" }],
87
+ });
88
+ bb.addChild(new Text(this.theme.fg("muted", EMPTY_MESSAGE)));
89
+ this.addChild(bb);
90
+ }
91
+ return super.render(width);
92
+ }
93
+
94
+ handleInput(data: string): void {
95
+ this.table?.handleInput(data);
96
+ }
97
+
98
+ override invalidate(): void {
99
+ super.invalidate();
100
+ this.table?.invalidate();
101
+ }
102
+ }
@@ -0,0 +1,108 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Container, Text, type TUI } from "@earendil-works/pi-tui";
3
+ import { ColorPalette } from "../colorPalette";
4
+ import { cell, type CellComponent } from "../components/cells";
5
+ import { SortedTable } from "../components/SortedTable";
6
+ import { formatCost, formatModelName, formatNumber } from "../format";
7
+ import { type ModelStat } from "../types";
8
+ import { BorderBox } from "@mohndoe/pi-tui-extras";
9
+
10
+ const EMPTY_MESSAGE = "No model data for this time range";
11
+
12
+ export class Models extends Container {
13
+ private isEmpty: boolean;
14
+ private theme: Theme;
15
+ private table: SortedTable | null = null;
16
+ private rows: CellComponent[][] = [];
17
+
18
+ constructor(
19
+ private models: ModelStat[],
20
+ theme: Theme,
21
+ private palette: ColorPalette,
22
+ private tui: TUI,
23
+ private maxHeight: number,
24
+ ) {
25
+ super();
26
+ this.theme = theme;
27
+ this.isEmpty = models.length === 0;
28
+ this.buildRows();
29
+ }
30
+
31
+ /** Build row cells once in constructor. Data is stable per Models instance —
32
+ * a new Models is created whenever Dashboard.buildTabs() runs (range switch).
33
+ * Building rows every render would destroy marquee state on each frame. */
34
+ private buildRows(): void {
35
+ if (this.isEmpty) return;
36
+ const totalCost = this.models.reduce((sum, item) => sum + item.cost, 0);
37
+ const maxCost = Math.max(...this.models.map((m) => m.cost), 0);
38
+ this.rows = this.models.map((m) => {
39
+ let pct = 0;
40
+ let barPct = 0;
41
+ if (totalCost > 0) {
42
+ pct = (m.cost * 100) / totalCost;
43
+ barPct = maxCost > 0 ? (m.cost / maxCost) * 100 : 0;
44
+ }
45
+ return [
46
+ cell.marquee(formatModelName(m.model), this.tui),
47
+ cell.text(this.theme.fg("muted", m.provider ?? "Unknown")),
48
+ cell.bar(barPct, this.palette.getColor(m.provider ?? "Unknown"), "transparent"),
49
+ cell.text(this.theme.fg("muted", formatNumber(m.calls))),
50
+ cell.text(m.cost > 0 ? this.theme.bold(formatCost(m.cost)) : this.theme.fg("dim", "Free")),
51
+ ];
52
+ });
53
+ }
54
+
55
+ override render(width: number): string[] {
56
+ this.clear();
57
+ const baseBorderBoxOptions = {
58
+ borderStyle: "singleRounded" as const,
59
+ borderFn: (s: string) => this.theme.fg("border", s),
60
+ };
61
+ if (!this.isEmpty) {
62
+ const bb = new BorderBox({
63
+ ...baseBorderBoxOptions,
64
+ titles: [
65
+ { text: "Models", align: "left" },
66
+ { text: this.theme.fg("dim", formatNumber(this.models.length)), align: "right" },
67
+ ],
68
+ });
69
+ if (!this.table) {
70
+ this.table = new SortedTable(
71
+ {
72
+ columns: [
73
+ { header: cell.header("Name"), width: 32 },
74
+ { header: cell.header("Provider"), width: 16 },
75
+ { header: cell.header("Cost %"), width: "fill" },
76
+ { header: cell.header("Calls"), width: 10 },
77
+ { header: cell.header("Cost"), width: 12 },
78
+ ],
79
+ rows: this.rows,
80
+ maxHeight: this.maxHeight,
81
+ sort: { column: 4, direction: "desc" },
82
+ tui: this.tui,
83
+ },
84
+ this.theme,
85
+ );
86
+ }
87
+ bb.addChild(this.table);
88
+ this.addChild(bb);
89
+ } else {
90
+ const bb = new BorderBox({
91
+ ...baseBorderBoxOptions,
92
+ titles: [{ text: "Models", align: "left" }],
93
+ });
94
+ bb.addChild(new Text(this.theme.fg("muted", EMPTY_MESSAGE)));
95
+ this.addChild(bb);
96
+ }
97
+ return super.render(width);
98
+ }
99
+
100
+ handleInput(data: string): void {
101
+ this.table?.handleInput(data);
102
+ }
103
+
104
+ override invalidate(): void {
105
+ super.invalidate();
106
+ this.table?.invalidate();
107
+ }
108
+ }
@@ -0,0 +1,152 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
3
+ import { BorderBox } from "@mohndoe/pi-tui-extras";
4
+ import { langPalette, modelPalette } from "../colorPalette";
5
+ import { BarChart } from "../components/BarChart";
6
+ import { KpiCards, type KpiData } from "../components/KpiCards";
7
+ import { GridRow } from "../components/shared/GridRow";
8
+ import { StatCard } from "../components/StatCard";
9
+ import { formatCost, formatNumber } from "../format";
10
+ import { type StatsSummary, type TimeRange } from "../types";
11
+
12
+ const SPACER_HEIGHT = 1;
13
+ const BAR_CHART_MAX_HEIGHT = 18;
14
+
15
+ export class Overview extends Container {
16
+ private kpiCards: KpiCards;
17
+ private barChart: BarChart;
18
+ private topCards: GridRow;
19
+
20
+ constructor(
21
+ private summary: StatsSummary,
22
+ rangeKey: TimeRange,
23
+ private theme: Theme,
24
+ maxHeight: number,
25
+ ) {
26
+ super();
27
+ const kpis: KpiData = {
28
+ totalCost: this.summary.totalCost,
29
+ sessionCount: this.summary.sessionCount,
30
+ totalMessages: this.summary.totalMessages,
31
+ totalTokens: this.summary.totalTokens,
32
+ daysActive: this.summary.daysActive,
33
+ avgCostPerDay: this.summary.avgCostPerDay,
34
+ };
35
+ this.kpiCards = new KpiCards(kpis, this.theme);
36
+
37
+ const topLanguage = summary.languages[0];
38
+ const topModel = summary.models[0];
39
+ const topProject = summary.projects[0];
40
+
41
+ const langBox = new BorderBox({
42
+ titles: [{ text: this.theme.bold("Top Language"), align: "left" }],
43
+ padding: { left: 1, right: 1 },
44
+ borderFn: topLanguage
45
+ ? langPalette.getColor(topLanguage.language)
46
+ : (s: string) => this.theme.fg("borderMuted", s),
47
+ });
48
+ langBox.addChild(
49
+ topLanguage
50
+ ? new StatCard(
51
+ {
52
+ label: { text: topLanguage.language },
53
+ value: {
54
+ text: this.theme.bold(formatNumber(topLanguage.lines) + " lines"),
55
+ color: "text",
56
+ },
57
+ },
58
+ this.theme,
59
+ )
60
+ : new Text("No data"),
61
+ );
62
+
63
+ const modelBox = new BorderBox({
64
+ titles: [{ text: this.theme.bold("Top model"), align: "left" }],
65
+ padding: { left: 1, right: 1 },
66
+ borderFn: modelPalette.getColor(topModel?.provider || ""),
67
+ });
68
+ modelBox.addChild(
69
+ topModel
70
+ ? new StatCard(
71
+ {
72
+ label: { text: topModel.model },
73
+ value: {
74
+ text: this.theme.bold(formatCost(topModel.cost)),
75
+ color: "text",
76
+ },
77
+ },
78
+ this.theme,
79
+ )
80
+ : new Text("No data."),
81
+ );
82
+
83
+ const projectBox = new BorderBox({
84
+ titles: [{ text: this.theme.bold("Top project"), align: "left" }],
85
+ padding: { left: 1, right: 1 },
86
+ borderFn: (s: string) => this.theme.fg("borderMuted", s),
87
+ });
88
+ projectBox.addChild(
89
+ topProject
90
+ ? new StatCard(
91
+ {
92
+ label: { text: topProject.project },
93
+ value: {
94
+ text: this.theme.bold(formatCost(topProject.cost)),
95
+ color: "text",
96
+ },
97
+ },
98
+ this.theme,
99
+ )
100
+ : new Text("No data."),
101
+ );
102
+
103
+ this.topCards = new GridRow([langBox, modelBox, projectBox], [33, 33, 34]);
104
+
105
+ const kpiCardsHeight = this.kpiCards.render(80).length;
106
+ const topCardsHeight = this.topCards.render(80).length;
107
+
108
+ const chartHeight = Math.min(
109
+ BAR_CHART_MAX_HEIGHT,
110
+ maxHeight - kpiCardsHeight - topCardsHeight - SPACER_HEIGHT * 0,
111
+ );
112
+ this.barChart = new BarChart(
113
+ this.summary.dailySpend,
114
+ rangeKey,
115
+ chartHeight,
116
+ this.theme,
117
+ undefined,
118
+ this.summary.hourlySpend,
119
+ );
120
+ }
121
+
122
+ override render(width: number): string[] {
123
+ this.clear();
124
+ const kpiBorderBox = new BorderBox({
125
+ borderStyle: "singleRounded",
126
+ padding: { left: 1, right: 1 },
127
+ });
128
+ kpiBorderBox.addChild(this.kpiCards);
129
+ this.addChild(kpiBorderBox);
130
+
131
+ const costBarChartBox = new BorderBox({
132
+ borderStyle: "singleRounded",
133
+ titles: [{ text: this.theme.bold("Cost overtime"), align: "left" }],
134
+ borderFn: (s: string) => this.theme.fg("border", s),
135
+ padding: { left: 1, right: 1, top: 1 },
136
+ });
137
+ costBarChartBox.addChild(this.barChart);
138
+ this.addChild(costBarChartBox);
139
+
140
+ this.addChild(this.topCards);
141
+
142
+ return super.render(width);
143
+ }
144
+
145
+ override invalidate(): void {
146
+ super.invalidate();
147
+ this.kpiCards.invalidate();
148
+ this.barChart.invalidate();
149
+ this.topCards.invalidate();
150
+ this.children.forEach((c) => c.invalidate?.());
151
+ }
152
+ }
@@ -0,0 +1,92 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Container, Text, type TUI } from "@earendil-works/pi-tui";
3
+ import { BorderBox, type BorderBoxOptions } from "@mohndoe/pi-tui-extras";
4
+ import { cell, type CellComponent } from "../components/cells";
5
+ import { SortedTable } from "../components/SortedTable";
6
+ import { formatCost, formatNumber } from "../format";
7
+ import { type ProjectStat } from "../types";
8
+
9
+ const EMPTY_MESSAGE = "No projects data for this time range";
10
+
11
+ export class Projects extends Container {
12
+ private isEmpty: boolean;
13
+ private theme: Theme;
14
+ private table: SortedTable | null = null;
15
+ private rows: CellComponent[][] = [];
16
+
17
+ constructor(
18
+ private projects: ProjectStat[],
19
+ theme: Theme,
20
+ private tui: TUI,
21
+ private maxHeight: number,
22
+ ) {
23
+ super();
24
+ this.theme = theme;
25
+ this.isEmpty = projects.length === 0;
26
+ this.buildRows();
27
+ }
28
+
29
+ /** Build row cells once in constructor. Data is stable per Projects instance. */
30
+ private buildRows(): void {
31
+ if (this.isEmpty) return;
32
+ const maxCost = Math.max(...this.projects.map((p) => p.cost), 0);
33
+ this.rows = this.projects.map((p) => {
34
+ const barPct = maxCost > 0 ? (p.cost / maxCost) * 100 : 0;
35
+ return [
36
+ cell.marquee(p.project, this.tui),
37
+ cell.bar(barPct, (s) => s, "transparent"),
38
+ cell.text(this.theme.fg("muted", formatNumber(p.sessions))),
39
+ cell.text(p.cost > 0 ? this.theme.bold(formatCost(p.cost)) : this.theme.fg("dim", "Free")),
40
+ ];
41
+ });
42
+ }
43
+
44
+ override render(width: number): string[] {
45
+ this.clear();
46
+
47
+ const borderBoxOptions: BorderBoxOptions = {
48
+ borderStyle: "singleRounded",
49
+ borderFn: (s) => this.theme.fg("border", s),
50
+ titles: [{ text: "Projects", align: "left" }],
51
+ };
52
+ let borderBox = new BorderBox(borderBoxOptions);
53
+ if (!this.isEmpty) {
54
+ borderBoxOptions.titles = [
55
+ ...borderBoxOptions.titles!,
56
+ { text: this.theme.fg("dim", formatNumber(this.projects.length)), align: "right" },
57
+ ];
58
+ if (!this.table) {
59
+ this.table = new SortedTable(
60
+ {
61
+ columns: [
62
+ { header: cell.header("Name"), width: 20 },
63
+ { header: cell.header("Share %"), width: "fill" },
64
+ { header: cell.header("Sessions"), width: 14 },
65
+ { header: cell.header("Cost"), width: 8 },
66
+ ],
67
+ rows: this.rows,
68
+ maxHeight: this.maxHeight,
69
+ sort: { column: 3, direction: "desc" },
70
+ tui: this.tui,
71
+ },
72
+ this.theme,
73
+ );
74
+ }
75
+ borderBox = new BorderBox(borderBoxOptions);
76
+ borderBox.addChild(this.table);
77
+ } else {
78
+ borderBox.addChild(new Text(this.theme.fg("muted", EMPTY_MESSAGE)));
79
+ }
80
+ this.addChild(borderBox);
81
+ return super.render(width);
82
+ }
83
+
84
+ handleInput(data: string): void {
85
+ this.table?.handleInput(data);
86
+ }
87
+
88
+ override invalidate(): void {
89
+ super.invalidate();
90
+ this.table?.invalidate();
91
+ }
92
+ }
@@ -0,0 +1,181 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Container, Text, type TUI } from "@earendil-works/pi-tui";
3
+ import { BorderBox, type BorderBoxOptions } from "@mohndoe/pi-tui-extras";
4
+ import { cell, type CellComponent } from "../components/cells";
5
+ import { GridRow } from "../components/shared/GridRow";
6
+ import { SortedTable } from "../components/SortedTable";
7
+ import { StatCard } from "../components/StatCard";
8
+ import { formatNumber, stripAnsi } from "../format";
9
+ import type { StatsSummary, ToolStat } from "../types";
10
+
11
+ interface TokenUsageStat {
12
+ total: StatsSummary["totalTokens"];
13
+ input: StatsSummary["totalInputTokens"];
14
+ output: StatsSummary["totalOutputTokens"];
15
+ cacheRead: StatsSummary["totalCacheReadTokens"];
16
+ cacheWrite: StatsSummary["totalCacheWriteTokens"];
17
+ }
18
+
19
+ const TOOL_NAME_MAX_LENGTH = 120;
20
+
21
+ const EMPTY_MESSAGE = "No tools data for this time range";
22
+
23
+ export class Usage extends Container {
24
+ private isEmpty: boolean;
25
+ private theme: Theme;
26
+ private tokenUsage: TokenUsageStat;
27
+ private rows: CellComponent[][] = [];
28
+ private table: SortedTable | null = null;
29
+ private tableHeight: number;
30
+
31
+ constructor(
32
+ private tools: ToolStat[],
33
+ tokenUsage: TokenUsageStat,
34
+ theme: Theme,
35
+ private tui: TUI,
36
+ maxHeight: number,
37
+ ) {
38
+ super();
39
+ this.theme = theme;
40
+ this.tokenUsage = tokenUsage;
41
+ this.isEmpty = tools.length === 0;
42
+ // Tool table gets contentHeight - 4 to account for "Tokens" title + spacer + 2 line overhead
43
+ this.tableHeight = Math.max(3, maxHeight - 4);
44
+ this.buildRows();
45
+ }
46
+
47
+ /** Build row cells once in constructor. */
48
+ private buildRows(): void {
49
+ if (this.isEmpty) return;
50
+ const maxCount = Math.max(...this.tools.map((t) => t.count), 0);
51
+ this.rows = this.tools.map((t) => {
52
+ const barPct = maxCount > 0 ? (t.count / maxCount) * 100 : 0;
53
+ return [
54
+ cell.marquee(stripAnsi(t.name).slice(0, TOOL_NAME_MAX_LENGTH), this.tui),
55
+ cell.bar(barPct, (s) => s, "transparent"),
56
+ cell.text(this.theme.bold(formatNumber(t.count))),
57
+ ];
58
+ });
59
+ }
60
+
61
+ override render(width: number): string[] {
62
+ this.clear();
63
+
64
+ // Token section: title + stat cards
65
+ const title = this.theme.bold("Tokens");
66
+ const subtitle = this.theme.fg("muted", formatNumber(this.tokenUsage.total));
67
+ const row = new GridRow(
68
+ [
69
+ new StatCard(
70
+ {
71
+ label: {
72
+ text: "Input",
73
+ },
74
+ value: {
75
+ text: this.theme.bold(formatNumber(this.tokenUsage.input)),
76
+ color: "accent",
77
+ },
78
+ },
79
+ this.theme,
80
+ ),
81
+ new StatCard(
82
+ {
83
+ label: {
84
+ text: "Output",
85
+ },
86
+ value: {
87
+ text: this.theme.bold(formatNumber(this.tokenUsage.output)),
88
+ color: "accent",
89
+ },
90
+ },
91
+
92
+ this.theme,
93
+ ),
94
+ new StatCard(
95
+ {
96
+ label: {
97
+ text: "Cache Read",
98
+ },
99
+ value: {
100
+ text: this.theme.bold(formatNumber(this.tokenUsage.cacheRead)),
101
+ color: "accent",
102
+ },
103
+ },
104
+ this.theme,
105
+ ),
106
+ new StatCard(
107
+ {
108
+ label: {
109
+ text: "Cache Write",
110
+ },
111
+ value: {
112
+ text: this.theme.bold(formatNumber(this.tokenUsage.cacheWrite)),
113
+ color: "accent",
114
+ },
115
+ },
116
+ this.theme,
117
+ ),
118
+ ],
119
+ [25, 25, 25, 25],
120
+ );
121
+ const statRowBorderBox = new BorderBox({
122
+ titles: [
123
+ { text: title, align: "left" },
124
+ { text: subtitle, align: "right" },
125
+ ],
126
+ borderFn: (s: string) => this.theme.fg("border", s),
127
+ padding: { left: 1, right: 1 },
128
+ });
129
+ statRowBorderBox.addChild(row);
130
+ this.addChild(statRowBorderBox);
131
+
132
+ const borderBoxOptions: BorderBoxOptions = {
133
+ borderStyle: "singleRounded",
134
+ borderFn: (s) => this.theme.fg("border", s),
135
+ titles: [{ text: "Tools", align: "left" }],
136
+ };
137
+
138
+ let borderBox = new BorderBox(borderBoxOptions);
139
+
140
+ // Tool table section
141
+ if (!this.isEmpty) {
142
+ borderBoxOptions.titles = [
143
+ ...borderBoxOptions.titles!,
144
+ { text: this.theme.fg("dim", formatNumber(this.rows.length)), align: "right" },
145
+ ];
146
+
147
+ if (!this.table) {
148
+ this.table = new SortedTable(
149
+ {
150
+ columns: [
151
+ { header: cell.header("Command"), width: 20 },
152
+ { header: cell.header("Share %"), width: "fill" },
153
+ { header: cell.header("Calls"), width: 12 },
154
+ ],
155
+ rows: this.rows,
156
+ maxHeight: this.tableHeight,
157
+ sort: { column: 2, direction: "desc" },
158
+ tui: this.tui,
159
+ },
160
+ this.theme,
161
+ );
162
+ }
163
+ borderBox = new BorderBox(borderBoxOptions);
164
+ borderBox.addChild(this.table);
165
+ } else {
166
+ borderBox.addChild(new Text(this.theme.fg("muted", EMPTY_MESSAGE)));
167
+ }
168
+ this.addChild(borderBox);
169
+
170
+ return super.render(width);
171
+ }
172
+
173
+ handleInput(data: string): void {
174
+ this.table?.handleInput(data);
175
+ }
176
+
177
+ override invalidate(): void {
178
+ super.invalidate();
179
+ this.table?.invalidate();
180
+ }
181
+ }