@tokscale/cli 1.0.5 → 1.0.7
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/cli.js +14 -3
- package/dist/cli.js.map +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +3 -2
- package/dist/native.js.map +1 -1
- package/package.json +6 -4
- package/src/auth.ts +211 -0
- package/src/cli.ts +1040 -0
- package/src/credentials.ts +123 -0
- package/src/cursor.ts +558 -0
- package/src/graph-types.ts +188 -0
- package/src/graph.ts +485 -0
- package/src/native-runner.ts +105 -0
- package/src/native.ts +938 -0
- package/src/pricing.ts +309 -0
- package/src/sessions/claudecode.ts +119 -0
- package/src/sessions/codex.ts +227 -0
- package/src/sessions/gemini.ts +108 -0
- package/src/sessions/index.ts +126 -0
- package/src/sessions/opencode.ts +94 -0
- package/src/sessions/reports.ts +475 -0
- package/src/sessions/types.ts +59 -0
- package/src/spinner.ts +283 -0
- package/src/submit.ts +175 -0
- package/src/table.ts +233 -0
- package/src/tui/App.tsx +339 -0
- package/src/tui/components/BarChart.tsx +198 -0
- package/src/tui/components/DailyView.tsx +113 -0
- package/src/tui/components/DateBreakdownPanel.tsx +79 -0
- package/src/tui/components/Footer.tsx +225 -0
- package/src/tui/components/Header.tsx +68 -0
- package/src/tui/components/Legend.tsx +39 -0
- package/src/tui/components/LoadingSpinner.tsx +82 -0
- package/src/tui/components/ModelRow.tsx +47 -0
- package/src/tui/components/ModelView.tsx +145 -0
- package/src/tui/components/OverviewView.tsx +108 -0
- package/src/tui/components/StatsView.tsx +225 -0
- package/src/tui/components/TokenBreakdown.tsx +46 -0
- package/src/tui/components/index.ts +15 -0
- package/src/tui/config/settings.ts +130 -0
- package/src/tui/config/themes.ts +115 -0
- package/src/tui/hooks/useData.ts +518 -0
- package/src/tui/index.tsx +44 -0
- package/src/tui/opentui.d.ts +137 -0
- package/src/tui/types/index.ts +165 -0
- package/src/tui/utils/cleanup.ts +65 -0
- package/src/tui/utils/colors.ts +65 -0
- package/src/tui/utils/format.ts +36 -0
- package/src/tui/utils/responsive.ts +8 -0
- package/src/types.d.ts +28 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Show, For, createMemo, type Accessor } from "solid-js";
|
|
2
|
+
import { BarChart } from "./BarChart.js";
|
|
3
|
+
import { Legend } from "./Legend.js";
|
|
4
|
+
import { ModelRow } from "./ModelRow.js";
|
|
5
|
+
import type { TUIData, SortType } from "../hooks/useData.js";
|
|
6
|
+
import { formatCost } from "../utils/format.js";
|
|
7
|
+
import { isNarrow, isVeryNarrow } from "../utils/responsive.js";
|
|
8
|
+
|
|
9
|
+
interface OverviewViewProps {
|
|
10
|
+
data: TUIData;
|
|
11
|
+
sortBy: SortType;
|
|
12
|
+
sortDesc: boolean;
|
|
13
|
+
selectedIndex: Accessor<number>;
|
|
14
|
+
scrollOffset: Accessor<number>;
|
|
15
|
+
height: number;
|
|
16
|
+
width: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function OverviewView(props: OverviewViewProps) {
|
|
20
|
+
const safeHeight = () => Math.max(props.height, 12);
|
|
21
|
+
const chartHeight = () => Math.max(5, Math.floor(safeHeight() * 0.35));
|
|
22
|
+
const listHeight = () => Math.max(4, safeHeight() - chartHeight() - 4);
|
|
23
|
+
const itemsPerPage = () => Math.max(1, Math.floor(listHeight() / 2));
|
|
24
|
+
|
|
25
|
+
const isNarrowTerminal = () => isNarrow(props.width);
|
|
26
|
+
const isVeryNarrowTerminal = () => isVeryNarrow(props.width);
|
|
27
|
+
|
|
28
|
+
const legendModelLimit = () => isVeryNarrowTerminal() ? 3 : 5;
|
|
29
|
+
const topModelsForLegend = () => props.data.topModels.slice(0, legendModelLimit()).map(m => m.modelId);
|
|
30
|
+
|
|
31
|
+
const maxModelNameWidth = () => isVeryNarrowTerminal() ? 20 : isNarrowTerminal() ? 30 : 50;
|
|
32
|
+
|
|
33
|
+
const sortedModels = createMemo(() => {
|
|
34
|
+
const models = [...props.data.topModels];
|
|
35
|
+
return models.sort((a, b) => {
|
|
36
|
+
let cmp = 0;
|
|
37
|
+
if (props.sortBy === "cost") cmp = a.cost - b.cost;
|
|
38
|
+
else if (props.sortBy === "tokens") cmp = a.totalTokens - b.totalTokens;
|
|
39
|
+
return props.sortDesc ? -cmp : cmp;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const totalForPercentage = createMemo(() => {
|
|
44
|
+
const models = props.data.topModels;
|
|
45
|
+
if (props.sortBy === "tokens") {
|
|
46
|
+
return models.reduce((sum, m) => sum + m.totalTokens, 0) || 1;
|
|
47
|
+
}
|
|
48
|
+
return models.reduce((sum, m) => sum + m.cost, 0) || 1;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const getPercentage = (model: typeof props.data.topModels[0]) => {
|
|
52
|
+
if (props.sortBy === "tokens") {
|
|
53
|
+
return (model.totalTokens / totalForPercentage()) * 100;
|
|
54
|
+
}
|
|
55
|
+
return (model.cost / totalForPercentage()) * 100;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const visibleModels = () => sortedModels().slice(props.scrollOffset(), props.scrollOffset() + itemsPerPage());
|
|
59
|
+
const totalModels = () => sortedModels().length;
|
|
60
|
+
const endIndex = () => Math.min(props.scrollOffset() + visibleModels().length, totalModels());
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<box flexDirection="column" gap={1}>
|
|
64
|
+
<box flexDirection="column">
|
|
65
|
+
<BarChart data={props.data.chartData} width={props.width - 4} height={chartHeight()} />
|
|
66
|
+
<Legend models={topModelsForLegend()} width={props.width} />
|
|
67
|
+
</box>
|
|
68
|
+
|
|
69
|
+
<box flexDirection="column">
|
|
70
|
+
<box flexDirection="row" justifyContent="space-between" marginBottom={0}>
|
|
71
|
+
<text bold>{isVeryNarrowTerminal() ? "Top Models" : `Models by ${props.sortBy === "tokens" ? "Tokens" : "Cost"}`}</text>
|
|
72
|
+
<box flexDirection="row">
|
|
73
|
+
<text dim>{isVeryNarrowTerminal() ? "" : "Total: "}</text>
|
|
74
|
+
<text fg="green">{formatCost(props.data.totalCost)}</text>
|
|
75
|
+
</box>
|
|
76
|
+
</box>
|
|
77
|
+
|
|
78
|
+
<box flexDirection="column">
|
|
79
|
+
<For each={visibleModels()}>
|
|
80
|
+
{(model, i) => {
|
|
81
|
+
const isActive = createMemo(() => i() === props.selectedIndex());
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<ModelRow
|
|
85
|
+
modelId={model.modelId}
|
|
86
|
+
tokens={{
|
|
87
|
+
input: model.inputTokens,
|
|
88
|
+
output: model.outputTokens,
|
|
89
|
+
cacheRead: model.cacheReadTokens,
|
|
90
|
+
cacheWrite: model.cacheWriteTokens,
|
|
91
|
+
}}
|
|
92
|
+
percentage={getPercentage(model)}
|
|
93
|
+
isActive={isActive()}
|
|
94
|
+
compact={isVeryNarrowTerminal()}
|
|
95
|
+
maxNameWidth={maxModelNameWidth()}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}}
|
|
99
|
+
</For>
|
|
100
|
+
</box>
|
|
101
|
+
|
|
102
|
+
<Show when={totalModels() > visibleModels().length}>
|
|
103
|
+
<text dim>{`↓ ${props.scrollOffset() + 1}-${endIndex()} of ${totalModels()} models (↑↓ to scroll)`}</text>
|
|
104
|
+
</Show>
|
|
105
|
+
</box>
|
|
106
|
+
</box>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { For, Show, createMemo, createSignal } from "solid-js";
|
|
2
|
+
import type { TUIData } from "../hooks/useData.js";
|
|
3
|
+
import type { ColorPaletteName } from "../config/themes.js";
|
|
4
|
+
import { getPalette, getGradeColor } from "../config/themes.js";
|
|
5
|
+
import { getModelColor } from "../utils/colors.js";
|
|
6
|
+
import { formatTokens } from "../utils/format.js";
|
|
7
|
+
import { isNarrow } from "../utils/responsive.js";
|
|
8
|
+
import { DateBreakdownPanel } from "./DateBreakdownPanel.js";
|
|
9
|
+
|
|
10
|
+
interface StatsViewProps {
|
|
11
|
+
data: TUIData;
|
|
12
|
+
height: number;
|
|
13
|
+
colorPalette: ColorPaletteName;
|
|
14
|
+
width?: number;
|
|
15
|
+
selectedDate?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
19
|
+
const MONTHS_SHORT = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
|
|
20
|
+
const DAYS = ["", "Mon", "", "Wed", "", "Fri", ""];
|
|
21
|
+
|
|
22
|
+
interface MonthLabel {
|
|
23
|
+
month: string;
|
|
24
|
+
weekIndex: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function StatsView(props: StatsViewProps) {
|
|
28
|
+
const palette = () => getPalette(props.colorPalette);
|
|
29
|
+
const isNarrowTerminal = () => isNarrow(props.width);
|
|
30
|
+
const grid = () => props.data.contributionGrid;
|
|
31
|
+
const cellWidth = 2;
|
|
32
|
+
|
|
33
|
+
const [clickedCell, setClickedCell] = createSignal<string | null>(null);
|
|
34
|
+
|
|
35
|
+
const selectedBreakdown = createMemo(() => {
|
|
36
|
+
const date = clickedCell();
|
|
37
|
+
if (!date) return null;
|
|
38
|
+
if (!props.data.dailyBreakdowns) return null;
|
|
39
|
+
if (!(props.data.dailyBreakdowns instanceof Map)) return null;
|
|
40
|
+
return props.data.dailyBreakdowns.get(date) || null;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const monthPositions = createMemo(() => {
|
|
44
|
+
const sundayRow = grid()[0] || [];
|
|
45
|
+
if (sundayRow.length === 0) return [];
|
|
46
|
+
|
|
47
|
+
const positions: MonthLabel[] = [];
|
|
48
|
+
let lastMonth = -1;
|
|
49
|
+
const monthNames = isNarrowTerminal() ? MONTHS_SHORT : MONTHS;
|
|
50
|
+
|
|
51
|
+
for (let weekIdx = 0; weekIdx < sundayRow.length; weekIdx++) {
|
|
52
|
+
const cell = sundayRow[weekIdx];
|
|
53
|
+
if (!cell.date) continue;
|
|
54
|
+
const month = new Date(cell.date + "T00:00:00").getMonth();
|
|
55
|
+
if (month !== lastMonth) {
|
|
56
|
+
positions.push({ month: monthNames[month], weekIndex: weekIdx });
|
|
57
|
+
lastMonth = month;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return positions;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const totalWeeks = createMemo(() => (grid()[0] || []).length);
|
|
64
|
+
|
|
65
|
+
const monthLabelRow = createMemo(() => {
|
|
66
|
+
const weeks = totalWeeks();
|
|
67
|
+
const positions = monthPositions();
|
|
68
|
+
const chars: string[] = new Array(weeks * cellWidth).fill(" ");
|
|
69
|
+
|
|
70
|
+
for (const pos of positions) {
|
|
71
|
+
const startIdx = pos.weekIndex * cellWidth;
|
|
72
|
+
const monthChars = pos.month.split("");
|
|
73
|
+
for (let i = 0; i < monthChars.length && startIdx + i < chars.length; i++) {
|
|
74
|
+
chars[startIdx + i] = monthChars[i];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return chars.join("");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const dayLabelWidth = () => isNarrowTerminal() ? 2 : 4;
|
|
82
|
+
|
|
83
|
+
const isSelected = (cellDate: string | null) =>
|
|
84
|
+
cellDate && (clickedCell() === cellDate || props.selectedDate === cellDate);
|
|
85
|
+
|
|
86
|
+
const getCellColor = (level: number) =>
|
|
87
|
+
level === 0 ? "#666666" : getGradeColor(palette(), level as 0 | 1 | 2 | 3 | 4);
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<box flexDirection="column" gap={1}>
|
|
93
|
+
<box flexDirection="column">
|
|
94
|
+
<box flexDirection="row">
|
|
95
|
+
<text dim>{" ".repeat(dayLabelWidth())}</text>
|
|
96
|
+
<text dim>{monthLabelRow()}</text>
|
|
97
|
+
</box>
|
|
98
|
+
|
|
99
|
+
<box onMouseDown={(e: { x: number; y: number }) => {
|
|
100
|
+
const labelW = dayLabelWidth();
|
|
101
|
+
const col = Math.floor((e.x - labelW) / cellWidth);
|
|
102
|
+
const row = e.y - 2;
|
|
103
|
+
const gridRows = grid().length;
|
|
104
|
+
|
|
105
|
+
if (row < 0 || row >= gridRows) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (col < 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rowData = grid()[row];
|
|
113
|
+
if (!rowData || col >= rowData.length) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cell = rowData[col];
|
|
118
|
+
if (!cell?.date) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const newDate = clickedCell() === cell.date ? null : cell.date;
|
|
123
|
+
setClickedCell(newDate);
|
|
124
|
+
}}>
|
|
125
|
+
<For each={DAYS}>
|
|
126
|
+
{(day, dayIndex) => (
|
|
127
|
+
<box flexDirection="row">
|
|
128
|
+
<text dim>{isNarrowTerminal() ? " " : day.padStart(3) + " "}</text>
|
|
129
|
+
<For each={grid()[dayIndex()] || []}>
|
|
130
|
+
{(cell) => (
|
|
131
|
+
<text
|
|
132
|
+
fg={isSelected(cell.date) ? "#ffffff" : getCellColor(cell.level)}
|
|
133
|
+
bg={isSelected(cell.date) ? getCellColor(cell.level) : undefined}
|
|
134
|
+
>
|
|
135
|
+
{isSelected(cell.date) ? "▓▓" : (cell.level === 0 ? "· " : "██")}
|
|
136
|
+
</text>
|
|
137
|
+
)}
|
|
138
|
+
</For>
|
|
139
|
+
</box>
|
|
140
|
+
)}
|
|
141
|
+
</For>
|
|
142
|
+
</box>
|
|
143
|
+
</box>
|
|
144
|
+
|
|
145
|
+
<box flexDirection="row" gap={2} marginTop={1}>
|
|
146
|
+
<text dim>Less</text>
|
|
147
|
+
<box flexDirection="row" gap={0}>
|
|
148
|
+
<For each={[0, 1, 2, 3, 4]}>
|
|
149
|
+
{(level) => (
|
|
150
|
+
<text
|
|
151
|
+
fg={level === 0 ? "#666666" : getGradeColor(palette(), level as 0 | 1 | 2 | 3 | 4)}
|
|
152
|
+
>
|
|
153
|
+
{level === 0 ? "· " : "██"}
|
|
154
|
+
</text>
|
|
155
|
+
)}
|
|
156
|
+
</For>
|
|
157
|
+
</box>
|
|
158
|
+
<text dim>More</text>
|
|
159
|
+
<Show when={!isNarrowTerminal()}>
|
|
160
|
+
<text dim>|</text>
|
|
161
|
+
<text dim>Click on a day to see breakdown</text>
|
|
162
|
+
</Show>
|
|
163
|
+
</box>
|
|
164
|
+
|
|
165
|
+
<Show when={selectedBreakdown()}>
|
|
166
|
+
<DateBreakdownPanel breakdown={selectedBreakdown()!} isNarrow={isNarrowTerminal()} />
|
|
167
|
+
</Show>
|
|
168
|
+
|
|
169
|
+
<Show when={!selectedBreakdown()}>
|
|
170
|
+
<box flexDirection="column" marginTop={1}>
|
|
171
|
+
<box flexDirection={isNarrowTerminal() ? "column" : "row"} gap={isNarrowTerminal() ? 0 : 4}>
|
|
172
|
+
<box flexDirection="column">
|
|
173
|
+
<box flexDirection="row" gap={1}>
|
|
174
|
+
<text dim>{isNarrowTerminal() ? "Model:" : "Favorite model:"}</text>
|
|
175
|
+
<text fg={getModelColor(props.data.stats.favoriteModel)}>{props.data.stats.favoriteModel}</text>
|
|
176
|
+
</box>
|
|
177
|
+
<box flexDirection="row" gap={1}>
|
|
178
|
+
<text dim>Sessions:</text>
|
|
179
|
+
<text fg="cyan">{props.data.stats.sessions.toLocaleString()}</text>
|
|
180
|
+
</box>
|
|
181
|
+
<box flexDirection="row" gap={1}>
|
|
182
|
+
<text dim>{isNarrowTerminal() ? "Streak:" : "Current streak:"}</text>
|
|
183
|
+
<text fg="cyan">{`${props.data.stats.currentStreak} days`}</text>
|
|
184
|
+
</box>
|
|
185
|
+
<box flexDirection="row" gap={1}>
|
|
186
|
+
<text dim>{isNarrowTerminal() ? "Active:" : "Active days:"}</text>
|
|
187
|
+
<text fg="cyan">{`${props.data.stats.activeDays}/${props.data.stats.totalDays}`}</text>
|
|
188
|
+
</box>
|
|
189
|
+
</box>
|
|
190
|
+
|
|
191
|
+
<box flexDirection="column">
|
|
192
|
+
<box flexDirection="row" gap={1}>
|
|
193
|
+
<text dim>{isNarrowTerminal() ? "Tokens:" : "Total tokens:"}</text>
|
|
194
|
+
<text fg="cyan">{formatTokens(props.data.stats.totalTokens)}</text>
|
|
195
|
+
</box>
|
|
196
|
+
<box flexDirection="row" gap={1}>
|
|
197
|
+
<text dim>{isNarrowTerminal() ? "Session:" : "Longest session:"}</text>
|
|
198
|
+
<text fg="cyan">{props.data.stats.longestSession}</text>
|
|
199
|
+
</box>
|
|
200
|
+
<box flexDirection="row" gap={1}>
|
|
201
|
+
<text dim>{isNarrowTerminal() ? "Max streak:" : "Longest streak:"}</text>
|
|
202
|
+
<text fg="cyan">{`${props.data.stats.longestStreak} days`}</text>
|
|
203
|
+
</box>
|
|
204
|
+
<box flexDirection="row" gap={1}>
|
|
205
|
+
<text dim>{isNarrowTerminal() ? "Peak:" : "Peak hour:"}</text>
|
|
206
|
+
<text fg="cyan">{props.data.stats.peakHour}</text>
|
|
207
|
+
</box>
|
|
208
|
+
</box>
|
|
209
|
+
</box>
|
|
210
|
+
</box>
|
|
211
|
+
|
|
212
|
+
<Show when={!isNarrowTerminal()}>
|
|
213
|
+
<box marginTop={1}>
|
|
214
|
+
<text fg="yellow" italic>{`Your total spending is $${props.data.totalCost.toFixed(2)} on AI coding assistants!`}</text>
|
|
215
|
+
</box>
|
|
216
|
+
</Show>
|
|
217
|
+
<Show when={isNarrowTerminal()}>
|
|
218
|
+
<box marginTop={1}>
|
|
219
|
+
<text fg="yellow" italic>{`Total: $${props.data.totalCost.toFixed(2)}`}</text>
|
|
220
|
+
</box>
|
|
221
|
+
</Show>
|
|
222
|
+
</Show>
|
|
223
|
+
</box>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { formatTokens, formatTokensCompact } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
export interface TokenBreakdownData {
|
|
4
|
+
input: number;
|
|
5
|
+
output: number;
|
|
6
|
+
cacheRead: number;
|
|
7
|
+
cacheWrite: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TokenBreakdownProps {
|
|
11
|
+
tokens: TokenBreakdownData;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
indent?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TokenBreakdown(props: TokenBreakdownProps) {
|
|
17
|
+
const indentStr = () => " ".repeat(props.indent ?? 0);
|
|
18
|
+
|
|
19
|
+
if (props.compact) {
|
|
20
|
+
return (
|
|
21
|
+
<box flexDirection="row">
|
|
22
|
+
<text fg="#666666">{indentStr()}</text>
|
|
23
|
+
<text fg="#AAAAAA">{formatTokensCompact(props.tokens.input)}</text>
|
|
24
|
+
<text fg="#666666">{"/"}</text>
|
|
25
|
+
<text fg="#AAAAAA">{formatTokensCompact(props.tokens.output)}</text>
|
|
26
|
+
<text fg="#666666">{"/"}</text>
|
|
27
|
+
<text fg="#AAAAAA">{formatTokensCompact(props.tokens.cacheRead)}</text>
|
|
28
|
+
<text fg="#666666">{"/"}</text>
|
|
29
|
+
<text fg="#AAAAAA">{formatTokensCompact(props.tokens.cacheWrite)}</text>
|
|
30
|
+
</box>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<box flexDirection="row">
|
|
36
|
+
<text fg="#666666">{`${indentStr()}In: `}</text>
|
|
37
|
+
<text fg="#AAAAAA">{formatTokens(props.tokens.input)}</text>
|
|
38
|
+
<text fg="#666666">{" · Out: "}</text>
|
|
39
|
+
<text fg="#AAAAAA">{formatTokens(props.tokens.output)}</text>
|
|
40
|
+
<text fg="#666666">{" · CR: "}</text>
|
|
41
|
+
<text fg="#AAAAAA">{formatTokens(props.tokens.cacheRead)}</text>
|
|
42
|
+
<text fg="#666666">{" · CW: "}</text>
|
|
43
|
+
<text fg="#AAAAAA">{formatTokens(props.tokens.cacheWrite)}</text>
|
|
44
|
+
</box>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Header } from "./Header.js";
|
|
2
|
+
export { Footer } from "./Footer.js";
|
|
3
|
+
export { OverviewView } from "./OverviewView.js";
|
|
4
|
+
export { ModelView } from "./ModelView.js";
|
|
5
|
+
export { DailyView } from "./DailyView.js";
|
|
6
|
+
export { StatsView } from "./StatsView.js";
|
|
7
|
+
export { BarChart } from "./BarChart.js";
|
|
8
|
+
export type { ChartDataPoint } from "./BarChart.js";
|
|
9
|
+
export { Legend } from "./Legend.js";
|
|
10
|
+
export { LoadingSpinner } from "./LoadingSpinner.js";
|
|
11
|
+
export { ModelRow } from "./ModelRow.js";
|
|
12
|
+
export { TokenBreakdown } from "./TokenBreakdown.js";
|
|
13
|
+
export type { TokenBreakdownData } from "./TokenBreakdown.js";
|
|
14
|
+
export { DateBreakdownPanel } from "./DateBreakdownPanel.js";
|
|
15
|
+
export type { DateBreakdownPanelProps } from "./DateBreakdownPanel.js";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
4
|
+
import type { TUIData, DailyModelBreakdown } from "../types/index.js";
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = join(homedir(), ".config", "tokscale");
|
|
7
|
+
const CACHE_DIR = join(homedir(), ".cache", "tokscale");
|
|
8
|
+
const CONFIG_FILE = join(CONFIG_DIR, "tui-settings.json");
|
|
9
|
+
const CACHE_FILE = join(CACHE_DIR, "tui-data-cache.json");
|
|
10
|
+
|
|
11
|
+
const CACHE_STALE_THRESHOLD_MS = 60 * 1000;
|
|
12
|
+
|
|
13
|
+
interface TUISettings {
|
|
14
|
+
colorPalette: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CachedTUIData {
|
|
18
|
+
timestamp: number;
|
|
19
|
+
enabledSources: string[];
|
|
20
|
+
data: Omit<TUIData, 'dailyBreakdowns'> & {
|
|
21
|
+
dailyBreakdowns: Array<[string, DailyModelBreakdown]>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadSettings(): TUISettings {
|
|
26
|
+
try {
|
|
27
|
+
if (existsSync(CONFIG_FILE)) {
|
|
28
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
return { colorPalette: "green" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function saveSettings(settings: TUISettings): void {
|
|
36
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
37
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(settings, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sourcesMatch(enabledSources: Set<string>, cachedSources: string[]): boolean {
|
|
43
|
+
const cachedSet = new Set(cachedSources);
|
|
44
|
+
if (enabledSources.size !== cachedSet.size) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
for (const source of enabledSources) {
|
|
48
|
+
if (!cachedSet.has(source)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function loadCachedData(enabledSources: Set<string>): TUIData | null {
|
|
56
|
+
try {
|
|
57
|
+
if (!existsSync(CACHE_FILE)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cached: CachedTUIData = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
62
|
+
|
|
63
|
+
if (!sourcesMatch(enabledSources, cached.enabledSources)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!cached.data.dailyBreakdowns || !Array.isArray(cached.data.dailyBreakdowns)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...cached.data,
|
|
73
|
+
dailyBreakdowns: new Map(cached.data.dailyBreakdowns),
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function saveCachedData(data: TUIData, enabledSources: Set<string>): void {
|
|
81
|
+
try {
|
|
82
|
+
if (!existsSync(CACHE_DIR)) {
|
|
83
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const serializableBreakdowns = Array.from(data.dailyBreakdowns.entries());
|
|
87
|
+
const cached: CachedTUIData = {
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
enabledSources: Array.from(enabledSources),
|
|
90
|
+
data: {
|
|
91
|
+
...data,
|
|
92
|
+
dailyBreakdowns: serializableBreakdowns,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cached));
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isCacheStale(enabledSources: Set<string>): boolean {
|
|
102
|
+
try {
|
|
103
|
+
if (!existsSync(CACHE_FILE)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cached: CachedTUIData = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
108
|
+
const cacheAge = Date.now() - cached.timestamp;
|
|
109
|
+
|
|
110
|
+
if (!sourcesMatch(enabledSources, cached.enabledSources)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return cacheAge > CACHE_STALE_THRESHOLD_MS;
|
|
115
|
+
} catch {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getCacheTimestamp(): number | null {
|
|
121
|
+
try {
|
|
122
|
+
if (!existsSync(CACHE_FILE)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const cached: CachedTUIData = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
126
|
+
return cached.timestamp;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export interface GraphColorPalette {
|
|
2
|
+
name: string;
|
|
3
|
+
grade0: string;
|
|
4
|
+
grade1: string;
|
|
5
|
+
grade2: string;
|
|
6
|
+
grade3: string;
|
|
7
|
+
grade4: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ColorPaletteName =
|
|
11
|
+
| "green"
|
|
12
|
+
| "halloween"
|
|
13
|
+
| "teal"
|
|
14
|
+
| "blue"
|
|
15
|
+
| "pink"
|
|
16
|
+
| "purple"
|
|
17
|
+
| "orange"
|
|
18
|
+
| "monochrome"
|
|
19
|
+
| "YlGnBu";
|
|
20
|
+
|
|
21
|
+
const GRAPH_EMPTY = "#1F1F20";
|
|
22
|
+
|
|
23
|
+
export const colorPalettes: Record<ColorPaletteName, GraphColorPalette> = {
|
|
24
|
+
green: {
|
|
25
|
+
name: "Green",
|
|
26
|
+
grade0: GRAPH_EMPTY,
|
|
27
|
+
grade1: "#9be9a8",
|
|
28
|
+
grade2: "#40c463",
|
|
29
|
+
grade3: "#30a14e",
|
|
30
|
+
grade4: "#216e39",
|
|
31
|
+
},
|
|
32
|
+
halloween: {
|
|
33
|
+
name: "Halloween",
|
|
34
|
+
grade0: GRAPH_EMPTY,
|
|
35
|
+
grade1: "#FFEE4A",
|
|
36
|
+
grade2: "#FFC501",
|
|
37
|
+
grade3: "#FE9600",
|
|
38
|
+
grade4: "#03001C",
|
|
39
|
+
},
|
|
40
|
+
teal: {
|
|
41
|
+
name: "Teal",
|
|
42
|
+
grade0: GRAPH_EMPTY,
|
|
43
|
+
grade1: "#7ee5e5",
|
|
44
|
+
grade2: "#2dc5c5",
|
|
45
|
+
grade3: "#0d9e9e",
|
|
46
|
+
grade4: "#0e6d6d",
|
|
47
|
+
},
|
|
48
|
+
blue: {
|
|
49
|
+
name: "Blue",
|
|
50
|
+
grade0: GRAPH_EMPTY,
|
|
51
|
+
grade1: "#79b8ff",
|
|
52
|
+
grade2: "#388bfd",
|
|
53
|
+
grade3: "#1f6feb",
|
|
54
|
+
grade4: "#0d419d",
|
|
55
|
+
},
|
|
56
|
+
pink: {
|
|
57
|
+
name: "Pink",
|
|
58
|
+
grade0: GRAPH_EMPTY,
|
|
59
|
+
grade1: "#f0b5d2",
|
|
60
|
+
grade2: "#d961a0",
|
|
61
|
+
grade3: "#bf4b8a",
|
|
62
|
+
grade4: "#99286e",
|
|
63
|
+
},
|
|
64
|
+
purple: {
|
|
65
|
+
name: "Purple",
|
|
66
|
+
grade0: GRAPH_EMPTY,
|
|
67
|
+
grade1: "#cdb4ff",
|
|
68
|
+
grade2: "#a371f7",
|
|
69
|
+
grade3: "#8957e5",
|
|
70
|
+
grade4: "#6e40c9",
|
|
71
|
+
},
|
|
72
|
+
orange: {
|
|
73
|
+
name: "Orange",
|
|
74
|
+
grade0: GRAPH_EMPTY,
|
|
75
|
+
grade1: "#ffd699",
|
|
76
|
+
grade2: "#ffb347",
|
|
77
|
+
grade3: "#ff8c00",
|
|
78
|
+
grade4: "#cc5500",
|
|
79
|
+
},
|
|
80
|
+
monochrome: {
|
|
81
|
+
name: "Monochrome",
|
|
82
|
+
grade0: GRAPH_EMPTY,
|
|
83
|
+
grade1: "#9e9e9e",
|
|
84
|
+
grade2: "#757575",
|
|
85
|
+
grade3: "#424242",
|
|
86
|
+
grade4: "#212121",
|
|
87
|
+
},
|
|
88
|
+
YlGnBu: {
|
|
89
|
+
name: "YlGnBu",
|
|
90
|
+
grade0: GRAPH_EMPTY,
|
|
91
|
+
grade1: "#a1dab4",
|
|
92
|
+
grade2: "#41b6c4",
|
|
93
|
+
grade3: "#2c7fb8",
|
|
94
|
+
grade4: "#253494",
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const DEFAULT_PALETTE: ColorPaletteName = "green";
|
|
99
|
+
|
|
100
|
+
export const getPaletteNames = (): ColorPaletteName[] =>
|
|
101
|
+
Object.keys(colorPalettes) as ColorPaletteName[];
|
|
102
|
+
|
|
103
|
+
export const getPalette = (name: ColorPaletteName): GraphColorPalette =>
|
|
104
|
+
colorPalettes[name] || colorPalettes[DEFAULT_PALETTE];
|
|
105
|
+
|
|
106
|
+
export const getGradeColor = (
|
|
107
|
+
palette: GraphColorPalette,
|
|
108
|
+
intensity: 0 | 1 | 2 | 3 | 4
|
|
109
|
+
): string => {
|
|
110
|
+
return (
|
|
111
|
+
[palette.grade0, palette.grade1, palette.grade2, palette.grade3, palette.grade4][
|
|
112
|
+
intensity
|
|
113
|
+
] || palette.grade0
|
|
114
|
+
);
|
|
115
|
+
};
|