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.
- package/README.md +1 -0
- package/ink-app/dist/index.js +474 -103
- package/ink-app/dist/providers/antigravity.js +290 -0
- package/ink-app/dist/providers/claude.js +458 -54
- package/ink-app/dist/providers/codex.js +161 -23
- package/ink-app/dist/providers/contract.js +14 -46
- package/ink-app/dist/providers/copilot.js +31 -46
- package/ink-app/dist/providers/index.js +14 -1
- package/ink-app/dist/providers/limits.js +1 -1
- package/ink-app/dist/providers/pricing.js +18 -0
- package/ink-app/dist/reporting.js +99 -0
- package/package.json +15 -14
package/ink-app/dist/index.js
CHANGED
|
@@ -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
|
|
29
|
+
const MODEL_USAGE_COLUMNS = {
|
|
21
30
|
model: 17,
|
|
22
|
-
input:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
40
|
+
const DAY_USAGE_COLUMNS = {
|
|
39
41
|
day: 11,
|
|
40
42
|
events: 6,
|
|
41
43
|
input: 11,
|
|
42
44
|
output: 10,
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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,
|
|
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: ["
|
|
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
|
-
|
|
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
|
-
? "
|
|
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
|
-
|
|
353
|
-
|
|
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:
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
407
|
-
|
|
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 (
|
|
537
|
+
return formatOptionalTokens(value);
|
|
410
538
|
}
|
|
411
|
-
function
|
|
412
|
-
if (totals.
|
|
413
|
-
return "
|
|
539
|
+
function formatCompactCacheTokens(totals, value) {
|
|
540
|
+
if (totals.cacheStatus === "unavailable") {
|
|
541
|
+
return "-";
|
|
414
542
|
}
|
|
415
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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();
|