@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.
- package/README.md +12 -29
- package/package.json +1 -1
- 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
|
|
5
|
+
When you hit `/quit` (or Ctrl+C twice), pi exits and you'll see a compact summary like:
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
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
|
-
//
|
|
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: "
|
|
45
|
-
{ text: "
|
|
46
|
-
{ text: "
|
|
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:
|
|
229
|
+
rows.push({ type: "info", label: d("Session"), value: b(truncate(s.sessionName, 42)) });
|
|
213
230
|
}
|
|
214
|
-
rows.push({ type: "info", label:
|
|
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)
|
|
218
|
-
: truncate(s.model,
|
|
219
|
-
rows.push({ type: "info", label:
|
|
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:
|
|
227
|
-
value: `${g(String(s.userMessages))} user
|
|
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,
|
|
234
|
-
.map(([
|
|
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:
|
|
239
|
-
value: `${b(String(s.toolCalls))}
|
|
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: "
|
|
247
|
-
rows.push({
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
294
|
-
return `${lPad} ${row.value}`;
|
|
299
|
+
return `${padTo(row.label, maxLabelW)} ${row.value}`;
|
|
295
300
|
});
|
|
296
301
|
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
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
|
-
|
|
306
|
-
const
|
|
307
|
-
const
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|