@pi-unipi/footer 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -0
- package/index.ts +6 -0
- package/package.json +52 -0
- package/src/commands.ts +204 -0
- package/src/config.ts +177 -0
- package/src/events.ts +256 -0
- package/src/index.ts +208 -0
- package/src/presets.ts +131 -0
- package/src/registry/index.ts +162 -0
- package/src/rendering/icons.ts +318 -0
- package/src/rendering/renderer.ts +310 -0
- package/src/rendering/separators.ts +112 -0
- package/src/rendering/theme.ts +98 -0
- package/src/segments/compactor.ts +135 -0
- package/src/segments/core.ts +283 -0
- package/src/segments/kanboard.ts +75 -0
- package/src/segments/mcp.ts +100 -0
- package/src/segments/memory.ts +140 -0
- package/src/segments/notify.ts +50 -0
- package/src/segments/ralph.ts +109 -0
- package/src/segments/status-ext.ts +119 -0
- package/src/segments/workflow.ts +100 -0
- package/src/tui/settings-tui.ts +252 -0
- package/src/types.ts +183 -0
package/src/events.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — Event subscription wiring
|
|
3
|
+
*
|
|
4
|
+
* Wires FooterRegistry to UNIPI_EVENTS for all relevant events.
|
|
5
|
+
* Each event handler updates the registry cache for the appropriate group.
|
|
6
|
+
*
|
|
7
|
+
* Note: pi.events.on() returns an unsubscribe function directly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { UNIPI_EVENTS } from "@pi-unipi/core";
|
|
12
|
+
import type { FooterRegistry } from "./registry/index.js";
|
|
13
|
+
|
|
14
|
+
/** Cleanup function returned by subscribeToEvents */
|
|
15
|
+
type UnsubscribeFn = () => void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Subscribe to all relevant UNIPI_EVENTS and wire them to the registry.
|
|
19
|
+
* Returns an unsubscribe function for cleanup on session shutdown.
|
|
20
|
+
*/
|
|
21
|
+
export function subscribeToEvents(
|
|
22
|
+
pi: ExtensionAPI,
|
|
23
|
+
registry: FooterRegistry,
|
|
24
|
+
): UnsubscribeFn {
|
|
25
|
+
const unsubscribers: UnsubscribeFn[] = [];
|
|
26
|
+
|
|
27
|
+
// ─── Compactor events ───────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
unsubscribers.push(
|
|
30
|
+
pi.events.on(UNIPI_EVENTS.COMPACTOR_STATS_UPDATED, (event: unknown) => {
|
|
31
|
+
try {
|
|
32
|
+
registry.updateData("compactor", event);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error("[footer] Compactor stats handler error:", err);
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
unsubscribers.push(
|
|
40
|
+
pi.events.on(UNIPI_EVENTS.COMPACTOR_COMPACTED, (event: unknown) => {
|
|
41
|
+
try {
|
|
42
|
+
const existing = registry.getGroupData("compactor") as Record<string, unknown> | undefined;
|
|
43
|
+
registry.updateData("compactor", { ...existing, lastCompaction: event });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error("[footer] Compaction handler error:", err);
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// ─── Memory events ─────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
unsubscribers.push(
|
|
53
|
+
pi.events.on(UNIPI_EVENTS.MEMORY_STORED, (event: unknown) => {
|
|
54
|
+
try {
|
|
55
|
+
const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
|
|
56
|
+
registry.updateData("memory", { ...existing, lastStored: event });
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error("[footer] Memory stored handler error:", err);
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
unsubscribers.push(
|
|
64
|
+
pi.events.on(UNIPI_EVENTS.MEMORY_DELETED, (event: unknown) => {
|
|
65
|
+
try {
|
|
66
|
+
const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
|
|
67
|
+
registry.updateData("memory", { ...existing, lastDeleted: event });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error("[footer] Memory deleted handler error:", err);
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
unsubscribers.push(
|
|
75
|
+
pi.events.on(UNIPI_EVENTS.MEMORY_CONSOLIDATED, (event: unknown) => {
|
|
76
|
+
try {
|
|
77
|
+
const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
|
|
78
|
+
registry.updateData("memory", { ...existing, lastConsolidated: event });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error("[footer] Memory consolidated handler error:", err);
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// ─── MCP events ────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
unsubscribers.push(
|
|
88
|
+
pi.events.on(UNIPI_EVENTS.MCP_SERVER_STARTED, (event: unknown) => {
|
|
89
|
+
try {
|
|
90
|
+
const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
|
|
91
|
+
const evt = event as Record<string, unknown> | undefined;
|
|
92
|
+
const toolCount = typeof evt?.toolCount === "number" ? evt.toolCount : 0;
|
|
93
|
+
const serversTotal = (typeof existing?.serversTotal === "number" ? existing.serversTotal : 0) + 1;
|
|
94
|
+
const serversActive = (typeof existing?.serversActive === "number" ? existing.serversActive : 0) + 1;
|
|
95
|
+
const toolsTotal = (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) + toolCount;
|
|
96
|
+
registry.updateData("mcp", { ...existing, serversTotal, serversActive, toolsTotal, lastServerStarted: event });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error("[footer] MCP server started handler error:", err);
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
unsubscribers.push(
|
|
104
|
+
pi.events.on(UNIPI_EVENTS.MCP_SERVER_STOPPED, (event: unknown) => {
|
|
105
|
+
try {
|
|
106
|
+
const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
|
|
107
|
+
const evt = event as Record<string, unknown> | undefined;
|
|
108
|
+
const stoppedName = typeof evt?.name === "string" ? evt.name : "";
|
|
109
|
+
const serversActive = Math.max(0, (typeof existing?.serversActive === "number" ? existing.serversActive : 1) - 1);
|
|
110
|
+
// Subtract tools for this server if tracked
|
|
111
|
+
const lastStartedTools = existing?.lastServerStarted as Record<string, unknown> | undefined;
|
|
112
|
+
const lastStartedName = typeof lastStartedTools?.name === "string" ? lastStartedTools.name : "";
|
|
113
|
+
const lastStartedCount = typeof lastStartedTools?.toolCount === "number" ? lastStartedTools.toolCount : 0;
|
|
114
|
+
let toolsTotal = typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0;
|
|
115
|
+
if (stoppedName && stoppedName === lastStartedName) {
|
|
116
|
+
toolsTotal = Math.max(0, toolsTotal - lastStartedCount);
|
|
117
|
+
}
|
|
118
|
+
registry.updateData("mcp", { ...existing, serversActive, toolsTotal, lastServerStopped: event });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error("[footer] MCP server stopped handler error:", err);
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
unsubscribers.push(
|
|
126
|
+
pi.events.on(UNIPI_EVENTS.MCP_SERVER_ERROR, (event: unknown) => {
|
|
127
|
+
try {
|
|
128
|
+
const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
|
|
129
|
+
const serversTotal = (typeof existing?.serversTotal === "number" ? existing.serversTotal : 0) + 1;
|
|
130
|
+
const serversFailed = (typeof existing?.serversFailed === "number" ? existing.serversFailed : 0) + 1;
|
|
131
|
+
registry.updateData("mcp", { ...existing, serversTotal, serversFailed, lastServerError: event });
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error("[footer] MCP server error handler error:", err);
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
unsubscribers.push(
|
|
139
|
+
pi.events.on(UNIPI_EVENTS.MCP_TOOLS_REGISTERED, (event: unknown) => {
|
|
140
|
+
try {
|
|
141
|
+
const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
|
|
142
|
+
const evt = event as Record<string, unknown> | undefined;
|
|
143
|
+
const toolNames = Array.isArray(evt?.toolNames) ? evt.toolNames : [];
|
|
144
|
+
const toolsTotal = (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) + toolNames.length;
|
|
145
|
+
registry.updateData("mcp", { ...existing, toolsTotal, lastToolsRegistered: event });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error("[footer] MCP tools registered handler error:", err);
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
unsubscribers.push(
|
|
153
|
+
pi.events.on(UNIPI_EVENTS.MCP_TOOLS_UNREGISTERED, (event: unknown) => {
|
|
154
|
+
try {
|
|
155
|
+
const existing = registry.getGroupData("mcp") as Record<string, unknown> | undefined;
|
|
156
|
+
const evt = event as Record<string, unknown> | undefined;
|
|
157
|
+
const toolNames = Array.isArray(evt?.toolNames) ? evt.toolNames : [];
|
|
158
|
+
const toolsTotal = Math.max(0, (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) - toolNames.length);
|
|
159
|
+
registry.updateData("mcp", { ...existing, toolsTotal, lastToolsUnregistered: event });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error("[footer] MCP tools unregistered handler error:", err);
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// ─── Ralph events ──────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
unsubscribers.push(
|
|
169
|
+
pi.events.on(UNIPI_EVENTS.RALPH_LOOP_START, (event: unknown) => {
|
|
170
|
+
try {
|
|
171
|
+
registry.updateData("ralph", { ...(event as Record<string, unknown>), active: true });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error("[footer] Ralph loop start handler error:", err);
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
unsubscribers.push(
|
|
179
|
+
pi.events.on(UNIPI_EVENTS.RALPH_LOOP_END, (event: unknown) => {
|
|
180
|
+
try {
|
|
181
|
+
registry.updateData("ralph", { ...(event as Record<string, unknown>), active: false });
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error("[footer] Ralph loop end handler error:", err);
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
unsubscribers.push(
|
|
189
|
+
pi.events.on(UNIPI_EVENTS.RALPH_ITERATION_DONE, (event: unknown) => {
|
|
190
|
+
try {
|
|
191
|
+
const existing = registry.getGroupData("ralph") as Record<string, unknown> | undefined;
|
|
192
|
+
registry.updateData("ralph", { ...existing, lastIteration: event });
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error("[footer] Ralph iteration handler error:", err);
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// ─── Workflow events ───────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
unsubscribers.push(
|
|
202
|
+
pi.events.on(UNIPI_EVENTS.WORKFLOW_START, (event: unknown) => {
|
|
203
|
+
try {
|
|
204
|
+
registry.updateData("workflow", { ...(event as Record<string, unknown>), active: true, startTime: Date.now() });
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error("[footer] Workflow start handler error:", err);
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
unsubscribers.push(
|
|
212
|
+
pi.events.on(UNIPI_EVENTS.WORKFLOW_END, (event: unknown) => {
|
|
213
|
+
try {
|
|
214
|
+
registry.updateData("workflow", { ...(event as Record<string, unknown>), active: false });
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error("[footer] Workflow end handler error:", err);
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// ─── Notification events ───────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
unsubscribers.push(
|
|
224
|
+
pi.events.on(UNIPI_EVENTS.NOTIFICATION_SENT, (event: unknown) => {
|
|
225
|
+
try {
|
|
226
|
+
registry.updateData("notify", event);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error("[footer] Notification handler error:", err);
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// ─── Module ready events ───────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
unsubscribers.push(
|
|
236
|
+
pi.events.on(UNIPI_EVENTS.MODULE_READY, (_event: unknown) => {
|
|
237
|
+
try {
|
|
238
|
+
// Invalidate all caches when new modules load — they may bring fresh data
|
|
239
|
+
registry.invalidateAll();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error("[footer] Module ready handler error:", err);
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Return composite unsubscribe function
|
|
247
|
+
return () => {
|
|
248
|
+
for (const unsub of unsubscribers) {
|
|
249
|
+
try {
|
|
250
|
+
unsub();
|
|
251
|
+
} catch {
|
|
252
|
+
// Ignore cleanup errors
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — Extension entry point
|
|
3
|
+
*
|
|
4
|
+
* Main extension function that registers commands, subscribes to events,
|
|
5
|
+
* initializes renderer on session_start.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS } from "@pi-unipi/core";
|
|
10
|
+
import { FooterRegistry, getFooterRegistry } from "./registry/index.js";
|
|
11
|
+
import { FooterRenderer } from "./rendering/renderer.js";
|
|
12
|
+
import { subscribeToEvents } from "./events.js";
|
|
13
|
+
import { loadFooterSettings, saveFooterSettings } from "./config.js";
|
|
14
|
+
import { getPreset } from "./presets.js";
|
|
15
|
+
import { registerCommands } from "./commands.js";
|
|
16
|
+
|
|
17
|
+
// Import segment groups
|
|
18
|
+
import { CORE_SEGMENTS } from "./segments/core.js";
|
|
19
|
+
import { COMPACTOR_SEGMENTS } from "./segments/compactor.js";
|
|
20
|
+
import { MEMORY_SEGMENTS } from "./segments/memory.js";
|
|
21
|
+
import { MCP_SEGMENTS } from "./segments/mcp.js";
|
|
22
|
+
import { RALPH_SEGMENTS } from "./segments/ralph.js";
|
|
23
|
+
import { WORKFLOW_SEGMENTS } from "./segments/workflow.js";
|
|
24
|
+
import { KANBOARD_SEGMENTS } from "./segments/kanboard.js";
|
|
25
|
+
import { NOTIFY_SEGMENTS } from "./segments/notify.js";
|
|
26
|
+
import { STATUS_EXT_SEGMENTS } from "./segments/status-ext.js";
|
|
27
|
+
|
|
28
|
+
import type { FooterGroup, FooterSegment } from "./types.js";
|
|
29
|
+
import { getThinkingLevel, rainbowBorder } from "./segments/core.js";
|
|
30
|
+
|
|
31
|
+
/** All segment groups */
|
|
32
|
+
const ALL_GROUPS: FooterGroup[] = [
|
|
33
|
+
{ id: "core", name: "Core", icon: "", segments: CORE_SEGMENTS, defaultShow: true },
|
|
34
|
+
{ id: "compactor", name: "Compactor", icon: "", segments: COMPACTOR_SEGMENTS, defaultShow: true },
|
|
35
|
+
{ id: "memory", name: "Memory", icon: "", segments: MEMORY_SEGMENTS, defaultShow: true },
|
|
36
|
+
{ id: "mcp", name: "MCP", icon: "", segments: MCP_SEGMENTS, defaultShow: true },
|
|
37
|
+
{ id: "ralph", name: "Ralph", icon: "", segments: RALPH_SEGMENTS, defaultShow: true },
|
|
38
|
+
{ id: "workflow", name: "Workflow", icon: "", segments: WORKFLOW_SEGMENTS, defaultShow: true },
|
|
39
|
+
{ id: "kanboard", name: "Kanboard", icon: "", segments: KANBOARD_SEGMENTS, defaultShow: true },
|
|
40
|
+
{ id: "notify", name: "Notify", icon: "", segments: NOTIFY_SEGMENTS, defaultShow: false },
|
|
41
|
+
{ id: "status_ext", name: "Extensions", icon: "", segments: STATUS_EXT_SEGMENTS, defaultShow: true },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Build a segment lookup from all groups */
|
|
45
|
+
function buildSegmentLookup(): Map<string, FooterSegment> {
|
|
46
|
+
const map = new Map<string, FooterSegment>();
|
|
47
|
+
for (const group of ALL_GROUPS) {
|
|
48
|
+
for (const segment of group.segments) {
|
|
49
|
+
map.set(segment.id, segment);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return map;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Extension state */
|
|
56
|
+
interface FooterState {
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
registry: FooterRegistry;
|
|
59
|
+
renderer: FooterRenderer;
|
|
60
|
+
segmentLookup: Map<string, FooterSegment>;
|
|
61
|
+
unsubscribeEvents: (() => void) | null;
|
|
62
|
+
piContext: unknown;
|
|
63
|
+
footerData: unknown;
|
|
64
|
+
tuiRef: any;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default function footerExtension(pi: ExtensionAPI): void {
|
|
68
|
+
// Build segment lookup
|
|
69
|
+
const segmentLookup = buildSegmentLookup();
|
|
70
|
+
|
|
71
|
+
// Create state
|
|
72
|
+
const state: FooterState = {
|
|
73
|
+
enabled: true,
|
|
74
|
+
registry: getFooterRegistry(),
|
|
75
|
+
renderer: new FooterRenderer(
|
|
76
|
+
getFooterRegistry(),
|
|
77
|
+
{ get: (id: string) => segmentLookup.get(id) },
|
|
78
|
+
loadFooterSettings().preset,
|
|
79
|
+
),
|
|
80
|
+
segmentLookup,
|
|
81
|
+
unsubscribeEvents: null,
|
|
82
|
+
piContext: null,
|
|
83
|
+
footerData: null,
|
|
84
|
+
tuiRef: null,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Register all groups in the registry
|
|
88
|
+
for (const group of ALL_GROUPS) {
|
|
89
|
+
state.registry.registerGroup(group);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Session lifecycle ──────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
95
|
+
const settings = loadFooterSettings();
|
|
96
|
+
state.enabled = settings.enabled;
|
|
97
|
+
state.piContext = ctx;
|
|
98
|
+
state.renderer.setPreset(settings.preset);
|
|
99
|
+
state.renderer.setActive(settings.enabled);
|
|
100
|
+
|
|
101
|
+
if (!settings.enabled || !ctx.hasUI) return;
|
|
102
|
+
|
|
103
|
+
// Subscribe to events
|
|
104
|
+
state.unsubscribeEvents = subscribeToEvents(pi, state.registry);
|
|
105
|
+
|
|
106
|
+
// Setup footer + widgets
|
|
107
|
+
setupFooterUI(pi, ctx, state);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
pi.on("session_shutdown", async () => {
|
|
111
|
+
state.renderer.setActive(false);
|
|
112
|
+
state.unsubscribeEvents?.();
|
|
113
|
+
state.unsubscribeEvents = null;
|
|
114
|
+
state.piContext = null;
|
|
115
|
+
state.footerData = null;
|
|
116
|
+
state.tuiRef = null;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ─── Register commands ──────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
registerCommands(pi, state, ALL_GROUPS);
|
|
122
|
+
|
|
123
|
+
// ─── Emit MODULE_READY ──────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
pi.on("session_start", async () => {
|
|
126
|
+
emitEvent(pi as any, UNIPI_EVENTS.MODULE_READY, {
|
|
127
|
+
name: "@pi-unipi/footer",
|
|
128
|
+
version: "0.1.0",
|
|
129
|
+
commands: [`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER}`, `${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_SETTINGS}`],
|
|
130
|
+
tools: [],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Footer UI setup ────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function setupFooterUI(pi: ExtensionAPI, ctx: any, state: FooterState): void {
|
|
138
|
+
// Register footer (minimal — handles branch changes)
|
|
139
|
+
ctx.ui.setFooter((tui: any, _theme: Theme, footerData: any) => {
|
|
140
|
+
state.tuiRef = tui;
|
|
141
|
+
state.footerData = footerData;
|
|
142
|
+
state.renderer.setContext(state.piContext, footerData);
|
|
143
|
+
|
|
144
|
+
const unsub = footerData.onBranchChange(() => {
|
|
145
|
+
state.renderer.resetLayoutCache();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
dispose: unsub,
|
|
150
|
+
invalidate() {
|
|
151
|
+
state.renderer.resetLayoutCache();
|
|
152
|
+
},
|
|
153
|
+
render(): string[] {
|
|
154
|
+
return [];
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Top row widget
|
|
160
|
+
ctx.ui.setWidget("footer-top", (_tui: any, theme: Theme) => {
|
|
161
|
+
// Update the renderer's theme-like
|
|
162
|
+
const themeLike = { fg: (color: string, text: string) => theme.fg(color as any, text) };
|
|
163
|
+
// We need to patch the context with proper theme
|
|
164
|
+
state.renderer.setContext(state.piContext, state.footerData);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
dispose() {},
|
|
168
|
+
invalidate() {
|
|
169
|
+
state.renderer.resetLayoutCache();
|
|
170
|
+
},
|
|
171
|
+
render(width: number): string[] {
|
|
172
|
+
if (!state.enabled || !state.piContext) return [];
|
|
173
|
+
|
|
174
|
+
// Build layout with proper theme by creating segment contexts
|
|
175
|
+
const layout = state.renderer.computeLayout(width);
|
|
176
|
+
return layout.topContent ? [layout.topContent] : [];
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}, { placement: "aboveEditor" });
|
|
180
|
+
|
|
181
|
+
// Secondary row widget + rainbow input border for xhigh thinking
|
|
182
|
+
ctx.ui.setWidget("footer-secondary", (_tui: any, _theme: Theme) => {
|
|
183
|
+
return {
|
|
184
|
+
dispose() {},
|
|
185
|
+
invalidate() {
|
|
186
|
+
state.renderer.resetLayoutCache();
|
|
187
|
+
},
|
|
188
|
+
render(width: number): string[] {
|
|
189
|
+
if (!state.enabled || !state.piContext) return [];
|
|
190
|
+
|
|
191
|
+
const lines: string[] = [];
|
|
192
|
+
|
|
193
|
+
// Rainbow border for input bar when thinking level is xhigh
|
|
194
|
+
const thinkingLevel = getThinkingLevel(state.piContext);
|
|
195
|
+
if (thinkingLevel === "xhigh") {
|
|
196
|
+
lines.push(rainbowBorder(width));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const layout = state.renderer.computeLayout(width);
|
|
200
|
+
if (layout.secondaryContent) {
|
|
201
|
+
lines.push(layout.secondaryContent);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return lines;
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}, { placement: "belowEditor" });
|
|
208
|
+
}
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — Presets system
|
|
3
|
+
*
|
|
4
|
+
* Preset definitions: default, minimal, compact, full, nerd, ascii.
|
|
5
|
+
* Each preset defines which segments appear on left, right, and secondary rows,
|
|
6
|
+
* plus separator style and color scheme.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PresetDef, SeparatorStyle, ColorScheme } from "./types.js";
|
|
10
|
+
import { getDefaultColors } from "./rendering/theme.js";
|
|
11
|
+
|
|
12
|
+
/** Default preset — balanced view */
|
|
13
|
+
const DEFAULT_PRESET: PresetDef = {
|
|
14
|
+
leftSegments: [
|
|
15
|
+
"model", "thinking", "path", "git", "context_pct", "cost",
|
|
16
|
+
],
|
|
17
|
+
rightSegments: [
|
|
18
|
+
"compactions", "tokens_saved", "project_count", "loop_status",
|
|
19
|
+
],
|
|
20
|
+
secondarySegments: [
|
|
21
|
+
"current_command", "session_events",
|
|
22
|
+
],
|
|
23
|
+
separator: "powerline-thin",
|
|
24
|
+
colors: getDefaultColors(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Minimal preset — just the essentials */
|
|
28
|
+
const MINIMAL_PRESET: PresetDef = {
|
|
29
|
+
leftSegments: [
|
|
30
|
+
"path", "git", "context_pct",
|
|
31
|
+
],
|
|
32
|
+
rightSegments: [],
|
|
33
|
+
secondarySegments: [],
|
|
34
|
+
separator: "pipe",
|
|
35
|
+
colors: getDefaultColors(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Compact preset — core + key stats */
|
|
39
|
+
const COMPACT_PRESET: PresetDef = {
|
|
40
|
+
leftSegments: [
|
|
41
|
+
"model", "git", "cost", "context_pct",
|
|
42
|
+
],
|
|
43
|
+
rightSegments: [
|
|
44
|
+
"compactions", "total_count",
|
|
45
|
+
],
|
|
46
|
+
secondarySegments: [],
|
|
47
|
+
separator: "dot",
|
|
48
|
+
colors: getDefaultColors(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Full preset — everything */
|
|
52
|
+
const FULL_PRESET: PresetDef = {
|
|
53
|
+
leftSegments: [
|
|
54
|
+
"model", "thinking", "path", "git", "context_pct", "cost",
|
|
55
|
+
"tokens_total", "tokens_in", "tokens_out",
|
|
56
|
+
],
|
|
57
|
+
rightSegments: [
|
|
58
|
+
"session_events", "compactions", "tokens_saved", "compression_ratio",
|
|
59
|
+
"indexed_docs", "sandbox_runs", "search_queries",
|
|
60
|
+
"project_count", "total_count", "consolidations",
|
|
61
|
+
"servers_total", "servers_active", "tools_total", "servers_failed",
|
|
62
|
+
"active_loops", "total_iterations", "loop_status",
|
|
63
|
+
"current_command", "command_duration",
|
|
64
|
+
"docs_count", "tasks_done", "tasks_total", "task_pct",
|
|
65
|
+
"extension_statuses",
|
|
66
|
+
],
|
|
67
|
+
secondarySegments: [
|
|
68
|
+
"hostname", "time",
|
|
69
|
+
"platforms_enabled", "last_sent",
|
|
70
|
+
],
|
|
71
|
+
separator: "powerline-thin",
|
|
72
|
+
colors: getDefaultColors(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Nerd preset — maximum detail for Nerd Font users */
|
|
76
|
+
const NERD_PRESET: PresetDef = {
|
|
77
|
+
leftSegments: [
|
|
78
|
+
"model", "thinking", "path", "git", "context_pct", "cost",
|
|
79
|
+
"tokens_total",
|
|
80
|
+
],
|
|
81
|
+
rightSegments: [
|
|
82
|
+
"session_events", "compactions", "tokens_saved",
|
|
83
|
+
"project_count", "total_count",
|
|
84
|
+
"servers_total", "servers_active", "tools_total",
|
|
85
|
+
"active_loops", "loop_status",
|
|
86
|
+
"current_command",
|
|
87
|
+
"docs_count", "tasks_done", "tasks_total", "task_pct",
|
|
88
|
+
"extension_statuses",
|
|
89
|
+
],
|
|
90
|
+
secondarySegments: [
|
|
91
|
+
"hostname", "time",
|
|
92
|
+
"compression_ratio", "indexed_docs",
|
|
93
|
+
"platforms_enabled", "last_sent",
|
|
94
|
+
],
|
|
95
|
+
separator: "powerline",
|
|
96
|
+
colors: getDefaultColors(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** ASCII preset — safe for any terminal */
|
|
100
|
+
const ASCII_PRESET: PresetDef = {
|
|
101
|
+
leftSegments: [
|
|
102
|
+
"model", "path", "git", "context_pct", "cost",
|
|
103
|
+
],
|
|
104
|
+
rightSegments: [
|
|
105
|
+
"compactions", "tokens_saved", "project_count",
|
|
106
|
+
],
|
|
107
|
+
secondarySegments: [],
|
|
108
|
+
separator: "ascii",
|
|
109
|
+
colors: getDefaultColors(),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** All preset definitions */
|
|
113
|
+
export const PRESETS: Record<string, PresetDef> = {
|
|
114
|
+
default: DEFAULT_PRESET,
|
|
115
|
+
minimal: MINIMAL_PRESET,
|
|
116
|
+
compact: COMPACT_PRESET,
|
|
117
|
+
full: FULL_PRESET,
|
|
118
|
+
nerd: NERD_PRESET,
|
|
119
|
+
ascii: ASCII_PRESET,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** Valid preset names */
|
|
123
|
+
export const PRESET_NAMES = Object.keys(PRESETS);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get a preset definition by name.
|
|
127
|
+
* Falls back to "default" if the name is not recognized.
|
|
128
|
+
*/
|
|
129
|
+
export function getPreset(name: string): PresetDef {
|
|
130
|
+
return PRESETS[name] ?? PRESETS.default ?? DEFAULT_PRESET;
|
|
131
|
+
}
|