@pi-unipi/footer 0.1.4 → 2.0.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/package.json +1 -1
- package/src/commands.ts +2 -2
- package/src/config.ts +12 -0
- package/src/help.ts +2 -2
- package/src/index.ts +23 -14
- package/src/presets.ts +2 -2
- package/src/registry/index.ts +1 -1
- package/src/rendering/icons.ts +43 -43
- package/src/rendering/renderer.ts +67 -20
- package/src/segments/compactor.ts +58 -23
- package/src/segments/core.ts +2 -1
- package/src/segments/kanboard.ts +4 -4
- package/src/segments/mcp.ts +3 -11
- package/src/segments/memory.ts +2 -3
- package/src/segments/notify.ts +2 -2
- package/src/segments/ralph.ts +6 -8
- package/src/segments/workflow.ts +2 -1
- package/src/tui/settings-tui.ts +26 -7
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* /unipi:footer-settings.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } 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
11
|
import { showFooterSettings } from "./tui/settings-tui.js";
|
|
@@ -23,7 +23,7 @@ interface FooterState {
|
|
|
23
23
|
};
|
|
24
24
|
segmentLookup: Map<string, FooterSegment>;
|
|
25
25
|
piContext: unknown;
|
|
26
|
-
setupUI: ((pi: ExtensionAPI, ctx:
|
|
26
|
+
setupUI: ((pi: ExtensionAPI, ctx: ExtensionContext) => void) | null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
package/src/config.ts
CHANGED
|
@@ -144,6 +144,18 @@ export function isSegmentEnabled(groupId: string, segmentId: string): boolean {
|
|
|
144
144
|
return true;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Check if a segment is explicitly enabled by user settings (toggled on).
|
|
149
|
+
* Returns true only if the segment appears in the settings with value `true`.
|
|
150
|
+
* Segments that are enabled by default but not explicitly configured return false.
|
|
151
|
+
*/
|
|
152
|
+
export function isSegmentExplicitlyEnabled(groupId: string, segmentId: string): boolean {
|
|
153
|
+
const settings = loadFooterSettings();
|
|
154
|
+
const groupSettings = settings.groups[groupId];
|
|
155
|
+
if (!groupSettings) return false;
|
|
156
|
+
return groupSettings.segments?.[segmentId] === true;
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
148
160
|
|
|
149
161
|
function isValidSeparator(value: unknown): boolean {
|
package/src/help.ts
CHANGED
|
@@ -75,7 +75,7 @@ function buildHelpLines(
|
|
|
75
75
|
function getGroupForSegment(segId: string): string {
|
|
76
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
77
|
if (coreIds.includes(segId)) return "core";
|
|
78
|
-
const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "
|
|
78
|
+
const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "cocoindex_status", "sandbox_runs", "search_queries"];
|
|
79
79
|
if (compactorIds.includes(segId)) return "compactor";
|
|
80
80
|
if (["project_count", "total_count", "consolidations"].includes(segId)) return "memory";
|
|
81
81
|
if (["servers_total", "servers_active", "tools_total", "servers_failed"].includes(segId)) return "mcp";
|
|
@@ -101,7 +101,7 @@ export function showFooterHelp(
|
|
|
101
101
|
// Use pi's custom UI overlay
|
|
102
102
|
const ctx = (pi as any)._ctx;
|
|
103
103
|
if (ctx?.ui?.custom) {
|
|
104
|
-
ctx.ui.custom((tui:
|
|
104
|
+
ctx.ui.custom((tui: import("@mariozechner/pi-tui").TUI) => {
|
|
105
105
|
let scrollOffset = 0;
|
|
106
106
|
|
|
107
107
|
return {
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* initializes renderer on session_start.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { ExtensionAPI, Theme, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
9
10
|
import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS } from "@pi-unipi/core";
|
|
10
11
|
import { FooterRegistry, getFooterRegistry } from "./registry/index.js";
|
|
11
12
|
import { FooterRenderer } from "./rendering/renderer.js";
|
|
@@ -62,10 +63,10 @@ export interface FooterState {
|
|
|
62
63
|
unsubscribeEvents: (() => void) | null;
|
|
63
64
|
piContext: unknown;
|
|
64
65
|
footerData: unknown;
|
|
65
|
-
tuiRef:
|
|
66
|
+
tuiRef: import("@mariozechner/pi-tui").TUI | null | undefined;
|
|
66
67
|
refreshTimer: ReturnType<typeof setInterval> | null;
|
|
67
68
|
/** Re-register footer + widgets with pi UI (for live enable) */
|
|
68
|
-
setupUI: ((pi: ExtensionAPI, ctx:
|
|
69
|
+
setupUI: ((pi: ExtensionAPI, ctx: ExtensionContext) => void) | null;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
export default function footerExtension(pi: ExtensionAPI): void {
|
|
@@ -78,7 +79,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
78
79
|
registry: getFooterRegistry(),
|
|
79
80
|
renderer: new FooterRenderer(
|
|
80
81
|
getFooterRegistry(),
|
|
81
|
-
{ get: (id: string) => segmentLookup.get(id) },
|
|
82
|
+
{ get: (id: string) => segmentLookup.get(id), allIds: () => Array.from(segmentLookup.keys()) },
|
|
82
83
|
loadFooterSettings().preset,
|
|
83
84
|
),
|
|
84
85
|
segmentLookup,
|
|
@@ -111,7 +112,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
111
112
|
|
|
112
113
|
// Setup footer + widgets
|
|
113
114
|
setupFooterUI(pi, ctx, state);
|
|
114
|
-
state.setupUI = (p: ExtensionAPI, c:
|
|
115
|
+
state.setupUI = (p: ExtensionAPI, c: ExtensionContext) => setupFooterUI(p, c, state);
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
pi.on("session_shutdown", async () => {
|
|
@@ -135,7 +136,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
135
136
|
// ─── Emit MODULE_READY ──────────────────────────────────────────────────
|
|
136
137
|
|
|
137
138
|
pi.on("session_start", async () => {
|
|
138
|
-
emitEvent(pi
|
|
139
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
139
140
|
name: "@pi-unipi/footer",
|
|
140
141
|
version: "0.1.0",
|
|
141
142
|
commands: [`${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER}`, `${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_SETTINGS}`, `${UNIPI_PREFIX}${FOOTER_COMMANDS.FOOTER_HELP}`],
|
|
@@ -146,9 +147,9 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
146
147
|
|
|
147
148
|
// ─── Footer UI setup ────────────────────────────────────────────────────────
|
|
148
149
|
|
|
149
|
-
function setupFooterUI(pi: ExtensionAPI, ctx:
|
|
150
|
+
function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterState): void {
|
|
150
151
|
// Register footer (minimal — handles branch changes)
|
|
151
|
-
ctx.ui.setFooter((tui
|
|
152
|
+
ctx.ui.setFooter((tui, _theme, footerData) => {
|
|
152
153
|
state.tuiRef = tui;
|
|
153
154
|
|
|
154
155
|
// Start periodic refresh for time-sensitive segments (e.g. clock)
|
|
@@ -199,7 +200,7 @@ function setupFooterUI(pi: ExtensionAPI, ctx: any, state: FooterState): void {
|
|
|
199
200
|
});
|
|
200
201
|
|
|
201
202
|
// Top row widget
|
|
202
|
-
ctx.ui.setWidget("footer-top", (_tui
|
|
203
|
+
ctx.ui.setWidget("footer-top", (_tui, theme) => {
|
|
203
204
|
// Update the renderer's theme-like
|
|
204
205
|
const themeLike = { fg: (color: string, text: string) => theme.fg(color as any, text) };
|
|
205
206
|
// We need to patch the context with proper theme
|
|
@@ -211,30 +212,38 @@ function setupFooterUI(pi: ExtensionAPI, ctx: any, state: FooterState): void {
|
|
|
211
212
|
state.renderer.resetLayoutCache();
|
|
212
213
|
},
|
|
213
214
|
render(width: number): string[] {
|
|
214
|
-
if (!state.enabled || !state.piContext) return [];
|
|
215
|
+
if (!state.enabled || !state.piContext || width <= 0) return [];
|
|
215
216
|
|
|
216
217
|
// Build layout with proper theme by creating segment contexts
|
|
217
218
|
const layout = state.renderer.computeLayout(width);
|
|
218
|
-
|
|
219
|
+
if (!layout.topContent) return [];
|
|
220
|
+
|
|
221
|
+
// Hard safety net: never return a line wider than the terminal.
|
|
222
|
+
// This catches any edge cases in layout math or visibleWidth()
|
|
223
|
+
// inconsistencies with PUA characters + ANSI codes.
|
|
224
|
+
const line = layout.topContent;
|
|
225
|
+
return [visibleWidth(line) > width ? truncateToWidth(line, width) : line];
|
|
219
226
|
},
|
|
220
227
|
};
|
|
221
228
|
}, { placement: "aboveEditor" });
|
|
222
229
|
|
|
223
230
|
// Secondary row widget
|
|
224
|
-
ctx.ui.setWidget("footer-secondary", (_tui
|
|
231
|
+
ctx.ui.setWidget("footer-secondary", (_tui, _theme) => {
|
|
225
232
|
return {
|
|
226
233
|
dispose() {},
|
|
227
234
|
invalidate() {
|
|
228
235
|
state.renderer.resetLayoutCache();
|
|
229
236
|
},
|
|
230
237
|
render(width: number): string[] {
|
|
231
|
-
if (!state.enabled || !state.piContext) return [];
|
|
238
|
+
if (!state.enabled || !state.piContext || width <= 0) return [];
|
|
232
239
|
|
|
233
240
|
const lines: string[] = [];
|
|
234
241
|
|
|
235
242
|
const layout = state.renderer.computeLayout(width);
|
|
236
243
|
if (layout.secondaryContent) {
|
|
237
|
-
|
|
244
|
+
// Hard safety net: never return a line wider than the terminal.
|
|
245
|
+
const line = layout.secondaryContent;
|
|
246
|
+
lines.push(visibleWidth(line) > width ? truncateToWidth(line, width) : line);
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
return lines;
|
package/src/presets.ts
CHANGED
|
@@ -73,7 +73,7 @@ const FULL_PRESET: PresetDef = {
|
|
|
73
73
|
secondarySegments: [
|
|
74
74
|
"hostname",
|
|
75
75
|
"tokens_in", "tokens_out",
|
|
76
|
-
"compression_ratio", "
|
|
76
|
+
"compression_ratio", "cocoindex_status",
|
|
77
77
|
"platforms_enabled", "last_sent",
|
|
78
78
|
"thinking_level",
|
|
79
79
|
],
|
|
@@ -98,7 +98,7 @@ const NERD_PRESET: PresetDef = {
|
|
|
98
98
|
secondarySegments: [
|
|
99
99
|
"hostname",
|
|
100
100
|
"tokens_in", "tokens_out",
|
|
101
|
-
"compression_ratio", "
|
|
101
|
+
"compression_ratio", "cocoindex_status",
|
|
102
102
|
"platforms_enabled", "last_sent",
|
|
103
103
|
"thinking_level",
|
|
104
104
|
],
|
package/src/registry/index.ts
CHANGED
|
@@ -156,5 +156,5 @@ export function resetFooterRegistry(): void {
|
|
|
156
156
|
|
|
157
157
|
// Expose on globalThis for cross-package access
|
|
158
158
|
if (typeof globalThis !== "undefined") {
|
|
159
|
-
|
|
159
|
+
globalThis.__unipi_footer_registry = getFooterRegistry();
|
|
160
160
|
}
|
package/src/rendering/icons.ts
CHANGED
|
@@ -84,65 +84,65 @@ export interface IconSet {
|
|
|
84
84
|
/** Nerd Font glyphs — requires a Nerd Font installed in the terminal */
|
|
85
85
|
export const NERD_ICONS: IconSet = {
|
|
86
86
|
// Core
|
|
87
|
-
model: "\
|
|
88
|
-
apiState: "\
|
|
89
|
-
toolCount: "\
|
|
90
|
-
git: "\
|
|
91
|
-
context: "\
|
|
92
|
-
cost: "\uF155", //
|
|
93
|
-
tokens: "\
|
|
94
|
-
tokensIn: "\
|
|
95
|
-
tokensOut: "\
|
|
96
|
-
session: "\
|
|
97
|
-
hostname: "\
|
|
98
|
-
time: "\uF017", //
|
|
99
|
-
tps: "\
|
|
100
|
-
clock: "\uF017", //
|
|
101
|
-
duration: "\
|
|
102
|
-
thinkingLevel: "\
|
|
87
|
+
model: "\u{F06A9}", //
|
|
88
|
+
apiState: "\u{F109B}", //
|
|
89
|
+
toolCount: "\u{F1064}", //
|
|
90
|
+
git: "\uEAFE", //
|
|
91
|
+
context: "\u{F0077}", //
|
|
92
|
+
cost: "\uF155", //
|
|
93
|
+
tokens: "\uEDE8", //
|
|
94
|
+
tokensIn: "\uEDE8", //
|
|
95
|
+
tokensOut: "\uEDE8", //
|
|
96
|
+
session: "\uF03A", //
|
|
97
|
+
hostname: "\uEA7A", //
|
|
98
|
+
time: "\uF017", //
|
|
99
|
+
tps: "\u{F04C5}", //
|
|
100
|
+
clock: "\uF017", //
|
|
101
|
+
duration: "\u{F13AB}", //
|
|
102
|
+
thinkingLevel: "\uF400", //
|
|
103
103
|
|
|
104
104
|
// Compactor
|
|
105
|
-
sessionEvents: "\
|
|
106
|
-
compactions: "\
|
|
107
|
-
tokensSaved: "\uF155", //
|
|
108
|
-
compressionRatio:"\
|
|
109
|
-
indexedDocs: "\
|
|
110
|
-
sandboxRuns: "\
|
|
111
|
-
searchQueries: "\uF002", //
|
|
105
|
+
sessionEvents: "\uEA86", //
|
|
106
|
+
compactions: "\u{F0C8F}", //
|
|
107
|
+
tokensSaved: "\uF155", // (kept — missing from customization)
|
|
108
|
+
compressionRatio:"\u{F0C8F}", //
|
|
109
|
+
indexedDocs: "\u{F0219}", //
|
|
110
|
+
sandboxRuns: "\uF233", //
|
|
111
|
+
searchQueries: "\uF002", //
|
|
112
112
|
|
|
113
113
|
// Memory
|
|
114
|
-
projectCount: "\
|
|
115
|
-
totalCount: "\
|
|
116
|
-
consolidations: "\
|
|
114
|
+
projectCount: "\uEE9C", //
|
|
115
|
+
totalCount: "\uEE9C", //
|
|
116
|
+
consolidations: "\uEE9C", //
|
|
117
117
|
|
|
118
118
|
// MCP
|
|
119
|
-
serversTotal: "\
|
|
120
|
-
serversActive: "\
|
|
121
|
-
toolsTotal: "\
|
|
122
|
-
serversFailed: "\
|
|
119
|
+
serversTotal: "\u{F05B7}", //
|
|
120
|
+
serversActive: "\u{F05B7}", //
|
|
121
|
+
toolsTotal: "\u{F05B7}", //
|
|
122
|
+
serversFailed: "\u{F05B7}", //
|
|
123
123
|
|
|
124
124
|
// Ralph
|
|
125
|
-
activeLoops: "\
|
|
126
|
-
totalIterations: "\
|
|
127
|
-
loopStatus: "\
|
|
125
|
+
activeLoops: "\u{F0709}", //
|
|
126
|
+
totalIterations: "\u{F0709}", //
|
|
127
|
+
loopStatus: "\u{F0709}", //
|
|
128
128
|
|
|
129
129
|
// Workflow
|
|
130
|
-
currentCommand: "\
|
|
131
|
-
sandboxLevel: "\
|
|
132
|
-
commandDuration: "\
|
|
130
|
+
currentCommand: "\uF124", //
|
|
131
|
+
sandboxLevel: "\u{F07FE}", //
|
|
132
|
+
commandDuration: "\u{F13AB}", //
|
|
133
133
|
|
|
134
134
|
// Kanboard
|
|
135
|
-
docsCount: "\
|
|
136
|
-
tasksDone: "\
|
|
137
|
-
tasksTotal: "\
|
|
138
|
-
taskPct: "\
|
|
135
|
+
docsCount: "\u{F09EE}", //
|
|
136
|
+
tasksDone: "\u{F1A9A}", //
|
|
137
|
+
tasksTotal: "\uF4A0", //
|
|
138
|
+
taskPct: "\uF4A0", //
|
|
139
139
|
|
|
140
140
|
// Notify
|
|
141
|
-
platformsEnabled:"\
|
|
142
|
-
lastSent: "\
|
|
141
|
+
platformsEnabled:"\uEB9A", //
|
|
142
|
+
lastSent: "\u{F13AB}", //
|
|
143
143
|
|
|
144
144
|
// Extension status
|
|
145
|
-
extensionStatuses:"\
|
|
145
|
+
extensionStatuses:"\u{F15AB}", //
|
|
146
146
|
|
|
147
147
|
separator: "\uE0B1", // nf-pl-left_soft_divider
|
|
148
148
|
};
|
|
@@ -14,11 +14,13 @@ import { getSeparator, separatorVisibleWidth } from "./separators.js";
|
|
|
14
14
|
import { getDefaultColors } from "./theme.js";
|
|
15
15
|
import { setIconStyle } from "./icons.js";
|
|
16
16
|
import { getPreset } from "../presets.js";
|
|
17
|
-
import { isSegmentEnabled, loadFooterSettings } from "../config.js";
|
|
17
|
+
import { isSegmentEnabled, isSegmentExplicitlyEnabled, loadFooterSettings } from "../config.js";
|
|
18
18
|
|
|
19
19
|
/** Segment lookup by ID across all groups */
|
|
20
20
|
interface SegmentLookup {
|
|
21
21
|
get(id: string): FooterSegment | undefined;
|
|
22
|
+
/** All known segment IDs */
|
|
23
|
+
allIds(): string[];
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** Rendered segment with width info */
|
|
@@ -162,6 +164,19 @@ export class FooterRenderer {
|
|
|
162
164
|
const primaryIds = [...presetDef.leftSegments, ...presetDef.rightSegments];
|
|
163
165
|
const secondaryIds = [...presetDef.secondarySegments];
|
|
164
166
|
|
|
167
|
+
// Also include segments explicitly enabled by user that are NOT in the preset.
|
|
168
|
+
// This ensures toggling a segment "on" in the settings TUI makes it visible
|
|
169
|
+
// even when the active preset doesn't include it.
|
|
170
|
+
if (this.segmentLookup.allIds) {
|
|
171
|
+
for (const segId of this.segmentLookup.allIds()) {
|
|
172
|
+
if (primaryIds.includes(segId) || secondaryIds.includes(segId)) continue;
|
|
173
|
+
const groupId = this.getGroupForSegment(segId);
|
|
174
|
+
if (isSegmentExplicitlyEnabled(groupId, segId)) {
|
|
175
|
+
primaryIds.push(segId);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
165
180
|
// Render segments grouped by their zone
|
|
166
181
|
const zones: Record<SegmentZone, RenderedSegmentWithWidth[]> = {
|
|
167
182
|
left: [],
|
|
@@ -203,9 +218,11 @@ export class FooterRenderer {
|
|
|
203
218
|
|
|
204
219
|
const sepDef = getSeparator(settings.separator);
|
|
205
220
|
const sepWidth = visibleWidth(sepDef.left) + 2;
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const
|
|
221
|
+
const rawZoneSep = presetDef.zoneSeparator ?? settings.zoneSeparator ?? "\u2502";
|
|
222
|
+
const zoneSepHidden = !rawZoneSep || rawZoneSep === "none";
|
|
223
|
+
const zoneSep = zoneSepHidden ? "" : rawZoneSep;
|
|
224
|
+
const zoneSepWidth = zoneSepHidden ? 0 : visibleWidth(zoneSep) + 2; // +2 for spaces around zone sep
|
|
225
|
+
const dimZoneSep = zoneSepHidden ? "" : `\x1b[2m${zoneSep}\x1b[0m`; // dimmed zone separator
|
|
209
226
|
|
|
210
227
|
// Calculate widths per zone
|
|
211
228
|
const leftWidth = this.measureZoneWidth(zones.left, sepWidth);
|
|
@@ -213,15 +230,36 @@ export class FooterRenderer {
|
|
|
213
230
|
const numZoneSeps = (leftWidth > 0 ? 1 : 0) + (rightWidth > 0 ? 1 : 0);
|
|
214
231
|
const availableForCenter = width - leftWidth - rightWidth - numZoneSeps * zoneSepWidth - 2; // -2 for margins
|
|
215
232
|
|
|
233
|
+
// Progressive segment dropping: if left + right already exceed width,
|
|
234
|
+
// drop right-zone segments from the end until they fit.
|
|
235
|
+
const marginWidth = 2; // leading + trailing space
|
|
236
|
+
let adjustedRightWidth = rightWidth;
|
|
237
|
+
while (zones.right.length > 0 && leftWidth + adjustedRightWidth + marginWidth > width) {
|
|
238
|
+
const dropped = zones.right.pop()!;
|
|
239
|
+
adjustedRightWidth = this.measureZoneWidth(zones.right, sepWidth);
|
|
240
|
+
overflowZones.right.push(dropped);
|
|
241
|
+
}
|
|
242
|
+
// If left zone alone exceeds width, drop segments from the end until it fits.
|
|
243
|
+
let adjustedLeftWidth = leftWidth;
|
|
244
|
+
while (zones.left.length > 1 && adjustedLeftWidth + marginWidth > width) {
|
|
245
|
+
const dropped = zones.left.pop()!;
|
|
246
|
+
adjustedLeftWidth = this.measureZoneWidth(zones.left, sepWidth);
|
|
247
|
+
overflowZones.left.push(dropped);
|
|
248
|
+
}
|
|
249
|
+
// Recalculate available center after possible right-zone dropping
|
|
250
|
+
const adjLeftWidth = zones.left.length > 0 ? this.measureZoneWidth(zones.left, sepWidth) : 0;
|
|
251
|
+
const adjNumZoneSeps = (zones.left.length > 0 ? 1 : 0) + (zones.right.length > 0 ? 1 : 0);
|
|
252
|
+
const adjAvailableForCenter = width - adjLeftWidth - adjustedRightWidth - adjNumZoneSeps * zoneSepWidth - marginWidth;
|
|
253
|
+
|
|
216
254
|
// Overflow check: if center doesn't fit, move excess to overflow
|
|
217
255
|
const centerWidth = this.measureZoneWidth(zones.center, sepWidth);
|
|
218
|
-
if (centerWidth > Math.max(0,
|
|
256
|
+
if (centerWidth > Math.max(0, adjAvailableForCenter)) {
|
|
219
257
|
// Move overflow center segments to secondary
|
|
220
258
|
let fitWidth = 0;
|
|
221
259
|
let cutoffIdx = 0;
|
|
222
260
|
for (let i = 0; i < zones.center.length; i++) {
|
|
223
261
|
const needed = zones.center[i].width + (i > 0 ? sepWidth : 0);
|
|
224
|
-
if (fitWidth + needed <= Math.max(0,
|
|
262
|
+
if (fitWidth + needed <= Math.max(0, adjAvailableForCenter)) {
|
|
225
263
|
fitWidth += needed;
|
|
226
264
|
cutoffIdx = i + 1;
|
|
227
265
|
} else {
|
|
@@ -236,10 +274,11 @@ export class FooterRenderer {
|
|
|
236
274
|
const topContent = this.buildZoneRow(zones, width, sepDef, dimZoneSep);
|
|
237
275
|
|
|
238
276
|
// Build secondary row with overflow + preset secondary segments
|
|
239
|
-
const allSecondary = [...overflowZones.center, ...secondaryRendered];
|
|
277
|
+
const allSecondary = [...overflowZones.left, ...overflowZones.center, ...overflowZones.right, ...secondaryRendered];
|
|
240
278
|
const secondaryContent = this.buildContentFromParts(
|
|
241
279
|
allSecondary.map(s => s.content),
|
|
242
280
|
sepDef,
|
|
281
|
+
width,
|
|
243
282
|
);
|
|
244
283
|
|
|
245
284
|
this.lastLayoutResult = { topContent, secondaryContent };
|
|
@@ -334,7 +373,7 @@ export class FooterRenderer {
|
|
|
334
373
|
}
|
|
335
374
|
|
|
336
375
|
if (centerContent) {
|
|
337
|
-
if (leftContent) result += ` ${dimZoneSep} `;
|
|
376
|
+
if (leftContent && dimZoneSep) result += ` ${dimZoneSep} `;
|
|
338
377
|
result += centerContent;
|
|
339
378
|
}
|
|
340
379
|
|
|
@@ -348,20 +387,22 @@ export class FooterRenderer {
|
|
|
348
387
|
result += " ".repeat(gap);
|
|
349
388
|
}
|
|
350
389
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const sepPos = result.length - gap + Math.floor((gap - visibleWidth(dimZoneSep)) / 2);
|
|
356
|
-
// Simpler: just put it at the boundary
|
|
357
|
-
}
|
|
390
|
+
// If gap is negative, right zone doesn't fit — skip it to prevent overflow.
|
|
391
|
+
// truncateToWidth below is the safety net for any remaining excess.
|
|
392
|
+
if (gap > visibleWidth(dimZoneSep) + 2) {
|
|
393
|
+
// Enough room for zone separator aesthetic
|
|
358
394
|
}
|
|
359
395
|
|
|
360
|
-
|
|
396
|
+
// Only append right zone if it fits within terminal width
|
|
397
|
+
if (gap >= 0) {
|
|
398
|
+
result += rightContent;
|
|
399
|
+
}
|
|
361
400
|
}
|
|
362
401
|
|
|
363
402
|
result += " "; // trailing margin
|
|
364
|
-
|
|
403
|
+
|
|
404
|
+
// Safety net: never exceed terminal width
|
|
405
|
+
return truncateToWidth(result, fullWidth);
|
|
365
406
|
}
|
|
366
407
|
|
|
367
408
|
/** Build content from parts array (raw strings) */
|
|
@@ -374,11 +415,17 @@ export class FooterRenderer {
|
|
|
374
415
|
|
|
375
416
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
376
417
|
|
|
377
|
-
private buildContentFromParts(parts: string[], sepDef: { left: string }): string {
|
|
418
|
+
private buildContentFromParts(parts: string[], sepDef: { left: string }, maxWidth?: number): string {
|
|
378
419
|
if (parts.length === 0) return "";
|
|
379
420
|
const sep = sepDef.left;
|
|
380
421
|
const sepAnsi = getFgAnsiCode(getPreset(this.presetName).colors ?? getDefaultColors(), "separator");
|
|
381
|
-
|
|
422
|
+
const result = " " + parts.join(` ${sepAnsi}${sep}${ANSI_RESET} `) + ANSI_RESET + " ";
|
|
423
|
+
// Safety net: never exceed maxWidth if provided
|
|
424
|
+
if (maxWidth != null && maxWidth > 0) {
|
|
425
|
+
return truncateToWidth(result, maxWidth);
|
|
426
|
+
}
|
|
427
|
+
// If no maxWidth, truncate to a reasonable default to prevent unbounded output
|
|
428
|
+
return truncateToWidth(result, 200);
|
|
382
429
|
}
|
|
383
430
|
|
|
384
431
|
/** Map a segment ID to its group ID */
|
|
@@ -388,7 +435,7 @@ export class FooterRenderer {
|
|
|
388
435
|
if (coreIds.includes(segId)) return "core";
|
|
389
436
|
|
|
390
437
|
// Compactor segments
|
|
391
|
-
const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "
|
|
438
|
+
const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "cocoindex_status", "sandbox_runs", "search_queries"];
|
|
392
439
|
if (compactorIds.includes(segId)) return "compactor";
|
|
393
440
|
|
|
394
441
|
// Memory segments
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @pi-unipi/footer — Compactor segments
|
|
3
3
|
*
|
|
4
4
|
* Segment renderers for the compactor group: session_events, compactions,
|
|
5
|
-
* tokens_saved, compression_ratio,
|
|
5
|
+
* tokens_saved, compression_ratio, cocoindex_status, sandbox_runs, search_queries.
|
|
6
6
|
*
|
|
7
7
|
* Data sourced from piContext.sessionManager (live session data).
|
|
8
8
|
* Segments without a reliable data source are hidden (visible: false)
|
|
@@ -53,7 +53,7 @@ function renderSessionEventsSegment(ctx: FooterSegmentContext): RenderedSegment
|
|
|
53
53
|
const count = events.length;
|
|
54
54
|
if (count === 0) {
|
|
55
55
|
if (isSegmentEnabled("compactor", "session_events")) {
|
|
56
|
-
return { content: mutedPlaceholder("
|
|
56
|
+
return { content: mutedPlaceholder(withIcon("sessionEvents", "0")), visible: true };
|
|
57
57
|
}
|
|
58
58
|
return hidden();
|
|
59
59
|
}
|
|
@@ -74,7 +74,7 @@ function renderCompactionsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
74
74
|
}
|
|
75
75
|
if (compactionCount === 0) {
|
|
76
76
|
if (isSegmentEnabled("compactor", "compactions")) {
|
|
77
|
-
return { content: mutedPlaceholder("
|
|
77
|
+
return { content: mutedPlaceholder(withIcon("compactions", "0")), visible: true };
|
|
78
78
|
}
|
|
79
79
|
return hidden();
|
|
80
80
|
}
|
|
@@ -84,15 +84,20 @@ function renderCompactionsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function renderTokensSavedSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
87
|
-
// Sum tokens saved from compaction
|
|
87
|
+
// Sum tokens saved from compaction entries.
|
|
88
|
+
// Pi's CompactionEntry has tokensBefore (total tokens before compaction).
|
|
89
|
+
// Compaction keeps ~10-15% of context, so tokens saved ≈ tokensBefore × 0.85.
|
|
88
90
|
const events = getSessionEvents(ctx);
|
|
89
91
|
let tokensSaved = 0;
|
|
90
92
|
let hasCompaction = false;
|
|
91
93
|
for (const e of events) {
|
|
92
94
|
if (!e || typeof e !== "object") continue;
|
|
93
|
-
if (e.type === "compaction"
|
|
95
|
+
if (e.type === "compaction") {
|
|
94
96
|
hasCompaction = true;
|
|
95
|
-
|
|
97
|
+
const tokensBefore = Number(e.tokensBefore ?? 0);
|
|
98
|
+
// Estimate tokens kept at ~12% (compaction summary + recent messages)
|
|
99
|
+
const tokensAfter = Math.round(tokensBefore * 0.12);
|
|
100
|
+
tokensSaved += Math.max(0, tokensBefore - tokensAfter);
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
if (!hasCompaction || tokensSaved === 0) return hidden();
|
|
@@ -102,37 +107,67 @@ function renderTokensSavedSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
function renderCompressionRatioSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
105
|
-
//
|
|
110
|
+
// Calculate compression ratio from Pi's CompactionEntry.tokensBefore.
|
|
111
|
+
// Compaction keeps ~12% of context, giving ~8:1 compression.
|
|
106
112
|
const events = getSessionEvents(ctx);
|
|
107
|
-
let
|
|
113
|
+
let totalBefore = 0;
|
|
114
|
+
let totalAfter = 0;
|
|
108
115
|
for (const e of events) {
|
|
109
116
|
if (!e || typeof e !== "object") continue;
|
|
110
|
-
if (e.type === "compaction"
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
117
|
+
if (e.type === "compaction") {
|
|
118
|
+
const before = Number(e.tokensBefore ?? 0);
|
|
119
|
+
if (before > 0) {
|
|
120
|
+
totalBefore += before;
|
|
121
|
+
totalAfter += Math.round(before * 0.12);
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
|
-
if (
|
|
125
|
+
if (totalBefore === 0 || totalAfter === 0) return hidden();
|
|
118
126
|
|
|
119
|
-
const
|
|
127
|
+
const ratio = totalBefore / totalAfter;
|
|
128
|
+
const content = withIcon("compressionRatio", `${ratio.toFixed(1)}x`);
|
|
120
129
|
return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
function
|
|
124
|
-
//
|
|
132
|
+
function renderCocoindexStatusSegment(_ctx: FooterSegmentContext): RenderedSegment {
|
|
133
|
+
// CocoIndex status — would need to query cocoindex bridge at render time.
|
|
134
|
+
// For now, hidden. Use /unipi:cocoindex-status for live status.
|
|
125
135
|
return hidden();
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
function renderSandboxRunsSegment(
|
|
129
|
-
//
|
|
130
|
-
|
|
138
|
+
function renderSandboxRunsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
139
|
+
// Count sandbox events from session manager branch
|
|
140
|
+
const events = getSessionEvents(ctx);
|
|
141
|
+
let sandboxCount = 0;
|
|
142
|
+
for (const e of events) {
|
|
143
|
+
if (!e || typeof e !== "object") continue;
|
|
144
|
+
// Count tool calls that are sandbox/execute tools
|
|
145
|
+
const name = String((e as any).name ?? "").toLowerCase();
|
|
146
|
+
if (name.includes("sandbox") || name.includes("ctx_execute") || name === "execute") {
|
|
147
|
+
sandboxCount++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (sandboxCount === 0) return hidden();
|
|
151
|
+
|
|
152
|
+
const content = withIcon("sandboxRuns", `${sandboxCount}`);
|
|
153
|
+
return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
|
|
131
154
|
}
|
|
132
155
|
|
|
133
|
-
function renderSearchQueriesSegment(
|
|
134
|
-
//
|
|
135
|
-
|
|
156
|
+
function renderSearchQueriesSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
157
|
+
// Count search events from session manager branch
|
|
158
|
+
const events = getSessionEvents(ctx);
|
|
159
|
+
let searchCount = 0;
|
|
160
|
+
for (const e of events) {
|
|
161
|
+
if (!e || typeof e !== "object") continue;
|
|
162
|
+
const name = String((e as any).name ?? "").toLowerCase();
|
|
163
|
+
if (name.includes("search") || name.includes("ctx_search")) {
|
|
164
|
+
searchCount++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (searchCount === 0) return hidden();
|
|
168
|
+
|
|
169
|
+
const content = withIcon("searchQueries", `${searchCount}`);
|
|
170
|
+
return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
|
|
136
171
|
}
|
|
137
172
|
|
|
138
173
|
export const COMPACTOR_SEGMENTS: FooterSegment[] = [
|
|
@@ -140,7 +175,7 @@ export const COMPACTOR_SEGMENTS: FooterSegment[] = [
|
|
|
140
175
|
{ id: "compactions", label: "Compactions", shortLabel: "CMP", description: "Number of context compactions", zone: "center", icon: "", render: renderCompactionsSegment, defaultShow: true },
|
|
141
176
|
{ id: "tokens_saved", label: "Tokens Saved", shortLabel: "SVD", description: "Tokens saved by compaction", zone: "center", icon: "", render: renderTokensSavedSegment, defaultShow: true },
|
|
142
177
|
{ id: "compression_ratio", label: "Compression Ratio", shortLabel: "RAT", description: "Last compaction compression ratio", zone: "center", icon: "", render: renderCompressionRatioSegment, defaultShow: false },
|
|
143
|
-
{ id: "
|
|
178
|
+
{ id: "cocoindex_status", label: "CocoIndex", shortLabel: "CIDX", description: "CocoIndex indexing status", zone: "center", icon: "", render: renderCocoindexStatusSegment, defaultShow: false },
|
|
144
179
|
{ id: "sandbox_runs", label: "Sandbox Runs", shortLabel: "SBX", description: "Number of sandbox code runs", zone: "center", icon: "", render: renderSandboxRunsSegment, defaultShow: false },
|
|
145
180
|
{ id: "search_queries", label: "Search Queries", shortLabel: "QRY", description: "Number of search queries", zone: "center", icon: "", render: renderSearchQueriesSegment, defaultShow: false },
|
|
146
181
|
];
|
package/src/segments/core.ts
CHANGED
|
@@ -176,7 +176,8 @@ function renderCostSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
176
176
|
if (!stats.cost && !usingSubscription) return { content: "", visible: false };
|
|
177
177
|
|
|
178
178
|
const costDisplay = usingSubscription ? "(sub)" : `$${stats.cost.toFixed(2)}`;
|
|
179
|
-
|
|
179
|
+
const content = withIcon("cost", costDisplay);
|
|
180
|
+
return { content: color(ctx, "cost", content), visible: true };
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
function renderTokensSegment(variant: "total" | "in" | "out"): (ctx: FooterSegmentContext) => RenderedSegment {
|
package/src/segments/kanboard.ts
CHANGED
|
@@ -22,7 +22,7 @@ function withIcon(segmentId: string, text: string): string {
|
|
|
22
22
|
*/
|
|
23
23
|
function getKanboardData(): Record<string, unknown> | null {
|
|
24
24
|
try {
|
|
25
|
-
const registry =
|
|
25
|
+
const registry = globalThis.__unipi_kanboard_registry;
|
|
26
26
|
if (!registry || typeof registry !== "object") return null;
|
|
27
27
|
return registry as Record<string, unknown>;
|
|
28
28
|
} catch {
|
|
@@ -35,7 +35,7 @@ function renderDocsCountSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
35
35
|
const value = kb?.docsCount;
|
|
36
36
|
if (value === undefined || value === null) {
|
|
37
37
|
if (isSegmentEnabled("kanboard", "docs_count")) {
|
|
38
|
-
return { content: mutedPlaceholder("
|
|
38
|
+
return { content: mutedPlaceholder(withIcon("docsCount", "0")), visible: true };
|
|
39
39
|
}
|
|
40
40
|
return { content: "", visible: false };
|
|
41
41
|
}
|
|
@@ -48,7 +48,7 @@ function renderTasksDoneSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
48
48
|
const value = kb?.tasksDone;
|
|
49
49
|
if (value === undefined || value === null) {
|
|
50
50
|
if (isSegmentEnabled("kanboard", "tasks_done")) {
|
|
51
|
-
return { content: mutedPlaceholder("
|
|
51
|
+
return { content: mutedPlaceholder(withIcon("tasksDone", "0")), visible: true };
|
|
52
52
|
}
|
|
53
53
|
return { content: "", visible: false };
|
|
54
54
|
}
|
|
@@ -61,7 +61,7 @@ function renderTasksTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
61
61
|
const value = kb?.tasksTotal;
|
|
62
62
|
if (value === undefined || value === null) {
|
|
63
63
|
if (isSegmentEnabled("kanboard", "tasks_total")) {
|
|
64
|
-
return { content: mutedPlaceholder("
|
|
64
|
+
return { content: mutedPlaceholder(withIcon("tasksTotal", "0")), visible: true };
|
|
65
65
|
}
|
|
66
66
|
return { content: "", visible: false };
|
|
67
67
|
}
|
package/src/segments/mcp.ts
CHANGED
|
@@ -25,14 +25,6 @@ interface McpStats {
|
|
|
25
25
|
toolsTotal?: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** Shape of the global escape-hatch object */
|
|
29
|
-
interface GlobalMcpStats extends McpStats {}
|
|
30
|
-
|
|
31
|
-
declare global {
|
|
32
|
-
// eslint-disable-next-line no-var
|
|
33
|
-
var __unipi_mcp_stats: GlobalMcpStats | undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
28
|
function withIcon(segmentId: string, text: string): string {
|
|
37
29
|
const icon = getIcon(segmentId);
|
|
38
30
|
return icon ? `${icon} ${text}` : text;
|
|
@@ -67,7 +59,7 @@ function renderServersTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
67
59
|
const stats = getMcpStats(ctx);
|
|
68
60
|
if (!hasUsefulValue(stats.serversTotal)) {
|
|
69
61
|
if (isSegmentEnabled("mcp", "servers_total")) {
|
|
70
|
-
return { content: mutedPlaceholder("
|
|
62
|
+
return { content: mutedPlaceholder(withIcon("serversTotal", "0")), visible: true };
|
|
71
63
|
}
|
|
72
64
|
return { content: "", visible: false };
|
|
73
65
|
}
|
|
@@ -80,7 +72,7 @@ function renderServersActiveSegment(ctx: FooterSegmentContext): RenderedSegment
|
|
|
80
72
|
if (!hasUsefulValue(stats.serversActive)) {
|
|
81
73
|
if (isSegmentEnabled("mcp", "servers_active")) {
|
|
82
74
|
const total = stats.serversTotal ?? 0;
|
|
83
|
-
return { content: mutedPlaceholder(
|
|
75
|
+
return { content: mutedPlaceholder(withIcon("serversActive", `${total}/0`)), visible: true };
|
|
84
76
|
}
|
|
85
77
|
return { content: "", visible: false };
|
|
86
78
|
}
|
|
@@ -92,7 +84,7 @@ function renderToolsTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
92
84
|
const stats = getMcpStats(ctx);
|
|
93
85
|
if (!hasUsefulValue(stats.toolsTotal)) {
|
|
94
86
|
if (isSegmentEnabled("mcp", "tools_total")) {
|
|
95
|
-
return { content: mutedPlaceholder("
|
|
87
|
+
return { content: mutedPlaceholder(withIcon("toolsTotal", "0")), visible: true };
|
|
96
88
|
}
|
|
97
89
|
return { content: "", visible: false };
|
|
98
90
|
}
|
package/src/segments/memory.ts
CHANGED
|
@@ -51,8 +51,7 @@ interface InfoRegistryLike {
|
|
|
51
51
|
*/
|
|
52
52
|
function getInfoRegistryMemoryData(): InfoMemoryData | null {
|
|
53
53
|
try {
|
|
54
|
-
const
|
|
55
|
-
const registry = g.__unipi_info_registry;
|
|
54
|
+
const registry = globalThis.__unipi_info_registry;
|
|
56
55
|
if (!registry || typeof registry !== "object") return null;
|
|
57
56
|
|
|
58
57
|
// The info registry exposes getCachedData(groupId) synchronously
|
|
@@ -83,7 +82,7 @@ function renderProjectCountSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
83
82
|
const counts = getMemoryCounts();
|
|
84
83
|
if (counts.project === null) {
|
|
85
84
|
if (isSegmentEnabled("memory", "project_count")) {
|
|
86
|
-
return { content: mutedPlaceholder("
|
|
85
|
+
return { content: mutedPlaceholder(withIcon("projectCount", "0")), visible: true };
|
|
87
86
|
}
|
|
88
87
|
return { content: "", visible: false };
|
|
89
88
|
}
|
package/src/segments/notify.ts
CHANGED
|
@@ -26,7 +26,7 @@ function renderPlatformsEnabledSegment(ctx: FooterSegmentContext): RenderedSegme
|
|
|
26
26
|
const platforms = data.platforms as string[] | undefined;
|
|
27
27
|
if (!platforms || platforms.length === 0) {
|
|
28
28
|
if (isSegmentEnabled("notify", "platforms_enabled")) {
|
|
29
|
-
return { content: mutedPlaceholder("
|
|
29
|
+
return { content: mutedPlaceholder(withIcon("platformsEnabled", "OFF")), visible: true };
|
|
30
30
|
}
|
|
31
31
|
return { content: "", visible: false };
|
|
32
32
|
}
|
|
@@ -40,7 +40,7 @@ function renderLastSentSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
40
40
|
const timestamp = data.timestamp as string | undefined;
|
|
41
41
|
if (!timestamp) {
|
|
42
42
|
if (isSegmentEnabled("notify", "last_sent")) {
|
|
43
|
-
return { content: mutedPlaceholder("
|
|
43
|
+
return { content: mutedPlaceholder(withIcon("lastSent", "0")), visible: true };
|
|
44
44
|
}
|
|
45
45
|
return { content: "", visible: false };
|
|
46
46
|
}
|
package/src/segments/ralph.ts
CHANGED
|
@@ -51,7 +51,8 @@ function renderActiveLoopsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
51
51
|
if (!active && !name && iteration === undefined) {
|
|
52
52
|
// Show muted placeholder when enabled but no data
|
|
53
53
|
if (isSegmentEnabled("ralph", "active_loops")) {
|
|
54
|
-
|
|
54
|
+
const ralphIcon = getIcon("activeLoops");
|
|
55
|
+
return { content: mutedPlaceholder(`${ralphIcon} OFF`), visible: true };
|
|
55
56
|
}
|
|
56
57
|
return { content: "", visible: false };
|
|
57
58
|
}
|
|
@@ -81,9 +82,7 @@ function renderTotalIterationsSegment(ctx: FooterSegmentContext): RenderedSegmen
|
|
|
81
82
|
const lastIteration = data.lastIteration as Record<string, unknown> | undefined;
|
|
82
83
|
const iteration = data.iteration ?? lastIteration?.iteration;
|
|
83
84
|
if (iteration === undefined || iteration === null) {
|
|
84
|
-
|
|
85
|
-
return { content: mutedPlaceholder("🔁 RL 0"), visible: true };
|
|
86
|
-
}
|
|
85
|
+
// No data — hide to avoid duplicating active_loops placeholder
|
|
87
86
|
return { content: "", visible: false };
|
|
88
87
|
}
|
|
89
88
|
const maxIterations = data.maxIterations;
|
|
@@ -101,15 +100,14 @@ function renderLoopStatusSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
101
100
|
const status = data.status as string | undefined;
|
|
102
101
|
const name = data.name as string | undefined;
|
|
103
102
|
if (!status && !name) {
|
|
104
|
-
|
|
105
|
-
return { content: mutedPlaceholder("🔁 RL OFF"), visible: true };
|
|
106
|
-
}
|
|
103
|
+
// No data — hide to avoid duplicating active_loops placeholder
|
|
107
104
|
return { content: "", visible: false };
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
const ralphIcon = getIcon("activeLoops");
|
|
111
108
|
const dot = status === "active" ? GREEN_DOT : status === "completed" ? GREEN_DOT : RED_DOT;
|
|
112
|
-
|
|
109
|
+
// Small geometric status indicators (▶ ⏸ ✓) work in all icon styles
|
|
110
|
+
const statusIcon = status === "active" ? "\u25B6" : status === "paused" ? "\u23F8" : status === "completed" ? "\u2713" : "";
|
|
113
111
|
const display = name ? `${statusIcon} ${name}` : `${statusIcon}`;
|
|
114
112
|
|
|
115
113
|
const active = status === "active" || status === "completed";
|
package/src/segments/workflow.ts
CHANGED
|
@@ -75,7 +75,8 @@ function renderCurrentCommandSegment(ctx: FooterSegmentContext): RenderedSegment
|
|
|
75
75
|
return { content: applyColor("workflow", content, ctx.theme, ctx.colors), visible: true };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// Small geometric status indicators work in all icon styles
|
|
79
|
+
const statusPrefix = active ? "\u25B6" : "\u2713";
|
|
79
80
|
const semanticColor = getWorkflowSemanticColor(command);
|
|
80
81
|
const content = `${workflowIcon} ${statusPrefix} ${command}`;
|
|
81
82
|
return { content: applyColor(semanticColor, content, ctx.theme, ctx.colors), visible: true };
|
package/src/tui/settings-tui.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Uses pi-tui SettingsList for vim/arrow keybinding support.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
13
13
|
import { SettingsList, type SettingItem, type SettingsListTheme } from "@mariozechner/pi-tui";
|
|
14
14
|
import { loadFooterSettings, saveFooterSettings } from "../config.js";
|
|
15
15
|
import { PRESET_NAMES } from "../presets.js";
|
|
@@ -30,6 +30,7 @@ const SECTION_LABELS: Record<Section, string> = {
|
|
|
30
30
|
|
|
31
31
|
const SEPARATOR_STYLES: SeparatorStyle[] = ["powerline", "powerline-thin", "slash", "pipe", "dot", "ascii"];
|
|
32
32
|
const ICON_STYLES: IconStyle[] = ["nerd", "emoji", "text"];
|
|
33
|
+
const ZONE_SEPARATOR_OPTIONS = ["│", "╎", "·", "─", "none"];
|
|
33
34
|
|
|
34
35
|
// ─── Theme for SettingsList ────────────────────────────────────────────
|
|
35
36
|
|
|
@@ -65,9 +66,9 @@ function visibleWidth(text: string): number {
|
|
|
65
66
|
|
|
66
67
|
// ─── Show the footer settings overlay ──────────────────────────────────
|
|
67
68
|
|
|
68
|
-
export function showFooterSettings(ctx:
|
|
69
|
-
ctx.ui.custom(
|
|
70
|
-
(tui
|
|
69
|
+
export function showFooterSettings(ctx: ExtensionCommandContext, groups: FooterGroup[], onSettingsChanged?: () => void): void {
|
|
70
|
+
ctx.ui.custom<void>(
|
|
71
|
+
(tui, _theme, _keybindings, done) => {
|
|
71
72
|
const overlay = new FooterSettingsOverlay(groups, onSettingsChanged);
|
|
72
73
|
overlay.onClose = () => done();
|
|
73
74
|
|
|
@@ -85,8 +86,7 @@ export function showFooterSettings(ctx: any, groups: FooterGroup[], onSettingsCh
|
|
|
85
86
|
{
|
|
86
87
|
overlay: true,
|
|
87
88
|
overlayOptions: () => ({
|
|
88
|
-
|
|
89
|
-
horizontalAlign: "center",
|
|
89
|
+
anchor: "center" as const,
|
|
90
90
|
}),
|
|
91
91
|
},
|
|
92
92
|
).catch(() => {
|
|
@@ -251,6 +251,13 @@ class FooterSettingsOverlay {
|
|
|
251
251
|
currentValue: this.settings.iconStyle,
|
|
252
252
|
values: ICON_STYLES,
|
|
253
253
|
},
|
|
254
|
+
{
|
|
255
|
+
id: "zoneSeparator",
|
|
256
|
+
label: "Zone Separator",
|
|
257
|
+
description: "Divider between zones (left · center · right)",
|
|
258
|
+
currentValue: this.settings.zoneSeparator ?? "│",
|
|
259
|
+
values: ZONE_SEPARATOR_OPTIONS,
|
|
260
|
+
},
|
|
254
261
|
{
|
|
255
262
|
id: "showFullLabels",
|
|
256
263
|
label: "Full Labels",
|
|
@@ -371,6 +378,9 @@ class FooterSettingsOverlay {
|
|
|
371
378
|
this.settings.iconStyle = newValue as IconStyle;
|
|
372
379
|
setIconStyle(newValue as IconStyle);
|
|
373
380
|
break;
|
|
381
|
+
case "zoneSeparator":
|
|
382
|
+
this.settings.zoneSeparator = newValue;
|
|
383
|
+
break;
|
|
374
384
|
case "showFullLabels":
|
|
375
385
|
this.settings.showFullLabels = newValue === "on";
|
|
376
386
|
// Sync with labels section
|
|
@@ -420,7 +430,16 @@ class FooterSettingsOverlay {
|
|
|
420
430
|
// ─── Section navigation ────────────────────────────────────────────
|
|
421
431
|
|
|
422
432
|
private getFocusedGroupId(): string | null {
|
|
423
|
-
|
|
433
|
+
if (this.selectedGroupId) return this.selectedGroupId;
|
|
434
|
+
// Access SettingsList internal state — it doesn't expose a getSelectedId() method
|
|
435
|
+
const list = this.groupList as unknown as {
|
|
436
|
+
selectedIndex: number;
|
|
437
|
+
items: SettingItem[];
|
|
438
|
+
filteredItems: SettingItem[];
|
|
439
|
+
searchEnabled: boolean;
|
|
440
|
+
};
|
|
441
|
+
const displayItems = list.searchEnabled ? list.filteredItems : list.items;
|
|
442
|
+
return displayItems[list.selectedIndex]?.id ?? null;
|
|
424
443
|
}
|
|
425
444
|
|
|
426
445
|
private enterSegmentsMode(groupId: string): void {
|