@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.
Files changed (188) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +128 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +19 -26
  6. package/dist/auth.d.ts +0 -17
  7. package/dist/auth.d.ts.map +0 -1
  8. package/dist/auth.js +0 -162
  9. package/dist/auth.js.map +0 -1
  10. package/dist/cli.d.ts +0 -9
  11. package/dist/cli.d.ts.map +0 -1
  12. package/dist/cli.js +0 -1550
  13. package/dist/cli.js.map +0 -1
  14. package/dist/credentials.d.ts +0 -50
  15. package/dist/credentials.d.ts.map +0 -1
  16. package/dist/credentials.js +0 -151
  17. package/dist/credentials.js.map +0 -1
  18. package/dist/cursor.d.ts +0 -167
  19. package/dist/cursor.d.ts.map +0 -1
  20. package/dist/cursor.js +0 -906
  21. package/dist/cursor.js.map +0 -1
  22. package/dist/date-utils.d.ts +0 -10
  23. package/dist/date-utils.d.ts.map +0 -1
  24. package/dist/date-utils.js +0 -47
  25. package/dist/date-utils.js.map +0 -1
  26. package/dist/graph-types.d.ts +0 -142
  27. package/dist/graph-types.d.ts.map +0 -1
  28. package/dist/graph-types.js +0 -6
  29. package/dist/graph-types.js.map +0 -1
  30. package/dist/native-runner.d.ts +0 -11
  31. package/dist/native-runner.d.ts.map +0 -1
  32. package/dist/native-runner.js +0 -77
  33. package/dist/native-runner.js.map +0 -1
  34. package/dist/native.d.ts +0 -106
  35. package/dist/native.d.ts.map +0 -1
  36. package/dist/native.js +0 -302
  37. package/dist/native.js.map +0 -1
  38. package/dist/sessions/types.d.ts +0 -28
  39. package/dist/sessions/types.d.ts.map +0 -1
  40. package/dist/sessions/types.js +0 -27
  41. package/dist/sessions/types.js.map +0 -1
  42. package/dist/spinner.d.ts +0 -75
  43. package/dist/spinner.d.ts.map +0 -1
  44. package/dist/spinner.js +0 -203
  45. package/dist/spinner.js.map +0 -1
  46. package/dist/submit.d.ts +0 -23
  47. package/dist/submit.d.ts.map +0 -1
  48. package/dist/submit.js +0 -294
  49. package/dist/submit.js.map +0 -1
  50. package/dist/table.d.ts +0 -42
  51. package/dist/table.d.ts.map +0 -1
  52. package/dist/table.js +0 -181
  53. package/dist/table.js.map +0 -1
  54. package/dist/tui/App.d.ts +0 -4
  55. package/dist/tui/App.d.ts.map +0 -1
  56. package/dist/tui/App.js +0 -333
  57. package/dist/tui/App.js.map +0 -1
  58. package/dist/tui/components/BarChart.d.ts +0 -17
  59. package/dist/tui/components/BarChart.d.ts.map +0 -1
  60. package/dist/tui/components/BarChart.js +0 -146
  61. package/dist/tui/components/BarChart.js.map +0 -1
  62. package/dist/tui/components/DailyView.d.ts +0 -13
  63. package/dist/tui/components/DailyView.d.ts.map +0 -1
  64. package/dist/tui/components/DailyView.js +0 -86
  65. package/dist/tui/components/DailyView.js.map +0 -1
  66. package/dist/tui/components/DateBreakdownPanel.d.ts +0 -7
  67. package/dist/tui/components/DateBreakdownPanel.d.ts.map +0 -1
  68. package/dist/tui/components/DateBreakdownPanel.js +0 -36
  69. package/dist/tui/components/DateBreakdownPanel.js.map +0 -1
  70. package/dist/tui/components/Footer.d.ts +0 -28
  71. package/dist/tui/components/Footer.d.ts.map +0 -1
  72. package/dist/tui/components/Footer.js +0 -130
  73. package/dist/tui/components/Footer.js.map +0 -1
  74. package/dist/tui/components/Header.d.ts +0 -9
  75. package/dist/tui/components/Header.d.ts.map +0 -1
  76. package/dist/tui/components/Header.js +0 -20
  77. package/dist/tui/components/Header.js.map +0 -1
  78. package/dist/tui/components/Legend.d.ts +0 -7
  79. package/dist/tui/components/Legend.d.ts.map +0 -1
  80. package/dist/tui/components/Legend.js +0 -16
  81. package/dist/tui/components/Legend.js.map +0 -1
  82. package/dist/tui/components/LoadingSpinner.d.ts +0 -8
  83. package/dist/tui/components/LoadingSpinner.d.ts.map +0 -1
  84. package/dist/tui/components/LoadingSpinner.js +0 -55
  85. package/dist/tui/components/LoadingSpinner.js.map +0 -1
  86. package/dist/tui/components/ModelRow.d.ts +0 -13
  87. package/dist/tui/components/ModelRow.d.ts.map +0 -1
  88. package/dist/tui/components/ModelRow.js +0 -15
  89. package/dist/tui/components/ModelRow.js.map +0 -1
  90. package/dist/tui/components/ModelView.d.ts +0 -13
  91. package/dist/tui/components/ModelView.d.ts.map +0 -1
  92. package/dist/tui/components/ModelView.js +0 -96
  93. package/dist/tui/components/ModelView.js.map +0 -1
  94. package/dist/tui/components/OverviewView.d.ts +0 -14
  95. package/dist/tui/components/OverviewView.d.ts.map +0 -1
  96. package/dist/tui/components/OverviewView.js +0 -65
  97. package/dist/tui/components/OverviewView.js.map +0 -1
  98. package/dist/tui/components/StatsView.d.ts +0 -14
  99. package/dist/tui/components/StatsView.d.ts.map +0 -1
  100. package/dist/tui/components/StatsView.js +0 -102
  101. package/dist/tui/components/StatsView.js.map +0 -1
  102. package/dist/tui/components/TokenBreakdown.d.ts +0 -14
  103. package/dist/tui/components/TokenBreakdown.d.ts.map +0 -1
  104. package/dist/tui/components/TokenBreakdown.js +0 -10
  105. package/dist/tui/components/TokenBreakdown.js.map +0 -1
  106. package/dist/tui/components/index.d.ts +0 -16
  107. package/dist/tui/components/index.d.ts.map +0 -1
  108. package/dist/tui/components/index.js +0 -13
  109. package/dist/tui/components/index.js.map +0 -1
  110. package/dist/tui/config/settings.d.ts +0 -15
  111. package/dist/tui/config/settings.d.ts.map +0 -1
  112. package/dist/tui/config/settings.js +0 -147
  113. package/dist/tui/config/settings.js.map +0 -1
  114. package/dist/tui/config/themes.d.ts +0 -15
  115. package/dist/tui/config/themes.d.ts.map +0 -1
  116. package/dist/tui/config/themes.js +0 -82
  117. package/dist/tui/config/themes.js.map +0 -1
  118. package/dist/tui/hooks/useData.d.ts +0 -19
  119. package/dist/tui/hooks/useData.d.ts.map +0 -1
  120. package/dist/tui/hooks/useData.js +0 -468
  121. package/dist/tui/hooks/useData.js.map +0 -1
  122. package/dist/tui/index.d.ts +0 -4
  123. package/dist/tui/index.d.ts.map +0 -1
  124. package/dist/tui/index.js +0 -36
  125. package/dist/tui/index.js.map +0 -1
  126. package/dist/tui/types/index.d.ts +0 -137
  127. package/dist/tui/types/index.d.ts.map +0 -1
  128. package/dist/tui/types/index.js +0 -26
  129. package/dist/tui/types/index.js.map +0 -1
  130. package/dist/tui/utils/cleanup.d.ts +0 -22
  131. package/dist/tui/utils/cleanup.d.ts.map +0 -1
  132. package/dist/tui/utils/cleanup.js +0 -59
  133. package/dist/tui/utils/cleanup.js.map +0 -1
  134. package/dist/tui/utils/colors.d.ts +0 -19
  135. package/dist/tui/utils/colors.d.ts.map +0 -1
  136. package/dist/tui/utils/colors.js +0 -71
  137. package/dist/tui/utils/colors.js.map +0 -1
  138. package/dist/tui/utils/format.d.ts +0 -7
  139. package/dist/tui/utils/format.d.ts.map +0 -1
  140. package/dist/tui/utils/format.js +0 -45
  141. package/dist/tui/utils/format.js.map +0 -1
  142. package/dist/tui/utils/responsive.d.ts +0 -5
  143. package/dist/tui/utils/responsive.d.ts.map +0 -1
  144. package/dist/tui/utils/responsive.js +0 -5
  145. package/dist/tui/utils/responsive.js.map +0 -1
  146. package/dist/wrapped.d.ts +0 -43
  147. package/dist/wrapped.d.ts.map +0 -1
  148. package/dist/wrapped.js +0 -719
  149. package/dist/wrapped.js.map +0 -1
  150. package/src/auth.ts +0 -211
  151. package/src/cli.ts +0 -1892
  152. package/src/credentials.ts +0 -176
  153. package/src/cursor.ts +0 -1044
  154. package/src/date-utils.ts +0 -51
  155. package/src/graph-types.ts +0 -175
  156. package/src/native-runner.js +0 -4
  157. package/src/native-runner.ts +0 -91
  158. package/src/native.ts +0 -633
  159. package/src/sessions/types.ts +0 -59
  160. package/src/spinner.ts +0 -283
  161. package/src/submit.ts +0 -360
  162. package/src/table.ts +0 -233
  163. package/src/tui/App.tsx +0 -453
  164. package/src/tui/components/BarChart.tsx +0 -205
  165. package/src/tui/components/DailyView.tsx +0 -132
  166. package/src/tui/components/DateBreakdownPanel.tsx +0 -79
  167. package/src/tui/components/Footer.tsx +0 -380
  168. package/src/tui/components/Header.tsx +0 -68
  169. package/src/tui/components/Legend.tsx +0 -39
  170. package/src/tui/components/LoadingSpinner.tsx +0 -81
  171. package/src/tui/components/ModelRow.tsx +0 -47
  172. package/src/tui/components/ModelView.tsx +0 -147
  173. package/src/tui/components/OverviewView.tsx +0 -121
  174. package/src/tui/components/StatsView.tsx +0 -249
  175. package/src/tui/components/TokenBreakdown.tsx +0 -46
  176. package/src/tui/components/index.ts +0 -15
  177. package/src/tui/config/settings.ts +0 -183
  178. package/src/tui/config/themes.ts +0 -115
  179. package/src/tui/hooks/useData.ts +0 -558
  180. package/src/tui/index.tsx +0 -44
  181. package/src/tui/opentui.d.ts +0 -166
  182. package/src/tui/types/index.ts +0 -173
  183. package/src/tui/utils/cleanup.ts +0 -65
  184. package/src/tui/utils/colors.ts +0 -78
  185. package/src/tui/utils/format.ts +0 -36
  186. package/src/tui/utils/responsive.ts +0 -8
  187. package/src/types.d.ts +0 -28
  188. 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
- }