letmecode 0.1.3 → 0.1.5

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,7 +1,16 @@
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, { useCallback, useEffect, useRef, useState } from "react";
3
+ import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout, render } from "ink";
4
4
  import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
5
+ import { reportAnonymousUsage } from "./reporting.js";
6
+ const ESC = String.fromCharCode(0x1b);
7
+ // Normal mouse tracking (button press/release only) + SGR extended coordinates.
8
+ // This makes the tabs clickable while leaving Shift+drag native text selection
9
+ // available everywhere else, exactly like Midnight Commander.
10
+ const ENABLE_MOUSE_TRACKING = `${ESC}[?1000h${ESC}[?1006h`;
11
+ const DISABLE_MOUSE_TRACKING = `${ESC}[?1006l${ESC}[?1000l`;
12
+ // SGR mouse report: ESC [ < button ; column ; row, ending in M (press) or m (release).
13
+ const SGR_MOUSE_SEQUENCE = new RegExp(`${ESC}\\[<(\\d+);(\\d+);(\\d+)([Mm])`, "g");
5
14
  const VERTICAL_TABS = [
6
15
  { id: "limit-windows", label: "Limits" },
7
16
  { id: "summary", label: "Summary" },
@@ -17,46 +26,40 @@ const LIMIT_WINDOW_COLUMNS = {
17
26
  date: 17,
18
27
  value: 10
19
28
  };
20
- const OPENAI_MODEL_USAGE_COLUMNS = {
29
+ const MODEL_USAGE_COLUMNS = {
21
30
  model: 17,
22
- input: 12,
23
- cached: 12,
24
- output: 11,
25
- credits: 12,
26
- value: 12
27
- };
28
- const ANTHROPIC_MODEL_USAGE_COLUMNS = {
29
- model: 17,
30
- input: 10,
31
+ input: 11,
32
+ output: 10,
33
+ cacheRead: 11,
34
+ cacheWrite: 11,
31
35
  cacheWrite5m: 10,
32
36
  cacheWrite1h: 10,
33
- cacheRead: 10,
34
- output: 10,
35
37
  credits: 12,
36
38
  value: 12
37
39
  };
38
- const OPENAI_DAY_USAGE_COLUMNS = {
40
+ const DAY_USAGE_COLUMNS = {
39
41
  day: 11,
40
42
  events: 6,
41
43
  input: 11,
42
44
  output: 10,
43
- value: 10
44
- };
45
- const ANTHROPIC_DAY_USAGE_COLUMNS = {
46
- day: 11,
47
- events: 6,
48
- input: 10,
45
+ cacheRead: 11,
46
+ cacheWrite: 11,
49
47
  cacheWrite5m: 10,
50
48
  cacheWrite1h: 10,
51
- cacheRead: 10,
52
- output: 10,
53
49
  value: 10
54
50
  };
55
51
  const COPILOT_ACTIONS = [
56
52
  { id: "vscode", label: "Start logging VS Code", enabled: true }
57
53
  ];
54
+ const ENTER_FULLSCREEN_MODE = "\u001B[?1049h\u001B[2J\u001B[H";
55
+ const EXIT_FULLSCREEN_MODE = "\u001B[?1049l";
56
+ const SCROLLBAR_TRACK_GLYPH = "│";
57
+ const SCROLLBAR_THUMB_GLYPH = "█";
58
58
  function App(props) {
59
59
  const { exit } = useApp();
60
+ const viewportHeight = useViewportHeight();
61
+ const { ref: contentPanelRef, height: contentPanelHeight } = useMeasuredElementSize();
62
+ const { getRegionRef, resolveClick } = useClickRegions();
60
63
  const providers = React.useState(() => createProviders())[0];
61
64
  const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
62
65
  const [selectedProviderId, setSelectedProviderId] = useState(providers[0]?.id ?? "");
@@ -67,6 +70,7 @@ function App(props) {
67
70
  const [selectedModelRowIndex, setSelectedModelRowIndex] = useState(0);
68
71
  const [selectedCopilotActionIndex, setSelectedCopilotActionIndex] = useState(0);
69
72
  const [copilotActionMessage, setCopilotActionMessage] = useState();
73
+ const hasReportedAnonymousUsageRef = useRef(false);
70
74
  const sortedProviderStates = React.useMemo(() => sortProviderStatesByUsage(providerStates), [providerStates]);
71
75
  const selectedProviderIndex = Math.max(0, sortedProviderStates.findIndex((state) => state.provider.id === selectedProviderId));
72
76
  const selectedProvider = sortedProviderStates[selectedProviderIndex];
@@ -117,7 +121,38 @@ function App(props) {
117
121
  }
118
122
  setSelectedProviderId(topProvider.provider.id);
119
123
  }, [hasUserSelectedProvider, providerStates, sortedProviderStates]);
124
+ useEffect(() => {
125
+ if (hasReportedAnonymousUsageRef.current || providerStates.some((state) => state.status === "loading")) {
126
+ return;
127
+ }
128
+ hasReportedAnonymousUsageRef.current = true;
129
+ const readyStats = providerStates
130
+ .filter((state) => state.status === "ready")
131
+ .map((state) => state.stats);
132
+ void reportAnonymousUsage(readyStats).catch(() => {
133
+ // Anonymous usage reporting is best-effort and must never disturb the TUI.
134
+ });
135
+ }, [providerStates]);
136
+ useMouseClick((click) => {
137
+ const regionId = resolveClick(click);
138
+ if (!regionId) {
139
+ return;
140
+ }
141
+ if (regionId.startsWith("provider:")) {
142
+ setSelectedProviderId(regionId.slice("provider:".length));
143
+ setHasUserSelectedProvider(true);
144
+ return;
145
+ }
146
+ if (regionId.startsWith("vtab:")) {
147
+ setSelectedVerticalTabIndex(Number(regionId.slice("vtab:".length)));
148
+ }
149
+ });
120
150
  useInput((input, key) => {
151
+ // Mouse reports arrive as SGR escape sequences and are handled by useMouseClick.
152
+ // Ink strips the leading ESC, leaving e.g. "[<0;10;5M" — never treat that as a key.
153
+ if (input.startsWith("[<")) {
154
+ return;
155
+ }
121
156
  if (input === "q" || key.escape) {
122
157
  exit();
123
158
  return;
@@ -185,7 +220,7 @@ function App(props) {
185
220
  setSelectedVerticalTabIndex((current) => (current - 1 + VERTICAL_TABS.length) % VERTICAL_TABS.length);
186
221
  }
187
222
  });
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] }));
223
+ 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, click tabs to switch, 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, regionRef: getRegionRef(`provider:${state.provider.id}`) }, 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, regionRef: getRegionRef(`vtab:${index}`) }, 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
224
  }
190
225
  function CopilotActionsPanel(props) {
191
226
  if (props.providerState.provider.id !== "copilot") {
@@ -222,14 +257,15 @@ function formatCopilotLoggingResult(result) {
222
257
  function ProviderTab(props) {
223
258
  const statusColor = props.status === "error" ? "red" : props.status === "loading" ? "yellow" : "green";
224
259
  const tabLabel = props.active ? ` ${props.label} ` : `[${props.label}]`;
225
- return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: props.active, color: statusColor, children: tabLabel }) }));
260
+ return (_jsx(Box, { marginRight: 1, ref: props.regionRef, children: _jsx(Text, { inverse: props.active, color: statusColor, children: tabLabel }) }));
226
261
  }
227
262
  function VerticalTab(props) {
228
- return (_jsx(Box, { width: VERTICAL_TAB_WIDTH, children: _jsx(Text, { wrap: "truncate-end", inverse: props.active, children: props.active ? ` ${props.label} ` : ` ${props.label}` }) }));
263
+ return (_jsx(Box, { width: VERTICAL_TAB_WIDTH, ref: props.regionRef, children: _jsx(Text, { wrap: "truncate-end", inverse: props.active, children: props.active ? ` ${props.label} ` : ` ${props.label}` }) }));
229
264
  }
230
265
  function SummaryPanel(props) {
231
266
  const { summary } = props.stats;
232
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: props.stats.providerLabel }), _jsxs(Text, { children: ["root: ", summary.rootLabel, " (", summary.rootPath, ")"] }), _jsxs(Text, { children: ["files: ", formatInteger(summary.filesScanned), " lines: ", formatInteger(summary.linesRead), " token events: ", formatInteger(summary.tokenEvents)] }), _jsx(UsageBreakdownLines, { totals: summary.totals }), _jsxs(Text, { children: ["estimated credits: ", formatUsageCredits(summary.totals)] }), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(summary.totals)] }), _jsxs(Text, { children: ["models: ", summary.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
267
+ const isCodexProvider = props.stats.providerId === "codex";
268
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: props.stats.providerLabel }), _jsxs(Text, { children: ["root: ", summary.rootLabel, " (", summary.rootPath, ")"] }), _jsxs(Text, { children: ["files: ", formatInteger(summary.filesScanned), " lines: ", formatInteger(summary.linesRead), " token events: ", formatInteger(summary.tokenEvents)] }), _jsx(UsageBreakdownLines, { totals: summary.totals }), isCodexProvider ? (_jsxs(Text, { children: ["Burned tokens for: ", formatUsageUsd(summary.totals)] })) : (_jsxs(Text, { children: ["estimated credits: ", formatUsageCredits(summary.totals)] })), _jsxs(Text, { children: ["IpO: ", formatInputPerOutput(summary.totals)] }), _jsxs(Text, { children: ["models: ", summary.distinctModels.join(", ") || "none"] }), _jsxs(Text, { children: ["plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
233
269
  }
234
270
  function ContentPanel(props) {
235
271
  if (props.providerState.status === "loading") {
@@ -239,73 +275,115 @@ function ContentPanel(props) {
239
275
  return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
240
276
  }
241
277
  if (props.tabId === "limit-windows") {
242
- return _jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey });
278
+ return (_jsx(LimitWindowsPanel, { stats: props.providerState.stats, selectedRowKey: props.selectedLimitRowKey, availableHeight: props.availableHeight }));
243
279
  }
244
280
  if (props.tabId === "summary") {
245
281
  return _jsx(SummaryPanel, { stats: props.providerState.stats });
246
282
  }
247
283
  if (props.tabId === "day-to-day-analyses") {
248
- return _jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey });
284
+ return (_jsx(DayToDayPanel, { stats: props.providerState.stats, selectedDayKey: props.selectedDayKey, availableHeight: props.availableHeight }));
249
285
  }
250
- return _jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId });
286
+ return (_jsx(UsageByModelPanel, { stats: props.providerState.stats, selectedModelId: props.selectedModelId, availableHeight: props.availableHeight }));
251
287
  }
252
288
  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
- })] }));
289
+ const hasPrimaryWindows = props.stats.primaryLimitWindows.length > 0;
290
+ const bodyLines = [
291
+ { key: "primary-title", text: "Primary Limit Windows", bold: true },
292
+ ...buildLimitWindowSectionLines("primary", props.stats.primaryLimitWindows, props.selectedRowKey),
293
+ ...(hasPrimaryWindows
294
+ ? [{ key: "section-separator", text: buildLimitSectionSeparatorLine(), color: "gray" }]
295
+ : [{ key: "section-gap", text: "" }]),
296
+ { key: "secondary-title", text: "Secondary Limit Windows", bold: true },
297
+ ...buildLimitWindowSectionLines("secondary", props.stats.secondaryLimitWindows, props.selectedRowKey)
298
+ ];
299
+ return (_jsx(ScrollableLineViewport, { bodyLines: bodyLines, selectedBodyLineKey: props.selectedRowKey ? `limit-row:${props.selectedRowKey}` : undefined, availableHeight: props.availableHeight }));
265
300
  }
266
301
  function UsageByModelPanel(props) {
267
302
  if (props.stats.modelUsage.length === 0) {
268
303
  return _jsx(Text, { color: "gray", children: "No model usage found." });
269
304
  }
270
305
  const totals = props.stats.summary.totals;
271
- 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;
276
- }
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)] })] }));
279
- }
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;
284
- }
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)] })] }));
306
+ const isCodexProvider = props.stats.providerId === "codex";
307
+ return (_jsx(ScrollableLineViewport, { headerLines: [
308
+ {
309
+ key: "model-header",
310
+ text: isCodexProvider
311
+ ? `${pad("model", MODEL_USAGE_COLUMNS.model)} ${pad("input", MODEL_USAGE_COLUMNS.input)} ${pad("output", MODEL_USAGE_COLUMNS.output)} ${pad("cacheRead", MODEL_USAGE_COLUMNS.cacheRead)} ${pad("cacheWrite", MODEL_USAGE_COLUMNS.cacheWrite)} ${pad("cacheW5m", MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", MODEL_USAGE_COLUMNS.cacheWrite1h)} value`
312
+ : `${pad("model", MODEL_USAGE_COLUMNS.model)} ${pad("input", MODEL_USAGE_COLUMNS.input)} ${pad("output", MODEL_USAGE_COLUMNS.output)} ${pad("cacheRead", MODEL_USAGE_COLUMNS.cacheRead)} ${pad("cacheWrite", MODEL_USAGE_COLUMNS.cacheWrite)} ${pad("cacheW5m", MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", MODEL_USAGE_COLUMNS.cacheWrite1h)} ${pad("credits", MODEL_USAGE_COLUMNS.credits)} value`,
313
+ color: "gray"
314
+ }
315
+ ], bodyLines: props.stats.modelUsage.map((row) => ({
316
+ key: `model-row:${row.modelId}`,
317
+ text: formatModelUsageRow(row.modelId, row.totals, isCodexProvider),
318
+ inverse: props.selectedModelId === row.modelId,
319
+ color: props.selectedModelId === row.modelId ? "cyan" : undefined
320
+ })), footerLines: [
321
+ {
322
+ key: "model-total",
323
+ text: formatModelUsageRow("TOTAL", totals, isCodexProvider),
324
+ color: "cyan"
325
+ }
326
+ ], selectedBodyLineKey: props.selectedModelId ? `model-row:${props.selectedModelId}` : undefined, availableHeight: props.availableHeight }));
287
327
  }
288
328
  function DayToDayPanel(props) {
289
329
  if (props.stats.dayUsage.length === 0) {
290
330
  return _jsx(Text, { color: "gray", children: "No day-by-day usage found." });
291
331
  }
292
332
  const totals = props.stats.summary.totals;
293
- 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;
298
- }
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
- })] }));
301
- }
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;
306
- }
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
- })] }));
333
+ return (_jsx(ScrollableLineViewport, { headerLines: [
334
+ {
335
+ key: "day-header",
336
+ text: `${pad("day", DAY_USAGE_COLUMNS.day)} ${pad("events", DAY_USAGE_COLUMNS.events)} ${pad("input", DAY_USAGE_COLUMNS.input)} ${pad("output", DAY_USAGE_COLUMNS.output)} ${pad("cacheRead", DAY_USAGE_COLUMNS.cacheRead)} ${pad("cacheWrite", DAY_USAGE_COLUMNS.cacheWrite)} ${pad("cacheW5m", DAY_USAGE_COLUMNS.cacheWrite5m)} ${pad("cacheW1h", DAY_USAGE_COLUMNS.cacheWrite1h)} value`,
337
+ color: "gray"
338
+ }
339
+ ], bodyLines: props.stats.dayUsage.map((row) => ({
340
+ key: `day-row:${row.dayKey}`,
341
+ text: `${pad(formatUtcDay(row.dayKey), DAY_USAGE_COLUMNS.day)} ${pad(formatCompactTokenCount(row.totals.eventCount), DAY_USAGE_COLUMNS.events)} ${pad(formatCompactTokenCount(row.totals.inputTokens), DAY_USAGE_COLUMNS.input)} ${pad(formatCompactTokenCount(row.totals.outputTokens), DAY_USAGE_COLUMNS.output)} ${pad(formatCompactCacheTokens(row.totals, row.totals.cacheReadInputTokens), DAY_USAGE_COLUMNS.cacheRead)} ${pad(formatCompactCacheTokens(row.totals, row.totals.cacheWriteInputTokens), DAY_USAGE_COLUMNS.cacheWrite)} ${pad(formatOptionalCompactTokens(row.totals.cacheWrite5mInputTokens), DAY_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatOptionalCompactTokens(row.totals.cacheWrite1hInputTokens), DAY_USAGE_COLUMNS.cacheWrite1h)} ${pad(formatUsageUsd(row.totals), DAY_USAGE_COLUMNS.value)}`,
342
+ inverse: props.selectedDayKey === row.dayKey,
343
+ color: props.selectedDayKey === row.dayKey ? "cyan" : undefined
344
+ })), selectedBodyLineKey: props.selectedDayKey ? `day-row:${props.selectedDayKey}` : undefined, availableHeight: props.availableHeight }));
345
+ }
346
+ function ScrollableLineViewport(props) {
347
+ const headerLines = props.headerLines ?? [];
348
+ const footerLines = props.footerLines ?? [];
349
+ const layout = resolveScrollableViewportLayout(props.availableHeight, headerLines.length, props.bodyLines.length, footerLines.length);
350
+ const selectedBodyLineIndex = props.selectedBodyLineKey
351
+ ? props.bodyLines.findIndex((line) => line.key === props.selectedBodyLineKey)
352
+ : -1;
353
+ const scrollOffset = useAutoScrollOffset(selectedBodyLineIndex, props.bodyLines.length, layout.bodyVisibleCount);
354
+ const visibleHeaderLines = headerLines.slice(0, layout.headerVisibleCount);
355
+ const visibleBodyLines = props.bodyLines.slice(scrollOffset, scrollOffset + layout.bodyVisibleCount);
356
+ const visibleFooterLines = footerLines.slice(Math.max(0, footerLines.length - layout.footerVisibleCount));
357
+ const bodyScrollbarLines = buildScrollbarLines(layout.bodyVisibleCount, props.bodyLines.length, scrollOffset);
358
+ const showScrollbar = bodyScrollbarLines.length > 0;
359
+ 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] }));
360
+ }
361
+ function ScrollableViewportLine(props) {
362
+ return (_jsx(Text, { bold: props.line.bold, color: props.line.color, inverse: props.line.inverse, wrap: "truncate-end", children: props.line.text }));
363
+ }
364
+ function buildLimitWindowSectionLines(scope, windows, selectedRowKey) {
365
+ if (windows.length === 0) {
366
+ return [{ key: `${scope}-empty`, text: "No windows found.", color: "gray" }];
367
+ }
368
+ return [
369
+ {
370
+ key: `${scope}-header`,
371
+ 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`,
372
+ color: "gray"
373
+ },
374
+ ...windows.map((window) => {
375
+ const lineKey = getLimitRowKey(window);
376
+ const windowLabel = formatWindowMinutes(window.windowMinutes);
377
+ const usedLabel = formatUsedPercentRange(window.minUsedPercent, window.maxUsedPercent);
378
+ const isSelected = selectedRowKey === lineKey;
379
+ return {
380
+ key: `limit-row:${lineKey}`,
381
+ 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)}`,
382
+ inverse: isSelected,
383
+ color: isSelected ? "cyan" : undefined
384
+ };
385
+ })
386
+ ];
309
387
  }
310
388
  function SelectionDetailsPanel(props) {
311
389
  if (props.providerState.status !== "ready") {
@@ -313,24 +391,44 @@ function SelectionDetailsPanel(props) {
313
391
  }
314
392
  if (props.tabId === "limit-windows" && props.selectedLimitRow) {
315
393
  const row = props.selectedLimitRow;
316
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Limit details" }), _jsxs(Text, { children: [row.scope, " plan: ", row.planType, " window: ", formatWindowMinutes(row.windowMinutes), " used: ", row.minUsedPercent, "%", "->", row.maxUsedPercent, "% limit: ", row.limitId] }), _jsxs(Text, { children: ["range: ", formatLocalDateTime(row.startTimeUtcIso), " ", "->", " ", formatLocalDateTime(row.endTimeUtcIso), " events: ", formatInteger(row.eventCount)] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
394
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Limit details" }), _jsxs(Text, { children: [row.scope, " plan: ", row.planType, " window: ", formatWindowMinutes(row.windowMinutes), " used: ", formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent), " limit: ", row.limitId] }), _jsxs(Text, { children: ["range: ", formatLocalDateTime(row.startTimeUtcIso), " ", "->", " ", formatLocalDateTime(row.endTimeUtcIso), " events: ", formatInteger(row.eventCount)] }), _jsx(UsageTotalsDetails, { totals: row.totals })] }));
317
395
  }
318
396
  if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
319
397
  const row = props.selectedDayRow;
320
398
  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
399
  }
322
400
  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 })] }));
401
+ 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
402
  }
325
403
  return null;
326
404
  }
327
405
  function UsageTotalsDetails(props) {
328
406
  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)] })] }));
407
+ 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
408
  }
331
409
  function formatInteger(value) {
332
410
  return Math.round(value).toLocaleString("en-US");
333
411
  }
412
+ function formatCompactTokenCount(value) {
413
+ const roundedValue = Math.round(value);
414
+ if (roundedValue < 1000) {
415
+ return formatInteger(roundedValue);
416
+ }
417
+ if (roundedValue < 100000) {
418
+ return `${formatCompactNumber(roundedValue / 1000)}K`;
419
+ }
420
+ if (roundedValue < 1000000) {
421
+ return `${formatInteger(Math.round(roundedValue / 1000))}K`;
422
+ }
423
+ return `${formatCompactNumber(roundedValue / 1000000)}M`;
424
+ }
425
+ function formatCompactNumber(value) {
426
+ const maximumFractionDigits = value < 10 ? 2 : value < 100 ? 1 : 0;
427
+ return value.toLocaleString("en-US", {
428
+ maximumFractionDigits,
429
+ minimumFractionDigits: 0
430
+ });
431
+ }
334
432
  function formatCredits(value) {
335
433
  if (value > 0 && value < 0.01) {
336
434
  return "<0.01";
@@ -340,25 +438,45 @@ function formatCredits(value) {
340
438
  maximumFractionDigits: 2
341
439
  });
342
440
  }
343
- function formatUsageCredits(totals) {
344
- return totals.estimatedCreditsStatus === "unavailable" ? "unknown" : formatCredits(totals.estimatedCredits);
441
+ function formatUsageCredits(totals, modelId) {
442
+ if (isInternalUsageModel(modelId)) {
443
+ return "N/A";
444
+ }
445
+ return totals.estimatedCreditsStatus === "unavailable" ? "-" : formatCredits(totals.estimatedCredits);
345
446
  }
346
- function formatUsageUsd(totals) {
447
+ function formatUsageUsd(totals, modelId) {
448
+ if (isInternalUsageModel(modelId)) {
449
+ return "N/A";
450
+ }
347
451
  return totals.estimatedCreditsStatus === "unavailable"
348
- ? "unknown"
452
+ ? "-"
349
453
  : formatUsd(totals.estimatedCredits * CODEX_CREDIT_COST_USD);
350
454
  }
455
+ function isInternalUsageModel(modelId) {
456
+ return modelId === "codex-auto-review" || modelId === "<synthetic>";
457
+ }
351
458
  function formatUsd(value) {
352
- if (value > 0 && value < 0.0001) {
353
- return "<$0.0001";
354
- }
355
- return value.toLocaleString("en-US", {
459
+ const roundedUpValue = value > 0 ? Math.ceil(value * 100) / 100 : value;
460
+ return roundedUpValue.toLocaleString("en-US", {
356
461
  currency: "USD",
357
462
  style: "currency",
358
463
  minimumFractionDigits: 2,
359
- maximumFractionDigits: 4
464
+ maximumFractionDigits: 2
360
465
  });
361
466
  }
467
+ function formatUsedPercentRange(minUsedPercent, maxUsedPercent) {
468
+ return minUsedPercent === maxUsedPercent
469
+ ? `${minUsedPercent}%`
470
+ : `${minUsedPercent}%->${maxUsedPercent}%`;
471
+ }
472
+ function buildLimitSectionSeparatorLine() {
473
+ return "─".repeat(LIMIT_WINDOW_COLUMNS.plan +
474
+ LIMIT_WINDOW_COLUMNS.window +
475
+ LIMIT_WINDOW_COLUMNS.used +
476
+ LIMIT_WINDOW_COLUMNS.date * 2 +
477
+ LIMIT_WINDOW_COLUMNS.value +
478
+ 5);
479
+ }
362
480
  function formatWindowMinutes(value) {
363
481
  const hours = value / 60;
364
482
  if (hours >= 24) {
@@ -381,7 +499,7 @@ function formatLocalDateTime(value) {
381
499
  }
382
500
  function formatUtcDay(value) {
383
501
  if (value === "unknown") {
384
- return "unknown";
502
+ return "-";
385
503
  }
386
504
  const parts = new Intl.DateTimeFormat("en-US", {
387
505
  month: "short",
@@ -394,37 +512,221 @@ function formatUtcDay(value) {
394
512
  }
395
513
  function formatEventRange(firstEventUtcIso, lastEventUtcIso) {
396
514
  if (!firstEventUtcIso || !lastEventUtcIso) {
397
- return "unknown";
515
+ return "-";
398
516
  }
399
517
  return `${formatLocalDateTime(firstEventUtcIso)} -> ${formatLocalDateTime(lastEventUtcIso)}`;
400
518
  }
401
519
  function pad(value, length) {
402
520
  return value.length >= length ? value.slice(0, length) : value.padEnd(length);
403
521
  }
522
+ function formatModelUsageRow(modelId, totals, isCodexProvider) {
523
+ const displayModelId = modelId === "unknown" ? "-" : modelId;
524
+ const prefix = `${pad(displayModelId, MODEL_USAGE_COLUMNS.model)} ${pad(formatCompactTokenCount(totals.inputTokens), MODEL_USAGE_COLUMNS.input)} ${pad(formatCompactTokenCount(totals.outputTokens), MODEL_USAGE_COLUMNS.output)} ${pad(formatCompactCacheTokens(totals, totals.cacheReadInputTokens), MODEL_USAGE_COLUMNS.cacheRead)} ${pad(formatCompactCacheTokens(totals, totals.cacheWriteInputTokens), MODEL_USAGE_COLUMNS.cacheWrite)} ${pad(formatOptionalCompactTokens(totals.cacheWrite5mInputTokens), MODEL_USAGE_COLUMNS.cacheWrite5m)} ${pad(formatOptionalCompactTokens(totals.cacheWrite1hInputTokens), MODEL_USAGE_COLUMNS.cacheWrite1h)}`;
525
+ return isCodexProvider
526
+ ? `${prefix} ${pad(formatUsageUsd(totals, modelId), MODEL_USAGE_COLUMNS.value)}`
527
+ : `${prefix} ${pad(formatUsageCredits(totals, modelId), MODEL_USAGE_COLUMNS.credits)} ${pad(formatUsageUsd(totals, modelId), MODEL_USAGE_COLUMNS.value)}`;
528
+ }
404
529
  function UsageBreakdownLines(props) {
405
530
  const { totals } = props;
406
- if (totals.tokenBreakdown.schema === "anthropic") {
407
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input total: ", formatInteger(totals.inputTotalTokens), " input: ", formatInteger(totals.tokenBreakdown.inputTokens), " cacheW5m: ", formatInteger(totals.tokenBreakdown.cacheWrite5mInputTokens)] }), _jsxs(Text, { children: ["cacheW1h: ", formatInteger(totals.tokenBreakdown.cacheWrite1hInputTokens), " cacheRead: ", formatInteger(totals.tokenBreakdown.cacheReadInputTokens), " output: ", formatInteger(totals.outputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
531
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input: ", formatInteger(totals.inputTokens), " output: ", formatInteger(totals.outputTokens), " cacheRead: ", formatCacheTokens(totals, totals.cacheReadInputTokens)] }), _jsxs(Text, { children: ["cacheWrite: ", formatCacheTokens(totals, totals.cacheWriteInputTokens), " cacheW5m: ", formatOptionalTokens(totals.cacheWrite5mInputTokens), " cacheW1h: ", formatOptionalTokens(totals.cacheWrite1hInputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
532
+ }
533
+ function formatCacheTokens(totals, value) {
534
+ if (totals.cacheStatus === "unavailable") {
535
+ return "-";
408
536
  }
409
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input total: ", formatInteger(totals.inputTotalTokens), " uncached: ", formatOpenAiTokens(totals, "non-cached"), " cached: ", formatOpenAiTokens(totals, "cached")] }), _jsxs(Text, { children: ["output: ", formatInteger(totals.outputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
537
+ return formatOptionalTokens(value);
410
538
  }
411
- function formatOpenAiTokens(totals, kind) {
412
- if (totals.tokenBreakdown.schema !== "openai" || totals.cacheStatus === "unavailable") {
413
- return "unknown";
539
+ function formatCompactCacheTokens(totals, value) {
540
+ if (totals.cacheStatus === "unavailable") {
541
+ return "-";
414
542
  }
415
- return formatInteger(kind === "non-cached" ? totals.tokenBreakdown.nonCachedInputTokens : totals.tokenBreakdown.cachedInputTokens);
543
+ return formatOptionalCompactTokens(value);
544
+ }
545
+ function formatOptionalTokens(value) {
546
+ return value > 0 ? formatInteger(value) : "-";
547
+ }
548
+ function formatOptionalCompactTokens(value) {
549
+ return value > 0 ? formatCompactTokenCount(value) : "-";
416
550
  }
417
551
  function formatInputPerOutput(totals) {
418
552
  if (totals.outputTokens <= 0) {
419
- return totals.tokenBreakdown.schema === "anthropic" ? "input:cacheW5m:cacheW1h:cacheRead:output = 0:0:0:0:0" : "uncached:cached:output = 0:0:0";
420
- }
421
- if (totals.tokenBreakdown.schema === "anthropic") {
422
- return `input:cacheW5m:cacheW1h:cacheRead:output = ${formatInteger(Math.round(totals.tokenBreakdown.inputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheWrite5mInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheWrite1hInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cacheReadInputTokens / totals.outputTokens))}:1`;
553
+ return "input:cacheRead:cacheWrite:output = 0:0:0:0";
423
554
  }
424
555
  if (totals.cacheStatus === "unavailable") {
425
- return "uncached:cached:output = unknown:unknown:1";
556
+ return `input:cacheRead:cacheWrite:output = ${formatInteger(Math.round(totals.inputTokens / totals.outputTokens))}:-:-:1`;
557
+ }
558
+ return `input:cacheRead:cacheWrite:output = ${formatInteger(Math.round(totals.inputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.cacheReadInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.cacheWriteInputTokens / totals.outputTokens))}:1`;
559
+ }
560
+ function useMeasuredElementSize() {
561
+ const ref = useRef(null);
562
+ const [size, setSize] = useState({ width: 0, height: 0 });
563
+ useEffect(() => {
564
+ if (!ref.current) {
565
+ return;
566
+ }
567
+ const nextSize = measureElement(ref.current);
568
+ setSize((current) => current.width === nextSize.width && current.height === nextSize.height ? current : nextSize);
569
+ });
570
+ return {
571
+ ref,
572
+ width: size.width,
573
+ height: size.height
574
+ };
575
+ }
576
+ // Walks the Yoga layout tree to get a node's absolute position/size (in terminal
577
+ // cells). Reading it live at click time keeps hit-testing correct across resizes,
578
+ // wrapping, and re-layouts without tracking coordinates by hand.
579
+ function getAbsoluteRect(node) {
580
+ const yogaNode = node?.yogaNode;
581
+ if (!node || !yogaNode) {
582
+ return undefined;
583
+ }
584
+ let left = 0;
585
+ let top = 0;
586
+ let current = node;
587
+ while (current?.yogaNode) {
588
+ left += current.yogaNode.getComputedLeft();
589
+ top += current.yogaNode.getComputedTop();
590
+ current = current.parentNode;
591
+ }
592
+ return {
593
+ left,
594
+ top,
595
+ width: yogaNode.getComputedWidth(),
596
+ height: yogaNode.getComputedHeight()
597
+ };
598
+ }
599
+ function parseMouseClicks(chunk) {
600
+ const clicks = [];
601
+ for (const match of chunk.matchAll(SGR_MOUSE_SEQUENCE)) {
602
+ const buttonCode = Number(match[1]);
603
+ const isPress = match[4] === "M";
604
+ // Only react to a plain left-button press (button 0, no modifier bits). Any
605
+ // press carrying Shift/Ctrl/Alt or motion/wheel bits is left alone so the
606
+ // terminal can still use it for native text selection.
607
+ if (!isPress || buttonCode !== 0) {
608
+ continue;
609
+ }
610
+ clicks.push({ column: Number(match[2]), row: Number(match[3]) });
611
+ }
612
+ return clicks;
613
+ }
614
+ // Tracks the on-screen rectangle of named clickable regions (the tabs) via Ink
615
+ // refs and resolves a click coordinate back to a region id.
616
+ function useClickRegions() {
617
+ const nodesRef = useRef(new Map());
618
+ const refCallbacksRef = useRef(new Map());
619
+ const getRegionRef = useCallback((id) => {
620
+ const cached = refCallbacksRef.current.get(id);
621
+ if (cached) {
622
+ return cached;
623
+ }
624
+ const callback = (node) => {
625
+ if (node) {
626
+ nodesRef.current.set(id, node);
627
+ }
628
+ else {
629
+ nodesRef.current.delete(id);
630
+ }
631
+ };
632
+ refCallbacksRef.current.set(id, callback);
633
+ return callback;
634
+ }, []);
635
+ const resolveClick = useCallback((click) => {
636
+ // SGR mouse coordinates are 1-based; Ink layout coordinates are 0-based.
637
+ const column = click.column - 1;
638
+ const row = click.row - 1;
639
+ for (const [id, node] of nodesRef.current) {
640
+ const rect = getAbsoluteRect(node);
641
+ if (rect &&
642
+ column >= rect.left &&
643
+ column < rect.left + rect.width &&
644
+ row >= rect.top &&
645
+ row < rect.top + rect.height) {
646
+ return id;
647
+ }
648
+ }
649
+ return undefined;
650
+ }, []);
651
+ return { getRegionRef, resolveClick };
652
+ }
653
+ // Parses left-clicks out of Ink's raw input stream and forwards them to onClick.
654
+ // Keyboard handling stays in useInput; this only consumes mouse reports.
655
+ function useMouseClick(onClick) {
656
+ const { internal_eventEmitter } = useStdin();
657
+ const handlerRef = useRef(onClick);
658
+ handlerRef.current = onClick;
659
+ useEffect(() => {
660
+ const handleInput = (chunk) => {
661
+ for (const click of parseMouseClicks(chunk)) {
662
+ handlerRef.current(click);
663
+ }
664
+ };
665
+ internal_eventEmitter.on("input", handleInput);
666
+ return () => {
667
+ internal_eventEmitter.off("input", handleInput);
668
+ };
669
+ }, [internal_eventEmitter]);
670
+ }
671
+ function useAutoScrollOffset(selectedIndex, rowCount, viewportSize) {
672
+ const [scrollOffset, setScrollOffset] = useState(0);
673
+ useEffect(() => {
674
+ const maxOffset = Math.max(0, rowCount - Math.max(0, viewportSize));
675
+ setScrollOffset((current) => {
676
+ let next = Math.max(0, Math.min(current, maxOffset));
677
+ if (selectedIndex < 0 || viewportSize <= 0) {
678
+ return next;
679
+ }
680
+ if (selectedIndex < next) {
681
+ next = selectedIndex;
682
+ }
683
+ else if (selectedIndex >= next + viewportSize) {
684
+ next = selectedIndex - viewportSize + 1;
685
+ }
686
+ return Math.max(0, Math.min(next, maxOffset));
687
+ });
688
+ }, [rowCount, selectedIndex, viewportSize]);
689
+ return scrollOffset;
690
+ }
691
+ function resolveScrollableViewportLayout(availableHeight, headerCount, bodyCount, footerCount) {
692
+ const totalHeight = Math.max(1, availableHeight || 1);
693
+ if (bodyCount === 0) {
694
+ const headerVisibleCount = Math.min(headerCount, totalHeight);
695
+ return {
696
+ headerVisibleCount,
697
+ bodyVisibleCount: 0,
698
+ footerVisibleCount: Math.min(footerCount, Math.max(0, totalHeight - headerVisibleCount))
699
+ };
700
+ }
701
+ let headerVisibleCount = Math.min(headerCount, totalHeight);
702
+ let footerVisibleCount = Math.min(footerCount, Math.max(0, totalHeight - headerVisibleCount - 1));
703
+ let bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
704
+ if (bodyVisibleCount === 0 && footerVisibleCount > 0) {
705
+ footerVisibleCount -= 1;
706
+ bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
707
+ }
708
+ if (bodyVisibleCount === 0 && headerVisibleCount > 0) {
709
+ headerVisibleCount -= 1;
710
+ bodyVisibleCount = Math.min(bodyCount, Math.max(0, totalHeight - headerVisibleCount - footerVisibleCount));
711
+ }
712
+ if (bodyVisibleCount === 0) {
713
+ return {
714
+ headerVisibleCount: 0,
715
+ bodyVisibleCount: Math.min(bodyCount, totalHeight),
716
+ footerVisibleCount: 0
717
+ };
718
+ }
719
+ return { headerVisibleCount, bodyVisibleCount, footerVisibleCount };
720
+ }
721
+ function buildScrollbarLines(viewportSize, rowCount, scrollOffset) {
722
+ if (viewportSize <= 0 || rowCount <= viewportSize) {
723
+ return [];
426
724
  }
427
- return `uncached:cached:output = ${formatInteger(Math.round(totals.tokenBreakdown.nonCachedInputTokens / totals.outputTokens))}:${formatInteger(Math.round(totals.tokenBreakdown.cachedInputTokens / totals.outputTokens))}:1`;
725
+ const thumbSize = Math.max(1, Math.round((viewportSize * viewportSize) / rowCount));
726
+ const maxThumbOffset = Math.max(0, viewportSize - thumbSize);
727
+ const maxScrollOffset = Math.max(1, rowCount - viewportSize);
728
+ const thumbOffset = Math.round((Math.max(0, Math.min(scrollOffset, maxScrollOffset)) / maxScrollOffset) * maxThumbOffset);
729
+ return Array.from({ length: viewportSize }, (_, index) => index >= thumbOffset && index < thumbOffset + thumbSize ? SCROLLBAR_THUMB_GLYPH : SCROLLBAR_TRACK_GLYPH);
428
730
  }
429
731
  function clampSelectionIndex(value, rowCount) {
430
732
  if (rowCount === 0) {
@@ -444,7 +746,7 @@ function providerUsageScore(state) {
444
746
  return 0;
445
747
  }
446
748
  const totals = state.stats.summary.totals;
447
- return totals.inputTotalTokens + totals.outputTokens;
749
+ return totals.inputTokens + totals.outputTokens + totals.cacheReadInputTokens + totals.cacheWriteInputTokens;
448
750
  }
449
751
  function getLimitRows(providerState) {
450
752
  if (providerState.status !== "ready") {
@@ -472,7 +774,76 @@ function parseStatsOptions(argv) {
472
774
  verbose: argv.includes("-v") || argv.includes("--verbose")
473
775
  };
474
776
  }
777
+ function useViewportHeight() {
778
+ const { stdout } = useStdout();
779
+ const [viewportHeight, setViewportHeight] = useState(() => resolveViewportHeight(stdout.rows));
780
+ useEffect(() => {
781
+ const updateViewportHeight = () => {
782
+ setViewportHeight(resolveViewportHeight(stdout.rows));
783
+ };
784
+ updateViewportHeight();
785
+ if (!stdout.isTTY) {
786
+ return;
787
+ }
788
+ stdout.on("resize", updateViewportHeight);
789
+ return () => {
790
+ stdout.off("resize", updateViewportHeight);
791
+ };
792
+ }, [stdout]);
793
+ return viewportHeight;
794
+ }
795
+ function resolveViewportHeight(rows) {
796
+ const terminalRows = typeof rows === "number" && rows > 0 ? rows : 24;
797
+ // Keep Ink below the terminal height so it stays on its incremental redraw path
798
+ // instead of the full-screen print path that can leave the viewport scrolled.
799
+ return Math.max(1, terminalRows - 1);
800
+ }
475
801
  export function main(argv = process.argv.slice(2)) {
476
- render(_jsx(App, { statsOptions: parseStatsOptions(argv) }));
802
+ const restoreFullscreen = enterFullscreenMode(process.stdout);
803
+ const disableMouse = enableMouseReporting(process.stdout);
804
+ const exitHandler = () => {
805
+ disableMouse();
806
+ restoreFullscreen();
807
+ };
808
+ process.once("exit", exitHandler);
809
+ const instance = render(_jsx(App, { statsOptions: parseStatsOptions(argv) }), {
810
+ stdout: process.stdout,
811
+ stdin: process.stdin,
812
+ stderr: process.stderr
813
+ });
814
+ void instance.waitUntilExit().finally(() => {
815
+ process.off("exit", exitHandler);
816
+ instance.cleanup();
817
+ disableMouse();
818
+ restoreFullscreen();
819
+ });
820
+ }
821
+ function enterFullscreenMode(stdout) {
822
+ if (!stdout.isTTY) {
823
+ return () => { };
824
+ }
825
+ let restored = false;
826
+ stdout.write(ENTER_FULLSCREEN_MODE);
827
+ return () => {
828
+ if (restored) {
829
+ return;
830
+ }
831
+ restored = true;
832
+ stdout.write(EXIT_FULLSCREEN_MODE);
833
+ };
834
+ }
835
+ function enableMouseReporting(stdout) {
836
+ if (!stdout.isTTY) {
837
+ return () => { };
838
+ }
839
+ let disabled = false;
840
+ stdout.write(ENABLE_MOUSE_TRACKING);
841
+ return () => {
842
+ if (disabled) {
843
+ return;
844
+ }
845
+ disabled = true;
846
+ stdout.write(DISABLE_MOUSE_TRACKING);
847
+ };
477
848
  }
478
849
  main();