@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.
- package/.pi/extensions/guardrails.json +10 -0
- package/.pi/extensions/guardrails.v0.json +8 -0
- package/AGENTS.md +13 -0
- package/CONTEXT.md +119 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/bun.lock +325 -0
- package/docs/ARCHITECTURE.md +66 -0
- package/docs/adr/0001-global-session-project-map.md +9 -0
- package/docs/adr/0002-precomputed-summaries.md +9 -0
- package/docs/agents/domain.md +42 -0
- package/docs/agents/issue-tracker.md +22 -0
- package/docs/agents/triage-labels.md +14 -0
- package/package.json +49 -0
- package/src/__tests__/cache.test.ts +388 -0
- package/src/__tests__/components.fixtures.ts +54 -0
- package/src/__tests__/compute.fixtures.ts +49 -0
- package/src/__tests__/compute.test.ts +336 -0
- package/src/__tests__/e2e.test.ts +182 -0
- package/src/__tests__/format.test.ts +232 -0
- package/src/__tests__/parser.test.ts +1396 -0
- package/src/cache.ts +178 -0
- package/src/colorPalette.ts +119 -0
- package/src/components/BarChart.ts +288 -0
- package/src/components/Dashboard.ts +222 -0
- package/src/components/Header.ts +40 -0
- package/src/components/KpiCards.ts +104 -0
- package/src/components/LoadingView.ts +38 -0
- package/src/components/MarqueeText.ts +79 -0
- package/src/components/RangeSelector.ts +63 -0
- package/src/components/RankedBarList.ts +71 -0
- package/src/components/SortedTable.ts +221 -0
- package/src/components/StatCard.ts +64 -0
- package/src/components/TabBar.ts +59 -0
- package/src/components/UsageRow.ts +55 -0
- package/src/components/__tests__/Bar.test.ts +66 -0
- package/src/components/__tests__/BarChart.test.ts +224 -0
- package/src/components/__tests__/Dashboard.test.ts +452 -0
- package/src/components/__tests__/KpiCards.test.ts +83 -0
- package/src/components/__tests__/LoadingView.test.ts +26 -0
- package/src/components/__tests__/MarqueeText.test.ts +75 -0
- package/src/components/__tests__/RangeSelector.test.ts +34 -0
- package/src/components/__tests__/RankedBarList.test.ts +110 -0
- package/src/components/__tests__/SortedTable.integration.test.ts +228 -0
- package/src/components/__tests__/SortedTable.test.ts +723 -0
- package/src/components/__tests__/TabBar.test.ts +62 -0
- package/src/components/__tests__/cells.test.ts +193 -0
- package/src/components/cells.ts +108 -0
- package/src/components/shared/Bar.ts +22 -0
- package/src/components/shared/GridRow.ts +22 -0
- package/src/compute.ts +210 -0
- package/src/format.ts +219 -0
- package/src/index.ts +88 -0
- package/src/parser.ts +363 -0
- package/src/tabs/Languages.ts +102 -0
- package/src/tabs/Models.ts +108 -0
- package/src/tabs/Overview.ts +152 -0
- package/src/tabs/Projects.ts +92 -0
- package/src/tabs/Usage.ts +181 -0
- package/src/tabs/__tests__/Languages.test.ts +158 -0
- package/src/tabs/__tests__/Models.test.ts +143 -0
- package/src/tabs/__tests__/Overview.test.ts +92 -0
- package/src/tabs/__tests__/Projects.test.ts +142 -0
- package/src/tabs/__tests__/Usage.test.ts +174 -0
- package/src/types.ts +99 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { makeMockTUI, makeTheme } from "../../__tests__/components.fixtures";
|
|
3
|
+
import { type ToolStat } from "../../types";
|
|
4
|
+
import { Usage } from "../Usage";
|
|
5
|
+
|
|
6
|
+
describe("Usage", () => {
|
|
7
|
+
const mockTui = makeMockTUI();
|
|
8
|
+
|
|
9
|
+
const tokenUsage = {
|
|
10
|
+
total: 10000,
|
|
11
|
+
input: 5000,
|
|
12
|
+
output: 4000,
|
|
13
|
+
cacheRead: 500,
|
|
14
|
+
cacheWrite: 500,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const tools: ToolStat[] = [
|
|
18
|
+
{ name: "bash", count: 150 },
|
|
19
|
+
{ name: "read", count: 120 },
|
|
20
|
+
{ name: "edit", count: 45 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
it("renders token section with StatCards", () => {
|
|
24
|
+
const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10);
|
|
25
|
+
const text = tab.render(80).join("\n");
|
|
26
|
+
|
|
27
|
+
expect(text).toContain("Tokens");
|
|
28
|
+
expect(text).toContain("10.0k");
|
|
29
|
+
expect(text).toContain("Input");
|
|
30
|
+
expect(text).toContain("5.0k");
|
|
31
|
+
expect(text).toContain("Output");
|
|
32
|
+
expect(text).toContain("4.0k");
|
|
33
|
+
expect(text).toContain("Cache Read");
|
|
34
|
+
expect(text).toContain("500");
|
|
35
|
+
expect(text).toContain("Cache Write");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders tool table with formatted data", () => {
|
|
39
|
+
const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10);
|
|
40
|
+
const lines = tab.render(80).slice(4);
|
|
41
|
+
|
|
42
|
+
const text = lines.join("\n");
|
|
43
|
+
|
|
44
|
+
expect(lines[0]).toContain("Tools");
|
|
45
|
+
expect(lines[0]).toContain(tools.length.toString());
|
|
46
|
+
|
|
47
|
+
// Headers
|
|
48
|
+
expect(text).toContain("Command");
|
|
49
|
+
expect(text).toContain("Calls");
|
|
50
|
+
expect(text).toContain("Share %");
|
|
51
|
+
|
|
52
|
+
// Tool names
|
|
53
|
+
expect(text).toContain("bash");
|
|
54
|
+
expect(text).toContain("read");
|
|
55
|
+
expect(text).toContain("edit");
|
|
56
|
+
|
|
57
|
+
// Call counts
|
|
58
|
+
expect(text).toContain("150");
|
|
59
|
+
expect(text).toContain("120");
|
|
60
|
+
expect(text).toContain("45");
|
|
61
|
+
});
|
|
62
|
+
|
|
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
|
+
it("shows empty state when tools is empty", () => {
|
|
71
|
+
const tab = new Usage([], tokenUsage, makeTheme(), mockTui, 10);
|
|
72
|
+
const lines = tab.render(80).slice(4);
|
|
73
|
+
const text = lines.join("\n");
|
|
74
|
+
|
|
75
|
+
expect(lines[0]).toContain("Tools");
|
|
76
|
+
// don't display 0 counter
|
|
77
|
+
expect(lines[0]).not.toContain("0");
|
|
78
|
+
expect(text).toContain("No tools data for this time range");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("renders tool names with control characters as single lines", () => {
|
|
82
|
+
const dirtyTools: ToolStat[] = [
|
|
83
|
+
{ name: "ls -la agent/\n</parameter", count: 2 },
|
|
84
|
+
{ name: "bash", count: 100 },
|
|
85
|
+
];
|
|
86
|
+
const tab = new Usage(dirtyTools, tokenUsage, makeTheme(), mockTui, 10);
|
|
87
|
+
const lines = tab.render(80);
|
|
88
|
+
// Every line should be a single-line render — no extra lines from \n
|
|
89
|
+
const text = lines.join("\n");
|
|
90
|
+
// Tool column is ~20 chars wide — the \n is stripped, leaving truncated "ls -la agent/</param…"
|
|
91
|
+
expect(text).not.toContain("agent/\n");
|
|
92
|
+
expect(text).toContain("bash");
|
|
93
|
+
// Verify no broken rows (each rendered line is a single table row)
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
// Each line should be a continuous visible string without embedded newlines
|
|
96
|
+
expect(line).not.toContain("\n");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("renders within width", () => {
|
|
101
|
+
const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10);
|
|
102
|
+
const lines = tab.render(50);
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const visLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
105
|
+
expect(visLen).toBeLessThanOrEqual(50);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("shows sort indicator on Calls column", () => {
|
|
110
|
+
const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10);
|
|
111
|
+
const lines = tab.render(80);
|
|
112
|
+
const text = lines.join("\n");
|
|
113
|
+
expect(text).toContain("Calls ▼");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("invalidates render cache", () => {
|
|
117
|
+
const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10);
|
|
118
|
+
tab.render(80);
|
|
119
|
+
tab.invalidate();
|
|
120
|
+
const lines = tab.render(60);
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const visLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
123
|
+
expect(visLen).toBeLessThanOrEqual(60);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("supports re-render after invalidation (lifecycle path)", () => {
|
|
128
|
+
const tab = new Usage(tools, tokenUsage, makeTheme(), mockTui, 10);
|
|
129
|
+
|
|
130
|
+
const lines1 = tab.render(80);
|
|
131
|
+
expect(lines1.join("\n")).toContain("bash");
|
|
132
|
+
|
|
133
|
+
tab.invalidate();
|
|
134
|
+
|
|
135
|
+
const lines2 = tab.render(80);
|
|
136
|
+
const text = lines2.join("\n");
|
|
137
|
+
expect(text).toContain("bash");
|
|
138
|
+
expect(text).toContain("Command");
|
|
139
|
+
expect(text).toContain("Calls ▼");
|
|
140
|
+
// Token section still intact
|
|
141
|
+
expect(text).toContain("5.0k");
|
|
142
|
+
expect(text).toContain("4.0k");
|
|
143
|
+
|
|
144
|
+
for (const line of lines2) {
|
|
145
|
+
const visLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
146
|
+
expect(visLen).toBeLessThanOrEqual(80);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("marquee lifecycle", () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
vi.useFakeTimers();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
vi.useRealTimers();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("clears marquee timers on invalidate", () => {
|
|
160
|
+
const longTool: ToolStat[] = [{ name: "a-very-long-tool-name-x", count: 150 }];
|
|
161
|
+
const tab = new Usage(longTool, tokenUsage, makeTheme(), mockTui, 10);
|
|
162
|
+
|
|
163
|
+
tab.render(30);
|
|
164
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
165
|
+
|
|
166
|
+
tab.invalidate();
|
|
167
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
168
|
+
|
|
169
|
+
const lines = tab.render(80);
|
|
170
|
+
const text = lines.join("\n");
|
|
171
|
+
expect(text).toContain("a-very-long-tool");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface DayAgg {
|
|
2
|
+
date: string; // "YYYY-MM-DD"
|
|
3
|
+
cost: number;
|
|
4
|
+
hourCost: Record<number, number>; // accumulated cost per UTC hour 0-23
|
|
5
|
+
inTok: number;
|
|
6
|
+
outTok: number;
|
|
7
|
+
crTok: number;
|
|
8
|
+
cwTok: number;
|
|
9
|
+
userMsgs: number;
|
|
10
|
+
asstMsgs: number;
|
|
11
|
+
toolResults: number;
|
|
12
|
+
sessionIds: Set<string>;
|
|
13
|
+
langLines: Record<string, number>;
|
|
14
|
+
langEdits: Record<string, number>;
|
|
15
|
+
modelCost: Record<string, number>;
|
|
16
|
+
modelCount: Record<string, number>;
|
|
17
|
+
providerCost: Record<string, number>;
|
|
18
|
+
providerCount: Record<string, number>;
|
|
19
|
+
modelToProvider: Map<string, string>;
|
|
20
|
+
projectCost: Record<string, number>;
|
|
21
|
+
projectSessions: Record<string, Set<string>>;
|
|
22
|
+
toolCount: Record<string, number>;
|
|
23
|
+
// New fields tracking pi session entry types beyond session+message
|
|
24
|
+
compactionCount: number;
|
|
25
|
+
compactedTokens: number;
|
|
26
|
+
modelChanges: number;
|
|
27
|
+
thinkingLevelCount: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type TimeRange = "1d" | "7d" | "30d" | "All";
|
|
31
|
+
|
|
32
|
+
export interface DaySpend {
|
|
33
|
+
date: string; // "YYYY-MM-DD"
|
|
34
|
+
cost: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface HourSpend {
|
|
38
|
+
hour: number; // 0-23
|
|
39
|
+
cost: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LangStat {
|
|
43
|
+
language: string;
|
|
44
|
+
lines: number;
|
|
45
|
+
edits: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ModelStat {
|
|
49
|
+
provider?: string;
|
|
50
|
+
model: string;
|
|
51
|
+
cost: number;
|
|
52
|
+
calls: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProjectStat {
|
|
56
|
+
project: string;
|
|
57
|
+
cost: number;
|
|
58
|
+
sessions: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ToolStat {
|
|
62
|
+
name: string;
|
|
63
|
+
count: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StatsSummary {
|
|
67
|
+
totalCost: number;
|
|
68
|
+
sessionCount: number;
|
|
69
|
+
totalMessages: number;
|
|
70
|
+
totalTokens: number;
|
|
71
|
+
totalInputTokens: number;
|
|
72
|
+
totalOutputTokens: number;
|
|
73
|
+
totalCacheReadTokens: number;
|
|
74
|
+
totalCacheWriteTokens: number;
|
|
75
|
+
daysActive: number;
|
|
76
|
+
avgCostPerDay: number;
|
|
77
|
+
todayCost: number;
|
|
78
|
+
languages: LangStat[];
|
|
79
|
+
models: ModelStat[];
|
|
80
|
+
projects: ProjectStat[];
|
|
81
|
+
tools: ToolStat[];
|
|
82
|
+
dailySpend: DaySpend[];
|
|
83
|
+
hourlySpend: HourSpend[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CachePayload {
|
|
87
|
+
signature: string;
|
|
88
|
+
generatedAt: string;
|
|
89
|
+
days: SerializedDayAgg[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SerializedDayAgg extends Omit<
|
|
93
|
+
DayAgg,
|
|
94
|
+
"sessionIds" | "projectSessions" | "modelToProvider"
|
|
95
|
+
> {
|
|
96
|
+
sessionIds: string[];
|
|
97
|
+
projectSessions: Record<string, string[]>;
|
|
98
|
+
modelToProvider: Record<string, string>;
|
|
99
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"types": ["bun"],
|
|
11
|
+
|
|
12
|
+
// Bundler mode
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
|
|
18
|
+
// Best practices
|
|
19
|
+
"strict": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedIndexedAccess": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
|
|
25
|
+
// Some stricter flags (disabled by default)
|
|
26
|
+
"noUnusedLocals": false,
|
|
27
|
+
"noUnusedParameters": false,
|
|
28
|
+
"noPropertyAccessFromIndexSignature": false
|
|
29
|
+
}
|
|
30
|
+
}
|