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 +243 -0
- package/extensions/observability.ts +238 -101
- package/package.json +48 -39
- package/tsconfig.json +12 -12
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)
|
|
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")
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 =
|
|
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(
|
|
497
|
+
? theme.fg(
|
|
498
|
+
"dim",
|
|
499
|
+
`ctx ${fmtTokens(ctxUsage.tokens || 0)}/${fmtTokens(ctxUsage.contextWindow)}`,
|
|
500
|
+
)
|
|
345
501
|
: "";
|
|
346
502
|
|
|
347
|
-
const segTokens = theme.fg(
|
|
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 =
|
|
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]
|
|
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
|
|
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:
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
}
|