@pi-unipi/footer 0.1.3 → 0.1.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.
- package/README.md +73 -158
- package/package.json +1 -1
- package/src/commands.ts +36 -121
- package/src/config.ts +4 -0
- package/src/help.ts +160 -0
- package/src/index.ts +25 -1
- package/src/presets.ts +40 -31
- package/src/rendering/icons.ts +38 -20
- package/src/rendering/renderer.ts +195 -76
- package/src/rendering/theme.ts +56 -29
- package/src/segments/compactor.ts +21 -10
- package/src/segments/core.ts +122 -14
- package/src/segments/kanboard.ts +24 -8
- package/src/segments/mcp.ts +25 -8
- package/src/segments/memory.ts +8 -4
- package/src/segments/notify.ts +16 -5
- package/src/segments/ralph.ts +24 -7
- package/src/segments/status-ext.ts +1 -1
- package/src/segments/workflow.ts +39 -17
- package/src/tps-tracker.ts +204 -0
- package/src/tui/settings-tui.ts +228 -57
- package/src/types.ts +51 -12
package/README.md
CHANGED
|
@@ -1,78 +1,65 @@
|
|
|
1
1
|
# @pi-unipi/footer
|
|
2
2
|
|
|
3
|
-
Persistent status bar
|
|
3
|
+
Persistent status bar at the bottom of the terminal. Shows live stats from all Unipi packages — compactor tokens saved, memory count, MCP status, Ralph loops, workflow state, kanboard tasks, notifications.
|
|
4
4
|
|
|
5
|
-
Subscribes to
|
|
5
|
+
Subscribes to events from every package and renders segments using Pi's `setFooter` + `setWidget` APIs. Responsive layout adjusts to terminal width, with a secondary row for narrow terminals.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Commands
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
9
|
+
| Command | Description |
|
|
10
|
+
|---------|-------------|
|
|
11
|
+
| `/unipi:footer` | Toggle footer on/off |
|
|
12
|
+
| `/unipi:footer <preset>` | Switch preset (default, minimal, compact, full, nerd, ascii) |
|
|
13
|
+
| `/unipi:footer sep:<style>` | Change separator style |
|
|
14
|
+
| `/unipi:footer icon:<style>` | Change icon style (nerd, emoji, text) |
|
|
15
|
+
| `/unipi:footer on` / `/unipi:footer off` | Enable/disable explicitly |
|
|
16
|
+
| `/unipi:footer-settings` | Open settings TUI for per-group/per-segment toggles |
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Special Triggers
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
┌─────────────────────────────────────────────────────┐
|
|
22
|
-
│ FooterRenderer (setFooter + setWidget) │ ← Renders to screen
|
|
23
|
-
│ - Responsive layout (top + secondary rows) │
|
|
24
|
-
│ - Preset system, separators, theming │
|
|
25
|
-
├─────────────────────────────────────────────────────┤
|
|
26
|
-
│ FooterRegistry (segment groups) │ ← Manages segments
|
|
27
|
-
│ - Subscribes to UNIPI_EVENTS │
|
|
28
|
-
│ - Per-segment enable/disable │
|
|
29
|
-
│ - Reactive data caching │
|
|
30
|
-
├─────────────────────────────────────────────────────┤
|
|
31
|
-
│ Event Sources (existing packages) │ ← Data providers
|
|
32
|
-
│ - compactor, memory, workflow, ralph, mcp, │
|
|
33
|
-
│ kanboard, notify, core │
|
|
34
|
-
└─────────────────────────────────────────────────────┘
|
|
35
|
-
```
|
|
20
|
+
Footer subscribes to events from every Unipi package:
|
|
36
21
|
|
|
37
|
-
|
|
22
|
+
| Group | Events | Segments |
|
|
23
|
+
|-------|--------|----------|
|
|
24
|
+
| core | Pi SDK | model, thinking, path, git, context_pct, cost, tokens, session |
|
|
25
|
+
| compactor | `COMPACTOR_STATS_UPDATED` | session_events, compactions, tokens_saved, compression_ratio |
|
|
26
|
+
| memory | `MEMORY_STORED`/`DELETED`/`CONSOLIDATED` | project_count, total_count, consolidations |
|
|
27
|
+
| mcp | `MCP_SERVER_STARTED`/`STOPPED`/`ERROR` | servers_total, servers_active, tools_total |
|
|
28
|
+
| ralph | `RALPH_LOOP_START`/`END`/`ITERATION_DONE` | active_loops, total_iterations, loop_status |
|
|
29
|
+
| workflow | `WORKFLOW_START`/`END` | current_command, sandbox_level, command_duration |
|
|
30
|
+
| kanboard | Direct registry read | docs_count, tasks_done, tasks_total, task_pct |
|
|
31
|
+
| notify | `NOTIFICATION_SENT` | platforms_enabled, last_sent |
|
|
38
32
|
|
|
39
|
-
|
|
33
|
+
Footer works even if packages load after it — late-arriving events update the cache.
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
- `/unipi:footer <preset>` — switch preset (default, minimal, compact, full, nerd, ascii)
|
|
43
|
-
- `/unipi:footer sep:<style>` — change separator style (powerline, powerline-thin, slash, pipe, dot, ascii)
|
|
44
|
-
- `/unipi:footer icon:<style>` — change icon style (nerd, emoji, text)
|
|
45
|
-
- `/unipi:footer on` / `/unipi:footer off` — enable/disable explicitly
|
|
46
|
-
- `/unipi:footer-settings` — open settings TUI for per-group/per-segment toggles
|
|
47
|
-
|
|
48
|
-
## Segment Groups
|
|
35
|
+
## Presets
|
|
49
36
|
|
|
50
|
-
|
|
|
51
|
-
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
| **kanboard** | `docs_count`, `tasks_done`, `tasks_total`, `task_pct` | ON | Kanboard registry (direct read) |
|
|
59
|
-
| **notify** | `platforms_enabled`, `last_sent` | OFF | `NOTIFICATION_SENT` event |
|
|
60
|
-
| **status_ext** | `extension_statuses` | ON | `footerData.getExtensionStatuses()` |
|
|
37
|
+
| Preset | Description |
|
|
38
|
+
|--------|-------------|
|
|
39
|
+
| `default` | Balanced: model, thinking, path, git, context, cost + compactor + memory + ralph |
|
|
40
|
+
| `minimal` | Essentials only: path, git, context |
|
|
41
|
+
| `compact` | Core + key stats: model, git, cost, context + compactor + memory |
|
|
42
|
+
| `full` | Everything from all groups |
|
|
43
|
+
| `nerd` | Full + hostname + time + session + extensions |
|
|
44
|
+
| `ascii` | Core segments with ASCII icons |
|
|
61
45
|
|
|
62
|
-
##
|
|
46
|
+
## Segment Groups
|
|
63
47
|
|
|
64
|
-
|
|
|
65
|
-
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
48
|
+
| Group | Default | Data Source |
|
|
49
|
+
|-------|---------|-------------|
|
|
50
|
+
| **core** | ON | Pi SDK (ctx.sessionManager, footerData) |
|
|
51
|
+
| **compactor** | ON | `COMPACTOR_STATS_UPDATED` event |
|
|
52
|
+
| **memory** | ON | `MEMORY_STORED`/`DELETED`/`CONSOLIDATED` events |
|
|
53
|
+
| **mcp** | ON | `MCP_SERVER_STARTED`/`STOPPED`/`ERROR` events |
|
|
54
|
+
| **ralph** | ON | `RALPH_LOOP_START`/`END`/`ITERATION_DONE` events |
|
|
55
|
+
| **workflow** | ON | `WORKFLOW_START`/`END` events |
|
|
56
|
+
| **kanboard** | ON | Kanboard registry (direct read) |
|
|
57
|
+
| **notify** | OFF | `NOTIFICATION_SENT` event |
|
|
58
|
+
| **status_ext** | ON | `footerData.getExtensionStatuses()` |
|
|
72
59
|
|
|
73
|
-
##
|
|
60
|
+
## Configurables
|
|
74
61
|
|
|
75
|
-
Settings
|
|
62
|
+
Settings in `~/.pi/agent/settings.json` under `unipi.footer`:
|
|
76
63
|
|
|
77
64
|
```json
|
|
78
65
|
{
|
|
@@ -88,19 +75,7 @@ Settings are stored in `~/.pi/agent/settings.json` under `unipi.footer`:
|
|
|
88
75
|
"segments": {
|
|
89
76
|
"session_events": true,
|
|
90
77
|
"compactions": true,
|
|
91
|
-
"tokens_saved": true
|
|
92
|
-
"compression_ratio": false,
|
|
93
|
-
"indexed_docs": false,
|
|
94
|
-
"sandbox_runs": false,
|
|
95
|
-
"search_queries": false
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
"memory": {
|
|
99
|
-
"show": true,
|
|
100
|
-
"segments": {
|
|
101
|
-
"project_count": true,
|
|
102
|
-
"total_count": true,
|
|
103
|
-
"consolidations": false
|
|
78
|
+
"tokens_saved": true
|
|
104
79
|
}
|
|
105
80
|
}
|
|
106
81
|
}
|
|
@@ -109,98 +84,38 @@ Settings are stored in `~/.pi/agent/settings.json` under `unipi.footer`:
|
|
|
109
84
|
}
|
|
110
85
|
```
|
|
111
86
|
|
|
112
|
-
|
|
87
|
+
### Separator Styles
|
|
113
88
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
89
|
+
| Style | Look |
|
|
90
|
+
|-------|------|
|
|
91
|
+
| `powerline` | Thick powerline arrows |
|
|
92
|
+
| `powerline-thin` | Thin powerline arrows (default) |
|
|
93
|
+
| `slash` | / |
|
|
94
|
+
| `pipe` | \| |
|
|
95
|
+
| `dot` | Middle dot |
|
|
96
|
+
| `ascii` | > < |
|
|
118
97
|
|
|
119
|
-
|
|
120
|
-
┌─ model │ thinking │ path │ git │ context │ cost ───────────────────────────────────────────────┐
|
|
121
|
-
└─ compactions │ tokens_saved │ project_count │ ralph │ workflow ────────────────────────────────┘
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Separator Styles
|
|
125
|
-
|
|
126
|
-
| Style | Look | Description |
|
|
127
|
-
|-------|------|-------------|
|
|
128
|
-
| `powerline` | ◀ ▶ | Thick powerline arrows |
|
|
129
|
-
| `powerline-thin` | | Thin powerline arrows (default) |
|
|
130
|
-
| `slash` | / | Slash separator |
|
|
131
|
-
| `pipe` | \| | Pipe separator |
|
|
132
|
-
| `dot` | · | Middle dot separator |
|
|
133
|
-
| `ascii` | > < | ASCII angle brackets |
|
|
134
|
-
|
|
135
|
-
## Icon Styles
|
|
136
|
-
|
|
137
|
-
Three icon styles are available, controlled by `/unipi:footer icon:<style>` or the `iconStyle` setting:
|
|
138
|
-
|
|
139
|
-
| Style | Description | Example |
|
|
140
|
-
-------|-------------|--------|
|
|
141
|
-
| `nerd` | Nerd Font glyphs (default, requires Nerd Font terminal) | , , |
|
|
142
|
-
| `emoji` | Unicode emoji/symbols (works on most terminals) | ⚡, ◧, $] |
|
|
143
|
-
| `text` | Plain text abbreviations (works everywhere, most compact) | evt, cmp, $] |
|
|
98
|
+
### Icon Styles
|
|
144
99
|
|
|
145
|
-
|
|
146
|
-
|
|
100
|
+
| Style | Description |
|
|
101
|
+
|-------|-------------|
|
|
102
|
+
| `nerd` | Nerd Font glyphs (auto-detected) |
|
|
103
|
+
| `emoji` | Unicode symbols (works on most terminals) |
|
|
104
|
+
| `text` | Plain text abbreviations (works everywhere) |
|
|
147
105
|
|
|
148
|
-
|
|
106
|
+
When `iconStyle` is not set, footer auto-detects Nerd Font support and defaults to `nerd` if available, `emoji` otherwise.
|
|
149
107
|
|
|
150
|
-
|
|
151
|
-
- **Data provider failures:** Segments hide when data unavailable (graceful degradation)
|
|
152
|
-
- **Config parse failures:** Fall back to default preset with warning
|
|
153
|
-
- **Module loading order:** Footer works even if packages load after it — late-arriving events update cache
|
|
108
|
+
### Responsive Layout
|
|
154
109
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
# Run tests
|
|
159
|
-
pnpm test
|
|
110
|
+
```
|
|
111
|
+
Wide terminal (>120 cols):
|
|
112
|
+
model | thinking | path | git | context | cost | compactions | tokens_saved | project_count
|
|
160
113
|
|
|
161
|
-
|
|
162
|
-
|
|
114
|
+
Narrow terminal (<120 cols):
|
|
115
|
+
Row 1: model | thinking | path | git | context | cost
|
|
116
|
+
Row 2: compactions | tokens_saved | project_count | ralph | workflow
|
|
163
117
|
```
|
|
164
118
|
|
|
165
|
-
##
|
|
119
|
+
## License
|
|
166
120
|
|
|
167
|
-
|
|
168
|
-
packages/footer/
|
|
169
|
-
├── index.ts # Re-exports
|
|
170
|
-
├── types.ts # Re-exports from src/types.ts
|
|
171
|
-
├── package.json # Package manifest
|
|
172
|
-
├── tsconfig.json # TypeScript config
|
|
173
|
-
├── README.md # This file
|
|
174
|
-
├── src/
|
|
175
|
-
│ ├── index.ts # Extension entry point
|
|
176
|
-
│ ├── types.ts # Type definitions
|
|
177
|
-
│ ├── config.ts # Settings load/save
|
|
178
|
-
│ ├── events.ts # Event subscription wiring
|
|
179
|
-
│ ├── commands.ts # Command registration
|
|
180
|
-
│ ├── presets.ts # Preset definitions
|
|
181
|
-
│ ├── registry/ # FooterRegistry
|
|
182
|
-
│ │ └── index.ts
|
|
183
|
-
│ ├── rendering/ # Rendering engine
|
|
184
|
-
│ │ ├── renderer.ts # FooterRenderer class
|
|
185
|
-
│ │ ├── separators.ts # Separator system
|
|
186
|
-
│ │ ├── theme.ts # Theme color resolution
|
|
187
|
-
│ │ └── icons.ts # Icon system with Nerd Font detection
|
|
188
|
-
│ ├── segments/ # Segment implementations
|
|
189
|
-
│ │ ├── core.ts # Core segments (model, path, git, etc.)
|
|
190
|
-
│ │ ├── compactor.ts # Compactor segments
|
|
191
|
-
│ │ ├── memory.ts # Memory segments
|
|
192
|
-
│ │ ├── mcp.ts # MCP segments
|
|
193
|
-
│ │ ├── ralph.ts # Ralph segments
|
|
194
|
-
│ │ ├── workflow.ts # Workflow segments
|
|
195
|
-
│ │ ├── kanboard.ts # Kanboard segments
|
|
196
|
-
│ │ ├── notify.ts # Notify segments
|
|
197
|
-
│ │ └── status-ext.ts # Extension statuses segment
|
|
198
|
-
│ └── tui/
|
|
199
|
-
│ └── settings-tui.ts # Settings overlay TUI
|
|
200
|
-
└── tests/ # Unit tests
|
|
201
|
-
├── separators.test.ts
|
|
202
|
-
├── registry.test.ts
|
|
203
|
-
├── config.test.ts
|
|
204
|
-
├── segments.test.ts
|
|
205
|
-
└── events.test.ts
|
|
206
|
-
```
|
|
121
|
+
MIT
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -8,34 +8,9 @@
|
|
|
8
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { UNIPI_PREFIX, FOOTER_COMMANDS } from "@pi-unipi/core";
|
|
10
10
|
import { loadFooterSettings, saveFooterSettings } from "./config.js";
|
|
11
|
-
import { PRESET_NAMES } from "./presets.js";
|
|
12
11
|
import { showFooterSettings } from "./tui/settings-tui.js";
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
/** Minimal autocomplete item (compatible with pi-tui AutocompleteItem) */
|
|
17
|
-
interface ArgSuggestion {
|
|
18
|
-
value: string;
|
|
19
|
-
label: string;
|
|
20
|
-
description?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** All valid separator styles */
|
|
24
|
-
const SEPARATOR_STYLES: SeparatorStyle[] = [
|
|
25
|
-
"powerline",
|
|
26
|
-
"powerline-thin",
|
|
27
|
-
"slash",
|
|
28
|
-
"pipe",
|
|
29
|
-
"dot",
|
|
30
|
-
"ascii",
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
/** All valid icon styles */
|
|
34
|
-
const ICON_STYLES: IconStyle[] = [
|
|
35
|
-
"nerd",
|
|
36
|
-
"emoji",
|
|
37
|
-
"text",
|
|
38
|
-
];
|
|
12
|
+
import { showFooterHelp } from "./help.js";
|
|
13
|
+
import type { FooterGroup, FooterSegment } from "./types.js";
|
|
39
14
|
|
|
40
15
|
/** Extension state interface */
|
|
41
16
|
interface FooterState {
|
|
@@ -46,6 +21,7 @@ interface FooterState {
|
|
|
46
21
|
getPresetName(): string;
|
|
47
22
|
resetLayoutCache(): void;
|
|
48
23
|
};
|
|
24
|
+
segmentLookup: Map<string, FooterSegment>;
|
|
49
25
|
piContext: unknown;
|
|
50
26
|
setupUI: ((pi: ExtensionAPI, ctx: any) => void) | null;
|
|
51
27
|
}
|
|
@@ -58,69 +34,13 @@ export function registerCommands(
|
|
|
58
34
|
state: FooterState,
|
|
59
35
|
groups?: FooterGroup[],
|
|
60
36
|
): void {
|
|
61
|
-
// /unipi:footer — toggle
|
|
37
|
+
// /unipi:footer — toggle on/off only
|
|
62
38
|
pi.registerCommand(`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER}`, {
|
|
63
|
-
description: "Toggle footer
|
|
64
|
-
getArgumentCompletions(argumentPrefix: string): ArgSuggestion[] | null {
|
|
65
|
-
const allOptions: ArgSuggestion[] = [
|
|
66
|
-
...PRESET_NAMES.map(p => ({
|
|
67
|
-
value: p,
|
|
68
|
-
label: p,
|
|
69
|
-
description: `Switch to ${p} preset`,
|
|
70
|
-
})),
|
|
71
|
-
...SEPARATOR_STYLES.map(s => ({
|
|
72
|
-
value: `sep:${s}`,
|
|
73
|
-
label: `sep:${s}`,
|
|
74
|
-
description: `Set separator style: ${s}`,
|
|
75
|
-
})),
|
|
76
|
-
...ICON_STYLES.map(s => ({
|
|
77
|
-
value: `icon:${s}`,
|
|
78
|
-
label: `icon:${s}`,
|
|
79
|
-
description: `Set icon style: ${s}`,
|
|
80
|
-
})),
|
|
81
|
-
{
|
|
82
|
-
value: "on",
|
|
83
|
-
label: "on",
|
|
84
|
-
description: "Enable footer",
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
value: "off",
|
|
88
|
-
label: "off",
|
|
89
|
-
description: "Disable footer",
|
|
90
|
-
},
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
if (!argumentPrefix) return allOptions;
|
|
94
|
-
|
|
95
|
-
const prefix = argumentPrefix.toLowerCase();
|
|
96
|
-
const filtered = allOptions.filter(o =>
|
|
97
|
-
o.value.toLowerCase().startsWith(prefix),
|
|
98
|
-
);
|
|
99
|
-
return filtered.length > 0 ? filtered : null;
|
|
100
|
-
},
|
|
39
|
+
description: "Toggle footer on/off",
|
|
101
40
|
handler: async (args, ctx) => {
|
|
102
|
-
|
|
103
|
-
// Toggle on/off
|
|
104
|
-
state.enabled = !state.enabled;
|
|
105
|
-
state.renderer.setActive(state.enabled);
|
|
106
|
-
|
|
107
|
-
if (state.enabled) {
|
|
108
|
-
state.setupUI?.(pi, ctx);
|
|
109
|
-
ctx.ui.notify("Footer enabled", "info");
|
|
110
|
-
} else {
|
|
111
|
-
ctx.ui.setFooter(undefined);
|
|
112
|
-
ctx.ui.setWidget("footer-top", undefined);
|
|
113
|
-
ctx.ui.setWidget("footer-secondary", undefined);
|
|
114
|
-
ctx.ui.notify("Footer disabled", "info");
|
|
115
|
-
}
|
|
41
|
+
const arg = args?.trim().toLowerCase();
|
|
116
42
|
|
|
117
|
-
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const arg = args.trim().toLowerCase();
|
|
122
|
-
|
|
123
|
-
// on / off
|
|
43
|
+
// on
|
|
124
44
|
if (arg === "on") {
|
|
125
45
|
state.enabled = true;
|
|
126
46
|
state.renderer.setActive(true);
|
|
@@ -129,6 +49,8 @@ export function registerCommands(
|
|
|
129
49
|
ctx.ui.notify("Footer enabled", "info");
|
|
130
50
|
return;
|
|
131
51
|
}
|
|
52
|
+
|
|
53
|
+
// off
|
|
132
54
|
if (arg === "off") {
|
|
133
55
|
state.enabled = false;
|
|
134
56
|
state.renderer.setActive(false);
|
|
@@ -140,42 +62,21 @@ export function registerCommands(
|
|
|
140
62
|
return;
|
|
141
63
|
}
|
|
142
64
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (SEPARATOR_STYLES.includes(style)) {
|
|
147
|
-
saveFooterSettings({ separator: style });
|
|
148
|
-
state.renderer.resetLayoutCache();
|
|
149
|
-
ctx.ui.notify(`Separator: ${style}`, "info");
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
ctx.ui.notify(`Unknown separator. Available: ${SEPARATOR_STYLES.join(", ")}`, "warning");
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// icon:<style> — change icon style
|
|
157
|
-
if (arg.startsWith("icon:")) {
|
|
158
|
-
const style = arg.slice(5) as IconStyle;
|
|
159
|
-
if (ICON_STYLES.includes(style)) {
|
|
160
|
-
saveFooterSettings({ iconStyle: style });
|
|
161
|
-
setIconStyle(style);
|
|
162
|
-
state.renderer.resetLayoutCache();
|
|
163
|
-
ctx.ui.notify(`Icon style: ${style}`, "info");
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
ctx.ui.notify(`Unknown icon style. Available: ${ICON_STYLES.join(", ")}`, "warning");
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
65
|
+
// Toggle (no args or unknown args)
|
|
66
|
+
state.enabled = !state.enabled;
|
|
67
|
+
state.renderer.setActive(state.enabled);
|
|
169
68
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
ctx.ui.
|
|
175
|
-
|
|
69
|
+
if (state.enabled) {
|
|
70
|
+
state.setupUI?.(pi, ctx);
|
|
71
|
+
ctx.ui.notify("Footer enabled", "info");
|
|
72
|
+
} else {
|
|
73
|
+
ctx.ui.setFooter(undefined);
|
|
74
|
+
ctx.ui.setWidget("footer-top", undefined);
|
|
75
|
+
ctx.ui.setWidget("footer-secondary", undefined);
|
|
76
|
+
ctx.ui.notify("Footer disabled", "info");
|
|
176
77
|
}
|
|
177
78
|
|
|
178
|
-
|
|
79
|
+
saveFooterSettings({ enabled: state.enabled });
|
|
179
80
|
},
|
|
180
81
|
});
|
|
181
82
|
|
|
@@ -189,7 +90,12 @@ export function registerCommands(
|
|
|
189
90
|
}
|
|
190
91
|
|
|
191
92
|
if (groups && groups.length > 0) {
|
|
192
|
-
showFooterSettings(ctx, groups)
|
|
93
|
+
showFooterSettings(ctx, groups, () => {
|
|
94
|
+
// Re-read settings and update renderer
|
|
95
|
+
const updated = loadFooterSettings();
|
|
96
|
+
state.renderer.setPreset(updated.preset);
|
|
97
|
+
state.renderer.resetLayoutCache();
|
|
98
|
+
});
|
|
193
99
|
} else {
|
|
194
100
|
// Fallback: show text summary
|
|
195
101
|
const settings = loadFooterSettings();
|
|
@@ -204,4 +110,13 @@ export function registerCommands(
|
|
|
204
110
|
}
|
|
205
111
|
},
|
|
206
112
|
});
|
|
113
|
+
|
|
114
|
+
// /unipi:footer-help — show help overlay
|
|
115
|
+
pi.registerCommand(`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_HELP}`, {
|
|
116
|
+
description: "Show footer segment guide (icons, labels, descriptions)",
|
|
117
|
+
handler: async (_args, _ctx) => {
|
|
118
|
+
const allSegments = Array.from(state.segmentLookup.values());
|
|
119
|
+
showFooterHelp(pi, allSegments, state.renderer.getPresetName());
|
|
120
|
+
},
|
|
121
|
+
});
|
|
207
122
|
}
|
package/src/config.ts
CHANGED
|
@@ -17,6 +17,8 @@ export const DEFAULT_FOOTER_SETTINGS: FooterSettings = {
|
|
|
17
17
|
preset: "default",
|
|
18
18
|
separator: "powerline-thin",
|
|
19
19
|
iconStyle: "nerd",
|
|
20
|
+
zoneSeparator: "\u2502", // │
|
|
21
|
+
showFullLabels: false,
|
|
20
22
|
groups: {
|
|
21
23
|
core: { show: true, segments: {} },
|
|
22
24
|
compactor: { show: true, segments: {} },
|
|
@@ -92,6 +94,8 @@ export function loadFooterSettings(): FooterSettings {
|
|
|
92
94
|
preset: typeof footer.preset === "string" ? footer.preset : DEFAULT_FOOTER_SETTINGS.preset,
|
|
93
95
|
separator: isValidSeparator(footer.separator) ? footer.separator as SeparatorStyle : DEFAULT_FOOTER_SETTINGS.separator,
|
|
94
96
|
iconStyle: isValidIconStyle(footer.iconStyle) ? footer.iconStyle as IconStyle : DEFAULT_FOOTER_SETTINGS.iconStyle,
|
|
97
|
+
zoneSeparator: typeof footer.zoneSeparator === "string" ? footer.zoneSeparator : DEFAULT_FOOTER_SETTINGS.zoneSeparator,
|
|
98
|
+
showFullLabels: typeof footer.showFullLabels === "boolean" ? footer.showFullLabels : DEFAULT_FOOTER_SETTINGS.showFullLabels,
|
|
95
99
|
groups: mergeGroupSettings(
|
|
96
100
|
DEFAULT_FOOTER_SETTINGS.groups,
|
|
97
101
|
footer.groups as Record<string, FooterGroupSettings> | undefined,
|
package/src/help.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — Footer help overlay
|
|
3
|
+
*
|
|
4
|
+
* Shows an overlay listing all enabled segments grouped by zone,
|
|
5
|
+
* with icons, short labels, and descriptions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { FooterSegment, SegmentZone } from "./types.js";
|
|
10
|
+
import { getIcon } from "./rendering/icons.js";
|
|
11
|
+
import { loadFooterSettings, isSegmentEnabled } from "./config.js";
|
|
12
|
+
import { getPreset } from "./presets.js";
|
|
13
|
+
|
|
14
|
+
/** Zone display names and order */
|
|
15
|
+
const ZONE_META: Record<SegmentZone, { title: string; order: number }> = {
|
|
16
|
+
left: { title: "LEFT ZONE (Identity)", order: 0 },
|
|
17
|
+
center: { title: "CENTER ZONE (Metrics)", order: 1 },
|
|
18
|
+
right: { title: "RIGHT ZONE (Time)", order: 2 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Build the help content lines */
|
|
22
|
+
function buildHelpLines(
|
|
23
|
+
segments: FooterSegment[],
|
|
24
|
+
presetName: string,
|
|
25
|
+
): string[] {
|
|
26
|
+
const settings = loadFooterSettings();
|
|
27
|
+
const preset = getPreset(presetName);
|
|
28
|
+
const enabledIds = new Set([
|
|
29
|
+
...preset.leftSegments,
|
|
30
|
+
...preset.rightSegments,
|
|
31
|
+
...preset.secondarySegments,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// Filter to enabled segments only
|
|
35
|
+
const enabled = segments.filter(seg => {
|
|
36
|
+
if (!enabledIds.has(seg.id)) return false;
|
|
37
|
+
return isSegmentEnabled(getGroupForSegment(seg.id), seg.id);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (enabled.length === 0) {
|
|
41
|
+
return ["No segments enabled."];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Group by zone
|
|
45
|
+
const zones: Record<SegmentZone, FooterSegment[]> = { left: [], center: [], right: [] };
|
|
46
|
+
for (const seg of enabled) {
|
|
47
|
+
zones[seg.zone].push(seg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lines: string[] = [];
|
|
51
|
+
|
|
52
|
+
for (const zoneKey of (["left", "center", "right"] as SegmentZone[])) {
|
|
53
|
+
const zoneSegs = zones[zoneKey];
|
|
54
|
+
if (zoneSegs.length === 0) continue;
|
|
55
|
+
|
|
56
|
+
const meta = ZONE_META[zoneKey];
|
|
57
|
+
lines.push(` ${meta.title}`);
|
|
58
|
+
lines.push("");
|
|
59
|
+
|
|
60
|
+
for (const seg of zoneSegs) {
|
|
61
|
+
const icon = getIcon(seg.id);
|
|
62
|
+
const label = seg.shortLabel;
|
|
63
|
+
const desc = seg.description;
|
|
64
|
+
const iconStr = icon ? `${icon} ` : " ";
|
|
65
|
+
lines.push(` ${iconStr}${label.padEnd(6)} ${desc}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Simple group lookup for help */
|
|
75
|
+
function getGroupForSegment(segId: string): string {
|
|
76
|
+
const coreIds = ["model", "api_state", "tool_count", "git", "context_pct", "cost", "tokens_total", "tokens_in", "tokens_out", "session", "hostname", "time", "tps", "clock", "duration", "thinking_level"];
|
|
77
|
+
if (coreIds.includes(segId)) return "core";
|
|
78
|
+
const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "indexed_docs", "sandbox_runs", "search_queries"];
|
|
79
|
+
if (compactorIds.includes(segId)) return "compactor";
|
|
80
|
+
if (["project_count", "total_count", "consolidations"].includes(segId)) return "memory";
|
|
81
|
+
if (["servers_total", "servers_active", "tools_total", "servers_failed"].includes(segId)) return "mcp";
|
|
82
|
+
if (["active_loops", "total_iterations", "loop_status"].includes(segId)) return "ralph";
|
|
83
|
+
if (["current_command", "sandbox_level", "command_duration"].includes(segId)) return "workflow";
|
|
84
|
+
if (["docs_count", "tasks_done", "tasks_total", "task_pct"].includes(segId)) return "kanboard";
|
|
85
|
+
if (["platforms_enabled", "last_sent"].includes(segId)) return "notify";
|
|
86
|
+
if (segId === "extension_statuses") return "status_ext";
|
|
87
|
+
return "core";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Show the footer help overlay.
|
|
92
|
+
* Lists all enabled segments grouped by zone with descriptions.
|
|
93
|
+
*/
|
|
94
|
+
export function showFooterHelp(
|
|
95
|
+
pi: ExtensionAPI,
|
|
96
|
+
segments: FooterSegment[],
|
|
97
|
+
presetName: string,
|
|
98
|
+
): void {
|
|
99
|
+
const lines = buildHelpLines(segments, presetName);
|
|
100
|
+
|
|
101
|
+
// Use pi's custom UI overlay
|
|
102
|
+
const ctx = (pi as any)._ctx;
|
|
103
|
+
if (ctx?.ui?.custom) {
|
|
104
|
+
ctx.ui.custom((tui: any) => {
|
|
105
|
+
let scrollOffset = 0;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
dispose() {},
|
|
109
|
+
render(width: number, height: number): string[] {
|
|
110
|
+
const maxVisible = height - 2; // border lines
|
|
111
|
+
const visibleLines = lines.slice(scrollOffset, scrollOffset + maxVisible);
|
|
112
|
+
|
|
113
|
+
const result: string[] = [];
|
|
114
|
+
|
|
115
|
+
// Top border
|
|
116
|
+
const title = " ? Footer Segment Guide ";
|
|
117
|
+
const borderLen = Math.max(width - 2, title.length + 4);
|
|
118
|
+
result.push(`\x1b[2m┌${"─".repeat(borderLen)}┐\x1b[0m`);
|
|
119
|
+
|
|
120
|
+
// Title
|
|
121
|
+
result.push(`\x1b[2m│\x1b[0m \x1b[1m${title}\x1b[0m${" ".repeat(Math.max(0, borderLen - title.length - 1))}\x1b[2m│\x1b[0m`);
|
|
122
|
+
|
|
123
|
+
// Content
|
|
124
|
+
for (const line of visibleLines) {
|
|
125
|
+
const padded = line.length > borderLen - 2
|
|
126
|
+
? line.slice(0, borderLen - 2)
|
|
127
|
+
: line + " ".repeat(Math.max(0, borderLen - 2 - line.length));
|
|
128
|
+
result.push(`\x1b[2m│\x1b[0m ${padded} \x1b[2m│\x1b[0m`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Bottom border
|
|
132
|
+
result.push(`\x1b[2m├${"─".repeat(borderLen)}┤\x1b[0m`);
|
|
133
|
+
result.push(`\x1b[2m│\x1b[0m \x1b[2m↑↓ scroll · q close\x1b[0m${" ".repeat(Math.max(0, borderLen - 20))} \x1b[2m│\x1b[0m`);
|
|
134
|
+
result.push(`\x1b[2m└${"─".repeat(borderLen)}┘\x1b[0m`);
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
},
|
|
138
|
+
handleInput(key: string): boolean {
|
|
139
|
+
if (key === "q" || key === "Escape" || key === "Enter") {
|
|
140
|
+
return false; // Close overlay
|
|
141
|
+
}
|
|
142
|
+
if (key === "ArrowUp" || key === "k") {
|
|
143
|
+
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (key === "ArrowDown" || key === "j") {
|
|
147
|
+
scrollOffset = Math.min(Math.max(0, lines.length - 5), scrollOffset + 1);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return true; // Consume all other keys
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
// Fallback: print to console
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
console.log(line);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|