@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/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 (err) {
34
- console.error("[footer] Compactor stats handler error:", err);
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 (err) {
45
- console.error("[footer] Compaction handler error:", err);
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 (err) {
58
- console.error("[footer] Memory stored handler error:", err);
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 (err) {
69
- console.error("[footer] Memory deleted handler error:", err);
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 (err) {
80
- console.error("[footer] Memory consolidated handler error:", err);
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 (err) {
98
- console.error("[footer] MCP server started handler error:", err);
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 (err) {
120
- console.error("[footer] MCP server stopped handler error:", err);
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 (err) {
133
- console.error("[footer] MCP server error handler error:", err);
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 (err) {
147
- console.error("[footer] MCP tools registered handler error:", err);
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 (err) {
161
- console.error("[footer] MCP tools unregistered handler error:", err);
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 (err) {
173
- console.error("[footer] Ralph loop start handler error:", err);
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 (err) {
183
- console.error("[footer] Ralph loop end handler error:", err);
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 (err) {
194
- console.error("[footer] Ralph iteration handler error:", err);
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 (err) {
206
- console.error("[footer] Workflow start handler error:", err);
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 (err) {
216
- console.error("[footer] Workflow end handler error:", err);
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 (err) {
228
- console.error("[footer] Notification handler error:", err);
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 (err) {
241
- console.error("[footer] Module ready handler error:", err);
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 { getThinkingLevel, rainbowBorder } from "./segments/core.js";
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 + rainbow input border for xhigh thinking
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 on left, right, and secondary rows,
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", "thinking", "path", "git", "context_pct", "cost",
18
+ "model", "api_state", "tool_count", "git",
16
19
  ],
17
20
  rightSegments: [
18
- "compactions", "tokens_saved", "project_count", "loop_status",
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
- "current_command", "session_events",
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
- "path", "git", "context_pct",
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", "cost", "context_pct",
48
+ "model", "git",
42
49
  ],
43
50
  rightSegments: [
44
- "compactions", "total_count",
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", "thinking", "path", "git", "context_pct", "cost",
55
- "tokens_total", "tokens_in", "tokens_out",
61
+ "model", "api_state", "tool_count", "git", "current_command", "session",
56
62
  ],
57
63
  rightSegments: [
58
- "session_events", "compactions", "tokens_saved", "compression_ratio",
59
- "indexed_docs", "sandbox_runs", "search_queries",
60
- "project_count", "total_count", "consolidations",
61
- "servers_total", "servers_active", "tools_total", "servers_failed",
62
- "active_loops", "total_iterations", "loop_status",
63
- "current_command", "command_duration",
64
- "docs_count", "tasks_done", "tasks_total", "task_pct",
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", "time",
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", "thinking", "path", "git", "context_pct", "cost",
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
- "current_command",
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", "time",
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", "path", "git", "context_pct", "cost",
111
+ "model", "git",
103
112
  ],
104
113
  rightSegments: [
105
- "compactions", "tokens_saved", "project_count",
114
+ "tps", "context_pct", "cost",
115
+ "clock", "duration",
106
116
  ],
107
117
  secondarySegments: [],
108
- separator: "ascii",
109
118
  colors: getDefaultColors(),
110
119
  };
111
120
 
@@ -115,19 +115,17 @@ export class FooterRegistry {
115
115
  for (const callback of this.subscribers) {
116
116
  try {
117
117
  callback();
118
- } catch (err) {
119
- console.error("[footer] Subscriber error:", err);
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(event: string, ...args: unknown[]): void {
127
- if (!this.debug) return;
128
- const ts = new Date().toISOString().slice(11, 23);
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