@pi-unipi/footer 0.1.2 → 0.1.3

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.
@@ -16,8 +16,8 @@ import { detectNerdFontSupport } from "./separators.js";
16
16
  export interface IconSet {
17
17
  // Core segments
18
18
  model: string;
19
- thinking: string;
20
- path: string;
19
+ apiState: string;
20
+ toolCount: string;
21
21
  git: string;
22
22
  context: string;
23
23
  cost: string;
@@ -80,38 +80,38 @@ export interface IconSet {
80
80
  /** Nerd Font glyphs — requires a Nerd Font installed in the terminal */
81
81
  export const NERD_ICONS: IconSet = {
82
82
  // Core
83
- model: "\uEC19", // nf-md-chip
84
- thinking: "\uE22C", // nf-oct-pi
85
- path: "\uF115", // nf-fa-folder_open
86
- git: "\uF126", // nf-fa-code_fork
87
- context: "\uE70F", // nf-dev-database
88
- cost: "\uF155", // nf-fa-dollar
89
- tokens: "\uE26B", // nf-seti-html
90
- tokensIn: "\uF090", // nf-fa-sign_in
91
- tokensOut: "\uF08B", // nf-fa-sign_out
83
+ model: "\uDB81\uDE5B", // 󰚩 custom model icon
84
+ apiState: "\uF725", // 󱂛 api state icon
85
+ toolCount: "\uF0AD", // tool count icon
86
+ git: "\uF0E8", // git icon
87
+ context: "\uF8D8", // context icon
88
+ cost: "\uF155", // cost icon
89
+ tokens: "\uF07B", // tokens icon
90
+ tokensIn: "\uF07B", // tokens in icon
91
+ tokensOut: "\uF07B", // tokens out icon
92
92
  session: "\uF550", // nf-md-identifier
93
93
  hostname: "\uF109", // nf-fa-laptop
94
94
  time: "\uF017", // nf-fa-clock_o
95
95
 
96
96
  // Compactor
97
- sessionEvents: "\uF0C0", // nf-fa-users
98
- compactions: "\uF1C0", // nf-fa-database
99
- tokensSaved: "\uF155", // nf-fa-dollar
100
- compressionRatio:"\uE70F", // nf-dev-database
101
- indexedDocs: "\uF02D", // nf-fa-book
102
- sandboxRuns: "\uF121", // nf-fa-terminal
103
- searchQueries: "\uF002", // nf-fa-search
97
+ sessionEvents: "\uDBB1\uDECF", // 󰲏 session events icon
98
+ compactions: "\uDBB1\uDECF", // 󰲏 compactions icon
99
+ tokensSaved: "\uF155", // tokens saved icon
100
+ compressionRatio:"\uDBB1\uDECF", // 󰲏 compression ratio icon
101
+ indexedDocs: "\uDB81\uDE19", // 󰈙 indexed docs icon
102
+ sandboxRuns: "\uF121", // sandbox runs icon
103
+ searchQueries: "\uF002", // search queries icon
104
104
 
105
105
  // Memory
106
- projectCount: "\uee9c", // memory icon
107
- totalCount: "\uee9c", // memory icon
108
- consolidations: "\uee9c", // memory icon
106
+ projectCount: "\uDB81\uDED4", // 󰍚 memory icon
107
+ totalCount: "\uEB9C", // total count icon
108
+ consolidations: "\uDB81\uDED4", // 󰍚 consolidations icon
109
109
 
110
110
  // MCP
111
- serversTotal: "\uF233", // nf-fa-server
112
- serversActive: "\uF058", // nf-fa-check_circle
113
- toolsTotal: "\uF0AD", // nf-fa-wrench
114
- serversFailed: "\uF071", // nf-fa-warning
111
+ serversTotal: "\uF0F6", // servers total icon
112
+ serversActive: "\uF058", // servers active icon
113
+ toolsTotal: "\uF0AD", // tools total icon
114
+ serversFailed: "\uF467", // servers failed icon
115
115
 
116
116
  // Ralph
117
117
  activeLoops: "\udb81\udf09", // 󰼉 ralph loop icon
@@ -119,22 +119,22 @@ export const NERD_ICONS: IconSet = {
119
119
  loopStatus: "\udb81\udf09", // 󰼉 ralph loop icon
120
120
 
121
121
  // Workflow
122
- currentCommand: "\uf52e", // workflow icon
123
- sandboxLevel: "\uf023", // nf-fa-lock
124
- commandDuration: "\uf017", // nf-fa-clock_o
122
+ currentCommand: "\uF0E8", // current command icon
123
+ sandboxLevel: "\uDBB1\uDDFE", // 󰟾 sandbox level icon
124
+ commandDuration: "\uDBB9\uDEAB", // 󱎫 command duration icon
125
125
 
126
126
  // Kanboard
127
- docsCount: "\uF15C", // nf-fa-file_text
128
- tasksDone: "\uF058", // nf-fa-check_circle
129
- tasksTotal: "\uF0AE", // nf-fa-tasks
130
- taskPct: "\uF200", // nf-fa-pie_chart
127
+ docsCount: "\uDB81\uDE19", // 󰈙 docs count icon
128
+ tasksDone: "\uF0E8", // tasks done icon
129
+ tasksTotal: "\uF0E8", // tasks total icon
130
+ taskPct: "\uF0E8", // task pct icon
131
131
 
132
132
  // Notify
133
133
  platformsEnabled:"\uF0E0", // nf-fa-envelope
134
134
  lastSent: "\uF017", // nf-fa-clock_o
135
135
 
136
136
  // Extension status
137
- extensionStatuses:"\uF1E6", // nf-fa-plug
137
+ extensionStatuses:"\uDBB5\uDEAB", // 󱖫 extension statuses icon
138
138
 
139
139
  separator: "\uE0B1", // nf-pl-left_soft_divider
140
140
  };
@@ -144,12 +144,12 @@ export const NERD_ICONS: IconSet = {
144
144
  /** Unicode emoji / symbol icons — works on most modern terminals */
145
145
  export const EMOJI_ICONS: IconSet = {
146
146
  // Core
147
- model: "",
148
- thinking: "π",
149
- path: "",
147
+ model: "🤖",
148
+ apiState: "🔄",
149
+ toolCount: "🔧",
150
150
  git: "⎇",
151
- context: "",
152
- cost: "$",
151
+ context: "🗄️",
152
+ cost: "💲",
153
153
  tokens: "⊛",
154
154
  tokensIn: "→",
155
155
  tokensOut: "←",
@@ -160,31 +160,31 @@ export const EMOJI_ICONS: IconSet = {
160
160
  // Compactor
161
161
  sessionEvents: "⚡",
162
162
  compactions: "◧",
163
- tokensSaved: "$",
163
+ tokensSaved: "💲",
164
164
  compressionRatio:"⇄",
165
165
  indexedDocs: "☰",
166
166
  sandboxRuns: "▶",
167
167
  searchQueries: "⊗",
168
168
 
169
169
  // Memory
170
- projectCount: "\uee9c",
171
- totalCount: "\uee9c",
172
- consolidations: "\uee9c",
170
+ projectCount: "🧠",
171
+ totalCount: "🧠",
172
+ consolidations: "🧠",
173
173
 
174
174
  // MCP
175
- serversTotal: "srv",
175
+ serversTotal: "🖥️",
176
176
  serversActive: "●",
177
177
  toolsTotal: "🔧",
178
- serversFailed: "",
178
+ serversFailed: "⚠️",
179
179
 
180
180
  // Ralph
181
- activeLoops: "",
182
- totalIterations: "",
183
- loopStatus: "",
181
+ activeLoops: "🔁",
182
+ totalIterations: "🔁",
183
+ loopStatus: "🔁",
184
184
 
185
185
  // Workflow
186
- currentCommand: "",
187
- sandboxLevel: "",
186
+ currentCommand: "▶️",
187
+ sandboxLevel: "🔒",
188
188
  commandDuration: "⏱",
189
189
 
190
190
  // Kanboard
@@ -208,61 +208,61 @@ export const EMOJI_ICONS: IconSet = {
208
208
  /** Plain text labels — works everywhere, most compact */
209
209
  export const TEXT_ICONS: IconSet = {
210
210
  // Core
211
- model: "",
212
- thinking: "",
213
- path: "",
214
- git: "",
215
- context: "",
216
- cost: "",
217
- tokens: "",
218
- tokensIn: "",
219
- tokensOut: "",
220
- session: "",
221
- hostname: "",
222
- time: "",
211
+ model: "MDL",
212
+ apiState: "API",
213
+ toolCount: "TLS",
214
+ git: "GIT",
215
+ context: "CTX",
216
+ cost: "CST",
217
+ tokens: "TOK",
218
+ tokensIn: "TKI",
219
+ tokensOut: "TKO",
220
+ session: "SES",
221
+ hostname: "HST",
222
+ time: "TIM",
223
223
 
224
224
  // Compactor
225
- sessionEvents: "evt",
226
- compactions: "cmp",
227
- tokensSaved: "svd",
228
- compressionRatio:"rat",
229
- indexedDocs: "idx",
230
- sandboxRuns: "sbx",
231
- searchQueries: "qry",
225
+ sessionEvents: "EVT",
226
+ compactions: "CMP",
227
+ tokensSaved: "SVD",
228
+ compressionRatio:"RAT",
229
+ indexedDocs: "IDX",
230
+ sandboxRuns: "SBX",
231
+ searchQueries: "QRY",
232
232
 
233
233
  // Memory
234
- projectCount: "mem",
235
- totalCount: "mem",
236
- consolidations: "cns",
234
+ projectCount: "MEM",
235
+ totalCount: "MEM",
236
+ consolidations: "CNS",
237
237
 
238
238
  // MCP
239
- serversTotal: "srv",
240
- serversActive: "act",
241
- toolsTotal: "tls",
242
- serversFailed: "err",
239
+ serversTotal: "SRV",
240
+ serversActive: "ACT",
241
+ toolsTotal: "TLS",
242
+ serversFailed: "ERR",
243
243
 
244
244
  // Ralph
245
- activeLoops: "",
246
- totalIterations: "",
247
- loopStatus: "",
245
+ activeLoops: "LPS",
246
+ totalIterations: "ITR",
247
+ loopStatus: "STS",
248
248
 
249
249
  // Workflow
250
- currentCommand: "",
251
- sandboxLevel: "sbx",
252
- commandDuration: "dur",
250
+ currentCommand: "CMD",
251
+ sandboxLevel: "SBX",
252
+ commandDuration: "DUR",
253
253
 
254
254
  // Kanboard
255
- docsCount: "doc",
256
- tasksDone: "",
257
- tasksTotal: "tsk",
258
- taskPct: "pct",
255
+ docsCount: "DOC",
256
+ tasksDone: "DNE",
257
+ tasksTotal: "TSK",
258
+ taskPct: "PCT",
259
259
 
260
260
  // Notify
261
- platformsEnabled:"ntf",
262
- lastSent: "lst",
261
+ platformsEnabled:"NTF",
262
+ lastSent: "LST",
263
263
 
264
264
  // Extension status
265
- extensionStatuses:"ext",
265
+ extensionStatuses:"EXT",
266
266
 
267
267
  separator: "|",
268
268
  };
@@ -9,6 +9,7 @@
9
9
  import type { Theme } from "@mariozechner/pi-coding-agent";
10
10
  import type { PresetDef, FooterSegmentContext, FooterSegment, ColorScheme, RenderedSegment } from "../types.js";
11
11
  import type { FooterRegistry } from "../registry/index.js";
12
+ import { visibleWidth as piVisibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
12
13
  import { getSeparator, separatorVisibleWidth } from "./separators.js";
13
14
  import { getDefaultColors } from "./theme.js";
14
15
  import { setIconStyle } from "./icons.js";
@@ -29,10 +30,9 @@ interface RenderedSegmentWithWidth {
29
30
 
30
31
  // ─── ANSI helpers ───────────────────────────────────────────────────────────
31
32
 
32
- /** Strip ANSI escape codes and measure visible width */
33
+ /** ANSI-aware visible width using pi-tui */
33
34
  function visibleWidth(text: string): number {
34
- const stripped = text.replace(/\x1b\[[0-9;]*m/g, "");
35
- return stripped.length;
35
+ return piVisibleWidth(text);
36
36
  }
37
37
 
38
38
  const ANSI_RESET = "\x1b[0m";
@@ -265,7 +265,7 @@ export class FooterRenderer {
265
265
  /** Map a segment ID to its group ID */
266
266
  private getGroupForSegment(segId: string): string {
267
267
  // Core segments
268
- const coreIds = ["model", "thinking", "path", "git", "context_pct", "cost", "tokens_total", "tokens_in", "tokens_out", "session", "hostname", "time"];
268
+ const coreIds = ["model", "api_state", "tool_count", "git", "context_pct", "cost", "tokens_total", "tokens_in", "tokens_out", "session", "hostname", "time"];
269
269
  if (coreIds.includes(segId)) return "core";
270
270
 
271
271
  // Compactor segments
@@ -1,13 +1,12 @@
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";
@@ -61,9 +60,9 @@ function getUsageStats(piContext: unknown): UsageStats {
61
60
  return { input, output, cacheRead, cacheWrite, cost };
62
61
  }
63
62
 
64
- // ─── Rainbow helpers for xhigh thinking level ───────────────────────────────
63
+ // ─── Rainbow helpers (kept for potential future use) ─────────────────────────
65
64
 
66
- /** ANSI 256-color rainbow palette for xhigh thinking level */
65
+ /** ANSI 256-color rainbow palette */
67
66
  const RAINBOW_COLORS = [
68
67
  "\x1b[38;5;196m", // red
69
68
  "\x1b[38;5;202m", // orange
@@ -100,14 +99,6 @@ export function rainbowBorder(width: number): string {
100
99
  return result;
101
100
  }
102
101
 
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
102
  // ─── Segment Renderers ──────────────────────────────────────────────────────
112
103
 
113
104
  function renderModelSegment(ctx: FooterSegmentContext): RenderedSegment {
@@ -121,49 +112,17 @@ function renderModelSegment(ctx: FooterSegmentContext): RenderedSegment {
121
112
  return { content: color(ctx, "model", content), visible: true };
122
113
  }
123
114
 
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 };
115
+ function renderApiStateSegment(ctx: FooterSegmentContext): RenderedSegment {
116
+ // Show WEB to indicate the web-api package is active.
117
+ const content = "WEB";
118
+ return { content: color(ctx, "model", content), visible: true };
149
119
  }
150
120
 
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 };
121
+ function renderToolCountSegment(ctx: FooterSegmentContext): RenderedSegment {
122
+ // Tool count is not directly exposed in piContext yet.
123
+ // TODO: Connect to actual tool count when pi exposes it.
124
+ const content = withIcon("toolCount", "—");
125
+ return { content: color(ctx, "model", content), visible: true };
167
126
  }
168
127
 
169
128
  function renderGitSegment(ctx: FooterSegmentContext): RenderedSegment {
@@ -269,8 +228,8 @@ function renderTimeSegment(ctx: FooterSegmentContext): RenderedSegment {
269
228
 
270
229
  export const CORE_SEGMENTS: FooterSegment[] = [
271
230
  { 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 },
231
+ { id: "api_state", label: "WEB", icon: "", render: renderApiStateSegment, defaultShow: true },
232
+ { id: "tool_count", label: "Tool Count", icon: "", render: renderToolCountSegment, defaultShow: true },
274
233
  { id: "git", label: "Git", icon: "", render: renderGitSegment, defaultShow: true },
275
234
  { id: "context_pct", label: "Context %", icon: "", render: renderContextPctSegment, defaultShow: true },
276
235
  { id: "cost", label: "Cost", icon: "", render: renderCostSegment, defaultShow: true },
@@ -21,8 +21,10 @@ import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../ty
21
21
  import { applyColor } from "../rendering/theme.js";
22
22
  import { getIcon } from "../rendering/icons.js";
23
23
 
24
- /** Nerd Font icon for memory: */
25
- const MEMORY_ICON = "\uee9c";
24
+ function withIcon(segmentId: string, text: string): string {
25
+ const icon = getIcon(segmentId);
26
+ return icon ? `${icon} ${text}` : text;
27
+ }
26
28
 
27
29
  /**
28
30
  * Shape of the info-screen memory group data:
@@ -84,11 +86,11 @@ function renderProjectCountSegment(ctx: FooterSegmentContext): RenderedSegment {
84
86
 
85
87
  // Show as 76/102 format when both are available
86
88
  if (counts.total !== null) {
87
- const content = `${MEMORY_ICON} ${counts.project}/${counts.total}`;
89
+ const content = withIcon("projectCount", `${counts.project}/${counts.total}`);
88
90
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
89
91
  }
90
92
 
91
- const content = `${MEMORY_ICON} ${counts.project}`;
93
+ const content = withIcon("projectCount", `${counts.project}`);
92
94
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
93
95
  }
94
96
 
@@ -106,7 +108,7 @@ function renderTotalCountSegment(ctx: FooterSegmentContext): RenderedSegment {
106
108
  return { content: "", visible: false };
107
109
  }
108
110
 
109
- const content = `${MEMORY_ICON} ${counts.total}`;
111
+ const content = withIcon("totalCount", `${counts.total}`);
110
112
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
111
113
  }
112
114
 
@@ -116,7 +118,7 @@ function renderConsolidationsSegment(ctx: FooterSegmentContext): RenderedSegment
116
118
  // Check for explicit consolidations stat from info registry
117
119
  const consolidationsValue = infoData?.consolidations?.value;
118
120
  if (consolidationsValue !== undefined && consolidationsValue !== null) {
119
- const content = `${MEMORY_ICON} cns:${consolidationsValue}`;
121
+ const content = withIcon("consolidations", `cns:${consolidationsValue}`);
120
122
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
121
123
  }
122
124
 
@@ -129,7 +131,7 @@ function renderConsolidationsSegment(ctx: FooterSegmentContext): RenderedSegment
129
131
  return { content: "", visible: false };
130
132
  }
131
133
 
132
- const content = `${MEMORY_ICON} cns:${count}`;
134
+ const content = withIcon("consolidations", `cns:${count}`);
133
135
  return { content: applyColor("memory", content, ctx.theme, ctx.colors), visible: true };
134
136
  }
135
137
 
@@ -14,8 +14,7 @@ import type { FooterSegment, FooterSegmentContext, RenderedSegment, SemanticColo
14
14
  import { applyColor } from "../rendering/theme.js";
15
15
  import { getIcon } from "../rendering/icons.js";
16
16
 
17
- /** Nerd Font icon for ralph: 󰼉 */
18
- const RALPH_ICON = "\udb81\udf09";
17
+
19
18
 
20
19
  /** Green dot indicator (with explicit ANSI codes) */
21
20
  const GREEN_DOT = "\x1b[38;5;82m●\x1b[0m";
@@ -52,6 +51,8 @@ function renderActiveLoopsSegment(ctx: FooterSegmentContext): RenderedSegment {
52
51
 
53
52
  const dot = active ? GREEN_DOT : RED_DOT;
54
53
 
54
+ const ralphIcon = getIcon("activeLoops");
55
+
55
56
  if (active) {
56
57
  // Active: green dot + iteration stats
57
58
  const iterStr = iteration !== undefined
@@ -59,15 +60,11 @@ function renderActiveLoopsSegment(ctx: FooterSegmentContext): RenderedSegment {
59
60
  : "";
60
61
  const nameStr = name ? ` ${name}` : "";
61
62
  // Color the icon and text parts, keep dot's own color
62
- const iconAndText = `${RALPH_ICON} ${iterStr}${nameStr}`;
63
- const coloredText = colorText(ctx, "ralphOn", iconAndText);
64
- // Insert the dot after the icon
65
- const content = `${RALPH_ICON} ${dot} ${colorText(ctx, "ralphOn", `${iterStr}${nameStr}`)}`;
63
+ const content = `${ralphIcon} ${dot} ${colorText(ctx, "ralphOn", `${iterStr}${nameStr}`)}`;
66
64
  return { content, visible: true };
67
65
  } else {
68
66
  // Off/inactive: red dot
69
- const content = `${RALPH_ICON} ${dot}`;
70
- return { content: `${colorText(ctx, "ralphOff", RALPH_ICON)} ${dot}`, visible: true };
67
+ return { content: `${colorText(ctx, "ralphOff", ralphIcon)} ${dot}`, visible: true };
71
68
  }
72
69
  }
73
70
 
@@ -80,9 +77,10 @@ function renderTotalIterationsSegment(ctx: FooterSegmentContext): RenderedSegmen
80
77
  const maxIterations = data.maxIterations;
81
78
  const display = maxIterations ? `${iteration}/${maxIterations}` : `${iteration}`;
82
79
 
80
+ const ralphIcon = getIcon("activeLoops");
83
81
  const dot = active ? GREEN_DOT : RED_DOT;
84
82
  const semantic: SemanticColor = active ? "ralphOn" : "ralphOff";
85
- const content = `${colorText(ctx, semantic, RALPH_ICON)} ${dot} ${colorText(ctx, semantic, display)}`;
83
+ const content = `${colorText(ctx, semantic, ralphIcon)} ${dot} ${colorText(ctx, semantic, display)}`;
86
84
  return { content, visible: true };
87
85
  }
88
86
 
@@ -92,13 +90,14 @@ function renderLoopStatusSegment(ctx: FooterSegmentContext): RenderedSegment {
92
90
  const name = data.name as string | undefined;
93
91
  if (!status && !name) return { content: "", visible: false };
94
92
 
93
+ const ralphIcon = getIcon("activeLoops");
95
94
  const dot = status === "active" ? GREEN_DOT : status === "completed" ? GREEN_DOT : RED_DOT;
96
95
  const statusIcon = status === "active" ? "▶" : status === "paused" ? "⏸" : status === "completed" ? "✓" : "";
97
96
  const display = name ? `${statusIcon} ${name}` : `${statusIcon}`;
98
97
 
99
98
  const active = status === "active" || status === "completed";
100
99
  const semantic: SemanticColor = active ? "ralphOn" : "ralphOff";
101
- const content = `${colorText(ctx, semantic, RALPH_ICON)} ${dot} ${colorText(ctx, semantic, display)}`;
100
+ const content = `${colorText(ctx, semantic, ralphIcon)} ${dot} ${colorText(ctx, semantic, display)}`;
102
101
  return { content, visible: true };
103
102
  }
104
103
 
@@ -7,7 +7,7 @@
7
7
  * Status keys from packages:
8
8
  * "unipi-workflow" → "⚡ wf:brainstorm ✓ rl" (active command shown)
9
9
  * "ralph" → "rl:loop-name 3/50"
10
- * "unipi-memory" → "⚡ mem 75p/101all"
10
+ * "unipi-memory" → "⚡ MEM 75p/101all"
11
11
  * "subagents" → various
12
12
  */
13
13
 
@@ -15,20 +15,20 @@ import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../ty
15
15
  import { getIcon } from "../rendering/icons.js";
16
16
  import { loadFooterSettings } from "../config.js";
17
17
  import { getSeparator } from "../rendering/separators.js";
18
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
18
19
 
19
20
  /** Map status keys to short display names and segment IDs for icons */
20
21
  const STATUS_DISPLAY: Record<string, { short: string; segmentId: string }> = {
21
- "unipi-workflow": { short: "wf", segmentId: "currentCommand" },
22
- workflow: { short: "wf", segmentId: "currentCommand" },
23
- ralph: { short: "rl", segmentId: "activeLoops" },
24
- "unipi-memory": { short: "mem", segmentId: "projectCount" },
25
- memory: { short: "mem", segmentId: "projectCount" },
26
- compactor: { short: "cmp", segmentId: "compactions" },
27
- mcp: { short: "mcp", segmentId: "serversTotal" },
28
- notify: { short: "ntf", segmentId: "platformsEnabled" },
29
- kanboard: { short: "kb", segmentId: "docsCount" },
30
- info: { short: "info", segmentId: "extensionStatuses" },
31
- subagents: { short: "sa", segmentId: "extensionStatuses" },
22
+ "unipi-workflow": { short: "WF", segmentId: "currentCommand" },
23
+ workflow: { short: "WF", segmentId: "currentCommand" },
24
+ ralph: { short: "RL", segmentId: "activeLoops" },
25
+ memory: { short: "MEM", segmentId: "projectCount" },
26
+ compactor: { short: "CMP", segmentId: "compactions" },
27
+ mcp: { short: "MCP", segmentId: "serversTotal" },
28
+ notify: { short: "NTF", segmentId: "platformsEnabled" },
29
+ kanboard: { short: "KB", segmentId: "docsCount" },
30
+ info: { short: "INF", segmentId: "extensionStatuses" },
31
+ subagents: { short: "SA", segmentId: "extensionStatuses" },
32
32
  };
33
33
 
34
34
  /** Get the separator character for the current settings */
@@ -110,7 +110,12 @@ function renderExtensionStatusesSegment(ctx: FooterSegmentContext): RenderedSegm
110
110
 
111
111
  if (parts.length === 0) return { content: "", visible: false };
112
112
 
113
+ // Clamp total content to terminal width to prevent TUI crash
113
114
  const content = parts.join(` ${sep} `);
115
+ const maxW = ctx.width > 0 ? ctx.width : 120;
116
+ if (visibleWidth(content) > maxW) {
117
+ return { content: truncateToWidth(content, maxW, "…"), visible: true };
118
+ }
114
119
  return { content, visible: true };
115
120
  }
116
121
 
@@ -10,8 +10,7 @@ import type { FooterSegment, FooterSegmentContext, RenderedSegment, SemanticColo
10
10
  import { applyColor } from "../rendering/theme.js";
11
11
  import { getIcon } from "../rendering/icons.js";
12
12
 
13
- /** Nerd Font icon for workflow: */
14
- const WORKFLOW_ICON = "\uf52e";
13
+
15
14
 
16
15
  function withIcon(segmentId: string, text: string): string {
17
16
  const icon = getIcon(segmentId);
@@ -46,15 +45,17 @@ function renderCurrentCommandSegment(ctx: FooterSegmentContext): RenderedSegment
46
45
  const active = data.active === true;
47
46
  const command = data.command as string | undefined;
48
47
 
48
+ const workflowIcon = getIcon("currentCommand");
49
+
49
50
  // No workflow — show dash
50
51
  if (!command) {
51
- const content = `${WORKFLOW_ICON} -`;
52
+ const content = withIcon("currentCommand", "-");
52
53
  return { content: applyColor("workflow", content, ctx.theme, ctx.colors), visible: true };
53
54
  }
54
55
 
55
56
  const statusPrefix = active ? "▶" : "✓";
56
57
  const semanticColor = getWorkflowSemanticColor(command);
57
- const content = `${WORKFLOW_ICON} ${statusPrefix} ${command}`;
58
+ const content = `${workflowIcon} ${statusPrefix} ${command}`;
58
59
  return { content: applyColor(semanticColor, content, ctx.theme, ctx.colors), visible: true };
59
60
  }
60
61