@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.
@@ -8,39 +8,66 @@
8
8
  import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
9
9
  import type { ColorScheme, ColorValue, SemanticColor, ThemeLike } from "../types.js";
10
10
 
11
+ /** Wrap text in dim ANSI codes for muted placeholder display */
12
+ export function mutedPlaceholder(text: string): string {
13
+ return `\x1b[2m${text}\x1b[0m`;
14
+ }
15
+
11
16
  /** Default semantic-to-theme-color mapping */
12
- const DEFAULT_COLOR_MAP: Record<SemanticColor, ThemeColor> = {
13
- model: "accent",
17
+ const DEFAULT_COLOR_MAP: Record<SemanticColor, ThemeColor | `#${string}`> = {
18
+ // ── Model & Identity (Left zone) ──
19
+ model: "#c792ea", // Soft purple — model name
14
20
  path: "text",
15
- git: "accent",
16
- compactor: "muted",
17
- memory: "accent",
18
- mcp: "success",
19
- ralph: "warning",
20
- ralphOn: "success",
21
- ralphOff: "error",
22
- workflow: "accent",
23
- workflowBrainstorm: "warning",
24
- workflowPlan: "success",
25
- workflowWork: "accent",
26
- workflowReview: "muted",
27
- workflowAuto: "thinkingHigh",
28
- workflowOther: "dim",
29
- kanboard: "dim",
30
- notify: "muted",
21
+ git: "#82cc6f", // Green (clean default)
22
+ gitClean: "#82cc6f", // Green — clean branch
23
+ gitDirty: "#e5c07b", // Amber — dirty branch
24
+ session: "#61afef", // Blue — session name
25
+ worktree: "#61afef", // Blue — worktree indicator
26
+ // ── Workflow (Left zone) ──
27
+ workflow: "#c792ea", // Purple (default)
28
+ workflowNone: "#4a6a7a", // Muted teal — idle
29
+ workflowBrainstorm: "#e06c75", // Red
30
+ workflowPlan: "#d19a66", // Orange
31
+ workflowWork: "#e5c07b", // Yellow
32
+ workflowReview: "#82cc6f", // Green
33
+ workflowAuto: "#c792ea", // Purple
34
+ workflowDebug: "#e06c75", // Red
35
+ workflowChoreExec: "#d19a66", // Orange
36
+ workflowOther: "#c792ea", // Purple
37
+ // ── TPS tiers (Center zone) ──
38
+ tpsSlow: "#e06c75", // Red — < 30 t/s
39
+ tpsModerate: "#e5c07b", // Amber — 30-50 t/s
40
+ tpsGood: "#56d4bc", // Teal — 50-100 t/s
41
+ tpsFast: "#82cc6f", // Green — 100-200 t/s
42
+ tpsBlazing: "#c792ea", // Purple — > 200 t/s
43
+ tpsIdle: "#4a6a7a", // Muted teal — session avg when idle
44
+ // ── Metrics (Center zone) ──
45
+ compactor: "#56b6c2", // Cyan — compaction stats
46
+ memory: "#61afef", // Blue — memory count
47
+ mcp: "#82cc6f", // Green — MCP status
48
+ ralph: "#e5c07b", // Amber — ralph loops
49
+ ralphOn: "#82cc6c", // Green — ralph active
50
+ ralphOff: "#e06c75", // Red — ralph inactive
51
+ kanboard: "#c678dd", // Purple — kanboard
52
+ notify: "#56b6c2", // Cyan — notifications
53
+ context: "muted", // Theme token for OK context
54
+ contextWarn: "#e5c07b", // Amber — context 70-90%
55
+ contextError: "#e06c75", // Red — context > 90%
56
+ cost: "#d19a66", // Gold — cost
57
+ tokens: "#abb2bf", // Silver — token counts
58
+ // ── Time (Right zone) ──
59
+ clock: "#abb2bf", // Silver — wall clock
60
+ duration: "#61afef", // Blue — session duration
61
+ // ── Thinking levels ──
62
+ thinking: "#61afef",
63
+ thinkingMinimal: "#56b6c2", // Cyan
64
+ thinkingLow: "#61afef", // Blue
65
+ thinkingMedium: "#c792ea", // Purple
66
+ thinkingHigh: "#d19a66", // Gold
67
+ thinkingXhigh: "#e06c75", // Red
68
+ // ── UI chrome ──
31
69
  separator: "dim",
32
70
  border: "dim",
33
- context: "muted",
34
- contextWarn: "warning",
35
- contextError: "error",
36
- cost: "text",
37
- tokens: "muted",
38
- thinking: "accent",
39
- thinkingMinimal: "thinkingMinimal",
40
- thinkingLow: "thinkingLow",
41
- thinkingMedium: "thinkingMedium",
42
- thinkingHigh: "thinkingHigh",
43
- thinkingXhigh: "thinkingXhigh",
44
71
  };
45
72
 
46
73
  /**
@@ -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, indexed_docs, sandbox_runs, search_queries.
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)
@@ -10,8 +10,9 @@
10
10
  */
11
11
 
12
12
  import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
13
- import { applyColor } from "../rendering/theme.js";
13
+ import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
14
14
  import { getIcon } from "../rendering/icons.js";
15
+ import { isSegmentEnabled } from "../config.js";
15
16
 
16
17
  function withIcon(segmentId: string, text: string): string {
17
18
  const icon = getIcon(segmentId);
@@ -50,7 +51,12 @@ function getSessionEvents(ctx: FooterSegmentContext): any[] {
50
51
  function renderSessionEventsSegment(ctx: FooterSegmentContext): RenderedSegment {
51
52
  const events = getSessionEvents(ctx);
52
53
  const count = events.length;
53
- if (count === 0) return hidden();
54
+ if (count === 0) {
55
+ if (isSegmentEnabled("compactor", "session_events")) {
56
+ return { content: mutedPlaceholder(withIcon("sessionEvents", "0")), visible: true };
57
+ }
58
+ return hidden();
59
+ }
54
60
 
55
61
  const content = withIcon("sessionEvents", `${count}`);
56
62
  return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
@@ -66,22 +72,32 @@ function renderCompactionsSegment(ctx: FooterSegmentContext): RenderedSegment {
66
72
  compactionCount++;
67
73
  }
68
74
  }
69
- if (compactionCount === 0) return hidden();
75
+ if (compactionCount === 0) {
76
+ if (isSegmentEnabled("compactor", "compactions")) {
77
+ return { content: mutedPlaceholder(withIcon("compactions", "0")), visible: true };
78
+ }
79
+ return hidden();
80
+ }
70
81
 
71
82
  const content = withIcon("compactions", `${compactionCount}`);
72
83
  return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
73
84
  }
74
85
 
75
86
  function renderTokensSavedSegment(ctx: FooterSegmentContext): RenderedSegment {
76
- // Sum tokens saved from compaction events if available
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.
77
90
  const events = getSessionEvents(ctx);
78
91
  let tokensSaved = 0;
79
92
  let hasCompaction = false;
80
93
  for (const e of events) {
81
94
  if (!e || typeof e !== "object") continue;
82
- if (e.type === "compaction" || e.type === "compacted") {
95
+ if (e.type === "compaction") {
83
96
  hasCompaction = true;
84
- tokensSaved += Number(e.tokensSaved ?? e.tokens_saved ?? 0);
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);
85
101
  }
86
102
  }
87
103
  if (!hasCompaction || tokensSaved === 0) return hidden();
@@ -91,45 +107,75 @@ function renderTokensSavedSegment(ctx: FooterSegmentContext): RenderedSegment {
91
107
  }
92
108
 
93
109
  function renderCompressionRatioSegment(ctx: FooterSegmentContext): RenderedSegment {
94
- // Check last compaction event for compression ratio
110
+ // Calculate compression ratio from Pi's CompactionEntry.tokensBefore.
111
+ // Compaction keeps ~12% of context, giving ~8:1 compression.
95
112
  const events = getSessionEvents(ctx);
96
- let lastRatio: number | undefined;
113
+ let totalBefore = 0;
114
+ let totalAfter = 0;
97
115
  for (const e of events) {
98
116
  if (!e || typeof e !== "object") continue;
99
- if (e.type === "compaction" || e.type === "compacted") {
100
- const ratio = e.compressionRatio ?? e.compression_ratio;
101
- if (ratio !== undefined && ratio !== null) {
102
- lastRatio = Number(ratio);
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);
103
122
  }
104
123
  }
105
124
  }
106
- if (lastRatio === undefined) return hidden();
125
+ if (totalBefore === 0 || totalAfter === 0) return hidden();
107
126
 
108
- const content = withIcon("compressionRatio", `${lastRatio.toFixed(1)}x`);
127
+ const ratio = totalBefore / totalAfter;
128
+ const content = withIcon("compressionRatio", `${ratio.toFixed(1)}x`);
109
129
  return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
110
130
  }
111
131
 
112
- function renderIndexedDocsSegment(_ctx: FooterSegmentContext): RenderedSegment {
113
- // No reliable data source for indexed docs count
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.
114
135
  return hidden();
115
136
  }
116
137
 
117
- function renderSandboxRunsSegment(_ctx: FooterSegmentContext): RenderedSegment {
118
- // No reliable data source for sandbox run count
119
- return hidden();
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 };
120
154
  }
121
155
 
122
- function renderSearchQueriesSegment(_ctx: FooterSegmentContext): RenderedSegment {
123
- // No reliable data source for search query count
124
- return hidden();
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 };
125
171
  }
126
172
 
127
173
  export const COMPACTOR_SEGMENTS: FooterSegment[] = [
128
- { id: "session_events", label: "Session Events", icon: "", render: renderSessionEventsSegment, defaultShow: true },
129
- { id: "compactions", label: "Compactions", icon: "", render: renderCompactionsSegment, defaultShow: true },
130
- { id: "tokens_saved", label: "Tokens Saved", icon: "", render: renderTokensSavedSegment, defaultShow: true },
131
- { id: "compression_ratio", label: "Compression Ratio", icon: "", render: renderCompressionRatioSegment, defaultShow: false },
132
- { id: "indexed_docs", label: "Indexed Docs", icon: "", render: renderIndexedDocsSegment, defaultShow: false },
133
- { id: "sandbox_runs", label: "Sandbox Runs", icon: "", render: renderSandboxRunsSegment, defaultShow: false },
134
- { id: "search_queries", label: "Search Queries", icon: "", render: renderSearchQueriesSegment, defaultShow: false },
174
+ { id: "session_events", label: "Session Events", shortLabel: "EVT", description: "Number of session events", zone: "center", icon: "", render: renderSessionEventsSegment, defaultShow: true },
175
+ { id: "compactions", label: "Compactions", shortLabel: "CMP", description: "Number of context compactions", zone: "center", icon: "", render: renderCompactionsSegment, defaultShow: true },
176
+ { id: "tokens_saved", label: "Tokens Saved", shortLabel: "SVD", description: "Tokens saved by compaction", zone: "center", icon: "", render: renderTokensSavedSegment, defaultShow: true },
177
+ { id: "compression_ratio", label: "Compression Ratio", shortLabel: "RAT", description: "Last compaction compression ratio", zone: "center", icon: "", render: renderCompressionRatioSegment, defaultShow: false },
178
+ { id: "cocoindex_status", label: "CocoIndex", shortLabel: "CIDX", description: "CocoIndex indexing status", zone: "center", icon: "", render: renderCocoindexStatusSegment, defaultShow: false },
179
+ { id: "sandbox_runs", label: "Sandbox Runs", shortLabel: "SBX", description: "Number of sandbox code runs", zone: "center", icon: "", render: renderSandboxRunsSegment, defaultShow: false },
180
+ { id: "search_queries", label: "Search Queries", shortLabel: "QRY", description: "Number of search queries", zone: "center", icon: "", render: renderSearchQueriesSegment, defaultShow: false },
135
181
  ];
@@ -10,6 +10,7 @@ import { hostname as osHostname } from "node:os";
10
10
  import type { FooterSegment, FooterSegmentContext, RenderedSegment, SemanticColor } from "../types.js";
11
11
  import { applyColor } from "../rendering/theme.js";
12
12
  import { getIcon } from "../rendering/icons.js";
13
+ import { tpsTracker } from "../tps-tracker.js";
13
14
 
14
15
  // ─── Helpers ────────────────────────────────────────────────────────────────
15
16
 
@@ -130,8 +131,10 @@ function renderGitSegment(ctx: FooterSegmentContext): RenderedSegment {
130
131
  const branch = footerData?.getGitBranch?.() ?? null;
131
132
  if (!branch) return { content: "", visible: false };
132
133
 
134
+ const isDirty = footerData?.getGitDirty?.() ?? false;
135
+ const semanticColor: SemanticColor = isDirty ? "gitDirty" : "gitClean";
133
136
  const content = withIcon("git", branch);
134
- return { content: color(ctx, "git", content), visible: true };
137
+ return { content: color(ctx, semanticColor, content), visible: true };
135
138
  }
136
139
 
137
140
  function renderContextPctSegment(ctx: FooterSegmentContext): RenderedSegment {
@@ -173,7 +176,8 @@ function renderCostSegment(ctx: FooterSegmentContext): RenderedSegment {
173
176
  if (!stats.cost && !usingSubscription) return { content: "", visible: false };
174
177
 
175
178
  const costDisplay = usingSubscription ? "(sub)" : `$${stats.cost.toFixed(2)}`;
176
- return { content: color(ctx, "cost", costDisplay), visible: true };
179
+ const content = withIcon("cost", costDisplay);
180
+ return { content: color(ctx, "cost", content), visible: true };
177
181
  }
178
182
 
179
183
  function renderTokensSegment(variant: "total" | "in" | "out"): (ctx: FooterSegmentContext) => RenderedSegment {
@@ -206,7 +210,7 @@ function renderSessionSegment(ctx: FooterSegmentContext): RenderedSegment {
206
210
  const sessionId = (piCtx?.sessionManager as any)?.getSessionId?.();
207
211
  const display = sessionId?.slice(0, 8) || "new";
208
212
  const content = withIcon("session", display);
209
- return { content: color(ctx, "model", content), visible: true };
213
+ return { content: color(ctx, "session", content), visible: true };
210
214
  }
211
215
 
212
216
  function renderHostnameSegment(_ctx: FooterSegmentContext): RenderedSegment {
@@ -224,19 +228,124 @@ function renderTimeSegment(ctx: FooterSegmentContext): RenderedSegment {
224
228
  return { content, visible: true };
225
229
  }
226
230
 
231
+ // ─── TPS tier color function ────────────────────────────────────────────────
232
+
233
+ function getTpsSemanticColor(tps: number): SemanticColor {
234
+ if (tps > 200) return "tpsBlazing";
235
+ if (tps > 100) return "tpsFast";
236
+ if (tps > 50) return "tpsGood";
237
+ if (tps > 30) return "tpsModerate";
238
+ return "tpsSlow";
239
+ }
240
+
241
+ function renderTpsSegment(ctx: FooterSegmentContext): RenderedSegment {
242
+ const streaming = tpsTracker.isStreaming();
243
+ const liveTps = tpsTracker.getLiveTps();
244
+ const avgTps = tpsTracker.getSessionAvgTps();
245
+
246
+ // No data yet — hide
247
+ if (!tpsTracker.getTotalOutput()) return { content: "", visible: false };
248
+
249
+ const icon = getIcon("tps");
250
+
251
+ if (streaming && liveTps > 0) {
252
+ // Active generation: show live rate + avg
253
+ const liveDisplay = Math.round(liveTps);
254
+ const avgDisplay = Math.round(avgTps);
255
+ const liveText = `\u2191 ${liveDisplay} T/S`;
256
+ const avgText = `AVG ${avgDisplay}`;
257
+ const liveColored = applyColor(getTpsSemanticColor(liveTps), liveText, ctx.theme, ctx.colors);
258
+ const avgColored = applyColor("tpsIdle", avgText, ctx.theme, ctx.colors);
259
+ const content = icon ? `${icon} ${liveColored} \u00b7 ${avgColored}` : `${liveColored} \u00b7 ${avgColored}`;
260
+ return { content, visible: true };
261
+ }
262
+
263
+ // Idle: show session average
264
+ const avgDisplay = Math.round(avgTps);
265
+ const avgText = `AVG ${avgDisplay} T/S`;
266
+ const avgColored = applyColor("tpsIdle", avgText, ctx.theme, ctx.colors);
267
+ const content = icon ? `${icon} ${avgColored}` : avgColored;
268
+ return { content, visible: true };
269
+ }
270
+
271
+ function renderClockSegment(ctx: FooterSegmentContext): RenderedSegment {
272
+ const now = new Date();
273
+ const h = now.getHours().toString().padStart(2, "0");
274
+ const m = now.getMinutes().toString().padStart(2, "0");
275
+ const s = now.getSeconds().toString().padStart(2, "0");
276
+ const timeStr = `${h}:${m}:${s}`;
277
+ const content = withIcon("clock", timeStr);
278
+ return { content: color(ctx, "clock", content), visible: true };
279
+ }
280
+
281
+ function renderDurationSegment(ctx: FooterSegmentContext): RenderedSegment {
282
+ // Derive session duration from sessionManager
283
+ const piCtx = ctx.piContext as Record<string, unknown> | undefined;
284
+ const sessionStart = (piCtx?.sessionManager as any)?.getSessionStartTime?.();
285
+ if (!sessionStart) {
286
+ // Fallback: show current time segment style
287
+ return { content: "", visible: false };
288
+ }
289
+
290
+ const elapsedMs = Date.now() - sessionStart;
291
+ const totalSeconds = Math.floor(elapsedMs / 1000);
292
+ const hours = Math.floor(totalSeconds / 3600);
293
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
294
+ const seconds = totalSeconds % 60;
295
+
296
+ let display: string;
297
+ if (hours > 0) {
298
+ display = `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
299
+ } else {
300
+ display = `${minutes}:${seconds.toString().padStart(2, "0")}`;
301
+ }
302
+
303
+ const content = withIcon("duration", display);
304
+ return { content: color(ctx, "duration", content), visible: true };
305
+ }
306
+
307
+ // ─── Thinking level ──────────────────────────────────────────────────────────
308
+
309
+ /** Map thinking level to semantic color */
310
+ function getThinkingSemanticColor(level: string | undefined): SemanticColor {
311
+ switch (level) {
312
+ case "minimal": return "thinkingMinimal";
313
+ case "low": return "thinkingLow";
314
+ case "medium": return "thinkingMedium";
315
+ case "high": return "thinkingHigh";
316
+ case "xhigh": return "thinkingXhigh";
317
+ default: return "thinking";
318
+ }
319
+ }
320
+
321
+ function renderThinkingLevelSegment(ctx: FooterSegmentContext): RenderedSegment {
322
+ const piCtx = ctx.piContext as Record<string, unknown> | undefined;
323
+ const model = piCtx?.model as Record<string, unknown> | undefined;
324
+ const thinkingLevel = model?.thinkingLevel as string | undefined;
325
+
326
+ if (!thinkingLevel || thinkingLevel === "off") return { content: "", visible: false };
327
+
328
+ const semanticColor = getThinkingSemanticColor(thinkingLevel);
329
+ const content = withIcon("thinkingLevel", thinkingLevel);
330
+ return { content: color(ctx, semanticColor, content), visible: true };
331
+ }
332
+
227
333
  // ─── Core segments array ────────────────────────────────────────────────────
228
334
 
229
335
  export const CORE_SEGMENTS: FooterSegment[] = [
230
- { id: "model", label: "Model", icon: "", render: renderModelSegment, defaultShow: true },
231
- { id: "api_state", label: "WEB", icon: "", render: renderApiStateSegment, defaultShow: true },
232
- { id: "tool_count", label: "Tool Count", icon: "", render: renderToolCountSegment, defaultShow: true },
233
- { id: "git", label: "Git", icon: "", render: renderGitSegment, defaultShow: true },
234
- { id: "context_pct", label: "Context %", icon: "", render: renderContextPctSegment, defaultShow: true },
235
- { id: "cost", label: "Cost", icon: "", render: renderCostSegment, defaultShow: true },
236
- { id: "tokens_total", label: "Tokens Total", icon: "", render: renderTokensSegment("total"), defaultShow: false },
237
- { id: "tokens_in", label: "Tokens In", icon: "", render: renderTokensSegment("in"), defaultShow: false },
238
- { id: "tokens_out", label: "Tokens Out", icon: "", render: renderTokensSegment("out"), defaultShow: false },
239
- { id: "session", label: "Session", icon: "", render: renderSessionSegment, defaultShow: false },
240
- { id: "hostname", label: "Hostname", icon: "", render: renderHostnameSegment, defaultShow: false },
241
- { id: "time", label: "Time", icon: "", render: renderTimeSegment, defaultShow: false },
336
+ { id: "model", label: "Model", shortLabel: "MDL", description: "Current model name", zone: "left", icon: "", render: renderModelSegment, defaultShow: true },
337
+ { id: "api_state", label: "API", shortLabel: "API", description: "API connection state", zone: "left", icon: "", render: renderApiStateSegment, defaultShow: true },
338
+ { id: "tool_count", label: "Tool Count", shortLabel: "TLS", description: "Number of tools available", zone: "left", icon: "", render: renderToolCountSegment, defaultShow: true },
339
+ { id: "git", label: "Git", shortLabel: "GIT", description: "Current git branch + dirty/clean status", zone: "left", icon: "", render: renderGitSegment, defaultShow: true },
340
+ { id: "tps", label: "TPS", shortLabel: "TPS", description: "Tokens per second \u2014 live during generation", zone: "center", icon: "", render: renderTpsSegment, defaultShow: true },
341
+ { id: "context_pct", label: "Context %", shortLabel: "CTX", description: "Context window usage percentage", zone: "center", icon: "", render: renderContextPctSegment, defaultShow: true },
342
+ { id: "cost", label: "Cost", shortLabel: "CST", description: "Session cost in USD", zone: "center", icon: "", render: renderCostSegment, defaultShow: true },
343
+ { id: "tokens_total", label: "Tokens Total", shortLabel: "TOK", description: "Total tokens used this session", zone: "center", icon: "", render: renderTokensSegment("total"), defaultShow: false },
344
+ { id: "tokens_in", label: "Tokens In", shortLabel: "TIN", description: "Input tokens consumed", zone: "center", icon: "", render: renderTokensSegment("in"), defaultShow: false },
345
+ { id: "tokens_out", label: "Tokens Out", shortLabel: "TOUT", description: "Output tokens generated", zone: "center", icon: "", render: renderTokensSegment("out"), defaultShow: false },
346
+ { id: "session", label: "Session", shortLabel: "SES", description: "Session identifier", zone: "left", icon: "", render: renderSessionSegment, defaultShow: false },
347
+ { id: "hostname", label: "Hostname", shortLabel: "HST", description: "Machine hostname", zone: "left", icon: "", render: renderHostnameSegment, defaultShow: false },
348
+ { id: "clock", label: "Clock", shortLabel: "CLK", description: "Current wall time (HH:MM:SS)", zone: "right", icon: "", render: renderClockSegment, defaultShow: true },
349
+ { id: "duration", label: "Duration", shortLabel: "DUR", description: "Session duration", zone: "right", icon: "", render: renderDurationSegment, defaultShow: true },
350
+ { id: "thinking_level", label: "Thinking", shortLabel: "THK", description: "Current model thinking level", zone: "center", icon: "", render: renderThinkingLevelSegment, defaultShow: false },
242
351
  ];
@@ -7,8 +7,9 @@
7
7
  */
8
8
 
9
9
  import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
10
- import { applyColor } from "../rendering/theme.js";
10
+ import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
11
11
  import { getIcon } from "../rendering/icons.js";
12
+ import { isSegmentEnabled } from "../config.js";
12
13
 
13
14
  function withIcon(segmentId: string, text: string): string {
14
15
  const icon = getIcon(segmentId);
@@ -21,7 +22,7 @@ function withIcon(segmentId: string, text: string): string {
21
22
  */
22
23
  function getKanboardData(): Record<string, unknown> | null {
23
24
  try {
24
- const registry = (globalThis as Record<string, unknown>).__unipi_kanboard_registry;
25
+ const registry = globalThis.__unipi_kanboard_registry;
25
26
  if (!registry || typeof registry !== "object") return null;
26
27
  return registry as Record<string, unknown>;
27
28
  } catch {
@@ -32,7 +33,12 @@ function getKanboardData(): Record<string, unknown> | null {
32
33
  function renderDocsCountSegment(ctx: FooterSegmentContext): RenderedSegment {
33
34
  const kb = getKanboardData();
34
35
  const value = kb?.docsCount;
35
- if (value === undefined || value === null) return { content: "", visible: false };
36
+ if (value === undefined || value === null) {
37
+ if (isSegmentEnabled("kanboard", "docs_count")) {
38
+ return { content: mutedPlaceholder(withIcon("docsCount", "0")), visible: true };
39
+ }
40
+ return { content: "", visible: false };
41
+ }
36
42
  const content = withIcon("docsCount", `${value}`);
37
43
  return { content: applyColor("kanboard", content, ctx.theme, ctx.colors), visible: true };
38
44
  }
@@ -40,7 +46,12 @@ function renderDocsCountSegment(ctx: FooterSegmentContext): RenderedSegment {
40
46
  function renderTasksDoneSegment(ctx: FooterSegmentContext): RenderedSegment {
41
47
  const kb = getKanboardData();
42
48
  const value = kb?.tasksDone;
43
- if (value === undefined || value === null) return { content: "", visible: false };
49
+ if (value === undefined || value === null) {
50
+ if (isSegmentEnabled("kanboard", "tasks_done")) {
51
+ return { content: mutedPlaceholder(withIcon("tasksDone", "0")), visible: true };
52
+ }
53
+ return { content: "", visible: false };
54
+ }
44
55
  const content = withIcon("tasksDone", `${value}`);
45
56
  return { content: applyColor("kanboard", content, ctx.theme, ctx.colors), visible: true };
46
57
  }
@@ -48,7 +59,12 @@ function renderTasksDoneSegment(ctx: FooterSegmentContext): RenderedSegment {
48
59
  function renderTasksTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
49
60
  const kb = getKanboardData();
50
61
  const value = kb?.tasksTotal;
51
- if (value === undefined || value === null) return { content: "", visible: false };
62
+ if (value === undefined || value === null) {
63
+ if (isSegmentEnabled("kanboard", "tasks_total")) {
64
+ return { content: mutedPlaceholder(withIcon("tasksTotal", "0")), visible: true };
65
+ }
66
+ return { content: "", visible: false };
67
+ }
52
68
  const content = withIcon("tasksTotal", `${value}`);
53
69
  return { content: applyColor("kanboard", content, ctx.theme, ctx.colors), visible: true };
54
70
  }
@@ -68,8 +84,8 @@ function renderTaskPctSegment(ctx: FooterSegmentContext): RenderedSegment {
68
84
  }
69
85
 
70
86
  export const KANBOARD_SEGMENTS: FooterSegment[] = [
71
- { id: "docs_count", label: "Docs Count", icon: "", render: renderDocsCountSegment, defaultShow: true },
72
- { id: "tasks_done", label: "Tasks Done", icon: "", render: renderTasksDoneSegment, defaultShow: true },
73
- { id: "tasks_total", label: "Tasks Total", icon: "", render: renderTasksTotalSegment, defaultShow: true },
74
- { id: "task_pct", label: "Task %", icon: "", render: renderTaskPctSegment, defaultShow: true },
87
+ { id: "docs_count", label: "Docs", shortLabel: "DOC", description: "Workflow documents count", zone: "center", icon: "", render: renderDocsCountSegment, defaultShow: true },
88
+ { id: "tasks_done", label: "Done", shortLabel: "DNE", description: "Completed tasks", zone: "center", icon: "", render: renderTasksDoneSegment, defaultShow: true },
89
+ { id: "tasks_total", label: "Total", shortLabel: "TSK", description: "Total tasks", zone: "center", icon: "", render: renderTasksTotalSegment, defaultShow: true },
90
+ { id: "task_pct", label: "Progress", shortLabel: "PCT", description: "Task completion percentage", zone: "center", icon: "", render: renderTaskPctSegment, defaultShow: true },
75
91
  ];
@@ -13,8 +13,9 @@
13
13
  */
14
14
 
15
15
  import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
16
- import { applyColor } from "../rendering/theme.js";
16
+ import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
17
17
  import { getIcon } from "../rendering/icons.js";
18
+ import { isSegmentEnabled } from "../config.js";
18
19
 
19
20
  /** Shape of aggregate MCP stats from globalThis or registry */
20
21
  interface McpStats {
@@ -24,14 +25,6 @@ interface McpStats {
24
25
  toolsTotal?: number;
25
26
  }
26
27
 
27
- /** Shape of the global escape-hatch object */
28
- interface GlobalMcpStats extends McpStats {}
29
-
30
- declare global {
31
- // eslint-disable-next-line no-var
32
- var __unipi_mcp_stats: GlobalMcpStats | undefined;
33
- }
34
-
35
28
  function withIcon(segmentId: string, text: string): string {
36
29
  const icon = getIcon(segmentId);
37
30
  return icon ? `${icon} ${text}` : text;
@@ -64,21 +57,37 @@ function hasUsefulValue(value: unknown): value is number {
64
57
 
65
58
  function renderServersTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
66
59
  const stats = getMcpStats(ctx);
67
- if (!hasUsefulValue(stats.serversTotal)) return { content: "", visible: false };
60
+ if (!hasUsefulValue(stats.serversTotal)) {
61
+ if (isSegmentEnabled("mcp", "servers_total")) {
62
+ return { content: mutedPlaceholder(withIcon("serversTotal", "0")), visible: true };
63
+ }
64
+ return { content: "", visible: false };
65
+ }
68
66
  const content = withIcon("serversTotal", `${stats.serversTotal}`);
69
67
  return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
70
68
  }
71
69
 
72
70
  function renderServersActiveSegment(ctx: FooterSegmentContext): RenderedSegment {
73
71
  const stats = getMcpStats(ctx);
74
- if (!hasUsefulValue(stats.serversActive)) return { content: "", visible: false };
72
+ if (!hasUsefulValue(stats.serversActive)) {
73
+ if (isSegmentEnabled("mcp", "servers_active")) {
74
+ const total = stats.serversTotal ?? 0;
75
+ return { content: mutedPlaceholder(withIcon("serversActive", `${total}/0`)), visible: true };
76
+ }
77
+ return { content: "", visible: false };
78
+ }
75
79
  const content = withIcon("serversActive", `${stats.serversActive}`);
76
80
  return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
77
81
  }
78
82
 
79
83
  function renderToolsTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
80
84
  const stats = getMcpStats(ctx);
81
- if (!hasUsefulValue(stats.toolsTotal)) return { content: "", visible: false };
85
+ if (!hasUsefulValue(stats.toolsTotal)) {
86
+ if (isSegmentEnabled("mcp", "tools_total")) {
87
+ return { content: mutedPlaceholder(withIcon("toolsTotal", "0")), visible: true };
88
+ }
89
+ return { content: "", visible: false };
90
+ }
82
91
  const content = withIcon("toolsTotal", `${stats.toolsTotal}`);
83
92
  return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
84
93
  }
@@ -93,8 +102,8 @@ function renderServersFailedSegment(ctx: FooterSegmentContext): RenderedSegment
93
102
  }
94
103
 
95
104
  export const MCP_SEGMENTS: FooterSegment[] = [
96
- { id: "servers_total", label: "Servers Total", icon: "", render: renderServersTotalSegment, defaultShow: true },
97
- { id: "servers_active", label: "Servers Active", icon: "", render: renderServersActiveSegment, defaultShow: true },
98
- { id: "tools_total", label: "Tools Total", icon: "", render: renderToolsTotalSegment, defaultShow: true },
99
- { id: "servers_failed", label: "Servers Failed", icon: "", render: renderServersFailedSegment, defaultShow: true },
105
+ { id: "servers_total", label: "Servers", shortLabel: "SRV", description: "Total MCP servers configured", zone: "center", icon: "", render: renderServersTotalSegment, defaultShow: true },
106
+ { id: "servers_active", label: "Active", shortLabel: "ACT", description: "Currently connected MCP servers", zone: "center", icon: "", render: renderServersActiveSegment, defaultShow: true },
107
+ { id: "tools_total", label: "Tools", shortLabel: "TLS", description: "Total MCP tools available", zone: "center", icon: "", render: renderToolsTotalSegment, defaultShow: true },
108
+ { id: "servers_failed", label: "Failed", shortLabel: "ERR", description: "Failed MCP server connections", zone: "center", icon: "", render: renderServersFailedSegment, defaultShow: true },
100
109
  ];
@@ -18,8 +18,9 @@
18
18
  */
19
19
 
20
20
  import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
21
- import { applyColor } from "../rendering/theme.js";
21
+ import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
22
22
  import { getIcon } from "../rendering/icons.js";
23
+ import { isSegmentEnabled } from "../config.js";
23
24
 
24
25
  function withIcon(segmentId: string, text: string): string {
25
26
  const icon = getIcon(segmentId);
@@ -50,8 +51,7 @@ interface InfoRegistryLike {
50
51
  */
51
52
  function getInfoRegistryMemoryData(): InfoMemoryData | null {
52
53
  try {
53
- const g = globalThis as Record<string, unknown>;
54
- const registry = g.__unipi_info_registry;
54
+ const registry = globalThis.__unipi_info_registry;
55
55
  if (!registry || typeof registry !== "object") return null;
56
56
 
57
57
  // The info registry exposes getCachedData(groupId) synchronously
@@ -81,6 +81,9 @@ function getMemoryCounts(): { project: number | null; total: number | null } {
81
81
  function renderProjectCountSegment(ctx: FooterSegmentContext): RenderedSegment {
82
82
  const counts = getMemoryCounts();
83
83
  if (counts.project === null) {
84
+ if (isSegmentEnabled("memory", "project_count")) {
85
+ return { content: mutedPlaceholder(withIcon("projectCount", "0")), visible: true };
86
+ }
84
87
  return { content: "", visible: false };
85
88
  }
86
89
 
@@ -136,7 +139,7 @@ function renderConsolidationsSegment(ctx: FooterSegmentContext): RenderedSegment
136
139
  }
137
140
 
138
141
  export const MEMORY_SEGMENTS: FooterSegment[] = [
139
- { id: "project_count", label: "Project Count", icon: "", render: renderProjectCountSegment, defaultShow: true },
140
- { id: "total_count", label: "Total Count", icon: "", render: renderTotalCountSegment, defaultShow: true },
141
- { id: "consolidations", label: "Consolidations", icon: "", render: renderConsolidationsSegment, defaultShow: false },
142
+ { id: "project_count", label: "Project Memory", shortLabel: "MEM", description: "Memory entries for this project", zone: "center", icon: "", render: renderProjectCountSegment, defaultShow: true },
143
+ { id: "total_count", label: "Total Memory", shortLabel: "TOT", description: "Total memory entries across projects", zone: "center", icon: "", render: renderTotalCountSegment, defaultShow: true },
144
+ { id: "consolidations", label: "Consolidations", shortLabel: "CNS", description: "Number of memory consolidations", zone: "center", icon: "", render: renderConsolidationsSegment, defaultShow: false },
142
145
  ];