letmecode 0.1.20 → 0.1.22

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 CHANGED
@@ -3,43 +3,26 @@
3
3
  Ussage:
4
4
 
5
5
  ```bash
6
- npx -y letmecode
7
- npx -y letmecode -- -h
8
- npx -y letmecode -- --log-to log.txt
6
+ npx -y letmecode@latest
9
7
  ```
10
8
 
11
- `--log-to` now records Claude binary discovery, session-root selection, parsed transcript file summaries, entrypoint matching, raw `/usage` output, and live-window event matching so zero-token windows are diagnosable.
12
9
 
13
- <img width="2308" height="1491" alt="image" src="https://github.com/user-attachments/assets/f3f52d79-00e3-4ff5-bf2f-65f8be632aaa" />
10
+ Preview:
14
11
 
15
- ## Controls
12
+ <img width="2301" height="1397" alt="image" src="https://github.com/user-attachments/assets/c37d6847-9926-4977-8592-5cab8346a86f" />
16
13
 
17
- | Key | Action |
18
- | --- | --- |
19
- | `[` / `]` | Switch providers |
20
- | `Tab` / `Shift+Tab` | Switch providers when supported by the terminal |
21
- | `Up` / `Down` or `k` / `j` | Switch dashboard sections |
22
- | `Left` / `Right` | Select the previous or next table row |
23
- | `Enter` | Run the selected provider action |
24
- | `1` | Select the Copilot VS Code setup action |
25
- | `h` / `l` | Select a Copilot setup action |
26
- | `q` or `Esc` | Quit |
27
- | Mouse click | Click a provider tab or a section tab to switch to it. Hold `Shift` while dragging to select/copy text anywhere else. |
28
14
 
29
- ## Copilot
30
15
 
31
- Copilot CLI usage is read from `~/.copilot/session-state`.
32
16
 
33
- VS Code extension usage needs file OTEL logging first. Select the `Copilot` provider, choose `Start logging VS Code` with `1` or `h` / `l`, then press `Enter`; letmecode will update the current user's VS Code settings with:
17
+ ## Advanced ussage
34
18
 
35
- ```json
36
- {
37
- "github.copilot.chat.otel.enabled": true,
38
- "github.copilot.chat.otel.exporterType": "file",
39
- "github.copilot.chat.otel.outfile": "~/.copilot/otel/vscode.jsonl",
40
- "github.copilot.chat.otel.captureContent": false
41
- }
42
19
  ```
20
+ npx -y letmecode -- -h
21
+ npx -y letmecode -- --log-to log.txt
22
+ ```
23
+
24
+ `--log-to` now records Claude binary discovery, session-root selection, parsed transcript file summaries, entrypoint matching, raw `/usage` output, and live-window event matching so zero-token windows are diagnosable.
25
+
43
26
 
44
27
  ## Local development
45
28
 
@@ -19,7 +19,7 @@ const DETAIL_TABS = [
19
19
  { id: "usage-by-model", label: "Models" }
20
20
  ];
21
21
  const CODEX_CREDIT_COST_USD = 0.01;
22
- const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Window", "Used", "Start", "End", "API eq."];
22
+ const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq."];
23
23
  const DAILY_TABLE_HEADERS = ["Day", "Ev", "Input", "Output", "C read", "C write", "API eq."];
24
24
  const MODEL_TABLE_HEADERS = ["Model", "Input", "Output", "C read", "C write", "API eq."];
25
25
  const COPILOT_ACTIONS = [
@@ -195,7 +195,7 @@ function App(props) {
195
195
  moveSelectedTableRow(-1);
196
196
  }
197
197
  });
198
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "white", backgroundColor: "green", children: " beta " })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "gray", children: "Provider " }), sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status, regionRef: getRegionRef(`provider:${state.provider.id}`) }, state.provider.id)))] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "View " }), DETAIL_TABS.map((tab, index) => (_jsx(DetailTab, { label: tab.label, active: index === selectedDetailTabIndex, regionRef: getRegionRef(`vtab:${index}`) }, tab.id)))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) }) }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { 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] }), _jsx(Text, { color: "gray", children: "Tab provider \u00B7 \u2190/\u2192 view \u00B7 \u2191/\u2193 row \u00B7 q quit" })] }));
198
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "white", backgroundColor: "green", children: " beta " })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "Provider " }), sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status, regionRef: getRegionRef(`provider:${state.provider.id}`) }, state.provider.id)))] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "View " }), DETAIL_TABS.map((tab, index) => (_jsx(DetailTab, { label: tab.label, active: index === selectedDetailTabIndex, regionRef: getRegionRef(`vtab:${index}`) }, tab.id)))] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) }) }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }), _jsx(Text, { color: "gray", children: "Tab provider \u00B7 \u2190/\u2192 view \u00B7 \u2191/\u2193 row \u00B7 q quit" })] }));
199
199
  }
200
200
  function CopilotActionsPanel(props) {
201
201
  if (props.providerState.provider.id !== "copilot") {
@@ -203,7 +203,7 @@ function CopilotActionsPanel(props) {
203
203
  }
204
204
  const hasNoUsage = props.providerState.status === "ready" && props.providerState.stats.summary.tokenEvents === 0;
205
205
  const accentColor = hasNoUsage ? "red" : "cyan";
206
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: accentColor, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "Copilot setup" }), _jsx(Box, { children: COPILOT_ACTIONS.map((action, index) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: index === (props.selectedActionIndex ?? 0), bold: hasNoUsage && action.id === "vscode", color: action.enabled ? (hasNoUsage && action.id === "vscode" ? accentColor : undefined) : "gray", children: `${index + 1} ${action.label}` }) }, action.id))) }), _jsx(Text, { color: hasNoUsage ? accentColor : "gray", children: "Press 1 or h/l to select an action, enter to run selected." }), props.actionMessage ? _jsx(Text, { children: props.actionMessage }) : null] }));
206
+ return (_jsxs(Box, { borderStyle: "round", borderColor: accentColor, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "Copilot setup" }), _jsx(Box, { children: COPILOT_ACTIONS.map((action, index) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: index === (props.selectedActionIndex ?? 0), bold: hasNoUsage && action.id === "vscode", color: action.enabled ? (hasNoUsage && action.id === "vscode" ? accentColor : undefined) : "gray", children: `${index + 1} ${action.label}` }) }, action.id))) }), _jsx(Text, { color: hasNoUsage ? accentColor : "gray", children: "Press 1 or h/l to select an action, enter to run selected." }), props.actionMessage ? _jsx(Text, { children: props.actionMessage }) : null] }));
207
207
  }
208
208
  function runCopilotAction(actionId, setCopilotActionMessage) {
209
209
  setCopilotActionMessage("Updating VS Code settings...");
@@ -247,7 +247,10 @@ function SummaryPanel(props) {
247
247
  const costPerEvent = totals.eventCount > 0
248
248
  ? (totals.estimatedCredits * CODEX_CREDIT_COST_USD) / totals.eventCount
249
249
  : 0;
250
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [props.stats.providerLabel, " overview"] }), _jsx(Text, { children: " " }), _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 45, children: [_jsx(DetailRow, { label: "Plan", value: summary.distinctPlanTypes.join(", ") || "none" }), _jsx(DetailRow, { label: "Models", value: summary.distinctModels.join(", ") || "none" }), _jsx(DetailRow, { label: "Period", value: period }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "Usage" }), _jsx(DetailRow, { label: "Events", value: formatInteger(totals.eventCount) }), _jsx(DetailRow, { label: "Input", value: formatOverviewTokenCount(totals.inputTokens) }), _jsx(DetailRow, { label: "Output", value: formatOverviewTokenCount(totals.outputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheOverviewTokenCount(totals, totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Reasoning", value: formatOverviewTokenCount(totals.reasoningOutputTokens) }), _jsx(DetailRow, { label: "Total", value: formatOverviewTokenCount(totals.totalTokens) }), _jsx(DetailRow, { label: "API equiv.", value: formatUsageUsd(totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Efficiency" }), _jsx(DetailRow, { label: "Cache ratio", value: formatPercent(cacheRatio) }), _jsx(DetailRow, { label: "Input/output", value: formatInputOutputRatio(totals) }), _jsx(DetailRow, { label: "Avg/event", value: `${formatOverviewTokenCount(averageTokensPerEvent)} tokens` }), _jsx(DetailRow, { label: "Cost/event", value: formatUnitUsd(costPerEvent) }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "Data source" }), _jsx(DetailRow, { label: "Files", value: formatInteger(summary.filesScanned) }), _jsx(DetailRow, { label: "Lines", value: formatInteger(summary.linesRead) }), _jsx(DetailRow, { label: "Path", value: summary.rootPath })] })] })] }));
250
+ return (_jsxs(Box, { borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Summary" }), _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 45, children: [_jsx(DetailRow, { label: "Plan", value: summary.distinctPlanTypes.join(", ") || "none" }), _jsx(DetailRow, { label: "Models", value: summary.distinctModels.join(", ") || "none" }), _jsx(DetailRow, { label: "Period", value: period }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "Usage" }), _jsx(DetailRow, { label: "Events", value: formatInteger(totals.eventCount) }), _jsx(DetailRow, { label: "Input", value: formatOverviewTokenCount(totals.inputTokens) }), _jsx(DetailRow, { label: "Output", value: formatOverviewTokenCount(totals.outputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheOverviewTokenCount(totals.cacheReadStatus, totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Reasoning", value: formatOverviewTokenCount(totals.reasoningOutputTokens) }), _jsx(DetailRow, { label: "Total", value: formatOverviewTokenCount(totals.totalTokens) }), _jsx(DetailRow, { label: "API equiv.", value: formatUsageUsd(totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Efficiency" }), _jsx(DetailRow, { label: "Cache ratio", value: formatPercent(cacheRatio) }), (() => {
251
+ const { label, value } = formatInputOutputRatio(totals, props.stats.providerId);
252
+ return _jsx(DetailRow, { label: label, value: value, padLength: Math.max(14, label.length + 1), noSlice: true });
253
+ })(), _jsx(DetailRow, { label: "Avg/event", value: `${formatOverviewTokenCount(averageTokensPerEvent)} tokens` }), _jsx(DetailRow, { label: "Cost/event", value: formatUnitUsd(costPerEvent) }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "Data source" }), _jsx(DetailRow, { label: "Files", value: formatInteger(summary.filesScanned) }), _jsx(DetailRow, { label: "Lines", value: formatInteger(summary.linesRead) }), _jsx(DetailRow, { label: "Path", value: summary.rootPath })] })] })] }));
251
254
  }
252
255
  function ContentPanel(props) {
253
256
  if (props.providerState.status === "loading") {
@@ -320,7 +323,6 @@ function buildTextTableLines(options) {
320
323
  if (options.rows.length === 0 && !options.totalRow) {
321
324
  return {
322
325
  bodyLines: [
323
- { key: `${options.lineKeyPrefix}-title`, text: options.title, bold: true },
324
326
  { key: `${options.lineKeyPrefix}-empty`, text: "No rows found.", color: "gray" }
325
327
  ]
326
328
  };
@@ -330,7 +332,6 @@ function buildTextTableLines(options) {
330
332
  : options.rows;
331
333
  const table = createTextTable(options.headers, tableRows.map((row) => row.cells));
332
334
  const headerLines = [
333
- { key: `${options.lineKeyPrefix}-title`, text: options.title, bold: true },
334
335
  {
335
336
  key: `${options.lineKeyPrefix}-top-border`,
336
337
  text: buildTableBorder(table, "┌", "┬", "┐"),
@@ -394,7 +395,6 @@ function buildLimitWindowTableLines(stats, selectedRowKey) {
394
395
  ? new Set([primaryRows[primaryRows.length - 1].key])
395
396
  : undefined;
396
397
  return buildTextTableLines({
397
- title: "Limits",
398
398
  lineKeyPrefix: "limits",
399
399
  headers: LIMIT_TABLE_HEADERS,
400
400
  rows: [...primaryRows, ...secondaryRows],
@@ -408,17 +408,19 @@ function buildLimitWindowTableRow(window) {
408
408
  cells: [
409
409
  window.scope,
410
410
  window.planType,
411
+ formatLimitWindowModels(window),
411
412
  formatCompactWindowMinutes(window.windowMinutes),
412
413
  formatUsedPercentRange(window.minUsedPercent, window.maxUsedPercent),
413
414
  formatCompactLocalDateTime(window.startTimeUtcIso),
414
415
  formatCompactLocalDateTime(window.endTimeUtcIso),
415
- formatUsd(window.totals.estimatedCredits * CODEX_CREDIT_COST_USD)
416
+ // Status-aware: shows "-" when the API-equivalent cost is unknown rather
417
+ // than a misleading $0.00.
418
+ formatUsageUsd(window.totals)
416
419
  ]
417
420
  };
418
421
  }
419
422
  function buildDailyUsageTableLines(rows, selectedDayKey) {
420
423
  return buildTextTableLines({
421
- title: "Daily usage",
422
424
  lineKeyPrefix: "daily",
423
425
  headers: DAILY_TABLE_HEADERS,
424
426
  rows: rows.map((row) => ({
@@ -428,8 +430,8 @@ function buildDailyUsageTableLines(rows, selectedDayKey) {
428
430
  formatCompactTokenCount(row.totals.eventCount),
429
431
  formatCompactTokenCount(row.totals.inputTokens),
430
432
  formatCompactTokenCount(row.totals.outputTokens),
431
- formatCompactCacheTokens(row.totals, row.totals.cacheReadInputTokens),
432
- formatCompactCacheTokens(row.totals, row.totals.cacheWriteInputTokens),
433
+ formatCompactCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens),
434
+ formatCompactCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens),
433
435
  formatUsageUsd(row.totals)
434
436
  ]
435
437
  })),
@@ -438,7 +440,6 @@ function buildDailyUsageTableLines(rows, selectedDayKey) {
438
440
  }
439
441
  function buildModelUsageTableLines(rows, totals, selectedModelId) {
440
442
  return buildTextTableLines({
441
- title: "Model usage",
442
443
  lineKeyPrefix: "model",
443
444
  headers: MODEL_TABLE_HEADERS,
444
445
  rows: rows.map((row) => ({
@@ -458,7 +459,7 @@ function SelectionDetailsPanel(props) {
458
459
  }
459
460
  if (props.tabId === "limit-windows" && props.selectedLimitRow) {
460
461
  const row = props.selectedLimitRow;
461
- return (_jsx(DetailsPanelFrame, { children: _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }) }));
462
+ return (_jsx(DetailsPanelFrame, { children: _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }) }));
462
463
  }
463
464
  if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
464
465
  const row = props.selectedDayRow;
@@ -470,10 +471,13 @@ function SelectionDetailsPanel(props) {
470
471
  return null;
471
472
  }
472
473
  function DetailsPanelFrame(props) {
473
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Details" }), props.children] }));
474
+ return (_jsxs(Box, { borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Details" }), props.children] }));
474
475
  }
475
476
  function DetailRow(props) {
476
- return (_jsxs(Text, { children: [pad(props.label, 14), props.value] }));
477
+ const labelText = props.noSlice
478
+ ? props.label.padEnd(props.padLength ?? 14)
479
+ : pad(props.label, props.padLength ?? 14);
480
+ return (_jsxs(Text, { children: [labelText, props.value] }));
477
481
  }
478
482
  function UsageTotalsDetails(props) {
479
483
  const { totals } = props;
@@ -495,8 +499,8 @@ function formatOverviewTokenCount(value) {
495
499
  }
496
500
  return formatInteger(roundedValue);
497
501
  }
498
- function formatCacheOverviewTokenCount(totals, value) {
499
- return totals.cacheStatus === "unavailable" ? "-" : formatOverviewTokenCount(value);
502
+ function formatCacheOverviewTokenCount(status, value) {
503
+ return status === "unavailable" ? "-" : formatOverviewTokenCount(value);
500
504
  }
501
505
  function formatFixedCompactNumber(value) {
502
506
  return value.toLocaleString("en-US", {
@@ -580,7 +584,7 @@ function formatPercent(value) {
580
584
  })}%`;
581
585
  }
582
586
  function resolveCacheRatio(totals) {
583
- if (totals.cacheStatus === "unavailable") {
587
+ if (totals.cacheReadStatus === "unavailable") {
584
588
  return NaN;
585
589
  }
586
590
  const inputPool = totals.inputTokens +
@@ -590,14 +594,32 @@ function resolveCacheRatio(totals) {
590
594
  ? (totals.cacheReadInputTokens / inputPool) * 100
591
595
  : 0;
592
596
  }
593
- function formatInputOutputRatio(totals) {
597
+ function formatInputOutputRatio(totals, providerId) {
594
598
  if (totals.outputTokens <= 0) {
595
- return "-";
599
+ return {
600
+ label: providerId === "claude" ? "Input/Write/Read/W5m/W1h/Output" : "Input/ReadCache/Output",
601
+ value: "-"
602
+ };
603
+ }
604
+ const fmt = (val) => {
605
+ const ratio = val / totals.outputTokens;
606
+ return ratio.toLocaleString("en-US", {
607
+ maximumFractionDigits: 1,
608
+ minimumFractionDigits: 0
609
+ });
610
+ };
611
+ if (providerId === "claude") {
612
+ return {
613
+ label: "Input/Write/Read/W5m/W1h/Output",
614
+ value: `${fmt(totals.inputTokens)} : ${fmt(totals.cacheWriteInputTokens)} : ${fmt(totals.cacheReadInputTokens)} : ${fmt(totals.cacheWrite5mInputTokens)} : ${fmt(totals.cacheWrite1hInputTokens)} : 1`
615
+ };
616
+ }
617
+ else {
618
+ return {
619
+ label: "Input/ReadCache/Output",
620
+ value: `${fmt(totals.inputTokens)} : ${fmt(totals.cacheReadInputTokens)} : 1`
621
+ };
596
622
  }
597
- return `${(totals.inputTokens / totals.outputTokens).toLocaleString("en-US", {
598
- maximumFractionDigits: 1,
599
- minimumFractionDigits: 0
600
- })} : 1`;
601
623
  }
602
624
  function formatUsedPercentRange(minUsedPercent, maxUsedPercent) {
603
625
  const fmt = (v) => `${Math.round(v)}%`;
@@ -619,6 +641,13 @@ function formatCompactWindowMinutes(value) {
619
641
  }
620
642
  return `${formatCompactNumber(hours)}h`;
621
643
  }
644
+ function formatLimitWindowModels(window) {
645
+ const label = window.modelType?.trim();
646
+ if (!label) {
647
+ return "All";
648
+ }
649
+ return label.charAt(0).toUpperCase() + label.slice(1);
650
+ }
622
651
  function formatLocalDateTime(value) {
623
652
  const parts = new Intl.DateTimeFormat("en-US", {
624
653
  month: "short",
@@ -683,23 +712,23 @@ function formatModelUsageTableCells(modelId, totals) {
683
712
  displayModelId,
684
713
  formatCompactTokenCount(totals.inputTokens),
685
714
  formatCompactTokenCount(totals.outputTokens),
686
- formatCompactCacheTokens(totals, totals.cacheReadInputTokens),
687
- formatCompactCacheTokens(totals, totals.cacheWriteInputTokens),
715
+ formatCompactCacheTokens(totals.cacheReadStatus, totals.cacheReadInputTokens),
716
+ formatCompactCacheTokens(totals.cacheWriteStatus, totals.cacheWriteInputTokens),
688
717
  formatUsageUsd(totals, modelId)
689
718
  ];
690
719
  }
691
720
  function UsageBreakdownLines(props) {
692
721
  const { totals } = props;
693
- 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)] })] }));
722
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input: ", formatInteger(totals.inputTokens), " output: ", formatInteger(totals.outputTokens), " cacheRead: ", formatCacheTokens(totals.cacheReadStatus, totals.cacheReadInputTokens)] }), _jsxs(Text, { children: ["cacheWrite: ", formatCacheTokens(totals.cacheWriteStatus, totals.cacheWriteInputTokens), " cacheW5m: ", formatOptionalTokens(totals.cacheWrite5mInputTokens), " cacheW1h: ", formatOptionalTokens(totals.cacheWrite1hInputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
694
723
  }
695
- function formatCacheTokens(totals, value) {
696
- if (totals.cacheStatus === "unavailable") {
724
+ function formatCacheTokens(status, value) {
725
+ if (status === "unavailable") {
697
726
  return "-";
698
727
  }
699
728
  return formatOptionalTokens(value);
700
729
  }
701
- function formatCompactCacheTokens(totals, value) {
702
- if (totals.cacheStatus === "unavailable") {
730
+ function formatCompactCacheTokens(status, value) {
731
+ if (status === "unavailable") {
703
732
  return "-";
704
733
  }
705
734
  return formatOptionalCompactTokens(value);
@@ -714,10 +743,14 @@ function formatInputPerOutput(totals) {
714
743
  if (totals.outputTokens <= 0) {
715
744
  return "input:cacheRead:cacheWrite:output = 0:0:0:0";
716
745
  }
717
- if (totals.cacheStatus === "unavailable") {
718
- return `input:cacheRead:cacheWrite:output = ${formatInteger(Math.round(totals.inputTokens / totals.outputTokens))}:-:-:1`;
719
- }
720
- 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`;
746
+ const inputRatio = formatInteger(Math.round(totals.inputTokens / totals.outputTokens));
747
+ const cacheReadRatio = totals.cacheReadStatus === "unavailable"
748
+ ? "-"
749
+ : formatInteger(Math.round(totals.cacheReadInputTokens / totals.outputTokens));
750
+ const cacheWriteRatio = totals.cacheWriteStatus === "unavailable"
751
+ ? "-"
752
+ : formatInteger(Math.round(totals.cacheWriteInputTokens / totals.outputTokens));
753
+ return `input:cacheRead:cacheWrite:output = ${inputRatio}:${cacheReadRatio}:${cacheWriteRatio}:1`;
721
754
  }
722
755
  function useMeasuredElementSize() {
723
756
  const ref = useRef(null);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Single source of truth for mapping Antigravity model identifiers to canonical
3
+ * model ids. Covers both the opaque RPC placeholders (MODEL_PLACEHOLDER_*) and
4
+ * the dated/suffixed aliases that show up in usage records.
5
+ */
6
+ const MODEL_IDS = {
7
+ MODEL_PLACEHOLDER_M20: "gemini-3.5-flash",
8
+ MODEL_PLACEHOLDER_M132: "gemini-3.5-flash",
9
+ MODEL_PLACEHOLDER_M187: "gemini-3.5-flash",
10
+ MODEL_PLACEHOLDER_M36: "gemini-3.1-pro",
11
+ MODEL_PLACEHOLDER_M16: "gemini-3.1-pro",
12
+ MODEL_PLACEHOLDER_M35: "claude-sonnet-4-6",
13
+ MODEL_PLACEHOLDER_M26: "claude-opus-4-6",
14
+ MODEL_OPENAI_GPT_OSS_120B_MEDIUM: "gpt-oss-120b",
15
+ "gemini-3-flash-a": "gemini-3-flash",
16
+ "gemini-3-flash-preview": "gemini-3-flash",
17
+ "gemini-3.1-pro-preview": "gemini-3.1-pro",
18
+ "gemini-3.5-flash-preview": "gemini-3.5-flash",
19
+ "claude-sonnet-4-6-20251201": "claude-sonnet-4-6",
20
+ "claude-opus-4-6-20251201": "claude-opus-4-6"
21
+ };
22
+ export function normalizeAntigravityModelId(modelId) {
23
+ return MODEL_IDS[modelId] ?? (modelId || "unknown");
24
+ }
25
+ export function antigravityModelScope(rawModelId) {
26
+ const modelId = normalizeAntigravityModelId(rawModelId);
27
+ if (modelId.startsWith("gemini")) {
28
+ return "gemini";
29
+ }
30
+ if (modelId.startsWith("claude") || modelId.startsWith("gpt")) {
31
+ return "third-party";
32
+ }
33
+ return "unknown";
34
+ }
35
+ export function modelScopeMatches(scope, modelId) {
36
+ return antigravityModelScope(modelId) === scope;
37
+ }
38
+ export function modelScopeLabel(scope) {
39
+ if (scope === "gemini") {
40
+ return "Gemini";
41
+ }
42
+ if (scope === "third-party") {
43
+ return "Claude/GPT";
44
+ }
45
+ return "Unknown";
46
+ }
@@ -0,0 +1,288 @@
1
+ import { createHash } from "node:crypto";
2
+ import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "../contract.js";
3
+ import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "../daily.js";
4
+ import { resolveUsageRate } from "../pricing.js";
5
+ import { modelScopeLabel, modelScopeMatches, normalizeAntigravityModelId } from "./models.js";
6
+ import { parseAntigravityQuotaEntries } from "./quota-parser.js";
7
+ import { findAntigravityLocalServer } from "./rpc/discovery.js";
8
+ import { extractQuotaGroups, fetchAntigravityUserStatus } from "./rpc/quota.js";
9
+ import { collectUsageFromLocalRpc } from "./usage-parse.js";
10
+ const RATE_CARD = {
11
+ "gemini-3.5-flash": {
12
+ input: 150,
13
+ cacheRead: 15,
14
+ cacheWrite: 150,
15
+ cacheWrite5m: 150,
16
+ cacheWrite1h: 150,
17
+ output: 900
18
+ },
19
+ "gemini-3.1-pro": {
20
+ input: 200,
21
+ cacheRead: 20,
22
+ cacheWrite: 200,
23
+ cacheWrite5m: 200,
24
+ cacheWrite1h: 200,
25
+ output: 1200,
26
+ longContext: {
27
+ thresholdTokens: 200000,
28
+ rate: {
29
+ input: 400,
30
+ cacheRead: 40,
31
+ cacheWrite: 400,
32
+ cacheWrite5m: 400,
33
+ cacheWrite1h: 400,
34
+ output: 1800
35
+ }
36
+ }
37
+ },
38
+ "gemini-3-flash": {
39
+ input: 50,
40
+ cacheRead: 5,
41
+ cacheWrite: 50,
42
+ cacheWrite5m: 50,
43
+ cacheWrite1h: 50,
44
+ output: 300
45
+ },
46
+ "claude-sonnet-4-6": {
47
+ input: 300,
48
+ cacheRead: 30,
49
+ cacheWrite: 375,
50
+ cacheWrite5m: 375,
51
+ cacheWrite1h: 600,
52
+ output: 1500
53
+ },
54
+ "claude-opus-4-6": {
55
+ input: 500,
56
+ cacheRead: 50,
57
+ cacheWrite: 625,
58
+ cacheWrite5m: 625,
59
+ cacheWrite1h: 1000,
60
+ output: 2500
61
+ }
62
+ };
63
+ const UNPRICED_MODELS = new Set([
64
+ "gpt-oss-120b"
65
+ ]);
66
+ export class AntigravityUsageProvider extends UsageProviderBase {
67
+ constructor(options = {}) {
68
+ super("antigravity", "Antigravity");
69
+ this.collectUsageOverride = options.collectUsage;
70
+ this.collectQuotaOverride = options.collectQuota;
71
+ this.findConnection = options.findConnection ?? findAntigravityLocalServer;
72
+ }
73
+ async getStats(options = {}) {
74
+ const warnings = [];
75
+ // Discover the local language server at most once per refresh and share the
76
+ // resulting connection (and its probe payload) between both collectors.
77
+ let connectionPromise;
78
+ const connect = () => (connectionPromise ?? (connectionPromise = this.findConnection()));
79
+ const [usageResult, quotaResult] = await Promise.allSettled([
80
+ this.collectUsageOverride
81
+ ? this.collectUsageOverride(options)
82
+ : collectUsageFromConnection(connect, options),
83
+ this.collectQuotaOverride
84
+ ? this.collectQuotaOverride()
85
+ : collectQuotaFromConnection(connect)
86
+ ]);
87
+ const records = usageResult.status === "fulfilled"
88
+ ? usageResult.value
89
+ : [];
90
+ const quotaSnapshot = quotaResult.status === "fulfilled"
91
+ ? quotaResult.value
92
+ : null;
93
+ if (usageResult.status === "rejected") {
94
+ warnings.push("Could not read Antigravity usage from the local RPC.");
95
+ }
96
+ if (quotaResult.status === "rejected") {
97
+ warnings.push("Live Antigravity quota is unavailable. Ensure the Antigravity IDE is running.");
98
+ }
99
+ else if (quotaResult.value.entries.length === 0) {
100
+ warnings.push("Antigravity local quota RPC responded, but no recognized model quota windows were found.");
101
+ }
102
+ const selectedRecords = deduplicateRecords(records);
103
+ const duplicateEvents = records.length - selectedRecords.length;
104
+ if (duplicateEvents > 0) {
105
+ warnings.push(`Collapsed ${duplicateEvents} duplicate Antigravity usage response(s).`);
106
+ }
107
+ const byModel = new Map();
108
+ const byDay = createDailyUsageAggregates();
109
+ for (const record of selectedRecords) {
110
+ const modelId = normalizeAntigravityModelId(record.modelId);
111
+ const totals = usageRecordToTotals(modelId, record);
112
+ addModelUsage(byModel, modelId, totals);
113
+ addDailyUsage(byDay, record.timestamp, modelId, undefined, totals);
114
+ }
115
+ const modelUsage = [...byModel.entries()]
116
+ .map(([modelId, totals]) => ({
117
+ modelId,
118
+ totals
119
+ }))
120
+ .sort((left, right) => right.totals.estimatedCredits -
121
+ left.totals.estimatedCredits);
122
+ const unknownPricedModels = modelUsage
123
+ .filter((row) => !rateForModel(row.modelId, rowInputTokens(row)) && !UNPRICED_MODELS.has(row.modelId))
124
+ .map((row) => row.modelId);
125
+ if (unknownPricedModels.length > 0) {
126
+ warnings.push(`No Antigravity estimated API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
127
+ }
128
+ const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, quotaSnapshot.planType, selectedRecords, quotaSnapshot.fetchedAt)) ?? [];
129
+ return {
130
+ providerId: this.id,
131
+ providerLabel: this.label,
132
+ summary: {
133
+ // The provider reads no files or lines; usage comes from the local RPC.
134
+ filesScanned: 0,
135
+ linesRead: 0,
136
+ tokenEvents: selectedRecords.length,
137
+ totals: sumUsageTotals(modelUsage.map((row) => row.totals)),
138
+ distinctModels: modelUsage.map((row) => row.modelId),
139
+ distinctPlanTypes: [
140
+ ...new Set(limitWindows.map((window) => window.planType))
141
+ ],
142
+ rootLabel: "Antigravity local RPC",
143
+ rootPath: "127.0.0.1"
144
+ },
145
+ modelUsage,
146
+ dayUsage: buildDailyUsageRows(byDay),
147
+ primaryLimitWindows: limitWindows.filter((window) => window.scope === "primary"),
148
+ secondaryLimitWindows: limitWindows.filter((window) => window.scope === "secondary"),
149
+ warnings,
150
+ analytics: quotaSnapshot?.userIdHash
151
+ ? {
152
+ agentName: this.label.replace(/\s/g, ""),
153
+ userIdHash: quotaSnapshot.userIdHash
154
+ }
155
+ : undefined
156
+ };
157
+ }
158
+ }
159
+ async function collectUsageFromConnection(connect, options) {
160
+ const connection = await connect();
161
+ return connection
162
+ ? collectUsageFromLocalRpc(connection.server, options)
163
+ : [];
164
+ }
165
+ async function collectQuotaFromConnection(connect) {
166
+ const connection = await connect();
167
+ if (!connection) {
168
+ throw new Error("Antigravity local language server was not found.");
169
+ }
170
+ const status = await fetchAntigravityUserStatus(connection.server);
171
+ return {
172
+ entries: parseAntigravityQuotaEntries(extractQuotaGroups(connection.quotaSummary)),
173
+ fetchedAt: Date.now(),
174
+ planType: status.planName ?? "unknown",
175
+ userIdHash: status.email
176
+ ? createHash("md5").update(status.email).digest("hex")
177
+ : null
178
+ };
179
+ }
180
+ function buildAntigravityLimitWindow(quota, planType, records, fetchedAt) {
181
+ const startAt = quota.resetAt - quota.windowMinutes * 60000;
182
+ const byModel = new Map();
183
+ const matchingTimestamps = [];
184
+ for (const record of records) {
185
+ const modelId = normalizeAntigravityModelId(record.modelId);
186
+ if (record.timestamp < startAt ||
187
+ record.timestamp >= quota.resetAt ||
188
+ !modelScopeMatches(quota.modelScope, modelId)) {
189
+ continue;
190
+ }
191
+ matchingTimestamps.push(record.timestamp);
192
+ addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record));
193
+ }
194
+ const modelUsage = [...byModel.entries()]
195
+ .map(([modelId, totals]) => ({
196
+ modelId,
197
+ totals
198
+ }))
199
+ .sort((left, right) => right.totals.estimatedCredits -
200
+ left.totals.estimatedCredits);
201
+ const totals = sumUsageTotals(modelUsage.map((row) => row.totals));
202
+ const usedPercent = clampPercent((1 - quota.remainingFraction) * 100);
203
+ // The first/last-seen range reflects the matched local usage events inside
204
+ // the window. With no matches, fall back to the window start and the fetch
205
+ // time. Quota percentage is authoritative from Antigravity RPC; token totals
206
+ // are reconstructed locally and may not match its internal accounting exactly.
207
+ const firstSeenMs = matchingTimestamps.length
208
+ ? Math.min(...matchingTimestamps)
209
+ : startAt;
210
+ const lastSeenMs = matchingTimestamps.length
211
+ ? Math.max(...matchingTimestamps)
212
+ : fetchedAt;
213
+ return {
214
+ scope: quota.scope,
215
+ planType,
216
+ limitId: quota.limitId,
217
+ modelType: modelScopeLabel(quota.modelScope),
218
+ windowMinutes: quota.windowMinutes,
219
+ startTimeUtcIso: new Date(startAt).toISOString(),
220
+ endTimeUtcIso: new Date(quota.resetAt).toISOString(),
221
+ firstSeenUtcIso: new Date(firstSeenMs).toISOString(),
222
+ lastSeenUtcIso: new Date(lastSeenMs).toISOString(),
223
+ minUsedPercent: usedPercent,
224
+ maxUsedPercent: usedPercent,
225
+ totals,
226
+ modelUsage,
227
+ eventCount: totals.eventCount
228
+ };
229
+ }
230
+ function clampPercent(value) {
231
+ if (!Number.isFinite(value)) {
232
+ return 0;
233
+ }
234
+ return Math.min(100, Math.max(0, value));
235
+ }
236
+ function deduplicateRecords(records) {
237
+ const byKey = new Map();
238
+ for (const record of records) {
239
+ byKey.set(`${record.sessionId}:${record.responseId}`, record);
240
+ }
241
+ return [...byKey.values()];
242
+ }
243
+ function usageRecordToTotals(modelId, record) {
244
+ return {
245
+ inputTokens: record.input,
246
+ outputTokens: record.output,
247
+ cacheReadInputTokens: record.cacheRead,
248
+ cacheWriteInputTokens: record.cacheWrite,
249
+ cacheWrite5mInputTokens: 0,
250
+ cacheWrite1hInputTokens: 0,
251
+ reasoningOutputTokens: Math.min(record.reasoning, record.output),
252
+ totalTokens: record.input +
253
+ record.cacheRead +
254
+ record.cacheWrite +
255
+ record.output,
256
+ estimatedCredits: creditsFor(modelId, record),
257
+ eventCount: 1,
258
+ // The local RPC reports cache reads but never cache writes, so cache reads
259
+ // are accurate while cache writes are genuinely unknown (not a confirmed
260
+ // zero) — surfaced as "-" everywhere, including the input/output ratio.
261
+ cacheReadStatus: "known",
262
+ cacheWriteStatus: "unavailable",
263
+ estimatedCreditsStatus: rateForModel(modelId, record.input)
264
+ ? "known"
265
+ : "unavailable"
266
+ };
267
+ }
268
+ function creditsFor(modelId, record) {
269
+ const rate = rateForModel(modelId, record.input);
270
+ if (!rate) {
271
+ return 0;
272
+ }
273
+ return ((record.input / 1000000) * rate.input +
274
+ (record.cacheRead / 1000000) * rate.cacheRead +
275
+ (record.cacheWrite / 1000000) * rate.cacheWrite +
276
+ (record.output / 1000000) * rate.output);
277
+ }
278
+ function rateForModel(modelId, inputTokens) {
279
+ return resolveUsageRate(RATE_CARD, modelId, inputTokens);
280
+ }
281
+ function rowInputTokens(row) {
282
+ return row.totals.inputTokens + row.totals.cacheReadInputTokens + row.totals.cacheWriteInputTokens;
283
+ }
284
+ function addModelUsage(byModel, modelId, deltaTotals) {
285
+ const totals = byModel.get(modelId) ?? createEmptyUsageTotals();
286
+ addUsageTotals(totals, deltaTotals);
287
+ byModel.set(modelId, totals);
288
+ }