pi-observability 1.0.1 → 1.3.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/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +15 -0
- package/.zed/settings.json +222 -0
- package/README.md +66 -25
- package/demo-preview.gif +0 -0
- package/diff.png +0 -0
- package/extensions/lib/footer-engine/format.ts +67 -0
- package/extensions/lib/footer-engine/index.ts +55 -0
- package/extensions/lib/footer-engine/layout.ts +47 -0
- package/extensions/lib/footer-engine/segments.ts +94 -0
- package/extensions/lib/footer-engine/types.ts +53 -0
- package/extensions/lib/settings/domain.ts +161 -0
- package/extensions/lib/settings/index.ts +32 -0
- package/extensions/lib/settings/manager.ts +58 -0
- package/extensions/lib/settings/metadata.ts +114 -0
- package/extensions/lib/settings/storage.ts +38 -0
- package/extensions/lib/settings/tui.ts +44 -0
- package/extensions/lib/settings/types.ts +40 -0
- package/extensions/lib/storage/file-backend.ts +62 -0
- package/extensions/lib/storage/index.ts +33 -0
- package/extensions/lib/storage/json-store.ts +32 -0
- package/extensions/lib/storage/jsonl-store.ts +29 -0
- package/extensions/lib/storage/memory-backend.ts +37 -0
- package/extensions/lib/storage/types.ts +23 -0
- package/extensions/observability.ts +638 -557
- package/output.mp4 +0 -0
- package/package.json +55 -48
package/.oxfmtrc.json
ADDED
package/.oxlintrc.json
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lsp": {
|
|
3
|
+
"oxlint": {
|
|
4
|
+
"initialization_options": {
|
|
5
|
+
"settings": {
|
|
6
|
+
"configPath": "./.oxlintrc.json",
|
|
7
|
+
"run": "onType",
|
|
8
|
+
"disableNestedConfig": false,
|
|
9
|
+
"fixKind": "safe_fix",
|
|
10
|
+
"typeAware": true,
|
|
11
|
+
"unusedDisableDirectives": "deny"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"oxfmt": {
|
|
16
|
+
"initialization_options": {
|
|
17
|
+
"settings": {
|
|
18
|
+
"configPath": "./vite.config.ts",
|
|
19
|
+
"run": "onSave"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"languages": {
|
|
25
|
+
"CSS": {
|
|
26
|
+
"format_on_save": "on",
|
|
27
|
+
"prettier": {
|
|
28
|
+
"allowed": false
|
|
29
|
+
},
|
|
30
|
+
"formatter": [
|
|
31
|
+
{
|
|
32
|
+
"language_server": {
|
|
33
|
+
"name": "oxfmt"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"HTML": {
|
|
39
|
+
"format_on_save": "on",
|
|
40
|
+
"prettier": {
|
|
41
|
+
"allowed": false
|
|
42
|
+
},
|
|
43
|
+
"formatter": [
|
|
44
|
+
{
|
|
45
|
+
"language_server": {
|
|
46
|
+
"name": "oxfmt"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"JavaScript": {
|
|
52
|
+
"format_on_save": "on",
|
|
53
|
+
"prettier": {
|
|
54
|
+
"allowed": false
|
|
55
|
+
},
|
|
56
|
+
"formatter": [
|
|
57
|
+
{
|
|
58
|
+
"language_server": {
|
|
59
|
+
"name": "oxfmt"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"code_action": "source.fixAll.oxc"
|
|
64
|
+
},
|
|
65
|
+
"JSX": {
|
|
66
|
+
"format_on_save": "on",
|
|
67
|
+
"prettier": {
|
|
68
|
+
"allowed": false
|
|
69
|
+
},
|
|
70
|
+
"formatter": [
|
|
71
|
+
{
|
|
72
|
+
"language_server": {
|
|
73
|
+
"name": "oxfmt"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"JSON": {
|
|
79
|
+
"format_on_save": "on",
|
|
80
|
+
"prettier": {
|
|
81
|
+
"allowed": false
|
|
82
|
+
},
|
|
83
|
+
"formatter": [
|
|
84
|
+
{
|
|
85
|
+
"language_server": {
|
|
86
|
+
"name": "oxfmt"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"JSON5": {
|
|
92
|
+
"format_on_save": "on",
|
|
93
|
+
"prettier": {
|
|
94
|
+
"allowed": false
|
|
95
|
+
},
|
|
96
|
+
"formatter": [
|
|
97
|
+
{
|
|
98
|
+
"language_server": {
|
|
99
|
+
"name": "oxfmt"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"JSONC": {
|
|
105
|
+
"format_on_save": "on",
|
|
106
|
+
"prettier": {
|
|
107
|
+
"allowed": false
|
|
108
|
+
},
|
|
109
|
+
"formatter": [
|
|
110
|
+
{
|
|
111
|
+
"language_server": {
|
|
112
|
+
"name": "oxfmt"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"Less": {
|
|
118
|
+
"format_on_save": "on",
|
|
119
|
+
"prettier": {
|
|
120
|
+
"allowed": false
|
|
121
|
+
},
|
|
122
|
+
"formatter": [
|
|
123
|
+
{
|
|
124
|
+
"language_server": {
|
|
125
|
+
"name": "oxfmt"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"Markdown": {
|
|
131
|
+
"format_on_save": "on",
|
|
132
|
+
"prettier": {
|
|
133
|
+
"allowed": false
|
|
134
|
+
},
|
|
135
|
+
"formatter": [
|
|
136
|
+
{
|
|
137
|
+
"language_server": {
|
|
138
|
+
"name": "oxfmt"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"MDX": {
|
|
144
|
+
"format_on_save": "on",
|
|
145
|
+
"prettier": {
|
|
146
|
+
"allowed": false
|
|
147
|
+
},
|
|
148
|
+
"formatter": [
|
|
149
|
+
{
|
|
150
|
+
"language_server": {
|
|
151
|
+
"name": "oxfmt"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
"SCSS": {
|
|
157
|
+
"format_on_save": "on",
|
|
158
|
+
"prettier": {
|
|
159
|
+
"allowed": false
|
|
160
|
+
},
|
|
161
|
+
"formatter": [
|
|
162
|
+
{
|
|
163
|
+
"language_server": {
|
|
164
|
+
"name": "oxfmt"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
"TypeScript": {
|
|
170
|
+
"format_on_save": "on",
|
|
171
|
+
"prettier": {
|
|
172
|
+
"allowed": false
|
|
173
|
+
},
|
|
174
|
+
"formatter": [
|
|
175
|
+
{
|
|
176
|
+
"language_server": {
|
|
177
|
+
"name": "oxfmt"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
"TSX": {
|
|
183
|
+
"format_on_save": "on",
|
|
184
|
+
"prettier": {
|
|
185
|
+
"allowed": false
|
|
186
|
+
},
|
|
187
|
+
"formatter": [
|
|
188
|
+
{
|
|
189
|
+
"language_server": {
|
|
190
|
+
"name": "oxfmt"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
"Vue.js": {
|
|
196
|
+
"format_on_save": "on",
|
|
197
|
+
"prettier": {
|
|
198
|
+
"allowed": false
|
|
199
|
+
},
|
|
200
|
+
"formatter": [
|
|
201
|
+
{
|
|
202
|
+
"language_server": {
|
|
203
|
+
"name": "oxfmt"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
"YAML": {
|
|
209
|
+
"format_on_save": "on",
|
|
210
|
+
"prettier": {
|
|
211
|
+
"allowed": false
|
|
212
|
+
},
|
|
213
|
+
"formatter": [
|
|
214
|
+
{
|
|
215
|
+
"language_server": {
|
|
216
|
+
"name": "oxfmt"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🔭 pi-observability
|
|
2
2
|
|
|
3
|
-
A [pi](https://github.com/mariozechner/pi) extension that replaces the default footer with a live observability bar
|
|
3
|
+
A [pi](https://github.com/mariozechner/pi) extension that replaces the default footer with a live observability bar, provides a full dashboard command, and prints a TPS summary after each agent run.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -8,19 +8,69 @@ A [pi](https://github.com/mariozechner/pi) extension that replaces the default f
|
|
|
8
8
|
- Session input/output tokens & estimated cost
|
|
9
9
|
- Live TPS (tokens per second) during streaming
|
|
10
10
|
- Session runtime
|
|
11
|
-
- Current model & git branch
|
|
11
|
+
- Current model, thinking level, fast mode & git branch
|
|
12
12
|
- Git diff stats (added/removed lines)
|
|
13
13
|
- Context usage (current / max)
|
|
14
|
+
- **Thinking level colors match pi's input field** — off/low/medium/high use the same theme colors as the editor border
|
|
15
|
+
- **Rainbow mode** — `xhigh` and `max` thinking levels render the model indicator in cycling rainbow colors
|
|
14
16
|
|
|
15
|
-
- **`/obs` command** —
|
|
17
|
+
- **`/obs` command** — Full-screen TUI dashboard with per-turn breakdowns and last 10 session history. Renders through pi's native TUI (no console spam), with theme-aware borders and dynamic terminal width.
|
|
18
|
+
|
|
19
|
+
- **End-of-run TPS notification** — Prints the legacy TPS summary after each agent run: output TPS, input/output tokens, cache read/write tokens, total tokens, and elapsed time.
|
|
16
20
|
|
|
17
21
|
- **`/obs-toggle` command** — Toggle the live footer on/off
|
|
18
22
|
|
|
19
23
|
## Preview
|
|
20
24
|
|
|
25
|
+
### Screed recording
|
|
26
|
+
|
|
27
|
+
> GitHub does not render inline MP4 players in `README.md`, so here's a short animated preview. Click it to open the full recording.
|
|
28
|
+
|
|
29
|
+
[](./output.mp4)
|
|
30
|
+
|
|
31
|
+
[Open the full screed recording (MP4)](./output.mp4)
|
|
32
|
+
|
|
33
|
+
### Footer
|
|
34
|
+
|
|
35
|
+
Compact single-line layout that falls back to two lines when the terminal is narrow:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
gpt-5.5:high ▸ ⏱ 12:34 ▸ 📁 my-app ▸ main +42 -7 ▸ ctx 4.2k/200k ▸ ↑1.2k ↓3.4k ▸ ⚡45.2 ▸ $0.0042
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With `xhigh` or `max` thinking, the model name renders in rainbow:
|
|
42
|
+
|
|
21
43
|
```
|
|
22
|
-
|
|
23
|
-
|
|
44
|
+
gpt-5.5:xhigh ▸ ⏱ 12:34 ▸ 📁 my-app ▸ ↑1.2k ↓3.4k ▸ ⚡45.2 ▸ $0.0042
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Git diff in the status bar
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
### Dashboard (`/obs`)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
┌──────────────────────────────────────────┐
|
|
55
|
+
│ Agent Observability Dashboard │
|
|
56
|
+
├──────────────────────────────────────────┤
|
|
57
|
+
│ Runtime: 12:34 Dir: ~/projects/my-app │
|
|
58
|
+
│ Branch: main Model: claude-sonnet-4 │
|
|
59
|
+
├──────────────────────────────────────────┤
|
|
60
|
+
│ Tokens: ↑1.2k ↓3.4k │
|
|
61
|
+
│ Cost: $0.004200 │
|
|
62
|
+
└──────────────────────────────────────────┘
|
|
63
|
+
|
|
64
|
+
TURNS (2)
|
|
65
|
+
# Input Output Time TPS Cost Model
|
|
66
|
+
─────────────────────────────────────────────────
|
|
67
|
+
1 ↑450 ↓1200 0:45 26.7 $0.00 claude-sonnet-4
|
|
68
|
+
2 ↑320 ↓900 0:32 28.1 $0.00 claude-sonnet-4
|
|
69
|
+
|
|
70
|
+
LAST 10 SESSIONS
|
|
71
|
+
When Duration Turns Input Output Cost
|
|
72
|
+
───────────────────────────────────────────────────────────
|
|
73
|
+
Apr 18, 04:19 PM 9:05 10 ↑110k ↓9.9k $0.00
|
|
24
74
|
```
|
|
25
75
|
|
|
26
76
|
## Install
|
|
@@ -39,34 +89,25 @@ pi install git:github.com/imran-vz/pi-observability
|
|
|
39
89
|
|
|
40
90
|
### Manual
|
|
41
91
|
|
|
42
|
-
Copy `extensions
|
|
92
|
+
Copy the entire `extensions/` directory to `~/.pi/agent/extensions/` (or `.pi/extensions/` for project-local):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cp -r extensions/* ~/.pi/agent/extensions/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> **Note:** This extension is split into multiple files (`observability.ts` + `lib/`). Copying only the main file will break imports.
|
|
43
99
|
|
|
44
100
|
## Commands
|
|
45
101
|
|
|
46
102
|
| Command | Description |
|
|
47
103
|
|---------|-------------|
|
|
48
|
-
| `/obs` |
|
|
104
|
+
| `/obs` | Open full observability dashboard in TUI overlay |
|
|
49
105
|
| `/obs-toggle` | Toggle the observability footer on/off |
|
|
106
|
+
| `/obs-toggle-path` | Toggle between folder name and full path in footer |
|
|
50
107
|
|
|
51
|
-
##
|
|
108
|
+
## Migration from TPS
|
|
52
109
|
|
|
53
|
-
|
|
54
|
-
╔══════════════════════════════════════════════════════════════╗
|
|
55
|
-
║ 🕵️ Agent Observability Dashboard ║
|
|
56
|
-
╠══════════════════════════════════════════════════════════════╣
|
|
57
|
-
║ Runtime: 12:34 ║
|
|
58
|
-
║ Dir: ~/projects/my-app ║
|
|
59
|
-
║ Branch: main ║
|
|
60
|
-
║ Model: claude-sonnet-4 ║
|
|
61
|
-
╠══════════════════════════════════════════════════════════════╣
|
|
62
|
-
║ Tokens: ↑1.2k ↓3.4k ║
|
|
63
|
-
║ Cost: $0.004200 ║
|
|
64
|
-
╠══════════════════════════════════════════════════════════════╣
|
|
65
|
-
║ Turns: ║
|
|
66
|
-
║ #1 ↑450 ↓1200 0:45 26.7/s $0.0015 claude-sonne ║
|
|
67
|
-
║ #2 ↑320 ↓900 0:32 28.1/s $0.0012 claude-sonne ║
|
|
68
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
69
|
-
```
|
|
110
|
+
The standalone TPS extension is no longer required. pi-observability now includes its end-of-run TPS notification, so remove `~/.pi/agent/extensions/tps.ts` if it is installed to avoid duplicate notifications.
|
|
70
111
|
|
|
71
112
|
## Requirements
|
|
72
113
|
|
package/demo-preview.gif
ADDED
|
Binary file
|
package/diff.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import type { ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
export function fmtDuration(ms: number): string {
|
|
5
|
+
if (!Number.isFinite(ms) || ms < 0) ms = 0;
|
|
6
|
+
const s = Math.floor(ms / 1000);
|
|
7
|
+
const h = Math.floor(s / 3600);
|
|
8
|
+
const m = Math.floor((s % 3600) / 60);
|
|
9
|
+
const sec = s % 60;
|
|
10
|
+
if (h > 0) return `${h}:${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`;
|
|
11
|
+
return `${m}:${sec.toString().padStart(2, "0")}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function fmtTokens(n: number): string {
|
|
15
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
16
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
17
|
+
return `${n}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function shortenPath(p: string): string {
|
|
21
|
+
const home = homedir();
|
|
22
|
+
if (home && p.startsWith(home)) return p.replace(home, "~");
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function thinkingColor(level: string): ThemeColor {
|
|
27
|
+
switch (level) {
|
|
28
|
+
case "off":
|
|
29
|
+
return "thinkingOff";
|
|
30
|
+
case "minimal":
|
|
31
|
+
return "thinkingMinimal";
|
|
32
|
+
case "low":
|
|
33
|
+
return "thinkingLow";
|
|
34
|
+
case "medium":
|
|
35
|
+
return "thinkingMedium";
|
|
36
|
+
case "high":
|
|
37
|
+
return "thinkingHigh";
|
|
38
|
+
case "xhigh":
|
|
39
|
+
return "thinkingXhigh";
|
|
40
|
+
default:
|
|
41
|
+
return "thinkingOff";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function contextUsageColor(pct: number, expert: number, warning: number): ThemeColor {
|
|
46
|
+
if (pct <= expert) return "success";
|
|
47
|
+
if (pct <= warning) return "warning";
|
|
48
|
+
return "error";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function rainbowText(text: string): string {
|
|
52
|
+
const colors = [
|
|
53
|
+
"\x1b[38;2;255;0;0m", // red
|
|
54
|
+
"\x1b[38;2;255;127;0m", // orange
|
|
55
|
+
"\x1b[38;2;255;255;0m", // yellow
|
|
56
|
+
"\x1b[38;2;0;255;0m", // green
|
|
57
|
+
"\x1b[38;2;0;255;255m", // cyan
|
|
58
|
+
"\x1b[38;2;0;0;255m", // blue
|
|
59
|
+
"\x1b[38;2;255;0;255m", // magenta
|
|
60
|
+
];
|
|
61
|
+
let result = "";
|
|
62
|
+
for (let i = 0; i < text.length; i++) {
|
|
63
|
+
result += colors[i % colors.length] + text[i];
|
|
64
|
+
}
|
|
65
|
+
result += "\x1b[0m";
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SegmentKey,
|
|
3
|
+
FooterSettings,
|
|
4
|
+
FooterInput,
|
|
5
|
+
SegmentRenderer,
|
|
6
|
+
LayoutAssembler,
|
|
7
|
+
FooterEngineOptions,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
fmtDuration,
|
|
12
|
+
fmtTokens,
|
|
13
|
+
shortenPath,
|
|
14
|
+
thinkingColor,
|
|
15
|
+
contextUsageColor,
|
|
16
|
+
rainbowText,
|
|
17
|
+
} from "./format.js";
|
|
18
|
+
|
|
19
|
+
export { builtinRenderers } from "./segments.js";
|
|
20
|
+
export { defaultAssembler } from "./layout.js";
|
|
21
|
+
|
|
22
|
+
import { builtinRenderers } from "./segments.js";
|
|
23
|
+
import { defaultAssembler } from "./layout.js";
|
|
24
|
+
import type { FooterInput, FooterEngineOptions } from "./types.js";
|
|
25
|
+
|
|
26
|
+
export function renderFooter(input: FooterInput, width: number): string[] {
|
|
27
|
+
const segments: Record<string, string> = {};
|
|
28
|
+
for (const [key, renderer] of Object.entries(builtinRenderers)) {
|
|
29
|
+
if (input.settings.segments[key as keyof FooterInput["settings"]["segments"]]) {
|
|
30
|
+
segments[key] = renderer(input);
|
|
31
|
+
} else {
|
|
32
|
+
segments[key] = "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return defaultAssembler(segments, width, input.theme);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createFooterEngine(options: FooterEngineOptions) {
|
|
39
|
+
const segmentRenderers = { ...builtinRenderers, ...options.segments };
|
|
40
|
+
const assembler = options.layout ?? defaultAssembler;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
render(input: FooterInput, width: number): string[] {
|
|
44
|
+
const segments: Record<string, string> = {};
|
|
45
|
+
for (const [key, renderer] of Object.entries(segmentRenderers)) {
|
|
46
|
+
if (input.settings.segments[key as keyof FooterInput["settings"]["segments"]]) {
|
|
47
|
+
segments[key] = renderer(input);
|
|
48
|
+
} else {
|
|
49
|
+
segments[key] = "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return assembler(segments, width, input.theme);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { LayoutAssembler } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const defaultAssembler: LayoutAssembler = (segments, width, theme) => {
|
|
5
|
+
const sep = " " + theme.fg("dim", "▸") + " ";
|
|
6
|
+
|
|
7
|
+
const leftParts = [segments["modelThink"]].filter(Boolean);
|
|
8
|
+
const rightParts = [
|
|
9
|
+
segments["contextUsage"],
|
|
10
|
+
segments["tokens"],
|
|
11
|
+
segments["tps"],
|
|
12
|
+
segments["cost"],
|
|
13
|
+
].filter(Boolean);
|
|
14
|
+
const middleParts = [segments["runtime"], segments["pwd"], segments["git"]].filter(Boolean);
|
|
15
|
+
|
|
16
|
+
const leftStr = leftParts.join(sep);
|
|
17
|
+
const rightStr = rightParts.join(sep);
|
|
18
|
+
const middleStr = middleParts.join(sep);
|
|
19
|
+
|
|
20
|
+
const singleLine = middleStr
|
|
21
|
+
? leftStr + sep + middleStr + sep + rightStr
|
|
22
|
+
: leftStr + sep + rightStr;
|
|
23
|
+
|
|
24
|
+
if (visibleWidth(singleLine) <= width) {
|
|
25
|
+
const pad = width - visibleWidth(singleLine);
|
|
26
|
+
return [singleLine + " ".repeat(Math.max(0, pad))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fallback: two lines
|
|
30
|
+
function fitLine(parts: string[]): string {
|
|
31
|
+
const line = parts.filter(Boolean).join(sep);
|
|
32
|
+
const w = visibleWidth(line);
|
|
33
|
+
if (w < width) return line + " ".repeat(width - w);
|
|
34
|
+
if (w > width) return truncateToWidth(line, width);
|
|
35
|
+
return line;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const line1 = fitLine([segments["modelThink"], segments["pwd"], segments["git"]]);
|
|
39
|
+
const line2 = fitLine([
|
|
40
|
+
segments["runtime"],
|
|
41
|
+
segments["contextUsage"],
|
|
42
|
+
segments["tokens"],
|
|
43
|
+
segments["tps"],
|
|
44
|
+
segments["cost"],
|
|
45
|
+
]);
|
|
46
|
+
return [line1, line2];
|
|
47
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import type { SegmentRenderer } from "./types.js";
|
|
3
|
+
import {
|
|
4
|
+
fmtDuration,
|
|
5
|
+
fmtTokens,
|
|
6
|
+
shortenPath,
|
|
7
|
+
thinkingColor,
|
|
8
|
+
contextUsageColor,
|
|
9
|
+
rainbowText,
|
|
10
|
+
} from "./format.js";
|
|
11
|
+
|
|
12
|
+
export const builtinRenderers: Record<string, SegmentRenderer> = {
|
|
13
|
+
modelThink(input) {
|
|
14
|
+
const { model, thinkingLevel, theme } = input;
|
|
15
|
+
const text = `${model}:${thinkingLevel}`;
|
|
16
|
+
if (thinkingLevel === "xhigh" || thinkingLevel === "max") {
|
|
17
|
+
return rainbowText(text);
|
|
18
|
+
}
|
|
19
|
+
return theme.fg(thinkingColor(thinkingLevel), text);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
runtime(input) {
|
|
23
|
+
return input.theme.fg("dim", `⏱ ${fmtDuration(input.runtimeMs)}`);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
pwd(input) {
|
|
27
|
+
const path = input.showFullPath ? shortenPath(input.cwd) : basename(input.cwd);
|
|
28
|
+
return input.theme.fg("dim", `📁 ${path}`);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
git(input) {
|
|
32
|
+
const { gitBranch, gitDiffAdded, gitDiffRemoved, theme } = input;
|
|
33
|
+
if (!gitBranch) return "";
|
|
34
|
+
let text = theme.fg("dim", ` ${gitBranch}`);
|
|
35
|
+
if (gitDiffAdded > 0 || gitDiffRemoved > 0) {
|
|
36
|
+
text += ` ${theme.fg("success", `+${gitDiffAdded}`)} ${theme.fg("error", `-${gitDiffRemoved}`)}`;
|
|
37
|
+
}
|
|
38
|
+
return text;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
contextUsage(input) {
|
|
42
|
+
const { contextUsage, theme, settings } = input;
|
|
43
|
+
if (!contextUsage || !contextUsage.contextWindow) return "";
|
|
44
|
+
|
|
45
|
+
const tokens = contextUsage.tokens || 0;
|
|
46
|
+
const max = contextUsage.contextWindow;
|
|
47
|
+
const pct = Math.min(100, Math.max(0, Math.round((tokens / max) * 100)));
|
|
48
|
+
|
|
49
|
+
let text = "ctx";
|
|
50
|
+
|
|
51
|
+
if (settings.segments.contextProgress) {
|
|
52
|
+
const barWidth = 10;
|
|
53
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
54
|
+
const empty = barWidth - filled;
|
|
55
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
56
|
+
text += ` [${bar}]`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (settings.segments.contextPercentage) {
|
|
60
|
+
text += ` ${pct}%`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (settings.segments.contextNumbers) {
|
|
64
|
+
text += ` ${fmtTokens(tokens)}/${fmtTokens(max)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return theme.fg(
|
|
68
|
+
contextUsageColor(pct, settings.contextZones.expert, settings.contextZones.warning),
|
|
69
|
+
text,
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
tokens(input) {
|
|
74
|
+
const { totalInputTokens, totalOutputTokens, theme } = input;
|
|
75
|
+
return theme.fg("dim", `↑${fmtTokens(totalInputTokens)} ↓${fmtTokens(totalOutputTokens)}`);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
tps(input) {
|
|
79
|
+
const { isStreaming, currentTurnStartTime, currentTurnUpdateCount, lastTurnTps, theme } = input;
|
|
80
|
+
if (isStreaming && currentTurnStartTime) {
|
|
81
|
+
const elapsed = (Date.now() - currentTurnStartTime) / 1000;
|
|
82
|
+
const liveTps = elapsed > 0 ? currentTurnUpdateCount / elapsed : 0;
|
|
83
|
+
return theme.fg("accent", `⚡${liveTps.toFixed(1)}`);
|
|
84
|
+
} else if (lastTurnTps > 0) {
|
|
85
|
+
return theme.fg("dim", `⚡${lastTurnTps.toFixed(1)}`);
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
cost(input) {
|
|
91
|
+
const { totalCost, theme } = input;
|
|
92
|
+
return theme.fg("dim", `$${totalCost.toFixed(4)}`);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ContextUsage, Theme as PiTheme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type SegmentKey =
|
|
4
|
+
| "modelThink"
|
|
5
|
+
| "runtime"
|
|
6
|
+
| "pwd"
|
|
7
|
+
| "git"
|
|
8
|
+
| "contextUsage"
|
|
9
|
+
| "contextProgress"
|
|
10
|
+
| "contextPercentage"
|
|
11
|
+
| "contextNumbers"
|
|
12
|
+
| "tokens"
|
|
13
|
+
| "tps"
|
|
14
|
+
| "cost";
|
|
15
|
+
|
|
16
|
+
export interface FooterSettings {
|
|
17
|
+
segments: Record<SegmentKey, boolean>;
|
|
18
|
+
contextZones: { expert: number; warning: number };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FooterInput {
|
|
22
|
+
model: string;
|
|
23
|
+
thinkingLevel: string;
|
|
24
|
+
runtimeMs: number;
|
|
25
|
+
isStreaming: boolean;
|
|
26
|
+
currentTurnStartTime: number | null;
|
|
27
|
+
currentTurnUpdateCount: number;
|
|
28
|
+
lastTurnTps: number;
|
|
29
|
+
totalInputTokens: number;
|
|
30
|
+
totalOutputTokens: number;
|
|
31
|
+
totalCost: number;
|
|
32
|
+
contextUsage: ContextUsage | null;
|
|
33
|
+
cwd: string;
|
|
34
|
+
showFullPath: boolean;
|
|
35
|
+
gitBranch: string | null;
|
|
36
|
+
gitDiffAdded: number;
|
|
37
|
+
gitDiffRemoved: number;
|
|
38
|
+
settings: FooterSettings;
|
|
39
|
+
theme: PiTheme;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SegmentRenderer {
|
|
43
|
+
(input: FooterInput): string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LayoutAssembler {
|
|
47
|
+
(segments: Record<string, string>, width: number, theme: PiTheme): string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FooterEngineOptions {
|
|
51
|
+
segments?: Partial<Record<SegmentKey, SegmentRenderer>>;
|
|
52
|
+
layout?: LayoutAssembler;
|
|
53
|
+
}
|