@d3ara1n/pi-editor-shell 0.2.1 → 0.3.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +67 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-editor-shell",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Replaces pi's default editor and status bar with a unified rounded-corner shell — no Nerd Font required",
6
6
  "keywords": [
package/src/index.ts CHANGED
@@ -53,14 +53,20 @@ const ICON = {
53
53
  thinking: "\uf400", // oct-light-bulb
54
54
  context: "\uf49b", // oct-cache
55
55
  cache: "\u26a1", // ⚡ oct-zap
56
+ hitRate: "\uf140", // fa-bullseye(靶心,缓存命中率)
56
57
  folder: "\uf07c", // fa-folder
57
58
  } as const;
58
59
 
59
60
  /** Minimal inline types to read cache-read totals without importing the
60
61
  * full pi-ai message union tree. */
62
+ interface UsageSnap {
63
+ input?: number;
64
+ cacheRead?: number;
65
+ cacheWrite?: number;
66
+ }
61
67
  interface MsgSnap {
62
68
  role: string;
63
- usage?: { cacheRead?: number };
69
+ usage?: UsageSnap;
64
70
  }
65
71
  interface EntrySnap {
66
72
  type: string;
@@ -79,6 +85,28 @@ function sumCacheRead(ctx: { sessionManager: { getEntries(): unknown[] } }): num
79
85
  return total;
80
86
  }
81
87
 
88
+ /** Usage of the most recent assistant message — drives the per-turn
89
+ * cacheRead and the hit rate, matching pi's footer (last entry wins). */
90
+ function latestAssistantUsage(ctx: { sessionManager: { getEntries(): unknown[] } }): UsageSnap | undefined {
91
+ let latest: UsageSnap | undefined;
92
+ for (const entry of ctx.sessionManager.getEntries()) {
93
+ const e = entry as EntrySnap;
94
+ if (e.type !== "message" || e.message?.role !== "assistant" || !e.message.usage) continue;
95
+ latest = e.message.usage;
96
+ }
97
+ return latest;
98
+ }
99
+
100
+ /** Cache hit rate for a single turn: cacheRead / (input + cacheRead +
101
+ * cacheWrite) × 100 — same formula pi's footer uses for "CHxx%".
102
+ * Returns undefined when there's no usage or no prompt tokens. */
103
+ function cacheHitRate(u: UsageSnap | undefined): number | undefined {
104
+ if (!u) return undefined;
105
+ const prompt = (u.input ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0);
106
+ if (prompt <= 0) return undefined;
107
+ return ((u.cacheRead ?? 0) / prompt) * 100;
108
+ }
109
+
82
110
  /** Format a token count for display: 14000000 → "14.0M", 132000 → "132.0k". */
83
111
  function formatTokens(n: number): string {
84
112
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
@@ -170,9 +198,11 @@ export default function (pi: ExtensionAPI) {
170
198
  let footerSnap: FooterSnap | undefined;
171
199
  // CWD cached from session_start — used by turn_end to refresh git dirty.
172
200
  let _cwd = "";
173
- // cacheRead total, refreshed at session_start + agent_end. The render
174
- // provider reads this instead of re-scanning entries every frame.
201
+ // cacheRead total + latest-turn usage, refreshed at session_start +
202
+ // agent_end. The render provider reads these instead of re-scanning
203
+ // entries every frame.
175
204
  let _cacheTotal = 0;
205
+ let _latestUsage: UsageSnap | undefined;
176
206
 
177
207
  // ── Phase-aware spinner + lifecycle ────────────────────────────
178
208
  // Each event asks the editor for a phase; CardEditor.setSpinner is itself
@@ -189,9 +219,10 @@ export default function (pi: ExtensionAPI) {
189
219
  });
190
220
  pi.on("tool_execution_start", () => editor?.setSpinner("exec"));
191
221
  pi.on("agent_end", (_event, ctx) => {
192
- // cacheRead totals are stable once a turn finishes — recompute here
193
- // instead of on every render frame.
222
+ // cacheRead totals + latest usage are stable once a turn finishes —
223
+ // recompute here instead of on every render frame.
194
224
  _cacheTotal = sumCacheRead(ctx);
225
+ _latestUsage = latestAssistantUsage(ctx);
195
226
  editor?.setSpinner(null);
196
227
  });
197
228
  pi.on("session_shutdown", () => {
@@ -211,6 +242,7 @@ export default function (pi: ExtensionAPI) {
211
242
  _cwd = ctx.cwd;
212
243
  config = loadEditorShellConfig(ctx.cwd);
213
244
  _cacheTotal = sumCacheRead(ctx);
245
+ _latestUsage = latestAssistantUsage(ctx);
214
246
  refreshGitDirty(ctx.cwd, () => editor?.requestRender());
215
247
 
216
248
  // Fresh segments on every render — reads live ctx state, so thinking /
@@ -241,15 +273,17 @@ export default function (pi: ExtensionAPI) {
241
273
  const ctxWindow = usage?.contextWindow ?? ctx.model?.contextWindow;
242
274
  const ctxText =
243
275
  pct != null && ctxWindow
244
- ? `${Math.round(pct)}%/${(ctxWindow / 1000).toFixed(0)}k`
276
+ ? `${pct.toFixed(1)}%/${(ctxWindow / 1000).toFixed(0)}k`
245
277
  : "?/??k";
246
278
 
247
- // Cache-read token total refreshed at agent_end (same data source
248
- // pi's own footer uses for "R14M"); read from cache off the hot path.
249
- const cacheTokens = _cacheTotal;
279
+ // Cache-read tokensper-turn figure first, session total in parens,
280
+ // then hit rate (pi's "CHxx%" formula). All refreshed at agent_end and
281
+ // read from cache off the hot path.
282
+ const cacheReadNow = _latestUsage?.cacheRead ?? 0;
283
+ const hitRate = cacheHitRate(_latestUsage);
250
284
  const cachePart =
251
- cacheTokens > 0
252
- ? `${theme.fg("dim", " · ")}${theme.fg("warning", `${ICON.cache} ${formatTokens(cacheTokens)}`)}`
285
+ _cacheTotal > 0
286
+ ? `${theme.fg("dim", " · ")}${theme.fg("warning", `${ICON.cache} ${formatTokens(cacheReadNow)} (${formatTokens(_cacheTotal)})${hitRate != null ? ` ${ICON.hitRate} ${hitRate.toFixed(1)}%` : ""}`)}`
253
287
  : "";
254
288
 
255
289
  // Git branch + dirty state — pi's format: ~/Projects (main).
@@ -333,7 +367,11 @@ export default function (pi: ExtensionAPI) {
333
367
  } else {
334
368
  const pinned = new Set(config.pinnedStatus);
335
369
  for (const [key, text] of entries.sort(([a], [b]) => a.localeCompare(b))) {
336
- const mark = pinned.has(key) ? " pinned" : "";
370
+ // The pin marker sits after status text, whose embedded reset
371
+ // would wash it to default white — re-wrap it in dim so it stays
372
+ // consistent with the surrounding text. (status text itself
373
+ // keeps its original color by design.)
374
+ const mark = pinned.has(key) ? ctx.ui.theme.fg("dim", " ← pinned") : "";
337
375
  lines.push(` ${key}: ${text}${mark}`);
338
376
  }
339
377
  }
@@ -344,7 +382,12 @@ export default function (pi: ExtensionAPI) {
344
382
  lines.push("");
345
383
  lines.push("[cache totals]");
346
384
  const tokens = sumCacheRead(ctx);
347
- lines.push(` cacheRead: ${tokens > 0 ? formatTokens(tokens) : "0"}`);
385
+ lines.push(` cacheRead (session): ${tokens > 0 ? formatTokens(tokens) : "0"}`);
386
+ const latest = latestAssistantUsage(ctx);
387
+ const now = latest?.cacheRead ?? 0;
388
+ lines.push(` cacheRead (this turn): ${formatTokens(now)}`);
389
+ const hr = cacheHitRate(latest);
390
+ lines.push(` hit rate: ${hr != null ? `${hr.toFixed(1)}%` : "n/a"}`);
348
391
 
349
392
  lines.push("");
350
393
  lines.push(`[context] cwd: ${ctx.cwd}`);
@@ -355,9 +398,17 @@ export default function (pi: ExtensionAPI) {
355
398
  lines.push(` git dirty: ${dirty || "clean"}`);
356
399
  }
357
400
  const m = ctx.model;
358
- lines.push(` model: ${m ? `${m.provider}/${m.id}` : "none"}`);
359
-
360
- ctx.ui.notify(lines.join("\n"), "info");
401
+ lines.push(` model: ${m ? `${m.provider}/${m.id}:${pi.getThinkingLevel()}` : "none"}`);
402
+
403
+ // Wrap each line in dim explicitly. notify adds its own outer dim
404
+ // layer, but extension status text carries its own color codes that
405
+ // reset the foreground mid-message. Per-line wrapping re-asserts dim
406
+ // at the start of every line, so a status row's reset can't bleed past
407
+ // it: status stays in its original color, everything else reads dim.
408
+ ctx.ui.notify(
409
+ lines.map((l) => ctx.ui.theme.fg("dim", l)).join("\n"),
410
+ "info",
411
+ );
361
412
  },
362
413
  });
363
414
  }