pi-observability 1.0.1 → 1.3.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/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +15 -0
- package/.zed/settings.json +222 -0
- package/README.md +98 -31
- package/demo-preview.gif +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 +95 -0
- package/extensions/lib/footer-engine/types.ts +56 -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 +653 -558
- 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,26 +1,101 @@
|
|
|
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
|
|
|
7
|
-
- **Live footer bar**
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
7
|
+
- **Live footer bar** — Fully customizable status bar with configurable segments, layout presets, and context-zone thresholds:
|
|
8
|
+
- **Model & thinking level** — Colors match pi's input field (off/low/medium/high). `xhigh`/`max` renders in rainbow
|
|
9
|
+
- **Session runtime**
|
|
10
|
+
- **Working directory** — Toggle between folder name or full path
|
|
11
|
+
- **Git branch & diff stats** — Added/removed lines
|
|
12
|
+
- **Context usage** — Progress bar + percentage + token count, with color-coded zones
|
|
13
|
+
- **Session tokens** — Input/output totals
|
|
14
|
+
- **Live TPS** — During streaming (chunk-based estimate)
|
|
15
|
+
- **Estimated cost**
|
|
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
|
|
|
23
|
+
- **`/obs-settings` command** — Interactive TUI for customizing the footer: choose from 4 layout presets or toggle individual segments and set context-usage warning thresholds
|
|
24
|
+
|
|
19
25
|
## Preview
|
|
20
26
|
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
### Footer
|
|
30
|
+
|
|
31
|
+
Compact single-line layout that falls back to two lines when the terminal is narrow:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
gpt-5.5:high ▸ ⏱ 12:34 ▸ 📁 my-app ▸ main +42 -7 ▸ ctx [████░░░░░░] 42% 4.2k/200k ▸ ↑1.2k ↓3.4k ▸ $0.0042
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
With `xhigh` or `max` thinking, the model name renders in rainbow:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
gpt-5.5:xhigh ▸ ⏱ 12:34 ▸ 📁 my-app ▸ ↑1.2k ↓3.4k ▸ $0.0042
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Settings
|
|
44
|
+
|
|
45
|
+
Run `/obs-settings` to open the interactive settings panel:
|
|
46
|
+
|
|
47
|
+
| Preset | Description |
|
|
48
|
+
|--------|-------------|
|
|
49
|
+
| `minimal` | Model, context usage (bar + numbers), cost only |
|
|
50
|
+
| `standard` | Everything except TPS (default) |
|
|
51
|
+
| `verbose` | All segments on |
|
|
52
|
+
| `performance` | Model, context %, TPS, cost |
|
|
53
|
+
|
|
54
|
+
Individual segments you can toggle:
|
|
55
|
+
|
|
56
|
+
- **Model & Thinking** — Model name + thinking level
|
|
57
|
+
- **Runtime** — Session timer
|
|
58
|
+
- **Working Directory** — Current folder or full path (`/obs-toggle-path`)
|
|
59
|
+
- **Git Branch & Diff** — Branch name + added/removed line counts
|
|
60
|
+
- **Context Usage** — Master toggle with 3 sub-options:
|
|
61
|
+
- Progress bar (`[████░░░░░░]`)
|
|
62
|
+
- Percentage
|
|
63
|
+
- Token count (`used/total`)
|
|
64
|
+
- **Session Tokens** — Total input/output
|
|
65
|
+
- **TPS** — Live during streaming, last-turn when idle
|
|
66
|
+
- **Cost** — Estimated session cost
|
|
67
|
+
|
|
68
|
+
Context-usage color zones (configurable):
|
|
69
|
+
|
|
70
|
+
| Zone | Default | Color |
|
|
71
|
+
|------|---------|-------|
|
|
72
|
+
| Normal | ≤ 70% | Green |
|
|
73
|
+
| Expert | 71–85% | Yellow |
|
|
74
|
+
| Warning | > 85% | Red |
|
|
75
|
+
|
|
76
|
+
### Dashboard (`/obs`)
|
|
77
|
+
|
|
21
78
|
```
|
|
22
|
-
|
|
23
|
-
|
|
79
|
+
┌──────────────────────────────────────────┐
|
|
80
|
+
│ Agent Observability Dashboard │
|
|
81
|
+
├──────────────────────────────────────────┤
|
|
82
|
+
│ Runtime: 12:34 Dir: ~/projects/my-app │
|
|
83
|
+
│ Branch: main Model: claude-sonnet-4 │
|
|
84
|
+
├──────────────────────────────────────────┤
|
|
85
|
+
│ Tokens: ↑1.2k ↓3.4k │
|
|
86
|
+
│ Cost: $0.004200 │
|
|
87
|
+
└──────────────────────────────────────────┘
|
|
88
|
+
|
|
89
|
+
TURNS (2)
|
|
90
|
+
# Input Output Time TPS Cost Model
|
|
91
|
+
─────────────────────────────────────────────────
|
|
92
|
+
1 ↑450 ↓1200 0:45 26.7 $0.00 claude-sonnet-4
|
|
93
|
+
2 ↑320 ↓900 0:32 28.1 $0.00 claude-sonnet-4
|
|
94
|
+
|
|
95
|
+
LAST 10 SESSIONS
|
|
96
|
+
When Duration Turns Input Output Cost
|
|
97
|
+
───────────────────────────────────────────────────────────
|
|
98
|
+
Apr 18, 04:19 PM 9:05 10 ↑110k ↓9.9k $0.00
|
|
24
99
|
```
|
|
25
100
|
|
|
26
101
|
## Install
|
|
@@ -39,34 +114,26 @@ pi install git:github.com/imran-vz/pi-observability
|
|
|
39
114
|
|
|
40
115
|
### Manual
|
|
41
116
|
|
|
42
|
-
Copy `extensions
|
|
117
|
+
Copy the entire `extensions/` directory to `~/.pi/agent/extensions/` (or `.pi/extensions/` for project-local):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cp -r extensions/* ~/.pi/agent/extensions/
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> **Note:** This extension is split into multiple files (`observability.ts` + `lib/`). Copying only the main file will break imports.
|
|
43
124
|
|
|
44
125
|
## Commands
|
|
45
126
|
|
|
46
127
|
| Command | Description |
|
|
47
128
|
|---------|-------------|
|
|
48
|
-
| `/obs` |
|
|
129
|
+
| `/obs` | Open full observability dashboard in TUI overlay |
|
|
49
130
|
| `/obs-toggle` | Toggle the observability footer on/off |
|
|
131
|
+
| `/obs-toggle-path` | Toggle between folder name and full path in footer |
|
|
132
|
+
| `/obs-settings` | Open interactive footer settings (presets, segments, context zones) |
|
|
50
133
|
|
|
51
|
-
##
|
|
134
|
+
## Migration from TPS
|
|
52
135
|
|
|
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
|
-
```
|
|
136
|
+
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
137
|
|
|
71
138
|
## Requirements
|
|
72
139
|
|
package/demo-preview.gif
ADDED
|
Binary file
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import type { ThemeColor } from "@earendil-works/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 "@earendil-works/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,95 @@
|
|
|
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, fastModeEnabled, serviceTier, theme } = input;
|
|
15
|
+
const text = `${model}:${thinkingLevel}`;
|
|
16
|
+
const tier = fastModeEnabled ? theme.fg("accent", ` ⚡${serviceTier ?? "fast"}`) : "";
|
|
17
|
+
if (thinkingLevel === "xhigh" || thinkingLevel === "max") {
|
|
18
|
+
return rainbowText(text) + tier;
|
|
19
|
+
}
|
|
20
|
+
return theme.fg(thinkingColor(thinkingLevel), text) + tier;
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
runtime(input) {
|
|
24
|
+
return input.theme.fg("dim", `⏱ ${fmtDuration(input.runtimeMs)}`);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
pwd(input) {
|
|
28
|
+
const path = input.showFullPath ? shortenPath(input.cwd) : basename(input.cwd);
|
|
29
|
+
return input.theme.fg("dim", `📁 ${path}`);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
git(input) {
|
|
33
|
+
const { gitBranch, gitDiffAdded, gitDiffRemoved, theme } = input;
|
|
34
|
+
if (!gitBranch) return "";
|
|
35
|
+
let text = theme.fg("dim", ` ${gitBranch}`);
|
|
36
|
+
if (gitDiffAdded > 0 || gitDiffRemoved > 0) {
|
|
37
|
+
text += ` ${theme.fg("success", `+${gitDiffAdded}`)} ${theme.fg("error", `-${gitDiffRemoved}`)}`;
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
contextUsage(input) {
|
|
43
|
+
const { contextUsage, theme, settings } = input;
|
|
44
|
+
if (!contextUsage || !contextUsage.contextWindow) return "";
|
|
45
|
+
|
|
46
|
+
const tokens = contextUsage.tokens || 0;
|
|
47
|
+
const max = contextUsage.contextWindow;
|
|
48
|
+
const pct = Math.min(100, Math.max(0, Math.round((tokens / max) * 100)));
|
|
49
|
+
|
|
50
|
+
let text = "ctx";
|
|
51
|
+
|
|
52
|
+
if (settings.segments.contextProgress) {
|
|
53
|
+
const barWidth = 10;
|
|
54
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
55
|
+
const empty = barWidth - filled;
|
|
56
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
57
|
+
text += ` [${bar}]`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (settings.segments.contextPercentage) {
|
|
61
|
+
text += ` ${pct}%`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (settings.segments.contextNumbers) {
|
|
65
|
+
text += ` ${fmtTokens(tokens)}/${fmtTokens(max)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return theme.fg(
|
|
69
|
+
contextUsageColor(pct, settings.contextZones.expert, settings.contextZones.warning),
|
|
70
|
+
text,
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
tokens(input) {
|
|
75
|
+
const { totalInputTokens, totalOutputTokens, theme } = input;
|
|
76
|
+
return theme.fg("dim", `↑${fmtTokens(totalInputTokens)} ↓${fmtTokens(totalOutputTokens)}`);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
tps(input) {
|
|
80
|
+
const { isStreaming, currentTurnStartTime, currentTurnUpdateCount, lastTurnTps, theme } = input;
|
|
81
|
+
if (isStreaming && currentTurnStartTime) {
|
|
82
|
+
const elapsed = (Date.now() - currentTurnStartTime) / 1000;
|
|
83
|
+
const liveTps = elapsed > 0 ? currentTurnUpdateCount / elapsed : 0;
|
|
84
|
+
return theme.fg("accent", `⚡${liveTps.toFixed(1)}`);
|
|
85
|
+
} else if (lastTurnTps > 0) {
|
|
86
|
+
return theme.fg("dim", `⚡${lastTurnTps.toFixed(1)}`);
|
|
87
|
+
}
|
|
88
|
+
return "";
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
cost(input) {
|
|
92
|
+
const { totalCost, theme } = input;
|
|
93
|
+
return theme.fg("dim", `$${totalCost.toFixed(4)}`);
|
|
94
|
+
},
|
|
95
|
+
};
|