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.
- package/dashboard/dist/assets/{AchievementBadge-Ddtn6vg2.js → AchievementBadge-3L7czzR1.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-AMo1Ib5I.js → AchievementsPage-BFfuj5At.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CUMjcYst.js → AchievementsSection-HYShnmyO.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CA03iBRx.js → BadgeDetailModal-CCSn5R-f.js} +1 -1
- package/dashboard/dist/assets/{Card-DscI10IE.js → Card-DXegrNGi.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-5cIXJ0NQ.js → ClawdAnimated-D72waMCF.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-qCpUMoAO.js → CommandPalette-BMng2nFX.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-FT_T1UzH.js → CommunityStatsModal-BQmxfHSr.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-GI2RxzAE.js → ConfirmModal-CpyPGo-h.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CkA3pfjj.js → DashboardPage-CQ2A9jo4.js} +1 -1
- package/dashboard/dist/assets/DevicePage-DG0Vxsww.js +1 -0
- package/dashboard/dist/assets/{DialogClose-cfdVTNyN.js → DialogClose-DJ8mCX28.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CJDYVJ8S.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CtptgR8Z.js → DialogTitle-1vR-kqKB.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CPpE5UGk.js → FadeIn-DHkAS7mo.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-vN3e1w0o.js → HeaderGithubStar-DR0AXuPQ.js} +1 -1
- package/dashboard/dist/assets/HoverTooltip-CcGvGB5a.js +1 -0
- package/dashboard/dist/assets/{IpCheckPage-BqJPwFGV.js → IpCheckPage-DycvTdue.js} +1 -1
- package/dashboard/dist/assets/LandingPage-CHs8wugk.js +2 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-BRWPXDZl.js → LeaderboardAvatar-CN9dR1b7.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Dy55Qbr0.js → LeaderboardPage-BLXve5xc.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-Dm5bW2pX.js → LeaderboardProfileModal-CarZtrz9.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CWmLrKsM.js → LeaderboardProfilePage-DAoBzt2h.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-DFn8Z9MC.js +4 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-Dso8I7Y4.js → LocalOnlyNotice-H0JRSn1e.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CnghY3oG.js → LoginCard-wd3R-8P7.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B30O2N_x.js → LoginModal-DYXk0bmO.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DglsaA23.js → LoginPage-DxN9qFIi.js} +1 -1
- package/dashboard/dist/assets/{PetPage-C7-xrZix.js → PetPage-BjZIfiI3.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-CTbuRUy2.js → PopoverPopup-CQIKHlVw.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-D-sr7Qko.js → ProviderIcon-BrrTOC6_.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-CIbj5Hpp.js → ResetPasswordPage-BNwsqyqJ.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-dxer1vVZ.js → SegmentedControl-cSXGJx_j.js} +1 -1
- package/dashboard/dist/assets/{Select-BSQkd3VW.js → Select-D5H9WvyT.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-EGKNiN7_.js → SelectItemText-fSnMRQbs.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-CXF87EFA.js +2 -0
- package/dashboard/dist/assets/{SettingsPage-3MQDH9bF.js → SettingsPage-Hmqe_c9L.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-DZ8k3xK4.js → SkillsPage-CpL3mDUz.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-B5q_U7lQ.js → TokenGalaxy-CnutAaeA.js} +2 -2
- package/dashboard/dist/assets/{ToolbarRootContext-DIy-0I9L.js → ToolbarRootContext-CbdtTEGs.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-Bj558t-a.js → TrendMonitor-CfbNLJ9Z.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-t9EgVnVK.js → WidgetsPage-DOCARdp-.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B-8f6-oI.js → WrappedPage-BKNZTfkp.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DwFlHgQR.js → __vite-browser-external-CvrVYYUZ.js} +27 -17
- package/dashboard/dist/assets/{__vite-browser-external-CvhQU1Bj.css → __vite-browser-external-WKilYZnq.css} +1 -1
- package/dashboard/dist/assets/{agent-logos-C6ooyoY_.js → agent-logos-CvFfhBDq.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CWOhbEwU.js → arrow-up-right-BtBLF2n1.js} +1 -1
- package/dashboard/dist/assets/{check-DqrkOjUC.js → check-DfPQe0v9.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DlP0Hiu-.js → chevron-right-isIHsrvd.js} +1 -1
- package/dashboard/dist/assets/{copy-Dh_ZalIb.js → copy-CJrwu3Xh.js} +1 -1
- package/dashboard/dist/assets/{download-fssIVO33.js → download-D-TaSqSP.js} +1 -1
- package/dashboard/dist/assets/{flame-COf56i7e.js → flame-95I2KBCp.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DA4cG58O.js → format-tokens-D0_AljQl.js} +1 -1
- package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
- package/dashboard/dist/assets/{icons-DigJtAfR.js → icons-B0jhI6as.js} +1 -1
- package/dashboard/dist/assets/{index-BR90lSkB.js → index-DNueKe7x.js} +1 -1
- package/dashboard/dist/assets/{index-DIEScXsc.js → index-Do9pBHr0.js} +1 -1
- package/dashboard/dist/assets/{index-b_fO7MtI.js → index-zL31qR6U.js} +1 -1
- package/dashboard/dist/assets/{info-mwER43h2.js → info-DXinzd-a.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-DyEdxJUi.js → limits-providers-DEb3zmKt.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bfgtp0co.js → link-2-BVAdYWaq.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B8ASQdW3.js → loader-circle-nu9zNFC8.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-88sF_sdM.js → maximize-2-D3RclWuw.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CYWy8Wjh.js → provider-display-ChB_33mn.js} +1 -1
- package/dashboard/dist/assets/{react-CcvGEOfz.js → react-Djyp9kg9.js} +1 -1
- package/dashboard/dist/assets/{search-C3gFd5W0.js → search-CRzn5fb2.js} +1 -1
- package/dashboard/dist/assets/{skills-api-BkjJfFxG.js → skills-api-D4df2IJr.js} +1 -1
- package/dashboard/dist/assets/{store-CGE8neJG.js → store-BE4jimvO.js} +1 -1
- package/dashboard/dist/assets/{terminal-CjoZ2jlv.js → terminal-C8bbfANZ.js} +1 -1
- package/dashboard/dist/assets/{trash-2-Buwry_AW.js → trash-2-B1pgRqNS.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DZW_XQoS.js → use-community-stats-Co5GxKNb.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BD0CXAYR.js → use-limits-display-prefs-CVxmTm05.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CPVIdR4z.js → use-native-settings-C3cNbS0b.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-_yETN2gW.js → use-ordered-list-DqGSSTpQ.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-D7Wk0w1o.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-mOQHAb6j.js → use-session-efficiency-pref-w80Pa5Dd.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CvmlOKm0.js → use-transform-DxH5_rBe.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-9W0OuOdp.js → use-usage-limits-BY0LkALC.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BZVRsDTW.js → useCurrency-CG55rarM.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-Dw-jQnFB.js → useScrollLock-BjjtVeB5.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BHw_Jkes.js → useTokenFormat-ChaFYAh3.js} +1 -1
- package/dashboard/dist/assets/{zap-CeYmAnn-.js → zap-Doeerimb.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/init.js +5 -6
- package/src/commands/status.js +10 -9
- package/src/lib/codex-config.js +45 -4
- package/src/lib/local-api.js +4 -1
- package/src/lib/pricing/curated-overrides.json +4 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/session-analytics.js +133 -30
- package/dashboard/dist/assets/DevicePage-CdtlanVo.js +0 -1
- package/dashboard/dist/assets/DialogDescription-CKjbQpir.js +0 -1
- package/dashboard/dist/assets/LandingPage-DrjAIdTL.js +0 -2
- package/dashboard/dist/assets/LimitsPage-B538PSle.js +0 -4
- package/dashboard/dist/assets/SessionsPage-CugLdscs.js +0 -1
- 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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
record.duration_ms =
|
|
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 =
|
|
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)
|
|
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:
|
|
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 =
|
|
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)
|
|
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 {
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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) ===
|
|
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
|
-
|
|
760
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
row.duration_ms =
|
|
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)
|
|
784
|
-
|
|
785
|
-
|
|
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};
|