@pi-unipi/footer 0.1.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -158
- package/package.json +1 -1
- package/src/commands.ts +38 -123
- package/src/config.ts +16 -0
- package/src/help.ts +160 -0
- package/src/index.ts +48 -15
- package/src/presets.ts +41 -32
- package/src/registry/index.ts +1 -1
- package/src/rendering/icons.ts +77 -59
- package/src/rendering/renderer.ts +246 -80
- package/src/rendering/theme.ts +56 -29
- package/src/segments/compactor.ts +76 -30
- package/src/segments/core.ts +124 -15
- package/src/segments/kanboard.ts +25 -9
- package/src/segments/mcp.ts +25 -16
- package/src/segments/memory.ts +9 -6
- package/src/segments/notify.ts +16 -5
- package/src/segments/ralph.ts +23 -8
- package/src/segments/status-ext.ts +1 -1
- package/src/segments/workflow.ts +41 -18
- package/src/tps-tracker.ts +204 -0
- package/src/tui/settings-tui.ts +253 -63
- package/src/types.ts +51 -12
package/src/rendering/icons.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface IconSet {
|
|
|
27
27
|
session: string;
|
|
28
28
|
hostname: string;
|
|
29
29
|
time: string;
|
|
30
|
+
tps: string;
|
|
31
|
+
clock: string;
|
|
32
|
+
duration: string;
|
|
33
|
+
thinkingLevel: string;
|
|
30
34
|
|
|
31
35
|
// Compactor segments
|
|
32
36
|
sessionEvents: string;
|
|
@@ -80,61 +84,65 @@ export interface IconSet {
|
|
|
80
84
|
/** Nerd Font glyphs — requires a Nerd Font installed in the terminal */
|
|
81
85
|
export const NERD_ICONS: IconSet = {
|
|
82
86
|
// Core
|
|
83
|
-
model: "\
|
|
84
|
-
apiState: "\
|
|
85
|
-
toolCount: "\
|
|
86
|
-
git: "\
|
|
87
|
-
context: "\
|
|
88
|
-
cost: "\uF155", //
|
|
89
|
-
tokens: "\
|
|
90
|
-
tokensIn: "\
|
|
91
|
-
tokensOut: "\
|
|
92
|
-
session: "\
|
|
93
|
-
hostname: "\
|
|
94
|
-
time: "\uF017", //
|
|
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", //
|
|
95
103
|
|
|
96
104
|
// Compactor
|
|
97
|
-
sessionEvents: "\
|
|
98
|
-
compactions: "\
|
|
99
|
-
tokensSaved: "\uF155", //
|
|
100
|
-
compressionRatio:"\
|
|
101
|
-
indexedDocs: "\
|
|
102
|
-
sandboxRuns: "\
|
|
103
|
-
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", //
|
|
104
112
|
|
|
105
113
|
// Memory
|
|
106
|
-
projectCount: "\
|
|
107
|
-
totalCount: "\
|
|
108
|
-
consolidations: "\
|
|
114
|
+
projectCount: "\uEE9C", //
|
|
115
|
+
totalCount: "\uEE9C", //
|
|
116
|
+
consolidations: "\uEE9C", //
|
|
109
117
|
|
|
110
118
|
// MCP
|
|
111
|
-
serversTotal: "\
|
|
112
|
-
serversActive: "\
|
|
113
|
-
toolsTotal: "\
|
|
114
|
-
serversFailed: "\
|
|
119
|
+
serversTotal: "\u{F05B7}", //
|
|
120
|
+
serversActive: "\u{F05B7}", //
|
|
121
|
+
toolsTotal: "\u{F05B7}", //
|
|
122
|
+
serversFailed: "\u{F05B7}", //
|
|
115
123
|
|
|
116
124
|
// Ralph
|
|
117
|
-
activeLoops: "\
|
|
118
|
-
totalIterations: "\
|
|
119
|
-
loopStatus: "\
|
|
125
|
+
activeLoops: "\u{F0709}", //
|
|
126
|
+
totalIterations: "\u{F0709}", //
|
|
127
|
+
loopStatus: "\u{F0709}", //
|
|
120
128
|
|
|
121
129
|
// Workflow
|
|
122
|
-
currentCommand: "\
|
|
123
|
-
sandboxLevel: "\
|
|
124
|
-
commandDuration: "\
|
|
130
|
+
currentCommand: "\uF124", //
|
|
131
|
+
sandboxLevel: "\u{F07FE}", //
|
|
132
|
+
commandDuration: "\u{F13AB}", //
|
|
125
133
|
|
|
126
134
|
// Kanboard
|
|
127
|
-
docsCount: "\
|
|
128
|
-
tasksDone: "\
|
|
129
|
-
tasksTotal: "\
|
|
130
|
-
taskPct: "\
|
|
135
|
+
docsCount: "\u{F09EE}", //
|
|
136
|
+
tasksDone: "\u{F1A9A}", //
|
|
137
|
+
tasksTotal: "\uF4A0", //
|
|
138
|
+
taskPct: "\uF4A0", //
|
|
131
139
|
|
|
132
140
|
// Notify
|
|
133
|
-
platformsEnabled:"\
|
|
134
|
-
lastSent: "\
|
|
141
|
+
platformsEnabled:"\uEB9A", //
|
|
142
|
+
lastSent: "\u{F13AB}", //
|
|
135
143
|
|
|
136
144
|
// Extension status
|
|
137
|
-
extensionStatuses:"\
|
|
145
|
+
extensionStatuses:"\u{F15AB}", //
|
|
138
146
|
|
|
139
147
|
separator: "\uE0B1", // nf-pl-left_soft_divider
|
|
140
148
|
};
|
|
@@ -147,33 +155,38 @@ export const EMOJI_ICONS: IconSet = {
|
|
|
147
155
|
model: "🤖",
|
|
148
156
|
apiState: "🔄",
|
|
149
157
|
toolCount: "🔧",
|
|
150
|
-
git: "
|
|
158
|
+
git: "🔀",
|
|
151
159
|
context: "🗄️",
|
|
152
160
|
cost: "💲",
|
|
153
|
-
tokens: "
|
|
154
|
-
tokensIn: "
|
|
155
|
-
tokensOut: "
|
|
156
|
-
session: "
|
|
157
|
-
hostname: "
|
|
161
|
+
tokens: "📊",
|
|
162
|
+
tokensIn: "⬇️",
|
|
163
|
+
tokensOut: "⬆️",
|
|
164
|
+
session: "📋",
|
|
165
|
+
hostname: "🏠",
|
|
158
166
|
time: "⏱",
|
|
159
167
|
|
|
168
|
+
tps: "⚡",
|
|
169
|
+
clock: "🕔",
|
|
170
|
+
duration: "⏱",
|
|
171
|
+
thinkingLevel: "💡",
|
|
172
|
+
|
|
160
173
|
// Compactor
|
|
161
|
-
sessionEvents: "
|
|
162
|
-
compactions: "
|
|
174
|
+
sessionEvents: "📈",
|
|
175
|
+
compactions: "🗜️",
|
|
163
176
|
tokensSaved: "💲",
|
|
164
|
-
compressionRatio:"
|
|
165
|
-
indexedDocs: "
|
|
166
|
-
sandboxRuns: "
|
|
167
|
-
searchQueries: "
|
|
177
|
+
compressionRatio:"📐",
|
|
178
|
+
indexedDocs: "📑",
|
|
179
|
+
sandboxRuns: "▶️",
|
|
180
|
+
searchQueries: "🔍",
|
|
168
181
|
|
|
169
182
|
// Memory
|
|
170
183
|
projectCount: "🧠",
|
|
171
184
|
totalCount: "🧠",
|
|
172
|
-
consolidations: "
|
|
185
|
+
consolidations: "🔄",
|
|
173
186
|
|
|
174
187
|
// MCP
|
|
175
188
|
serversTotal: "🖥️",
|
|
176
|
-
serversActive: "
|
|
189
|
+
serversActive: "🟢",
|
|
177
190
|
toolsTotal: "🔧",
|
|
178
191
|
serversFailed: "⚠️",
|
|
179
192
|
|
|
@@ -188,17 +201,17 @@ export const EMOJI_ICONS: IconSet = {
|
|
|
188
201
|
commandDuration: "⏱",
|
|
189
202
|
|
|
190
203
|
// Kanboard
|
|
191
|
-
docsCount: "
|
|
192
|
-
tasksDone: "
|
|
193
|
-
tasksTotal: "
|
|
194
|
-
taskPct: "
|
|
204
|
+
docsCount: "📑",
|
|
205
|
+
tasksDone: "✅",
|
|
206
|
+
tasksTotal: "📋",
|
|
207
|
+
taskPct: "📊",
|
|
195
208
|
|
|
196
209
|
// Notify
|
|
197
|
-
platformsEnabled:"
|
|
210
|
+
platformsEnabled:"🔔",
|
|
198
211
|
lastSent: "⏱",
|
|
199
212
|
|
|
200
213
|
// Extension status
|
|
201
|
-
extensionStatuses:"
|
|
214
|
+
extensionStatuses:"🧩",
|
|
202
215
|
|
|
203
216
|
separator: "|",
|
|
204
217
|
};
|
|
@@ -221,6 +234,11 @@ export const TEXT_ICONS: IconSet = {
|
|
|
221
234
|
hostname: "HST",
|
|
222
235
|
time: "TIM",
|
|
223
236
|
|
|
237
|
+
tps: "TPS",
|
|
238
|
+
clock: "CLK",
|
|
239
|
+
duration: "DUR",
|
|
240
|
+
thinkingLevel: "THK",
|
|
241
|
+
|
|
224
242
|
// Compactor
|
|
225
243
|
sessionEvents: "EVT",
|
|
226
244
|
compactions: "CMP",
|
|
@@ -7,18 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
10
|
-
import type { PresetDef, FooterSegmentContext, FooterSegment, ColorScheme, RenderedSegment } from "../types.js";
|
|
10
|
+
import type { PresetDef, FooterSegmentContext, FooterSegment, ColorScheme, RenderedSegment, SegmentZone } from "../types.js";
|
|
11
11
|
import type { FooterRegistry } from "../registry/index.js";
|
|
12
12
|
import { visibleWidth as piVisibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
13
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 */
|
|
@@ -143,8 +145,8 @@ export class FooterRenderer {
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
/**
|
|
146
|
-
* Compute responsive layout for the given width.
|
|
147
|
-
* Segments
|
|
148
|
+
* Compute responsive zone-based layout for the given width.
|
|
149
|
+
* Segments are grouped by zone (left/center/right) and rendered with alignment.
|
|
148
150
|
*/
|
|
149
151
|
computeLayout(width: number): { topContent: string; secondaryContent: string } {
|
|
150
152
|
// Return cached layout if still valid
|
|
@@ -155,41 +157,58 @@ export class FooterRenderer {
|
|
|
155
157
|
|
|
156
158
|
const presetDef = getPreset(this.presetName);
|
|
157
159
|
const colors = presetDef.colors ?? getDefaultColors();
|
|
160
|
+
const settings = loadFooterSettings();
|
|
161
|
+
const labelMode = settings.showFullLabels ? "labeled" as const : "compact" as const;
|
|
162
|
+
|
|
163
|
+
// Collect all segment IDs from preset
|
|
164
|
+
const primaryIds = [...presetDef.leftSegments, ...presetDef.rightSegments];
|
|
165
|
+
const secondaryIds = [...presetDef.secondarySegments];
|
|
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
|
+
}
|
|
158
179
|
|
|
159
|
-
// Render
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
// Render segments grouped by their zone
|
|
181
|
+
const zones: Record<SegmentZone, RenderedSegmentWithWidth[]> = {
|
|
182
|
+
left: [],
|
|
183
|
+
center: [],
|
|
184
|
+
right: [],
|
|
185
|
+
};
|
|
186
|
+
const overflowZones: Record<SegmentZone, RenderedSegmentWithWidth[]> = {
|
|
187
|
+
left: [],
|
|
188
|
+
center: [],
|
|
189
|
+
right: [],
|
|
190
|
+
};
|
|
169
191
|
|
|
192
|
+
// Render primary segments and group by zone
|
|
193
|
+
for (const segId of primaryIds) {
|
|
194
|
+
const rendered = this.renderSegment(segId, colors, width, labelMode);
|
|
195
|
+
if (!rendered) continue;
|
|
170
196
|
const segment = this.segmentLookup.get(segId);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const ctx: FooterSegmentContext = {
|
|
174
|
-
theme: this.getThemeLike(),
|
|
175
|
-
colors,
|
|
176
|
-
data: this.registry.getGroupData(this.getGroupForSegment(segId)),
|
|
177
|
-
width,
|
|
178
|
-
piContext: this.piContext,
|
|
179
|
-
footerData: this.footerData,
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const rendered = segment.render(ctx);
|
|
183
|
-
if (!rendered.visible || !rendered.content) continue;
|
|
184
|
-
|
|
185
|
-
renderedSegments.push({
|
|
186
|
-
content: rendered.content,
|
|
187
|
-
width: visibleWidth(rendered.content),
|
|
188
|
-
visible: true,
|
|
189
|
-
});
|
|
197
|
+
const zone = segment?.zone ?? "center";
|
|
198
|
+
zones[zone].push(rendered);
|
|
190
199
|
}
|
|
191
200
|
|
|
192
|
-
|
|
201
|
+
// Render secondary segments
|
|
202
|
+
const secondaryRendered: RenderedSegmentWithWidth[] = [];
|
|
203
|
+
for (const segId of secondaryIds) {
|
|
204
|
+
const rendered = this.renderSegment(segId, colors, width, labelMode);
|
|
205
|
+
if (!rendered) continue;
|
|
206
|
+
secondaryRendered.push(rendered);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if we have any content
|
|
210
|
+
const totalSegments = zones.left.length + zones.center.length + zones.right.length;
|
|
211
|
+
if (totalSegments === 0 && secondaryRendered.length === 0) {
|
|
193
212
|
this.lastLayoutResult = { topContent: "", secondaryContent: "" };
|
|
194
213
|
this.lastLayoutWidth = width;
|
|
195
214
|
this.lastLayoutTimestamp = now;
|
|
@@ -197,53 +216,70 @@ export class FooterRenderer {
|
|
|
197
216
|
return this.lastLayoutResult;
|
|
198
217
|
}
|
|
199
218
|
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
const sepDef = getSeparator(settings.separator);
|
|
220
|
+
const sepWidth = visibleWidth(sepDef.left) + 2;
|
|
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
|
|
226
|
+
|
|
227
|
+
// Calculate widths per zone
|
|
228
|
+
const leftWidth = this.measureZoneWidth(zones.left, sepWidth);
|
|
229
|
+
const rightWidth = this.measureZoneWidth(zones.right, sepWidth);
|
|
230
|
+
const numZoneSeps = (leftWidth > 0 ? 1 : 0) + (rightWidth > 0 ? 1 : 0);
|
|
231
|
+
const availableForCenter = width - leftWidth - rightWidth - numZoneSeps * zoneSepWidth - 2; // -2 for margins
|
|
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);
|
|
209
241
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let currentWidth = baseOverhead;
|
|
217
|
-
let topParts: string[] = [];
|
|
218
|
-
let overflowParts: RenderedSegmentWithWidth[] = [];
|
|
219
|
-
let overflow = false;
|
|
220
|
-
|
|
221
|
-
for (const seg of primarySegments) {
|
|
222
|
-
const needed = seg.width + (topParts.length > 0 ? sepWidth : 0);
|
|
223
|
-
if (!overflow && currentWidth + needed <= width) {
|
|
224
|
-
topParts.push(seg.content);
|
|
225
|
-
currentWidth += needed;
|
|
226
|
-
} else {
|
|
227
|
-
overflow = true;
|
|
228
|
-
overflowParts.push(seg);
|
|
229
|
-
}
|
|
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);
|
|
230
248
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
|
|
254
|
+
// Overflow check: if center doesn't fit, move excess to overflow
|
|
255
|
+
const centerWidth = this.measureZoneWidth(zones.center, sepWidth);
|
|
256
|
+
if (centerWidth > Math.max(0, adjAvailableForCenter)) {
|
|
257
|
+
// Move overflow center segments to secondary
|
|
258
|
+
let fitWidth = 0;
|
|
259
|
+
let cutoffIdx = 0;
|
|
260
|
+
for (let i = 0; i < zones.center.length; i++) {
|
|
261
|
+
const needed = zones.center[i].width + (i > 0 ? sepWidth : 0);
|
|
262
|
+
if (fitWidth + needed <= Math.max(0, adjAvailableForCenter)) {
|
|
263
|
+
fitWidth += needed;
|
|
264
|
+
cutoffIdx = i + 1;
|
|
265
|
+
} else {
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
242
268
|
}
|
|
269
|
+
const overflow = zones.center.splice(cutoffIdx);
|
|
270
|
+
overflowZones.center.push(...overflow);
|
|
243
271
|
}
|
|
244
272
|
|
|
245
|
-
|
|
246
|
-
const
|
|
273
|
+
// Build top row with zones
|
|
274
|
+
const topContent = this.buildZoneRow(zones, width, sepDef, dimZoneSep);
|
|
275
|
+
|
|
276
|
+
// Build secondary row with overflow + preset secondary segments
|
|
277
|
+
const allSecondary = [...overflowZones.left, ...overflowZones.center, ...overflowZones.right, ...secondaryRendered];
|
|
278
|
+
const secondaryContent = this.buildContentFromParts(
|
|
279
|
+
allSecondary.map(s => s.content),
|
|
280
|
+
sepDef,
|
|
281
|
+
width,
|
|
282
|
+
);
|
|
247
283
|
|
|
248
284
|
this.lastLayoutResult = { topContent, secondaryContent };
|
|
249
285
|
this.lastLayoutWidth = width;
|
|
@@ -253,23 +289,153 @@ export class FooterRenderer {
|
|
|
253
289
|
return this.lastLayoutResult;
|
|
254
290
|
}
|
|
255
291
|
|
|
292
|
+
/** Render a single segment by ID, returns null if not visible */
|
|
293
|
+
private renderSegment(
|
|
294
|
+
segId: string,
|
|
295
|
+
colors: ColorScheme,
|
|
296
|
+
fullWidth: number,
|
|
297
|
+
labelMode: "compact" | "labeled",
|
|
298
|
+
): RenderedSegmentWithWidth | null {
|
|
299
|
+
if (!isSegmentEnabled(this.getGroupForSegment(segId), segId)) return null;
|
|
300
|
+
|
|
301
|
+
const segment = this.segmentLookup.get(segId);
|
|
302
|
+
if (!segment) return null;
|
|
303
|
+
|
|
304
|
+
const ctx: FooterSegmentContext = {
|
|
305
|
+
theme: this.getThemeLike(),
|
|
306
|
+
colors,
|
|
307
|
+
data: this.registry.getGroupData(this.getGroupForSegment(segId)),
|
|
308
|
+
width: fullWidth,
|
|
309
|
+
piContext: this.piContext,
|
|
310
|
+
footerData: this.footerData,
|
|
311
|
+
labelMode,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const rendered = segment.render(ctx);
|
|
315
|
+
if (!rendered.visible || !rendered.content) return null;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
content: rendered.content,
|
|
319
|
+
width: visibleWidth(rendered.content),
|
|
320
|
+
visible: true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Measure total width of a zone's rendered segments */
|
|
325
|
+
private measureZoneWidth(segments: RenderedSegmentWithWidth[], sepWidth: number): number {
|
|
326
|
+
if (segments.length === 0) return 0;
|
|
327
|
+
let total = 0;
|
|
328
|
+
for (let i = 0; i < segments.length; i++) {
|
|
329
|
+
total += segments[i].width + (i > 0 ? sepWidth : 0);
|
|
330
|
+
}
|
|
331
|
+
return total;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Build a zone-based row string */
|
|
335
|
+
private buildZoneRow(
|
|
336
|
+
zones: Record<SegmentZone, RenderedSegmentWithWidth[]>,
|
|
337
|
+
fullWidth: number,
|
|
338
|
+
sepDef: { left: string },
|
|
339
|
+
dimZoneSep: string,
|
|
340
|
+
): string {
|
|
341
|
+
const parts: string[] = [];
|
|
342
|
+
|
|
343
|
+
// Left zone
|
|
344
|
+
const leftContent = this.buildContentFromPartsRaw(
|
|
345
|
+
zones.left.map(s => s.content),
|
|
346
|
+
sepDef,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Center zone
|
|
350
|
+
const centerContent = this.buildContentFromPartsRaw(
|
|
351
|
+
zones.center.map(s => s.content),
|
|
352
|
+
sepDef,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Right zone
|
|
356
|
+
const rightContent = this.buildContentFromPartsRaw(
|
|
357
|
+
zones.right.map(s => s.content),
|
|
358
|
+
sepDef,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Assemble zones with alignment
|
|
362
|
+
const leftWidth = zones.left.length > 0 ? this.measureZoneWidth(zones.left, visibleWidth(sepDef.left) + 2) : 0;
|
|
363
|
+
const rightWidth = zones.right.length > 0 ? this.measureZoneWidth(zones.right, visibleWidth(sepDef.left) + 2) : 0;
|
|
364
|
+
|
|
365
|
+
// Simple case: no zones → return empty
|
|
366
|
+
if (!leftContent && !centerContent && !rightContent) return "";
|
|
367
|
+
|
|
368
|
+
// Build with zone separators
|
|
369
|
+
let result = " "; // leading margin
|
|
370
|
+
|
|
371
|
+
if (leftContent) {
|
|
372
|
+
result += leftContent;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (centerContent) {
|
|
376
|
+
if (leftContent && dimZoneSep) result += ` ${dimZoneSep} `;
|
|
377
|
+
result += centerContent;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (rightContent) {
|
|
381
|
+
const currentLen = visibleWidth(result);
|
|
382
|
+
const rightStart = fullWidth - rightWidth - 1; // -1 for trailing margin
|
|
383
|
+
const gap = rightStart - currentLen;
|
|
384
|
+
|
|
385
|
+
if (gap > 0) {
|
|
386
|
+
// Pad to right-align the right zone
|
|
387
|
+
result += " ".repeat(gap);
|
|
388
|
+
}
|
|
389
|
+
|
|
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
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Only append right zone if it fits within terminal width
|
|
397
|
+
if (gap >= 0) {
|
|
398
|
+
result += rightContent;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
result += " "; // trailing margin
|
|
403
|
+
|
|
404
|
+
// Safety net: never exceed terminal width
|
|
405
|
+
return truncateToWidth(result, fullWidth);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Build content from parts array (raw strings) */
|
|
409
|
+
private buildContentFromPartsRaw(parts: string[], sepDef: { left: string }): string {
|
|
410
|
+
if (parts.length === 0) return "";
|
|
411
|
+
const sep = sepDef.left;
|
|
412
|
+
const sepAnsi = getFgAnsiCode(getPreset(this.presetName).colors ?? getDefaultColors(), "separator");
|
|
413
|
+
return parts.join(` ${sepAnsi}${sep}${ANSI_RESET} `);
|
|
414
|
+
}
|
|
415
|
+
|
|
256
416
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
257
417
|
|
|
258
|
-
private buildContentFromParts(parts: string[], sepDef: { left: string }): string {
|
|
418
|
+
private buildContentFromParts(parts: string[], sepDef: { left: string }, maxWidth?: number): string {
|
|
259
419
|
if (parts.length === 0) return "";
|
|
260
420
|
const sep = sepDef.left;
|
|
261
421
|
const sepAnsi = getFgAnsiCode(getPreset(this.presetName).colors ?? getDefaultColors(), "separator");
|
|
262
|
-
|
|
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);
|
|
263
429
|
}
|
|
264
430
|
|
|
265
431
|
/** Map a segment ID to its group ID */
|
|
266
432
|
private getGroupForSegment(segId: string): string {
|
|
267
433
|
// Core segments
|
|
268
|
-
const coreIds = ["model", "api_state", "tool_count", "git", "context_pct", "cost", "tokens_total", "tokens_in", "tokens_out", "session", "hostname", "time"];
|
|
434
|
+
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"];
|
|
269
435
|
if (coreIds.includes(segId)) return "core";
|
|
270
436
|
|
|
271
437
|
// Compactor segments
|
|
272
|
-
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"];
|
|
273
439
|
if (compactorIds.includes(segId)) return "compactor";
|
|
274
440
|
|
|
275
441
|
// Memory segments
|