@herbertgao/pi-extensions 2026.8.3 → 2026.8.4

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.
Files changed (122) hide show
  1. package/README.md +41 -0
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  4. package/node_modules/pi-footer/LICENSE +21 -0
  5. package/node_modules/pi-footer/README.md +354 -0
  6. package/node_modules/pi-footer/package.json +68 -0
  7. package/node_modules/pi-footer/src/cache.ts +106 -0
  8. package/node_modules/pi-footer/src/colors.ts +237 -0
  9. package/node_modules/pi-footer/src/config.ts +202 -0
  10. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  11. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  12. package/node_modules/pi-footer/src/git.ts +135 -0
  13. package/node_modules/pi-footer/src/index.ts +268 -0
  14. package/node_modules/pi-footer/src/metrics.ts +141 -0
  15. package/node_modules/pi-footer/src/presets.ts +383 -0
  16. package/node_modules/pi-footer/src/render.ts +114 -0
  17. package/node_modules/pi-footer/src/separators.ts +52 -0
  18. package/node_modules/pi-footer/src/types.ts +122 -0
  19. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  20. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  21. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  22. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  23. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  24. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  25. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  26. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  27. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  28. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  29. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  30. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  31. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  32. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  33. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  34. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  35. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  36. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  37. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  38. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  39. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  40. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  41. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  42. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  43. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  44. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  45. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  46. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  47. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  48. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  49. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  50. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  51. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  52. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  53. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  54. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  55. package/node_modules/pi-footer/src/ui.ts +66 -0
  56. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  57. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  58. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  59. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  60. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  61. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  62. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  63. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  64. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  65. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  66. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  67. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  68. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  69. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  70. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  71. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  72. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  73. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  74. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  75. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  76. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  77. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  78. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  79. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  80. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  81. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  82. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  83. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  84. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  85. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  86. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  87. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  88. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  89. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  90. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  91. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  92. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  93. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  94. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  95. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  96. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  97. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  99. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  100. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  102. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  103. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  104. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  105. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  106. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  107. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  108. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  109. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  110. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  111. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  112. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  113. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  114. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  115. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  116. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  117. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  118. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  119. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  120. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  121. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  122. package/package.json +7 -2
@@ -0,0 +1,354 @@
1
+ # pi-footer
2
+
3
+ A configurable, Ultimate multi-line footer/statusline extension for [`pi`](https://pi.dev).
4
+
5
+ ![pi-footer](https://raw.githubusercontent.com/wobondar/pi-footer/main/assets/statusline-custom.png)
6
+
7
+ `pi-footer` is built for people who live in the terminal and want their agent UI to expose useful state at a glance: model, provider, context, tokens, cost, git state, session activity, extension statuses, and custom values published by other extensions.
8
+
9
+ ![pi-footer teaser](https://raw.githubusercontent.com/wobondar/pi-footer/main/assets/demo-teaser.gif)
10
+
11
+ ## Why use it?
12
+
13
+ - **Make pi feel like your editor.** Pick a compact, plain, or powerline-style footer and tune every segment.
14
+ - **See important agent state without opening menus.** Model, reasoning level, verbosity, context usage, token counts, cost, git info, session stats, and more.
15
+ - **Give other extensions a place to speak.** Extensions can publish values through `pi.events` or `ctx.ui.setStatus(...)`, and users decide where those values appear.
16
+ - **Experiment safely.** The config UI gives a live preview, but only writes to disk when you explicitly save.
17
+
18
+ ### Complete demo video
19
+
20
+ <details>
21
+ <summary>Click to expand video</summary>
22
+
23
+ <video src="https://github.com/user-attachments/assets/abba51c6-9880-4bde-b7fa-849544b07329" controls muted width="100%"></video>
24
+
25
+ </details>
26
+
27
+ ## Install
28
+
29
+ Install extension from npm:
30
+
31
+ ```bash
32
+ pi install npm:pi-footer
33
+ ```
34
+
35
+ Try it for one run without installing:
36
+
37
+ ```bash
38
+ pi -e npm:pi-footer
39
+ ```
40
+
41
+ Local development checkout:
42
+
43
+ ```bash
44
+ npm install
45
+ pi -e ./src/index.ts
46
+ ```
47
+
48
+ ## Quick start
49
+
50
+ Open the configuration UI:
51
+
52
+ ```text
53
+ /footer
54
+ ```
55
+
56
+ Try a preset:
57
+
58
+ ```text
59
+ /footer preset powerline
60
+ /footer preset powerline-bright
61
+ /footer preset powerline-blocks
62
+ /footer preset powerline-mono
63
+ /footer preset pi-footer
64
+ /footer preset compact
65
+ ```
66
+
67
+ Other quick commands:
68
+
69
+ ```text
70
+ /footer on
71
+ /footer off
72
+ /footer reset
73
+ ```
74
+
75
+ Settings are persisted to:
76
+
77
+ ```text
78
+ ~/.pi/agent/extensions/pi-footer.json
79
+ ```
80
+
81
+ Override with `PI_FOOTER_CONFIG=/path/to/settings.json`.
82
+
83
+ ## Presets
84
+
85
+ Presets are starting points. After applying one, you can edit lines, widgets, separators, colors, icons, and terminal behavior in the config UI.
86
+
87
+ | Preset | Best for | Notes |
88
+ | --- | --- | --- |
89
+ | `default` | Everyday use | Balanced model/context/git/cost/session layout. |
90
+ | `compact` | Narrow terminals | Uses full width minus 40 columns and a tighter widget set. |
91
+ | `powerline` | A clean colored powerline footer | Uses Nerd Font icons, explicit colored separator widgets, and no global separator. |
92
+ | `powerline-bright` | A colorful two-line powerline layout | Inspired by ccstatusline-style bright blocks. |
93
+ | `powerline-blocks` | Multi-line blocky powerline layouts | Demonstrates multiline styling, token rows, and context blocks. |
94
+ | `powerline-mono` | High-contrast monochrome powerline | Gray/black palette inspired by terminal powerline themes. |
95
+ | `git-heavy` | Git-heavy workflows | Emphasizes cwd, branch, sha, status, diff, and upstream info. |
96
+ | `pi-footer` | Default pi-like footer | Dimmed with pi theme colors; uses full cwd, bracketed branch, session name, pi-style token/cost/cache formatting, context percentage/window, subscription marker, and right-aligned model/thinking. |
97
+ | `demo` | Preset gallery | Demo preset combines `pi-footer` and **powerline** presets. |
98
+ | `demo-standard` | Standard preset gallery | Demo preset for the standard non-powerline presets. |
99
+
100
+ **demo preset:**
101
+
102
+ ![Demo preset gallery](https://raw.githubusercontent.com/wobondar/pi-footer/main/assets/preset-demo.png)
103
+
104
+ **demo-standard preset:**
105
+
106
+ ![Demo standard preset gallery](https://raw.githubusercontent.com/wobondar/pi-footer/main/assets/preset-demo-standard.png)
107
+
108
+ The `pi-footer` preset is intended to look like pi's built-in footer while staying editable. It follows the active pi theme.
109
+
110
+ Powerline presets look best with a Nerd Font-compatible terminal font.
111
+
112
+ ## Configuration UI
113
+
114
+ ```text
115
+ /footer
116
+ ```
117
+
118
+ Main menu:
119
+
120
+ - **Edit lines** — add, clone, move, delete, and select pi-footer rows.
121
+ - **Edit colors** — choose a line, then configure widget foreground/background/bold.
122
+ - **Terminal Options** — configure terminal width behavior and color level.
123
+ - **Global Overrides** — choose presets, separators, icon mode, minimalist mode, and global separator colors.
124
+ - **Pi extensions** — choose which published extension statuses appear in the extension status row.
125
+ - **Save & Exit** — explicitly save the current configuration.
126
+ - **Exit without saving** — discard changes and restore the last saved config.
127
+
128
+ Common keys:
129
+
130
+ | Key | Action |
131
+ | --- | --- |
132
+ | `↑` / `↓` | Select item |
133
+ | `page up` / `page down` | Jump through long lists |
134
+ | `enter` | Open/select/change |
135
+ | `a` | Add line/widget |
136
+ | `c` | Clone line/widget |
137
+ | `w` / `s` | Move line/widget up/down |
138
+ | `d` | Delete line/widget |
139
+ | `space` | Enable/disable widget |
140
+ | `r` | Toggle raw value in the widget list |
141
+ | `ctrl+s` | Save without closing |
142
+ | `esc` | Back/close; dirty exit asks for confirmation |
143
+
144
+ ### Save/discard behavior
145
+
146
+ The UI updates the live preview immediately while you edit. Disk writes only happen when you explicitly save:
147
+
148
+ - `ctrl+s`
149
+ - `Save & Exit`
150
+ - confirm save from the unsaved-changes dialog
151
+
152
+ If you exit without saving, the runtime config is restored to the last saved config.
153
+
154
+ The title shows inline config state: `Unsaved`, `Saving…`, `Saved`.
155
+
156
+ ## Widgets
157
+
158
+ Widgets are instance-based. You can add the same widget multiple times, clone it, disable individual instances, and customize each instance separately.
159
+
160
+ Every non-layout widget supports common options: **Enabled**, **Raw value only**, **Hide when empty** where relevant, **Custom icon**, foreground/background/bold colors, and ANSI256 overrides.
161
+
162
+ The **Value example** column shows the widget value before labels/icons and colors are applied. In normal mode, pi-footer prefixes non-layout widgets with the selected emoji, Nerd Font icon, text label, or custom icon. `Raw value only` and minimalist mode render the raw value directly.
163
+
164
+ ### Core widgets
165
+
166
+ | Widget | Shows | Widget-specific options | Value example |
167
+ | --- | --- | --- | --- |
168
+ | `Model` | Active model id. | Show provider. | `claude-sonnet-4-5` |
169
+ | `Provider` | Active model provider. | — | `anthropic` |
170
+ | `Provider/Model` | Provider and model together. | — | `anthropic/claude-sonnet-4-5` |
171
+ | `Thinking Level` | Current pi thinking/reasoning level. Hidden when unavailable. | — | `high` |
172
+ | `Text Verbosity` | Text verbosity for providers that expose it. Hidden when unavailable. | — | `low` |
173
+ | `Context Window` | Model context window size. | Token format, hide when zero. | `200k` |
174
+ | `Working Dir` | Current working directory. | Display style: default, full `~`, fish-style; segment count. | `~/…/projects/pi-footer` |
175
+ | `Working Dir Name` | Basename of current working directory. | — | `pi-footer` |
176
+ | `Session Name` | pi session display name. | Text when empty, hide when empty. | `release prep` |
177
+ | `Active Tools` | Count of active tools. | — | `4` |
178
+ | `Pi Event Value` | Value published through `pi.events`. | Widget ID, text when empty, hide when empty. | `on` |
179
+ | `Pi Extension Status` | Value published by another extension through `ctx.ui.setStatus`. | Status key, trim value, preserve trim styles, text when empty, hide when empty. | `fast` |
180
+
181
+ ### Tokens, context, and cost widgets
182
+
183
+ | Widget | Shows | Widget-specific options | Value example |
184
+ | --- | --- | --- | --- |
185
+ | `Input/Output Tokens` | Session input and output token totals. | Token format. | `↑42k ↓8.4k` |
186
+ | `Input Tokens` | Session input token total. | Token format, hide when zero. | `42k` |
187
+ | `Output Tokens` | Session output token total. | Token format, hide when zero. | `8.4k` |
188
+ | `Total Tokens` | Total tokens from usage records. | Token format, hide when zero. | `182.4k` |
189
+ | `Cache Read` | Cache-read token total. | Token format, hide when zero. | `120k` |
190
+ | `Cache Write` | Cache-write token total. | Token format, hide when zero. | `12k` |
191
+ | `Cache Hit Rate` | Session or latest turn cache hit percentage. | Source, display style, hide when zero. | `72.4%` |
192
+ | `Context Length` | Current context token estimate/usage. | Token format, conditional colors, hide when zero. | `50k` |
193
+ | `Context %` | Used context percentage. | Conditional colors. | `25%` |
194
+ | `Context Remaining` | Remaining context percentage. | Conditional colors. | `75%` |
195
+ | `Context Bar` | Progress bar plus context usage. | Display: default, short, short-only, medium; token format; conditional colors. | `[████████░░░░░░░░░░░░░░░░░░░░░░░░] 50k/200k (25%)` |
196
+ | `Session Cost` | Estimated session cost. | Cost format, show subscription marker. | `$0.1234` |
197
+ | `Input Speed` | Average input tokens per minute across transcript span. | Token format, hide when zero. | `20.1k/min` |
198
+ | `Output Speed` | Average output tokens per minute across transcript span. | Token format, hide when zero. | `1.1k/min` |
199
+ | `Total Speed` | Average total tokens per minute across transcript span. | Token format, hide when zero. | `21.2k/min` |
200
+
201
+ ### Session widgets
202
+
203
+ | Widget | Shows | Widget-specific options | Value example |
204
+ | --- | --- | --- | --- |
205
+ | `Message Counts` | User/assistant/tool result counts. | — | `7u/6a/12t` |
206
+ | `User Messages` | User message count. | — | `7` |
207
+ | `Assistant Messages` | Assistant message count. | — | `6` |
208
+ | `Tool Results` | Tool result count. | — | `12` |
209
+ | `Total Messages` | Total user, assistant, and tool messages. | — | `25` |
210
+ | `Transcript Span` | Time between first and latest recorded session entry. | — | `1h 12m` |
211
+ | `Session Total Time` | Live wall-clock time since first session entry. | — | `1h 20m` |
212
+ | `Session Start` | First session entry time. | — | `09:41` |
213
+ | `Last Activity` | Most recent session entry time. | — | `11:06` |
214
+ | `Session ID` | Current pi session id. | — | `018f1234` |
215
+ | `Compactions` | Number of compaction summaries. | — | `1` |
216
+
217
+ ### Git widgets
218
+
219
+ | Widget | Shows | Widget-specific options | Value example |
220
+ | --- | --- | --- | --- |
221
+ | `Git Branch` | Current Git branch. | Display: plain, round brackets, custom surround; hide when empty. | `main` or `(main)` |
222
+ | `Git SHA` | Short `HEAD` commit SHA. | Hide when empty. | `a1b2c3d` |
223
+ | `Git Root Dir` | Repository root directory name. | Hide when empty. | `pi-footer` |
224
+ | `Git Status` | Staged, unstaged, and untracked file counts. | Hide when empty. | `+2 ±3 ?1` |
225
+ | `Git Diff` | Uncommitted insertion/deletion summary. | Display: plain or compact; hide when empty. | `+42/-10` or `(+42,-10)` |
226
+ | `Git Clean Status` | Clean/dirty repository state. | Hide when empty. | `clean` or `dirty` |
227
+ | `Git Staged Files` | Staged file count. | Hide when empty. | `2` |
228
+ | `Git Unstaged Files` | Unstaged file count. | Hide when empty. | `3` |
229
+ | `Git Untracked Files` | Untracked file count. | Hide when empty. | `1` |
230
+ | `Git Insertions` | Uncommitted insertion count. | Hide when empty. | `42` |
231
+ | `Git Deletions` | Uncommitted deletion count. | Hide when empty. | `10` |
232
+ | `Git Ahead/Behind` | Ahead/behind counts relative to upstream. | Hide when empty. | `↑1 ↓0` |
233
+ | `Git Remote` | `origin` remote URL. | Hide when empty. | `git@github.com:user/repo.git` |
234
+
235
+ ### Custom and layout widgets
236
+
237
+ | Widget | Shows | Widget-specific options | Value example |
238
+ | --- | --- | --- | --- |
239
+ | `Custom Text` | Literal user-defined text. | Text. | `prod` |
240
+ | `Separator` | Explicit separator segment. | Separator style; custom text for custom separator. | ` • `, ` \| `, `-`, `,` |
241
+ | `Spacer` | Fixed blank space. | Width from 1 to 40. | `<empty space>` |
242
+ | `Flex Separator` | Invisible split point that pushes following widgets to the right side. | — | `left widgets <---> right widgets` |
243
+
244
+ ## Styling
245
+
246
+ ### Icon modes
247
+
248
+ Supported icon modes: Emoji, Nerd Font icons, Text labels.
249
+
250
+ Individual widgets can override their icon with **Custom icon**.
251
+
252
+ ### Colors
253
+
254
+ Each widget supports:
255
+
256
+ - foreground
257
+ - background
258
+ - bold
259
+ - custom ANSI256 foreground/background (`0-255`)
260
+
261
+ Foreground colors also include pi theme colors such as `Pi Dim`, `Pi Accent`, `Pi Warning`, and `Pi Error`. These follow the active pi theme and are useful for native-looking presets like `pi-footer`.
262
+
263
+ In color editing:
264
+
265
+ - `←` / `→` cycle/change
266
+ - type digits to edit ANSI256 values
267
+ - backspace deletes one digit
268
+ - values over `255` clamp to `255`
269
+
270
+ Terminal color levels: Truecolor, 256 Color, Basic 16-color, No Color.
271
+
272
+ ### Separators
273
+
274
+ Global separator modes: `none`, `dot`, `pipe`, `space`, `powerline`, `dash`, `comma`
275
+
276
+ Global separator foreground/background colors apply only to automatic separators inserted between widgets.
277
+
278
+ Separator widgets are independent. They have their own separator style and colors and are not affected by global separator colors.
279
+
280
+ Powerline-oriented separator widget styles include hard transitions, soft transitions, and caps. The powerline presets use explicit separator widgets so transitions can be colored segment-by-segment.
281
+
282
+ ### Terminal width modes
283
+
284
+ - `Full width always`
285
+ - `Full width minus 40` - truncates the pi-footer to fit within `terminal width - 40` columns.
286
+
287
+ The `compact` preset uses `Full width minus 40`; other presets use full width.
288
+
289
+ ## Extension integration
290
+
291
+ `pi-footer` exposes two integration paths for extension authors.
292
+
293
+ ### 1. Event widgets
294
+
295
+ Add a `Pi Event Value` widget and set its `Widget ID` to a stable value, for example `fast_mode` or `service_tier`.
296
+
297
+ Other extensions can update that widget through pi's shared event bus:
298
+
299
+ ```typescript
300
+ pi.events.emit("pi-footer:update-widget", {
301
+ widgetId: "fast_mode",
302
+ value: "on",
303
+ });
304
+ ```
305
+
306
+ Clear a previously published value by sending `null`:
307
+
308
+ ```typescript
309
+ pi.events.emit("pi-footer:update-widget", {
310
+ widgetId: "fast_mode",
311
+ value: null,
312
+ });
313
+ ```
314
+
315
+ Values are live/in-memory. After reload or session switch, publisher extensions should emit their current value again.
316
+
317
+ ### 2. Pi extension statuses
318
+
319
+ Extensions can publish status text through pi's UI API:
320
+
321
+ ```typescript
322
+ const STATUS_KEY = "my-extension";
323
+
324
+ ctx.ui.setStatus(STATUS_KEY, "fast");
325
+ ```
326
+
327
+ Users can display that value in two ways:
328
+
329
+ - add a `Pi Extension Status` widget and set `Status key` to `my-extension`
330
+ - show/hide it in the `Pi extensions` menu for the extension status row
331
+
332
+ Multiple widgets may point at the same status key. `pi-footer` does not auto-hide or deduplicate extension statuses; the user controls what appears.
333
+
334
+ Use **Trim value** on a `Pi Extension Status` widget to remove leading visible characters from the published status. For example, trim `2` turns `● On`, `● Enabled`, or `◌ Disabled` into `On`, `Enabled`, or `Disabled`. **Preserve trim styles** is enabled by default, so whole-status ANSI styling is replayed after the trimmed prefix; turn it off to drop styling that was attached only to the trimmed prefix.
335
+
336
+ Incoming ANSI styling from extension statuses is preserved by default. If the user sets a custom foreground/background/bold override on the `Pi Extension Status` widget, incoming ANSI is stripped first so the user override wins cleanly.
337
+
338
+ ## Development
339
+
340
+ ```bash
341
+ npm run lint
342
+ npm run fmt
343
+ npm run typecheck
344
+ npm run test
345
+ ```
346
+
347
+ ## Credits
348
+
349
+ - [@sirmalloc](https://github.com/sirmalloc) for [`ccstatusline`](https://github.com/sirmalloc/ccstatusline) inspiration.
350
+ - [@badlogic](https://github.com/badlogic) for [`pi`](https://pi.dev) and its built-in footer.
351
+
352
+ ## License
353
+
354
+ MIT
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "pi-footer",
3
+ "version": "0.5.1",
4
+ "description": "Configurable, Ultimate multi-line footer/statusline extension for pi",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "statusline",
10
+ "pi-statusline",
11
+ "pi-footer",
12
+ "footer",
13
+ "tui"
14
+ ],
15
+ "pi": {
16
+ "extensions": [
17
+ "./src/index.ts"
18
+ ],
19
+ "video": "https://raw.githubusercontent.com/wobondar/pi-footer/main/assets/demo-video.mp4",
20
+ "image": "https://raw.githubusercontent.com/wobondar/pi-footer/main/assets/demo-teaser.gif"
21
+ },
22
+ "files": [
23
+ "src/",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
28
+ "homepage": "https://github.com/wobondar/pi-footer#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/wobondar/pi-footer/issues"
31
+ },
32
+ "license": "MIT",
33
+ "author": "wobondar",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/wobondar/pi-footer.git"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "lint": "oxlint -f default",
43
+ "lint:check": "oxlint",
44
+ "lint:fix": "oxlint --fix",
45
+ "fmt": "oxfmt",
46
+ "fmt:check": "oxfmt --check",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run",
49
+ "coverage": "vitest run --coverage",
50
+ "prepublishOnly": "npm run typecheck && npm run fmt:check && npm run lint && npm run test"
51
+ },
52
+ "peerDependencies": {
53
+ "@earendil-works/pi-coding-agent": "*",
54
+ "@earendil-works/pi-tui": "*"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^26.0.0",
58
+ "@vitest/coverage-v8": "^4.1.5",
59
+ "oxfmt": "^0.61.0",
60
+ "oxlint": "^1.62.0",
61
+ "oxlint-tsgolint": "^7.0.2001",
62
+ "typescript": "^7.0.2",
63
+ "vitest": "^4.1.5"
64
+ },
65
+ "dependencies": {
66
+ "chalk": "^6.0.0"
67
+ }
68
+ }
@@ -0,0 +1,106 @@
1
+ export const CACHE_NAMESPACES = { runtime: "runtime", git: "git" } as const;
2
+ type CacheNamespace = (typeof CACHE_NAMESPACES)[keyof typeof CACHE_NAMESPACES];
3
+
4
+ interface CacheEntry<V> {
5
+ value: V | null;
6
+ updatedAt: number | null;
7
+ pending: Promise<void> | null;
8
+ listeners: Set<() => void>;
9
+ }
10
+
11
+ export class AsyncCache {
12
+ private readonly entries = new Map<string, CacheEntry<unknown>>();
13
+ private readonly maxEntries: number;
14
+ private readonly now: () => number;
15
+
16
+ constructor(maxEntries = 200, now: () => number = () => Date.now()) {
17
+ this.maxEntries = maxEntries;
18
+ this.now = now;
19
+ }
20
+
21
+ get<K, V, F>(
22
+ ns: CacheNamespace,
23
+ key: K,
24
+ ttlMs: number,
25
+ filter: F,
26
+ fetcher: (filter: F) => Promise<V | null>,
27
+ onRefresh?: () => void,
28
+ ): V | null {
29
+ const cacheKey = this.cacheKey(ns, key);
30
+ const entry = this.entryFor<V>(cacheKey);
31
+ if (this.isFresh(entry, ttlMs)) return entry.value;
32
+
33
+ this.addListener(entry, onRefresh);
34
+ if (!entry.pending) entry.pending = this.refresh(entry, filter, fetcher);
35
+
36
+ return entry.value;
37
+ }
38
+
39
+ clear(): void {
40
+ this.entries.clear();
41
+ }
42
+
43
+ private entryFor<V>(cacheKey: string): CacheEntry<V> {
44
+ const cached = this.entries.get(cacheKey);
45
+ if (cached) return cached as CacheEntry<V>;
46
+
47
+ const entry: CacheEntry<V> = {
48
+ value: null,
49
+ updatedAt: null,
50
+ pending: null,
51
+ listeners: new Set(),
52
+ };
53
+ this.entries.set(cacheKey, entry as CacheEntry<unknown>);
54
+ this.evictOldestEntry();
55
+ return entry;
56
+ }
57
+
58
+ private isFresh<V>(entry: CacheEntry<V>, ttlMs: number): boolean {
59
+ return entry.updatedAt !== null && this.now() - entry.updatedAt < ttlMs;
60
+ }
61
+
62
+ private addListener<V>(entry: CacheEntry<V>, listener: (() => void) | undefined): void {
63
+ if (listener) entry.listeners.add(listener);
64
+ }
65
+
66
+ private async refresh<V, F>(
67
+ entry: CacheEntry<V>,
68
+ filter: F,
69
+ fetcher: (filter: F) => Promise<V | null>,
70
+ ): Promise<void> {
71
+ try {
72
+ entry.value = await fetcher(filter);
73
+ } catch {
74
+ entry.value = null;
75
+ } finally {
76
+ entry.updatedAt = this.now();
77
+ entry.pending = null;
78
+ this.notify(entry);
79
+ }
80
+ }
81
+
82
+ private notify<V>(entry: CacheEntry<V>): void {
83
+ const listeners = [...entry.listeners];
84
+ entry.listeners.clear();
85
+
86
+ for (const listener of listeners) {
87
+ try {
88
+ listener();
89
+ } catch {
90
+ // Cache refresh notifications are best-effort.
91
+ }
92
+ }
93
+ }
94
+
95
+ private evictOldestEntry(): void {
96
+ if (this.entries.size <= this.maxEntries) return;
97
+ const oldestKey = this.entries.keys().next().value;
98
+ if (oldestKey !== undefined) this.entries.delete(oldestKey);
99
+ }
100
+
101
+ private cacheKey<K>(ns: CacheNamespace, key: K): string {
102
+ return `${ns}:${String(key)}`;
103
+ }
104
+ }
105
+
106
+ export const asyncCache = new AsyncCache();