codex-devtools 0.2.0 → 0.2.1

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.
@@ -369,16 +369,32 @@ function diffTokenUsage(previous, current) {
369
369
  reasoning_output_tokens: current.reasoning_output_tokens - previous.reasoning_output_tokens,
370
370
  total_tokens: current.total_tokens - previous.total_tokens
371
371
  };
372
- const hasNegative = delta.input_tokens < 0 || delta.cached_input_tokens < 0 || delta.output_tokens < 0 || delta.reasoning_output_tokens < 0 || delta.total_tokens < 0;
373
- if (hasNegative) {
374
- return null;
375
- }
376
- const isDuplicate = delta.input_tokens === 0 && delta.cached_input_tokens === 0 && delta.output_tokens === 0 && delta.reasoning_output_tokens === 0 && delta.total_tokens === 0;
377
- return isDuplicate ? null : delta;
372
+ const normalized = {
373
+ input_tokens: Math.max(delta.input_tokens, 0),
374
+ cached_input_tokens: Math.max(delta.cached_input_tokens, 0),
375
+ output_tokens: Math.max(delta.output_tokens, 0),
376
+ reasoning_output_tokens: Math.max(delta.reasoning_output_tokens, 0),
377
+ total_tokens: Math.max(delta.total_tokens, 0)
378
+ };
379
+ const hasAnyPositive = normalized.input_tokens > 0 || normalized.cached_input_tokens > 0 || normalized.output_tokens > 0 || normalized.reasoning_output_tokens > 0 || normalized.total_tokens > 0;
380
+ return hasAnyPositive ? normalized : null;
378
381
  }
379
382
  function isSameTokenUsage(left, right) {
380
383
  return left.input_tokens === right.input_tokens && left.cached_input_tokens === right.cached_input_tokens && left.output_tokens === right.output_tokens && left.reasoning_output_tokens === right.reasoning_output_tokens && left.total_tokens === right.total_tokens;
381
384
  }
385
+ function resolveTokenUsage(previousTotalUsage, currentTotalUsage, fallbackUsage) {
386
+ if (!previousTotalUsage) {
387
+ return fallbackUsage;
388
+ }
389
+ const delta = diffTokenUsage(previousTotalUsage, currentTotalUsage);
390
+ if (delta) {
391
+ return delta;
392
+ }
393
+ if (isSameTokenUsage(previousTotalUsage, currentTotalUsage)) {
394
+ return null;
395
+ }
396
+ return fallbackUsage;
397
+ }
382
398
  const AGENTS_HEADING_PATTERN = /^#?\s*AGENTS\.md instructions\b/i;
383
399
  const AGENTS_INSTRUCTIONS_BLOCK_PATTERN = /<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>/i;
384
400
  const ENVIRONMENT_CONTEXT_WRAPPER_PATTERN = /^<environment_context>[\s\S]*<\/environment_context>$/i;
@@ -1022,6 +1038,7 @@ class CodexChunkBuilder {
1022
1038
  let pendingUser = null;
1023
1039
  let lastSeenModelUsage = null;
1024
1040
  let lastSeenCollaborationMode = "";
1041
+ let previousTotalUsage = null;
1025
1042
  const flushAIChunk = () => {
1026
1043
  if (!currentAI) {
1027
1044
  return;
@@ -1276,30 +1293,33 @@ class CodexChunkBuilder {
1276
1293
  addReasoningSection(ai, "event", [entry.payload.text]);
1277
1294
  continue;
1278
1295
  }
1279
- if (isEventMsgEntry(entry) && isTokenCountPayload(entry.payload)) {
1280
- this.accumulateMetricsFromTokenEvent(ai.metrics, entry);
1281
- this.assignTokenUsageToPendingTool(ai, entry);
1296
+ if (isEventMsgEntry(entry) && isTokenCountPayload(entry.payload) && entry.payload.info) {
1297
+ const currentTotalUsage = entry.payload.info.total_token_usage;
1298
+ const usage = resolveTokenUsage(
1299
+ previousTotalUsage,
1300
+ currentTotalUsage,
1301
+ entry.payload.info.last_token_usage
1302
+ );
1303
+ previousTotalUsage = currentTotalUsage;
1304
+ if (!usage) {
1305
+ continue;
1306
+ }
1307
+ this.accumulateMetricsFromTokenUsage(ai.metrics, usage);
1308
+ this.assignTokenUsageToPendingTool(ai, usage);
1282
1309
  }
1283
1310
  }
1284
1311
  flushPendingEventUser();
1285
1312
  flushAIChunk();
1286
1313
  return chunks;
1287
1314
  }
1288
- accumulateMetricsFromTokenEvent(target, entry) {
1289
- if (!isTokenCountPayload(entry.payload) || !entry.payload.info) {
1290
- return;
1291
- }
1292
- const usage = entry.payload.info.last_token_usage;
1315
+ accumulateMetricsFromTokenUsage(target, usage) {
1293
1316
  target.inputTokens = (target.inputTokens ?? 0) + usage.input_tokens;
1294
1317
  target.cachedTokens = (target.cachedTokens ?? 0) + usage.cached_input_tokens;
1295
1318
  target.outputTokens = (target.outputTokens ?? 0) + usage.output_tokens;
1296
1319
  target.reasoningTokens = (target.reasoningTokens ?? 0) + usage.reasoning_output_tokens;
1297
1320
  target.totalTokens = (target.totalTokens ?? 0) + usage.total_tokens;
1298
1321
  }
1299
- assignTokenUsageToPendingTool(ai, entry) {
1300
- if (!isTokenCountPayload(entry.payload) || !entry.payload.info) {
1301
- return;
1302
- }
1322
+ assignTokenUsageToPendingTool(ai, usage) {
1303
1323
  if (ai.pendingUsageToolIndex === null) {
1304
1324
  return;
1305
1325
  }
@@ -1308,13 +1328,14 @@ class CodexChunkBuilder {
1308
1328
  ai.pendingUsageToolIndex = null;
1309
1329
  return;
1310
1330
  }
1311
- const usage = entry.payload.info.last_token_usage;
1312
1331
  if (tool.tokenUsage) {
1313
1332
  tool.tokenUsage.inputTokens += usage.input_tokens;
1333
+ tool.tokenUsage.cachedInputTokens += usage.cached_input_tokens;
1314
1334
  tool.tokenUsage.outputTokens += usage.output_tokens;
1315
1335
  } else {
1316
1336
  tool.tokenUsage = {
1317
1337
  inputTokens: usage.input_tokens,
1338
+ cachedInputTokens: usage.cached_input_tokens,
1318
1339
  outputTokens: usage.output_tokens
1319
1340
  };
1320
1341
  }
@@ -1478,21 +1499,11 @@ function buildSessionStatsRecord(parsed, revision, nowIso) {
1478
1499
  }
1479
1500
  const timestamp = parseTimestamp(entry.timestamp);
1480
1501
  const currentTotal = entry.payload.info.total_token_usage;
1481
- let usage;
1482
- if (previousTotalUsage) {
1483
- const delta = diffTokenUsage(previousTotalUsage, currentTotal);
1484
- if (delta) {
1485
- usage = delta;
1486
- } else if (isSameTokenUsage(previousTotalUsage, currentTotal)) {
1487
- previousTotalUsage = currentTotal;
1488
- continue;
1489
- } else {
1490
- usage = entry.payload.info.last_token_usage;
1491
- }
1492
- } else {
1493
- usage = currentTotal;
1494
- }
1502
+ const usage = resolveTokenUsage(previousTotalUsage, currentTotal, entry.payload.info.last_token_usage);
1495
1503
  previousTotalUsage = currentTotal;
1504
+ if (!usage) {
1505
+ continue;
1506
+ }
1496
1507
  const usageTotals = tokenUsageToTotals(usage);
1497
1508
  addTokenTotals(tokens, usageTotals);
1498
1509
  const modelTotals = ensureModelBucket(modelBuckets, currentModel, currentReasoningEffort);
@@ -1518,7 +1529,7 @@ function buildSessionStatsRecord(parsed, revision, nowIso) {
1518
1529
  }
1519
1530
  const lastActivity = parsed.entries[parsed.entries.length - 1]?.timestamp ?? parsed.session.startTime;
1520
1531
  return {
1521
- tokenComputationVersion: 2,
1532
+ tokenComputationVersion: 4,
1522
1533
  sessionId: parsed.session.id,
1523
1534
  filePath: parsed.session.filePath,
1524
1535
  revision,
@@ -2030,21 +2041,11 @@ class CodexSessionParser {
2030
2041
  eventMessages.push(entry);
2031
2042
  if (isTokenCountPayload(entry.payload) && entry.payload.info) {
2032
2043
  const currentTotal = entry.payload.info.total_token_usage;
2033
- let usage;
2034
- if (previousTotalUsage) {
2035
- const delta = diffTokenUsage(previousTotalUsage, currentTotal);
2036
- if (delta) {
2037
- usage = delta;
2038
- } else if (isSameTokenUsage(previousTotalUsage, currentTotal)) {
2039
- previousTotalUsage = currentTotal;
2040
- continue;
2041
- } else {
2042
- usage = entry.payload.info.last_token_usage;
2043
- }
2044
- } else {
2045
- usage = currentTotal;
2046
- }
2044
+ const usage = resolveTokenUsage(previousTotalUsage, currentTotal, entry.payload.info.last_token_usage);
2047
2045
  previousTotalUsage = currentTotal;
2046
+ if (!usage) {
2047
+ continue;
2048
+ }
2048
2049
  metrics.inputTokens += usage.input_tokens;
2049
2050
  metrics.cachedTokens += usage.cached_input_tokens;
2050
2051
  metrics.outputTokens += usage.output_tokens;
@@ -2383,12 +2384,12 @@ class StatsSnapshotStore {
2383
2384
  }
2384
2385
  }
2385
2386
  }
2386
- const DETAIL_CACHE_PREFIX = "detail-v2";
2387
- const CHUNKS_CACHE_PREFIX = "chunks-v2";
2387
+ const DETAIL_CACHE_PREFIX = "detail-v3";
2388
+ const CHUNKS_CACHE_PREFIX = "chunks-v4";
2388
2389
  const SESSIONS_CACHE_PREFIX = "sessions";
2389
- const STATS_CACHE_PREFIX = "stats-v1";
2390
+ const STATS_CACHE_PREFIX = "stats-v3";
2390
2391
  const UNKNOWN_REVISION = "unknown-revision";
2391
- const TOKEN_COMPUTATION_VERSION = 2;
2392
+ const TOKEN_COMPUTATION_VERSION = 4;
2392
2393
  function hasCompactionSignals(entries) {
2393
2394
  return entries.some(
2394
2395
  (entry) => isCompactedEntry(entry) || isCompactionEntry(entry) || isEventMsgEntry(entry) && isContextCompactedPayload(entry.payload)
@@ -2,7 +2,7 @@
2
2
  const electron = require("electron");
3
3
  const fs = require("node:fs");
4
4
  const path = require("node:path");
5
- const CodexServiceContext = require("./chunks/CodexServiceContext-BYe2UXME.cjs");
5
+ const CodexServiceContext = require("./chunks/CodexServiceContext-DaxLI918.cjs");
6
6
  require("node:events");
7
7
  require("node:os");
8
8
  require("node:crypto");
@@ -5,7 +5,7 @@ const fastifyStatic = require("@fastify/static");
5
5
  const Fastify = require("fastify");
6
6
  const path = require("node:path");
7
7
  const node_url = require("node:url");
8
- const CodexServiceContext = require("./chunks/CodexServiceContext-BYe2UXME.cjs");
8
+ const CodexServiceContext = require("./chunks/CodexServiceContext-DaxLI918.cjs");
9
9
  const fs = require("node:fs");
10
10
  require("node:events");
11
11
  require("node:os");
@@ -9423,27 +9423,124 @@ const DashboardView = () => {
9423
9423
  ] })
9424
9424
  ] });
9425
9425
  };
9426
- function formatDuration$1(durationMs) {
9427
- if (!Number.isFinite(durationMs) || durationMs <= 0) {
9428
- return "0m";
9429
- }
9430
- const totalMinutes = Math.round(durationMs / 6e4);
9431
- const hours = Math.floor(totalMinutes / 60);
9432
- const minutes = totalMinutes % 60;
9433
- if (hours <= 0) {
9434
- return `${minutes}m`;
9435
- }
9436
- if (minutes === 0) {
9437
- return `${hours}h`;
9438
- }
9439
- return `${hours}h ${minutes}m`;
9440
- }
9426
+ const CONTRIBUTION_WEEKDAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""];
9427
+ const CALENDAR_METRIC_OPTIONS = [
9428
+ { value: "totalTokens", label: "Total tokens", tooltipLabel: "total tokens" },
9429
+ { value: "inputTokens", label: "Input tokens", tooltipLabel: "input tokens" },
9430
+ { value: "outputTokens", label: "Output tokens", tooltipLabel: "output tokens" },
9431
+ { value: "eventCount", label: "Events", tooltipLabel: "events" },
9432
+ { value: "sessionCount", label: "Sessions", tooltipLabel: "sessions" }
9433
+ ];
9441
9434
  function normalizeScope(scope) {
9442
9435
  if (scope.type === "project" && scope.cwd.trim().length > 0) {
9443
9436
  return { type: "project", cwd: scope.cwd.trim() };
9444
9437
  }
9445
9438
  return { type: "all" };
9446
9439
  }
9440
+ function parseDateKey(value) {
9441
+ return /* @__PURE__ */ new Date(`${value}T00:00:00`);
9442
+ }
9443
+ function formatDateKey(date) {
9444
+ const year = date.getFullYear();
9445
+ const month = String(date.getMonth() + 1).padStart(2, "0");
9446
+ const day = String(date.getDate()).padStart(2, "0");
9447
+ return `${year}-${month}-${day}`;
9448
+ }
9449
+ function getContributionLevel(outputTokens, maxOutputTokens) {
9450
+ if (!Number.isFinite(outputTokens) || outputTokens <= 0 || maxOutputTokens <= 0) {
9451
+ return 0;
9452
+ }
9453
+ const normalized = Math.log(outputTokens + 1) / Math.log(maxOutputTokens + 1);
9454
+ if (normalized < 0.25) {
9455
+ return 1;
9456
+ }
9457
+ if (normalized < 0.5) {
9458
+ return 2;
9459
+ }
9460
+ if (normalized < 0.75) {
9461
+ return 3;
9462
+ }
9463
+ return 4;
9464
+ }
9465
+ function getDailyMetricValue(point, metric) {
9466
+ if (!point) {
9467
+ return 0;
9468
+ }
9469
+ switch (metric) {
9470
+ case "totalTokens":
9471
+ return point.totalTokens;
9472
+ case "inputTokens":
9473
+ return point.inputTokens;
9474
+ case "outputTokens":
9475
+ return point.outputTokens;
9476
+ case "eventCount":
9477
+ return point.eventCount;
9478
+ case "sessionCount":
9479
+ return point.sessionCount;
9480
+ default:
9481
+ return 0;
9482
+ }
9483
+ }
9484
+ function buildDailyContributionGrid(daily, metric) {
9485
+ if (daily.length === 0) {
9486
+ return {
9487
+ weeks: [],
9488
+ months: []
9489
+ };
9490
+ }
9491
+ const sorted = [...daily].sort((left, right) => left.date.localeCompare(right.date));
9492
+ const valuesByDate = new Map(sorted.map((point) => [point.date, point]));
9493
+ const maxMetricValue = Math.max(...sorted.map((point) => getDailyMetricValue(point, metric)), 0);
9494
+ const firstDate = parseDateKey(sorted[0].date);
9495
+ const lastDate = parseDateKey(sorted[sorted.length - 1].date);
9496
+ const paddedStart = new Date(firstDate);
9497
+ paddedStart.setDate(paddedStart.getDate() - paddedStart.getDay());
9498
+ const paddedEnd = new Date(lastDate);
9499
+ paddedEnd.setDate(paddedEnd.getDate() + (6 - paddedEnd.getDay()));
9500
+ const weeks = [];
9501
+ const cursor = new Date(paddedStart);
9502
+ while (cursor <= paddedEnd) {
9503
+ const week = [];
9504
+ for (let offset = 0; offset < 7; offset += 1) {
9505
+ const date = formatDateKey(cursor);
9506
+ const point = valuesByDate.get(date);
9507
+ const inRange = cursor >= firstDate && cursor <= lastDate;
9508
+ const metricValue = getDailyMetricValue(point, metric);
9509
+ week.push({
9510
+ date,
9511
+ metricValue,
9512
+ level: getContributionLevel(metricValue, maxMetricValue),
9513
+ inRange
9514
+ });
9515
+ cursor.setDate(cursor.getDate() + 1);
9516
+ }
9517
+ weeks.push(week);
9518
+ }
9519
+ const months = [];
9520
+ let previousMonthKey = "";
9521
+ weeks.forEach((week, weekIndex) => {
9522
+ const monthStartCell = week.find((cell) => cell.inRange && cell.date.endsWith("-01"));
9523
+ const anchor = monthStartCell != null ? monthStartCell : week.find((cell) => cell.inRange);
9524
+ if (!anchor) {
9525
+ return;
9526
+ }
9527
+ const monthDate = parseDateKey(anchor.date);
9528
+ const monthKey = `${monthDate.getFullYear()}-${String(monthDate.getMonth() + 1).padStart(2, "0")}`;
9529
+ if (monthKey === previousMonthKey) {
9530
+ return;
9531
+ }
9532
+ months.push({
9533
+ key: monthKey,
9534
+ label: monthDate.toLocaleString(void 0, { month: "short" }),
9535
+ weekIndex
9536
+ });
9537
+ previousMonthKey = monthKey;
9538
+ });
9539
+ return {
9540
+ weeks,
9541
+ months
9542
+ };
9543
+ }
9447
9544
  const StatsView = () => {
9448
9545
  const {
9449
9546
  projects,
@@ -9462,6 +9559,8 @@ const StatsView = () => {
9462
9559
  fetchStats: state.fetchStats,
9463
9560
  setStatsScope: state.setStatsScope
9464
9561
  }));
9562
+ const [calendarMetric, setCalendarMetric] = reactExports.useState("outputTokens");
9563
+ const [contributionTooltip, setContributionTooltip] = reactExports.useState(null);
9465
9564
  reactExports.useEffect(() => {
9466
9565
  if (!statsData && !statsLoading) {
9467
9566
  void fetchStats(statsScope);
@@ -9470,7 +9569,7 @@ const StatsView = () => {
9470
9569
  const dailyMaxOutput = reactExports.useMemo(
9471
9570
  () => {
9472
9571
  var _a;
9473
- return Math.max(...(_a = statsData == null ? void 0 : statsData.daily.map((point) => point.outputTokens)) != null ? _a : [1]);
9572
+ return Math.max(...(_a = statsData == null ? void 0 : statsData.daily.map((point) => point.outputTokens)) != null ? _a : [0]);
9474
9573
  },
9475
9574
  [statsData]
9476
9575
  );
@@ -9481,6 +9580,30 @@ const StatsView = () => {
9481
9580
  },
9482
9581
  [statsData]
9483
9582
  );
9583
+ const dailyContributionGrid = reactExports.useMemo(
9584
+ () => {
9585
+ var _a;
9586
+ return buildDailyContributionGrid((_a = statsData == null ? void 0 : statsData.daily) != null ? _a : [], calendarMetric);
9587
+ },
9588
+ [statsData, calendarMetric]
9589
+ );
9590
+ const selectedCalendarMetricOption = reactExports.useMemo(
9591
+ () => {
9592
+ var _a;
9593
+ return (_a = CALENDAR_METRIC_OPTIONS.find((option) => option.value === calendarMetric)) != null ? _a : CALENDAR_METRIC_OPTIONS[2];
9594
+ },
9595
+ [calendarMetric]
9596
+ );
9597
+ reactExports.useEffect(() => {
9598
+ setContributionTooltip(null);
9599
+ }, [calendarMetric, statsData]);
9600
+ const uncachedInputTokens = reactExports.useMemo(
9601
+ () => {
9602
+ var _a, _b;
9603
+ return Math.max(((_a = statsData == null ? void 0 : statsData.totals.inputTokens) != null ? _a : 0) - ((_b = statsData == null ? void 0 : statsData.totals.cachedTokens) != null ? _b : 0), 0);
9604
+ },
9605
+ [statsData]
9606
+ );
9484
9607
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "stats-shell", children: [
9485
9608
  /* @__PURE__ */ jsxRuntimeExports.jsxs("header", { className: "stats-header", children: [
9486
9609
  /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Stats" }),
@@ -9550,20 +9673,20 @@ const StatsView = () => {
9550
9673
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "stat-label", children: "Total tokens" }),
9551
9674
  /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "stat-value", children: statsData.totals.totalTokens.toLocaleString() })
9552
9675
  ] }),
9676
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
9677
+ "article",
9678
+ {
9679
+ className: "stat-card",
9680
+ title: `Cached: ${statsData.totals.cachedTokens.toLocaleString()} | Uncached: ${uncachedInputTokens.toLocaleString()}`,
9681
+ children: [
9682
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "stat-label", children: "In" }),
9683
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "stat-value", children: statsData.totals.inputTokens.toLocaleString() })
9684
+ ]
9685
+ }
9686
+ ),
9553
9687
  /* @__PURE__ */ jsxRuntimeExports.jsxs("article", { className: "stat-card", children: [
9554
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "stat-label", children: "Generated tokens" }),
9688
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "stat-label", children: "Out" }),
9555
9689
  /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "stat-value", children: statsData.totals.outputTokens.toLocaleString() })
9556
- ] }),
9557
- /* @__PURE__ */ jsxRuntimeExports.jsxs("article", { className: "stat-card", children: [
9558
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "stat-label", children: "Active time" }),
9559
- /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "stat-value", children: formatDuration$1(statsData.totals.durationMs) })
9560
- ] }),
9561
- /* @__PURE__ */ jsxRuntimeExports.jsxs("article", { className: "stat-card", children: [
9562
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "stat-label", children: "Sessions" }),
9563
- /* @__PURE__ */ jsxRuntimeExports.jsxs("strong", { className: "stat-value", children: [
9564
- statsData.totals.sessions.toLocaleString(),
9565
- statsData.totals.archivedSessions > 0 ? ` (${statsData.totals.archivedSessions.toLocaleString()} archived)` : ""
9566
- ] })
9567
9690
  ] })
9568
9691
  ] }),
9569
9692
  /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "stats-section", children: [
@@ -9572,7 +9695,8 @@ const StatsView = () => {
9572
9695
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "stats-section-subtle", children: "Output tokens" })
9573
9696
  ] }),
9574
9697
  statsData.daily.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "empty-copy", children: "No daily token data yet." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "stats-day-list", children: statsData.daily.map((point) => {
9575
- const widthPercent = dailyMaxOutput > 0 ? Math.max(6, point.outputTokens / dailyMaxOutput * 100) : 6;
9698
+ const rawWidthPercent = dailyMaxOutput > 0 ? point.outputTokens / dailyMaxOutput * 100 : 0;
9699
+ const widthPercent = Number.isFinite(rawWidthPercent) ? Math.max(0, Math.min(100, rawWidthPercent)) : 0;
9576
9700
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "stats-day-row", children: [
9577
9701
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "stats-day-label", children: point.date }),
9578
9702
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "stats-day-bar-wrap", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "stats-day-bar", style: { width: `${widthPercent}%` } }) }),
@@ -9580,6 +9704,117 @@ const StatsView = () => {
9580
9704
  ] }, point.date);
9581
9705
  }) })
9582
9706
  ] }),
9707
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "stats-section", children: [
9708
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "stats-section-header", children: [
9709
+ /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { children: "Output calendar" }),
9710
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "stats-section-header-actions", children: [
9711
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "sidebar-label", htmlFor: "stats-calendar-metric", children: "Type" }),
9712
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
9713
+ "select",
9714
+ {
9715
+ id: "stats-calendar-metric",
9716
+ className: "app-select stats-contrib-select",
9717
+ value: calendarMetric,
9718
+ onChange: (event) => {
9719
+ setCalendarMetric(event.target.value);
9720
+ },
9721
+ children: CALENDAR_METRIC_OPTIONS.map((option) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value))
9722
+ }
9723
+ )
9724
+ ] })
9725
+ ] }),
9726
+ dailyContributionGrid.weeks.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "empty-copy", children: "No daily token data yet." }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "stats-contrib-frame", children: [
9727
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "stats-contrib-weekdays", children: CONTRIBUTION_WEEKDAY_LABELS.map((label, index) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: label }, `contrib-weekday-${index}`)) }),
9728
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "stats-contrib-scroll", children: [
9729
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
9730
+ "div",
9731
+ {
9732
+ className: "stats-contrib-months",
9733
+ style: { gridTemplateColumns: `repeat(${dailyContributionGrid.weeks.length}, 12px)` },
9734
+ children: dailyContributionGrid.months.map((month) => /* @__PURE__ */ jsxRuntimeExports.jsx(
9735
+ "span",
9736
+ {
9737
+ className: "stats-contrib-month",
9738
+ style: { gridColumn: String(month.weekIndex + 1) },
9739
+ children: month.label
9740
+ },
9741
+ month.key
9742
+ ))
9743
+ }
9744
+ ),
9745
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
9746
+ "div",
9747
+ {
9748
+ className: "stats-contrib-weeks",
9749
+ role: "img",
9750
+ "aria-label": "Daily output tokens calendar heatmap",
9751
+ children: dailyContributionGrid.weeks.map((week, weekIndex) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "stats-contrib-week", children: week.map((cell) => {
9752
+ const tooltipText = `${cell.date} • ${cell.metricValue.toLocaleString()} ${selectedCalendarMetricOption.tooltipLabel}`;
9753
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
9754
+ "div",
9755
+ {
9756
+ className: `stats-contrib-cell level-${cell.level}${!cell.inRange ? " is-padding" : ""}${cell.inRange && cell.level === 0 ? " is-empty" : ""}`,
9757
+ title: cell.inRange ? tooltipText : void 0,
9758
+ "aria-label": cell.inRange ? tooltipText : void 0,
9759
+ tabIndex: cell.inRange ? 0 : -1,
9760
+ onMouseEnter: (event) => {
9761
+ if (!cell.inRange) {
9762
+ return;
9763
+ }
9764
+ setContributionTooltip({
9765
+ text: tooltipText,
9766
+ x: event.clientX,
9767
+ y: event.clientY
9768
+ });
9769
+ },
9770
+ onMouseMove: (event) => {
9771
+ if (!cell.inRange) {
9772
+ return;
9773
+ }
9774
+ setContributionTooltip({
9775
+ text: tooltipText,
9776
+ x: event.clientX,
9777
+ y: event.clientY
9778
+ });
9779
+ },
9780
+ onMouseLeave: () => {
9781
+ setContributionTooltip(null);
9782
+ },
9783
+ onFocus: (event) => {
9784
+ if (!cell.inRange) {
9785
+ return;
9786
+ }
9787
+ const rect = event.currentTarget.getBoundingClientRect();
9788
+ setContributionTooltip({
9789
+ text: tooltipText,
9790
+ x: rect.left + rect.width / 2,
9791
+ y: rect.bottom + 4
9792
+ });
9793
+ },
9794
+ onBlur: () => {
9795
+ setContributionTooltip(null);
9796
+ }
9797
+ },
9798
+ cell.date
9799
+ );
9800
+ }) }, `week-${weekIndex}`))
9801
+ }
9802
+ )
9803
+ ] })
9804
+ ] }),
9805
+ contributionTooltip ? /* @__PURE__ */ jsxRuntimeExports.jsx(
9806
+ "div",
9807
+ {
9808
+ className: "stats-contrib-tooltip",
9809
+ role: "tooltip",
9810
+ style: {
9811
+ left: `${contributionTooltip.x + 12}px`,
9812
+ top: `${contributionTooltip.y + 12}px`
9813
+ },
9814
+ children: contributionTooltip.text
9815
+ }
9816
+ ) : null
9817
+ ] }),
9583
9818
  /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "stats-grid-2", children: [
9584
9819
  /* @__PURE__ */ jsxRuntimeExports.jsxs("article", { className: "stats-section", children: [
9585
9820
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "stats-section-header", children: [
@@ -11149,12 +11384,15 @@ function parseCommandPreview(execution) {
11149
11384
  return null;
11150
11385
  }
11151
11386
  const ExecutionTrace = ({ execution }) => {
11152
- var _a, _b, _c;
11387
+ var _a, _b, _c, _d, _e;
11153
11388
  const [expanded, setExpanded] = reactExports.useState(false);
11154
11389
  const output = (_b = (_a = execution.functionOutput) == null ? void 0 : _a.output) != null ? _b : "";
11155
11390
  const commandPreview = reactExports.useMemo(() => parseCommandPreview(execution), [execution]);
11156
11391
  const isTerminalCommand = reactExports.useMemo(() => isTerminalCommandExecution(execution), [execution]);
11157
- const tokenUsageLabel = execution.tokenUsage ? `${execution.tokenUsage.inputTokens.toLocaleString()} in ${execution.tokenUsage.outputTokens.toLocaleString()} out` : null;
11392
+ const tokenUsageLabel = execution.tokenUsage ? `${execution.tokenUsage.inputTokens.toLocaleString()} in (${((_c = execution.tokenUsage.cachedInputTokens) != null ? _c : 0).toLocaleString()} cached + ${Math.max(
11393
+ execution.tokenUsage.inputTokens - ((_d = execution.tokenUsage.cachedInputTokens) != null ? _d : 0),
11394
+ 0
11395
+ ).toLocaleString()} uncached) • ${execution.tokenUsage.outputTokens.toLocaleString()} out` : null;
11158
11396
  const formattedArguments = reactExports.useMemo(
11159
11397
  () => prettyPrintJson(execution.functionCall.arguments),
11160
11398
  [execution.functionCall.arguments]
@@ -11163,7 +11401,7 @@ const ExecutionTrace = ({ execution }) => {
11163
11401
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
11164
11402
  "section",
11165
11403
  {
11166
- className: `trace-card ${((_c = execution.functionOutput) == null ? void 0 : _c.isError) ? "error" : ""} ${isTerminalCommand ? "terminal" : ""}`,
11404
+ className: `trace-card ${((_e = execution.functionOutput) == null ? void 0 : _e.isError) ? "error" : ""} ${isTerminalCommand ? "terminal" : ""}`,
11167
11405
  children: [
11168
11406
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
11169
11407
  "button",
@@ -11211,7 +11449,9 @@ const ExecutionTraceGroup = ({
11211
11449
  }) => {
11212
11450
  const [expanded, setExpanded] = reactExports.useState(false);
11213
11451
  const tokenTotals = reactExports.useMemo(() => {
11452
+ var _a;
11214
11453
  let inputTokens = 0;
11454
+ let cachedInputTokens = 0;
11215
11455
  let outputTokens = 0;
11216
11456
  let hasTokenUsage = false;
11217
11457
  for (const execution of executions) {
@@ -11220,15 +11460,19 @@ const ExecutionTraceGroup = ({
11220
11460
  }
11221
11461
  hasTokenUsage = true;
11222
11462
  inputTokens += execution.tokenUsage.inputTokens;
11463
+ cachedInputTokens += (_a = execution.tokenUsage.cachedInputTokens) != null ? _a : 0;
11223
11464
  outputTokens += execution.tokenUsage.outputTokens;
11224
11465
  }
11225
- return { hasTokenUsage, inputTokens, outputTokens };
11466
+ return { hasTokenUsage, inputTokens, cachedInputTokens, outputTokens };
11226
11467
  }, [executions]);
11227
11468
  if (executions.length === 0) {
11228
11469
  return null;
11229
11470
  }
11230
11471
  const countLabel = `${executions.length} command${executions.length === 1 ? "" : "s"}`;
11231
- const tokenUsageLabel = tokenTotals.hasTokenUsage ? `${tokenTotals.inputTokens.toLocaleString()} in ${tokenTotals.outputTokens.toLocaleString()} out` : null;
11472
+ const tokenUsageLabel = tokenTotals.hasTokenUsage ? `${tokenTotals.inputTokens.toLocaleString()} in (${tokenTotals.cachedInputTokens.toLocaleString()} cached + ${Math.max(
11473
+ tokenTotals.inputTokens - tokenTotals.cachedInputTokens,
11474
+ 0
11475
+ ).toLocaleString()} uncached) • ${tokenTotals.outputTokens.toLocaleString()} out` : null;
11232
11476
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "trace-group-card", children: [
11233
11477
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
11234
11478
  "button",
@@ -1021,6 +1021,25 @@ select {
1021
1021
  letter-spacing: 0.06em;
1022
1022
  }
1023
1023
 
1024
+ .stats-section-header-actions {
1025
+ margin-left: auto;
1026
+ display: inline-flex;
1027
+ align-items: center;
1028
+ gap: 8px;
1029
+ }
1030
+
1031
+ .stats-section-header-actions .sidebar-label,
1032
+ .stats-section-header-actions .stats-section-subtle {
1033
+ white-space: nowrap;
1034
+ }
1035
+
1036
+ .stats-contrib-select {
1037
+ width: auto;
1038
+ min-width: 156px;
1039
+ padding: 6px 9px;
1040
+ font-size: 0.78rem;
1041
+ }
1042
+
1024
1043
  .stats-day-list {
1025
1044
  list-style: none;
1026
1045
  margin: 0;
@@ -1061,6 +1080,120 @@ select {
1061
1080
  background: linear-gradient(90deg, rgba(59, 130, 246, 0.8), rgba(56, 189, 248, 0.85));
1062
1081
  }
1063
1082
 
1083
+ .stats-contrib-frame {
1084
+ display: grid;
1085
+ grid-template-columns: 30px minmax(0, 1fr);
1086
+ gap: 8px;
1087
+ }
1088
+
1089
+ .stats-contrib-weekdays {
1090
+ display: grid;
1091
+ grid-template-rows: repeat(7, 12px);
1092
+ gap: 3px;
1093
+ padding-top: 18px;
1094
+ }
1095
+
1096
+ .stats-contrib-weekdays span {
1097
+ font-size: 0.63rem;
1098
+ line-height: 12px;
1099
+ color: var(--text-muted);
1100
+ text-align: right;
1101
+ }
1102
+
1103
+ .stats-contrib-scroll {
1104
+ min-width: 0;
1105
+ overflow-x: auto;
1106
+ overflow-y: hidden;
1107
+ padding-bottom: 2px;
1108
+ }
1109
+
1110
+ .stats-contrib-months {
1111
+ display: grid;
1112
+ gap: 3px;
1113
+ margin-bottom: 4px;
1114
+ min-width: -moz-max-content;
1115
+ min-width: max-content;
1116
+ }
1117
+
1118
+ .stats-contrib-month {
1119
+ font-size: 0.66rem;
1120
+ color: var(--text-muted);
1121
+ font-family: var(--font-mono);
1122
+ }
1123
+
1124
+ .stats-contrib-weeks {
1125
+ display: flex;
1126
+ gap: 3px;
1127
+ min-width: -moz-max-content;
1128
+ min-width: max-content;
1129
+ }
1130
+
1131
+ .stats-contrib-week {
1132
+ display: grid;
1133
+ grid-template-rows: repeat(7, 12px);
1134
+ gap: 3px;
1135
+ }
1136
+
1137
+ .stats-contrib-cell {
1138
+ width: 12px;
1139
+ height: 12px;
1140
+ border-radius: 3px;
1141
+ border: 1px solid rgba(59, 130, 246, 0.25);
1142
+ background: rgba(59, 130, 246, 0.08);
1143
+ cursor: pointer;
1144
+ }
1145
+
1146
+ .stats-contrib-cell.level-0 {
1147
+ background: rgba(59, 130, 246, 0.08);
1148
+ border-color: rgba(59, 130, 246, 0.2);
1149
+ }
1150
+
1151
+ .stats-contrib-cell.level-1 {
1152
+ background: rgba(59, 130, 246, 0.28);
1153
+ border-color: rgba(59, 130, 246, 0.3);
1154
+ }
1155
+
1156
+ .stats-contrib-cell.level-2 {
1157
+ background: rgba(59, 130, 246, 0.46);
1158
+ border-color: rgba(59, 130, 246, 0.45);
1159
+ }
1160
+
1161
+ .stats-contrib-cell.level-3 {
1162
+ background: rgba(59, 130, 246, 0.64);
1163
+ border-color: rgba(59, 130, 246, 0.6);
1164
+ }
1165
+
1166
+ .stats-contrib-cell.level-4 {
1167
+ background: rgba(59, 130, 246, 0.82);
1168
+ border-color: rgba(59, 130, 246, 0.78);
1169
+ }
1170
+
1171
+ .stats-contrib-cell.is-empty {
1172
+ background: rgba(59, 130, 246, 0.05);
1173
+ }
1174
+
1175
+ .stats-contrib-cell.is-padding {
1176
+ background: transparent;
1177
+ border-color: transparent;
1178
+ cursor: default;
1179
+ }
1180
+
1181
+ .stats-contrib-tooltip {
1182
+ position: fixed;
1183
+ z-index: 30;
1184
+ max-width: min(320px, calc(100vw - 24px));
1185
+ border: 1px solid var(--border-strong);
1186
+ border-radius: 8px;
1187
+ background: var(--bg-panel-raised);
1188
+ color: var(--text-primary);
1189
+ padding: 6px 8px;
1190
+ font-size: 0.72rem;
1191
+ font-family: var(--font-mono);
1192
+ line-height: 1.3;
1193
+ pointer-events: none;
1194
+ white-space: nowrap;
1195
+ }
1196
+
1064
1197
  .stats-grid-2 {
1065
1198
  display: grid;
1066
1199
  gap: 12px;
@@ -1387,6 +1520,30 @@ select {
1387
1520
  gap: 8px;
1388
1521
  }
1389
1522
 
1523
+ .stats-section-header-actions {
1524
+ width: 100%;
1525
+ margin-left: 0;
1526
+ justify-content: space-between;
1527
+ }
1528
+
1529
+ .stats-contrib-select {
1530
+ flex: 1;
1531
+ min-width: 0;
1532
+ max-width: 180px;
1533
+ }
1534
+
1535
+ .stats-contrib-frame {
1536
+ grid-template-columns: 1fr;
1537
+ }
1538
+
1539
+ .stats-contrib-weekdays {
1540
+ display: none;
1541
+ }
1542
+
1543
+ .stats-contrib-month {
1544
+ font-size: 0.6rem;
1545
+ }
1546
+
1390
1547
  .chat-session-token-strip {
1391
1548
  flex-wrap: wrap;
1392
1549
  gap: 8px;
@@ -7,8 +7,8 @@
7
7
  <link rel="icon" type="image/png" sizes="32x32" href="./assets/32x32-DQgygEFU.png" />
8
8
  <link rel="icon" type="image/png" sizes="16x16" href="./assets/16x16-B2_QkmoB.png" />
9
9
  <title>codex-devtools</title>
10
- <script type="module" crossorigin src="./assets/index-C-iGxog-.js"></script>
11
- <link rel="stylesheet" crossorigin href="./assets/index-D3FYKy1U.css">
10
+ <script type="module" crossorigin src="./assets/index-5ydAmpDO.js"></script>
11
+ <link rel="stylesheet" crossorigin href="./assets/index-Cgat1ue6.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="root">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codex-devtools",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "description": "Desktop app for inspecting Codex session data",
6
6
  "license": "MIT",
7
7
  "author": {