pi-observability 1.0.0 → 1.0.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.
package/DEVELOPMENT.md ADDED
@@ -0,0 +1,243 @@
1
+ # Development Workflow
2
+
3
+ This guide covers how to develop, test, and publish the `pi-observability` extension.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Project Structure](#project-structure)
8
+ - [Local Development](#local-development)
9
+ - [Testing Changes](#testing-changes)
10
+ - [Publishing](#publishing)
11
+ - [Versioning](#versioning)
12
+ - [Troubleshooting](#troubleshooting)
13
+
14
+ ---
15
+
16
+ ## Project Structure
17
+
18
+ ```
19
+ pi-observability/
20
+ ├── extensions/
21
+ │ └── observability.ts # Main extension entry point
22
+ ├── package.json # Package manifest + pi config
23
+ ├── tsconfig.json # TypeScript config
24
+ ├── README.md # User-facing docs
25
+ ├── DEVELOPMENT.md # This file
26
+ └── LICENSE
27
+ ```
28
+
29
+ The `pi` key in `package.json` declares what pi loads:
30
+
31
+ ```json
32
+ {
33
+ "pi": {
34
+ "extensions": ["./extensions/observability.ts"]
35
+ }
36
+ }
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Local Development
42
+
43
+ ### 1. Clone and install
44
+
45
+ ```bash
46
+ git clone https://github.com/imran-vz/pi-observability.git
47
+ cd pi-observability
48
+ npm install
49
+ ```
50
+
51
+ ### 2. Link for live testing
52
+
53
+ The fastest way to iterate is to **symlink** the extension into pi's auto-discovery directory. This lets you edit the source file and hot-reload with `/reload`.
54
+
55
+ ```bash
56
+ npm run dev:link
57
+ ```
58
+
59
+ This creates a symlink:
60
+ ```
61
+ ~/.pi/agent/extensions/observability.ts → ./extensions/observability.ts
62
+ ```
63
+
64
+ > **Important:** If you previously had a copy (not symlink) at `~/.pi/agent/extensions/observability.ts`, this script removes it first.
65
+
66
+ ### 3. Start pi and test
67
+
68
+ ```bash
69
+ pi
70
+ ```
71
+
72
+ Make edits to `extensions/observability.ts`, then in pi run:
73
+
74
+ ```
75
+ /reload
76
+ ```
77
+
78
+ The extension reloads instantly. No need to restart pi.
79
+
80
+ ### 4. Unlink when done
81
+
82
+ ```bash
83
+ npm run dev:unlink
84
+ ```
85
+
86
+ This removes the symlink. To continue using the published version, reinstall:
87
+
88
+ ```bash
89
+ pi install npm:pi-observability
90
+ ```
91
+
92
+ ### Alternative: `-e` flag (quick tests)
93
+
94
+ For one-off testing without linking:
95
+
96
+ ```bash
97
+ pi -e ./extensions/observability.ts
98
+ ```
99
+
100
+ This loads the extension for that session only. Good for testing on a clean slate.
101
+
102
+ ### Alternative: Local path in settings
103
+
104
+ Add to `~/.pi/agent/settings.json`:
105
+
106
+ ```json
107
+ {
108
+ "extensions": ["/absolute/path/to/pi-observability/extensions/observability.ts"]
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Testing Changes
115
+
116
+ Before publishing, verify:
117
+
118
+ 1. **Type check:**
119
+ ```bash
120
+ npm run typecheck
121
+ ```
122
+
123
+ 2. **Load in pi:**
124
+ ```bash
125
+ npm run dev:link
126
+ pi
127
+ ```
128
+
129
+ 3. **Test all commands:**
130
+ - `/obs` — Dashboard prints correctly
131
+ - `/obs-toggle` — Footer toggles on/off
132
+ - Footer updates during streaming
133
+ - History persists across sessions
134
+
135
+ 4. **Test edge cases:**
136
+ - Non-git directories (diff stats should show 0)
137
+ - Very long paths (truncation works)
138
+ - Context window exceeded (usage display)
139
+ - Multiple models in one session
140
+
141
+ ---
142
+
143
+ ## Publishing
144
+
145
+ ### Prerequisites
146
+
147
+ - Logged into npm: `npm login`
148
+ - Write access to the GitHub repo
149
+ - Clean working tree: `git status`
150
+
151
+ ### Release workflow
152
+
153
+ **Patch release** (bug fixes):
154
+ ```bash
155
+ npm run version:patch # bumps 1.0.0 → 1.0.1, tags, pushes
156
+ npm run publish:pkg # publishes to npm
157
+ ```
158
+
159
+ **Minor release** (new features):
160
+ ```bash
161
+ npm run version:minor # bumps 1.0.0 → 1.1.0
162
+ npm run publish:pkg
163
+ ```
164
+
165
+ **Major release** (breaking changes):
166
+ ```bash
167
+ npm run version:major # bumps 1.0.0 → 2.0.0
168
+ npm run publish:pkg
169
+ ```
170
+
171
+ ### What `npm version` does
172
+
173
+ 1. Updates `version` in `package.json`
174
+ 2. Creates a git commit: `1.0.1`
175
+ 3. Creates a git tag: `v1.0.1`
176
+ 4. Pushes commit + tag to origin
177
+
178
+ ### After publishing
179
+
180
+ Users update with:
181
+ ```bash
182
+ pi update
183
+ ```
184
+
185
+ Or reinstall to get the latest:
186
+ ```bash
187
+ pi remove npm:pi-observability
188
+ pi install npm:pi-observability
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Versioning
194
+
195
+ We follow [SemVer](https://semver.org/):
196
+
197
+ | Version change | When to use |
198
+ |----------------|-------------|
199
+ | **Patch** `1.0.0 → 1.0.1` | Bug fixes, typo corrections, performance improvements |
200
+ | **Minor** `1.0.0 → 1.1.0` | New commands, new footer features, new metrics |
201
+ | **Major** `1.0.0 → 2.0.0` | Breaking changes (command renames, removed features) |
202
+
203
+ ---
204
+
205
+ ## Troubleshooting
206
+
207
+ ### Extension not loading after `/reload`
208
+
209
+ ```bash
210
+ # Check the symlink points to the right place
211
+ ls -la ~/.pi/agent/extensions/observability.ts
212
+
213
+ # If it's a copy instead of a symlink, remove and re-link
214
+ rm ~/.pi/agent/extensions/observability.ts
215
+ npm run dev:link
216
+ ```
217
+
218
+ ### Type errors from pi packages
219
+
220
+ Pi bundles its core packages at runtime. The `devDependencies` are only for IDE support. If TypeScript complains about missing modules during `npm run typecheck`, make sure you've run:
221
+
222
+ ```bash
223
+ npm install
224
+ ```
225
+
226
+ ### Published version not updating for users
227
+
228
+ npm has a TTL on package metadata. Users may need:
229
+ ```bash
230
+ pi remove npm:pi-observability
231
+ pi install npm:pi-observability
232
+ ```
233
+
234
+ Or wait a few minutes and run `pi update`.
235
+
236
+ ### Conflicts with local copy
237
+
238
+ If you have both the npm-installed version and a local symlink, pi may load both. Unlink during published-version testing:
239
+
240
+ ```bash
241
+ npm run dev:unlink
242
+ pi
243
+ ```
@@ -14,12 +14,20 @@
14
14
  * /obs-toggle - Toggle the observability footer on/off
15
15
  */
16
16
 
17
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
18
- import type { AssistantMessage } from "@mariozechner/pi-ai";
19
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
20
- import { homedir } from "node:os";
21
17
  import { mkdir, readFile, writeFile } from "node:fs/promises";
18
+ import { homedir } from "node:os";
22
19
  import { join } from "node:path";
20
+ import type { AssistantMessage } from "@mariozechner/pi-ai";
21
+ import type {
22
+ ExtensionAPI,
23
+ ExtensionContext,
24
+ } from "@mariozechner/pi-coding-agent";
25
+ import {
26
+ Key,
27
+ matchesKey,
28
+ truncateToWidth,
29
+ visibleWidth,
30
+ } from "@mariozechner/pi-tui";
23
31
 
24
32
  /* ───── Types ───── */
25
33
 
@@ -67,7 +75,8 @@ function fmtDuration(ms: number): string {
67
75
  const h = Math.floor(s / 3600);
68
76
  const m = Math.floor((s % 3600) / 60);
69
77
  const sec = s % 60;
70
- if (h > 0) return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
78
+ if (h > 0)
79
+ return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
71
80
  return `${m}:${sec.toString().padStart(2, "0")}`;
72
81
  }
73
82
 
@@ -103,6 +112,17 @@ function getSessionStartTime(ctx: ExtensionContext): number {
103
112
  return Date.now();
104
113
  }
105
114
 
115
+ function alignCell(
116
+ str: string,
117
+ width: number,
118
+ align: "left" | "right" = "left",
119
+ ): string {
120
+ const vis = visibleWidth(str);
121
+ if (vis > width) return truncateToWidth(str, width);
122
+ const pad = width - vis;
123
+ return align === "right" ? " ".repeat(pad) + str : str + " ".repeat(pad);
124
+ }
125
+
106
126
  /* ───── History persistence ───── */
107
127
 
108
128
  const HISTORY_DIR = join(homedir(), ".pi", "agent", "observability");
@@ -120,29 +140,163 @@ async function loadHistory(): Promise<SessionSummary[]> {
120
140
 
121
141
  async function saveHistory(sessions: SessionSummary[]): Promise<void> {
122
142
  await mkdir(HISTORY_DIR, { recursive: true });
123
- const text = sessions.map((s) => JSON.stringify(s)).join("\n") + "\n";
143
+ const text = `${sessions.map((s) => JSON.stringify(s)).join("\n")}\n`;
124
144
  await writeFile(HISTORY_FILE, text, "utf8");
125
145
  }
126
146
 
127
147
  /* ───── Dashboard formatting ───── */
128
148
 
129
- const BOX_W = 64; // total outer width including ║ borders
130
- const IN_W = BOX_W - 4; // inner width: "║ " + content + " ║"
149
+ type Theme = {
150
+ fg: (color: string, text: string) => string;
151
+ bold: (text: string) => string;
152
+ };
153
+
154
+ function buildDashboard(
155
+ state: SessionState,
156
+ ctx: ExtensionContext,
157
+ branch: string | null,
158
+ history: SessionSummary[],
159
+ termWidth: number,
160
+ theme: Theme,
161
+ ): string[] {
162
+ const runtime = Date.now() - state.startTime;
163
+ const totalIn = state.turns.reduce((s, t) => s + t.inputTokens, 0);
164
+ const totalOut = state.turns.reduce((s, t) => s + t.outputTokens, 0);
165
+ const totalCost = state.turns.reduce((s, t) => s + t.cost, 0);
166
+
167
+ const B = (s: string) => theme.fg("border", s);
168
+ const lines: string[] = [];
169
+
170
+ // ── Summary Card ──
171
+ const summaryLines = [
172
+ theme.bold("Agent Observability Dashboard"),
173
+ `Runtime: ${fmtDuration(runtime)} Dir: ${shortenPath(ctx.cwd)}`,
174
+ branch
175
+ ? `Branch: ${branch} Model: ${ctx.model?.id ?? "none"}`
176
+ : `Model: ${ctx.model?.id ?? "none"}`,
177
+ `Tokens: ↑${fmtTokens(totalIn)} ↓${fmtTokens(totalOut)}`,
178
+ `Cost: $${totalCost.toFixed(6)}`,
179
+ ];
180
+ const summaryW = Math.min(
181
+ Math.max(...summaryLines.map((c) => visibleWidth(c))) + 4,
182
+ termWidth,
183
+ );
184
+ const inner = summaryW - 4;
185
+ const padSummary = (text: string) => {
186
+ const safe = truncateToWidth(text, inner);
187
+ const vis = visibleWidth(safe);
188
+ const pad = Math.max(0, inner - vis);
189
+ return B("│ ") + safe + B(`${" ".repeat(pad)} │`);
190
+ };
131
191
 
132
- function boxTop(): string {
133
- return "╔" + "═".repeat(BOX_W - 2) + "╗";
134
- }
135
- function boxMid(): string {
136
- return "╠" + "═".repeat(BOX_W - 2) + "╣";
137
- }
138
- function boxBot(): string {
139
- return "╚" + "═".repeat(BOX_W - 2) + "╝";
140
- }
141
- function boxLine(text: string): string {
142
- const visible = visibleWidth(text);
143
- let pad = IN_W - visible;
144
- if (pad < 0) pad = 0;
145
- return "║ " + text + " ".repeat(pad) + " ║";
192
+ lines.push(B(`┌${"─".repeat(summaryW - 2)}┐`));
193
+ lines.push(padSummary(summaryLines[0]));
194
+ lines.push(B(`├${"─".repeat(summaryW - 2)}┤`));
195
+ for (let i = 1; i < summaryLines.length; i++) {
196
+ lines.push(padSummary(summaryLines[i]));
197
+ }
198
+ lines.push(B(`└${"─".repeat(summaryW - 2)}┘`));
199
+
200
+ // ── Turns Table ──
201
+ if (state.turns.length > 0) {
202
+ lines.push("");
203
+ lines.push(
204
+ ` ${theme.bold(theme.fg("accent", `TURNS (${state.turns.length})`))}`,
205
+ );
206
+
207
+ const headers = ["#", "Input", "Output", "Time", "TPS", "Cost", "Model"];
208
+ const rows = state.turns.map((t, i) => [
209
+ `${i + 1}`,
210
+ `↑${fmtTokens(t.inputTokens)}`,
211
+ `↓${fmtTokens(t.outputTokens)}`,
212
+ fmtDuration(t.durationMs),
213
+ `${t.tps.toFixed(1)}`,
214
+ `$${t.cost.toFixed(2)}`,
215
+ t.model,
216
+ ]);
217
+
218
+ const colW = headers.map((h, i) =>
219
+ Math.max(visibleWidth(h), ...rows.map((r) => visibleWidth(r[i]))),
220
+ );
221
+ const tableW = colW.reduce((a, b) => a + b, 0) + 2 * (colW.length - 1) + 2;
222
+ if (tableW > termWidth && colW[colW.length - 1]! > 10) {
223
+ colW[colW.length - 1] = Math.max(
224
+ 10,
225
+ colW[colW.length - 1]! - (tableW - termWidth),
226
+ );
227
+ }
228
+
229
+ const pad = " ";
230
+ const hdr = ` ${headers.map((h, i) => alignCell(h, colW[i]!)).join(pad)}`;
231
+ lines.push(theme.fg("dim", hdr));
232
+ lines.push(B(` ${"─".repeat(visibleWidth(hdr) - 2)}`));
233
+ for (const row of rows) {
234
+ const cells = row.map((c, i) =>
235
+ alignCell(c, colW[i]!, i === 0 || i >= 3 ? "left" : "right"),
236
+ );
237
+ lines.push(` ${cells.join(pad)}`);
238
+ }
239
+ }
240
+
241
+ // ── History Table ──
242
+ if (history.length > 0) {
243
+ lines.push("");
244
+ lines.push(` ${theme.bold(theme.fg("accent", "LAST 10 SESSIONS"))}`);
245
+
246
+ const headers = [
247
+ "When",
248
+ "Duration",
249
+ "Turns",
250
+ "Input",
251
+ "Output",
252
+ "Cost",
253
+ "Model",
254
+ ];
255
+ const rows = history
256
+ .slice()
257
+ .reverse()
258
+ .map((h) => {
259
+ const date = new Date(h.endedAt).toLocaleDateString("en-US", {
260
+ month: "short",
261
+ day: "numeric",
262
+ hour: "2-digit",
263
+ minute: "2-digit",
264
+ });
265
+ return [
266
+ date,
267
+ fmtDuration(h.runtimeMs),
268
+ `${h.turns}`,
269
+ `↑${fmtTokens(h.inputTokens)}`,
270
+ `↓${fmtTokens(h.outputTokens)}`,
271
+ `$${h.cost.toFixed(2)}`,
272
+ h.model,
273
+ ];
274
+ });
275
+
276
+ const colW = headers.map((h, i) =>
277
+ Math.max(visibleWidth(h), ...rows.map((r) => visibleWidth(r[i]))),
278
+ );
279
+ const tableW = colW.reduce((a, b) => a + b, 0) + 2 * (colW.length - 1) + 2;
280
+ if (tableW > termWidth && colW[colW.length - 1]! > 10) {
281
+ colW[colW.length - 1] = Math.max(
282
+ 10,
283
+ colW[colW.length - 1]! - (tableW - termWidth),
284
+ );
285
+ }
286
+
287
+ const pad = " ";
288
+ const hdr = ` ${headers.map((h, i) => alignCell(h, colW[i]!)).join(pad)}`;
289
+ lines.push(theme.fg("dim", hdr));
290
+ lines.push(B(` ${"─".repeat(visibleWidth(hdr) - 2)}`));
291
+ for (const row of rows) {
292
+ const cells = row.map((c, i) =>
293
+ alignCell(c, colW[i]!, i === 0 || i >= 2 ? "left" : "right"),
294
+ );
295
+ lines.push(` ${cells.join(pad)}`);
296
+ }
297
+ }
298
+
299
+ return lines;
146
300
  }
147
301
 
148
302
  /* ───── Extension ───── */
@@ -202,7 +356,8 @@ export default function (pi: ExtensionAPI) {
202
356
  }
203
357
  }
204
358
 
205
- const tps = duration > 0 && outputTokens >= 0 ? outputTokens / (duration / 1000) : 0;
359
+ const tps =
360
+ duration > 0 && outputTokens >= 0 ? outputTokens / (duration / 1000) : 0;
206
361
 
207
362
  const record: TurnRecord = {
208
363
  turnIndex: event.turnIndex,
@@ -236,7 +391,6 @@ export default function (pi: ExtensionAPI) {
236
391
  try {
237
392
  const result = await pi.exec("git", ["branch", "--show-current"], {
238
393
  cwd: ctx.cwd,
239
- throwOnError: false,
240
394
  });
241
395
  branch = result.stdout?.trim() || null;
242
396
  } catch {
@@ -272,7 +426,6 @@ export default function (pi: ExtensionAPI) {
272
426
  try {
273
427
  const result = await pi.exec("git", ["diff", "--numstat"], {
274
428
  cwd: ctx.cwd,
275
- throwOnError: false,
276
429
  });
277
430
  if (result.code !== 0 || !result.stdout) {
278
431
  diffAdded = 0;
@@ -341,16 +494,23 @@ export default function (pi: ExtensionAPI) {
341
494
 
342
495
  const ctxUsage = ctx.getContextUsage();
343
496
  const segCtx = ctxUsage
344
- ? theme.fg("dim", `ctx ${fmtTokens(ctxUsage.tokens)}/${fmtTokens(ctxUsage.contextWindow)}`)
497
+ ? theme.fg(
498
+ "dim",
499
+ `ctx ${fmtTokens(ctxUsage.tokens || 0)}/${fmtTokens(ctxUsage.contextWindow)}`,
500
+ )
345
501
  : "";
346
502
 
347
- const segTokens = theme.fg("dim", `↑${fmtTokens(totalIn)} ↓${fmtTokens(totalOut)}`);
503
+ const segTokens = theme.fg(
504
+ "dim",
505
+ `↑${fmtTokens(totalIn)} ↓${fmtTokens(totalOut)}`,
506
+ );
348
507
  const segCost = theme.fg("dim", `$${totalCost.toFixed(4)}`);
349
508
 
350
509
  let segTps = "";
351
510
  if (state.isStreaming && state.currentTurnStartTime) {
352
511
  const elapsed = (Date.now() - state.currentTurnStartTime) / 1000;
353
- const liveTps = elapsed > 0 ? state.currentTurnUpdateCount / elapsed : 0;
512
+ const liveTps =
513
+ elapsed > 0 ? state.currentTurnUpdateCount / elapsed : 0;
354
514
  segTps = theme.fg("accent", `⚡ ${liveTps.toFixed(1)} tok/s`);
355
515
  } else if (state.turns.length > 0) {
356
516
  const last = state.turns[state.turns.length - 1];
@@ -359,7 +519,9 @@ export default function (pi: ExtensionAPI) {
359
519
 
360
520
  const segModel = theme.fg("dim", model);
361
521
 
362
- const leftRaw = [segRuntime, segCtx, segTokens, segCost, segTps].filter(Boolean).join(" ");
522
+ const leftRaw = [segRuntime, segCtx, segTokens, segCost, segTps]
523
+ .filter(Boolean)
524
+ .join(" ");
363
525
  const leftW = visibleWidth(leftRaw);
364
526
  const rightW = visibleWidth(segModel);
365
527
 
@@ -368,7 +530,7 @@ export default function (pi: ExtensionAPI) {
368
530
  if (gap >= 1) {
369
531
  line2 = leftRaw + " ".repeat(gap) + segModel;
370
532
  } else {
371
- line2 = leftRaw + " " + segModel;
533
+ line2 = `${leftRaw} ${segModel}`;
372
534
  }
373
535
  line2 = truncateToWidth(line2, width);
374
536
 
@@ -385,84 +547,59 @@ export default function (pi: ExtensionAPI) {
385
547
  /* ─── Commands ─── */
386
548
 
387
549
  pi.registerCommand("obs", {
388
- description: "Show observability dashboard (tokens, cost, TPS, runtime, history)",
550
+ description:
551
+ "Show observability dashboard (tokens, cost, TPS, runtime, history)",
389
552
  handler: async (_args, ctx) => {
390
- const lines: string[] = [];
391
- const runtime = Date.now() - state.startTime;
392
-
393
553
  const branchResult = await pi.exec("git", ["branch", "--show-current"], {
394
554
  cwd: ctx.cwd,
395
- throwOnError: false,
396
555
  });
397
556
  const branch = branchResult.stdout?.trim() || null;
398
-
399
- const totalIn = state.turns.reduce((s, t) => s + t.inputTokens, 0);
400
- const totalOut = state.turns.reduce((s, t) => s + t.outputTokens, 0);
401
- const totalCost = state.turns.reduce((s, t) => s + t.cost, 0);
402
-
403
- // ── Current Session ──
404
- lines.push("");
405
- lines.push(boxTop());
406
- lines.push(boxLine("🕵️ Agent Observability Dashboard"));
407
- lines.push(boxMid());
408
- lines.push(boxLine(`Runtime: ${fmtDuration(runtime)}`));
409
- lines.push(boxLine(`Dir: ${shortenPath(ctx.cwd)}`));
410
- if (branch) lines.push(boxLine(`Branch: ${branch}`));
411
- lines.push(boxLine(`Model: ${ctx.model?.id ?? "none"}`));
412
- lines.push(boxMid());
413
- lines.push(boxLine(`Tokens: ↑${fmtTokens(totalIn)} ↓${fmtTokens(totalOut)}`));
414
- lines.push(boxLine(`Cost: $${totalCost.toFixed(6)}`));
415
-
416
- if (state.turns.length > 0) {
417
- lines.push(boxMid());
418
- lines.push(boxLine("Turns:"));
419
- for (let i = 0; i < state.turns.length; i++) {
420
- const t = state.turns[i];
421
- const parts = [
422
- `#${i + 1}`,
423
- `↑${fmtTokens(t.inputTokens)}`,
424
- `↓${fmtTokens(t.outputTokens)}`,
425
- fmtDuration(t.durationMs),
426
- `${t.tps.toFixed(1)}/s`,
427
- `$${t.cost.toFixed(2)}`,
428
- t.model.slice(0, 14),
429
- ];
430
- lines.push(boxLine(parts.join(" ")));
431
- }
432
- }
433
- lines.push(boxBot());
434
-
435
- // ── History ──
436
557
  const history = await loadHistory();
437
- if (history.length > 0) {
438
- lines.push("");
439
- lines.push(boxTop());
440
- lines.push(boxLine("📜 Last 10 Sessions"));
441
- lines.push(boxMid());
442
- for (const h of history.slice().reverse()) {
443
- const date = new Date(h.endedAt).toLocaleDateString("en-US", {
444
- month: "short",
445
- day: "numeric",
446
- hour: "2-digit",
447
- minute: "2-digit",
448
- });
449
- const parts = [
450
- date,
451
- fmtDuration(h.runtimeMs),
452
- `${h.turns}t`,
453
- `↑${fmtTokens(h.inputTokens)}`,
454
- `↓${fmtTokens(h.outputTokens)}`,
455
- `$${h.cost.toFixed(2)}`,
456
- h.model.slice(0, 10),
457
- ];
458
- lines.push(boxLine(parts.join(" ")));
459
- }
460
- lines.push(boxBot());
461
- }
462
558
 
463
- lines.push("");
464
- console.log(lines.join("\n"));
465
- ctx.ui.notify("Observability dashboard printed to console", "info");
559
+ await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
560
+ let cachedWidth = 0;
561
+ let cachedLines: string[] = [];
562
+
563
+ return {
564
+ invalidate() {
565
+ cachedWidth = 0;
566
+ cachedLines = [];
567
+ },
568
+ handleInput(data: string) {
569
+ if (
570
+ matchesKey(data, Key.escape) ||
571
+ matchesKey(data, Key.enter) ||
572
+ matchesKey(data, Key.space)
573
+ ) {
574
+ done();
575
+ }
576
+ },
577
+ render(width: number): string[] {
578
+ if (cachedWidth === width && cachedLines.length > 0) {
579
+ return cachedLines;
580
+ }
581
+
582
+ cachedLines = buildDashboard(
583
+ state,
584
+ ctx,
585
+ branch,
586
+ history,
587
+ width,
588
+ theme,
589
+ );
590
+
591
+ // Add hint at bottom
592
+ const hint = theme.fg("dim", "Press ESC or Enter to close");
593
+ const hintVisible = visibleWidth(hint);
594
+ const pad = Math.max(0, width - hintVisible);
595
+ cachedLines.push("");
596
+ cachedLines.push(hint + " ".repeat(pad));
597
+
598
+ cachedWidth = width;
599
+ return cachedLines;
600
+ },
601
+ };
602
+ });
466
603
  },
467
604
  });
468
605
 
package/package.json CHANGED
@@ -1,41 +1,50 @@
1
1
  {
2
- "name": "pi-observability",
3
- "version": "1.0.0",
4
- "description": "Live observability dashboard for pi coding agent sessions — tokens, cost, TPS, runtime, git stats, and context usage",
5
- "keywords": [
6
- "pi-package",
7
- "pi-extension",
8
- "observability",
9
- "dashboard",
10
- "tokens",
11
- "cost-tracking",
12
- "tps",
13
- "cli"
14
- ],
15
- "author": "",
16
- "license": "MIT",
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/imran-vz/pi-observability.git"
20
- },
21
- "bugs": {
22
- "url": "https://github.com/imran-vz/pi-observability/issues"
23
- },
24
- "homepage": "https://github.com/imran-vz/pi-observability#readme",
25
- "pi": {
26
- "extensions": [
27
- "./extensions/observability.ts"
28
- ]
29
- },
30
- "peerDependencies": {
31
- "@mariozechner/pi-coding-agent": "*",
32
- "@mariozechner/pi-ai": "*",
33
- "@mariozechner/pi-tui": "*"
34
- },
35
- "devDependencies": {
36
- "@mariozechner/pi-coding-agent": "^0.1.0",
37
- "@mariozechner/pi-ai": "^0.1.0",
38
- "@mariozechner/pi-tui": "^0.1.0",
39
- "typescript": "^5.4.0"
40
- }
2
+ "name": "pi-observability",
3
+ "version": "1.0.1",
4
+ "description": "Live observability dashboard for pi coding agent sessions — tokens, cost, TPS, runtime, git stats, and context usage",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "observability",
9
+ "dashboard",
10
+ "tokens",
11
+ "cost-tracking",
12
+ "tps",
13
+ "cli"
14
+ ],
15
+ "author": "",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/imran-vz/pi-observability.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/imran-vz/pi-observability/issues"
23
+ },
24
+ "homepage": "https://github.com/imran-vz/pi-observability#readme",
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit",
27
+ "dev:link": "node -e \"const fs=require('fs'),path=require('path'),src=path.resolve('extensions/observability.ts'),dest=path.join(require('os').homedir(),'.pi/agent/extensions/observability.ts');fs.existsSync(dest)&&fs.unlinkSync(dest);fs.symlinkSync(src,dest);console.log('Linked to',dest);\"",
28
+ "dev:unlink": "node -e \"const fs=require('fs'),path=require('path'),dest=path.join(require('os').homedir(),'.pi/agent/extensions/observability.ts');fs.existsSync(dest)&&(fs.unlinkSync(dest),console.log('Unlinked',dest));\"",
29
+ "version:patch": "npm version patch && git push --follow-tags",
30
+ "version:minor": "npm version minor && git push --follow-tags",
31
+ "version:major": "npm version major && git push --follow-tags",
32
+ "publish:pkg": "npm publish"
33
+ },
34
+ "pi": {
35
+ "extensions": [
36
+ "./extensions/observability.ts"
37
+ ]
38
+ },
39
+ "peerDependencies": {
40
+ "@mariozechner/pi-coding-agent": "*",
41
+ "@mariozechner/pi-ai": "*",
42
+ "@mariozechner/pi-tui": "*"
43
+ },
44
+ "devDependencies": {
45
+ "@mariozechner/pi-coding-agent": "latest",
46
+ "@mariozechner/pi-ai": "latest",
47
+ "@mariozechner/pi-tui": "latest",
48
+ "typescript": "^5.4.0"
49
+ }
41
50
  }
package/tsconfig.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "noEmit": true,
11
- "types": ["node"]
12
- },
13
- "include": ["extensions/**/*.ts"]
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "noEmit": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": ["extensions/**/*.ts"]
14
14
  }