@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.
@@ -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
  /**
@@ -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("📈 CMP 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,7 +72,12 @@ 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("🗜️ CMP 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 };
@@ -125,11 +136,11 @@ function renderSearchQueriesSegment(_ctx: FooterSegmentContext): RenderedSegment
125
136
  }
126
137
 
127
138
  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 },
139
+ { id: "session_events", label: "Session Events", shortLabel: "EVT", description: "Number of session events", zone: "center", icon: "", render: renderSessionEventsSegment, defaultShow: true },
140
+ { id: "compactions", label: "Compactions", shortLabel: "CMP", description: "Number of context compactions", zone: "center", icon: "", render: renderCompactionsSegment, defaultShow: true },
141
+ { id: "tokens_saved", label: "Tokens Saved", shortLabel: "SVD", description: "Tokens saved by compaction", zone: "center", icon: "", render: renderTokensSavedSegment, defaultShow: true },
142
+ { id: "compression_ratio", label: "Compression Ratio", shortLabel: "RAT", description: "Last compaction compression ratio", zone: "center", icon: "", render: renderCompressionRatioSegment, defaultShow: false },
143
+ { id: "indexed_docs", label: "Indexed Docs", shortLabel: "IDX", description: "Number of indexed documents", zone: "center", icon: "", render: renderIndexedDocsSegment, defaultShow: false },
144
+ { id: "sandbox_runs", label: "Sandbox Runs", shortLabel: "SBX", description: "Number of sandbox code runs", zone: "center", icon: "", render: renderSandboxRunsSegment, defaultShow: false },
145
+ { id: "search_queries", label: "Search Queries", shortLabel: "QRY", description: "Number of search queries", zone: "center", icon: "", render: renderSearchQueriesSegment, defaultShow: false },
135
146
  ];
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * @pi-unipi/footer — Core segments
3
3
  *
4
- * Segment renderers for the core group: model, thinking, path, git,
4
+ * Segment renderers for the core group: model, api_state, tool_count, git,
5
5
  * context_pct, cost, tokens_total, tokens_in, tokens_out, session,
6
6
  * hostname, time.
7
7
  */
8
8
 
9
9
  import { hostname as osHostname } from "node:os";
10
- import { basename } from "node:path";
11
10
  import type { FooterSegment, FooterSegmentContext, RenderedSegment, SemanticColor } from "../types.js";
12
11
  import { applyColor } from "../rendering/theme.js";
13
12
  import { getIcon } from "../rendering/icons.js";
13
+ import { tpsTracker } from "../tps-tracker.js";
14
14
 
15
15
  // ─── Helpers ────────────────────────────────────────────────────────────────
16
16
 
@@ -61,9 +61,9 @@ function getUsageStats(piContext: unknown): UsageStats {
61
61
  return { input, output, cacheRead, cacheWrite, cost };
62
62
  }
63
63
 
64
- // ─── Rainbow helpers for xhigh thinking level ───────────────────────────────
64
+ // ─── Rainbow helpers (kept for potential future use) ─────────────────────────
65
65
 
66
- /** ANSI 256-color rainbow palette for xhigh thinking level */
66
+ /** ANSI 256-color rainbow palette */
67
67
  const RAINBOW_COLORS = [
68
68
  "\x1b[38;5;196m", // red
69
69
  "\x1b[38;5;202m", // orange
@@ -100,14 +100,6 @@ export function rainbowBorder(width: number): string {
100
100
  return result;
101
101
  }
102
102
 
103
- /** Get the current thinking level from piContext */
104
- export function getThinkingLevel(piContext: unknown): string {
105
- const piCtx = piContext as Record<string, unknown> | undefined;
106
- return typeof piCtx?.getThinkingLevel === "function"
107
- ? (piCtx as any).getThinkingLevel()
108
- : "off";
109
- }
110
-
111
103
  // ─── Segment Renderers ──────────────────────────────────────────────────────
112
104
 
113
105
  function renderModelSegment(ctx: FooterSegmentContext): RenderedSegment {
@@ -121,49 +113,17 @@ function renderModelSegment(ctx: FooterSegmentContext): RenderedSegment {
121
113
  return { content: color(ctx, "model", content), visible: true };
122
114
  }
123
115
 
124
- function renderThinkingSegment(ctx: FooterSegmentContext): RenderedSegment {
125
- const thinkingLevel = getThinkingLevel(ctx.piContext);
126
-
127
- if (thinkingLevel === "off") return { content: "", visible: false };
128
-
129
- const levelText: Record<string, string> = {
130
- minimal: "min", low: "low", medium: "med", high: "high", xhigh: "xhigh",
131
- };
132
- const label = levelText[thinkingLevel] || thinkingLevel;
133
- const icon = getIcon("thinking");
134
- const text = `think:${label}`;
135
- const content = icon ? `${icon} ${text}` : text;
136
-
137
- // xhigh uses rainbow coloring
138
- if (thinkingLevel === "xhigh") {
139
- return { content: rainbowText(content), visible: true };
140
- }
141
-
142
- let semanticColor: SemanticColor = "thinking";
143
- if (thinkingLevel === "minimal") semanticColor = "thinkingMinimal";
144
- else if (thinkingLevel === "low") semanticColor = "thinkingLow";
145
- else if (thinkingLevel === "medium") semanticColor = "thinkingMedium";
146
- else if (thinkingLevel === "high") semanticColor = "thinkingHigh";
147
-
148
- return { content: color(ctx, semanticColor, content), visible: true };
116
+ function renderApiStateSegment(ctx: FooterSegmentContext): RenderedSegment {
117
+ // Show WEB to indicate the web-api package is active.
118
+ const content = "WEB";
119
+ return { content: color(ctx, "model", content), visible: true };
149
120
  }
150
121
 
151
- function renderPathSegment(ctx: FooterSegmentContext): RenderedSegment {
152
- const piCtx = ctx.piContext as Record<string, unknown> | undefined;
153
- const cwd = (piCtx?.cwd as string) || process.cwd();
154
- const home = process.env.HOME || process.env.USERPROFILE;
155
- let pwd = cwd;
156
-
157
- if (home && pwd.startsWith(home)) {
158
- pwd = `~${pwd.slice(home.length)}`;
159
- }
160
- // For brevity, show basename by default
161
- if (pwd.length > 30) {
162
- pwd = `…${pwd.slice(-29)}`;
163
- }
164
-
165
- const content = withIcon("path", pwd);
166
- return { content: color(ctx, "path", content), visible: true };
122
+ function renderToolCountSegment(ctx: FooterSegmentContext): RenderedSegment {
123
+ // Tool count is not directly exposed in piContext yet.
124
+ // TODO: Connect to actual tool count when pi exposes it.
125
+ const content = withIcon("toolCount", "—");
126
+ return { content: color(ctx, "model", content), visible: true };
167
127
  }
168
128
 
169
129
  function renderGitSegment(ctx: FooterSegmentContext): RenderedSegment {
@@ -171,8 +131,10 @@ function renderGitSegment(ctx: FooterSegmentContext): RenderedSegment {
171
131
  const branch = footerData?.getGitBranch?.() ?? null;
172
132
  if (!branch) return { content: "", visible: false };
173
133
 
134
+ const isDirty = footerData?.getGitDirty?.() ?? false;
135
+ const semanticColor: SemanticColor = isDirty ? "gitDirty" : "gitClean";
174
136
  const content = withIcon("git", branch);
175
- return { content: color(ctx, "git", content), visible: true };
137
+ return { content: color(ctx, semanticColor, content), visible: true };
176
138
  }
177
139
 
178
140
  function renderContextPctSegment(ctx: FooterSegmentContext): RenderedSegment {
@@ -247,7 +209,7 @@ function renderSessionSegment(ctx: FooterSegmentContext): RenderedSegment {
247
209
  const sessionId = (piCtx?.sessionManager as any)?.getSessionId?.();
248
210
  const display = sessionId?.slice(0, 8) || "new";
249
211
  const content = withIcon("session", display);
250
- return { content: color(ctx, "model", content), visible: true };
212
+ return { content: color(ctx, "session", content), visible: true };
251
213
  }
252
214
 
253
215
  function renderHostnameSegment(_ctx: FooterSegmentContext): RenderedSegment {
@@ -265,19 +227,124 @@ function renderTimeSegment(ctx: FooterSegmentContext): RenderedSegment {
265
227
  return { content, visible: true };
266
228
  }
267
229
 
230
+ // ─── TPS tier color function ────────────────────────────────────────────────
231
+
232
+ function getTpsSemanticColor(tps: number): SemanticColor {
233
+ if (tps > 200) return "tpsBlazing";
234
+ if (tps > 100) return "tpsFast";
235
+ if (tps > 50) return "tpsGood";
236
+ if (tps > 30) return "tpsModerate";
237
+ return "tpsSlow";
238
+ }
239
+
240
+ function renderTpsSegment(ctx: FooterSegmentContext): RenderedSegment {
241
+ const streaming = tpsTracker.isStreaming();
242
+ const liveTps = tpsTracker.getLiveTps();
243
+ const avgTps = tpsTracker.getSessionAvgTps();
244
+
245
+ // No data yet — hide
246
+ if (!tpsTracker.getTotalOutput()) return { content: "", visible: false };
247
+
248
+ const icon = getIcon("tps");
249
+
250
+ if (streaming && liveTps > 0) {
251
+ // Active generation: show live rate + avg
252
+ const liveDisplay = Math.round(liveTps);
253
+ const avgDisplay = Math.round(avgTps);
254
+ const liveText = `\u2191 ${liveDisplay} T/S`;
255
+ const avgText = `AVG ${avgDisplay}`;
256
+ const liveColored = applyColor(getTpsSemanticColor(liveTps), liveText, ctx.theme, ctx.colors);
257
+ const avgColored = applyColor("tpsIdle", avgText, ctx.theme, ctx.colors);
258
+ const content = icon ? `${icon} ${liveColored} \u00b7 ${avgColored}` : `${liveColored} \u00b7 ${avgColored}`;
259
+ return { content, visible: true };
260
+ }
261
+
262
+ // Idle: show session average
263
+ const avgDisplay = Math.round(avgTps);
264
+ const avgText = `AVG ${avgDisplay} T/S`;
265
+ const avgColored = applyColor("tpsIdle", avgText, ctx.theme, ctx.colors);
266
+ const content = icon ? `${icon} ${avgColored}` : avgColored;
267
+ return { content, visible: true };
268
+ }
269
+
270
+ function renderClockSegment(ctx: FooterSegmentContext): RenderedSegment {
271
+ const now = new Date();
272
+ const h = now.getHours().toString().padStart(2, "0");
273
+ const m = now.getMinutes().toString().padStart(2, "0");
274
+ const s = now.getSeconds().toString().padStart(2, "0");
275
+ const timeStr = `${h}:${m}:${s}`;
276
+ const content = withIcon("clock", timeStr);
277
+ return { content: color(ctx, "clock", content), visible: true };
278
+ }
279
+
280
+ function renderDurationSegment(ctx: FooterSegmentContext): RenderedSegment {
281
+ // Derive session duration from sessionManager
282
+ const piCtx = ctx.piContext as Record<string, unknown> | undefined;
283
+ const sessionStart = (piCtx?.sessionManager as any)?.getSessionStartTime?.();
284
+ if (!sessionStart) {
285
+ // Fallback: show current time segment style
286
+ return { content: "", visible: false };
287
+ }
288
+
289
+ const elapsedMs = Date.now() - sessionStart;
290
+ const totalSeconds = Math.floor(elapsedMs / 1000);
291
+ const hours = Math.floor(totalSeconds / 3600);
292
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
293
+ const seconds = totalSeconds % 60;
294
+
295
+ let display: string;
296
+ if (hours > 0) {
297
+ display = `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
298
+ } else {
299
+ display = `${minutes}:${seconds.toString().padStart(2, "0")}`;
300
+ }
301
+
302
+ const content = withIcon("duration", display);
303
+ return { content: color(ctx, "duration", content), visible: true };
304
+ }
305
+
306
+ // ─── Thinking level ──────────────────────────────────────────────────────────
307
+
308
+ /** Map thinking level to semantic color */
309
+ function getThinkingSemanticColor(level: string | undefined): SemanticColor {
310
+ switch (level) {
311
+ case "minimal": return "thinkingMinimal";
312
+ case "low": return "thinkingLow";
313
+ case "medium": return "thinkingMedium";
314
+ case "high": return "thinkingHigh";
315
+ case "xhigh": return "thinkingXhigh";
316
+ default: return "thinking";
317
+ }
318
+ }
319
+
320
+ function renderThinkingLevelSegment(ctx: FooterSegmentContext): RenderedSegment {
321
+ const piCtx = ctx.piContext as Record<string, unknown> | undefined;
322
+ const model = piCtx?.model as Record<string, unknown> | undefined;
323
+ const thinkingLevel = model?.thinkingLevel as string | undefined;
324
+
325
+ if (!thinkingLevel || thinkingLevel === "off") return { content: "", visible: false };
326
+
327
+ const semanticColor = getThinkingSemanticColor(thinkingLevel);
328
+ const content = withIcon("thinkingLevel", thinkingLevel);
329
+ return { content: color(ctx, semanticColor, content), visible: true };
330
+ }
331
+
268
332
  // ─── Core segments array ────────────────────────────────────────────────────
269
333
 
270
334
  export const CORE_SEGMENTS: FooterSegment[] = [
271
- { id: "model", label: "Model", icon: "", render: renderModelSegment, defaultShow: true },
272
- { id: "thinking", label: "Thinking", icon: "", render: renderThinkingSegment, defaultShow: true },
273
- { id: "path", label: "Path", icon: "", render: renderPathSegment, defaultShow: true },
274
- { id: "git", label: "Git", icon: "", render: renderGitSegment, defaultShow: true },
275
- { id: "context_pct", label: "Context %", icon: "", render: renderContextPctSegment, defaultShow: true },
276
- { id: "cost", label: "Cost", icon: "", render: renderCostSegment, defaultShow: true },
277
- { id: "tokens_total", label: "Tokens Total", icon: "", render: renderTokensSegment("total"), defaultShow: false },
278
- { id: "tokens_in", label: "Tokens In", icon: "", render: renderTokensSegment("in"), defaultShow: false },
279
- { id: "tokens_out", label: "Tokens Out", icon: "", render: renderTokensSegment("out"), defaultShow: false },
280
- { id: "session", label: "Session", icon: "", render: renderSessionSegment, defaultShow: false },
281
- { id: "hostname", label: "Hostname", icon: "", render: renderHostnameSegment, defaultShow: false },
282
- { id: "time", label: "Time", icon: "", render: renderTimeSegment, defaultShow: false },
335
+ { id: "model", label: "Model", shortLabel: "MDL", description: "Current model name", zone: "left", icon: "", render: renderModelSegment, defaultShow: true },
336
+ { id: "api_state", label: "API", shortLabel: "API", description: "API connection state", zone: "left", icon: "", render: renderApiStateSegment, defaultShow: true },
337
+ { id: "tool_count", label: "Tool Count", shortLabel: "TLS", description: "Number of tools available", zone: "left", icon: "", render: renderToolCountSegment, defaultShow: true },
338
+ { id: "git", label: "Git", shortLabel: "GIT", description: "Current git branch + dirty/clean status", zone: "left", icon: "", render: renderGitSegment, defaultShow: true },
339
+ { id: "tps", label: "TPS", shortLabel: "TPS", description: "Tokens per second \u2014 live during generation", zone: "center", icon: "", render: renderTpsSegment, defaultShow: true },
340
+ { id: "context_pct", label: "Context %", shortLabel: "CTX", description: "Context window usage percentage", zone: "center", icon: "", render: renderContextPctSegment, defaultShow: true },
341
+ { id: "cost", label: "Cost", shortLabel: "CST", description: "Session cost in USD", zone: "center", icon: "", render: renderCostSegment, defaultShow: true },
342
+ { id: "tokens_total", label: "Tokens Total", shortLabel: "TOK", description: "Total tokens used this session", zone: "center", icon: "", render: renderTokensSegment("total"), defaultShow: false },
343
+ { id: "tokens_in", label: "Tokens In", shortLabel: "TIN", description: "Input tokens consumed", zone: "center", icon: "", render: renderTokensSegment("in"), defaultShow: false },
344
+ { id: "tokens_out", label: "Tokens Out", shortLabel: "TOUT", description: "Output tokens generated", zone: "center", icon: "", render: renderTokensSegment("out"), defaultShow: false },
345
+ { id: "session", label: "Session", shortLabel: "SES", description: "Session identifier", zone: "left", icon: "", render: renderSessionSegment, defaultShow: false },
346
+ { id: "hostname", label: "Hostname", shortLabel: "HST", description: "Machine hostname", zone: "left", icon: "", render: renderHostnameSegment, defaultShow: false },
347
+ { id: "clock", label: "Clock", shortLabel: "CLK", description: "Current wall time (HH:MM:SS)", zone: "right", icon: "", render: renderClockSegment, defaultShow: true },
348
+ { id: "duration", label: "Duration", shortLabel: "DUR", description: "Session duration", zone: "right", icon: "", render: renderDurationSegment, defaultShow: true },
349
+ { id: "thinking_level", label: "Thinking", shortLabel: "THK", description: "Current model thinking level", zone: "center", icon: "", render: renderThinkingLevelSegment, defaultShow: false },
283
350
  ];
@@ -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);
@@ -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("KB 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("KB 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("KB 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 {
@@ -64,21 +65,37 @@ function hasUsefulValue(value: unknown): value is number {
64
65
 
65
66
  function renderServersTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
66
67
  const stats = getMcpStats(ctx);
67
- if (!hasUsefulValue(stats.serversTotal)) return { content: "", visible: false };
68
+ if (!hasUsefulValue(stats.serversTotal)) {
69
+ if (isSegmentEnabled("mcp", "servers_total")) {
70
+ return { content: mutedPlaceholder("🖥️ MCP 0"), visible: true };
71
+ }
72
+ return { content: "", visible: false };
73
+ }
68
74
  const content = withIcon("serversTotal", `${stats.serversTotal}`);
69
75
  return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
70
76
  }
71
77
 
72
78
  function renderServersActiveSegment(ctx: FooterSegmentContext): RenderedSegment {
73
79
  const stats = getMcpStats(ctx);
74
- if (!hasUsefulValue(stats.serversActive)) return { content: "", visible: false };
80
+ if (!hasUsefulValue(stats.serversActive)) {
81
+ if (isSegmentEnabled("mcp", "servers_active")) {
82
+ const total = stats.serversTotal ?? 0;
83
+ return { content: mutedPlaceholder(`🖥️ MCP ${total}/0`), visible: true };
84
+ }
85
+ return { content: "", visible: false };
86
+ }
75
87
  const content = withIcon("serversActive", `${stats.serversActive}`);
76
88
  return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
77
89
  }
78
90
 
79
91
  function renderToolsTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
80
92
  const stats = getMcpStats(ctx);
81
- if (!hasUsefulValue(stats.toolsTotal)) return { content: "", visible: false };
93
+ if (!hasUsefulValue(stats.toolsTotal)) {
94
+ if (isSegmentEnabled("mcp", "tools_total")) {
95
+ return { content: mutedPlaceholder("🖥️ MCP 0"), visible: true };
96
+ }
97
+ return { content: "", visible: false };
98
+ }
82
99
  const content = withIcon("toolsTotal", `${stats.toolsTotal}`);
83
100
  return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
84
101
  }
@@ -93,8 +110,8 @@ function renderServersFailedSegment(ctx: FooterSegmentContext): RenderedSegment
93
110
  }
94
111
 
95
112
  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 },
113
+ { id: "servers_total", label: "Servers", shortLabel: "SRV", description: "Total MCP servers configured", zone: "center", icon: "", render: renderServersTotalSegment, defaultShow: true },
114
+ { id: "servers_active", label: "Active", shortLabel: "ACT", description: "Currently connected MCP servers", zone: "center", icon: "", render: renderServersActiveSegment, defaultShow: true },
115
+ { id: "tools_total", label: "Tools", shortLabel: "TLS", description: "Total MCP tools available", zone: "center", icon: "", render: renderToolsTotalSegment, defaultShow: true },
116
+ { id: "servers_failed", label: "Failed", shortLabel: "ERR", description: "Failed MCP server connections", zone: "center", icon: "", render: renderServersFailedSegment, defaultShow: true },
100
117
  ];
@@ -18,11 +18,14 @@
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
- /** Nerd Font icon for memory: */
25
- const MEMORY_ICON = "\uee9c";
25
+ function withIcon(segmentId: string, text: string): string {
26
+ const icon = getIcon(segmentId);
27
+ return icon ? `${icon} ${text}` : text;
28
+ }
26
29
 
27
30
  /**
28
31
  * Shape of the info-screen memory group data:
@@ -79,16 +82,19 @@ function getMemoryCounts(): { project: number | null; total: number | null } {
79
82
  function renderProjectCountSegment(ctx: FooterSegmentContext): RenderedSegment {
80
83
  const counts = getMemoryCounts();
81
84
  if (counts.project === null) {
85
+ if (isSegmentEnabled("memory", "project_count")) {
86
+ return { content: mutedPlaceholder("🧠 MEM 0"), visible: true };
87
+ }
82
88
  return { content: "", visible: false };
83
89
  }
84
90
 
85
91
  // Show as 76/102 format when both are available
86
92
  if (counts.total !== null) {
87
- const content = `${MEMORY_ICON} ${counts.project}/${counts.total}`;
93
+ const content = withIcon("projectCount", `${counts.project}/${counts.total}`);
88
94
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
89
95
  }
90
96
 
91
- const content = `${MEMORY_ICON} ${counts.project}`;
97
+ const content = withIcon("projectCount", `${counts.project}`);
92
98
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
93
99
  }
94
100
 
@@ -106,7 +112,7 @@ function renderTotalCountSegment(ctx: FooterSegmentContext): RenderedSegment {
106
112
  return { content: "", visible: false };
107
113
  }
108
114
 
109
- const content = `${MEMORY_ICON} ${counts.total}`;
115
+ const content = withIcon("totalCount", `${counts.total}`);
110
116
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
111
117
  }
112
118
 
@@ -116,7 +122,7 @@ function renderConsolidationsSegment(ctx: FooterSegmentContext): RenderedSegment
116
122
  // Check for explicit consolidations stat from info registry
117
123
  const consolidationsValue = infoData?.consolidations?.value;
118
124
  if (consolidationsValue !== undefined && consolidationsValue !== null) {
119
- const content = `${MEMORY_ICON} cns:${consolidationsValue}`;
125
+ const content = withIcon("consolidations", `cns:${consolidationsValue}`);
120
126
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
121
127
  }
122
128
 
@@ -129,12 +135,12 @@ function renderConsolidationsSegment(ctx: FooterSegmentContext): RenderedSegment
129
135
  return { content: "", visible: false };
130
136
  }
131
137
 
132
- const content = `${MEMORY_ICON} cns:${count}`;
138
+ const content = withIcon("consolidations", `cns:${count}`);
133
139
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
134
140
  }
135
141
 
136
142
  export const MEMORY_SEGMENTS: FooterSegment[] = [
137
- { id: "project_count", label: "Project Count", icon: "", render: renderProjectCountSegment, defaultShow: true },
138
- { id: "total_count", label: "Total Count", icon: "", render: renderTotalCountSegment, defaultShow: true },
139
- { id: "consolidations", label: "Consolidations", icon: "", render: renderConsolidationsSegment, defaultShow: false },
143
+ { id: "project_count", label: "Project Memory", shortLabel: "MEM", description: "Memory entries for this project", zone: "center", icon: "", render: renderProjectCountSegment, defaultShow: true },
144
+ { id: "total_count", label: "Total Memory", shortLabel: "TOT", description: "Total memory entries across projects", zone: "center", icon: "", render: renderTotalCountSegment, defaultShow: true },
145
+ { id: "consolidations", label: "Consolidations", shortLabel: "CNS", description: "Number of memory consolidations", zone: "center", icon: "", render: renderConsolidationsSegment, defaultShow: false },
140
146
  ];