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 ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "ignorePatterns": []
3
+ }
package/.oxlintrc.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": [
4
+ "typescript",
5
+ "unicorn",
6
+ "oxc"
7
+ ],
8
+ "categories": {
9
+ "correctness": "error"
10
+ },
11
+ "rules": {},
12
+ "env": {
13
+ "builtin": true
14
+ }
15
+ }
@@ -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 and provides a full dashboard command.
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** showing:
8
- - Session input/output tokens & estimated cost
9
- - Live TPS (tokens per second) during streaming
10
- - Session runtime
11
- - Current model & git branch
12
- - Git diff stats (added/removed lines)
13
- - Context usage (current / max)
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** — Print a full observability dashboard with per-turn breakdowns and last 10 session history
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
+ ![pi-observability demo](./demo-preview.gif)
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
- ~/projects/my-app (main) +42 -7
23
- 12:34 ctx 4.2k/200k ↑1.2k ↓3.4k $0.0042 ⚡ 45.2 tok/s claude-sonnet-4
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/observability.ts` to `~/.pi/agent/extensions/observability.ts` (or `.pi/extensions/observability.ts` for project-local).
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` | Print full observability dashboard + last 10 sessions history |
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
- ## Dashboard Output
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
 
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
+ };