@gonrocca/zero-pi 0.1.39 → 0.1.41

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.
@@ -98,7 +98,7 @@ export function composeStatusline(p: StatuslineParts): string {
98
98
 
99
99
  if (p.branch) parts.push(fg(STEEL, p.branch));
100
100
 
101
- if (p.brand) parts.push(fg(AMBER, p.brand));
101
+ if (p.brand) parts.push(fg(VIOLET, p.brand));
102
102
 
103
103
  return parts.join(SEP);
104
104
  }
@@ -113,10 +113,21 @@ interface PiModel {
113
113
  name?: string;
114
114
  contextWindow?: number;
115
115
  }
116
+ interface PiSessionEntry {
117
+ type?: string;
118
+ message?: {
119
+ role?: string;
120
+ usage?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
121
+ };
122
+ }
123
+ interface PiSessionManager {
124
+ getEntries?(): PiSessionEntry[];
125
+ }
116
126
  interface PiCtx {
117
127
  ui?: PiUI;
118
128
  model?: PiModel;
119
129
  cwd?: string;
130
+ sessionManager?: PiSessionManager;
120
131
  getContextUsage?(): { tokens?: number } | null | undefined;
121
132
  }
122
133
  interface PiAPI {
@@ -127,9 +138,11 @@ const STATUS_ID = "zero-statusline";
127
138
  const BRAND = "www.ceroclawd.com";
128
139
 
129
140
  // ─── Session-scoped state ──────────────────────────────────────────────────
141
+ // Only git stays in state (it's expensive to re-shell on every render);
142
+ // tokens are computed fresh from sessionManager.getEntries() per render —
143
+ // the same pattern pi's native footer uses, so it can never double-count a
144
+ // streamed message_update.
130
145
 
131
- let tokensIn = 0;
132
- let tokensOut = 0;
133
146
  let branch: string | undefined;
134
147
  let added = 0;
135
148
  let removed = 0;
@@ -137,16 +150,42 @@ let lastCtx: PiCtx | undefined;
137
150
  let gitInFlight = false;
138
151
 
139
152
  function resetSessionState(): void {
140
- tokensIn = 0;
141
- tokensOut = 0;
142
153
  branch = undefined;
143
154
  added = 0;
144
155
  removed = 0;
145
156
  }
146
157
 
158
+ /**
159
+ * Sum assistant input/output tokens across the full session, matching pi's
160
+ * own footer logic. Defensive: a missing sessionManager or entries iteration
161
+ * failure yields zero, never throws.
162
+ */
163
+ export function computeSessionTokens(
164
+ sessionManager: PiSessionManager | undefined,
165
+ ): { input: number; output: number } {
166
+ if (!sessionManager || typeof sessionManager.getEntries !== "function") {
167
+ return { input: 0, output: 0 };
168
+ }
169
+ let input = 0;
170
+ let output = 0;
171
+ try {
172
+ for (const entry of sessionManager.getEntries()) {
173
+ if (entry?.type !== "message") continue;
174
+ if (entry.message?.role !== "assistant") continue;
175
+ const u = entry.message.usage;
176
+ if (typeof u?.input === "number" && u.input > 0) input += u.input;
177
+ if (typeof u?.output === "number" && u.output > 0) output += u.output;
178
+ }
179
+ } catch {
180
+ // session iteration failure — return what we summed so far.
181
+ }
182
+ return { input, output };
183
+ }
184
+
147
185
  // ─── Git read (best-effort, async, deduped) ────────────────────────────────
148
186
 
149
- async function readGit(cwd: string | undefined): Promise<void> {
187
+ async function readGit(cwdHint: string | undefined): Promise<void> {
188
+ const cwd = cwdHint || process.cwd();
150
189
  if (!cwd || gitInFlight) return;
151
190
  gitInFlight = true;
152
191
  try {
@@ -187,10 +226,11 @@ function render(ctx: PiCtx): void {
187
226
  const window = ctx.model?.contextWindow && ctx.model.contextWindow > 0 ? ctx.model.contextWindow : 200_000;
188
227
  const used = ctx.getContextUsage?.()?.tokens;
189
228
  const ctxPercent = typeof used === "number" && used >= 0 ? Math.min(100, (used / window) * 100) : undefined;
229
+ const tokens = computeSessionTokens(ctx.sessionManager);
190
230
  const text = composeStatusline({
191
231
  model: shortModel(ctx.model?.id ?? ctx.model?.name),
192
- tokensIn,
193
- tokensOut,
232
+ tokensIn: tokens.input,
233
+ tokensOut: tokens.output,
194
234
  diffAdded: added,
195
235
  diffRemoved: removed,
196
236
  ctxPercent,
@@ -237,19 +277,12 @@ export default function register(pi?: unknown): void {
237
277
  }
238
278
  });
239
279
 
240
- api.on("message_update", (event, ctx) => {
280
+ api.on("message_update", (_event, ctx) => {
241
281
  try {
242
282
  lastCtx = ctx;
243
- const usage = (event as { message?: { usage?: { inputTokens?: unknown; outputTokens?: unknown } } })?.message
244
- ?.usage;
245
- if (usage) {
246
- if (typeof usage.inputTokens === "number" && usage.inputTokens > 0) {
247
- tokensIn += usage.inputTokens;
248
- }
249
- if (typeof usage.outputTokens === "number" && usage.outputTokens > 0) {
250
- tokensOut += usage.outputTokens;
251
- }
252
- }
283
+ // Tokens are computed from sessionManager.getEntries() in render()
284
+ // no event-side accumulation, so a streamed message_update repeat
285
+ // can never double-count.
253
286
  render(ctx);
254
287
  } catch {
255
288
  // never break a session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonrocca/zero-pi",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, per-phase model autotune, and skill auto-learning. Adds capability to pi without modifying pi.",
5
5
  "type": "module",
6
6
  "keywords": [