@quandev104/pi-style 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.
@@ -18,27 +18,197 @@ interface UsageTotals {
18
18
  cost: number;
19
19
  }
20
20
 
21
+ interface UsageContribution extends UsageTotals {
22
+ sawUsage: boolean;
23
+ }
24
+
25
+ interface SessionUsageCacheState {
26
+ processedLength: number;
27
+ lastEntryId: string | undefined;
28
+ lastEntryRef: SessionEntry | undefined;
29
+ lastContribution: UsageContribution;
30
+ totals: UsageTotals;
31
+ cached: UsageSnapshot | undefined;
32
+ scannedEntries: number;
33
+ cacheHits: number;
34
+ rebuilds: number;
35
+ tailRefreshes: number;
36
+ }
37
+
38
+ export interface SessionUsageCacheStats {
39
+ processedLength: number;
40
+ scannedEntries: number;
41
+ cacheHits: number;
42
+ rebuilds: number;
43
+ tailRefreshes: number;
44
+ }
45
+
46
+ let usageCache = new WeakMap<SessionUsageSource, SessionUsageCacheState>();
47
+
21
48
  /**
22
- * Aggregate cumulative token/cost usage from all session entries, mirroring
23
- * Pi's native footer: assistant messages always carry usage, tool results and
24
- * compaction/branch summaries carry it when available.
49
+ * Aggregate cumulative token/cost usage from finalized session entries,
50
+ * mirroring Pi's native footer. The result is cached incrementally so repeated
51
+ * reads only scan appended entries and refresh the finalized tail entry when it
52
+ * changes in place.
25
53
  */
26
54
  export function usageFromSession(session: SessionUsageSource): UsageSnapshot | undefined {
27
- const totals: UsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
28
- let sawUsage = false;
29
- for (const entry of session.getEntries()) {
30
- if (entry.type === "message") {
31
- if (entry.message.role !== "assistant" && entry.message.role !== "toolResult") continue;
32
- const usage = "usage" in entry.message ? entry.message.usage : undefined;
33
- if (!usage) continue;
34
- addUsage(totals, usage);
35
- sawUsage = true;
36
- } else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
37
- addUsage(totals, entry.usage);
38
- sawUsage = true;
55
+ const entries = session.getEntries();
56
+ const state = usageCache.get(session) ?? createState();
57
+ usageCache.set(session, state);
58
+
59
+ if (entries.length === 0) {
60
+ state.cacheHits++;
61
+ state.processedLength = 0;
62
+ state.lastEntryId = undefined;
63
+ state.lastEntryRef = undefined;
64
+ state.lastContribution = emptyContribution();
65
+ state.totals = emptyTotals();
66
+ state.cached = undefined;
67
+ return undefined;
68
+ }
69
+
70
+ if (state.processedLength === 0) {
71
+ rebuildFrom(entries, state);
72
+ return state.cached;
73
+ }
74
+
75
+ if (entries.length < state.processedLength) {
76
+ rebuildFrom(entries, state);
77
+ return state.cached;
78
+ }
79
+
80
+ const currentLast = entries.at(-1);
81
+ if (!currentLast) {
82
+ state.cacheHits++;
83
+ state.cached = undefined;
84
+ return undefined;
85
+ }
86
+
87
+ if (entries.length === state.processedLength) {
88
+ if (currentLast.id !== state.lastEntryId) {
89
+ rebuildFrom(entries, state);
90
+ return state.cached;
39
91
  }
92
+ if (currentLast !== state.lastEntryRef) refreshTailEntry(currentLast, state);
93
+ else state.cacheHits++;
94
+ return state.cached;
95
+ }
96
+
97
+ const previousLast = entries[state.processedLength - 1];
98
+ if (!previousLast || previousLast.id !== state.lastEntryId) {
99
+ rebuildFrom(entries, state);
100
+ return state.cached;
40
101
  }
41
- if (!sawUsage) return undefined;
102
+ if (previousLast !== state.lastEntryRef) refreshTailEntry(previousLast, state);
103
+ for (const entry of entries.slice(state.processedLength)) appendEntry(entry, state);
104
+ return state.cached;
105
+ }
106
+
107
+ export function resetUsageFromSessionCache(session?: SessionUsageSource): void {
108
+ if (session) {
109
+ usageCache.delete(session);
110
+ return;
111
+ }
112
+ usageCache = new WeakMap<SessionUsageSource, SessionUsageCacheState>();
113
+ }
114
+
115
+ export function getUsageFromSessionCacheStats(session: SessionUsageSource): SessionUsageCacheStats {
116
+ const state = usageCache.get(session) ?? createState();
117
+ if (!usageCache.has(session)) usageCache.set(session, state);
118
+ return {
119
+ processedLength: state.processedLength,
120
+ scannedEntries: state.scannedEntries,
121
+ cacheHits: state.cacheHits,
122
+ rebuilds: state.rebuilds,
123
+ tailRefreshes: state.tailRefreshes,
124
+ };
125
+ }
126
+
127
+ function createState(): SessionUsageCacheState {
128
+ return {
129
+ processedLength: 0,
130
+ lastEntryId: undefined,
131
+ lastEntryRef: undefined,
132
+ lastContribution: emptyContribution(),
133
+ totals: emptyTotals(),
134
+ cached: undefined,
135
+ scannedEntries: 0,
136
+ cacheHits: 0,
137
+ rebuilds: 0,
138
+ tailRefreshes: 0,
139
+ };
140
+ }
141
+
142
+ function rebuildFrom(entries: readonly SessionEntry[], state: SessionUsageCacheState): void {
143
+ state.rebuilds++;
144
+ state.processedLength = 0;
145
+ state.lastEntryId = undefined;
146
+ state.lastEntryRef = undefined;
147
+ state.lastContribution = emptyContribution();
148
+ state.totals = emptyTotals();
149
+ state.cached = undefined;
150
+ for (const entry of entries) appendEntry(entry, state);
151
+ }
152
+
153
+ function refreshTailEntry(entry: SessionEntry, state: SessionUsageCacheState): void {
154
+ state.tailRefreshes++;
155
+ subtractContribution(state.totals, state.lastContribution);
156
+ const contribution = usageContribution(entry);
157
+ addContribution(state.totals, contribution);
158
+ state.lastEntryId = entry.id;
159
+ state.lastEntryRef = entry;
160
+ state.lastContribution = contribution;
161
+ state.cached = snapshotFromTotals(state.totals);
162
+ state.scannedEntries++;
163
+ }
164
+
165
+ function appendEntry(entry: SessionEntry, state: SessionUsageCacheState): void {
166
+ const contribution = usageContribution(entry);
167
+ addContribution(state.totals, contribution);
168
+ state.processedLength++;
169
+ state.lastEntryId = entry.id;
170
+ state.lastEntryRef = entry;
171
+ state.lastContribution = contribution;
172
+ state.cached = snapshotFromTotals(state.totals);
173
+ state.scannedEntries++;
174
+ }
175
+
176
+ function usageContribution(entry: SessionEntry): UsageContribution {
177
+ if (entry.type === "message") {
178
+ if (entry.message.role !== "assistant" && entry.message.role !== "toolResult") return emptyContribution();
179
+ const usage = "usage" in entry.message ? entry.message.usage : undefined;
180
+ if (!usage) return emptyContribution();
181
+ return {
182
+ input: usage.input,
183
+ output: usage.output,
184
+ cacheRead: usage.cacheRead,
185
+ cacheWrite: usage.cacheWrite,
186
+ cost: usage.cost.total,
187
+ sawUsage: true,
188
+ };
189
+ }
190
+ if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
191
+ return {
192
+ input: entry.usage.input,
193
+ output: entry.usage.output,
194
+ cacheRead: entry.usage.cacheRead,
195
+ cacheWrite: entry.usage.cacheWrite,
196
+ cost: entry.usage.cost.total,
197
+ sawUsage: true,
198
+ };
199
+ }
200
+ return emptyContribution();
201
+ }
202
+
203
+ function snapshotFromTotals(totals: UsageTotals): UsageSnapshot | undefined {
204
+ if (
205
+ totals.input === 0 &&
206
+ totals.output === 0 &&
207
+ totals.cacheRead === 0 &&
208
+ totals.cacheWrite === 0 &&
209
+ totals.cost === 0
210
+ )
211
+ return undefined;
42
212
  return {
43
213
  inputTokens: totals.input,
44
214
  outputTokens: totals.output,
@@ -50,13 +220,26 @@ export function usageFromSession(session: SessionUsageSource): UsageSnapshot | u
50
220
  };
51
221
  }
52
222
 
53
- function addUsage(
54
- totals: UsageTotals,
55
- usage: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } },
56
- ): void {
223
+ function addContribution(totals: UsageTotals, usage: UsageContribution): void {
57
224
  totals.input += usage.input;
58
225
  totals.output += usage.output;
59
226
  totals.cacheRead += usage.cacheRead;
60
227
  totals.cacheWrite += usage.cacheWrite;
61
- totals.cost += usage.cost.total;
228
+ totals.cost += usage.cost;
229
+ }
230
+
231
+ function subtractContribution(totals: UsageTotals, usage: UsageContribution): void {
232
+ totals.input -= usage.input;
233
+ totals.output -= usage.output;
234
+ totals.cacheRead -= usage.cacheRead;
235
+ totals.cacheWrite -= usage.cacheWrite;
236
+ totals.cost -= usage.cost;
237
+ }
238
+
239
+ function emptyTotals(): UsageTotals {
240
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
241
+ }
242
+
243
+ function emptyContribution(): UsageContribution {
244
+ return { ...emptyTotals(), sawUsage: false };
62
245
  }
@@ -36,6 +36,17 @@ export interface BoxTheme {
36
36
  getColorMode?(): string;
37
37
  }
38
38
 
39
+ const THEME_CACHE_KEYS = new WeakMap<object, number>();
40
+ let nextThemeCacheKey = 1;
41
+
42
+ export function themeCacheKey(theme: object): number {
43
+ const cached = THEME_CACHE_KEYS.get(theme);
44
+ if (cached !== undefined) return cached;
45
+ const created = nextThemeCacheKey++;
46
+ THEME_CACHE_KEYS.set(theme, created);
47
+ return created;
48
+ }
49
+
39
50
  export interface BoxedRenderOptions {
40
51
  widthKey?: string;
41
52
  /** Detail embedded in the top-border title after the tool name (e.g. the path). */
@@ -384,10 +395,10 @@ export function dimLine(text: string): string {
384
395
  return `\x1b[2m${text}\x1b[22m`;
385
396
  }
386
397
 
387
- function boxText(theme: BoxTheme, text: string): string {
398
+ function boxText(_theme: BoxTheme, text: string): string {
388
399
  return dimLine(text);
389
400
  }
390
- function boxFrameText(theme: BoxTheme, text: string): string {
401
+ function boxFrameText(_theme: BoxTheme, text: string): string {
391
402
  return dimLine(text);
392
403
  }
393
404
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quandev104/pi-style",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A native-layout, cohesive visual style package for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",