letmecode 0.1.16 → 0.1.18

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.
@@ -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: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _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: "black", 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" })] }));
199
199
  }
200
200
  function CopilotActionsPanel(props) {
201
201
  if (props.providerState.provider.id !== "copilot") {
@@ -97,8 +97,8 @@ export class ClaudeUsageProvider extends UsageProviderBase {
97
97
  }
98
98
  }
99
99
  const selectedEvents = [
100
- ...parsedEvents.keyedEvents.values(),
101
- ...parsedEvents.unkeyedEvents.values()
100
+ ...new Set(parsedEvents.keyedEvents.values()),
101
+ ...parsedEvents.unkeyedEvents
102
102
  ];
103
103
  traceClaude(options.traceLogger, [
104
104
  `Transcript selection summary: filesWithMatches=${parseTotals.filesScanned}/${parsedSessionFiles.length}`,
@@ -128,7 +128,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
128
128
  warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage; merged same-key rows by per-field maxima to avoid double-counting cumulative snapshots.`);
129
129
  }
130
130
  if (options.verbose && parsedEvents.duplicateUnkeyedEvents > 0) {
131
- warnings.push(`Collapsed ${parsedEvents.duplicateUnkeyedEvents} duplicate unkeyed Claude usage event(s) by usage signature.`);
131
+ warnings.push(`Collapsed ${parsedEvents.duplicateUnkeyedEvents} adjacent duplicate unkeyed Claude usage event(s) by usage signature.`);
132
132
  }
133
133
  const modelUsage = [...byModel.entries()]
134
134
  .map(([modelId, totals]) => ({ modelId, totals }))
@@ -446,12 +446,14 @@ async function parseSessionFile(filePath, sessionsRoot) {
446
446
  const entrypoint = typeof payloadObject.entrypoint === "string" ? payloadObject.entrypoint : "";
447
447
  const rateLimits = extractRateLimits(payloadObject, message);
448
448
  const normalizedUsage = normalizeUsage(usage);
449
- const usageKey = buildUsageEventKey(payloadObject, message);
449
+ const usageKeys = buildUsageEventKeys(payloadObject, message);
450
450
  const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
451
451
  assistantEntryPoints.add(entrypoint);
452
452
  events.push({
453
453
  entrypoint,
454
- usageKey,
454
+ filePath,
455
+ lineNumber: linesRead,
456
+ usageKeys,
455
457
  usageSignature,
456
458
  timestampMs: eventTimeMs,
457
459
  modelId,
@@ -556,14 +558,14 @@ function extractStringArray(value) {
556
558
  }
557
559
  return value.filter((item) => typeof item === "string");
558
560
  }
559
- function buildUsageEventKey(payloadObject, message) {
561
+ function buildUsageEventKeys(payloadObject, message) {
560
562
  const sessionId = String(payloadObject.sessionId ?? "");
561
563
  const requestId = typeof payloadObject.requestId === "string" ? payloadObject.requestId : "";
562
564
  const messageId = typeof message?.id === "string" ? message.id : "";
563
- if (!requestId && !messageId) {
564
- return null;
565
- }
566
- return `${sessionId}|${requestId || messageId}`;
565
+ return [...new Set([
566
+ requestId ? `${sessionId}|request:${requestId}` : "",
567
+ messageId ? `${sessionId}|message:${messageId}` : ""
568
+ ].filter(Boolean))];
567
569
  }
568
570
  function buildUsageSignature(payloadObject, modelId, usage) {
569
571
  return buildUsageSignatureFromParts(String(payloadObject.sessionId ?? ""), modelId, usage);
@@ -584,44 +586,66 @@ function buildUsageSignatureFromParts(sessionId, modelId, usage) {
584
586
  function createParsedUsageEventAccumulator() {
585
587
  return {
586
588
  keyedEvents: new Map(),
587
- unkeyedEvents: new Map(),
589
+ unkeyedEvents: [],
590
+ lastUnkeyedEventsBySignature: new Map(),
588
591
  duplicateUsageKeys: 0,
589
592
  duplicateUsageKeyCollisions: 0,
590
593
  duplicateUnkeyedEvents: 0
591
594
  };
592
595
  }
593
596
  function recordParsedUsageEvent(parsedEvents, event) {
594
- if (event.usageKey) {
595
- const previous = parsedEvents.keyedEvents.get(event.usageKey);
596
- if (!previous) {
597
- parsedEvents.keyedEvents.set(event.usageKey, event);
597
+ if (event.usageKeys.length > 0) {
598
+ const previousMatches = [...new Set(event.usageKeys
599
+ .map((usageKey) => parsedEvents.keyedEvents.get(usageKey))
600
+ .filter((candidate) => Boolean(candidate)))];
601
+ if (previousMatches.length === 0) {
602
+ for (const usageKey of event.usageKeys) {
603
+ parsedEvents.keyedEvents.set(usageKey, event);
604
+ }
598
605
  return;
599
606
  }
600
607
  parsedEvents.duplicateUsageKeys += 1;
601
- if (previous.usageSignature !== event.usageSignature) {
608
+ const distinctUsageSignatures = new Set([
609
+ event.usageSignature,
610
+ ...previousMatches.map((candidate) => candidate.usageSignature)
611
+ ]);
612
+ if (distinctUsageSignatures.size > 1) {
602
613
  parsedEvents.duplicateUsageKeyCollisions += 1;
603
614
  }
604
- parsedEvents.keyedEvents.set(event.usageKey, mergeParsedUsageEvents(previous, event));
615
+ const mergedEvent = previousMatches.reduce(mergeParsedUsageEvents, event);
616
+ for (const usageKey of mergedEvent.usageKeys) {
617
+ parsedEvents.keyedEvents.set(usageKey, mergedEvent);
618
+ }
605
619
  return;
606
620
  }
607
- const previous = parsedEvents.unkeyedEvents.get(event.usageSignature);
608
- if (!previous) {
609
- parsedEvents.unkeyedEvents.set(event.usageSignature, event);
621
+ const previousRecord = parsedEvents.lastUnkeyedEventsBySignature.get(event.usageSignature);
622
+ if (!previousRecord || !canCollapseAdjacentUnkeyedUsageEvents(previousRecord.event, event)) {
623
+ parsedEvents.unkeyedEvents.push(event);
624
+ parsedEvents.lastUnkeyedEventsBySignature.set(event.usageSignature, {
625
+ event,
626
+ index: parsedEvents.unkeyedEvents.length - 1
627
+ });
610
628
  return;
611
629
  }
612
630
  parsedEvents.duplicateUnkeyedEvents += 1;
613
- if (normalizeTimestamp(event.timestampMs) > normalizeTimestamp(previous.timestampMs)) {
614
- parsedEvents.unkeyedEvents.set(event.usageSignature, event);
631
+ if (normalizeTimestamp(event.timestampMs) > normalizeTimestamp(previousRecord.event.timestampMs)) {
632
+ parsedEvents.unkeyedEvents[previousRecord.index] = event;
633
+ parsedEvents.lastUnkeyedEventsBySignature.set(event.usageSignature, {
634
+ event,
635
+ index: previousRecord.index
636
+ });
615
637
  }
616
638
  }
617
639
  function mergeParsedUsageEvents(previous, next) {
618
640
  const mergedUsage = mergeClaudeUsage(previous.usage, next.usage);
619
641
  const modelId = selectMergedEventModelId(previous, next);
620
642
  const latestEvent = normalizeTimestamp(next.timestampMs) >= normalizeTimestamp(previous.timestampMs) ? next : previous;
621
- const sessionId = extractUsageKeySessionId(previous.usageKey) || extractUsageKeySessionId(next.usageKey);
643
+ const sessionId = extractUsageKeySessionId(previous.usageKeys) || extractUsageKeySessionId(next.usageKeys);
622
644
  return {
623
645
  entrypoint: latestEvent.entrypoint || previous.entrypoint || next.entrypoint,
624
- usageKey: previous.usageKey ?? next.usageKey,
646
+ filePath: latestEvent.filePath,
647
+ lineNumber: latestEvent.lineNumber,
648
+ usageKeys: [...new Set([...previous.usageKeys, ...next.usageKeys])],
625
649
  usageSignature: buildUsageSignatureFromParts(sessionId, modelId, mergedUsage),
626
650
  timestampMs: Math.max(normalizeTimestamp(previous.timestampMs), normalizeTimestamp(next.timestampMs)),
627
651
  modelId,
@@ -655,7 +679,11 @@ function selectMergedEventModelId(previous, next) {
655
679
  ? next.modelId
656
680
  : previous.modelId;
657
681
  }
658
- function extractUsageKeySessionId(usageKey) {
682
+ function canCollapseAdjacentUnkeyedUsageEvents(previous, next) {
683
+ return previous.filePath === next.filePath && next.lineNumber === previous.lineNumber + 1;
684
+ }
685
+ function extractUsageKeySessionId(usageKeys) {
686
+ const usageKey = usageKeys[0];
659
687
  if (!usageKey) {
660
688
  return "";
661
689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",