@badliveware/pi-footer-framework 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # pi-footer-framework
2
2
 
3
- A configurable footer framework extension that intentionally owns/hijacks the footer and lets users control layout sections.
3
+ Configurable footer replacement for Pi. It owns footer layout, formatting, color, truncation, and placement while other extensions publish structured status/data for it to render.
4
4
 
5
- This is designed to pair with primitive-emitting extensions (for example `pr-upstream-status` via `pr-upstream:state`).
6
-
7
- It ships with a bundled skill (`footer-framework-config`) and advertises it via package metadata + `resources_discover`, so Pi can apply footer tuning commands automatically when this extension is active.
5
+ Use it when you want to customize Pi's footer through a configurable framework that agents can inspect and reconfigure from natural-language prompts.
8
6
 
9
7
  ## Install
10
8
 
@@ -12,94 +10,330 @@ It ships with a bundled skill (`footer-framework-config`) and advertises it via
12
10
  pi install npm:@badliveware/pi-footer-framework
13
11
  ```
14
12
 
15
- For local testing from this repository:
13
+ No external services or credentials are required.
16
14
 
17
- ```bash
18
- pi -e /path/to/pi/agent/extensions/public/footer-framework
15
+ ## Quick use
16
+
17
+ ```text
18
+ /footerfx on
19
+ /footerfx item context line 1
20
+ /footerfx item context after model
21
+ /footerfx item pr line 3
22
+ /footerfx anchor all right
23
+ /footerfx save user
24
+ ```
25
+
26
+ The default layout uses two footer lines. If you place an item on another positive line number, the footer grows to include that line. Disable the replacement footer with:
27
+
28
+ ```text
29
+ /footerfx off
30
+ ```
31
+
32
+ ## Showcase
33
+
34
+ This is a real Pi terminal using TS render closures, theme-aware styles, right-anchored layout, responsive column placement, PR state, and an adapted extension status item:
35
+
36
+ ![Footer framework showcase with cwd, PR, model, token stats, context, and watchdog status](assets/footer-framework-showcase.png)
37
+
38
+ The screenshot demonstrates:
39
+
40
+ - combined render output: cwd plus branch/PR label in one item
41
+ - token-level styling: labels, model id, thinking level, token stats, context, and cost use different Pi theme colors/attributes
42
+ - built-in Pi data: `cwd`, `branch`, `model`, `stats`, `context`, and `pr`
43
+ - extension status adaptation: `compaction-continue` status becomes `watchdog:on`
44
+ - flexible placement: line 1 and line 2 are right-anchored, while line 3 uses responsive `column` placement for middle and center-right items
45
+
46
+ <details>
47
+ <summary>Config used for the screenshot</summary>
48
+
49
+ Save this as `~/.pi/agent/footer-framework.config.ts`. The same file ships as `examples/footer-framework.config.ts` in the npm package.
50
+
51
+ ```ts
52
+ import type { FooterFrameworkConfig } from "@badliveware/pi-footer-framework";
53
+
54
+ function shortPath(value: string, maxWidth = 48, tailSegments = 2): string {
55
+ const normalized = value.replace(/^\/home\/[^/]+/, "~");
56
+ const prefix = normalized.startsWith("~/") ? "~/" : normalized.startsWith("/") ? "/" : "";
57
+ const parts = normalized.slice(prefix.length).split("/").filter(Boolean);
58
+ const compact = parts.length > tailSegments ? `${prefix}…/${parts.slice(-tailSegments).join("/")}` : normalized;
59
+ return compact.length > maxWidth ? `…${compact.slice(-(maxWidth - 1))}` : compact;
60
+ }
61
+
62
+ const config = {
63
+ enabled: true,
64
+ lineAnchors: { 1: "right", 2: "right", 3: "left" },
65
+ minGap: 2,
66
+ maxGap: 24,
67
+ items: {
68
+ branch: { visible: false },
69
+ ext: { visible: false },
70
+ cwd: {
71
+ visible: true,
72
+ line: 1,
73
+ zone: "left",
74
+ order: 10,
75
+ render: ({ pi, span, fn }) => [
76
+ span("cwd", "muted"),
77
+ " ",
78
+ span(shortPath(pi.cwd.trim(), 48, 2), "dim"),
79
+ span(" · ", "muted"),
80
+ span(fn.truncate(pi.branch?.label ?? "", 22), "accent"),
81
+ ],
82
+ },
83
+ model: {
84
+ visible: true,
85
+ line: 1,
86
+ zone: "right",
87
+ order: 10,
88
+ render: ({ pi, span }) => [
89
+ span("model:", "muted"),
90
+ span(pi.model.id ?? "no-model", "accent"),
91
+ span("/", "muted"),
92
+ span(pi.model.thinking ?? "", "thinkingXhigh,bold"),
93
+ ],
94
+ },
95
+ stats: {
96
+ visible: true,
97
+ line: 2,
98
+ zone: "left",
99
+ order: 10,
100
+ render: ({ pi, span }) => [
101
+ span("↑", "dim"), span(pi.stats.inputText ?? "0", "dim"), " ",
102
+ span("↓", "dim"), span(pi.stats.outputText ?? "0", "dim"), " ",
103
+ span("$", "accent"), span(pi.stats.costText ?? "0.000", "success"),
104
+ ],
105
+ },
106
+ context: {
107
+ visible: true,
108
+ line: 3,
109
+ zone: "left",
110
+ order: 10,
111
+ column: "50%",
112
+ render: ({ pi, span }) => {
113
+ if (!pi.context) return undefined;
114
+ const tone = pi.context.tone ?? "muted";
115
+ return [span("ctx", tone), " ", span(pi.context.percentText ?? "?%", tone), " ", span(pi.context.tokenText ?? "?/?", tone)];
116
+ },
117
+ },
118
+ pr: {
119
+ visible: true,
120
+ line: 3,
121
+ zone: "left",
122
+ order: 20,
123
+ column: "66%",
124
+ render: ({ pi, span }) => {
125
+ if (!pi.pr) return undefined;
126
+ return [span("PR ", "muted"), span(pi.pr.checkGlyph ?? "•", pi.pr.checkTone ?? "muted"), span(pi.pr.commentsText ?? "", "muted")];
127
+ },
128
+ },
129
+ },
130
+ adapters: {
131
+ watchdog: {
132
+ source: "extensionStatus",
133
+ key: "compaction-continue",
134
+ itemId: "watchdog",
135
+ match: "(on|off)",
136
+ group: 1,
137
+ urlPath: "url",
138
+ placement: { visible: true, line: 2, zone: "right", order: 20 },
139
+ render: ({ value, span }) => [span("watchdog:", "muted"), span(value ?? "", "accent,bold")],
140
+ },
141
+ },
142
+ } satisfies FooterFrameworkConfig;
143
+
144
+ export default config;
145
+ ```
146
+
147
+ String `column` positions such as `"50%"`, `"66%"`, and `"center"` are resolved from the current terminal width, so they keep their relative position after resize. Numeric columns remain fixed absolute terminal columns.
148
+
149
+ </details>
150
+
151
+ ## How it works
152
+
153
+ Footer-framework renders normalized footer items from adapter mappings, direct TS/JS item renderers, and extension-published items. Adapter mappings can read three source types:
154
+
155
+ | Source | Use it for |
156
+ | --- | --- |
157
+ | `pi` | Built-in Pi/session/footer data such as `cwd`, `model`, `stats`, `context`, `branch`, `pr`, and `extensionStatuses`. |
158
+ | `extensionStatus` | Existing `ctx.ui.setStatus()` footer/status entries from other extensions. |
159
+ | `sessionEntry` | The latest custom session entry written by an extension with `pi.appendEntry()`. |
160
+
161
+ The built-in footer items (`cwd`, `model`, `branch`, `stats`, `context`, `pr`) are regular default adapters unless you replace them with `items.<id>.render` in TS/JS config. User/project config overrides built-in defaults and producer hints.
162
+
163
+ Agents can inspect concise footer-relevant data with `footer_framework_sources`, then add adapter rules with `footer_framework_adapter_config` or adjust layout with `footer_framework_config`. Runtime metadata such as tools, commands, skills, descriptions, and `sourceInfo` is opt-in via `includeTools`, `includeCommands`, `includeSkills`, and `includeDetails`.
164
+
165
+ ## Templates and styles
166
+
167
+ Adapters can render with a restricted Liquid-style interpolation subset:
168
+
169
+ ```liquid
170
+ {{ pi.stats.costText }}
171
+ {{ "EUR" }}
172
+ {{ "EUR" | style: "accent" }}{{ pi.stats.costText | style: "success,bold" }}
19
173
  ```
20
174
 
21
- ## Behavior
175
+ Quoted strings are literals. Unquoted terms are variables, so missing variables are reported as template diagnostics instead of being guessed as text. Diagnostics appear in `/footerfx-debug`, `footer_framework_state`, and `footer_framework_sources`.
176
+
177
+ Useful template context:
178
+
179
+ | Path | Meaning |
180
+ | --- | --- |
181
+ | `value`, `label`, `status`, `data`, `url` | The current adapter source. |
182
+ | `pi.cwd` | Current working directory. |
183
+ | `pi.model.id`, `pi.model.provider`, `pi.model.thinking` | Current model information. |
184
+ | `pi.stats.inputText`, `pi.stats.outputText`, `pi.stats.costText` | Formatted session token/cost stats. Raw numbers are `input`, `output`, and `cost`. |
185
+ | `pi.context.percentText`, `pi.context.tokenText`, `pi.context.tone` | Context usage and recommended tone. |
186
+ | `pi.branch.name`, `pi.branch.label` | Git branch values. Use `truncate` in templates when you want a shorter display. |
187
+ | `pi.pr.number`, `pi.pr.url`, `pi.pr.checkGlyph`, `pi.pr.checkTone`, `pi.pr.commentsText` | Pull request state when available. |
188
+
189
+ Supported filters:
190
+
191
+ | Filter | Example |
192
+ | --- | --- |
193
+ | `style` / `color` | `{{ value | style: "accent,bold" }}` |
194
+ | `bg` / `background` | `{{ value | bg: "toolSuccessBg" }}` |
195
+ | `bold`, `italic`, `underline`, `inverse`, `strikethrough` | `{{ value | underline }}` |
196
+ | `link` | `{{ pi.pr.number | link: pi.pr.url }}` |
197
+ | `truncate` | `{{ pi.branch.label | truncate: 22 }}` limits any value to 22 cells with an ellipsis. |
198
+ | `compactPath` | `{{ pi.cwd | compactPath: 48, 2 }}` keeps the last 2 path segments when the path is wider than 48 cells. |
199
+ | `default` | `{{ data.state | default: "unknown" }}` |
200
+
201
+ Style strings use Pi theme tokens and text attributes. Foreground examples: `accent`, `muted`, `dim`, `success`, `warning`, `error`, `text`, `mdLink`, `toolDiffAdded`, and the other Pi theme foreground tokens. Backgrounds use `bg:<token>`, such as `bg:toolSuccessBg`. Attributes are `bold`, `italic`, `underline`, `inverse`, and `strikethrough`.
202
+
203
+ ## TypeScript render config
204
+
205
+ For personal formatting logic, use a normal TS/JS config file with render closures. Footer-framework still owns data collection, layout, clipping, diagnostics, and final rendering; the closure only returns text/spans for one item.
206
+
207
+ ```ts
208
+ import type { FooterFrameworkConfig } from "@badliveware/pi-footer-framework";
209
+
210
+ function shortPath(value: string, maxWidth = 48, tailSegments = 2) {
211
+ const normalized = value.replace(/^\/home\/[^/]+/, "~");
212
+ const prefix = normalized.startsWith("~/") ? "~/" : normalized.startsWith("/") ? "/" : "";
213
+ const parts = normalized.slice(prefix.length).split("/").filter(Boolean);
214
+ const compact = parts.length > tailSegments ? `${prefix}…/${parts.slice(-tailSegments).join("/")}` : normalized;
215
+ return compact.length > maxWidth ? `…${compact.slice(-(maxWidth - 1))}` : compact;
216
+ }
217
+
218
+ export default {
219
+ items: {
220
+ branch: { visible: false },
221
+ ext: { visible: false },
222
+ cwd: {
223
+ line: 1,
224
+ zone: "left",
225
+ order: 10,
226
+ render: ({ pi, span, fn }) => [
227
+ span("cwd", "muted"),
228
+ " ",
229
+ span(shortPath(pi.cwd.trim(), 48, 2), "dim"),
230
+ span(" · ", "muted"),
231
+ span(fn.truncate(pi.branch?.label ?? "", 22), "accent"),
232
+ ],
233
+ },
234
+ },
235
+ } satisfies FooterFrameworkConfig;
236
+ ```
22
237
 
23
- - Replaces the default footer when enabled.
24
- - Keeps a stable 2-line footer layout.
25
- - Composes built-in footer items:
26
- - `cwd`, `model`, `branch`, `stats`, `context`, `pr`, `ext`
27
- - the `model` item includes the active thinking level as `<model-id>:<thinking-level>`
28
- - the `context` item shows current context-window usage as percent plus humanized `tokens/max` counts, for example `ctx 52.2% 142K/272K`
29
- - Supports extension-provided dynamic items via the event bus.
30
- - Lets users position each item independently by line, left/right zone, relative order, or absolute column.
238
+ Render functions are synchronous and may return strings, spans, arrays, `null`, or `undefined`. Use `span(text, style, { url })` for token-level style/link metadata and `fn.text`, `fn.width`, `fn.truncate`, or `fn.compactPath` for footer-safe helpers. Adapter render functions also receive `value`, `label`, `status`, `data`, `url`, and `source`.
31
239
 
32
- ## Persistence
240
+ `/footerfx-debug`, `footer_framework_state`, and `footer_framework_sources` show which TS/JS renderers loaded plus each rendered item's final tokens, width, placement, and diagnostics. If a render closure throws or returns a promise, the footer item is skipped and a diagnostic is recorded.
33
241
 
34
- Footer settings automatically persist to the user config file by default:
242
+ ## Configuration files
243
+
244
+ User settings persist to:
35
245
 
36
246
  ```text
37
247
  ~/.pi/agent/footer-framework.json
38
248
  ```
39
249
 
40
- If a project config exists, it overrides user settings for that project:
250
+ Optional user TS/JS config files live next to it:
251
+
252
+ ```text
253
+ ~/.pi/agent/footer-framework.config.ts
254
+ ~/.pi/agent/footer-framework.config.js
255
+ ```
256
+
257
+ Project settings can override them:
41
258
 
42
259
  ```text
43
260
  <project>/.pi/footer-framework.json
261
+ <project>/.pi/footer-framework.config.ts
44
262
  ```
45
263
 
46
- Use `/footerfx save project` to intentionally create/update the project config.
264
+ Load order is defaults, user TS/JS, user JSON, project TS/JS, then project JSON. `/footerfx` commands write JSON overrides; they do not rewrite TS/JS source. Use `/footerfx save project` only when you intentionally want a project-specific footer layout.
47
265
 
48
266
  ## Commands
49
267
 
50
- - `/footerfx` show current config and source
51
- - `/footerfx config` show user/project config paths and loaded source
52
- - `/footerfx load` reload user/project config files
53
- - `/footerfx save user` save current settings to user config
54
- - `/footerfx save project` save current settings to project config
55
- - `/footerfx on` enable framework footer
56
- - `/footerfx off` disable and restore default footer
57
- - `/footerfx reset` restore defaults and persist to user config
58
- - `/footerfx section <cwd|stats|context|model|branch|pr|ext> <on|off>` legacy section toggles
59
- - `/footerfx item <id> <show|hide|reset>`
60
- - `/footerfx item <id> line <1|2>`
61
- - `/footerfx item <id> zone <left|right>`
62
- - `/footerfx item <id> order <n>`
63
- - `/footerfx item <id> before <other-id>` / `/footerfx item <id> after <other-id>`
64
- - `/footerfx item <id> column <n|off>` absolute column placement
65
- - `/footerfx anchor <line1|line2|all> <gap|left|center|right|spread>` line-level right-zone anchoring
66
- - `/footerfx gap <min> <max>` spacing controls used by `gap`/`center`/`left` modes
67
- - `/footerfx branch-width <n>` max branch label width
68
- - `/footerfx mcp-zero <hide|show>` hide/show `MCP: 0/x servers`
69
- - `/footerfx-debug` dump latest footer snapshot and settings
70
- - includes per-line layout telemetry: left/right widths, pad width, right start/end columns, truncation
71
-
72
- ## Extension item API
73
-
74
- Other extensions can contribute footer items by emitting:
268
+ | Command | What it does |
269
+ | --- | --- |
270
+ | `/footerfx` | Show current config and source. |
271
+ | `/footerfx on` / `/footerfx off` | Enable or disable the replacement footer. |
272
+ | `/footerfx config` | Show loaded config source and config paths. |
273
+ | `/footerfx load` | Reload user/project config files. |
274
+ | `/footerfx save user` | Save current settings as the user default. |
275
+ | `/footerfx save project` | Save current settings for the current project. |
276
+ | `/footerfx reset` | Restore defaults and persist them to user config. |
277
+ | `/footerfx section <cwd|stats|context|model|branch|pr|ext> <on|off>` | Convenience alias for item visibility. |
278
+ | `/footerfx item <id> <show|hide|reset>` | Control item visibility. |
279
+ | `/footerfx item <id> line <n>` / `row <n>` | Move an item to any positive footer line. |
280
+ | `/footerfx item <id> zone <left|right>` | Move an item between left/right zones. |
281
+ | `/footerfx item <id> before <other-id>` / `after <other-id>` | Place an item relative to another item. |
282
+ | `/footerfx item <id> column <n|center|middle|percent|off>` | Pin, center, percentage-place, or unpin an item. Percent examples: `50%`, `66%`. |
283
+ | `/footerfx anchor <line|all> <gap|left|center|right|spread>` | Control line alignment. `line3` and `3` both work. |
284
+ | `/footerfx adapter` | List configured adapters. |
285
+ | `/footerfx adapter <id> pi <source-key> [label]` | Adapt a built-in Pi source. |
286
+ | `/footerfx adapter <id> status <status-key> [label]` | Adapt an existing extension status key. |
287
+ | `/footerfx adapter <id> custom <custom-type> <path> [label]` | Adapt the latest matching custom session entry. |
288
+ | `/footerfx adapter <id> template <template>` | Set the adapter's render template. |
289
+ | `/footerfx adapter <id> empty-template <template>` | Set the template used for an empty adapter value. |
290
+ | `/footerfx adapter <id> style <style>` | Apply a default style to the rendered adapter text. |
291
+ | `/footerfx adapter <id> remove` | Remove an adapter. For built-ins, hide the item with `/footerfx item <id> hide`. |
292
+ | `/footerfx gap <min> <max>` | Set spacing bounds. |
293
+ | `/footerfx-debug` | Show render snapshot, settings, template/render diagnostics, loaded TS/JS renderers, and layout telemetry. |
294
+
295
+ ## Agent tools
296
+
297
+ The extension exposes tools so agents can inspect and adjust the footer without asking you to run commands:
298
+
299
+ - `footer_framework_state`
300
+ - `footer_framework_sources`
301
+ - `footer_framework_config`
302
+ - `footer_framework_adapter_config`
303
+
304
+ `footer_framework_sources` is concise by default. Pass `includeTools`, `includeCommands`, `includeSkills`, and `includeDetails` only when runtime metadata is directly useful.
305
+
306
+ ## Extension data API
307
+
308
+ Compatible extensions should publish data, not pre-rendered layout. The framework decides final text, color, position, and truncation. Producers may include hints, but user config wins.
75
309
 
76
310
  ```ts
77
311
  pi.events.emit("footer-framework:item", {
78
- id: "my-extension:status",
79
- text: "cache warm",
80
- placement: { line: 2, zone: "right", order: 50 }
312
+ id: "cache:status",
313
+ label: "cache",
314
+ value: "warm",
315
+ tone: "success",
316
+ data: { entries: 42 },
317
+ hint: {
318
+ icon: "◇",
319
+ format: "label-value",
320
+ placement: { line: 2, zone: "right", order: 50 }
321
+ }
81
322
  });
82
323
  ```
83
324
 
84
- Remove an item:
325
+ Legacy `text` and top-level `placement` fields still work for existing extensions, but new integrations should prefer `label`, `value`, `status`, `data`, and `hint`.
326
+
327
+ Remove an item with:
85
328
 
86
329
  ```ts
87
- pi.events.emit("footer-framework:item", { id: "my-extension:status", remove: true });
330
+ pi.events.emit("footer-framework:item", { id: "cache:status", remove: true });
88
331
  ```
89
332
 
90
- Users can then reposition the item with `/footerfx item my-extension:status ...` and those overrides persist automatically.
91
-
92
- ## Agent automation primitives
93
-
94
- The extension exposes tools so the agent can introspect and tune the footer without asking the user to run commands:
333
+ ## Troubleshooting
95
334
 
96
- - `footer_framework_state` returns settings + latest rendered footer snapshot + layout telemetry
97
- - `footer_framework_config` — applies the same syntax as `/footerfx ...`
335
+ ### Blank space below the footer
98
336
 
99
- ## Notes
337
+ If blank rows sometimes appear below the footer and disappear after you send a prompt, check `/footerfx-debug` or `footer_framework_state`. When `lastFooterSnapshot.lines` contains only the expected footer lines, the framework is not rendering extra rows. This is usually Pi's TUI viewport/differential rendering leaving unused terminal space below the last rendered component.
100
338
 
101
- - The extension stores latest settings in session custom entries (`footer-framework-state`).
102
- - It listens to event bus topic `pr-upstream:state` for PR primitives.
103
- - Extension statuses with empty rendered text are ignored so transient or
104
- intentionally-cleared status providers do not leave phantom separators in the
105
- footer.
339
+ A Pi-side workaround is `terminal.clearOnShrink: true` in `~/.pi/agent/settings.json`, but that can add redraw flicker. Footer-framework does not change this setting.
@@ -0,0 +1,115 @@
1
+ import type { FooterFrameworkConfig } from "@badliveware/pi-footer-framework";
2
+
3
+ function shortPath(value: string, maxWidth = 48, tailSegments = 2): string {
4
+ const normalized = value.replace(/^\/home\/[^/]+/, "~");
5
+ const prefix = normalized.startsWith("~/") ? "~/" : normalized.startsWith("/") ? "/" : "";
6
+ const parts = normalized.slice(prefix.length).split("/").filter(Boolean);
7
+ const compact = parts.length > tailSegments ? `${prefix}…/${parts.slice(-tailSegments).join("/")}` : normalized;
8
+ return compact.length > maxWidth ? `…${compact.slice(-(maxWidth - 1))}` : compact;
9
+ }
10
+
11
+ const config = {
12
+ enabled: true,
13
+ lineAnchors: {
14
+ 1: "right",
15
+ 2: "right",
16
+ 3: "left",
17
+ },
18
+ minGap: 2,
19
+ maxGap: 24,
20
+ items: {
21
+ branch: { visible: false },
22
+ ext: { visible: false },
23
+ cwd: {
24
+ visible: true,
25
+ line: 1,
26
+ zone: "left",
27
+ order: 10,
28
+ render: ({ pi, span, fn }) => [
29
+ span("cwd", "muted"),
30
+ " ",
31
+ span(shortPath(pi.cwd.trim(), 48, 2), "dim"),
32
+ span(" · ", "muted"),
33
+ span(fn.truncate(pi.branch?.label ?? "", 22), "accent"),
34
+ ],
35
+ },
36
+ model: {
37
+ visible: true,
38
+ line: 1,
39
+ zone: "right",
40
+ order: 10,
41
+ render: ({ pi, span }) => [
42
+ span("model:", "muted"),
43
+ span(pi.model.id ?? "no-model", "accent"),
44
+ span("/", "muted"),
45
+ span(pi.model.thinking ?? "", "thinkingXhigh,bold"),
46
+ ],
47
+ },
48
+ stats: {
49
+ visible: true,
50
+ line: 2,
51
+ zone: "left",
52
+ order: 10,
53
+ render: ({ pi, span }) => [
54
+ span("↑", "dim"),
55
+ span(pi.stats.inputText ?? "0", "dim"),
56
+ " ",
57
+ span("↓", "dim"),
58
+ span(pi.stats.outputText ?? "0", "dim"),
59
+ " ",
60
+ span("$", "accent"),
61
+ span(pi.stats.costText ?? "0.000", "success"),
62
+ ],
63
+ },
64
+ context: {
65
+ visible: true,
66
+ line: 3,
67
+ zone: "left",
68
+ order: 10,
69
+ column: "50%",
70
+ render: ({ pi, span }) => {
71
+ if (!pi.context) return undefined;
72
+ const tone = pi.context.tone ?? "muted";
73
+ return [
74
+ span("ctx", tone),
75
+ " ",
76
+ span(pi.context.percentText ?? "?%", tone),
77
+ " ",
78
+ span(pi.context.tokenText ?? "?/?", tone),
79
+ ];
80
+ },
81
+ },
82
+ pr: {
83
+ visible: true,
84
+ line: 3,
85
+ zone: "left",
86
+ order: 20,
87
+ column: "66%",
88
+ render: ({ pi, span }) => {
89
+ if (!pi.pr) return undefined;
90
+ return [
91
+ span("PR ", "muted"),
92
+ span(pi.pr.checkGlyph ?? "•", pi.pr.checkTone ?? "muted"),
93
+ span(pi.pr.commentsText ?? "", "muted"),
94
+ ];
95
+ },
96
+ },
97
+ },
98
+ adapters: {
99
+ watchdog: {
100
+ source: "extensionStatus",
101
+ key: "compaction-continue",
102
+ itemId: "watchdog",
103
+ match: "(on|off)",
104
+ group: 1,
105
+ urlPath: "url",
106
+ placement: { visible: true, line: 2, zone: "right", order: 20 },
107
+ render: ({ value, span }) => [
108
+ span("watchdog:", "muted"),
109
+ span(value ?? "", "accent,bold"),
110
+ ],
111
+ },
112
+ },
113
+ } satisfies FooterFrameworkConfig;
114
+
115
+ export default config;