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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.1.164",
3
+ "version": "1.1.166",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -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
- // +1 for the fixed Language row prepended before settingData entries
880
- const totalRows = 3 + (settingData && typeof settingData === 'object' ? Object.keys(settingData).length : 0);
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 = 14;
139
+ const MODEL_COL = 28;
54
140
  const NUM_COL = 14;
55
- const COST_COL = 10;
56
- const visible = height - 2; // leave room for header row + bottom hint
57
-
58
- // Header
59
- const header = [
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
- <Text bold>{header}</Text>
73
- <Text dimColor>{'─'.repeat(width - 4)}</Text>
74
- <Box flexDirection="row" flexGrow={1} position="relative">
75
- <Box flexDirection="column" flexGrow={1}>
76
- {modelEntries.map(([model, usage]) => {
77
- const row = [
78
- safePadEnd(model.length > MODEL_COL - 3 ? model.slice(0, MODEL_COL - 4) + '…' : model, MODEL_COL),
79
- safePadEnd(formatCount(usage.inputTokens), NUM_COL),
80
- safePadEnd(formatCount(usage.outputTokens), NUM_COL),
81
- safePadEnd(formatCount(usage.cacheReadInputTokens), NUM_COL),
82
- safePadEnd(formatCount(usage.cacheCreationInputTokens), NUM_COL),
83
- ].join('');
84
- return <Text key={model}>{row}</Text>;
85
- })}
86
- </Box>
87
- <ScrollBar total={modelEntries.length} offset={0} height={visible - 2} />
88
- </Box>
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 to go back to settings</Text>
255
+ <Text dimColor>ESC back · 1-4 or ←→ to switch view</Text>
91
256
  </Box>
92
257
  );
93
258
  };