@tokscale/cli 1.4.3 → 2.0.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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -26
- package/dist/auth.d.ts +0 -17
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -162
- package/dist/auth.js.map +0 -1
- package/dist/cli.d.ts +0 -9
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -1550
- package/dist/cli.js.map +0 -1
- package/dist/credentials.d.ts +0 -50
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js +0 -151
- package/dist/credentials.js.map +0 -1
- package/dist/cursor.d.ts +0 -167
- package/dist/cursor.d.ts.map +0 -1
- package/dist/cursor.js +0 -906
- package/dist/cursor.js.map +0 -1
- package/dist/date-utils.d.ts +0 -10
- package/dist/date-utils.d.ts.map +0 -1
- package/dist/date-utils.js +0 -47
- package/dist/date-utils.js.map +0 -1
- package/dist/graph-types.d.ts +0 -142
- package/dist/graph-types.d.ts.map +0 -1
- package/dist/graph-types.js +0 -6
- package/dist/graph-types.js.map +0 -1
- package/dist/native-runner.d.ts +0 -11
- package/dist/native-runner.d.ts.map +0 -1
- package/dist/native-runner.js +0 -77
- package/dist/native-runner.js.map +0 -1
- package/dist/native.d.ts +0 -106
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -302
- package/dist/native.js.map +0 -1
- package/dist/sessions/types.d.ts +0 -28
- package/dist/sessions/types.d.ts.map +0 -1
- package/dist/sessions/types.js +0 -27
- package/dist/sessions/types.js.map +0 -1
- package/dist/spinner.d.ts +0 -75
- package/dist/spinner.d.ts.map +0 -1
- package/dist/spinner.js +0 -203
- package/dist/spinner.js.map +0 -1
- package/dist/submit.d.ts +0 -23
- package/dist/submit.d.ts.map +0 -1
- package/dist/submit.js +0 -294
- package/dist/submit.js.map +0 -1
- package/dist/table.d.ts +0 -42
- package/dist/table.d.ts.map +0 -1
- package/dist/table.js +0 -181
- package/dist/table.js.map +0 -1
- package/dist/tui/App.d.ts +0 -4
- package/dist/tui/App.d.ts.map +0 -1
- package/dist/tui/App.js +0 -333
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/components/BarChart.d.ts +0 -17
- package/dist/tui/components/BarChart.d.ts.map +0 -1
- package/dist/tui/components/BarChart.js +0 -146
- package/dist/tui/components/BarChart.js.map +0 -1
- package/dist/tui/components/DailyView.d.ts +0 -13
- package/dist/tui/components/DailyView.d.ts.map +0 -1
- package/dist/tui/components/DailyView.js +0 -86
- package/dist/tui/components/DailyView.js.map +0 -1
- package/dist/tui/components/DateBreakdownPanel.d.ts +0 -7
- package/dist/tui/components/DateBreakdownPanel.d.ts.map +0 -1
- package/dist/tui/components/DateBreakdownPanel.js +0 -36
- package/dist/tui/components/DateBreakdownPanel.js.map +0 -1
- package/dist/tui/components/Footer.d.ts +0 -28
- package/dist/tui/components/Footer.d.ts.map +0 -1
- package/dist/tui/components/Footer.js +0 -130
- package/dist/tui/components/Footer.js.map +0 -1
- package/dist/tui/components/Header.d.ts +0 -9
- package/dist/tui/components/Header.d.ts.map +0 -1
- package/dist/tui/components/Header.js +0 -20
- package/dist/tui/components/Header.js.map +0 -1
- package/dist/tui/components/Legend.d.ts +0 -7
- package/dist/tui/components/Legend.d.ts.map +0 -1
- package/dist/tui/components/Legend.js +0 -16
- package/dist/tui/components/Legend.js.map +0 -1
- package/dist/tui/components/LoadingSpinner.d.ts +0 -8
- package/dist/tui/components/LoadingSpinner.d.ts.map +0 -1
- package/dist/tui/components/LoadingSpinner.js +0 -55
- package/dist/tui/components/LoadingSpinner.js.map +0 -1
- package/dist/tui/components/ModelRow.d.ts +0 -13
- package/dist/tui/components/ModelRow.d.ts.map +0 -1
- package/dist/tui/components/ModelRow.js +0 -15
- package/dist/tui/components/ModelRow.js.map +0 -1
- package/dist/tui/components/ModelView.d.ts +0 -13
- package/dist/tui/components/ModelView.d.ts.map +0 -1
- package/dist/tui/components/ModelView.js +0 -96
- package/dist/tui/components/ModelView.js.map +0 -1
- package/dist/tui/components/OverviewView.d.ts +0 -14
- package/dist/tui/components/OverviewView.d.ts.map +0 -1
- package/dist/tui/components/OverviewView.js +0 -65
- package/dist/tui/components/OverviewView.js.map +0 -1
- package/dist/tui/components/StatsView.d.ts +0 -14
- package/dist/tui/components/StatsView.d.ts.map +0 -1
- package/dist/tui/components/StatsView.js +0 -102
- package/dist/tui/components/StatsView.js.map +0 -1
- package/dist/tui/components/TokenBreakdown.d.ts +0 -14
- package/dist/tui/components/TokenBreakdown.d.ts.map +0 -1
- package/dist/tui/components/TokenBreakdown.js +0 -10
- package/dist/tui/components/TokenBreakdown.js.map +0 -1
- package/dist/tui/components/index.d.ts +0 -16
- package/dist/tui/components/index.d.ts.map +0 -1
- package/dist/tui/components/index.js +0 -13
- package/dist/tui/components/index.js.map +0 -1
- package/dist/tui/config/settings.d.ts +0 -15
- package/dist/tui/config/settings.d.ts.map +0 -1
- package/dist/tui/config/settings.js +0 -147
- package/dist/tui/config/settings.js.map +0 -1
- package/dist/tui/config/themes.d.ts +0 -15
- package/dist/tui/config/themes.d.ts.map +0 -1
- package/dist/tui/config/themes.js +0 -82
- package/dist/tui/config/themes.js.map +0 -1
- package/dist/tui/hooks/useData.d.ts +0 -19
- package/dist/tui/hooks/useData.d.ts.map +0 -1
- package/dist/tui/hooks/useData.js +0 -468
- package/dist/tui/hooks/useData.js.map +0 -1
- package/dist/tui/index.d.ts +0 -4
- package/dist/tui/index.d.ts.map +0 -1
- package/dist/tui/index.js +0 -36
- package/dist/tui/index.js.map +0 -1
- package/dist/tui/types/index.d.ts +0 -137
- package/dist/tui/types/index.d.ts.map +0 -1
- package/dist/tui/types/index.js +0 -26
- package/dist/tui/types/index.js.map +0 -1
- package/dist/tui/utils/cleanup.d.ts +0 -22
- package/dist/tui/utils/cleanup.d.ts.map +0 -1
- package/dist/tui/utils/cleanup.js +0 -59
- package/dist/tui/utils/cleanup.js.map +0 -1
- package/dist/tui/utils/colors.d.ts +0 -19
- package/dist/tui/utils/colors.d.ts.map +0 -1
- package/dist/tui/utils/colors.js +0 -71
- package/dist/tui/utils/colors.js.map +0 -1
- package/dist/tui/utils/format.d.ts +0 -7
- package/dist/tui/utils/format.d.ts.map +0 -1
- package/dist/tui/utils/format.js +0 -45
- package/dist/tui/utils/format.js.map +0 -1
- package/dist/tui/utils/responsive.d.ts +0 -5
- package/dist/tui/utils/responsive.d.ts.map +0 -1
- package/dist/tui/utils/responsive.js +0 -5
- package/dist/tui/utils/responsive.js.map +0 -1
- package/dist/wrapped.d.ts +0 -43
- package/dist/wrapped.d.ts.map +0 -1
- package/dist/wrapped.js +0 -719
- package/dist/wrapped.js.map +0 -1
- package/src/auth.ts +0 -211
- package/src/cli.ts +0 -1892
- package/src/credentials.ts +0 -176
- package/src/cursor.ts +0 -1044
- package/src/date-utils.ts +0 -51
- package/src/graph-types.ts +0 -175
- package/src/native-runner.js +0 -4
- package/src/native-runner.ts +0 -91
- package/src/native.ts +0 -633
- package/src/sessions/types.ts +0 -59
- package/src/spinner.ts +0 -283
- package/src/submit.ts +0 -360
- package/src/table.ts +0 -233
- package/src/tui/App.tsx +0 -453
- package/src/tui/components/BarChart.tsx +0 -205
- package/src/tui/components/DailyView.tsx +0 -132
- package/src/tui/components/DateBreakdownPanel.tsx +0 -79
- package/src/tui/components/Footer.tsx +0 -380
- package/src/tui/components/Header.tsx +0 -68
- package/src/tui/components/Legend.tsx +0 -39
- package/src/tui/components/LoadingSpinner.tsx +0 -81
- package/src/tui/components/ModelRow.tsx +0 -47
- package/src/tui/components/ModelView.tsx +0 -147
- package/src/tui/components/OverviewView.tsx +0 -121
- package/src/tui/components/StatsView.tsx +0 -249
- package/src/tui/components/TokenBreakdown.tsx +0 -46
- package/src/tui/components/index.ts +0 -15
- package/src/tui/config/settings.ts +0 -183
- package/src/tui/config/themes.ts +0 -115
- package/src/tui/hooks/useData.ts +0 -558
- package/src/tui/index.tsx +0 -44
- package/src/tui/opentui.d.ts +0 -166
- package/src/tui/types/index.ts +0 -173
- package/src/tui/utils/cleanup.ts +0 -65
- package/src/tui/utils/colors.ts +0 -78
- package/src/tui/utils/format.ts +0 -36
- package/src/tui/utils/responsive.ts +0 -8
- package/src/types.d.ts +0 -28
- package/src/wrapped.ts +0 -848
package/src/table.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dynamic width table rendering (inspired by ccusage)
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import Table from "cli-table3";
|
|
6
|
-
import pc from "picocolors";
|
|
7
|
-
import stringWidth from "string-width";
|
|
8
|
-
|
|
9
|
-
export type TableCellAlign = "left" | "right" | "center";
|
|
10
|
-
export type TableRow = (string | number | { content: string; hAlign?: TableCellAlign })[];
|
|
11
|
-
|
|
12
|
-
export interface TableOptions {
|
|
13
|
-
head: string[];
|
|
14
|
-
colAligns?: TableCellAlign[];
|
|
15
|
-
style?: { head?: string[] };
|
|
16
|
-
compactHead?: string[];
|
|
17
|
-
compactColAligns?: TableCellAlign[];
|
|
18
|
-
compactThreshold?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class ResponsiveTable {
|
|
22
|
-
private head: string[];
|
|
23
|
-
private rows: TableRow[] = [];
|
|
24
|
-
private colAligns: TableCellAlign[];
|
|
25
|
-
private style?: { head?: string[] };
|
|
26
|
-
private compactHead?: string[];
|
|
27
|
-
private compactColAligns?: TableCellAlign[];
|
|
28
|
-
private compactThreshold: number;
|
|
29
|
-
private compactMode = false;
|
|
30
|
-
|
|
31
|
-
constructor(options: TableOptions) {
|
|
32
|
-
this.head = options.head;
|
|
33
|
-
this.colAligns = options.colAligns ?? Array.from({ length: this.head.length }, () => "left");
|
|
34
|
-
this.style = options.style;
|
|
35
|
-
this.compactHead = options.compactHead;
|
|
36
|
-
this.compactColAligns = options.compactColAligns;
|
|
37
|
-
this.compactThreshold = options.compactThreshold ?? 100;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
push(row: TableRow): void {
|
|
41
|
-
this.rows.push(row);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private filterRowToCompact(row: TableRow, compactIndices: number[]): TableRow {
|
|
45
|
-
return compactIndices.map((index) => row[index] ?? "");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private getCurrentTableConfig(): { head: string[]; colAligns: TableCellAlign[] } {
|
|
49
|
-
if (this.compactMode && this.compactHead && this.compactColAligns) {
|
|
50
|
-
return { head: this.compactHead, colAligns: this.compactColAligns };
|
|
51
|
-
}
|
|
52
|
-
return { head: this.head, colAligns: this.colAligns };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private getCompactIndices(): number[] {
|
|
56
|
-
if (!this.compactHead || !this.compactMode) {
|
|
57
|
-
return Array.from({ length: this.head.length }, (_, i) => i);
|
|
58
|
-
}
|
|
59
|
-
return this.compactHead.map((compactHeader) => {
|
|
60
|
-
const index = this.head.indexOf(compactHeader);
|
|
61
|
-
return index < 0 ? 0 : index;
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
toString(): string {
|
|
66
|
-
const terminalWidth =
|
|
67
|
-
Number.parseInt(process.env.COLUMNS ?? "", 10) || process.stdout.columns || 120;
|
|
68
|
-
|
|
69
|
-
this.compactMode = terminalWidth < this.compactThreshold && this.compactHead != null;
|
|
70
|
-
|
|
71
|
-
const { head, colAligns } = this.getCurrentTableConfig();
|
|
72
|
-
const compactIndices = this.getCompactIndices();
|
|
73
|
-
|
|
74
|
-
const processedRows = this.compactMode
|
|
75
|
-
? this.rows.map((row) => this.filterRowToCompact(row, compactIndices))
|
|
76
|
-
: this.rows;
|
|
77
|
-
|
|
78
|
-
const allRows = [
|
|
79
|
-
head.map(String),
|
|
80
|
-
...processedRows.map((row) =>
|
|
81
|
-
row.map((cell) => {
|
|
82
|
-
if (typeof cell === "object" && cell != null && "content" in cell) {
|
|
83
|
-
return String(cell.content);
|
|
84
|
-
}
|
|
85
|
-
return String(cell ?? "");
|
|
86
|
-
})
|
|
87
|
-
),
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
const contentWidths = head.map((_, colIndex) => {
|
|
91
|
-
const maxLength = Math.max(...allRows.map((row) => stringWidth(String(row[colIndex] ?? ""))));
|
|
92
|
-
return maxLength;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const numColumns = head.length;
|
|
96
|
-
const tableOverhead = 3 * numColumns + 1;
|
|
97
|
-
const availableWidth = terminalWidth - tableOverhead;
|
|
98
|
-
|
|
99
|
-
const columnWidths = contentWidths.map((width, index) => {
|
|
100
|
-
const align = colAligns[index];
|
|
101
|
-
if (align === "right") {
|
|
102
|
-
return Math.max(width + 3, 11);
|
|
103
|
-
} else if (index === 1) {
|
|
104
|
-
return Math.max(width + 2, 15);
|
|
105
|
-
}
|
|
106
|
-
return Math.max(width + 2, 10);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const totalRequiredWidth = columnWidths.reduce((sum, width) => sum + width, 0) + tableOverhead;
|
|
110
|
-
|
|
111
|
-
let finalWidths = columnWidths;
|
|
112
|
-
if (totalRequiredWidth > terminalWidth) {
|
|
113
|
-
const scaleFactor = availableWidth / columnWidths.reduce((sum, width) => sum + width, 0);
|
|
114
|
-
finalWidths = columnWidths.map((width, index) => {
|
|
115
|
-
const align = colAligns[index];
|
|
116
|
-
let adjustedWidth = Math.floor(width * scaleFactor);
|
|
117
|
-
if (align === "right") {
|
|
118
|
-
adjustedWidth = Math.max(adjustedWidth, 10);
|
|
119
|
-
} else if (index === 0) {
|
|
120
|
-
adjustedWidth = Math.max(adjustedWidth, 10);
|
|
121
|
-
} else {
|
|
122
|
-
adjustedWidth = Math.max(adjustedWidth, 8);
|
|
123
|
-
}
|
|
124
|
-
return adjustedWidth;
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const table = new Table({
|
|
129
|
-
head,
|
|
130
|
-
style: this.style,
|
|
131
|
-
colAligns,
|
|
132
|
-
colWidths: finalWidths,
|
|
133
|
-
wordWrap: true,
|
|
134
|
-
wrapOnWordBoundary: true,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
for (const row of processedRows) {
|
|
138
|
-
table.push(row as any);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return table.toString();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function formatNumber(num: number): string {
|
|
146
|
-
return num.toLocaleString("en-US");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function formatCurrency(amount: number): string {
|
|
150
|
-
return `$${amount.toFixed(2)}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function formatModelName(modelName: string): string {
|
|
154
|
-
// claude-sonnet-4-20250514 -> sonnet-4
|
|
155
|
-
// claude-opus-4-5-20251101 -> opus-4-5
|
|
156
|
-
const match = modelName.match(/claude-(\w+)-([\d-]+)-(\d{8})/);
|
|
157
|
-
if (match) {
|
|
158
|
-
return `${match[1]}-${match[2]}`;
|
|
159
|
-
}
|
|
160
|
-
// Handle OpenCode style: claude-opus-4-5-high -> opus-4-5-high
|
|
161
|
-
const openCodeMatch = modelName.match(/claude-(\w+)-(.+)/);
|
|
162
|
-
if (openCodeMatch) {
|
|
163
|
-
return `${openCodeMatch[1]}-${openCodeMatch[2]}`;
|
|
164
|
-
}
|
|
165
|
-
return modelName;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function formatModelsMultiline(models: string[]): string {
|
|
169
|
-
const unique = [...new Set(models.map(formatModelName))];
|
|
170
|
-
return unique.sort().map((m) => `- ${m}`).join("\n");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function createUsageTable(firstColumnName: string): ResponsiveTable {
|
|
174
|
-
return new ResponsiveTable({
|
|
175
|
-
head: [
|
|
176
|
-
firstColumnName,
|
|
177
|
-
"Models",
|
|
178
|
-
"Input",
|
|
179
|
-
"Output",
|
|
180
|
-
"Cache Write",
|
|
181
|
-
"Cache Read",
|
|
182
|
-
"Total",
|
|
183
|
-
"Cost",
|
|
184
|
-
],
|
|
185
|
-
style: { head: ["cyan"] },
|
|
186
|
-
colAligns: ["left", "left", "right", "right", "right", "right", "right", "right"],
|
|
187
|
-
compactHead: [firstColumnName, "Models", "Input", "Output", "Cost"],
|
|
188
|
-
compactColAligns: ["left", "left", "right", "right", "right"],
|
|
189
|
-
compactThreshold: 100,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function formatUsageRow(
|
|
194
|
-
firstCol: string,
|
|
195
|
-
models: string[],
|
|
196
|
-
input: number,
|
|
197
|
-
output: number,
|
|
198
|
-
cacheWrite: number,
|
|
199
|
-
cacheRead: number,
|
|
200
|
-
cost: number
|
|
201
|
-
): TableRow {
|
|
202
|
-
const total = input + output + cacheWrite + cacheRead;
|
|
203
|
-
return [
|
|
204
|
-
firstCol,
|
|
205
|
-
formatModelsMultiline(models),
|
|
206
|
-
formatNumber(input),
|
|
207
|
-
formatNumber(output),
|
|
208
|
-
formatNumber(cacheWrite),
|
|
209
|
-
formatNumber(cacheRead),
|
|
210
|
-
formatNumber(total),
|
|
211
|
-
formatCurrency(cost),
|
|
212
|
-
];
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function formatTotalsRow(
|
|
216
|
-
input: number,
|
|
217
|
-
output: number,
|
|
218
|
-
cacheWrite: number,
|
|
219
|
-
cacheRead: number,
|
|
220
|
-
cost: number
|
|
221
|
-
): TableRow {
|
|
222
|
-
const total = input + output + cacheWrite + cacheRead;
|
|
223
|
-
return [
|
|
224
|
-
pc.yellow("Total"),
|
|
225
|
-
"",
|
|
226
|
-
pc.yellow(formatNumber(input)),
|
|
227
|
-
pc.yellow(formatNumber(output)),
|
|
228
|
-
pc.yellow(formatNumber(cacheWrite)),
|
|
229
|
-
pc.yellow(formatNumber(cacheRead)),
|
|
230
|
-
pc.yellow(formatNumber(total)),
|
|
231
|
-
pc.yellow(formatCurrency(cost)),
|
|
232
|
-
];
|
|
233
|
-
}
|
package/src/tui/App.tsx
DELETED
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
import { createEffect, createSignal, Switch, Match, onCleanup } from "solid-js";
|
|
2
|
-
import { useKeyboard, useTerminalDimensions, useRenderer } from "@opentui/solid";
|
|
3
|
-
import clipboardy from "clipboardy";
|
|
4
|
-
import { Header } from "./components/Header.js";
|
|
5
|
-
import { Footer } from "./components/Footer.js";
|
|
6
|
-
import { ModelView } from "./components/ModelView.js";
|
|
7
|
-
import { DailyView } from "./components/DailyView.js";
|
|
8
|
-
import { StatsView } from "./components/StatsView.js";
|
|
9
|
-
import { OverviewView } from "./components/OverviewView.js";
|
|
10
|
-
import { LoadingSpinner } from "./components/LoadingSpinner.js";
|
|
11
|
-
import { useData, type DateFilters } from "./hooks/useData.js";
|
|
12
|
-
import type { ColorPaletteName } from "./config/themes.js";
|
|
13
|
-
import { DEFAULT_PALETTE, getPaletteNames } from "./config/themes.js";
|
|
14
|
-
import { loadSettings, saveSettings, getCacheTimestamp } from "./config/settings.js";
|
|
15
|
-
import { TABS, ALL_SOURCES, type TUIOptions, type TabType, type SortType, type SourceType } from "./types/index.js";
|
|
16
|
-
|
|
17
|
-
export type AppProps = TUIOptions;
|
|
18
|
-
|
|
19
|
-
const PALETTE_NAMES = getPaletteNames();
|
|
20
|
-
const SOURCE_HOTKEYS: Record<string, SourceType> = {
|
|
21
|
-
"1": "opencode",
|
|
22
|
-
"2": "claude",
|
|
23
|
-
"3": "codex",
|
|
24
|
-
"4": "cursor",
|
|
25
|
-
"5": "gemini",
|
|
26
|
-
"6": "amp",
|
|
27
|
-
"7": "droid",
|
|
28
|
-
"8": "openclaw",
|
|
29
|
-
"9": "pi",
|
|
30
|
-
"0": "kimi",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function cycleTabForward(current: TabType): TabType {
|
|
34
|
-
const idx = TABS.indexOf(current);
|
|
35
|
-
return TABS[(idx + 1) % TABS.length];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function cycleTabBackward(current: TabType): TabType {
|
|
39
|
-
const idx = TABS.indexOf(current);
|
|
40
|
-
return TABS[(idx - 1 + TABS.length) % TABS.length];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function App(props: AppProps) {
|
|
44
|
-
const renderer = useRenderer();
|
|
45
|
-
const terminalDimensions = useTerminalDimensions();
|
|
46
|
-
const columns = () => terminalDimensions().width;
|
|
47
|
-
const rows = () => terminalDimensions().height;
|
|
48
|
-
|
|
49
|
-
const settings = loadSettings();
|
|
50
|
-
const [activeTab, setActiveTab] = createSignal<TabType>(props.initialTab ?? "overview");
|
|
51
|
-
const [enabledSources, setEnabledSources] = createSignal<Set<SourceType>>(
|
|
52
|
-
new Set(props.enabledSources ?? ALL_SOURCES)
|
|
53
|
-
);
|
|
54
|
-
const [sortBy, setSortBy] = createSignal<SortType>(props.sortBy ?? "tokens");
|
|
55
|
-
const [sortDesc, setSortDesc] = createSignal(props.sortDesc ?? true);
|
|
56
|
-
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
57
|
-
const [scrollOffset, setScrollOffset] = createSignal(0);
|
|
58
|
-
const [colorPalette, setColorPalette] = createSignal<ColorPaletteName>(
|
|
59
|
-
props.colorPalette ?? (settings.colorPalette as ColorPaletteName) ?? DEFAULT_PALETTE
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const dateFilters: DateFilters = {
|
|
63
|
-
since: props.since,
|
|
64
|
-
until: props.until,
|
|
65
|
-
year: props.year,
|
|
66
|
-
sinceTs: props.sinceTs,
|
|
67
|
-
untilTs: props.untilTs,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const { data, loading, error, refresh, loadingPhase, isRefreshing } = useData(() => enabledSources(), dateFilters);
|
|
71
|
-
|
|
72
|
-
const cacheTimestamp = () => !isRefreshing() && !loading() ? getCacheTimestamp() : null;
|
|
73
|
-
|
|
74
|
-
const [selectedDate, setSelectedDate] = createSignal<string | null>(null);
|
|
75
|
-
|
|
76
|
-
const [statusMessage, setStatusMessage] = createSignal<string | null>(null);
|
|
77
|
-
let statusTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
78
|
-
const [autoRefreshEnabled, setAutoRefreshEnabled] = createSignal(settings.autoRefreshEnabled ?? false);
|
|
79
|
-
const [autoRefreshMs, setAutoRefreshMs] = createSignal(settings.autoRefreshMs ?? 60000);
|
|
80
|
-
|
|
81
|
-
const showStatus = (msg: string, duration = 2000) => {
|
|
82
|
-
if (statusTimeout) clearTimeout(statusTimeout);
|
|
83
|
-
setStatusMessage(msg);
|
|
84
|
-
statusTimeout = setTimeout(() => setStatusMessage(null), duration);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
onCleanup(() => {
|
|
88
|
-
if (statusTimeout) clearTimeout(statusTimeout);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const MIN_AUTO_REFRESH_MS = 30000;
|
|
92
|
-
const MAX_AUTO_REFRESH_MS = 3600000;
|
|
93
|
-
const AUTO_REFRESH_STEPS_MS = [
|
|
94
|
-
30000,
|
|
95
|
-
60000,
|
|
96
|
-
120000,
|
|
97
|
-
300000,
|
|
98
|
-
600000,
|
|
99
|
-
];
|
|
100
|
-
const AUTO_REFRESH_AFTER_MAX_STEP_MS = 600000;
|
|
101
|
-
|
|
102
|
-
const clampAutoRefresh = (value: number) =>
|
|
103
|
-
Math.min(MAX_AUTO_REFRESH_MS, Math.max(MIN_AUTO_REFRESH_MS, value));
|
|
104
|
-
|
|
105
|
-
const formatIntervalSeconds = (ms: number) => {
|
|
106
|
-
const seconds = Math.round(ms / 1000);
|
|
107
|
-
if (seconds < 60) return `${seconds}s`;
|
|
108
|
-
const minutes = Math.round(seconds / 60);
|
|
109
|
-
return `${minutes}m`;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const getAutoRefreshIntervalStep = (current: number, direction: "up" | "down") => {
|
|
113
|
-
const value = clampAutoRefresh(current);
|
|
114
|
-
const cappedSteps = AUTO_REFRESH_STEPS_MS;
|
|
115
|
-
const maxStep = cappedSteps[cappedSteps.length - 1];
|
|
116
|
-
|
|
117
|
-
if (value > maxStep) {
|
|
118
|
-
const delta = direction === "up" ? AUTO_REFRESH_AFTER_MAX_STEP_MS : -AUTO_REFRESH_AFTER_MAX_STEP_MS;
|
|
119
|
-
return clampAutoRefresh(value + delta);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (value === maxStep && direction === "up") {
|
|
123
|
-
return clampAutoRefresh(value + AUTO_REFRESH_AFTER_MAX_STEP_MS);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (value === maxStep && direction === "down") {
|
|
127
|
-
return cappedSteps[cappedSteps.length - 2];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
let idx = cappedSteps.findIndex((step) => step >= value);
|
|
131
|
-
if (idx === -1) idx = cappedSteps.length - 1;
|
|
132
|
-
|
|
133
|
-
if (direction === "up") {
|
|
134
|
-
const nextIndex = value === cappedSteps[idx] ? idx + 1 : idx;
|
|
135
|
-
return clampAutoRefresh(cappedSteps[Math.min(nextIndex, cappedSteps.length - 1)]);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const prevIndex = value === cappedSteps[idx] ? idx - 1 : idx - 1;
|
|
139
|
-
return clampAutoRefresh(cappedSteps[Math.max(prevIndex, 0)]);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
createEffect(() => {
|
|
143
|
-
if (!autoRefreshEnabled()) return;
|
|
144
|
-
const ms = autoRefreshMs();
|
|
145
|
-
const interval = setInterval(() => {
|
|
146
|
-
if (!loading() && !isRefreshing()) {
|
|
147
|
-
refresh();
|
|
148
|
-
}
|
|
149
|
-
}, ms);
|
|
150
|
-
onCleanup(() => clearInterval(interval));
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const contentHeight = () => Math.max(rows() - 4, 12);
|
|
154
|
-
const overviewChartHeight = () => Math.max(5, Math.floor(contentHeight() * 0.35));
|
|
155
|
-
const overviewListHeight = () => Math.max(4, contentHeight() - overviewChartHeight() - 4);
|
|
156
|
-
const overviewItemsPerPage = () => Math.max(1, Math.floor(overviewListHeight() / 2));
|
|
157
|
-
|
|
158
|
-
const handleSourceToggle = (source: SourceType) => {
|
|
159
|
-
const newSources = new Set(enabledSources());
|
|
160
|
-
if (newSources.has(source)) {
|
|
161
|
-
if (newSources.size > 1) {
|
|
162
|
-
newSources.delete(source);
|
|
163
|
-
}
|
|
164
|
-
} else {
|
|
165
|
-
newSources.add(source);
|
|
166
|
-
}
|
|
167
|
-
setEnabledSources(newSources);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const handlePaletteChange = () => {
|
|
171
|
-
const currentIdx = PALETTE_NAMES.indexOf(colorPalette());
|
|
172
|
-
const nextIdx = (currentIdx + 1) % PALETTE_NAMES.length;
|
|
173
|
-
const newPalette = PALETTE_NAMES[nextIdx];
|
|
174
|
-
saveSettings({ colorPalette: newPalette });
|
|
175
|
-
setColorPalette(newPalette);
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const handleSortChange = (sort: SortType) => {
|
|
179
|
-
if (sortBy() === sort) {
|
|
180
|
-
setSortDesc(!sortDesc());
|
|
181
|
-
} else {
|
|
182
|
-
setSortBy(sort);
|
|
183
|
-
setSortDesc(true);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
useKeyboard((key) => {
|
|
188
|
-
if (key.name === "q") {
|
|
189
|
-
renderer.destroy();
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (key.name === "r" && key.shift) {
|
|
194
|
-
const next = !autoRefreshEnabled();
|
|
195
|
-
setAutoRefreshEnabled(next);
|
|
196
|
-
saveSettings({ autoRefreshEnabled: next });
|
|
197
|
-
showStatus(`Auto update: ${next ? "ON" : "OFF"} (${formatIntervalSeconds(autoRefreshMs())})`);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if ((key.name === "+") || (key.name === "=" && key.shift)) {
|
|
202
|
-
const next = getAutoRefreshIntervalStep(autoRefreshMs(), "up");
|
|
203
|
-
setAutoRefreshMs(next);
|
|
204
|
-
saveSettings({ autoRefreshMs: next });
|
|
205
|
-
showStatus(`Auto update interval: ${formatIntervalSeconds(next)}`);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (key.name === "-" || key.name === "_") {
|
|
210
|
-
const next = getAutoRefreshIntervalStep(autoRefreshMs(), "down");
|
|
211
|
-
setAutoRefreshMs(next);
|
|
212
|
-
saveSettings({ autoRefreshMs: next });
|
|
213
|
-
showStatus(`Auto update interval: ${formatIntervalSeconds(next)}`);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (key.name === "r") {
|
|
218
|
-
refresh();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (key.name === "tab" || key.name === "right") {
|
|
223
|
-
setActiveTab(cycleTabForward(activeTab()));
|
|
224
|
-
setSelectedIndex(0);
|
|
225
|
-
setScrollOffset(0);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (key.name === "left") {
|
|
230
|
-
setActiveTab(cycleTabBackward(activeTab()));
|
|
231
|
-
setSelectedIndex(0);
|
|
232
|
-
setScrollOffset(0);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (key.name === "c" && !key.meta && !key.ctrl) {
|
|
237
|
-
handleSortChange("cost");
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (key.name === "y") {
|
|
242
|
-
const d = data();
|
|
243
|
-
if (!d) return;
|
|
244
|
-
|
|
245
|
-
let textToCopy = "";
|
|
246
|
-
const tab = activeTab();
|
|
247
|
-
|
|
248
|
-
if (tab === "model") {
|
|
249
|
-
const sorted = [...d.modelEntries].sort((a, b) => {
|
|
250
|
-
if (sortBy() === "cost") return sortDesc() ? b.cost - a.cost : a.cost - b.cost;
|
|
251
|
-
if (sortBy() === "tokens") return sortDesc() ? b.total - a.total : a.total - b.total;
|
|
252
|
-
return sortDesc() ? b.model.localeCompare(a.model) : a.model.localeCompare(b.model);
|
|
253
|
-
});
|
|
254
|
-
const entry = sorted[selectedIndex()];
|
|
255
|
-
if (entry) {
|
|
256
|
-
textToCopy = `${entry.source} ${entry.model}: ${entry.total.toLocaleString()} tokens, $${entry.cost.toFixed(2)}`;
|
|
257
|
-
}
|
|
258
|
-
} else if (tab === "daily") {
|
|
259
|
-
const sorted = [...d.dailyEntries].sort((a, b) => {
|
|
260
|
-
if (sortBy() === "cost") return sortDesc() ? b.cost - a.cost : a.cost - b.cost;
|
|
261
|
-
if (sortBy() === "tokens") return sortDesc() ? b.total - a.total : a.total - b.total;
|
|
262
|
-
return sortDesc() ? b.date.localeCompare(a.date) : a.date.localeCompare(b.date);
|
|
263
|
-
});
|
|
264
|
-
const entry = sorted[selectedIndex()];
|
|
265
|
-
if (entry) {
|
|
266
|
-
textToCopy = `${entry.date}: ${entry.total.toLocaleString()} tokens, $${entry.cost.toFixed(2)}`;
|
|
267
|
-
}
|
|
268
|
-
} else if (tab === "overview") {
|
|
269
|
-
const model = d.topModels[scrollOffset() + selectedIndex()];
|
|
270
|
-
if (model) {
|
|
271
|
-
textToCopy = `${model.modelId}: ${model.totalTokens.toLocaleString()} tokens, $${model.cost.toFixed(2)}`;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (textToCopy) {
|
|
276
|
-
clipboardy.write(textToCopy)
|
|
277
|
-
.then(() => showStatus("Copied to clipboard"))
|
|
278
|
-
.catch(() => showStatus("Failed to copy"));
|
|
279
|
-
}
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (key.name === "t") {
|
|
283
|
-
handleSortChange("tokens");
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (key.name === "d") {
|
|
288
|
-
handleSortChange("date");
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (key.name === "p") {
|
|
293
|
-
handlePaletteChange();
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (!key.meta && !key.ctrl) {
|
|
298
|
-
const source = SOURCE_HOTKEYS[key.name];
|
|
299
|
-
if (source) {
|
|
300
|
-
handleSourceToggle(source);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (key.name === "up") {
|
|
306
|
-
if (activeTab() === "overview") {
|
|
307
|
-
if (selectedIndex() > 0) {
|
|
308
|
-
setSelectedIndex(selectedIndex() - 1);
|
|
309
|
-
} else if (scrollOffset() > 0) {
|
|
310
|
-
setScrollOffset(scrollOffset() - 1);
|
|
311
|
-
}
|
|
312
|
-
} else {
|
|
313
|
-
setSelectedIndex(Math.max(0, selectedIndex() - 1));
|
|
314
|
-
}
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (key.name === "down") {
|
|
319
|
-
if (activeTab() === "overview") {
|
|
320
|
-
const maxVisible = Math.min(overviewItemsPerPage(), (data()?.topModels.length ?? 0) - scrollOffset());
|
|
321
|
-
const maxOffset = Math.max(0, (data()?.topModels.length ?? 0) - overviewItemsPerPage());
|
|
322
|
-
if (selectedIndex() < maxVisible - 1) {
|
|
323
|
-
setSelectedIndex(selectedIndex() + 1);
|
|
324
|
-
} else if (scrollOffset() < maxOffset) {
|
|
325
|
-
setScrollOffset(scrollOffset() + 1);
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
const d = data();
|
|
329
|
-
const maxIndex = activeTab() === "model"
|
|
330
|
-
? (d?.modelEntries.length ?? 0)
|
|
331
|
-
: (d?.dailyEntries.length ?? 0);
|
|
332
|
-
if (maxIndex > 0) {
|
|
333
|
-
setSelectedIndex(Math.min(selectedIndex() + 1, maxIndex - 1));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (key.name === "e" && data()) {
|
|
340
|
-
const d = data()!;
|
|
341
|
-
const exportData = {
|
|
342
|
-
exportedAt: new Date().toISOString(),
|
|
343
|
-
totalCost: d.totalCost,
|
|
344
|
-
modelCount: d.modelCount,
|
|
345
|
-
models: d.modelEntries,
|
|
346
|
-
daily: d.dailyEntries,
|
|
347
|
-
stats: d.stats,
|
|
348
|
-
};
|
|
349
|
-
const filename = `tokscale-export-${new Date().toISOString().split("T")[0]}.json`;
|
|
350
|
-
import("node:fs")
|
|
351
|
-
.then((fs) => {
|
|
352
|
-
fs.writeFileSync(filename, JSON.stringify(exportData, null, 2));
|
|
353
|
-
showStatus(`Exported to ${filename}`);
|
|
354
|
-
})
|
|
355
|
-
.catch(() => showStatus("Export failed"));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
const handleTabClick = (tab: TabType) => {
|
|
361
|
-
setActiveTab(tab);
|
|
362
|
-
setSelectedIndex(0);
|
|
363
|
-
setScrollOffset(0);
|
|
364
|
-
setSelectedDate(null);
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
return (
|
|
368
|
-
<box flexDirection="column" width={columns()} height={rows()} backgroundColor="black">
|
|
369
|
-
<Header activeTab={activeTab()} onTabClick={handleTabClick} width={columns()} />
|
|
370
|
-
|
|
371
|
-
<box flexDirection="column" flexGrow={1} paddingX={1}>
|
|
372
|
-
<Switch>
|
|
373
|
-
<Match when={loading()}>
|
|
374
|
-
<LoadingSpinner phase={loadingPhase()} />
|
|
375
|
-
</Match>
|
|
376
|
-
<Match when={error()}>
|
|
377
|
-
<box justifyContent="center" alignItems="center" flexGrow={1}>
|
|
378
|
-
<text fg="red">{`Error: ${error()}`}</text>
|
|
379
|
-
</box>
|
|
380
|
-
</Match>
|
|
381
|
-
<Match when={data()}>
|
|
382
|
-
<Switch>
|
|
383
|
-
<Match when={activeTab() === "overview"}>
|
|
384
|
-
<OverviewView
|
|
385
|
-
data={data()!}
|
|
386
|
-
sortBy={sortBy()}
|
|
387
|
-
sortDesc={sortDesc()}
|
|
388
|
-
selectedIndex={selectedIndex}
|
|
389
|
-
scrollOffset={scrollOffset}
|
|
390
|
-
height={contentHeight()}
|
|
391
|
-
width={columns()}
|
|
392
|
-
/>
|
|
393
|
-
</Match>
|
|
394
|
-
<Match when={activeTab() === "model"}>
|
|
395
|
-
<ModelView
|
|
396
|
-
data={data()!}
|
|
397
|
-
sortBy={sortBy()}
|
|
398
|
-
sortDesc={sortDesc()}
|
|
399
|
-
selectedIndex={selectedIndex}
|
|
400
|
-
height={contentHeight()}
|
|
401
|
-
width={columns()}
|
|
402
|
-
/>
|
|
403
|
-
</Match>
|
|
404
|
-
<Match when={activeTab() === "daily"}>
|
|
405
|
-
<DailyView
|
|
406
|
-
data={data()!}
|
|
407
|
-
sortBy={sortBy()}
|
|
408
|
-
sortDesc={sortDesc()}
|
|
409
|
-
selectedIndex={selectedIndex}
|
|
410
|
-
height={contentHeight()}
|
|
411
|
-
width={columns()}
|
|
412
|
-
/>
|
|
413
|
-
</Match>
|
|
414
|
-
<Match when={activeTab() === "stats"}>
|
|
415
|
-
<StatsView
|
|
416
|
-
data={data()!}
|
|
417
|
-
height={contentHeight()}
|
|
418
|
-
colorPalette={colorPalette()}
|
|
419
|
-
width={columns()}
|
|
420
|
-
selectedDate={selectedDate()}
|
|
421
|
-
sortBy={sortBy()}
|
|
422
|
-
/>
|
|
423
|
-
</Match>
|
|
424
|
-
</Switch>
|
|
425
|
-
</Match>
|
|
426
|
-
</Switch>
|
|
427
|
-
</box>
|
|
428
|
-
|
|
429
|
-
<Footer
|
|
430
|
-
enabledSources={enabledSources()}
|
|
431
|
-
sortBy={sortBy()}
|
|
432
|
-
totals={data()?.totals}
|
|
433
|
-
modelCount={data()?.modelCount ?? 0}
|
|
434
|
-
activeTab={activeTab()}
|
|
435
|
-
scrollStart={scrollOffset()}
|
|
436
|
-
scrollEnd={Math.min(scrollOffset() + overviewItemsPerPage(), data()?.topModels.length ?? 0)}
|
|
437
|
-
totalItems={data()?.topModels.length}
|
|
438
|
-
colorPalette={colorPalette()}
|
|
439
|
-
statusMessage={statusMessage()}
|
|
440
|
-
isRefreshing={isRefreshing()}
|
|
441
|
-
loadingPhase={loadingPhase()}
|
|
442
|
-
cacheTimestamp={cacheTimestamp()}
|
|
443
|
-
autoRefreshEnabled={autoRefreshEnabled()}
|
|
444
|
-
autoRefreshMs={autoRefreshMs()}
|
|
445
|
-
width={columns()}
|
|
446
|
-
onSourceToggle={handleSourceToggle}
|
|
447
|
-
onSortChange={handleSortChange}
|
|
448
|
-
onPaletteChange={handlePaletteChange}
|
|
449
|
-
onRefresh={refresh}
|
|
450
|
-
/>
|
|
451
|
-
</box>
|
|
452
|
-
);
|
|
453
|
-
}
|