@pi-unipi/footer 0.1.2 → 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 +38 -120
- package/src/config.ts +10 -6
- package/src/events.ts +34 -34
- package/src/help.ts +160 -0
- package/src/index.ts +46 -10
- package/src/presets.ts +40 -31
- package/src/registry/index.ts +5 -7
- package/src/rendering/icons.ts +125 -107
- package/src/rendering/renderer.ts +198 -79
- package/src/rendering/theme.ts +56 -29
- package/src/segments/compactor.ts +21 -10
- package/src/segments/core.ts +134 -67
- package/src/segments/kanboard.ts +24 -8
- package/src/segments/mcp.ts +25 -8
- package/src/segments/memory.ts +17 -11
- package/src/segments/notify.ts +16 -5
- package/src/segments/ralph.ts +33 -17
- package/src/segments/status-ext.ts +18 -13
- package/src/segments/workflow.ts +44 -21
- package/src/tps-tracker.ts +204 -0
- package/src/tui/settings-tui.ts +389 -157
- package/src/types.ts +51 -12
package/src/events.ts
CHANGED
|
@@ -30,8 +30,8 @@ export function subscribeToEvents(
|
|
|
30
30
|
pi.events.on(UNIPI_EVENTS.COMPACTOR_STATS_UPDATED, (event: unknown) => {
|
|
31
31
|
try {
|
|
32
32
|
registry.updateData("compactor", event);
|
|
33
|
-
} catch
|
|
34
|
-
|
|
33
|
+
} catch {
|
|
34
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
35
35
|
}
|
|
36
36
|
})
|
|
37
37
|
);
|
|
@@ -41,8 +41,8 @@ export function subscribeToEvents(
|
|
|
41
41
|
try {
|
|
42
42
|
const existing = registry.getGroupData("compactor") as Record<string, unknown> | undefined;
|
|
43
43
|
registry.updateData("compactor", { ...existing, lastCompaction: event });
|
|
44
|
-
} catch
|
|
45
|
-
|
|
44
|
+
} catch {
|
|
45
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
46
46
|
}
|
|
47
47
|
})
|
|
48
48
|
);
|
|
@@ -54,8 +54,8 @@ export function subscribeToEvents(
|
|
|
54
54
|
try {
|
|
55
55
|
const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
|
|
56
56
|
registry.updateData("memory", { ...existing, lastStored: event });
|
|
57
|
-
} catch
|
|
58
|
-
|
|
57
|
+
} catch {
|
|
58
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
59
59
|
}
|
|
60
60
|
})
|
|
61
61
|
);
|
|
@@ -65,8 +65,8 @@ export function subscribeToEvents(
|
|
|
65
65
|
try {
|
|
66
66
|
const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
|
|
67
67
|
registry.updateData("memory", { ...existing, lastDeleted: event });
|
|
68
|
-
} catch
|
|
69
|
-
|
|
68
|
+
} catch {
|
|
69
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
70
70
|
}
|
|
71
71
|
})
|
|
72
72
|
);
|
|
@@ -76,8 +76,8 @@ export function subscribeToEvents(
|
|
|
76
76
|
try {
|
|
77
77
|
const existing = registry.getGroupData("memory") as Record<string, unknown> | undefined;
|
|
78
78
|
registry.updateData("memory", { ...existing, lastConsolidated: event });
|
|
79
|
-
} catch
|
|
80
|
-
|
|
79
|
+
} catch {
|
|
80
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
81
81
|
}
|
|
82
82
|
})
|
|
83
83
|
);
|
|
@@ -94,8 +94,8 @@ export function subscribeToEvents(
|
|
|
94
94
|
const serversActive = (typeof existing?.serversActive === "number" ? existing.serversActive : 0) + 1;
|
|
95
95
|
const toolsTotal = (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) + toolCount;
|
|
96
96
|
registry.updateData("mcp", { ...existing, serversTotal, serversActive, toolsTotal, lastServerStarted: event });
|
|
97
|
-
} catch
|
|
98
|
-
|
|
97
|
+
} catch {
|
|
98
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
99
99
|
}
|
|
100
100
|
})
|
|
101
101
|
);
|
|
@@ -116,8 +116,8 @@ export function subscribeToEvents(
|
|
|
116
116
|
toolsTotal = Math.max(0, toolsTotal - lastStartedCount);
|
|
117
117
|
}
|
|
118
118
|
registry.updateData("mcp", { ...existing, serversActive, toolsTotal, lastServerStopped: event });
|
|
119
|
-
} catch
|
|
120
|
-
|
|
119
|
+
} catch {
|
|
120
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
121
121
|
}
|
|
122
122
|
})
|
|
123
123
|
);
|
|
@@ -129,8 +129,8 @@ export function subscribeToEvents(
|
|
|
129
129
|
const serversTotal = (typeof existing?.serversTotal === "number" ? existing.serversTotal : 0) + 1;
|
|
130
130
|
const serversFailed = (typeof existing?.serversFailed === "number" ? existing.serversFailed : 0) + 1;
|
|
131
131
|
registry.updateData("mcp", { ...existing, serversTotal, serversFailed, lastServerError: event });
|
|
132
|
-
} catch
|
|
133
|
-
|
|
132
|
+
} catch {
|
|
133
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
134
134
|
}
|
|
135
135
|
})
|
|
136
136
|
);
|
|
@@ -143,8 +143,8 @@ export function subscribeToEvents(
|
|
|
143
143
|
const toolNames = Array.isArray(evt?.toolNames) ? evt.toolNames : [];
|
|
144
144
|
const toolsTotal = (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) + toolNames.length;
|
|
145
145
|
registry.updateData("mcp", { ...existing, toolsTotal, lastToolsRegistered: event });
|
|
146
|
-
} catch
|
|
147
|
-
|
|
146
|
+
} catch {
|
|
147
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
148
148
|
}
|
|
149
149
|
})
|
|
150
150
|
);
|
|
@@ -157,8 +157,8 @@ export function subscribeToEvents(
|
|
|
157
157
|
const toolNames = Array.isArray(evt?.toolNames) ? evt.toolNames : [];
|
|
158
158
|
const toolsTotal = Math.max(0, (typeof existing?.toolsTotal === "number" ? existing.toolsTotal : 0) - toolNames.length);
|
|
159
159
|
registry.updateData("mcp", { ...existing, toolsTotal, lastToolsUnregistered: event });
|
|
160
|
-
} catch
|
|
161
|
-
|
|
160
|
+
} catch {
|
|
161
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
162
162
|
}
|
|
163
163
|
})
|
|
164
164
|
);
|
|
@@ -169,8 +169,8 @@ export function subscribeToEvents(
|
|
|
169
169
|
pi.events.on(UNIPI_EVENTS.RALPH_LOOP_START, (event: unknown) => {
|
|
170
170
|
try {
|
|
171
171
|
registry.updateData("ralph", { ...(event as Record<string, unknown>), active: true });
|
|
172
|
-
} catch
|
|
173
|
-
|
|
172
|
+
} catch {
|
|
173
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
174
174
|
}
|
|
175
175
|
})
|
|
176
176
|
);
|
|
@@ -179,8 +179,8 @@ export function subscribeToEvents(
|
|
|
179
179
|
pi.events.on(UNIPI_EVENTS.RALPH_LOOP_END, (event: unknown) => {
|
|
180
180
|
try {
|
|
181
181
|
registry.updateData("ralph", { ...(event as Record<string, unknown>), active: false });
|
|
182
|
-
} catch
|
|
183
|
-
|
|
182
|
+
} catch {
|
|
183
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
184
184
|
}
|
|
185
185
|
})
|
|
186
186
|
);
|
|
@@ -190,8 +190,8 @@ export function subscribeToEvents(
|
|
|
190
190
|
try {
|
|
191
191
|
const existing = registry.getGroupData("ralph") as Record<string, unknown> | undefined;
|
|
192
192
|
registry.updateData("ralph", { ...existing, lastIteration: event });
|
|
193
|
-
} catch
|
|
194
|
-
|
|
193
|
+
} catch {
|
|
194
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
195
195
|
}
|
|
196
196
|
})
|
|
197
197
|
);
|
|
@@ -202,8 +202,8 @@ export function subscribeToEvents(
|
|
|
202
202
|
pi.events.on(UNIPI_EVENTS.WORKFLOW_START, (event: unknown) => {
|
|
203
203
|
try {
|
|
204
204
|
registry.updateData("workflow", { ...(event as Record<string, unknown>), active: true, startTime: Date.now() });
|
|
205
|
-
} catch
|
|
206
|
-
|
|
205
|
+
} catch {
|
|
206
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
207
207
|
}
|
|
208
208
|
})
|
|
209
209
|
);
|
|
@@ -212,8 +212,8 @@ export function subscribeToEvents(
|
|
|
212
212
|
pi.events.on(UNIPI_EVENTS.WORKFLOW_END, (event: unknown) => {
|
|
213
213
|
try {
|
|
214
214
|
registry.updateData("workflow", { ...(event as Record<string, unknown>), active: false });
|
|
215
|
-
} catch
|
|
216
|
-
|
|
215
|
+
} catch {
|
|
216
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
217
217
|
}
|
|
218
218
|
})
|
|
219
219
|
);
|
|
@@ -224,8 +224,8 @@ export function subscribeToEvents(
|
|
|
224
224
|
pi.events.on(UNIPI_EVENTS.NOTIFICATION_SENT, (event: unknown) => {
|
|
225
225
|
try {
|
|
226
226
|
registry.updateData("notify", event);
|
|
227
|
-
} catch
|
|
228
|
-
|
|
227
|
+
} catch {
|
|
228
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
229
229
|
}
|
|
230
230
|
})
|
|
231
231
|
);
|
|
@@ -237,8 +237,8 @@ export function subscribeToEvents(
|
|
|
237
237
|
try {
|
|
238
238
|
// Invalidate all caches when new modules load — they may bring fresh data
|
|
239
239
|
registry.invalidateAll();
|
|
240
|
-
} catch
|
|
241
|
-
|
|
240
|
+
} catch {
|
|
241
|
+
// Silently ignore — event handler errors are non-blocking.
|
|
242
242
|
}
|
|
243
243
|
})
|
|
244
244
|
);
|
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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -26,7 +26,8 @@ import { NOTIFY_SEGMENTS } from "./segments/notify.js";
|
|
|
26
26
|
import { STATUS_EXT_SEGMENTS } from "./segments/status-ext.js";
|
|
27
27
|
|
|
28
28
|
import type { FooterGroup, FooterSegment } from "./types.js";
|
|
29
|
-
import {
|
|
29
|
+
import { rainbowBorder } from "./segments/core.js";
|
|
30
|
+
import { tpsTracker } from "./tps-tracker.js";
|
|
30
31
|
|
|
31
32
|
/** All segment groups */
|
|
32
33
|
const ALL_GROUPS: FooterGroup[] = [
|
|
@@ -53,7 +54,7 @@ function buildSegmentLookup(): Map<string, FooterSegment> {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Extension state */
|
|
56
|
-
interface FooterState {
|
|
57
|
+
export interface FooterState {
|
|
57
58
|
enabled: boolean;
|
|
58
59
|
registry: FooterRegistry;
|
|
59
60
|
renderer: FooterRenderer;
|
|
@@ -62,6 +63,9 @@ interface FooterState {
|
|
|
62
63
|
piContext: unknown;
|
|
63
64
|
footerData: unknown;
|
|
64
65
|
tuiRef: any;
|
|
66
|
+
refreshTimer: ReturnType<typeof setInterval> | null;
|
|
67
|
+
/** Re-register footer + widgets with pi UI (for live enable) */
|
|
68
|
+
setupUI: ((pi: ExtensionAPI, ctx: any) => void) | null;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
export default function footerExtension(pi: ExtensionAPI): void {
|
|
@@ -82,6 +86,8 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
82
86
|
piContext: null,
|
|
83
87
|
footerData: null,
|
|
84
88
|
tuiRef: null,
|
|
89
|
+
refreshTimer: null,
|
|
90
|
+
setupUI: null,
|
|
85
91
|
};
|
|
86
92
|
|
|
87
93
|
// Register all groups in the registry
|
|
@@ -105,6 +111,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
105
111
|
|
|
106
112
|
// Setup footer + widgets
|
|
107
113
|
setupFooterUI(pi, ctx, state);
|
|
114
|
+
state.setupUI = (p: ExtensionAPI, c: any) => setupFooterUI(p, c, state);
|
|
108
115
|
});
|
|
109
116
|
|
|
110
117
|
pi.on("session_shutdown", async () => {
|
|
@@ -113,7 +120,12 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
113
120
|
state.unsubscribeEvents = null;
|
|
114
121
|
state.piContext = null;
|
|
115
122
|
state.footerData = null;
|
|
123
|
+
if (state.refreshTimer) {
|
|
124
|
+
clearInterval(state.refreshTimer);
|
|
125
|
+
state.refreshTimer = null;
|
|
126
|
+
}
|
|
116
127
|
state.tuiRef = null;
|
|
128
|
+
tpsTracker.reset();
|
|
117
129
|
});
|
|
118
130
|
|
|
119
131
|
// ─── Register commands ──────────────────────────────────────────────────
|
|
@@ -126,7 +138,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
126
138
|
emitEvent(pi as any, UNIPI_EVENTS.MODULE_READY, {
|
|
127
139
|
name: "@pi-unipi/footer",
|
|
128
140
|
version: "0.1.0",
|
|
129
|
-
commands: [`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER}`, `${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_SETTINGS}`],
|
|
141
|
+
commands: [`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER}`, `${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_SETTINGS}`, `${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_HELP}`],
|
|
130
142
|
tools: [],
|
|
131
143
|
});
|
|
132
144
|
});
|
|
@@ -138,6 +150,36 @@ function setupFooterUI(pi: ExtensionAPI, ctx: any, state: FooterState): void {
|
|
|
138
150
|
// Register footer (minimal — handles branch changes)
|
|
139
151
|
ctx.ui.setFooter((tui: any, _theme: Theme, footerData: any) => {
|
|
140
152
|
state.tuiRef = tui;
|
|
153
|
+
|
|
154
|
+
// Start periodic refresh for time-sensitive segments (e.g. clock)
|
|
155
|
+
if (!state.refreshTimer) {
|
|
156
|
+
state.refreshTimer = setInterval(() => {
|
|
157
|
+
// Feed TPS tracker with per-message data
|
|
158
|
+
try {
|
|
159
|
+
const piCtx = state.piContext as Record<string, unknown> | undefined;
|
|
160
|
+
if (piCtx?.sessionManager) {
|
|
161
|
+
const sm = (piCtx as any).sessionManager;
|
|
162
|
+
const events = sm?.getBranch?.() ?? [];
|
|
163
|
+
let msgIndex = 0;
|
|
164
|
+
for (const e of events) {
|
|
165
|
+
if (!e || typeof e !== "object") continue;
|
|
166
|
+
if (e.type !== "message") continue;
|
|
167
|
+
const m = e.message;
|
|
168
|
+
if (!m || m.role !== "assistant") continue;
|
|
169
|
+
if (m.stopReason === "error" || m.stopReason === "aborted") continue;
|
|
170
|
+
const output = m.usage?.output ?? 0;
|
|
171
|
+
const hasStop = !!m.stopReason;
|
|
172
|
+
tpsTracker.onMessageUpdate(msgIndex, output, hasStop);
|
|
173
|
+
msgIndex++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Silently ignore — TPS is best-effort
|
|
178
|
+
}
|
|
179
|
+
state.renderer.resetLayoutCache();
|
|
180
|
+
state.tuiRef?.requestRender();
|
|
181
|
+
}, 1_000);
|
|
182
|
+
}
|
|
141
183
|
state.footerData = footerData;
|
|
142
184
|
state.renderer.setContext(state.piContext, footerData);
|
|
143
185
|
|
|
@@ -178,7 +220,7 @@ function setupFooterUI(pi: ExtensionAPI, ctx: any, state: FooterState): void {
|
|
|
178
220
|
};
|
|
179
221
|
}, { placement: "aboveEditor" });
|
|
180
222
|
|
|
181
|
-
// Secondary row widget
|
|
223
|
+
// Secondary row widget
|
|
182
224
|
ctx.ui.setWidget("footer-secondary", (_tui: any, _theme: Theme) => {
|
|
183
225
|
return {
|
|
184
226
|
dispose() {},
|
|
@@ -190,12 +232,6 @@ function setupFooterUI(pi: ExtensionAPI, ctx: any, state: FooterState): void {
|
|
|
190
232
|
|
|
191
233
|
const lines: string[] = [];
|
|
192
234
|
|
|
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
235
|
const layout = state.renderer.computeLayout(width);
|
|
200
236
|
if (layout.secondaryContent) {
|
|
201
237
|
lines.push(layout.secondaryContent);
|
package/src/presets.ts
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* @pi-unipi/footer — Presets system
|
|
3
3
|
*
|
|
4
4
|
* Preset definitions: default, minimal, compact, full, nerd, ascii.
|
|
5
|
-
* Each preset defines which segments appear
|
|
5
|
+
* Each preset defines which segments appear (left/center/right/secondary),
|
|
6
6
|
* plus separator style and color scheme.
|
|
7
|
+
*
|
|
8
|
+
* Segments are grouped by their zone field regardless of which array they're
|
|
9
|
+
* listed in. The arrays define ordering within the preset.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import type { PresetDef, SeparatorStyle, ColorScheme } from "./types.js";
|
|
@@ -12,100 +15,106 @@ import { getDefaultColors } from "./rendering/theme.js";
|
|
|
12
15
|
/** Default preset — balanced view */
|
|
13
16
|
const DEFAULT_PRESET: PresetDef = {
|
|
14
17
|
leftSegments: [
|
|
15
|
-
"model", "
|
|
18
|
+
"model", "api_state", "tool_count", "git",
|
|
16
19
|
],
|
|
17
20
|
rightSegments: [
|
|
18
|
-
"
|
|
21
|
+
"tps", "context_pct", "cost",
|
|
22
|
+
"compactions", "tokens_saved", "project_count",
|
|
23
|
+
"current_command", "loop_status", "extension_statuses",
|
|
24
|
+
"clock", "duration",
|
|
19
25
|
],
|
|
20
26
|
secondarySegments: [
|
|
21
|
-
"
|
|
27
|
+
"session",
|
|
22
28
|
],
|
|
23
|
-
separator: "powerline-thin",
|
|
24
29
|
colors: getDefaultColors(),
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
/** Minimal preset — just the essentials */
|
|
28
33
|
const MINIMAL_PRESET: PresetDef = {
|
|
29
34
|
leftSegments: [
|
|
30
|
-
"
|
|
35
|
+
"model", "git",
|
|
36
|
+
],
|
|
37
|
+
rightSegments: [
|
|
38
|
+
"context_pct",
|
|
39
|
+
"clock",
|
|
31
40
|
],
|
|
32
|
-
rightSegments: [],
|
|
33
41
|
secondarySegments: [],
|
|
34
|
-
separator: "pipe",
|
|
35
42
|
colors: getDefaultColors(),
|
|
36
43
|
};
|
|
37
44
|
|
|
38
45
|
/** Compact preset — core + key stats */
|
|
39
46
|
const COMPACT_PRESET: PresetDef = {
|
|
40
47
|
leftSegments: [
|
|
41
|
-
"model", "git",
|
|
48
|
+
"model", "git",
|
|
42
49
|
],
|
|
43
50
|
rightSegments: [
|
|
44
|
-
"
|
|
51
|
+
"tps", "context_pct", "cost",
|
|
52
|
+
"clock", "duration",
|
|
45
53
|
],
|
|
46
54
|
secondarySegments: [],
|
|
47
|
-
separator: "dot",
|
|
48
55
|
colors: getDefaultColors(),
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
/** Full preset — everything */
|
|
52
59
|
const FULL_PRESET: PresetDef = {
|
|
53
60
|
leftSegments: [
|
|
54
|
-
"model", "
|
|
55
|
-
"tokens_total", "tokens_in", "tokens_out",
|
|
61
|
+
"model", "api_state", "tool_count", "git", "current_command", "session",
|
|
56
62
|
],
|
|
57
63
|
rightSegments: [
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"project_count", "total_count",
|
|
61
|
-
"servers_total", "servers_active", "tools_total",
|
|
62
|
-
"active_loops", "
|
|
63
|
-
"
|
|
64
|
-
"docs_count", "tasks_done", "tasks_total", "task_pct",
|
|
64
|
+
"tps", "context_pct", "cost", "tokens_total",
|
|
65
|
+
"session_events", "compactions", "tokens_saved",
|
|
66
|
+
"project_count", "total_count",
|
|
67
|
+
"servers_total", "servers_active", "tools_total",
|
|
68
|
+
"active_loops", "loop_status",
|
|
69
|
+
"docs_count", "tasks_done", "task_pct",
|
|
65
70
|
"extension_statuses",
|
|
71
|
+
"clock", "duration",
|
|
66
72
|
],
|
|
67
73
|
secondarySegments: [
|
|
68
|
-
"hostname",
|
|
74
|
+
"hostname",
|
|
75
|
+
"tokens_in", "tokens_out",
|
|
76
|
+
"compression_ratio", "indexed_docs",
|
|
69
77
|
"platforms_enabled", "last_sent",
|
|
78
|
+
"thinking_level",
|
|
70
79
|
],
|
|
71
|
-
separator: "powerline-thin",
|
|
72
80
|
colors: getDefaultColors(),
|
|
73
81
|
};
|
|
74
82
|
|
|
75
83
|
/** Nerd preset — maximum detail for Nerd Font users */
|
|
76
84
|
const NERD_PRESET: PresetDef = {
|
|
77
85
|
leftSegments: [
|
|
78
|
-
"model", "
|
|
79
|
-
"tokens_total",
|
|
86
|
+
"model", "api_state", "tool_count", "git", "current_command", "session",
|
|
80
87
|
],
|
|
81
88
|
rightSegments: [
|
|
89
|
+
"tps", "context_pct", "cost", "tokens_total",
|
|
82
90
|
"session_events", "compactions", "tokens_saved",
|
|
83
91
|
"project_count", "total_count",
|
|
84
92
|
"servers_total", "servers_active", "tools_total",
|
|
85
93
|
"active_loops", "loop_status",
|
|
86
|
-
"
|
|
87
|
-
"docs_count", "tasks_done", "tasks_total", "task_pct",
|
|
94
|
+
"docs_count", "tasks_done", "task_pct",
|
|
88
95
|
"extension_statuses",
|
|
96
|
+
"clock", "duration",
|
|
89
97
|
],
|
|
90
98
|
secondarySegments: [
|
|
91
|
-
"hostname",
|
|
99
|
+
"hostname",
|
|
100
|
+
"tokens_in", "tokens_out",
|
|
92
101
|
"compression_ratio", "indexed_docs",
|
|
93
102
|
"platforms_enabled", "last_sent",
|
|
103
|
+
"thinking_level",
|
|
94
104
|
],
|
|
95
|
-
separator: "powerline",
|
|
96
105
|
colors: getDefaultColors(),
|
|
97
106
|
};
|
|
98
107
|
|
|
99
108
|
/** ASCII preset — safe for any terminal */
|
|
100
109
|
const ASCII_PRESET: PresetDef = {
|
|
101
110
|
leftSegments: [
|
|
102
|
-
"model", "
|
|
111
|
+
"model", "git",
|
|
103
112
|
],
|
|
104
113
|
rightSegments: [
|
|
105
|
-
"
|
|
114
|
+
"tps", "context_pct", "cost",
|
|
115
|
+
"clock", "duration",
|
|
106
116
|
],
|
|
107
117
|
secondarySegments: [],
|
|
108
|
-
separator: "ascii",
|
|
109
118
|
colors: getDefaultColors(),
|
|
110
119
|
};
|
|
111
120
|
|
package/src/registry/index.ts
CHANGED
|
@@ -115,19 +115,17 @@ export class FooterRegistry {
|
|
|
115
115
|
for (const callback of this.subscribers) {
|
|
116
116
|
try {
|
|
117
117
|
callback();
|
|
118
|
-
} catch
|
|
119
|
-
|
|
118
|
+
} catch {
|
|
119
|
+
// Silently ignore — subscriber errors are non-blocking.
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// ─── Debug ────────────────────────────────────────────────────────────────
|
|
125
125
|
|
|
126
|
-
private log(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const details = args.length > 0 ? " " + JSON.stringify(args) : "";
|
|
130
|
-
console.error(`[footer-registry:${ts}] ${event}${details}`);
|
|
126
|
+
private log(_event: string, ..._args: unknown[]): void {
|
|
127
|
+
// Debug logging disabled — was writing to stdout causing TUI rendering issues.
|
|
128
|
+
return;
|
|
131
129
|
}
|
|
132
130
|
}
|
|
133
131
|
|