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 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,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 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
 
@@ -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** — 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
 
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
+ [![Animated preview of pi-observability](./demo-preview.gif)](./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
- ~/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
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
+ ![Git diff in the footer status bar](./diff.png)
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/observability.ts` to `~/.pi/agent/extensions/observability.ts` (or `.pi/extensions/observability.ts` for project-local).
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` | Print full observability dashboard + last 10 sessions history |
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
- ## Dashboard Output
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
 
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
+ }