letmecode 0.1.24 → 0.1.26

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.
@@ -58,12 +58,12 @@ export function buildHelpText() {
58
58
  " --no-usage Disable anonymous usage reporting",
59
59
  "",
60
60
  "Controls:",
61
- " [ ] / Tab Switch providers",
62
- " Shift+Tab Switch providers backward",
63
- " j / k Switch dashboard sections",
64
- " Up / Down Switch dashboard sections",
65
- " Left / Right Select the previous or next row",
66
- " 1, h / l, Enter Run Copilot setup actions",
61
+ " Up / Down Select Provider, View, or table rows",
62
+ " Left / Right Change the selected Provider or View",
63
+ " Left / Right Do nothing when table rows are selected",
64
+ " Tab / Shift+Tab Switch providers without leaving the table",
65
+ " [ / ] Switch views without leaving the table",
66
+ " 1, Enter Run Copilot setup actions",
67
67
  " q or Esc Quit",
68
68
  "",
69
69
  "Trace logging:",
@@ -4,23 +4,25 @@ import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout, rende
4
4
  import { buildHelpText, buildProviderStatsOptions, parseCliOptions } from "./cli-options.js";
5
5
  import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
6
6
  import { reportAnonymousUsage } from "./reporting.js";
7
- import { estimateLimitFullValue } from "./providers/limits.js";
7
+ import { estimateLimitFullValue, selectLatestActiveLimitWindows } from "./providers/limits.js";
8
8
  const ESC = String.fromCharCode(0x1b);
9
9
  // Normal mouse tracking (button press/release only) + SGR extended coordinates.
10
- // This makes the tabs clickable while leaving Shift+drag native text selection
10
+ // This makes the controls clickable while leaving Shift+drag native text selection
11
11
  // available everywhere else, exactly like Midnight Commander.
12
12
  const ENABLE_MOUSE_TRACKING = `${ESC}[?1000h${ESC}[?1006h`;
13
13
  const DISABLE_MOUSE_TRACKING = `${ESC}[?1006l${ESC}[?1000l`;
14
14
  // SGR mouse report: ESC [ < button ; column ; row, ending in M (press) or m (release).
15
15
  const SGR_MOUSE_SEQUENCE = new RegExp(`${ESC}\\[<(\\d+);(\\d+);(\\d+)([Mm])`, "g");
16
- const DETAIL_TABS = [
16
+ const DETAIL_VIEWS = [
17
17
  { id: "limit-windows", label: "Limits" },
18
18
  { id: "summary", label: "Summary" },
19
19
  { id: "day-to-day-analyses", label: "Daily" },
20
20
  { id: "usage-by-model", label: "Models" }
21
21
  ];
22
+ const CONTROL_SECTIONS = ["provider", "view", "table"];
22
23
  const CODEX_CREDIT_COST_USD = 0.01;
23
- const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq.", "Full"];
24
+ const ACTIVE_LIMIT_WINDOW_COLOR = "#FFA500";
25
+ const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Tokens", "Start", "End", "API eq.", "Full"];
24
26
  const DAILY_TABLE_HEADERS = ["Day", "Ev", "Input", "Output", "C read", "C write", "API eq."];
25
27
  const MODEL_TABLE_HEADERS = ["Model", "Input", "Output", "C read", "C write", "API eq."];
26
28
  const COPILOT_ACTIONS = [
@@ -39,26 +41,29 @@ function App(props) {
39
41
  const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
40
42
  const [selectedProviderId, setSelectedProviderId] = useState(providers[0]?.id ?? "");
41
43
  const [hasUserSelectedProvider, setHasUserSelectedProvider] = useState(false);
42
- const [selectedDetailTabIndex, setSelectedDetailTabIndex] = useState(0);
43
- const [selectedLimitRowIndex, setSelectedLimitRowIndex] = useState(0);
44
- const [selectedDayRowIndex, setSelectedDayRowIndex] = useState(0);
45
- const [selectedModelRowIndex, setSelectedModelRowIndex] = useState(0);
44
+ const [selectedDetailViewIndex, setSelectedDetailViewIndex] = useState(0);
45
+ const [selectedControlSectionIndex, setSelectedControlSectionIndex] = useState(0);
46
+ const [selectedLimitRowIndex, setSelectedLimitRowIndex] = useState();
47
+ const [selectedDayRowIndex, setSelectedDayRowIndex] = useState();
48
+ const [selectedModelRowIndex, setSelectedModelRowIndex] = useState();
46
49
  const [selectedCopilotActionIndex, setSelectedCopilotActionIndex] = useState(0);
47
50
  const [copilotActionMessage, setCopilotActionMessage] = useState();
48
51
  const hasReportedAnonymousUsageRef = useRef(false);
49
52
  const sortedProviderStates = React.useMemo(() => sortProviderStatesByUsage(providerStates), [providerStates]);
50
53
  const selectedProviderIndex = Math.max(0, sortedProviderStates.findIndex((state) => state.provider.id === selectedProviderId));
51
54
  const selectedProvider = sortedProviderStates[selectedProviderIndex];
52
- const selectedDetailTab = DETAIL_TABS[selectedDetailTabIndex];
55
+ const selectedDetailView = DETAIL_VIEWS[selectedDetailViewIndex];
56
+ const selectedControlSection = CONTROL_SECTIONS[selectedControlSectionIndex];
57
+ const isTableControlSelected = selectedControlSection === "table";
53
58
  const limitRows = getLimitRows(selectedProvider);
54
59
  const dayRows = getDayRows(selectedProvider);
55
60
  const modelRows = getModelRows(selectedProvider);
56
- const activeLimitRowIndex = clampSelectionIndex(selectedLimitRowIndex, limitRows.length);
57
- const activeDayRowIndex = clampSelectionIndex(selectedDayRowIndex, dayRows.length);
58
- const activeModelRowIndex = clampSelectionIndex(selectedModelRowIndex, modelRows.length);
59
- const selectedLimitRow = activeLimitRowIndex >= 0 ? limitRows[activeLimitRowIndex] : undefined;
60
- const selectedDayRow = activeDayRowIndex >= 0 ? dayRows[activeDayRowIndex] : undefined;
61
- const selectedModelRow = activeModelRowIndex >= 0 ? modelRows[activeModelRowIndex] : undefined;
61
+ const activeLimitRowIndex = clampOptionalSelectionIndex(selectedLimitRowIndex, limitRows.length);
62
+ const activeDayRowIndex = clampOptionalSelectionIndex(selectedDayRowIndex, dayRows.length);
63
+ const activeModelRowIndex = clampOptionalSelectionIndex(selectedModelRowIndex, modelRows.length);
64
+ const selectedLimitRow = isTableControlSelected && activeLimitRowIndex >= 0 ? limitRows[activeLimitRowIndex] : undefined;
65
+ const selectedDayRow = isTableControlSelected && activeDayRowIndex >= 0 ? dayRows[activeDayRowIndex] : undefined;
66
+ const selectedModelRow = isTableControlSelected && activeModelRowIndex >= 0 ? modelRows[activeModelRowIndex] : undefined;
62
67
  useEffect(() => {
63
68
  let cancelled = false;
64
69
  for (const provider of providers) {
@@ -110,6 +115,11 @@ function App(props) {
110
115
  // Anonymous usage reporting is best-effort and must never disturb the TUI.
111
116
  });
112
117
  }, [props.usageReportingEnabled, providerStates]);
118
+ const clearSelectedTableRows = useCallback(() => {
119
+ setSelectedLimitRowIndex(undefined);
120
+ setSelectedDayRowIndex(undefined);
121
+ setSelectedModelRowIndex(undefined);
122
+ }, []);
113
123
  useMouseClick((click) => {
114
124
  const regionId = resolveClick(click);
115
125
  if (!regionId) {
@@ -118,23 +128,36 @@ function App(props) {
118
128
  if (regionId.startsWith("provider:")) {
119
129
  setSelectedProviderId(regionId.slice("provider:".length));
120
130
  setHasUserSelectedProvider(true);
131
+ setSelectedControlSectionIndex(CONTROL_SECTIONS.indexOf("provider"));
132
+ clearSelectedTableRows();
121
133
  return;
122
134
  }
123
- if (regionId.startsWith("vtab:")) {
124
- setSelectedDetailTabIndex(Number(regionId.slice("vtab:".length)));
135
+ if (regionId.startsWith("view:")) {
136
+ setSelectedDetailViewIndex(Number(regionId.slice("view:".length)));
137
+ setSelectedControlSectionIndex(CONTROL_SECTIONS.indexOf("view"));
138
+ clearSelectedTableRows();
125
139
  }
126
140
  });
127
141
  const moveSelectedTableRow = useCallback((delta) => {
128
- if (selectedDetailTab.id === "limit-windows") {
129
- setSelectedLimitRowIndex(clampSelectionIndex(activeLimitRowIndex + delta, limitRows.length));
142
+ if (selectedDetailView.id === "limit-windows") {
143
+ if (limitRows.length === 0) {
144
+ return;
145
+ }
146
+ setSelectedLimitRowIndex(clampSelectionIndex(activeLimitRowIndex < 0 ? 0 : activeLimitRowIndex + delta, limitRows.length));
130
147
  return;
131
148
  }
132
- if (selectedDetailTab.id === "usage-by-model") {
133
- setSelectedModelRowIndex(clampSelectionIndex(activeModelRowIndex + delta, modelRows.length));
149
+ if (selectedDetailView.id === "usage-by-model") {
150
+ if (modelRows.length === 0) {
151
+ return;
152
+ }
153
+ setSelectedModelRowIndex(clampSelectionIndex(activeModelRowIndex < 0 ? 0 : activeModelRowIndex + delta, modelRows.length));
134
154
  return;
135
155
  }
136
- if (selectedDetailTab.id === "day-to-day-analyses") {
137
- setSelectedDayRowIndex(clampSelectionIndex(activeDayRowIndex + delta, dayRows.length));
156
+ if (selectedDetailView.id === "day-to-day-analyses") {
157
+ if (dayRows.length === 0) {
158
+ return;
159
+ }
160
+ setSelectedDayRowIndex(clampSelectionIndex(activeDayRowIndex < 0 ? 0 : activeDayRowIndex + delta, dayRows.length));
138
161
  }
139
162
  }, [
140
163
  activeDayRowIndex,
@@ -143,8 +166,59 @@ function App(props) {
143
166
  dayRows.length,
144
167
  limitRows.length,
145
168
  modelRows.length,
146
- selectedDetailTab.id
169
+ selectedDetailView.id
147
170
  ]);
171
+ const canMoveTableRowUp = useCallback(() => {
172
+ if (selectedDetailView.id === "limit-windows") {
173
+ return activeLimitRowIndex > 0;
174
+ }
175
+ if (selectedDetailView.id === "usage-by-model") {
176
+ return activeModelRowIndex > 0;
177
+ }
178
+ if (selectedDetailView.id === "day-to-day-analyses") {
179
+ return activeDayRowIndex > 0;
180
+ }
181
+ return false;
182
+ }, [activeDayRowIndex, activeLimitRowIndex, activeModelRowIndex, selectedDetailView.id]);
183
+ const moveSelectionDown = useCallback(() => {
184
+ if (selectedControlSection === "table") {
185
+ moveSelectedTableRow(1);
186
+ return;
187
+ }
188
+ setSelectedControlSectionIndex((current) => {
189
+ const next = Math.min(current + 1, CONTROL_SECTIONS.length - 1);
190
+ if (CONTROL_SECTIONS[next] === "table") {
191
+ moveSelectedTableRow(1);
192
+ }
193
+ return next;
194
+ });
195
+ }, [moveSelectedTableRow, selectedControlSection]);
196
+ const moveSelectionUp = useCallback(() => {
197
+ if (selectedControlSection === "table" && canMoveTableRowUp()) {
198
+ moveSelectedTableRow(-1);
199
+ return;
200
+ }
201
+ if (selectedControlSection === "table") {
202
+ clearSelectedTableRows();
203
+ }
204
+ setSelectedControlSectionIndex((current) => Math.max(current - 1, 0));
205
+ }, [canMoveTableRowUp, clearSelectedTableRows, moveSelectedTableRow, selectedControlSection]);
206
+ const changeSelectedProvider = useCallback((delta) => {
207
+ setSelectedProviderId(sortedProviderStates[(selectedProviderIndex + delta + sortedProviderStates.length) % sortedProviderStates.length].provider.id);
208
+ setHasUserSelectedProvider(true);
209
+ }, [selectedProviderIndex, sortedProviderStates]);
210
+ const changeSelectedDetailView = useCallback((delta) => {
211
+ setSelectedDetailViewIndex((current) => (current + delta + DETAIL_VIEWS.length) % DETAIL_VIEWS.length);
212
+ }, []);
213
+ const changeSelectedSectionValue = useCallback((delta) => {
214
+ if (selectedControlSection === "provider") {
215
+ changeSelectedProvider(delta);
216
+ return;
217
+ }
218
+ if (selectedControlSection === "view") {
219
+ changeSelectedDetailView(delta);
220
+ }
221
+ }, [changeSelectedDetailView, changeSelectedProvider, selectedControlSection]);
148
222
  useInput((input, key) => {
149
223
  // Mouse reports arrive as SGR escape sequences and are handled by useMouseClick.
150
224
  // Ink strips the leading ESC, leaving e.g. "[<0;10;5M" — never treat that as a key.
@@ -160,45 +234,42 @@ function App(props) {
160
234
  return;
161
235
  }
162
236
  if (selectedProvider.provider.id === "copilot" && key.return) {
163
- runCopilotAction(COPILOT_ACTIONS[selectedCopilotActionIndex].id, setCopilotActionMessage);
237
+ runCopilotAction(setCopilotActionMessage);
164
238
  return;
165
239
  }
166
- if (selectedProvider.provider.id === "copilot" && input === "l") {
167
- setSelectedCopilotActionIndex((current) => (current + 1) % COPILOT_ACTIONS.length);
240
+ if (key.tab && !key.shift) {
241
+ changeSelectedProvider(1);
168
242
  return;
169
243
  }
170
- if (selectedProvider.provider.id === "copilot" && input === "h") {
171
- setSelectedCopilotActionIndex((current) => (current - 1 + COPILOT_ACTIONS.length) % COPILOT_ACTIONS.length);
244
+ if (key.tab && key.shift) {
245
+ changeSelectedProvider(-1);
172
246
  return;
173
247
  }
174
- if (key.rightArrow) {
175
- setSelectedDetailTabIndex((current) => (current + 1) % DETAIL_TABS.length);
248
+ if (input === "]") {
249
+ changeSelectedDetailView(1);
176
250
  return;
177
251
  }
178
- if (key.leftArrow) {
179
- setSelectedDetailTabIndex((current) => (current - 1 + DETAIL_TABS.length) % DETAIL_TABS.length);
252
+ if (input === "[") {
253
+ changeSelectedDetailView(-1);
180
254
  return;
181
255
  }
182
- if ((key.tab && !key.shift) || input === "]") {
183
- setSelectedProviderId(sortedProviderStates[(selectedProviderIndex + 1) % sortedProviderStates.length].provider.id);
184
- setHasUserSelectedProvider(true);
256
+ if (key.rightArrow) {
257
+ changeSelectedSectionValue(1);
185
258
  return;
186
259
  }
187
- if ((key.tab && key.shift) || input === "[") {
188
- setSelectedProviderId(sortedProviderStates[(selectedProviderIndex - 1 + sortedProviderStates.length) % sortedProviderStates.length]
189
- .provider.id);
190
- setHasUserSelectedProvider(true);
260
+ if (key.leftArrow) {
261
+ changeSelectedSectionValue(-1);
191
262
  return;
192
263
  }
193
- if (key.downArrow || input === "j") {
194
- moveSelectedTableRow(1);
264
+ if (key.downArrow) {
265
+ moveSelectionDown();
195
266
  return;
196
267
  }
197
- if (key.upArrow || input === "k") {
198
- moveSelectedTableRow(-1);
268
+ if (key.upArrow) {
269
+ moveSelectionUp();
199
270
  }
200
271
  });
201
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "white", backgroundColor: "green", children: " beta " })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "Provider " }), sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status, regionRef: getRegionRef(`provider:${state.provider.id}`) }, state.provider.id)))] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "View " }), DETAIL_TABS.map((tab, index) => (_jsx(DetailTab, { label: tab.label, active: index === selectedDetailTabIndex, regionRef: getRegionRef(`vtab:${index}`) }, tab.id)))] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) }) }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }), _jsx(Text, { color: "gray", children: "Tab provider \u00B7 \u2190/\u2192 view \u00B7 \u2191/\u2193 row \u00B7 q quit" })] }));
272
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "white", backgroundColor: "green", children: " beta " })] }), _jsxs(Box, { children: [_jsx(ControlSectionLabel, { label: "Provider", active: selectedControlSection === "provider" }), sortedProviderStates.map((state) => (_jsx(ProviderOption, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status, regionRef: getRegionRef(`provider:${state.provider.id}`) }, state.provider.id)))] }), _jsxs(Box, { children: [_jsx(ControlSectionLabel, { label: "View", active: selectedControlSection === "view" }), DETAIL_VIEWS.map((view, index) => (_jsx(ViewOption, { label: view.label, active: index === selectedDetailViewIndex, regionRef: getRegionRef(`view:${index}`) }, view.id)))] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, viewId: selectedDetailView.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) }) }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, viewId: selectedDetailView.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }), _jsx(Text, { color: "gray", children: "\u2191/\u2193 move \u00B7 \u2190/\u2192 selected \u00B7 Tab provider \u00B7 [/] view \u00B7 q quit" })] }));
202
273
  }
203
274
  function CopilotActionsPanel(props) {
204
275
  if (props.providerState.provider.id !== "copilot") {
@@ -206,9 +277,9 @@ function CopilotActionsPanel(props) {
206
277
  }
207
278
  const hasNoUsage = props.providerState.status === "ready" && props.providerState.stats.summary.tokenEvents === 0;
208
279
  const accentColor = hasNoUsage ? "red" : "cyan";
209
- return (_jsxs(Box, { borderStyle: "round", borderColor: accentColor, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "Copilot setup" }), _jsx(Box, { children: COPILOT_ACTIONS.map((action, index) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: index === (props.selectedActionIndex ?? 0), bold: hasNoUsage && action.id === "vscode", color: action.enabled ? (hasNoUsage && action.id === "vscode" ? accentColor : undefined) : "gray", children: `${index + 1} ${action.label}` }) }, action.id))) }), _jsx(Text, { color: hasNoUsage ? accentColor : "gray", children: "Press 1 or h/l to select an action, enter to run selected." }), props.actionMessage ? _jsx(Text, { children: props.actionMessage }) : null] }));
280
+ return (_jsxs(Box, { borderStyle: "round", borderColor: accentColor, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "Copilot setup" }), _jsx(Box, { children: COPILOT_ACTIONS.map((action, index) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: index === (props.selectedActionIndex ?? 0), bold: hasNoUsage && action.id === "vscode", color: action.enabled ? (hasNoUsage && action.id === "vscode" ? accentColor : undefined) : "gray", children: `${index + 1} ${action.label}` }) }, action.id))) }), _jsx(Text, { color: hasNoUsage ? accentColor : "gray", children: "Press 1 to select an action, enter to run selected." }), props.actionMessage ? _jsx(Text, { children: props.actionMessage }) : null] }));
210
281
  }
211
- function runCopilotAction(actionId, setCopilotActionMessage) {
282
+ function runCopilotAction(setCopilotActionMessage) {
212
283
  setCopilotActionMessage("Updating VS Code settings...");
213
284
  void configureCopilotVsCodeLogging()
214
285
  .then((result) => {
@@ -232,14 +303,17 @@ function formatCopilotLoggingResult(result) {
232
303
  `"github.copilot.chat.otel.outfile": "${result.outfile}"`
233
304
  ].join("\n");
234
305
  }
235
- function ProviderTab(props) {
306
+ function ProviderOption(props) {
236
307
  const statusColor = props.status === "error" ? "red" : props.status === "loading" ? "yellow" : "green";
237
- const tabLabel = props.active ? `[${props.label}]` : ` ${props.label} `;
238
- return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { color: statusColor, bold: props.active, children: tabLabel }) }));
308
+ const optionLabel = props.active ? `[${props.label}]` : ` ${props.label} `;
309
+ return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { color: statusColor, bold: props.active, children: optionLabel }) }));
310
+ }
311
+ function ControlSectionLabel(props) {
312
+ return (_jsxs(Text, { color: props.active ? "cyan" : "gray", bold: props.active, children: [props.active ? "› " : " ", pad(props.label, 10)] }));
239
313
  }
240
- function DetailTab(props) {
241
- const tabLabel = props.active ? `[${props.label}]` : ` ${props.label} `;
242
- return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { wrap: "truncate-end", bold: props.active, children: tabLabel }) }));
314
+ function ViewOption(props) {
315
+ const optionLabel = props.active ? `[${props.label}]` : ` ${props.label} `;
316
+ return (_jsx(Box, { marginRight: 2, ref: props.regionRef, children: _jsx(Text, { wrap: "truncate-end", bold: props.active, children: optionLabel }) }));
243
317
  }
244
318
  function SummaryPanel(props) {
245
319
  const { summary } = props.stats;
@@ -262,13 +336,13 @@ function ContentPanel(props) {
262
336
  if (props.providerState.status === "error") {
263
337
  return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
264
338
  }
265
- if (props.tabId === "limit-windows") {
339
+ if (props.viewId === "limit-windows") {
266
340
  return (_jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey, availableHeight: props.availableHeight }));
267
341
  }
268
- if (props.tabId === "summary") {
342
+ if (props.viewId === "summary") {
269
343
  return _jsx(SummaryPanel, { stats: props.providerState.stats });
270
344
  }
271
- if (props.tabId === "day-to-day-analyses") {
345
+ if (props.viewId === "day-to-day-analyses") {
272
346
  return (_jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey, availableHeight: props.availableHeight }));
273
347
  }
274
348
  return (_jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId, availableHeight: props.availableHeight }));
@@ -357,7 +431,7 @@ function buildTextTableLines(options) {
357
431
  key: row.key,
358
432
  text: buildTableRow(table, row.cells),
359
433
  inverse: isSelected,
360
- color: isSelected ? "cyan" : undefined
434
+ color: isSelected ? "cyan" : row.color
361
435
  }];
362
436
  if (options.separatorAfterRowKeys?.has(row.key)) {
363
437
  lines.push({
@@ -392,8 +466,10 @@ function buildTextTableLines(options) {
392
466
  };
393
467
  }
394
468
  function buildLimitWindowTableLines(stats, selectedRowKey) {
395
- const primaryRows = stats.primaryLimitWindows.map((window) => buildLimitWindowTableRow(window));
396
- const secondaryRows = stats.secondaryLimitWindows.map((window) => buildLimitWindowTableRow(window));
469
+ const allWindows = [...stats.primaryLimitWindows, ...stats.secondaryLimitWindows];
470
+ const activeWindows = selectLatestActiveLimitWindows(allWindows);
471
+ const primaryRows = stats.primaryLimitWindows.map((window) => buildLimitWindowTableRow(window, activeWindows.has(window)));
472
+ const secondaryRows = stats.secondaryLimitWindows.map((window) => buildLimitWindowTableRow(window, activeWindows.has(window)));
397
473
  const separatorAfterRowKeys = primaryRows.length > 0 && secondaryRows.length > 0
398
474
  ? new Set([primaryRows[primaryRows.length - 1].key])
399
475
  : undefined;
@@ -405,15 +481,17 @@ function buildLimitWindowTableLines(stats, selectedRowKey) {
405
481
  separatorAfterRowKeys
406
482
  });
407
483
  }
408
- function buildLimitWindowTableRow(window) {
484
+ function buildLimitWindowTableRow(window, isActive) {
409
485
  return {
410
486
  key: `limit-row:${getLimitRowKey(window)}`,
487
+ color: isActive ? ACTIVE_LIMIT_WINDOW_COLOR : undefined,
411
488
  cells: [
412
489
  window.scope,
413
490
  window.planType,
414
491
  formatLimitWindowModels(window),
415
492
  formatCompactWindowMinutes(window.windowMinutes),
416
493
  formatUsedPercentRange(window.minUsedPercent, window.maxUsedPercent),
494
+ formatCompactTokenCount(window.totals.totalTokens),
417
495
  formatCompactLocalDateTime(window.startTimeUtcIso),
418
496
  formatCompactLocalDateTime(window.endTimeUtcIso),
419
497
  // Status-aware: shows "-" when the API-equivalent cost is unknown rather
@@ -463,15 +541,15 @@ function SelectionDetailsPanel(props) {
463
541
  if (props.providerState.status !== "ready") {
464
542
  return null;
465
543
  }
466
- if (props.tabId === "limit-windows" && props.selectedLimitRow) {
544
+ if (props.viewId === "limit-windows" && props.selectedLimitRow) {
467
545
  const row = props.selectedLimitRow;
468
546
  return (_jsxs(DetailsPanelFrame, { children: [_jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals), note: "API equivalent cost" }), _jsx(DetailRow, { label: "Full value", value: formatLimitFullValueRange(row.totals, row.maxUsedPercent), note: "API equivalent cost if 100% will be used" })] }));
469
547
  }
470
- if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
548
+ if (props.viewId === "day-to-day-analyses" && props.selectedDayRow) {
471
549
  const row = props.selectedDayRow;
472
550
  return (_jsxs(DetailsPanelFrame, { children: [_jsxs(Text, { children: ["day: ", formatUtcDay(row.dayKey), " events: ", formatInteger(row.totals.eventCount), " models: ", formatInteger(row.distinctModels.length), " plans: ", formatInteger(row.distinctPlanTypes.length)] }), _jsxs(Text, { children: ["range: ", formatEventRange(row.firstEventUtcIso, row.lastEventUtcIso)] }), _jsxs(Text, { children: ["models: ", row.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", row.distinctPlanTypes.join(", ") || "none"] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
473
551
  }
474
- if (props.tabId === "usage-by-model" && props.selectedModelRow) {
552
+ if (props.viewId === "usage-by-model" && props.selectedModelRow) {
475
553
  return (_jsxs(DetailsPanelFrame, { children: [_jsxs(Text, { children: ["model: ", props.selectedModelRow.modelId, " events: ", formatInteger(props.selectedModelRow.totals.eventCount)] }), _jsx(UsageTotalsDetails, { totals: props.selectedModelRow.totals, modelId: props.selectedModelRow.modelId })] }));
476
554
  }
477
555
  return null;
@@ -534,21 +612,6 @@ function formatCompactNumber(value) {
534
612
  minimumFractionDigits: 0
535
613
  });
536
614
  }
537
- function formatCredits(value) {
538
- if (value > 0 && value < 0.01) {
539
- return "<0.01";
540
- }
541
- return value.toLocaleString("en-US", {
542
- minimumFractionDigits: 2,
543
- maximumFractionDigits: 2
544
- });
545
- }
546
- function formatUsageCredits(totals, modelId) {
547
- if (isInternalUsageModel(modelId)) {
548
- return "N/A";
549
- }
550
- return totals.estimatedCreditsStatus === "unavailable" ? "-" : formatCredits(totals.estimatedCredits);
551
- }
552
615
  function formatUsageUsd(totals, modelId) {
553
616
  if (isInternalUsageModel(modelId)) {
554
617
  return "N/A";
@@ -664,13 +727,6 @@ function formatUsedPercentRange(minUsedPercent, maxUsedPercent) {
664
727
  ? fmt(minUsedPercent)
665
728
  : `${fmt(minUsedPercent)}–${fmt(maxUsedPercent)}`;
666
729
  }
667
- function formatWindowMinutes(value) {
668
- const hours = value / 60;
669
- if (hours >= 24) {
670
- return `${(hours / 24).toFixed(2)}d`;
671
- }
672
- return `${hours.toFixed(2)}h`;
673
- }
674
730
  function formatCompactWindowMinutes(value) {
675
731
  const hours = value / 60;
676
732
  if (hours >= 24) {
@@ -843,7 +899,7 @@ function parseMouseClicks(chunk) {
843
899
  }
844
900
  return clicks;
845
901
  }
846
- // Tracks the on-screen rectangle of named clickable regions (the tabs) via Ink
902
+ // Tracks the on-screen rectangle of named clickable regions via Ink
847
903
  // refs and resolves a click coordinate back to a region id.
848
904
  function useClickRegions() {
849
905
  const nodesRef = useRef(new Map());
@@ -966,6 +1022,12 @@ function clampSelectionIndex(value, rowCount) {
966
1022
  }
967
1023
  return Math.max(0, Math.min(value, rowCount - 1));
968
1024
  }
1025
+ function clampOptionalSelectionIndex(value, rowCount) {
1026
+ if (value === undefined) {
1027
+ return -1;
1028
+ }
1029
+ return clampSelectionIndex(value, rowCount);
1030
+ }
969
1031
  function sortProviderStatesByUsage(states) {
970
1032
  return states
971
1033
  .map((state, index) => ({ state, index }))
@@ -13,7 +13,44 @@ import { resolveUsageRate } from "./pricing.js";
13
13
  // provider. These are the real gpt-5.* API prices, not the (4x cheaper) Codex
14
14
  // subscription credit prices.
15
15
  const USD_TO_CREDITS = 100;
16
+ const GPT_5_6_SOL_RATE = {
17
+ input: 5,
18
+ cacheRead: 0.5,
19
+ cacheWrite: 6.25,
20
+ cacheWrite5m: 6.25,
21
+ cacheWrite1h: 6.25,
22
+ output: 30,
23
+ longContext: {
24
+ thresholdTokens: 272000,
25
+ rate: { input: 10, cacheRead: 1, cacheWrite: 12.5, cacheWrite5m: 12.5, cacheWrite1h: 12.5, output: 45 }
26
+ }
27
+ };
16
28
  const RATE_CARD = {
29
+ "gpt-5.6-sol": GPT_5_6_SOL_RATE,
30
+ "gpt-5.6-terra": {
31
+ input: 2.5,
32
+ cacheRead: 0.25,
33
+ cacheWrite: 3.125,
34
+ cacheWrite5m: 3.125,
35
+ cacheWrite1h: 3.125,
36
+ output: 15,
37
+ longContext: {
38
+ thresholdTokens: 272000,
39
+ rate: { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 6.25, output: 22.5 }
40
+ }
41
+ },
42
+ "gpt-5.6-luna": {
43
+ input: 1,
44
+ cacheRead: 0.1,
45
+ cacheWrite: 1.25,
46
+ cacheWrite5m: 1.25,
47
+ cacheWrite1h: 1.25,
48
+ output: 6,
49
+ longContext: {
50
+ thresholdTokens: 272000,
51
+ rate: { input: 2, cacheRead: 0.2, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 9 }
52
+ }
53
+ },
17
54
  "gpt-5.5": { input: 5, cacheRead: 0.5, cacheWrite: 5, cacheWrite5m: 5, cacheWrite1h: 5, output: 30 },
18
55
  "gpt-5.4": { input: 2.5, cacheRead: 0.25, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 15 },
19
56
  "gpt-5.4-mini": { input: 0.75, cacheRead: 0.075, cacheWrite: 0.75, cacheWrite5m: 0.75, cacheWrite1h: 0.75, output: 4.5 }
@@ -53,7 +90,7 @@ export class CodexUsageProvider extends UsageProviderBase {
53
90
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
54
91
  const unknownPricedModels = modelUsage
55
92
  .map((row) => row.modelId)
56
- .filter((modelId) => !RATE_CARD[modelId] && !isAssumedZeroRatedCodexModel(modelId, knownModels));
93
+ .filter((modelId) => !rateForCodexModel(modelId) && !isAssumedZeroRatedCodexModel(modelId, knownModels));
57
94
  if (unknownPricedModels.length > 0) {
58
95
  warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
59
96
  }
@@ -235,7 +272,7 @@ function subtractRawUsage(current, previous) {
235
272
  };
236
273
  }
237
274
  function creditsFor(modelId, usage) {
238
- const rate = resolveUsageRate(RATE_CARD, modelId);
275
+ const rate = rateForCodexModel(modelId, usage.inputTokens);
239
276
  if (!rate) {
240
277
  return 0;
241
278
  }
@@ -246,6 +283,12 @@ function creditsFor(modelId, usage) {
246
283
  (usage.outputTokens / 1000000) * rate.output) *
247
284
  USD_TO_CREDITS);
248
285
  }
286
+ function rateForCodexModel(modelId, inputTokens = 0) {
287
+ // The unsuffixed API alias routes to Sol. Normalize only the exact alias so
288
+ // an unknown future gpt-5.6-* tier is not accidentally charged at Sol rates.
289
+ const pricedModelId = modelId === "gpt-5.6" ? "gpt-5.6-sol" : modelId;
290
+ return resolveUsageRate(RATE_CARD, pricedModelId, inputTokens, { prefixMatch: true });
291
+ }
249
292
  function rawUsageToTotals(usage) {
250
293
  const cacheReadInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
251
294
  const inputTokens = Math.max(0, usage.inputTokens - cacheReadInputTokens);
@@ -267,7 +310,7 @@ function createUsageTotalsForModel(modelId, usage, knownModels) {
267
310
  const deltaTotals = rawUsageToTotals(usage);
268
311
  deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
269
312
  deltaTotals.eventCount = 1;
270
- if (!RATE_CARD[resolvedModelId] && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
313
+ if (!rateForCodexModel(resolvedModelId, usage.inputTokens) && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
271
314
  deltaTotals.estimatedCreditsStatus = "unavailable";
272
315
  }
273
316
  return deltaTotals;
@@ -1,7 +1,32 @@
1
1
  import { addUsageTotals, cloneUsageTotals, createEmptyUsageTotals } from "./contract.js";
2
+ // Recent Codex monthly windows can report a reset timestamp that jitters by a
3
+ // few minutes while the logical quota cycle remains unchanged. Short and weekly
4
+ // windows retain exact reset identity so their established behavior is stable.
5
+ const MONTHLY_WINDOW_MINUTES = 30 * 24 * 60;
6
+ const MONTHLY_RESET_JITTER_SECONDS = 15 * 60;
2
7
  export function createLimitWindowAggregates() {
3
8
  return new Map();
4
9
  }
10
+ export function isLimitWindowActive(window, nowMs = Date.now()) {
11
+ const startMs = Date.parse(window.startTimeUtcIso);
12
+ const endMs = Date.parse(window.endTimeUtcIso);
13
+ return Number.isFinite(startMs) && Number.isFinite(endMs) && startMs <= nowMs && nowMs <= endMs;
14
+ }
15
+ export function selectLatestActiveLimitWindows(windows, nowMs = Date.now()) {
16
+ const latestByPlanAndWindow = new Map();
17
+ for (const window of windows) {
18
+ if (!isLimitWindowActive(window, nowMs)) {
19
+ continue;
20
+ }
21
+ const startMs = Date.parse(window.startTimeUtcIso);
22
+ const groupKey = JSON.stringify([window.planType, window.windowMinutes]);
23
+ const current = latestByPlanAndWindow.get(groupKey);
24
+ if (!current || startMs > current.startMs) {
25
+ latestByPlanAndWindow.set(groupKey, { window, startMs });
26
+ }
27
+ }
28
+ return new Set([...latestByPlanAndWindow.values()].map((entry) => entry.window));
29
+ }
5
30
  export function numberOrZero(value) {
6
31
  if (typeof value === "number") {
7
32
  return Number.isFinite(value) ? value : 0;
@@ -92,6 +117,35 @@ function makeWindowKey(scope, rateLimits, window) {
92
117
  numberOrZero(window.resets_at)
93
118
  ].join("|");
94
119
  }
120
+ function findMatchingWindowKey(windows, scope, rateLimits, windowMinutes, resetsAt) {
121
+ const exactKey = makeWindowKey(scope, rateLimits, { window_minutes: windowMinutes, resets_at: resetsAt });
122
+ if (windows.has(exactKey) || windowMinutes < MONTHLY_WINDOW_MINUTES) {
123
+ return exactKey;
124
+ }
125
+ const limitId = String(rateLimits.limit_id ?? "unknown");
126
+ const planType = String(rateLimits.plan_type ?? "unknown");
127
+ let closestKey = exactKey;
128
+ let closestDistance = Number.POSITIVE_INFINITY;
129
+ for (const [key, candidate] of windows) {
130
+ if (candidate.scope !== scope ||
131
+ candidate.limitId !== limitId ||
132
+ candidate.planType !== planType ||
133
+ candidate.windowMinutes !== windowMinutes) {
134
+ continue;
135
+ }
136
+ const minResetsAt = candidate.minStartsAt + candidate.windowMinutes * 60;
137
+ const distance = resetsAt < minResetsAt
138
+ ? minResetsAt - resetsAt
139
+ : resetsAt > candidate.maxResetsAt
140
+ ? resetsAt - candidate.maxResetsAt
141
+ : 0;
142
+ if (distance <= MONTHLY_RESET_JITTER_SECONDS && distance < closestDistance) {
143
+ closestKey = key;
144
+ closestDistance = distance;
145
+ }
146
+ }
147
+ return closestKey;
148
+ }
95
149
  function collapseNearbyWindows(rows) {
96
150
  const collapsed = new Map();
97
151
  for (const row of rows) {
@@ -163,7 +217,7 @@ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, modelId,
163
217
  }
164
218
  const startsAt = resetsAt - windowMinutes * 60;
165
219
  const usedPercent = numberOrZero(window.used_percent);
166
- const key = makeWindowKey(scope, rateLimits, window);
220
+ const key = findMatchingWindowKey(windows, scope, rateLimits, windowMinutes, resetsAt);
167
221
  const existing = windows.get(key);
168
222
  if (!existing) {
169
223
  windows.set(key, {
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
5
5
  "author": "Devforth (https://devforth.io)",
6
6
  "license": "MIT",
7
+ "packageManager": "pnpm@10.28.2",
7
8
  "type": "commonjs",
8
9
  "main": "./dist/index.js",
9
10
  "bin": {
@@ -29,6 +30,16 @@
29
30
  "publishConfig": {
30
31
  "access": "public"
31
32
  },
33
+ "scripts": {
34
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
35
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
36
+ "prepack": "npm run build",
37
+ "prestart": "npm run build",
38
+ "start": "node ./bin/letmecode.js",
39
+ "pretest": "npm run build",
40
+ "smoke": "node ./bin/letmecode.js",
41
+ "test": "node --test ink-app/test/*.test.mjs"
42
+ },
32
43
  "keywords": [
33
44
  "ai",
34
45
  "agents",
@@ -53,14 +64,5 @@
53
64
  "@types/node": "^24.0.7",
54
65
  "@types/react": "^18.3.24",
55
66
  "typescript": "^5.8.3"
56
- },
57
- "scripts": {
58
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
59
- "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
60
- "prestart": "npm run build",
61
- "start": "node ./bin/letmecode.js",
62
- "pretest": "npm run build",
63
- "smoke": "node ./bin/letmecode.js",
64
- "test": "node --test ink-app/test/*.test.mjs"
65
67
  }
66
- }
68
+ }