@agnishc/edb-quit-summary 0.13.0 → 0.14.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.
Files changed (3) hide show
  1. package/README.md +12 -29
  2. package/package.json +1 -1
  3. package/src/index.ts +98 -89
package/README.md CHANGED
@@ -2,37 +2,20 @@
2
2
 
3
3
  A [pi](https://pi.dev) extension that prints a session summary to your terminal when you quit pi.
4
4
 
5
- When you hit `/quit` (or Ctrl+C twice), pi exits and you'll see a formatted summary like:
5
+ When you hit `/quit` (or Ctrl+C twice), pi exits and you'll see a compact summary like:
6
6
 
7
7
  ```
8
- ── Session Summary ──
9
-
10
- Session: Refactor auth module
11
- Duration: 23m 45s
12
- Model: anthropic/claude-sonnet-4-5
13
-
14
- Messages:
15
- User: 8
16
- Assistant: 8
17
- Tool calls: 24
18
-
19
- Tools used:
20
- edit 12×
21
- bash 6×
22
- read 4×
23
- grep 2×
24
-
25
- Tokens:
26
- Input: 45.2k ████████████░░░░
27
- Output: 12.8k ████░░░░░░░░░░░░
28
- Cache read: 38.1k ███████████░░░░░
29
- Cache write: 5.3k
30
- ────────────────────────────────────
31
- Total: 101.4k
32
-
33
- Cost: $0.34
34
-
35
- ─────────────────────────────
8
+ Session Summary / quit
9
+ ─────────────────────────────────────────────────────
10
+ /\_/\ Session Refactor auth module
11
+ < ▓ ▓ > Duration 23m 45s
12
+ \___/ Model anthropic/claude-sonnet-4-5
13
+
14
+ Messages 8 user / 8 assistant
15
+ Tools 24 · edit 12× · bash 6× · read 4×
16
+
17
+ Tokens 101.4k total 45.2k in 12.8k out
18
+ Cost $0.34
36
19
  ```
37
20
 
38
21
  ## How It Works
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agnishc/edb-quit-summary",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Pi extension: prints a session summary to the terminal when you hit /quit",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -5,9 +5,8 @@
5
5
  * Uses process.on('exit') to print after the TUI alternate screen buffer
6
6
  * is restored, so the summary is visible in the user's terminal.
7
7
  *
8
- * Layout: raccoon ASCII art on the left, stats on the right.
9
- * Dynamically scales to terminal width — hides art on narrow terminals,
10
- * shrinks bars to fit available space.
8
+ * Layout: understated title, raccoon ASCII art on the left, stats on the right.
9
+ * Dynamically scales to terminal width — hides art on narrow terminals.
11
10
  */
12
11
 
13
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -33,23 +32,16 @@ const m = (t: string) => `${MAGENTA}${t}${RESET}`;
33
32
  const bl = (t: string) => `${BLUE}${t}${RESET}`;
34
33
  const w = (t: string) => `${WHITE}${t}${RESET}`;
35
34
 
36
- // ── Raccoon ────────────────────────────────────────────────────────────────────
35
+ // ── Raccoon mark ───────────────────────────────────────────────────────────────
37
36
  //
38
- // 9 lines. Each entry has:
37
+ // Small, understated raccoon mask. Each entry has:
39
38
  // text — plain text (used for width measurement)
40
39
  // colored — ANSI-coloured version
41
- // All text fields are exactly 12 visible chars.
42
40
 
43
41
  const RACCOON: { text: string; colored: string }[] = [
44
- { text: " /\\ /\\ ", colored: ` ${d("/\\")} ${d("/\\")} ` },
45
- { text: " ( oo ) ", colored: ` ( ${b(" oo ")} ) ` },
46
- { text: " ( ,-___-, )", colored: ` ( ,${d("_____")}, )` },
47
- { text: " \\_______/ ", colored: ` ${d("\\______/")} ` },
48
- { text: " / \\ ", colored: ` / \\ ` },
49
- { text: " ( | | | | )", colored: ` ( ${d("| | | |")} )` },
50
- { text: " \\ ===== / ", colored: ` \\ ${d("=====")} / ` },
51
- { text: " \\ / ", colored: ` \\ / ` },
52
- { text: " `---' ", colored: ` \`---' ` },
42
+ { text: " /\\_/\\ ", colored: ` ${d("/\\_/\\")} ` },
43
+ { text: " < ▓ ▓ > ", colored: ` < ${d("▓")} ${d("▓")} > ` },
44
+ { text: " \\___/ ", colored: ` ${d("\\___/")} ` },
53
45
  ];
54
46
 
55
47
  // ── Formatters ─────────────────────────────────────────────────────────────────
@@ -92,7 +84,32 @@ function padTo(s: string, width: number): string {
92
84
  /** Truncate a plain string to a max length, appending "…" if truncated. */
93
85
  function truncate(s: string, max: number): string {
94
86
  if (s.length <= max) return s;
95
- return s.slice(0, max - 1) + "…";
87
+ return `${s.slice(0, max - 1)}…`;
88
+ }
89
+
90
+ /** Truncate an ANSI string to a max visible length, preserving escape codes. */
91
+ function truncateAnsi(s: string, max: number): string {
92
+ if (max <= 0) return "";
93
+ if (visLen(s) <= max) return s;
94
+
95
+ let out = "";
96
+ let visible = 0;
97
+ const target = Math.max(0, max - 1);
98
+
99
+ for (let i = 0; i < s.length && visible < target; i++) {
100
+ if (s[i] === "\x1b") {
101
+ const end = s.indexOf("m", i);
102
+ if (end === -1) break;
103
+ out += s.slice(i, end + 1);
104
+ i = end;
105
+ continue;
106
+ }
107
+
108
+ out += s[i];
109
+ visible++;
110
+ }
111
+
112
+ return `${out}${RESET}…`;
96
113
  }
97
114
 
98
115
  // ── Stats collection ───────────────────────────────────────────────────────────
@@ -209,61 +226,60 @@ function buildStatRows(s: SessionStats): StatRow[] {
209
226
  const totalTok = s.inputTokens + s.outputTokens + s.cacheRead + s.cacheWrite;
210
227
 
211
228
  if (s.sessionName) {
212
- rows.push({ type: "info", label: c("session"), value: b(truncate(s.sessionName, 40)) });
229
+ rows.push({ type: "info", label: d("Session"), value: b(truncate(s.sessionName, 42)) });
213
230
  }
214
- rows.push({ type: "info", label: c("duration"), value: w(formatDuration(duration)) });
231
+ rows.push({ type: "info", label: d("Duration"), value: w(formatDuration(duration)) });
215
232
  if (s.model) {
216
233
  const modelStr = s.provider
217
- ? `${d(truncate(s.provider, 12) + "/")}${truncate(s.model, 24)}`
218
- : truncate(s.model, 28);
219
- rows.push({ type: "info", label: c("model"), value: modelStr });
234
+ ? `${d(`${truncate(s.provider, 12)}/`)}${truncate(s.model, 28)}`
235
+ : truncate(s.model, 34);
236
+ rows.push({ type: "info", label: d("Model"), value: modelStr });
220
237
  }
221
238
 
222
239
  rows.push({ type: "spacer", label: "", value: "" });
223
-
224
240
  rows.push({
225
241
  type: "info",
226
- label: c("messages"),
227
- value: `${g(String(s.userMessages))} user ${bl(String(s.assistantMessages))} asst`,
242
+ label: d("Messages"),
243
+ value: `${g(String(s.userMessages))} user ${d("/")} ${bl(String(s.assistantMessages))} assistant`,
228
244
  });
229
245
 
230
246
  if (s.toolCalls > 0) {
231
247
  const topTools = Array.from(s.toolCounts.entries())
232
248
  .sort((a, b) => b[1] - a[1])
233
- .slice(0, 3)
234
- .map(([n, cnt]) => `${n}${d(" " + String(cnt) + "\u00d7")}`)
235
- .join(" ");
249
+ .slice(0, 4)
250
+ .map(([name, count]) => `${truncate(name, 12)} ${d(`${count}×`)}`)
251
+ .join(d(" · "));
236
252
  rows.push({
237
253
  type: "info",
238
- label: c("tools"),
239
- value: `${b(String(s.toolCalls))} ${topTools}`,
254
+ label: d("Tools"),
255
+ value: `${b(String(s.toolCalls))}${topTools ? d(" · ") + topTools : ""}`,
240
256
  });
241
257
  }
242
258
 
243
- rows.push({ type: "spacer", label: "", value: "" });
244
-
245
259
  if (totalTok > 0) {
246
- rows.push({ type: "info", label: c("input"), value: y(formatTokens(s.inputTokens)) });
247
- rows.push({ type: "info", label: c("output"), value: g(formatTokens(s.outputTokens)) });
248
- if (s.cacheRead > 0) {
249
- rows.push({ type: "info", label: c("c.read"), value: bl(formatTokens(s.cacheRead)) });
250
- }
251
- if (s.cacheWrite > 0) {
252
- rows.push({ type: "info", label: c("c.write"), value: d(formatTokens(s.cacheWrite)) });
260
+ rows.push({ type: "spacer", label: "", value: "" });
261
+ rows.push({
262
+ type: "info",
263
+ label: d("Tokens"),
264
+ value: `${b(formatTokens(totalTok))} ${d("total")} ${y(formatTokens(s.inputTokens))} ${d("in")} ${g(formatTokens(s.outputTokens))} ${d("out")}`,
265
+ });
266
+ if (s.cacheRead > 0 || s.cacheWrite > 0) {
267
+ rows.push({
268
+ type: "info",
269
+ label: d("Cache"),
270
+ value: `${bl(formatTokens(s.cacheRead))} ${d("read")} ${d(formatTokens(s.cacheWrite))} ${d("write")}`,
271
+ });
253
272
  }
254
- rows.push({ type: "info", label: c("total"), value: b(formatTokens(totalTok)) });
255
273
  }
256
274
 
257
275
  if (s.totalCost > 0) {
258
- rows.push({ type: "spacer", label: "", value: "" });
259
276
  const costCol = s.totalCost < 1 ? g : s.totalCost < 5 ? y : m;
260
- rows.push({ type: "info", label: c("cost"), value: b(costCol(formatCost(s.totalCost))) });
277
+ rows.push({ type: "info", label: d("Cost"), value: b(costCol(formatCost(s.totalCost))) });
261
278
  }
262
279
 
263
- // Resume command
264
280
  if (s.sessionId) {
265
281
  rows.push({ type: "spacer", label: "", value: "" });
266
- rows.push({ type: "info", label: c("resume"), value: d(`pi --resume=${s.sessionId}`) });
282
+ rows.push({ type: "info", label: d("Resume"), value: d(`pi --resume=${s.sessionId}`) });
267
283
  }
268
284
 
269
285
  return rows;
@@ -273,63 +289,56 @@ function buildStatRows(s: SessionStats): StatRow[] {
273
289
 
274
290
  function render(stats: SessionStats): string {
275
291
  const termWidth = process.stdout.columns || 80;
292
+ const margin = termWidth >= 72 ? " " : "";
293
+ const availableWidth = Math.max(32, termWidth - visLen(margin) * 2);
276
294
 
277
- // Raccoon art metrics
278
- const artVisW = Math.max(...RACCOON.map((l) => visLen(l.text)));
279
- const artGutter = 3;
280
- const artTotal = artVisW + artGutter;
281
-
282
- // Show art only when there's at least 48 chars remaining for stats
283
- const showArt = termWidth >= artTotal + 48;
284
-
285
- // Build rows
286
295
  const rows = buildStatRows(stats);
287
-
288
- const maxLabelW = Math.max(0, ...rows.filter((r) => r.type !== "spacer").map((r) => visLen(r.label)));
289
-
290
- // Format each stat line: label (padded) + 2 spaces + value
291
- const statLines: string[] = rows.map((row) => {
296
+ const maxLabelW = Math.max(0, ...rows.filter((row) => row.type !== "spacer").map((row) => visLen(row.label)));
297
+ const rawStatLines = rows.map((row) => {
292
298
  if (row.type === "spacer") return "";
293
- const lPad = padTo(row.label, maxLabelW);
294
- return `${lPad} ${row.value}`;
299
+ return `${padTo(row.label, maxLabelW)} ${row.value}`;
295
300
  });
296
301
 
297
- // Max visible width of stat block
298
- const maxStatVis = Math.max(0, ...statLines.map((l) => visLen(l)));
299
-
300
- // Final box dimensions
301
- const contentW = showArt ? artTotal + maxStatVis : maxStatVis;
302
- const boxInner = Math.min(contentW + 2, termWidth - 2);
303
- const hLine = "─".repeat(boxInner);
302
+ const artWidth = Math.max(...RACCOON.map((line) => visLen(line.text)));
303
+ const gapWidth = 4;
304
+ const minStatWidth = 32;
305
+ const artFits = availableWidth >= artWidth + gapWidth + minStatWidth;
306
+ const statWidth = artFits ? availableWidth - artWidth - gapWidth : availableWidth;
307
+ const statLines = rawStatLines.map((line) => truncateAnsi(line, statWidth));
308
+
309
+ const bodyWidth = artFits
310
+ ? artWidth + gapWidth + Math.max(0, ...statLines.map((line) => visLen(line)))
311
+ : Math.max(0, ...statLines.map((line) => visLen(line)));
312
+ const title = `${b(c("Session Summary"))} ${d("/ quit")}`;
313
+ const ruleWidth = Math.min(availableWidth, Math.max(28, visLen(title), bodyWidth));
314
+ const rule = d("─".repeat(ruleWidth));
315
+
316
+ const out: string[] = ["", `${margin}${title}`, `${margin}${rule}`];
317
+
318
+ if (!artFits) {
319
+ for (const line of statLines) {
320
+ out.push(line ? `${margin}${line}` : "");
321
+ }
322
+ out.push("");
323
+ return out.join("\n");
324
+ }
304
325
 
305
- // Merge art + stat lines
306
- const totalLines = Math.max(RACCOON.length, statLines.length);
307
- const bodyLines: string[] = [];
326
+ const artHeight = RACCOON.length;
327
+ const statHeight = statLines.length;
328
+ const totalLines = Math.max(artHeight, statHeight);
329
+ const artTop = Math.floor((totalLines - artHeight) / 2);
330
+ const statTop = Math.floor((totalLines - statHeight) / 2);
308
331
 
309
332
  for (let i = 0; i < totalLines; i++) {
310
- let line = "";
311
-
312
- if (showArt) {
313
- const artColored = i < RACCOON.length ? RACCOON[i]!.colored : "";
314
- const artVisW_ = i < RACCOON.length ? visLen(RACCOON[i]!.text) : 0;
315
- const gap = artTotal - artVisW_;
316
- line += artColored + " ".repeat(Math.max(0, gap));
317
- }
318
-
319
- line += i < statLines.length ? statLines[i]! : "";
320
- bodyLines.push(padTo(` ${line}`, boxInner));
333
+ const artIndex = i - artTop;
334
+ const statIndex = i - statTop;
335
+ const art = artIndex >= 0 && artIndex < RACCOON.length ? RACCOON[artIndex]!.colored : "";
336
+ const artPad = artWidth - (artIndex >= 0 && artIndex < RACCOON.length ? visLen(RACCOON[artIndex]!.text) : 0);
337
+ const stat = statIndex >= 0 && statIndex < statLines.length ? statLines[statIndex]! : "";
338
+ out.push(`${margin}${art}${" ".repeat(Math.max(0, artPad + gapWidth))}${stat}`);
321
339
  }
322
340
 
323
- // Output
324
- const out: string[] = [];
325
341
  out.push("");
326
- out.push(d(`╭${hLine}╮`));
327
- for (const bLine of bodyLines) {
328
- out.push(`${d("│")}${bLine}${d("│")}`);
329
- }
330
- out.push(d(`╰${hLine}╯`));
331
- out.push("");
332
-
333
342
  return out.join("\n");
334
343
  }
335
344