letmecode 0.1.3 → 0.1.4

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useEffect, useState } from "react";
3
- import { Box, Text, useApp, useInput, render } from "ink";
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, measureElement, useApp, useInput, useStdout, render } from "ink";
4
4
  import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
5
5
  const VERTICAL_TABS = [
6
6
  { id: "limit-windows", label: "Limits" },
@@ -55,8 +55,14 @@ const ANTHROPIC_DAY_USAGE_COLUMNS = {
55
55
  const COPILOT_ACTIONS = [
56
56
  { id: "vscode", label: "Start logging VS Code", enabled: true }
57
57
  ];
58
+ const ENTER_FULLSCREEN_MODE = "\u001B[?1049h\u001B[2J\u001B[H";
59
+ const EXIT_FULLSCREEN_MODE = "\u001B[?1049l";
60
+ const SCROLLBAR_TRACK_GLYPH = "│";
61
+ const SCROLLBAR_THUMB_GLYPH = "█";
58
62
  function App(props) {
59
63
  const { exit } = useApp();
64
+ const viewportHeight = useViewportHeight();
65
+ const { ref: contentPanelRef, height: contentPanelHeight } = useMeasuredElementSize();
60
66
  const providers = React.useState(() => createProviders())[0];
61
67
  const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
62
68
  const [selectedProviderId, setSelectedProviderId] = useState(providers[0]?.id ?? "");
@@ -185,7 +191,7 @@ function App(props) {
185
191
  setSelectedVerticalTabIndex((current) => (current - 1 + VERTICAL_TABS.length) % VERTICAL_TABS.length);
186
192
  }
187
193
  });
188
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "[/]/tab to switch providers, j/k or up/down for details, left/right to select a row, enter for actions, q to quit" }), _jsx(Box, { marginTop: 1, children: sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status }, state.provider.id))) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: VERTICAL_TAB_WIDTH, marginRight: 2, children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId }) })] }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.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, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }));
194
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "[/]/tab to switch providers, j/k or up/down for details, left/right to select a row, enter for actions, q to quit" }), _jsx(Box, { marginTop: 1, children: sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status }, state.provider.id))) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", width: VERTICAL_TAB_WIDTH, marginRight: 2, overflow: "hidden", children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) })] }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.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, { marginTop: 1, 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] })] }));
189
195
  }
190
196
  function CopilotActionsPanel(props) {
191
197
  if (props.providerState.provider.id !== "copilot") {
@@ -239,29 +245,25 @@ function ContentPanel(props) {
239
245
  return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
240
246
  }
241
247
  if (props.tabId === "limit-windows") {
242
- return _jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey });
248
+ return (_jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey, availableHeight: props.availableHeight }));
243
249
  }
244
250
  if (props.tabId === "summary") {
245
251
  return _jsx(SummaryPanel, { stats: props.providerState.stats });
246
252
  }
247
253
  if (props.tabId === "day-to-day-analyses") {
248
- return _jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey });
254
+ return (_jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey, availableHeight: props.availableHeight }));
249
255
  }
250
- return _jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId });
256
+ return (_jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId, availableHeight: props.availableHeight }));
251
257
  }
252
258
  function LimitWindowsPanel(props) {
253
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Primary Limit Windows" }), _jsx(LimitWindowSection, { windows: props.stats.primaryLimitWindows, selectedRowKey: props.selectedRowKey }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: "Secondary Limit Windows" }), _jsx(LimitWindowSection, { windows: props.stats.secondaryLimitWindows, selectedRowKey: props.selectedRowKey })] }));
254
- }
255
- function LimitWindowSection(props) {
256
- if (props.windows.length === 0) {
257
- return _jsx(Text, { color: "gray", children: "No windows found." });
258
- }
259
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("plan", LIMIT_WINDOW_COLUMNS.plan), " ", pad("window", LIMIT_WINDOW_COLUMNS.window), " ", pad("used", LIMIT_WINDOW_COLUMNS.used), " ", pad("start", LIMIT_WINDOW_COLUMNS.date), " ", pad("end", LIMIT_WINDOW_COLUMNS.date), " value"] }), props.windows.map((window) => {
260
- const windowLabel = formatWindowMinutes(window.windowMinutes);
261
- const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
262
- const isSelected = props.selectedRowKey === getLimitRowKey(window);
263
- return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(window.planType, LIMIT_WINDOW_COLUMNS.plan), " ", pad(windowLabel, LIMIT_WINDOW_COLUMNS.window), " ", pad(usedLabel, LIMIT_WINDOW_COLUMNS.used), " ", pad(formatLocalDateTime(window.startTimeUtcIso), LIMIT_WINDOW_COLUMNS.date), " ", pad(formatLocalDateTime(window.endTimeUtcIso), LIMIT_WINDOW_COLUMNS.date), " ", pad(formatUsd(window.totals.estimatedCredits * CODEX_CREDIT_COST_USD), LIMIT_WINDOW_COLUMNS.value)] }, getLimitRowKey(window)));
264
- })] }));
259
+ const bodyLines = [
260
+ { key: "primary-title", text: "Primary Limit Windows", bold: true },
261
+ ...buildLimitWindowSectionLines("primary", props.stats.primaryLimitWindows, props.selectedRowKey),
262
+ { key: "section-gap", text: "" },
263
+ { key: "secondary-title", text: "Secondary Limit Windows", bold: true },
264
+ ...buildLimitWindowSectionLines("secondary", props.stats.secondaryLimitWindows, props.selectedRowKey)
265
+ ];
266
+ return (_jsx(ScrollableLineViewport, { bodyLines: bodyLines, selectedBodyLineKey: props.selectedRowKey ? `limit-row:${props.selectedRowKey}` : undefined, availableHeight: props.availableHeight }));
265
267
  }
266
268
  function UsageByModelPanel(props) {
267
269
  if (props.stats.modelUsage.length === 0) {
@@ -269,21 +271,57 @@ function UsageByModelPanel(props) {
269
271
  }
270
272
  const totals = props.stats.summary.totals;
271
273
  if (totals.tokenBreakdown.schema === "anthropic") {
272
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("model", ANTHROPIC_MODEL_USAGE_COLUMNS.model), " ", pad("input", ANTHROPIC_MODEL_USAGE_COLUMNS.input), " ", pad("cacheW5m", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m), " ", pad("cacheW1h", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h), " ", pad("cacheRead", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead), " ", pad("output", ANTHROPIC_MODEL_USAGE_COLUMNS.output), " ", pad("credits", ANTHROPIC_MODEL_USAGE_COLUMNS.credits), " value"] }), props.stats.modelUsage.map((row) => {
273
- const isSelected = props.selectedModelId === row.modelId;
274
- if (row.totals.tokenBreakdown.schema !== "anthropic") {
275
- return null;
274
+ return (_jsx(ScrollableLineViewport, { headerLines: [
275
+ {
276
+ key: "anthropic-model-header",
277
+ text: `${pad("model", ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad("input", ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad("cacheW5m", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad("cacheRead", ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad("output", ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad("credits", ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} value`,
278
+ color: "gray"
279
+ }
280
+ ], bodyLines: props.stats.modelUsage.flatMap((row) => {
281
+ if (row.totals.tokenBreakdown.schema !== "anthropic") {
282
+ return [];
283
+ }
284
+ return [
285
+ {
286
+ key: `model-row:${row.modelId}`,
287
+ text: `${pad(row.modelId, ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad(formatInteger(row.totals.tokenBreakdown.inputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(row.totals.outputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(row.totals, row.modelId), ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(row.totals, row.modelId), ANTHROPIC_MODEL_USAGE_COLUMNS.value)}`,
288
+ inverse: props.selectedModelId === row.modelId,
289
+ color: props.selectedModelId === row.modelId ? "cyan" : undefined
276
290
  }
277
- return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(row.modelId, ANTHROPIC_MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(row.totals.tokenBreakdown.inputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.input), " ", pad(formatInteger(row.totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m), " ", pad(formatInteger(row.totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h), " ", pad(formatInteger(row.totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead), " ", pad(formatInteger(row.totals.outputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.output), " ", pad(formatUsageCredits(row.totals), ANTHROPIC_MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(row.totals), ANTHROPIC_MODEL_USAGE_COLUMNS.value)] }, row.modelId));
278
- }), _jsxs(Text, { color: "cyan", children: [pad("TOTAL", ANTHROPIC_MODEL_USAGE_COLUMNS.model), " ", pad(formatInteger(totals.tokenBreakdown.inputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.input), " ", pad(formatInteger(totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m), " ", pad(formatInteger(totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h), " ", pad(formatInteger(totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead), " ", pad(formatInteger(totals.outputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.output), " ", pad(formatUsageCredits(totals), ANTHROPIC_MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(totals), ANTHROPIC_MODEL_USAGE_COLUMNS.value)] })] }));
291
+ ];
292
+ }), footerLines: [
293
+ {
294
+ key: "anthropic-model-total",
295
+ text: `${pad("TOTAL", ANTHROPIC_MODEL_USAGE_COLUMNS.model)} ${pad(formatInteger(totals.tokenBreakdown.inputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.input)} ${pad(formatInteger(totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(totals.outputTokens), ANTHROPIC_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(totals), ANTHROPIC_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(totals), ANTHROPIC_MODEL_USAGE_COLUMNS.value)}`,
296
+ color: "cyan"
297
+ }
298
+ ], selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
279
299
  }
280
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("model", OPENAI_MODEL_USAGE_COLUMNS.model), " ", pad("uncached", OPENAI_MODEL_USAGE_COLUMNS.input), " ", pad("cached", OPENAI_MODEL_USAGE_COLUMNS.cached), " ", pad("output", OPENAI_MODEL_USAGE_COLUMNS.output), " ", pad("credits", OPENAI_MODEL_USAGE_COLUMNS.credits), " value"] }), props.stats.modelUsage.map((row) => {
281
- const isSelected = props.selectedModelId === row.modelId;
282
- if (row.totals.tokenBreakdown.schema !== "openai") {
283
- return null;
300
+ return (_jsx(ScrollableLineViewport, { headerLines: [
301
+ {
302
+ key: "openai-model-header",
303
+ text: `${pad("model", OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad("uncached", OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad("cached", OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad("output", OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad("credits", OPENAI_MODEL_USAGE_COLUMNS.credits)} value`,
304
+ color: "gray"
305
+ }
306
+ ], bodyLines: props.stats.modelUsage.flatMap((row) => {
307
+ if (row.totals.tokenBreakdown.schema !== "openai") {
308
+ return [];
309
+ }
310
+ return [
311
+ {
312
+ key: `model-row:${row.modelId}`,
313
+ text: `${pad(row.modelId, OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad(formatOpenAiTokens(row.totals, "non-cached"), OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad(formatOpenAiTokens(row.totals, "cached"), OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad(formatInteger(row.totals.outputTokens), OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(row.totals, row.modelId), OPENAI_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(row.totals, row.modelId), OPENAI_MODEL_USAGE_COLUMNS.value)}`,
314
+ inverse: props.selectedModelId === row.modelId,
315
+ color: props.selectedModelId === row.modelId ? "cyan" : undefined
284
316
  }
285
- return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(row.modelId, OPENAI_MODEL_USAGE_COLUMNS.model), " ", pad(formatOpenAiTokens(row.totals, "non-cached"), OPENAI_MODEL_USAGE_COLUMNS.input), " ", pad(formatOpenAiTokens(row.totals, "cached"), OPENAI_MODEL_USAGE_COLUMNS.cached), " ", pad(formatInteger(row.totals.outputTokens), OPENAI_MODEL_USAGE_COLUMNS.output), " ", pad(formatUsageCredits(row.totals), OPENAI_MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(row.totals), OPENAI_MODEL_USAGE_COLUMNS.value)] }, row.modelId));
286
- }), _jsxs(Text, { color: "cyan", children: [pad("TOTAL", OPENAI_MODEL_USAGE_COLUMNS.model), " ", pad(formatOpenAiTokens(totals, "non-cached"), OPENAI_MODEL_USAGE_COLUMNS.input), " ", pad(formatOpenAiTokens(totals, "cached"), OPENAI_MODEL_USAGE_COLUMNS.cached), " ", pad(formatInteger(totals.outputTokens), OPENAI_MODEL_USAGE_COLUMNS.output), " ", pad(formatUsageCredits(totals), OPENAI_MODEL_USAGE_COLUMNS.credits), " ", pad(formatUsageUsd(totals), OPENAI_MODEL_USAGE_COLUMNS.value)] })] }));
317
+ ];
318
+ }), footerLines: [
319
+ {
320
+ key: "openai-model-total",
321
+ text: `${pad("TOTAL", OPENAI_MODEL_USAGE_COLUMNS.model)} ${pad(formatOpenAiTokens(totals, "non-cached"), OPENAI_MODEL_USAGE_COLUMNS.input)} ${pad(formatOpenAiTokens(totals, "cached"), OPENAI_MODEL_USAGE_COLUMNS.cached)} ${pad(formatInteger(totals.outputTokens), OPENAI_MODEL_USAGE_COLUMNS.output)} ${pad(formatUsageCredits(totals), OPENAI_MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(totals), OPENAI_MODEL_USAGE_COLUMNS.value)}`,
322
+ color: "cyan"
323
+ }
324
+ ], selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
287
325
  }
288
326
  function DayToDayPanel(props) {
289
327
  if (props.stats.dayUsage.length === 0) {
@@ -291,21 +329,87 @@ function DayToDayPanel(props) {
291
329
  }
292
330
  const totals = props.stats.summary.totals;
293
331
  if (totals.tokenBreakdown.schema === "anthropic") {
294
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("day", ANTHROPIC_DAY_USAGE_COLUMNS.day), " ", pad("events", ANTHROPIC_DAY_USAGE_COLUMNS.events), " ", pad("input", ANTHROPIC_DAY_USAGE_COLUMNS.input), " ", pad("cacheW5m", ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite5m), " ", pad("cacheW1h", ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite1h), " ", pad("cacheRead", ANTHROPIC_DAY_USAGE_COLUMNS.cacheRead), " ", pad("output", ANTHROPIC_DAY_USAGE_COLUMNS.output), " value"] }), props.stats.dayUsage.map((row) => {
295
- const isSelected = props.selectedDayKey === row.dayKey;
296
- if (row.totals.tokenBreakdown.schema !== "anthropic") {
297
- return null;
332
+ return (_jsx(ScrollableLineViewport, { headerLines: [
333
+ {
334
+ key: "anthropic-day-header",
335
+ text: `${pad("day", ANTHROPIC_DAY_USAGE_COLUMNS.day)} ${pad("events", ANTHROPIC_DAY_USAGE_COLUMNS.events)} ${pad("input", ANTHROPIC_DAY_USAGE_COLUMNS.input)} ${pad("cacheW5m", ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite1h)} ${pad("cacheRead", ANTHROPIC_DAY_USAGE_COLUMNS.cacheRead)} ${pad("output", ANTHROPIC_DAY_USAGE_COLUMNS.output)} value`,
336
+ color: "gray"
337
+ }
338
+ ], bodyLines: props.stats.dayUsage.flatMap((row) => {
339
+ if (row.totals.tokenBreakdown.schema !== "anthropic") {
340
+ return [];
341
+ }
342
+ return [
343
+ {
344
+ key: `day-row:${row.dayKey}`,
345
+ text: `${pad(formatUtcDay(row.dayKey), ANTHROPIC_DAY_USAGE_COLUMNS.day)} ${pad(formatInteger(row.totals.eventCount), ANTHROPIC_DAY_USAGE_COLUMNS.events)} ${pad(formatInteger(row.totals.tokenBreakdown.inputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatInteger(row.totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheRead)} ${pad(formatInteger(row.totals.outputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.output)} ${pad(formatUsageUsd(row.totals), ANTHROPIC_DAY_USAGE_COLUMNS.value)}`,
346
+ inverse: props.selectedDayKey === row.dayKey,
347
+ color: props.selectedDayKey === row.dayKey ? "cyan" : undefined
298
348
  }
299
- return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(formatUtcDay(row.dayKey), ANTHROPIC_DAY_USAGE_COLUMNS.day), " ", pad(formatInteger(row.totals.eventCount), ANTHROPIC_DAY_USAGE_COLUMNS.events), " ", pad(formatInteger(row.totals.tokenBreakdown.inputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.input), " ", pad(formatInteger(row.totals.tokenBreakdown.cacheWrite5mInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite5m), " ", pad(formatInteger(row.totals.tokenBreakdown.cacheWrite1hInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheWrite1h), " ", pad(formatInteger(row.totals.tokenBreakdown.cacheReadInputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.cacheRead), " ", pad(formatInteger(row.totals.outputTokens), ANTHROPIC_DAY_USAGE_COLUMNS.output), " ", pad(formatUsageUsd(row.totals), ANTHROPIC_DAY_USAGE_COLUMNS.value)] }, row.dayKey));
300
- })] }));
349
+ ];
350
+ }), selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
301
351
  }
302
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: [pad("day", OPENAI_DAY_USAGE_COLUMNS.day), " ", pad("events", OPENAI_DAY_USAGE_COLUMNS.events), " ", pad("input", OPENAI_DAY_USAGE_COLUMNS.input), " ", pad("output", OPENAI_DAY_USAGE_COLUMNS.output), " value"] }), props.stats.dayUsage.map((row) => {
303
- const isSelected = props.selectedDayKey === row.dayKey;
304
- if (row.totals.tokenBreakdown.schema !== "openai") {
305
- return null;
352
+ return (_jsx(ScrollableLineViewport, { headerLines: [
353
+ {
354
+ key: "openai-day-header",
355
+ text: `${pad("day", OPENAI_DAY_USAGE_COLUMNS.day)} ${pad("events", OPENAI_DAY_USAGE_COLUMNS.events)} ${pad("input", OPENAI_DAY_USAGE_COLUMNS.input)} ${pad("output", OPENAI_DAY_USAGE_COLUMNS.output)} value`,
356
+ color: "gray"
357
+ }
358
+ ], bodyLines: props.stats.dayUsage.flatMap((row) => {
359
+ if (row.totals.tokenBreakdown.schema !== "openai") {
360
+ return [];
361
+ }
362
+ return [
363
+ {
364
+ key: `day-row:${row.dayKey}`,
365
+ text: `${pad(formatUtcDay(row.dayKey), OPENAI_DAY_USAGE_COLUMNS.day)} ${pad(formatInteger(row.totals.eventCount), OPENAI_DAY_USAGE_COLUMNS.events)} ${pad(formatInteger(row.totals.inputTotalTokens), OPENAI_DAY_USAGE_COLUMNS.input)} ${pad(formatInteger(row.totals.outputTokens), OPENAI_DAY_USAGE_COLUMNS.output)} ${pad(formatUsageUsd(row.totals), OPENAI_DAY_USAGE_COLUMNS.value)}`,
366
+ inverse: props.selectedDayKey === row.dayKey,
367
+ color: props.selectedDayKey === row.dayKey ? "cyan" : undefined
306
368
  }
307
- return (_jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : undefined, children: [pad(formatUtcDay(row.dayKey), OPENAI_DAY_USAGE_COLUMNS.day), " ", pad(formatInteger(row.totals.eventCount), OPENAI_DAY_USAGE_COLUMNS.events), " ", pad(formatInteger(row.totals.inputTotalTokens), OPENAI_DAY_USAGE_COLUMNS.input), " ", pad(formatInteger(row.totals.outputTokens), OPENAI_DAY_USAGE_COLUMNS.output), " ", pad(formatUsageUsd(row.totals), OPENAI_DAY_USAGE_COLUMNS.value)] }, row.dayKey));
308
- })] }));
369
+ ];
370
+ }), selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
371
+ }
372
+ function ScrollableLineViewport(props) {
373
+ const headerLines = props.headerLines ?? [];
374
+ const footerLines = props.footerLines ?? [];
375
+ const layout = resolveScrollableViewportLayout(props.availableHeight, headerLines.length, props.bodyLines.length, footerLines.length);
376
+ const selectedBodyLineIndex = props.selectedBodyLineKey
377
+ ? props.bodyLines.findIndex((line) => line.key === props.selectedBodyLineKey)
378
+ : -1;
379
+ const scrollOffset = useAutoScrollOffset(selectedBodyLineIndex, props.bodyLines.length, layout.bodyVisibleCount);
380
+ const visibleHeaderLines = headerLines.slice(0, layout.headerVisibleCount);
381
+ const visibleBodyLines = props.bodyLines.slice(scrollOffset, scrollOffset + layout.bodyVisibleCount);
382
+ const visibleFooterLines = footerLines.slice(Math.max(0, footerLines.length - layout.footerVisibleCount));
383
+ const bodyScrollbarLines = buildScrollbarLines(layout.bodyVisibleCount, props.bodyLines.length, scrollOffset);
384
+ const showScrollbar = bodyScrollbarLines.length > 0;
385
+ return (_jsxs(Box, { flexDirection: "row", overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [visibleHeaderLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key))), visibleBodyLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key))), visibleFooterLines.map((line) => (_jsx(ScrollableViewportLine, { line: line }, line.key)))] }), showScrollbar ? (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [visibleHeaderLines.map((line) => (_jsx(Text, { color: "gray", children: " " }, `${line.key}-scrollbar`))), bodyScrollbarLines.map((line, index) => (_jsx(Text, { color: line === "#" ? "cyan" : "gray", children: line }, `scrollbar-${index}`))), visibleFooterLines.map((line) => (_jsx(Text, { color: "gray", children: " " }, `${line.key}-scrollbar`)))] })) : null] }));
386
+ }
387
+ function ScrollableViewportLine(props) {
388
+ return (_jsx(Text, { bold: props.line.bold, color: props.line.color, inverse: props.line.inverse, wrap: "truncate-end", children: props.line.text }));
389
+ }
390
+ function buildLimitWindowSectionLines(scope, windows, selectedRowKey) {
391
+ if (windows.length === 0) {
392
+ return [{ key: `${scope}-empty`, text: "No windows found.", color: "gray" }];
393
+ }
394
+ return [
395
+ {
396
+ key: `${scope}-header`,
397
+ text: `${pad("plan", LIMIT_WINDOW_COLUMNS.plan)} ${pad("window", LIMIT_WINDOW_COLUMNS.window)} ${pad("used", LIMIT_WINDOW_COLUMNS.used)} ${pad("start", LIMIT_WINDOW_COLUMNS.date)} ${pad("end", LIMIT_WINDOW_COLUMNS.date)} value`,
398
+ color: "gray"
399
+ },
400
+ ...windows.map((window) => {
401
+ const lineKey = getLimitRowKey(window);
402
+ const windowLabel = formatWindowMinutes(window.windowMinutes);
403
+ const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
404
+ const isSelected = selectedRowKey === lineKey;
405
+ return {
406
+ key: `limit-row:${lineKey}`,
407
+ text: `${pad(window.planType, LIMIT_WINDOW_COLUMNS.plan)} ${pad(windowLabel, LIMIT_WINDOW_COLUMNS.window)} ${pad(usedLabel, LIMIT_WINDOW_COLUMNS.used)} ${pad(formatLocalDateTime(window.startTimeUtcIso), LIMIT_WINDOW_COLUMNS.date)} ${pad(formatLocalDateTime(window.endTimeUtcIso), LIMIT_WINDOW_COLUMNS.date)} ${pad(formatUsd(window.totals.estimatedCredits * CODEX_CREDIT_COST_USD), LIMIT_WINDOW_COLUMNS.value)}`,
408
+ inverse: isSelected,
409
+ color: isSelected ? "cyan" : undefined
410
+ };
411
+ })
412
+ ];
309
413
  }
310
414
  function SelectionDetailsPanel(props) {
311
415
  if (props.providerState.status !== "ready") {
@@ -320,13 +424,13 @@ function SelectionDetailsPanel(props) {
320
424
  return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Day details" }), _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 })] }));
321
425
  }
322
426
  if (props.tabId === "usage-by-model" && props.selectedModelRow) {
323
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Model details" }), _jsxs(Text, { children: ["model: ", props.selectedModelRow.modelId, " events: ", formatInteger(props.selectedModelRow.totals.eventCount)] }), _jsx(UsageTotalsDetails, { totals: props.selectedModelRow.totals })] }));
427
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Model details" }), _jsxs(Text, { children: ["model: ", props.selectedModelRow.modelId, " events: ", formatInteger(props.selectedModelRow.totals.eventCount)] }), _jsx(UsageTotalsDetails, { totals: props.selectedModelRow.totals, modelId: props.selectedModelRow.modelId })] }));
324
428
  }
325
429
  return null;
326
430
  }
327
431
  function UsageTotalsDetails(props) {
328
432
  const { totals } = props;
329
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(UsageBreakdownLines, { totals: totals }), _jsxs(Text, { children: ["Total credits burned: ", formatUsageCredits(totals)] }), _jsxs(Text, { children: ["Credits Value (@ $0.01/credit): ", formatUsageUsd(totals)] }), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(totals)] })] }));
433
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(UsageBreakdownLines, { totals: totals }), _jsxs(Text, { children: ["Burned tokens for: ", formatUsageUsd(totals, props.modelId)] }), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(totals)] })] }));
330
434
  }
331
435
  function formatInteger(value) {
332
436
  return Math.round(value).toLocaleString("en-US");
@@ -340,14 +444,23 @@ function formatCredits(value) {
340
444
  maximumFractionDigits: 2
341
445
  });
342
446
  }
343
- function formatUsageCredits(totals) {
447
+ function formatUsageCredits(totals, modelId) {
448
+ if (isInternalUsageModel(modelId)) {
449
+ return "N/A";
450
+ }
344
451
  return totals.estimatedCreditsStatus === "unavailable" ? "unknown" : formatCredits(totals.estimatedCredits);
345
452
  }
346
- function formatUsageUsd(totals) {
453
+ function formatUsageUsd(totals, modelId) {
454
+ if (isInternalUsageModel(modelId)) {
455
+ return "N/A";
456
+ }
347
457
  return totals.estimatedCreditsStatus === "unavailable"
348
458
  ? "unknown"
349
459
  : formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD);
350
460
  }
461
+ function isInternalUsageModel(modelId) {
462
+ return modelId === "codex-auto-review" || modelId === "<synthetic>";
463
+ }
351
464
  function formatUsd(value) {
352
465
  if (value > 0 && value < 0.0001) {
353
466
  return "<$0.0001";
@@ -426,6 +539,82 @@ function formatInputPerOutput(totals) {
426
539
  }
427
540
  return `uncached:cached:output = ${formatInteger(Math.round(totals.tokenBreakdown.nonCachedInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cachedInputTokens / totals.outputTokens))}:1`;
428
541
  }
542
+ function useMeasuredElementSize() {
543
+ const ref = useRef(null);
544
+ const [size, setSize] = useState({ width: 0, height: 0 });
545
+ useEffect(() => {
546
+ if (!ref.current) {
547
+ return;
548
+ }
549
+ const nextSize = measureElement(ref.current);
550
+ setSize((current) => current.width === nextSize.width && current.height === nextSize.height ? current : nextSize);
551
+ });
552
+ return {
553
+ ref,
554
+ width: size.width,
555
+ height: size.height
556
+ };
557
+ }
558
+ function useAutoScrollOffset(selectedIndex, rowCount, viewportSize) {
559
+ const [scrollOffset, setScrollOffset] = useState(0);
560
+ useEffect(() => {
561
+ const maxOffset = Math.max(0, rowCount - Math.max(0, viewportSize));
562
+ setScrollOffset((current) => {
563
+ let next = Math.max(0, Math.min(current, maxOffset));
564
+ if (selectedIndex < 0 || viewportSize <= 0) {
565
+ return next;
566
+ }
567
+ if (selectedIndex < next) {
568
+ next = selectedIndex;
569
+ }
570
+ else if (selectedIndex >= next + viewportSize) {
571
+ next = selectedIndex - viewportSize + 1;
572
+ }
573
+ return Math.max(0, Math.min(next, maxOffset));
574
+ });
575
+ }, [rowCount, selectedIndex, viewportSize]);
576
+ return scrollOffset;
577
+ }
578
+ function resolveScrollableViewportLayout(availableHeight, headerCount, bodyCount, footerCount) {
579
+ const totalHeight = Math.max(1, availableHeight || 1);
580
+ if (bodyCount === 0) {
581
+ const headerVisibleCount = Math.min(headerCount, totalHeight);
582
+ return {
583
+ headerVisibleCount,
584
+ bodyVisibleCount: 0,
585
+ footerVisibleCount: Math.min(footerCount, Math.max(0, totalHeight - headerVisibleCount))
586
+ };
587
+ }
588
+ let headerVisibleCount = Math.min(headerCount, totalHeight);
589
+ let footerVisibleCount = Math.min(footerCount, Math.max(0, totalHeight - headerVisibleCount - 1));
590
+ let bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
591
+ if (bodyVisibleCount === 0 && footerVisibleCount > 0) {
592
+ footerVisibleCount -= 1;
593
+ bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
594
+ }
595
+ if (bodyVisibleCount === 0 && headerVisibleCount > 0) {
596
+ headerVisibleCount -= 1;
597
+ bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
598
+ }
599
+ if (bodyVisibleCount === 0) {
600
+ return {
601
+ headerVisibleCount: 0,
602
+ bodyVisibleCount: Math.min(bodyCount, totalHeight),
603
+ footerVisibleCount: 0
604
+ };
605
+ }
606
+ return { headerVisibleCount, bodyVisibleCount, footerVisibleCount };
607
+ }
608
+ function buildScrollbarLines(viewportSize, rowCount, scrollOffset) {
609
+ if (viewportSize <= 0 || rowCount <= viewportSize) {
610
+ return [];
611
+ }
612
+ const thumbSize = Math.max(1, Math.round((viewportSize * viewportSize) / rowCount));
613
+ const maxThumbOffset = Math.max(0, viewportSize - thumbSize);
614
+ const maxScrollOffset = Math.max(1, rowCount - viewportSize);
615
+ const thumbOffset = Math.round((Math.max(0, Math.min(scrollOffset, maxScrollOffset)) / maxScrollOffset) * maxThumbOffset);
616
+ return Array.from({ length: viewportSize }, (_, index) => index >= thumbOffset && index < thumbOffset + thumbSize ? SCROLLBAR_THUMB_GLYPH : SCROLLBAR_TRACK_GLYPH);
617
+ }
429
618
  function clampSelectionIndex(value, rowCount) {
430
619
  if (rowCount === 0) {
431
620
  return -1;
@@ -472,7 +661,59 @@ function parseStatsOptions(argv) {
472
661
  verbose: argv.includes("-v") || argv.includes("--verbose")
473
662
  };
474
663
  }
664
+ function useViewportHeight() {
665
+ const { stdout } = useStdout();
666
+ const [viewportHeight, setViewportHeight] = useState(() => resolveViewportHeight(stdout.rows));
667
+ useEffect(() => {
668
+ const updateViewportHeight = () => {
669
+ setViewportHeight(resolveViewportHeight(stdout.rows));
670
+ };
671
+ updateViewportHeight();
672
+ if (!stdout.isTTY) {
673
+ return;
674
+ }
675
+ stdout.on("resize", updateViewportHeight);
676
+ return () => {
677
+ stdout.off("resize", updateViewportHeight);
678
+ };
679
+ }, [stdout]);
680
+ return viewportHeight;
681
+ }
682
+ function resolveViewportHeight(rows) {
683
+ const terminalRows = typeof rows === "number" && rows > 0 ? rows : 24;
684
+ // Keep Ink below the terminal height so it stays on its incremental redraw path
685
+ // instead of the full-screen print path that can leave the viewport scrolled.
686
+ return Math.max(1, terminalRows - 1);
687
+ }
475
688
  export function main(argv = process.argv.slice(2)) {
476
- render(_jsx(App, { statsOptions: parseStatsOptions(argv) }));
689
+ const restoreFullscreen = enterFullscreenMode(process.stdout);
690
+ const exitHandler = () => {
691
+ restoreFullscreen();
692
+ };
693
+ process.once("exit", exitHandler);
694
+ const instance = render(_jsx(App, { statsOptions: parseStatsOptions(argv) }), {
695
+ stdout: process.stdout,
696
+ stdin: process.stdin,
697
+ stderr: process.stderr
698
+ });
699
+ void instance.waitUntilExit().finally(() => {
700
+ process.off("exit", exitHandler);
701
+ instance.cleanup();
702
+ restoreFullscreen();
703
+ });
704
+ }
705
+ function enterFullscreenMode(stdout) {
706
+ if (!stdout.isTTY) {
707
+ return () => { };
708
+ }
709
+ let restored = false;
710
+ stdout.write(ENTER_FULLSCREEN_MODE);
711
+ return () => {
712
+ if (restored) {
713
+ return;
714
+ }
715
+ restored = true;
716
+ stdout.write(EXIT_FULLSCREEN_MODE);
717
+ };
477
718
  }
478
719
  main();
@@ -73,7 +73,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
73
73
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
74
74
  const unknownPricedModels = modelUsage
75
75
  .map((row) => row.modelId)
76
- .filter((modelId) => !resolveRate(modelId));
76
+ .filter((modelId) => !resolveRate(modelId) && !isInternalClaudeModel(modelId));
77
77
  if (unknownPricedModels.length > 0) {
78
78
  warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
79
79
  }
@@ -135,6 +135,9 @@ function resolveRate(modelId) {
135
135
  }
136
136
  return undefined;
137
137
  }
138
+ function isInternalClaudeModel(modelId) {
139
+ return modelId === "<synthetic>";
140
+ }
138
141
  function creditsFor(modelId, usage) {
139
142
  const rate = resolveRate(modelId);
140
143
  if (!rate) {
@@ -17,6 +17,7 @@ export class CodexUsageProvider extends UsageProviderBase {
17
17
  }
18
18
  async getStats(_options = {}) {
19
19
  const sessionsRoot = path.join(this.root, ".codex", "sessions");
20
+ const knownModels = await readCodexModelMetadata(this.root);
20
21
  const byModel = new Map();
21
22
  const byDay = createDailyUsageAggregates();
22
23
  const windows = createLimitWindowAggregates();
@@ -30,7 +31,7 @@ export class CodexUsageProvider extends UsageProviderBase {
30
31
  };
31
32
  for await (const file of walkSessionFiles(sessionsRoot)) {
32
33
  parseTotals.filesScanned += 1;
33
- const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes);
34
+ const fileStats = await parseSessionFile(file, byModel, byDay, windows, planTypes, knownModels);
34
35
  parseTotals.linesRead += fileStats.linesRead;
35
36
  parseTotals.tokenEvents += fileStats.tokenEvents;
36
37
  parseTotals.malformedLines += fileStats.malformedLines;
@@ -43,7 +44,7 @@ export class CodexUsageProvider extends UsageProviderBase {
43
44
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
44
45
  const unknownPricedModels = modelUsage
45
46
  .map((row) => row.modelId)
46
- .filter((modelId) => !RATE_CARD[modelId]);
47
+ .filter((modelId) => !RATE_CARD[modelId] && !isAssumedZeroRatedCodexModel(modelId, knownModels));
47
48
  if (unknownPricedModels.length > 0) {
48
49
  warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
49
50
  }
@@ -74,6 +75,39 @@ export class CodexUsageProvider extends UsageProviderBase {
74
75
  };
75
76
  }
76
77
  }
78
+ async function readCodexModelMetadata(root) {
79
+ const modelsCachePath = path.join(root, ".codex", "models_cache.json");
80
+ let fileText;
81
+ try {
82
+ fileText = await fs.promises.readFile(modelsCachePath, "utf8");
83
+ }
84
+ catch {
85
+ return new Map();
86
+ }
87
+ let payload;
88
+ try {
89
+ payload = JSON.parse(fileText);
90
+ }
91
+ catch {
92
+ return new Map();
93
+ }
94
+ const models = asRecord(payload)?.models;
95
+ if (!Array.isArray(models)) {
96
+ return new Map();
97
+ }
98
+ const metadata = new Map();
99
+ for (const model of models) {
100
+ const record = asRecord(model);
101
+ const slug = typeof record?.slug === "string" ? record.slug : "";
102
+ if (!slug) {
103
+ continue;
104
+ }
105
+ metadata.set(slug, {
106
+ visibility: typeof record?.visibility === "string" ? record.visibility : ""
107
+ });
108
+ }
109
+ return metadata;
110
+ }
77
111
  function createEmptyRawUsage() {
78
112
  return {
79
113
  inputTokens: 0,
@@ -128,11 +162,14 @@ function rawUsageToTotals(usage) {
128
162
  }
129
163
  };
130
164
  }
131
- function createUsageTotalsForModel(modelId, usage) {
165
+ function createUsageTotalsForModel(modelId, usage, knownModels) {
132
166
  const resolvedModelId = modelId || "unknown";
133
167
  const deltaTotals = rawUsageToTotals(usage);
134
168
  deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
135
169
  deltaTotals.eventCount = 1;
170
+ if (!RATE_CARD[resolvedModelId] && !isAssumedZeroRatedCodexModel(resolvedModelId, knownModels)) {
171
+ deltaTotals.estimatedCreditsStatus = "unavailable";
172
+ }
136
173
  return deltaTotals;
137
174
  }
138
175
  function addModelUsage(byModel, modelId, deltaTotals) {
@@ -141,6 +178,14 @@ function addModelUsage(byModel, modelId, deltaTotals) {
141
178
  addUsageTotals(totals, deltaTotals);
142
179
  byModel.set(resolvedModelId, totals);
143
180
  }
181
+ function isHiddenCodexModel(modelId, knownModels) {
182
+ return knownModels.get(modelId)?.visibility === "hide";
183
+ }
184
+ function isAssumedZeroRatedCodexModel(modelId, knownModels) {
185
+ // Hidden internal Codex models do not have a public rate card entry. For dashboard
186
+ // rollups we treat them as zero-rated so they do not turn aggregate totals unknown.
187
+ return isHiddenCodexModel(modelId, knownModels);
188
+ }
144
189
  function isSessionFile(filePath) {
145
190
  return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
146
191
  }
@@ -162,7 +207,7 @@ async function* walkSessionFiles(directory) {
162
207
  }
163
208
  }
164
209
  }
165
- async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
210
+ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes, knownModels) {
166
211
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
167
212
  const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
168
213
  let currentModel = "unknown";
@@ -203,7 +248,7 @@ async function parseSessionFile(filePath, byModel, byDay, windows, planTypes) {
203
248
  const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
204
249
  previousTotal = totalUsage;
205
250
  const resolvedModelId = currentModel || "unknown";
206
- const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage);
251
+ const deltaTotals = createUsageTotalsForModel(resolvedModelId, usage, knownModels);
207
252
  tokenEvents += 1;
208
253
  addModelUsage(byModel, resolvedModelId, deltaTotals);
209
254
  const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",