tokentracker-cli 0.83.11 → 0.84.0

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.
Files changed (100) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-Ddtn6vg2.js → AchievementBadge-3L7czzR1.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-AMo1Ib5I.js → AchievementsPage-BFfuj5At.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-CUMjcYst.js → AchievementsSection-HYShnmyO.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-CA03iBRx.js → BadgeDetailModal-CCSn5R-f.js} +1 -1
  5. package/dashboard/dist/assets/{Card-DscI10IE.js → Card-DXegrNGi.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-5cIXJ0NQ.js → ClawdAnimated-D72waMCF.js} +1 -1
  7. package/dashboard/dist/assets/{CommandPalette-qCpUMoAO.js → CommandPalette-BMng2nFX.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-FT_T1UzH.js → CommunityStatsModal-BQmxfHSr.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-GI2RxzAE.js → ConfirmModal-CpyPGo-h.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-CkA3pfjj.js → DashboardPage-CQ2A9jo4.js} +1 -1
  11. package/dashboard/dist/assets/DevicePage-DG0Vxsww.js +1 -0
  12. package/dashboard/dist/assets/{DialogClose-cfdVTNyN.js → DialogClose-DJ8mCX28.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-CJDYVJ8S.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-CtptgR8Z.js → DialogTitle-1vR-kqKB.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-CPpE5UGk.js → FadeIn-DHkAS7mo.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-vN3e1w0o.js → HeaderGithubStar-DR0AXuPQ.js} +1 -1
  17. package/dashboard/dist/assets/HoverTooltip-CcGvGB5a.js +1 -0
  18. package/dashboard/dist/assets/{IpCheckPage-BqJPwFGV.js → IpCheckPage-DycvTdue.js} +1 -1
  19. package/dashboard/dist/assets/LandingPage-CHs8wugk.js +2 -0
  20. package/dashboard/dist/assets/{LeaderboardAvatar-BRWPXDZl.js → LeaderboardAvatar-CN9dR1b7.js} +1 -1
  21. package/dashboard/dist/assets/{LeaderboardPage-Dy55Qbr0.js → LeaderboardPage-BLXve5xc.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardProfileModal-Dm5bW2pX.js → LeaderboardProfileModal-CarZtrz9.js} +3 -3
  23. package/dashboard/dist/assets/{LeaderboardProfilePage-CWmLrKsM.js → LeaderboardProfilePage-DAoBzt2h.js} +1 -1
  24. package/dashboard/dist/assets/LimitsPage-DFn8Z9MC.js +4 -0
  25. package/dashboard/dist/assets/{LocalOnlyNotice-Dso8I7Y4.js → LocalOnlyNotice-H0JRSn1e.js} +1 -1
  26. package/dashboard/dist/assets/{LoginCard-CnghY3oG.js → LoginCard-wd3R-8P7.js} +1 -1
  27. package/dashboard/dist/assets/{LoginModal-B30O2N_x.js → LoginModal-DYXk0bmO.js} +1 -1
  28. package/dashboard/dist/assets/{LoginPage-DglsaA23.js → LoginPage-DxN9qFIi.js} +1 -1
  29. package/dashboard/dist/assets/{PetPage-C7-xrZix.js → PetPage-BjZIfiI3.js} +1 -1
  30. package/dashboard/dist/assets/{PopoverPopup-CTbuRUy2.js → PopoverPopup-CQIKHlVw.js} +1 -1
  31. package/dashboard/dist/assets/{ProviderIcon-D-sr7Qko.js → ProviderIcon-BrrTOC6_.js} +1 -1
  32. package/dashboard/dist/assets/{ResetPasswordPage-CIbj5Hpp.js → ResetPasswordPage-BNwsqyqJ.js} +1 -1
  33. package/dashboard/dist/assets/{SegmentedControl-dxer1vVZ.js → SegmentedControl-cSXGJx_j.js} +1 -1
  34. package/dashboard/dist/assets/{Select-BSQkd3VW.js → Select-D5H9WvyT.js} +1 -1
  35. package/dashboard/dist/assets/{SelectItemText-EGKNiN7_.js → SelectItemText-fSnMRQbs.js} +1 -1
  36. package/dashboard/dist/assets/SessionsPage-CXF87EFA.js +2 -0
  37. package/dashboard/dist/assets/{SettingsPage-3MQDH9bF.js → SettingsPage-Hmqe_c9L.js} +1 -1
  38. package/dashboard/dist/assets/{SkillsPage-DZ8k3xK4.js → SkillsPage-CpL3mDUz.js} +1 -1
  39. package/dashboard/dist/assets/{TokenGalaxy-B5q_U7lQ.js → TokenGalaxy-CnutAaeA.js} +2 -2
  40. package/dashboard/dist/assets/{ToolbarRootContext-DIy-0I9L.js → ToolbarRootContext-CbdtTEGs.js} +1 -1
  41. package/dashboard/dist/assets/{TrendMonitor-Bj558t-a.js → TrendMonitor-CfbNLJ9Z.js} +1 -1
  42. package/dashboard/dist/assets/{WidgetsPage-t9EgVnVK.js → WidgetsPage-DOCARdp-.js} +1 -1
  43. package/dashboard/dist/assets/{WrappedPage-B-8f6-oI.js → WrappedPage-BKNZTfkp.js} +1 -1
  44. package/dashboard/dist/assets/{__vite-browser-external-DwFlHgQR.js → __vite-browser-external-CvrVYYUZ.js} +27 -17
  45. package/dashboard/dist/assets/{__vite-browser-external-CvhQU1Bj.css → __vite-browser-external-WKilYZnq.css} +1 -1
  46. package/dashboard/dist/assets/{agent-logos-C6ooyoY_.js → agent-logos-CvFfhBDq.js} +1 -1
  47. package/dashboard/dist/assets/{arrow-up-right-CWOhbEwU.js → arrow-up-right-BtBLF2n1.js} +1 -1
  48. package/dashboard/dist/assets/{check-DqrkOjUC.js → check-DfPQe0v9.js} +1 -1
  49. package/dashboard/dist/assets/{chevron-right-DlP0Hiu-.js → chevron-right-isIHsrvd.js} +1 -1
  50. package/dashboard/dist/assets/{copy-Dh_ZalIb.js → copy-CJrwu3Xh.js} +1 -1
  51. package/dashboard/dist/assets/{download-fssIVO33.js → download-D-TaSqSP.js} +1 -1
  52. package/dashboard/dist/assets/{flame-COf56i7e.js → flame-95I2KBCp.js} +1 -1
  53. package/dashboard/dist/assets/{format-tokens-DA4cG58O.js → format-tokens-D0_AljQl.js} +1 -1
  54. package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
  55. package/dashboard/dist/assets/{icons-DigJtAfR.js → icons-B0jhI6as.js} +1 -1
  56. package/dashboard/dist/assets/{index-BR90lSkB.js → index-DNueKe7x.js} +1 -1
  57. package/dashboard/dist/assets/{index-DIEScXsc.js → index-Do9pBHr0.js} +1 -1
  58. package/dashboard/dist/assets/{index-b_fO7MtI.js → index-zL31qR6U.js} +1 -1
  59. package/dashboard/dist/assets/{info-mwER43h2.js → info-DXinzd-a.js} +1 -1
  60. package/dashboard/dist/assets/{limits-providers-DyEdxJUi.js → limits-providers-DEb3zmKt.js} +1 -1
  61. package/dashboard/dist/assets/{link-2-Bfgtp0co.js → link-2-BVAdYWaq.js} +1 -1
  62. package/dashboard/dist/assets/{loader-circle-B8ASQdW3.js → loader-circle-nu9zNFC8.js} +1 -1
  63. package/dashboard/dist/assets/{maximize-2-88sF_sdM.js → maximize-2-D3RclWuw.js} +1 -1
  64. package/dashboard/dist/assets/{provider-display-CYWy8Wjh.js → provider-display-ChB_33mn.js} +1 -1
  65. package/dashboard/dist/assets/{react-CcvGEOfz.js → react-Djyp9kg9.js} +1 -1
  66. package/dashboard/dist/assets/{search-C3gFd5W0.js → search-CRzn5fb2.js} +1 -1
  67. package/dashboard/dist/assets/{skills-api-BkjJfFxG.js → skills-api-D4df2IJr.js} +1 -1
  68. package/dashboard/dist/assets/{store-CGE8neJG.js → store-BE4jimvO.js} +1 -1
  69. package/dashboard/dist/assets/{terminal-CjoZ2jlv.js → terminal-C8bbfANZ.js} +1 -1
  70. package/dashboard/dist/assets/{trash-2-Buwry_AW.js → trash-2-B1pgRqNS.js} +1 -1
  71. package/dashboard/dist/assets/{use-community-stats-DZW_XQoS.js → use-community-stats-Co5GxKNb.js} +1 -1
  72. package/dashboard/dist/assets/{use-limits-display-prefs-BD0CXAYR.js → use-limits-display-prefs-CVxmTm05.js} +1 -1
  73. package/dashboard/dist/assets/{use-native-settings-CPVIdR4z.js → use-native-settings-C3cNbS0b.js} +1 -1
  74. package/dashboard/dist/assets/{use-ordered-list-_yETN2gW.js → use-ordered-list-DqGSSTpQ.js} +1 -1
  75. package/dashboard/dist/assets/use-reduced-motion-D7Wk0w1o.js +1 -0
  76. package/dashboard/dist/assets/{use-session-efficiency-pref-mOQHAb6j.js → use-session-efficiency-pref-w80Pa5Dd.js} +1 -1
  77. package/dashboard/dist/assets/{use-transform-CvmlOKm0.js → use-transform-DxH5_rBe.js} +1 -1
  78. package/dashboard/dist/assets/{use-usage-limits-9W0OuOdp.js → use-usage-limits-BY0LkALC.js} +1 -1
  79. package/dashboard/dist/assets/{useCurrency-BZVRsDTW.js → useCurrency-CG55rarM.js} +1 -1
  80. package/dashboard/dist/assets/{useScrollLock-Dw-jQnFB.js → useScrollLock-BjjtVeB5.js} +1 -1
  81. package/dashboard/dist/assets/{useTokenFormat-BHw_Jkes.js → useTokenFormat-ChaFYAh3.js} +1 -1
  82. package/dashboard/dist/assets/{zap-CeYmAnn-.js → zap-Doeerimb.js} +1 -1
  83. package/dashboard/dist/index.html +2 -2
  84. package/dashboard/dist/ip-check.html +2 -2
  85. package/dashboard/dist/leaderboard.html +2 -2
  86. package/dashboard/dist/share.html +2 -2
  87. package/package.json +1 -1
  88. package/src/commands/init.js +5 -6
  89. package/src/commands/status.js +10 -9
  90. package/src/lib/codex-config.js +45 -4
  91. package/src/lib/local-api.js +4 -1
  92. package/src/lib/pricing/curated-overrides.json +4 -0
  93. package/src/lib/pricing/seed-snapshot.json +1 -1
  94. package/src/lib/session-analytics.js +133 -30
  95. package/dashboard/dist/assets/DevicePage-CdtlanVo.js +0 -1
  96. package/dashboard/dist/assets/DialogDescription-CKjbQpir.js +0 -1
  97. package/dashboard/dist/assets/LandingPage-DrjAIdTL.js +0 -2
  98. package/dashboard/dist/assets/LimitsPage-B538PSle.js +0 -4
  99. package/dashboard/dist/assets/SessionsPage-CugLdscs.js +0 -1
  100. package/dashboard/dist/assets/use-reduced-motion-BOf4gJaX.js +0 -1
@@ -22,7 +22,14 @@ const { computeRowCost } = require("./pricing");
22
22
  // the agent wrote itself, never the raw prompt body, and stay local-only
23
23
  // alongside session_id (no self-parsed prompt fallback, so the strict
24
24
  // metadata-only guarantee holds).
25
- const SIDECAR_VERSION = 8;
25
+ // v9 changes derived metrics, so every cached row must be rebuilt:
26
+ // - duration_ms is now *active* time (see IDLE_GAP_MS), not the wall-clock
27
+ // span between the first and last log line, which read as 89 days on
28
+ // resumed/forked sessions.
29
+ // - session_id is only set when the log actually contained a sessionId
30
+ // record. It used to fall back to the file's basename, which produced
31
+ // resume commands for non-session files (`claude --resume journal`).
32
+ const SIDECAR_VERSION = 9;
26
33
  const EDIT_TOOLS = new Set(["apply_patch", "edit", "write", "multiedit", "notebookedit"]);
27
34
  const PLACEHOLDER_MODELS = new Set(["<synthetic>", "synthetic", "<unknown>", "unknown"]);
28
35
  const CLAUDE_MEM_OBSERVER_PROJECT_SUFFIX = "--claude-mem-observer-sessions";
@@ -74,11 +81,39 @@ function safeTimestamp(value) {
74
81
  return new Date(value).toISOString();
75
82
  }
76
83
 
84
+ // A session that was resumed days later, or whose file replays inherited
85
+ // history from a fork, spans a wall-clock range that says nothing about how
86
+ // long the user actually worked (observed: 2142h / 89 days on a session with
87
+ // zero turns). Accumulate *active* time instead: the sum of gaps between
88
+ // consecutive log timestamps, excluding any gap longer than IDLE_GAP_MS. The
89
+ // long jump from replayed history to real work is one such gap, so it drops
90
+ // out on its own. started_at / ended_at still carry the true span.
91
+ const IDLE_GAP_MS = 30 * 60 * 1000;
92
+
93
+ function emptyBounds() {
94
+ return { started_at: null, ended_at: null, active_ms: 0, _last_ts_ms: null };
95
+ }
96
+
77
97
  function updateBounds(bounds, value) {
78
98
  const timestamp = safeTimestamp(value);
79
99
  if (!timestamp) return;
80
100
  if (!bounds.started_at || timestamp < bounds.started_at) bounds.started_at = timestamp;
81
101
  if (!bounds.ended_at || timestamp > bounds.ended_at) bounds.ended_at = timestamp;
102
+ const ms = Date.parse(timestamp);
103
+ if (!Number.isFinite(ms)) return;
104
+ if (Number.isFinite(bounds._last_ts_ms)) {
105
+ const delta = ms - bounds._last_ts_ms;
106
+ // Ignore out-of-order lines and idle gaps; only real working time counts.
107
+ if (delta > 0 && delta <= IDLE_GAP_MS) bounds.active_ms += delta;
108
+ }
109
+ bounds._last_ts_ms = ms;
110
+ }
111
+
112
+ // Throwaway per-agent checkouts (`.claude/worktrees/<id>`, `.codex/worktrees/…`)
113
+ // are not the project the user is working on, and the directory is usually gone
114
+ // by the time the session is browsed.
115
+ function isWorktreeRef(ref) {
116
+ return /[\\/](worktrees|subagents)[\\/]/.test(String(ref || ""));
82
117
  }
83
118
 
84
119
  function projectKey(cwd, filePath) {
@@ -162,11 +197,9 @@ function extractCodexSignalTools(payload) {
162
197
  }
163
198
 
164
199
  function finalizeRecord(record) {
165
- const startedMs = Date.parse(record.started_at || "");
166
- const endedMs = Date.parse(record.ended_at || "");
167
- record.duration_ms = Number.isFinite(startedMs) && Number.isFinite(endedMs)
168
- ? Math.max(0, endedMs - startedMs)
169
- : 0;
200
+ delete record._last_ts_ms;
201
+ record.active_ms = Math.max(0, finite(record.active_ms));
202
+ record.duration_ms = record.active_ms;
170
203
  record.total_tokens = finite(record.total_tokens || record.tokens?.total_tokens);
171
204
  record.cost_usd = computeRowCost({ source: record.source, model: record.model, ...record.tokens });
172
205
  record.productive = record.edit_turns > 0;
@@ -184,9 +217,26 @@ async function scanClaudeSession(filePath) {
184
217
  const input = fs.createReadStream(filePath, { encoding: "utf8" });
185
218
  const lines = readline.createInterface({ input, crlfDelay: Infinity });
186
219
  const tokens = emptyTotals();
220
+ // Per-file, deliberately. rollout.js persists its dedup set across files to
221
+ // catch cross-file duplicates, but that is only sound there because it owns a
222
+ // single global total. Here each file becomes an independently cached row, so
223
+ // a cross-file set would make a row's tokens depend on which other files were
224
+ // scanned first — and the incremental cache reuses rows without replaying
225
+ // that state. Measured cost of the per-file scope on real data: 808 assistant
226
+ // messages appear in more than one file (fork/resume replays history into a
227
+ // new session file), inflating per-session tokens by ~1.5% in aggregate.
228
+ // Fixing it needs a deterministic owner (e.g. earliest started_at) plus
229
+ // message keys persisted per row — a sidecar redesign, not a one-liner. Do
230
+ // NOT "fix" this by widening the set without solving the cache interaction.
187
231
  const seenMessages = new Set();
188
- const bounds = { started_at: null, ended_at: null };
232
+ const bounds = emptyBounds();
233
+ // The basename keeps grouping stable for files that never write a sessionId
234
+ // record, but only an *observed* sessionId may become a resumable
235
+ // session_id — otherwise non-session logs under ~/.claude/projects (e.g.
236
+ // skill-injections.jsonl, journal.jsonl) yield `claude --resume journal`,
237
+ // a command that always fails.
189
238
  let rawSessionId = path.basename(filePath, ".jsonl");
239
+ let observedSessionId = null;
190
240
  let cwd = null;
191
241
  let model = "unknown";
192
242
  let aiTitle = null;
@@ -209,7 +259,10 @@ async function scanClaudeSession(filePath) {
209
259
  let obj;
210
260
  try { obj = JSON.parse(line); } catch { continue; }
211
261
  updateBounds(bounds, obj.timestamp || obj.message?.timestamp);
212
- if (typeof obj.sessionId === "string" && obj.sessionId) rawSessionId = obj.sessionId;
262
+ if (typeof obj.sessionId === "string" && obj.sessionId) {
263
+ rawSessionId = obj.sessionId;
264
+ observedSessionId = obj.sessionId;
265
+ }
213
266
  if (typeof obj.cwd === "string" && obj.cwd) cwd = obj.cwd;
214
267
  // Claude writes its own generated one-line summary as an "ai-title" record.
215
268
  // It is agent-authored metadata (not the raw prompt body); keep the latest.
@@ -257,7 +310,7 @@ async function scanClaudeSession(filePath) {
257
310
  return finalizeRecord({
258
311
  version: SIDECAR_VERSION,
259
312
  session_hash: sessionHash("claude", rawSessionId),
260
- session_id: rawSessionId || null,
313
+ session_id: observedSessionId || null,
261
314
  // Claude's own generated one-line title (the "ai-title" record). Agent-
262
315
  // authored metadata, never the raw prompt body. Local-only: stripped in
263
316
  // summarizeSessions before any cloud/CSV export. Null when Claude never
@@ -279,7 +332,7 @@ async function scanClaudeSession(filePath) {
279
332
  }
280
333
 
281
334
  async function scanCodexDeliverySignals(filePath) {
282
- const bounds = { started_at: null, ended_at: null };
335
+ const bounds = emptyBounds();
283
336
  const input = fs.createReadStream(filePath, { encoding: "utf8" });
284
337
  const lines = readline.createInterface({ input, crlfDelay: Infinity });
285
338
  let turns = 0;
@@ -541,16 +594,32 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
541
594
  ...discovered.claude.map((filePath) => ({ source: "claude", filePath, scan: scanClaudeSession })),
542
595
  ...discovered.codex.map((filePath) => ({ source: "codex", filePath, scan: scanCodexSession })),
543
596
  ];
597
+ // Files we could not turn into a row (permission denied, half-written line,
598
+ // vanished mid-scan). Swallowing these silently made sessions disappear with
599
+ // no signal at all; count them so the API can say so.
600
+ let skippedFiles = 0;
544
601
  for (const entry of entries) {
545
602
  const cacheKey = sessionFileCacheKey(entry.source, entry.filePath);
546
603
  const statKey = analyticsEntryStatKey(entry.source, entry.filePath);
547
- if (!statKey) continue;
604
+ if (!statKey) {
605
+ skippedFiles += 1;
606
+ continue;
607
+ }
548
608
  let row = null;
549
609
  if (!force && previousFiles[cacheKey]?.stat_key === statKey) {
550
610
  row = previousRowsByFile.get(cacheKey) || null;
551
611
  }
552
612
  if (!row) {
553
- try { row = await entry.scan(entry.filePath); } catch { /* one active/partial session must not poison the sidecar */ }
613
+ try {
614
+ row = await entry.scan(entry.filePath);
615
+ } catch (error) {
616
+ // One active/partial session must not poison the sidecar, but do not
617
+ // pretend it never existed either.
618
+ skippedFiles += 1;
619
+ if (!process.env.NODE_TEST_CONTEXT) {
620
+ console.warn(`[session-analytics] skipped ${entry.source} session file: ${error?.message || error}`);
621
+ }
622
+ }
554
623
  }
555
624
  if (!row) continue;
556
625
  // A one-way file hash enables incremental reuse without persisting or
@@ -570,6 +639,9 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
570
639
  checked_at: generatedAt,
571
640
  files: nextFiles,
572
641
  })}\n`);
642
+ // Non-enumerable so the array still behaves exactly like a plain row list
643
+ // for every existing caller (map/filter/JSON of the rows is unaffected).
644
+ Object.defineProperty(sessions, "skippedFiles", { value: skippedFiles, enumerable: false });
573
645
  return sessions;
574
646
  }
575
647
 
@@ -582,23 +654,41 @@ const sessionAnalyticsBuilds = new Map();
582
654
  function buildSessionAnalytics(options = {}) {
583
655
  const normalizedOptions = options && typeof options === "object" ? options : {};
584
656
  const home = path.resolve(String(normalizedOptions.home || os.homedir()));
657
+ const force = Boolean(normalizedOptions.force);
585
658
  const existing = sessionAnalyticsBuilds.get(home);
586
- if (existing) return existing;
587
-
588
- const promise = buildSessionAnalyticsInternal({ ...normalizedOptions, home });
589
- sessionAnalyticsBuilds.set(home, promise);
659
+ // Joining an in-flight build is only correct when that build is at least as
660
+ // thorough as what this caller asked for. A refresh that landed while a plain
661
+ // build was running used to be handed the un-refreshed result: the spinner
662
+ // stopped and nothing had actually been re-scanned. Chain instead of joining
663
+ // — never run two scans at once, they race the atomic sidecar write.
664
+ if (existing && (!force || existing.force)) return existing.promise;
665
+
666
+ const run = () => buildSessionAnalyticsInternal({ ...normalizedOptions, home, force });
667
+ const promise = existing ? existing.promise.then(run, run) : run();
668
+ const entry = { promise, force: force || Boolean(existing?.force) };
669
+ sessionAnalyticsBuilds.set(home, entry);
590
670
  const clear = () => {
591
- if (sessionAnalyticsBuilds.get(home) === promise) sessionAnalyticsBuilds.delete(home);
671
+ if (sessionAnalyticsBuilds.get(home) === entry) sessionAnalyticsBuilds.delete(home);
592
672
  };
593
673
  promise.then(clear, clear);
594
674
  return promise;
595
675
  }
596
676
 
677
+ // Does a session overlap the [from, to] day window? Testing started_at alone
678
+ // dropped every session that began before the window and ended inside it —
679
+ // exactly the long/resumed ones, which also sort to the top because the sort
680
+ // key is ended_at. On real data a 7-day window lost 6 sessions, the largest
681
+ // 224M tokens. Treat the window as an interval intersection instead.
682
+ function withinDayRange(row, from, to) {
683
+ const startDay = String(row?.started_at || row?.ended_at || "").slice(0, 10);
684
+ const endDay = String(row?.ended_at || row?.started_at || "").slice(0, 10);
685
+ if (from && (!endDay || endDay < from)) return false;
686
+ if (to && (!startDay || startDay > to)) return false;
687
+ return true;
688
+ }
689
+
597
690
  function summarizeSessions(sessions, { from = "", to = "", includeSessions = true } = {}) {
598
- const filtered = (sessions || []).filter((row) => {
599
- const day = String(row.started_at || row.ended_at || "").slice(0, 10);
600
- return (!from || day >= from) && (!to || day <= to);
601
- });
691
+ const filtered = (sessions || []).filter((row) => withinDayRange(row, from, to));
602
692
  const byModel = new Map();
603
693
  const subagents = new Map();
604
694
  for (const row of filtered) {
@@ -750,14 +840,22 @@ function mergeSessionFragments(rows) {
750
840
  cur.total_tokens = finite(cur.total_tokens) + finite(row.total_tokens);
751
841
  cur.cost_usd = finite(cur.cost_usd) + finite(row.cost_usd);
752
842
  cur.productive = Boolean(cur.productive) || Boolean(row.productive);
843
+ cur.active_ms = finite(cur.active_ms) + finite(row.active_ms);
753
844
  if (!cur.title && row.title) cur.title = row.title;
754
845
  // Representative model/project comes from the busiest fragment (most
755
846
  // tokens) so a tiny sidechain cannot overwrite the real coding model.
756
847
  if (finite(row.total_tokens) > finite(cur._repr_tokens)) {
757
848
  cur._repr_tokens = finite(row.total_tokens);
758
849
  if (row.model && row.model !== "unknown") cur.model = row.model;
759
- cur.project_key = row.project_key;
760
- cur.project_ref = row.project_ref;
850
+ // Project needs its own guard: a subagent running in a git worktree has
851
+ // that throwaway directory as its cwd, so the busiest fragment can label
852
+ // the whole session with a temp name like "agent-a3cb8089a11d3471a" and
853
+ // point project_ref at a directory that no longer exists. Only let a
854
+ // worktree fragment name the session when nothing better is available.
855
+ if (!isWorktreeRef(row.project_ref) || isWorktreeRef(cur.project_ref)) {
856
+ cur.project_key = row.project_key;
857
+ cur.project_ref = row.project_ref;
858
+ }
761
859
  }
762
860
  if (row.started_at && (!cur.started_at || row.started_at < cur.started_at)) cur.started_at = row.started_at;
763
861
  if (row.ended_at && (!cur.ended_at || row.ended_at > cur.ended_at)) cur.ended_at = row.ended_at;
@@ -765,9 +863,9 @@ function mergeSessionFragments(rows) {
765
863
  const merged = [];
766
864
  for (const row of byHash.values()) {
767
865
  delete row._repr_tokens;
768
- const startedMs = Date.parse(row.started_at || "");
769
- const endedMs = Date.parse(row.ended_at || "");
770
- row.duration_ms = Number.isFinite(startedMs) && Number.isFinite(endedMs) ? Math.max(0, endedMs - startedMs) : 0;
866
+ // Active time is additive across fragments (see IDLE_GAP_MS); the
867
+ // wall-clock span between first and last line is not.
868
+ row.duration_ms = finite(row.active_ms);
771
869
  row.first_pass = finite(row.edit_turns) === 1 && finite(row.retry_turns) === 0;
772
870
  row.one_shot = row.first_pass;
773
871
  merged.push(row);
@@ -780,10 +878,14 @@ function mergeSessionFragments(rows) {
780
878
  // the UI can offer one-click resume. Callers must only expose it over the
781
879
  // local API.
782
880
  function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}) {
783
- const filtered = mergeSessionFragments(sessions).filter((row) => {
784
- const day = String(row.started_at || row.ended_at || "").slice(0, 10);
785
- return (!from || day >= from) && (!to || day <= to);
786
- });
881
+ const filtered = mergeSessionFragments(sessions)
882
+ // Only sessions that actually spent tokens are worth listing. This drops
883
+ // two kinds of noise: non-session logs under ~/.claude/projects
884
+ // (skill-injections.jsonl, journal.jsonl, …), and sessions abandoned before
885
+ // the model replied. Both render as "unknown · 0 tokens · $0.00" rows with
886
+ // no model, no cost and nothing to analyze.
887
+ .filter((row) => finite(row.total_tokens) > 0)
888
+ .filter((row) => withinDayRange(row, from, to));
787
889
  filtered.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
788
890
  const cap = Number(limit) > 0 ? Number(limit) : 0;
789
891
  const limited = cap > 0 ? filtered.slice(0, cap) : filtered;
@@ -791,6 +893,7 @@ function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}
791
893
  available: filtered.length > 0,
792
894
  session_count: filtered.length,
793
895
  returned_count: limited.length,
896
+ skipped_files: finite(sessions?.skippedFiles),
794
897
  sessions: limited.map(toSessionBrowserRow),
795
898
  provenance: {
796
899
  source: "local-session-log",
@@ -1 +0,0 @@
1
- import{a as b,r as l,j as r,c as e,h as v}from"./__vite-browser-external-DwFlHgQR.js";const x={BASE_URL:"/",DEV:!1,MODE:"production",PROD:!0,SSR:!1,VITE_APP_VERSION:"0.83.11",VITE_INSFORGE_ANON_KEY:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3OC0xMjM0LTU2NzgtOTBhYi1jZGVmMTIzNDU2NzgiLCJlbWFpbCI6ImFub25AaW5zZm9yZ2UuY29tIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODExNDU5NDd9.T0auta_IrVIh0uXW1bob5QSnzvsnJmN28r5XkSGEuQY",VITE_INSFORGE_BASE_URL:"https://srctyff5.us-east.insforge.app"};function f(){return v(),`${(x&&"https://srctyff5.us-east.insforge.app"||"https://srctyff5.us-east.insforge.app").replace(/\/$/,"")}/functions/tokentracker-device-flow-grant`}function p(s){return(s||"").toUpperCase().replace(/\s+/g,"").replace(/[^A-Z0-9-]/g,"")}function h(){const s=b(),[m,g]=l.useState(""),[i,a]=l.useState({kind:"idle",message:""});l.useEffect(()=>{try{const d=new URLSearchParams(window.location.search).get("user_code");d&&g(p(d))}catch{}},[]);async function u(c){if(c?.preventDefault?.(),!s?.signedIn){a({kind:"error",message:e("device.approval.error.sign_in")});return}const d=p(m);if(!/^[A-HJKMNP-Z2-9]{4}-[A-HJKMNP-Z2-9]{4}$/.test(d)){a({kind:"error",message:e("device.approval.error.invalid_code")});return}a({kind:"working",message:e("device.approval.action.approving")});try{const n=await s.getAccessToken?.();if(!n){a({kind:"error",message:e("device.approval.error.no_token")});return}const o=await fetch(f(),{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({user_code:d})}),t=await o.json().catch(()=>({}));o.ok?a({kind:"success",message:t.status==="already_approved"?t.client_info?e("device.approval.success.already_client",{client:t.client_info}):e("device.approval.success.already"):t.client_info?e("device.approval.success.approved_client",{client:t.client_info}):e("device.approval.success.approved")}):o.status===404?a({kind:"error",message:e("device.approval.error.not_found")}):o.status===410?a({kind:"error",message:e("device.approval.error.expired")}):o.status===401?a({kind:"error",message:e("device.approval.error.session_expired")}):a({kind:"error",message:t?.error?e("device.approval.error.server_detail",{message:t.error}):e("device.approval.error.server",{status:o.status})})}catch(n){a({kind:"error",message:e("device.approval.error.network",{message:n?.message||n})})}}return r.jsx("div",{className:"min-h-screen flex items-center justify-center bg-white dark:bg-oai-gray-950 px-6 py-12",children:r.jsxs("div",{className:"w-full max-w-md rounded-xl border border-oai-gray-200 dark:border-oai-gray-800 bg-white dark:bg-oai-gray-900 p-8",children:[r.jsx("h1",{className:"text-2xl font-semibold text-oai-gray-900 dark:text-white mb-2",children:e("device.approval.title")}),r.jsx("p",{className:"text-sm text-oai-gray-500 dark:text-oai-gray-400 mb-6",children:e("device.approval.description")}),s?.signedIn?null:r.jsx("div",{className:"rounded-md border border-amber-200 bg-amber-50 dark:bg-amber-900/20 dark:border-amber-800 p-4 mb-4",children:r.jsx("p",{className:"text-sm text-amber-700 dark:text-amber-300",children:e("device.approval.sign_in_banner")})}),r.jsxs("form",{onSubmit:u,className:"space-y-4",children:[r.jsxs("label",{className:"block",children:[r.jsx("span",{className:"block text-xs font-medium uppercase tracking-wide text-oai-gray-500 dark:text-oai-gray-400 mb-1",children:e("device.approval.code_label")}),r.jsx("input",{type:"text",value:m,onChange:c=>g(p(c.target.value)),placeholder:"XXXX-XXXX",spellCheck:!1,autoCapitalize:"characters",maxLength:9,className:"w-full font-mono text-xl tracking-widest text-center rounded-md border border-oai-gray-300 dark:border-oai-gray-700 bg-white dark:bg-oai-gray-900 text-oai-gray-900 dark:text-white px-4 py-3 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-emerald-600"})]}),r.jsx("button",{type:"submit",disabled:i.kind==="working"||!s?.signedIn,className:"w-full rounded-md bg-emerald-600 hover:bg-emerald-500 disabled:bg-oai-gray-300 disabled:cursor-not-allowed text-white font-medium py-3 transition-colors",children:i.kind==="working"?e("device.approval.action.approving"):e("device.approval.action.approve")})]}),i.kind==="success"&&r.jsx("div",{className:"mt-4 rounded-md border border-emerald-200 bg-emerald-50 dark:bg-emerald-900/20 dark:border-emerald-800 p-4",children:r.jsx("p",{className:"text-sm text-emerald-700 dark:text-emerald-300",children:i.message})}),i.kind==="error"&&r.jsx("div",{className:"mt-4 rounded-md border border-red-200 bg-red-50 dark:bg-red-900/20 dark:border-red-800 p-4",children:r.jsx("p",{className:"text-sm text-red-700 dark:text-red-300",children:i.message})})]})})}export{h as default};
@@ -1 +0,0 @@
1
- import{r as n,z as a}from"./__vite-browser-external-DwFlHgQR.js";import{u as p}from"./DialogTitle-CtptgR8Z.js";import{u as c}from"./useScrollLock-Dw-jQnFB.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};