bingocode 1.1.164 → 1.1.166
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/package.json
CHANGED
|
@@ -876,8 +876,8 @@ export const CliMenuManager: React.FC = () => {
|
|
|
876
876
|
// Settings interactions
|
|
877
877
|
if (!showHelp && page === 'settings') {
|
|
878
878
|
if (settingsStage === 'list') {
|
|
879
|
-
//
|
|
880
|
-
const totalRows =
|
|
879
|
+
// Fixed rows: Language, Auto Mode, Bypass, Token Stats = 4 rows
|
|
880
|
+
const totalRows = 4 + (settingData && typeof settingData === 'object' ? Object.keys(settingData).length : 0);
|
|
881
881
|
const visible = Math.max(1, MID_H - 2);
|
|
882
882
|
if (key.downArrow || input === 'j') {
|
|
883
883
|
setSettingsCursor(c => Math.min(totalRows - 1, c + 1));
|
|
@@ -1,9 +1,72 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { StateDisplay, ScrollBar, safePadEnd } from '../manager/CliMenuUi.tsx';
|
|
4
|
-
import { aggregateClaudeCodeStats, type ClaudeCodeStats } from '../utils/stats.ts';
|
|
4
|
+
import { aggregateClaudeCodeStats, type ClaudeCodeStats, type DailyModelTokens } from '../utils/stats.ts';
|
|
5
5
|
import { formatNumber } from '../utils/format.js';
|
|
6
6
|
|
|
7
|
+
// Get ISO week number (Monday-based)
|
|
8
|
+
function getWeekNumber(dateStr: string): string {
|
|
9
|
+
const date = new Date(dateStr);
|
|
10
|
+
date.setHours(0, 0, 0, 0);
|
|
11
|
+
date.setDate(date.getDate() + 4 - (date.getDay() || 7));
|
|
12
|
+
const yearStart = new Date(date.getFullYear(), 0, 1);
|
|
13
|
+
const weekNo = Math.ceil((((date.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
|
14
|
+
return `${date.getFullYear()}-W${String(weekNo).padStart(2, '0')}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Get month key (YYYY-MM)
|
|
18
|
+
function getMonthKey(dateStr: string): string {
|
|
19
|
+
return dateStr.slice(0, 7);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Aggregate daily tokens by week
|
|
23
|
+
function aggregateByWeek(dailyTokens: DailyModelTokens[]): DailyModelTokens[] {
|
|
24
|
+
const weekMap = new Map<string, { [model: string]: number }>();
|
|
25
|
+
|
|
26
|
+
for (const day of dailyTokens) {
|
|
27
|
+
const weekKey = getWeekNumber(day.date);
|
|
28
|
+
const existing = weekMap.get(weekKey) || {};
|
|
29
|
+
|
|
30
|
+
for (const [model, tokens] of Object.entries(day.tokensByModel)) {
|
|
31
|
+
existing[model] = (existing[model] || 0) + tokens;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
weekMap.set(weekKey, existing);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Array.from(weekMap.entries())
|
|
38
|
+
.map(([date, tokensByModel]) => ({ date, tokensByModel }))
|
|
39
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Aggregate daily tokens by month
|
|
43
|
+
function aggregateByMonth(dailyTokens: DailyModelTokens[]): DailyModelTokens[] {
|
|
44
|
+
const monthMap = new Map<string, { [model: string]: number }>();
|
|
45
|
+
|
|
46
|
+
for (const day of dailyTokens) {
|
|
47
|
+
const monthKey = getMonthKey(day.date);
|
|
48
|
+
const existing = monthMap.get(monthKey) || {};
|
|
49
|
+
|
|
50
|
+
for (const [model, tokens] of Object.entries(day.tokensByModel)) {
|
|
51
|
+
existing[model] = (existing[model] || 0) + tokens;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
monthMap.set(monthKey, existing);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Array.from(monthMap.entries())
|
|
58
|
+
.map(([date, tokensByModel]) => ({ date, tokensByModel }))
|
|
59
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Get last N days
|
|
63
|
+
function getLastNDays(dailyTokens: DailyModelTokens[], n: number = 14): DailyModelTokens[] {
|
|
64
|
+
const sorted = [...dailyTokens].sort((a, b) => b.date.localeCompare(a.date));
|
|
65
|
+
return sorted.slice(0, n);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type TimeRange = 'day' | 'week' | 'month' | 'total';
|
|
69
|
+
|
|
7
70
|
type Props = {
|
|
8
71
|
width: number;
|
|
9
72
|
height: number;
|
|
@@ -14,6 +77,8 @@ export const TokenStatsPanel: React.FC<Props> = ({ width, height, onBack }) => {
|
|
|
14
77
|
const [loading, setLoading] = useState(true);
|
|
15
78
|
const [error, setError] = useState<string | null>(null);
|
|
16
79
|
const [stats, setStats] = useState<ClaudeCodeStats | null>(null);
|
|
80
|
+
const [timeRange, setTimeRange] = useState<TimeRange>('total');
|
|
81
|
+
const [activeButton, setActiveButton] = useState(3); // 0=day, 1=week, 2=month, 3=total
|
|
17
82
|
|
|
18
83
|
useEffect(() => {
|
|
19
84
|
let cancelled = false;
|
|
@@ -35,6 +100,31 @@ export const TokenStatsPanel: React.FC<Props> = ({ width, height, onBack }) => {
|
|
|
35
100
|
return () => { cancelled = true; };
|
|
36
101
|
}, []);
|
|
37
102
|
|
|
103
|
+
// Handle keyboard input for time range switching
|
|
104
|
+
useInput((input, key) => {
|
|
105
|
+
if (input === '1') {
|
|
106
|
+
setTimeRange('day');
|
|
107
|
+
setActiveButton(0);
|
|
108
|
+
} else if (input === '2') {
|
|
109
|
+
setTimeRange('week');
|
|
110
|
+
setActiveButton(1);
|
|
111
|
+
} else if (input === '3') {
|
|
112
|
+
setTimeRange('month');
|
|
113
|
+
setActiveButton(2);
|
|
114
|
+
} else if (input === '4') {
|
|
115
|
+
setTimeRange('total');
|
|
116
|
+
setActiveButton(3);
|
|
117
|
+
} else if (key.leftArrow && activeButton > 0) {
|
|
118
|
+
const newButton = activeButton - 1;
|
|
119
|
+
setActiveButton(newButton);
|
|
120
|
+
setTimeRange(['day', 'week', 'month', 'total'][newButton] as TimeRange);
|
|
121
|
+
} else if (key.rightArrow && activeButton < 3) {
|
|
122
|
+
const newButton = activeButton + 1;
|
|
123
|
+
setActiveButton(newButton);
|
|
124
|
+
setTimeRange(['day', 'week', 'month', 'total'][newButton] as TimeRange);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
38
128
|
if (loading) {
|
|
39
129
|
return <StateDisplay type="loading" message="Loading token stats..." />;
|
|
40
130
|
}
|
|
@@ -45,49 +135,124 @@ export const TokenStatsPanel: React.FC<Props> = ({ width, height, onBack }) => {
|
|
|
45
135
|
return <StateDisplay type="empty" message="No token data yet. Run some conversations first!" />;
|
|
46
136
|
}
|
|
47
137
|
|
|
48
|
-
const modelEntries = Object.entries(stats.modelUsage).sort(
|
|
49
|
-
(a, b) => (b[1].inputTokens + b[1].outputTokens) - (a[1].inputTokens + a[1].outputTokens)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
138
|
// Calculate column widths
|
|
53
|
-
const MODEL_COL =
|
|
139
|
+
const MODEL_COL = 28;
|
|
54
140
|
const NUM_COL = 14;
|
|
55
|
-
const
|
|
56
|
-
const visible = height -
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
safePadEnd('Model', MODEL_COL),
|
|
61
|
-
safePadEnd('Input Tokens', NUM_COL),
|
|
62
|
-
safePadEnd('Output Tokens', NUM_COL),
|
|
63
|
-
safePadEnd('Cache Read', NUM_COL),
|
|
64
|
-
safePadEnd('Cache Create', NUM_COL),
|
|
65
|
-
].join('');
|
|
141
|
+
const DATE_COL = 12;
|
|
142
|
+
const visible = height - 4; // leave room for header row + buttons + bottom hint
|
|
143
|
+
|
|
144
|
+
// Prepare data based on time range
|
|
145
|
+
let isDateView = timeRange !== 'total';
|
|
66
146
|
|
|
67
147
|
const formatCount = (n: number) => formatNumber(n);
|
|
68
148
|
|
|
149
|
+
// Time range buttons
|
|
150
|
+
const timeButtons = [
|
|
151
|
+
{ label: 'Day', key: '1' },
|
|
152
|
+
{ label: 'Week', key: '2' },
|
|
153
|
+
{ label: 'Month', key: '3' },
|
|
154
|
+
{ label: 'Total', key: '4' },
|
|
155
|
+
];
|
|
156
|
+
|
|
69
157
|
return (
|
|
70
158
|
<Box width={width} height={height} flexDirection="column">
|
|
159
|
+
{/* Time range buttons */}
|
|
160
|
+
<Box marginBottom={1}>
|
|
161
|
+
{timeButtons.map((btn, idx) => (
|
|
162
|
+
<Text key={btn.key} color={activeButton === idx ? 'cyan' : 'white'}>
|
|
163
|
+
[{btn.label}] {btn.key}
|
|
164
|
+
{idx < timeButtons.length - 1 ? ' ' : ''}
|
|
165
|
+
</Text>
|
|
166
|
+
))}
|
|
167
|
+
</Box>
|
|
168
|
+
|
|
71
169
|
<Box flexDirection="column" flexGrow={1}>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
170
|
+
{isDateView ? (
|
|
171
|
+
<>
|
|
172
|
+
<Text bold>
|
|
173
|
+
{safePadEnd('Date', DATE_COL)}
|
|
174
|
+
{safePadEnd('Model', MODEL_COL)}
|
|
175
|
+
{safePadEnd('Total Tokens', NUM_COL)}
|
|
176
|
+
</Text>
|
|
177
|
+
<Text dimColor>{'─'.repeat(width - 4)}</Text>
|
|
178
|
+
<Box flexDirection="row" flexGrow={1} position="relative">
|
|
179
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
180
|
+
{(() => {
|
|
181
|
+
// Date-based views - aggregate dailyModelTokens
|
|
182
|
+
let aggregated: DailyModelTokens[];
|
|
183
|
+
if (timeRange === 'day') {
|
|
184
|
+
aggregated = getLastNDays(stats.dailyModelTokens, 14);
|
|
185
|
+
} else if (timeRange === 'week') {
|
|
186
|
+
aggregated = aggregateByWeek(stats.dailyModelTokens);
|
|
187
|
+
} else {
|
|
188
|
+
// month
|
|
189
|
+
aggregated = aggregateByMonth(stats.dailyModelTokens);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return aggregated.flatMap(day =>
|
|
193
|
+
Object.entries(day.tokensByModel).map(([model, tokens]) => {
|
|
194
|
+
const row = [
|
|
195
|
+
safePadEnd(day.date, DATE_COL),
|
|
196
|
+
safePadEnd(model.length > MODEL_COL - 3 ? model.slice(0, MODEL_COL - 4) + '…' : model, MODEL_COL),
|
|
197
|
+
safePadEnd(formatCount(tokens), NUM_COL),
|
|
198
|
+
].join('');
|
|
199
|
+
return <Text key={`${day.date}-${model}`}>{row}</Text>;
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
})()}
|
|
203
|
+
</Box>
|
|
204
|
+
<ScrollBar
|
|
205
|
+
total={(() => {
|
|
206
|
+
let aggregated: DailyModelTokens[];
|
|
207
|
+
if (timeRange === 'day') {
|
|
208
|
+
aggregated = getLastNDays(stats.dailyModelTokens, 14);
|
|
209
|
+
} else if (timeRange === 'week') {
|
|
210
|
+
aggregated = aggregateByWeek(stats.dailyModelTokens);
|
|
211
|
+
} else {
|
|
212
|
+
aggregated = aggregateByMonth(stats.dailyModelTokens);
|
|
213
|
+
}
|
|
214
|
+
return aggregated.flatMap(day => Object.keys(day.tokensByModel)).length;
|
|
215
|
+
})()}
|
|
216
|
+
offset={0}
|
|
217
|
+
height={visible - 2}
|
|
218
|
+
/>
|
|
219
|
+
</Box>
|
|
220
|
+
</>
|
|
221
|
+
) : (
|
|
222
|
+
<>
|
|
223
|
+
<Text bold>
|
|
224
|
+
{safePadEnd('Model', MODEL_COL)}
|
|
225
|
+
{safePadEnd('Input Tokens', NUM_COL)}
|
|
226
|
+
{safePadEnd('Output Tokens', NUM_COL)}
|
|
227
|
+
{safePadEnd('Cache Read', NUM_COL)}
|
|
228
|
+
{safePadEnd('Cache Create', NUM_COL)}
|
|
229
|
+
</Text>
|
|
230
|
+
<Text dimColor>{'─'.repeat(width - 4)}</Text>
|
|
231
|
+
<Box flexDirection="row" flexGrow={1} position="relative">
|
|
232
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
233
|
+
{Object.entries(stats.modelUsage).sort(
|
|
234
|
+
(a, b) => (b[1].inputTokens + b[1].outputTokens) - (a[1].inputTokens + a[1].outputTokens)
|
|
235
|
+
).map(([model, usage]) => {
|
|
236
|
+
const row = [
|
|
237
|
+
safePadEnd(model.length > MODEL_COL - 3 ? model.slice(0, MODEL_COL - 4) + '…' : model, MODEL_COL),
|
|
238
|
+
safePadEnd(formatCount(usage.inputTokens), NUM_COL),
|
|
239
|
+
safePadEnd(formatCount(usage.outputTokens), NUM_COL),
|
|
240
|
+
safePadEnd(formatCount(usage.cacheReadInputTokens), NUM_COL),
|
|
241
|
+
safePadEnd(formatCount(usage.cacheCreationInputTokens), NUM_COL),
|
|
242
|
+
].join('');
|
|
243
|
+
return <Text key={model}>{row}</Text>;
|
|
244
|
+
})}
|
|
245
|
+
</Box>
|
|
246
|
+
<ScrollBar
|
|
247
|
+
total={Object.keys(stats.modelUsage).length}
|
|
248
|
+
offset={0}
|
|
249
|
+
height={visible - 2}
|
|
250
|
+
/>
|
|
251
|
+
</Box>
|
|
252
|
+
</>
|
|
253
|
+
)}
|
|
89
254
|
</Box>
|
|
90
|
-
<Text dimColor>ESC
|
|
255
|
+
<Text dimColor>ESC back · 1-4 or ←→ to switch view</Text>
|
|
91
256
|
</Box>
|
|
92
257
|
);
|
|
93
258
|
};
|