@tmustier/pi-usage-extension 0.7.0 → 0.7.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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/data.ts +44 -0
- package/index.ts +27 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Descriptive loading messages with a live file counter while `/usage` opens: “Building your usage history for the first time…” on a fresh install, “Rebuilding your usage history — the cache format changed…” after an upgrade that bumps the cache version, and “Updating your usage history since \<date>…” for routine incremental refreshes. Warm opens with nothing to parse keep the plain spinner.
|
|
7
|
+
- `collectUsageData` accepts an `onProgress` callback reporting mode (`first-run` / `rebuild` / `update`), files to parse, files parsed, and the newest already-ingested session activity timestamp.
|
|
8
|
+
|
|
3
9
|
## [0.7.0] - 2026-07-17
|
|
4
10
|
|
|
5
11
|
### Changed
|
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ On narrow terminals, `/usage` automatically switches to a compact table instead
|
|
|
159
159
|
- **On-disk cache.** Per-file extraction results are cached in `<agentDir>/usage-extension-cache.json` (respects `PI_CODING_AGENT_DIR`), keyed by file size + mtime. Warm opens only re-parse session files that changed since the last run — on a 5.2 GB / 3,310-file corpus that takes the open from ~17 s to ~0.3 s.
|
|
160
160
|
- **First open** after install (or after deleting the cache) does a one-off full build, showing the usual cancellable loader. Cancelling saves partial progress, so the next open resumes where it left off.
|
|
161
161
|
- The cache is safe to delete at any time; it is rebuilt automatically. Corrupt or version-mismatched caches are ignored and rebuilt rather than trusted.
|
|
162
|
-
- **0.7.0 bumps the cache format to v3** (adds session working directory and compaction markers; 0.6.0's v2 added thinking level and reasoning tokens). The first open after upgrading does a one-off full rebuild, then warm opens are fast again.
|
|
162
|
+
- **0.7.0 bumps the cache format to v3** (adds session working directory and compaction markers; 0.6.0's v2 added thinking level and reasoning tokens). The first open after upgrading does a one-off full rebuild (with a progress message and live file counter), then warm opens are fast again.
|
|
163
163
|
|
|
164
164
|
## Provider Notes
|
|
165
165
|
|
package/data.ts
CHANGED
|
@@ -619,6 +619,7 @@ function getPeriodsForTimestamp(
|
|
|
619
619
|
}
|
|
620
620
|
|
|
621
621
|
const DAY_MS = 24 * HOUR_MS;
|
|
622
|
+
const PROGRESS_REPORT_EVERY = 100;
|
|
622
623
|
|
|
623
624
|
function addMessagesToUsageData(
|
|
624
625
|
data: UsageData,
|
|
@@ -727,8 +728,21 @@ const STAT_CONCURRENCY = 16;
|
|
|
727
728
|
const DEFAULT_PARSE_CONCURRENCY = 4;
|
|
728
729
|
const AGGREGATE_YIELD_EVERY_FILES = 200;
|
|
729
730
|
|
|
731
|
+
export interface CollectProgress {
|
|
732
|
+
/** Why this pass needs to parse files. */
|
|
733
|
+
mode: "first-run" | "rebuild" | "update";
|
|
734
|
+
/** Session files that need parsing this pass (0 = fully warm). */
|
|
735
|
+
filesToParse: number;
|
|
736
|
+
/** Files parsed so far; reported in coarse increments. */
|
|
737
|
+
filesParsed: number;
|
|
738
|
+
/** Newest session activity already ingested (ms since epoch); null when starting fresh. */
|
|
739
|
+
sinceMs: number | null;
|
|
740
|
+
}
|
|
741
|
+
|
|
730
742
|
export interface CollectUsageOptions {
|
|
731
743
|
signal?: AbortSignal;
|
|
744
|
+
/** Called once before parsing begins and periodically while files are parsed. */
|
|
745
|
+
onProgress?: (progress: CollectProgress) => void;
|
|
732
746
|
/** Defaults to `<agentDir>/sessions`. */
|
|
733
747
|
sessionsDir?: string;
|
|
734
748
|
/** Defaults to `<agentDir>/usage-extension-cache.json`. Pass `null` to disable the on-disk cache. */
|
|
@@ -794,6 +808,15 @@ export async function collectUsageData(options: CollectUsageOptions = {}): Promi
|
|
|
794
808
|
if (signal?.aborted) return null;
|
|
795
809
|
|
|
796
810
|
// 3. Load the cache and decide which files actually need parsing.
|
|
811
|
+
let cacheFileExists = false;
|
|
812
|
+
if (cachePath) {
|
|
813
|
+
try {
|
|
814
|
+
await stat(cachePath);
|
|
815
|
+
cacheFileExists = true;
|
|
816
|
+
} catch {
|
|
817
|
+
// No cache file yet — first run.
|
|
818
|
+
}
|
|
819
|
+
}
|
|
797
820
|
const previous = cachePath ? await loadUsageCache(cachePath) : new Map<string, CachedFileState>();
|
|
798
821
|
if (signal?.aborted) return null;
|
|
799
822
|
const current = new Map<string, CachedFileState>();
|
|
@@ -818,6 +841,22 @@ export async function collectUsageData(options: CollectUsageOptions = {}): Promi
|
|
|
818
841
|
}
|
|
819
842
|
}
|
|
820
843
|
|
|
844
|
+
// Progress reporting: distinguish a true first run from a format-change
|
|
845
|
+
// rebuild (cache file present but unusable) and a routine incremental update.
|
|
846
|
+
const progressMode: CollectProgress["mode"] =
|
|
847
|
+
previous.size > 0 ? "update" : cacheFileExists ? "rebuild" : "first-run";
|
|
848
|
+
let sinceMs: number | null = null;
|
|
849
|
+
if (progressMode === "update") {
|
|
850
|
+
for (const state of previous.values()) {
|
|
851
|
+
if (state.mtimeMs > (sinceMs ?? 0)) sinceMs = state.mtimeMs;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
let filesParsed = 0;
|
|
855
|
+
const reportProgress = (): void => {
|
|
856
|
+
options.onProgress?.({ mode: progressMode, filesToParse: toParse.length, filesParsed, sinceMs });
|
|
857
|
+
};
|
|
858
|
+
reportProgress();
|
|
859
|
+
|
|
821
860
|
// 4. Parse new/changed files with bounded concurrency.
|
|
822
861
|
{
|
|
823
862
|
let next = 0;
|
|
@@ -831,11 +870,16 @@ export async function collectUsageData(options: CollectUsageOptions = {}): Promi
|
|
|
831
870
|
try {
|
|
832
871
|
buffer = await readFile(filePath);
|
|
833
872
|
} catch {
|
|
873
|
+
filesParsed++;
|
|
834
874
|
continue; // File vanished — skip it.
|
|
835
875
|
}
|
|
836
876
|
const parsed = await parseSessionBuffer(buffer, signal);
|
|
837
877
|
if (signal?.aborted) return; // Never cache a partial parse.
|
|
838
878
|
current.set(filePath, { size: st.size, mtimeMs: st.mtimeMs, parsed });
|
|
879
|
+
filesParsed++;
|
|
880
|
+
if (filesParsed % PROGRESS_REPORT_EVERY === 0 || filesParsed === toParse.length) {
|
|
881
|
+
reportProgress();
|
|
882
|
+
}
|
|
839
883
|
}
|
|
840
884
|
})
|
|
841
885
|
);
|
package/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
15
15
|
|
|
16
16
|
import { collectUsageData, TAB_ORDER } from "./data";
|
|
17
|
+
import type { CollectProgress } from "./data";
|
|
17
18
|
import type { BaseStats, TabName, UsageData } from "./data";
|
|
18
19
|
import {
|
|
19
20
|
buildGraphModel,
|
|
@@ -175,6 +176,18 @@ function seriesColor(index: number): string {
|
|
|
175
176
|
return SERIES_COLORS[index % SERIES_COLORS.length]!;
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
/** "14:32" if the timestamp is today, otherwise "16 Jul" (with year if not this year). */
|
|
180
|
+
function formatSinceDate(ms: number): string {
|
|
181
|
+
const d = new Date(ms);
|
|
182
|
+
const now = new Date();
|
|
183
|
+
if (d.toDateString() === now.toDateString()) {
|
|
184
|
+
return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
185
|
+
}
|
|
186
|
+
const opts: Intl.DateTimeFormatOptions = { day: "numeric", month: "short" };
|
|
187
|
+
if (d.getFullYear() !== now.getFullYear()) opts.year = "numeric";
|
|
188
|
+
return d.toLocaleDateString(undefined, opts);
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
function padLeft(s: string, len: number): string {
|
|
179
192
|
const vis = visibleWidth(s);
|
|
180
193
|
if (vis >= len) return s;
|
|
@@ -722,7 +735,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
722
735
|
|
|
723
736
|
loader.onAbort = () => finish(null);
|
|
724
737
|
|
|
725
|
-
|
|
738
|
+
const onProgress = (p: CollectProgress): void => {
|
|
739
|
+
if (finished || p.filesToParse === 0) return;
|
|
740
|
+
const files = `${p.filesParsed.toLocaleString()}/${p.filesToParse.toLocaleString()} files`;
|
|
741
|
+
if (p.mode === "update") {
|
|
742
|
+
const since = p.sinceMs !== null ? ` since ${formatSinceDate(p.sinceMs)}` : "";
|
|
743
|
+
loader.setMessage(`Updating your usage history${since}… (${files})`);
|
|
744
|
+
} else if (p.mode === "rebuild") {
|
|
745
|
+
loader.setMessage(`Rebuilding your usage history — the cache format changed… (${files})`);
|
|
746
|
+
} else {
|
|
747
|
+
loader.setMessage(`Building your usage history for the first time… (${files})`);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
collectUsageData({ signal: loader.signal, onProgress })
|
|
726
752
|
.then(finish)
|
|
727
753
|
.catch(() => finish(null));
|
|
728
754
|
|