@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,110 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import chalk from "chalk";
3
+ import { makeTheme } from "../../__tests__/components.fixtures";
4
+ import { RankedBarList } from "../RankedBarList";
5
+
6
+ describe("RankedBarList", () => {
7
+ it("renders a single item with 100% bar width", () => {
8
+ const theme = makeTheme();
9
+ const list = new RankedBarList(
10
+ [
11
+ {
12
+ name: "TypeScript",
13
+ primaryValue: 100,
14
+ mainValueText: "100 ln",
15
+ secondaryValueText: "5 edits",
16
+ color: chalk.green,
17
+ },
18
+ ],
19
+ theme,
20
+ );
21
+ const lines = list.render(80);
22
+ expect(lines.length).toBe(3); // name+value, bar+%, spacer
23
+ const text = lines.join("\n");
24
+ expect(text).toContain("TypeScript");
25
+ expect(text).toContain("100 ln");
26
+ expect(text).toContain("5 edits");
27
+ expect(text).toContain("100.00%");
28
+ });
29
+
30
+ it("returns empty array for empty items", () => {
31
+ const list = new RankedBarList([], makeTheme());
32
+ expect(list.render(80)).toEqual([]);
33
+ });
34
+
35
+ it("computes proportional bars for multiple items", () => {
36
+ const list = new RankedBarList(
37
+ [
38
+ { name: "Python", primaryValue: 60, mainValueText: "60 ln", color: chalk.blue },
39
+ { name: "Rust", primaryValue: 40, mainValueText: "40 ln", color: chalk.red },
40
+ ],
41
+ makeTheme(),
42
+ );
43
+ const lines = list.render(80);
44
+ const text = lines.join("\n");
45
+ // Python should be 60% and 100% bar, Rust should be 40% and 66.67% bar
46
+ expect(text).toContain("60.00%");
47
+ expect(text).toContain("40.00%");
48
+ expect(text).toContain("Python");
49
+ expect(text).toContain("Rust");
50
+ expect(lines.length).toBe(6); // 2 items × 3 lines each
51
+ });
52
+
53
+ it("renders without secondary value", () => {
54
+ const list = new RankedBarList(
55
+ [{ name: "bash", primaryValue: 10, mainValueText: "10", color: chalk.white }],
56
+ makeTheme(),
57
+ );
58
+ const lines = list.render(80);
59
+ const text = lines.join("\n");
60
+ expect(text).toContain("bash");
61
+ expect(text).toContain("10");
62
+ });
63
+
64
+ it("renders 0% bars when all primaryValues are zero", () => {
65
+ const list = new RankedBarList(
66
+ [
67
+ { name: "Empty1", primaryValue: 0, mainValueText: "0", color: chalk.gray },
68
+ { name: "Empty2", primaryValue: 0, mainValueText: "0", color: chalk.gray },
69
+ ],
70
+ makeTheme(),
71
+ );
72
+ const lines = list.render(80);
73
+ const text = lines.join("\n");
74
+ expect(text).toContain("0.00%");
75
+ // Should not crash with division by zero
76
+ expect(lines.length).toBe(6);
77
+ });
78
+
79
+ it("caches rendered output for same width", () => {
80
+ const list = new RankedBarList(
81
+ [{ name: "Rust", primaryValue: 50, mainValueText: "50 ln", color: chalk.red }],
82
+ makeTheme(),
83
+ );
84
+ const lines1 = list.render(80);
85
+ const lines2 = list.render(80);
86
+ expect(lines1).toBe(lines2); // same reference (cached)
87
+ });
88
+
89
+ it("re-renders when width changes", () => {
90
+ const list = new RankedBarList(
91
+ [{ name: "Rust", primaryValue: 50, mainValueText: "50 ln", color: chalk.red }],
92
+ makeTheme(),
93
+ );
94
+ const lines1 = list.render(80);
95
+ const lines2 = list.render(40);
96
+ expect(lines1).not.toBe(lines2);
97
+ });
98
+
99
+ it("invalidates cache", () => {
100
+ const list = new RankedBarList(
101
+ [{ name: "Rust", primaryValue: 50, mainValueText: "50 ln", color: chalk.red }],
102
+ makeTheme(),
103
+ );
104
+ list.render(80);
105
+ list.invalidate();
106
+ const lines = list.render(80);
107
+ // Should render (not throw), tested by the fact it returns 3 lines
108
+ expect(lines.length).toBe(3);
109
+ });
110
+ });
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Integration test: Dashboard → Models → SortedTable keyboard interaction.
3
+ */
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
5
+ import { makeMockTUI, makeRangeSelector, makeTheme } from "../../__tests__/components.fixtures";
6
+ import { makeSummary } from "../../__tests__/compute.fixtures";
7
+ import { Dashboard } from "../Dashboard";
8
+ import { SortedTable } from "../SortedTable";
9
+ import { allRanges, mapAllSummaries } from "./Dashboard.test";
10
+ import type { StatsSummary, TimeRange } from "../../types";
11
+
12
+ const CURSOR = SortedTable.DEFAULT_CURSOR_CHAR;
13
+ const mockTui = makeMockTUI();
14
+
15
+ const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
16
+
17
+ describe("Dashboard → Models → SortedTable arrow key integration", () => {
18
+ /** Check if any line contains cursor and a model name substring. */
19
+ function cursorOnModel(lines: string[], model: string): boolean {
20
+ return lines.some((l) => l.includes(CURSOR) && l.includes(model));
21
+ }
22
+
23
+ it("initial cursor on first model", () => {
24
+ const summary = {
25
+ ...makeSummary(),
26
+ models: [
27
+ { model: "alpha-model", cost: 10, calls: 100, provider: "p1" },
28
+ { model: "beta-model", cost: 5, calls: 50, provider: "p2" },
29
+ ],
30
+ };
31
+ const dash = new Dashboard(
32
+ mapAllSummaries(allRanges, summary),
33
+ makeTheme(),
34
+ mockTui,
35
+ null,
36
+ makeRangeSelector(makeTheme()),
37
+ );
38
+
39
+ // Navigate to Models tab
40
+ dash.handleInput("\x1b[C"); // → Languages
41
+ dash.handleInput("\x1b[C"); // → Models
42
+ const lines = dash.render(80);
43
+
44
+ // Model column is 6-char fill at width 80 — only first word visible
45
+ expect(cursorOnModel(lines, "Alpha")).toBe(true);
46
+ expect(cursorOnModel(lines, "Beta")).toBe(false);
47
+ });
48
+
49
+ it("down arrow moves cursor to next row via Dashboard dispatch", () => {
50
+ const summary = {
51
+ ...makeSummary(),
52
+ models: [
53
+ { model: "alpha-model", cost: 10, calls: 100, provider: "p1" },
54
+ { model: "beta-model", cost: 5, calls: 50, provider: "p2" },
55
+ { model: "gamma-model", cost: 1, calls: 10, provider: "p3" },
56
+ ],
57
+ };
58
+ const dash = new Dashboard(
59
+ mapAllSummaries(allRanges, summary),
60
+ makeTheme(),
61
+ mockTui,
62
+ null,
63
+ makeRangeSelector(makeTheme()),
64
+ );
65
+
66
+ dash.handleInput("\x1b[C"); // → Languages
67
+ dash.handleInput("\x1b[C"); // → Models
68
+ dash.render(80);
69
+
70
+ // Press down
71
+ dash.handleInput("\x1b[B");
72
+ const lines = dash.render(80);
73
+ // Cursor moves from row 0 to row 1
74
+ expect(cursorOnModel(lines, "Beta")).toBe(true);
75
+ expect(cursorOnModel(lines, "Alpha")).toBe(false);
76
+ });
77
+
78
+ it("up arrow moves cursor up", () => {
79
+ const summary = {
80
+ ...makeSummary(),
81
+ models: [
82
+ { model: "alpha-model", cost: 10, calls: 100, provider: "p1" },
83
+ { model: "beta-model", cost: 5, calls: 50, provider: "p2" },
84
+ { model: "gamma-model", cost: 1, calls: 10, provider: "p3" },
85
+ ],
86
+ };
87
+ const dash = new Dashboard(
88
+ mapAllSummaries(allRanges, summary),
89
+ makeTheme(),
90
+ mockTui,
91
+ null,
92
+ makeRangeSelector(makeTheme()),
93
+ );
94
+
95
+ dash.handleInput("\x1b[C"); // → Languages
96
+ dash.handleInput("\x1b[C"); // → Models
97
+ dash.render(80);
98
+
99
+ // Move down twice, then back up once
100
+ dash.handleInput("\x1b[B");
101
+ dash.handleInput("\x1b[B");
102
+ dash.handleInput("\x1b[A");
103
+ const lines = dash.render(80);
104
+
105
+ expect(cursorOnModel(lines, "Beta")).toBe(true);
106
+ expect(cursorOnModel(lines, "Alpha")).toBe(false);
107
+ expect(cursorOnModel(lines, "Gamma")).toBe(false);
108
+ });
109
+
110
+ it("arrow keys work across range switches", () => {
111
+ const summary1d = {
112
+ ...makeSummary(),
113
+ models: [{ model: "alpha-model", cost: 1, calls: 10, provider: "p1" }],
114
+ };
115
+ const summaryAll = {
116
+ ...makeSummary(),
117
+ models: [
118
+ { model: "alpha-model", cost: 10, calls: 100, provider: "p1" },
119
+ { model: "beta-model", cost: 5, calls: 50, provider: "p2" },
120
+ { model: "gamma-model", cost: 1, calls: 10, provider: "p3" },
121
+ ],
122
+ };
123
+
124
+ const summaries: Map<TimeRange, StatsSummary> = new Map([
125
+ ["1d", summary1d],
126
+ ["7d", summaryAll],
127
+ ["30d", summaryAll],
128
+ ["All", summaryAll],
129
+ ]);
130
+ const dash = new Dashboard(
131
+ summaries,
132
+ makeTheme(),
133
+ mockTui,
134
+ null,
135
+ makeRangeSelector(makeTheme()),
136
+ );
137
+
138
+ // Navigate to Models, switch to 1d
139
+ dash.handleInput("\x1b[C"); // → Languages
140
+ dash.handleInput("\x1b[C"); // → Models
141
+ dash.handleInput("r"); // All → 1d
142
+ let lines = dash.render(80);
143
+ // 1d: only Alpha Model
144
+ expect(cursorOnModel(lines, "Alpha")).toBe(true);
145
+
146
+ // Switch back to All
147
+ // Current range index = 0 (1d), need to cycle to 3 (All)
148
+ // r → 1, r → 2, r → 3
149
+ dash.handleInput("r");
150
+ dash.handleInput("r");
151
+ dash.handleInput("r");
152
+
153
+ // tabIndex is still 2 (Models) — no need to re-navigate.
154
+ // buildTabs was called on each r, creating a new SortedTable with cursor at row 0.
155
+ lines = dash.render(80);
156
+
157
+ // 3 models now. Move down twice.
158
+ dash.handleInput("\x1b[B");
159
+ dash.handleInput("\x1b[B");
160
+ lines = dash.render(80);
161
+
162
+ expect(cursorOnModel(lines, "Gamma")).toBe(true);
163
+ });
164
+
165
+ // FIX:
166
+ // describe("marquee animation persists across render cycles", () => {
167
+ // beforeEach(() => {
168
+ // vi.useFakeTimers();
169
+ // });
170
+ //
171
+ // afterEach(() => {
172
+ // vi.useRealTimers();
173
+ // });
174
+ //
175
+ // /** Extract the full visible text of the focused row. */
176
+ // function focusedRowText(lines: string[]): string {
177
+ // for (const line of lines) {
178
+ // if (line.includes(CURSOR)) {
179
+ // return strip(line);
180
+ // }
181
+ // }
182
+ // return "";
183
+ // }
184
+ //
185
+ // it("marquee advances on successive renders (not stuck at offset 0)", () => {
186
+ // // A model name long enough to overflow the ~27-char Model column at width=80
187
+ // const longModel = "A-Very-Long-Model-Name-That-Overflows-And-Should-Scroll";
188
+ // const summary = {
189
+ // ...makeSummary(),
190
+ // models: [{ model: longModel, cost: 10, calls: 100, provider: "p1" }],
191
+ // };
192
+ // const dash = new Dashboard(
193
+ // mapAllSummaries(allRanges, summary),
194
+ // makeTheme(),
195
+ // mockTui,
196
+ // null,
197
+ // makeRangeSelector(makeTheme()),
198
+ // );
199
+ //
200
+ // // Navigate to Models tab
201
+ // dash.handleInput("\x1b[C"); // → Languages
202
+ // dash.handleInput("\x1b[C"); // → Models
203
+ //
204
+ // // First render — marquee starts at offset 0
205
+ // let lines = dash.render(80);
206
+ // // Model column is 6-char fill — marquee starts at offset 0
207
+ // const render1 = focusedRowText(lines);
208
+ // expect(render1).toContain("A Very");
209
+ //
210
+ // // Advance 150ms = 1 timer tick → marquee text should scroll
211
+ // vi.advanceTimersByTime(150);
212
+ // lines = dash.render(80);
213
+ // const render2 = focusedRowText(lines);
214
+ // expect(render2).not.toBe("");
215
+ //
216
+ // // Content must have scrolled — slice differs from render1
217
+ // expect(render2).not.toBe(render1);
218
+ //
219
+ // // Advance another 150ms = 2nd tick → should scroll again
220
+ // vi.advanceTimersByTime(150);
221
+ // lines = dash.render(80);
222
+ // const render3 = focusedRowText(lines);
223
+ // expect(render3).not.toBe("");
224
+ // expect(render3).not.toBe(render1);
225
+ // expect(render3).not.toBe(render2);
226
+ // });
227
+ // });
228
+ });