@pi-unipi/footer 2.6.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.6.1",
4
- "description": "Persistent status bar for Unipi subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
3
+ "version": "2.9.0",
4
+ "description": "Persistent status bar for Unipi \u2014 subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "license": "MIT",
@@ -32,18 +32,20 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.6.1"
35
+ "@pi-unipi/core": "2.9.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@earendil-works/pi-coding-agent": "^0.80.0",
39
- "@earendil-works/pi-tui": "^0.80.0"
38
+ "@earendil-works/pi-coding-agent": "^0.84.0",
39
+ "@earendil-works/pi-tui": "^0.84.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^25.6.0",
43
43
  "typescript": "^6.0.0"
44
44
  },
45
45
  "pi": {
46
- "extensions": [],
46
+ "extensions": [
47
+ "./index.ts"
48
+ ],
47
49
  "skills": [],
48
50
  "prompts": [],
49
51
  "themes": []
package/src/help.ts CHANGED
@@ -72,10 +72,10 @@ function buildHelpLines(
72
72
  }
73
73
 
74
74
  /** Simple group lookup for help */
75
- function getGroupForSegment(segId: string): string {
75
+ export function getGroupForSegment(segId: string): string {
76
76
  const coreIds = ["model", "api_state", "tool_count", "git", "context_pct", "cost", "tokens_total", "tokens_in", "tokens_out", "session", "hostname", "time", "tps", "clock", "duration", "thinking_level"];
77
77
  if (coreIds.includes(segId)) return "core";
78
- const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "cocoindex_status", "sandbox_runs", "search_queries"];
78
+ const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "sandbox_runs", "search_queries"];
79
79
  if (compactorIds.includes(segId)) return "compactor";
80
80
  if (["project_count", "total_count", "consolidations"].includes(segId)) return "memory";
81
81
  if (["servers_total", "servers_active", "tools_total", "servers_failed"].includes(segId)) return "mcp";
package/src/index.ts CHANGED
@@ -27,20 +27,19 @@ import { NOTIFY_SEGMENTS } from "./segments/notify.js";
27
27
  import { STATUS_EXT_SEGMENTS } from "./segments/status-ext.js";
28
28
 
29
29
  import type { FooterGroup, FooterSegment } from "./types.js";
30
- import { rainbowBorder } from "./segments/core.js";
31
30
  import { tpsTracker } from "./tps-tracker.js";
32
31
 
33
32
  /** All segment groups */
34
33
  const ALL_GROUPS: FooterGroup[] = [
35
- { id: "core", name: "Core", icon: "", segments: CORE_SEGMENTS, defaultShow: true },
36
- { id: "compactor", name: "Compactor", icon: "", segments: COMPACTOR_SEGMENTS, defaultShow: true },
37
- { id: "memory", name: "Memory", icon: "", segments: MEMORY_SEGMENTS, defaultShow: true },
38
- { id: "mcp", name: "MCP", icon: "", segments: MCP_SEGMENTS, defaultShow: true },
39
- { id: "ralph", name: "Ralph", icon: "", segments: RALPH_SEGMENTS, defaultShow: true },
40
- { id: "workflow", name: "Workflow", icon: "", segments: WORKFLOW_SEGMENTS, defaultShow: true },
41
- { id: "kanboard", name: "Kanboard", icon: "", segments: KANBOARD_SEGMENTS, defaultShow: true },
42
- { id: "notify", name: "Notify", icon: "", segments: NOTIFY_SEGMENTS, defaultShow: false },
43
- { id: "status_ext", name: "Extensions", icon: "", segments: STATUS_EXT_SEGMENTS, defaultShow: true },
34
+ { id: "core", name: "Core", segments: CORE_SEGMENTS, defaultShow: true },
35
+ { id: "compactor", name: "Compactor", segments: COMPACTOR_SEGMENTS, defaultShow: true },
36
+ { id: "memory", name: "Memory", segments: MEMORY_SEGMENTS, defaultShow: true },
37
+ { id: "mcp", name: "MCP", segments: MCP_SEGMENTS, defaultShow: true },
38
+ { id: "ralph", name: "Ralph", segments: RALPH_SEGMENTS, defaultShow: true },
39
+ { id: "workflow", name: "Workflow", segments: WORKFLOW_SEGMENTS, defaultShow: true },
40
+ { id: "kanboard", name: "Kanboard", segments: KANBOARD_SEGMENTS, defaultShow: true },
41
+ { id: "notify", name: "Notify", segments: NOTIFY_SEGMENTS, defaultShow: false },
42
+ { id: "status_ext", name: "Extensions", segments: STATUS_EXT_SEGMENTS, defaultShow: true },
44
43
  ];
45
44
 
46
45
  /** Build a segment lookup from all groups */
package/src/presets.ts CHANGED
@@ -73,32 +73,7 @@ const FULL_PRESET: PresetDef = {
73
73
  secondarySegments: [
74
74
  "hostname",
75
75
  "tokens_in", "tokens_out",
76
- "compression_ratio", "cocoindex_status",
77
- "platforms_enabled", "last_sent",
78
- "thinking_level",
79
- ],
80
- colors: getDefaultColors(),
81
- };
82
-
83
- /** Nerd preset — maximum detail for Nerd Font users */
84
- const NERD_PRESET: PresetDef = {
85
- leftSegments: [
86
- "model", "api_state", "tool_count", "git", "current_command", "session",
87
- ],
88
- rightSegments: [
89
- "tps", "context_pct", "cost", "tokens_total",
90
- "session_events", "compactions", "tokens_saved",
91
- "project_count", "total_count",
92
- "servers_total", "servers_active", "tools_total",
93
- "active_loops", "loop_status",
94
- "docs_count", "tasks_done", "task_pct",
95
- "extension_statuses",
96
- "clock", "duration",
97
- ],
98
- secondarySegments: [
99
- "hostname",
100
- "tokens_in", "tokens_out",
101
- "compression_ratio", "cocoindex_status",
76
+ "compression_ratio",
102
77
  "platforms_enabled", "last_sent",
103
78
  "thinking_level",
104
79
  ],
@@ -124,7 +99,6 @@ export const PRESETS: Record<string, PresetDef> = {
124
99
  minimal: MINIMAL_PRESET,
125
100
  compact: COMPACT_PRESET,
126
101
  full: FULL_PRESET,
127
- nerd: NERD_PRESET,
128
102
  ascii: ASCII_PRESET,
129
103
  };
130
104
 
@@ -87,15 +87,6 @@ export class FooterRegistry {
87
87
  this.notifySubscribers();
88
88
  }
89
89
 
90
- /**
91
- * Clear cached data for a specific group.
92
- */
93
- invalidateGroup(groupId: string): void {
94
- this.dataCache.delete(groupId);
95
- this.log("invalidateGroup", groupId);
96
- this.notifySubscribers();
97
- }
98
-
99
90
  // ─── Reactive Subscriptions ───────────────────────────────────────────────
100
91
 
101
92
  /**
@@ -144,17 +135,3 @@ export function getFooterRegistry(): FooterRegistry {
144
135
  }
145
136
  return registryInstance;
146
137
  }
147
-
148
- /**
149
- * Reset the global registry (for testing).
150
- */
151
- export function resetFooterRegistry(): void {
152
- registryInstance = null;
153
- }
154
-
155
- // ─── Global reference ───────────────────────────────────────────────────────
156
-
157
- // Expose on globalThis for cross-package access
158
- if (typeof globalThis !== "undefined") {
159
- globalThis.__unipi_footer_registry = getFooterRegistry();
160
- }
@@ -321,16 +321,3 @@ export function getIcon(segmentId: string, overrideStyle?: "nerd" | "emoji" | "t
321
321
  const key = segmentId as keyof IconSet;
322
322
  return icons[key] ?? "";
323
323
  }
324
-
325
- /**
326
- * Get the full icon set based on the configured style.
327
- */
328
- export function getIcons(iconStyle?: "nerd" | "emoji" | "text"): IconSet {
329
- const style = iconStyle ?? currentIconStyle ?? resolveIconStyle();
330
- const sets: Record<string, IconSet> = {
331
- nerd: NERD_ICONS,
332
- emoji: EMOJI_ICONS,
333
- text: TEXT_ICONS,
334
- };
335
- return sets[style];
336
- }
@@ -10,7 +10,8 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
10
10
  import type { PresetDef, FooterSegmentContext, FooterSegment, ColorScheme, RenderedSegment, SegmentZone } from "../types.js";
11
11
  import type { FooterRegistry } from "../registry/index.js";
12
12
  import { visibleWidth as piVisibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
13
- import { getSeparator, separatorVisibleWidth } from "./separators.js";
13
+ import { getSeparator } from "./separators.js";
14
+ import { getGroupForSegment } from "../help.js";
14
15
  import { getDefaultColors, setColorMode, refreshColorMode } from "./theme.js";
15
16
  import { setIconStyle } from "./icons.js";
16
17
  import { getPreset } from "../presets.js";
@@ -27,7 +28,6 @@ interface SegmentLookup {
27
28
  interface RenderedSegmentWithWidth {
28
29
  content: string;
29
30
  width: number;
30
- visible: boolean;
31
31
  }
32
32
 
33
33
  // ─── ANSI helpers ───────────────────────────────────────────────────────────
@@ -183,7 +183,7 @@ export class FooterRenderer {
183
183
  if (this.segmentLookup.allIds) {
184
184
  for (const segId of this.segmentLookup.allIds()) {
185
185
  if (primaryIds.includes(segId) || secondaryIds.includes(segId)) continue;
186
- const groupId = this.getGroupForSegment(segId);
186
+ const groupId = getGroupForSegment(segId);
187
187
  if (isSegmentExplicitlyEnabled(groupId, segId)) {
188
188
  primaryIds.push(segId);
189
189
  }
@@ -309,7 +309,7 @@ export class FooterRenderer {
309
309
  fullWidth: number,
310
310
  labelMode: "compact" | "labeled",
311
311
  ): RenderedSegmentWithWidth | null {
312
- if (!isSegmentEnabled(this.getGroupForSegment(segId), segId)) return null;
312
+ if (!isSegmentEnabled(getGroupForSegment(segId), segId)) return null;
313
313
 
314
314
  const segment = this.segmentLookup.get(segId);
315
315
  if (!segment) return null;
@@ -317,7 +317,7 @@ export class FooterRenderer {
317
317
  const ctx: FooterSegmentContext = {
318
318
  theme: this.getThemeLike(),
319
319
  colors,
320
- data: this.registry.getGroupData(this.getGroupForSegment(segId)),
320
+ data: this.registry.getGroupData(getGroupForSegment(segId)),
321
321
  width: fullWidth,
322
322
  piContext: this.piContext,
323
323
  footerData: this.footerData,
@@ -330,7 +330,6 @@ export class FooterRenderer {
330
330
  return {
331
331
  content: rendered.content,
332
332
  width: visibleWidth(rendered.content),
333
- visible: true,
334
333
  };
335
334
  }
336
335
 
@@ -441,40 +440,6 @@ export class FooterRenderer {
441
440
  return truncateToWidth(result, 200);
442
441
  }
443
442
 
444
- /** Map a segment ID to its group ID */
445
- private getGroupForSegment(segId: string): string {
446
- // Core segments
447
- const coreIds = ["model", "api_state", "tool_count", "git", "context_pct", "cost", "tokens_total", "tokens_in", "tokens_out", "session", "hostname", "time", "tps", "clock", "duration", "thinking_level"];
448
- if (coreIds.includes(segId)) return "core";
449
-
450
- // Compactor segments
451
- const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "cocoindex_status", "sandbox_runs", "search_queries"];
452
- if (compactorIds.includes(segId)) return "compactor";
453
-
454
- // Memory segments
455
- if (["project_count", "total_count", "consolidations"].includes(segId)) return "memory";
456
-
457
- // MCP segments
458
- if (["servers_total", "servers_active", "tools_total", "servers_failed"].includes(segId)) return "mcp";
459
-
460
- // Ralph segments
461
- if (["active_loops", "total_iterations", "loop_status"].includes(segId)) return "ralph";
462
-
463
- // Workflow segments
464
- if (["current_command", "sandbox_level", "command_duration"].includes(segId)) return "workflow";
465
-
466
- // Kanboard segments
467
- if (["docs_count", "tasks_done", "tasks_total", "task_pct"].includes(segId)) return "kanboard";
468
-
469
- // Notify segments
470
- if (["platforms_enabled", "last_sent"].includes(segId)) return "notify";
471
-
472
- // Status extension
473
- if (segId === "extension_statuses") return "status_ext";
474
-
475
- return "core";
476
- }
477
-
478
443
  /** Get a ThemeLike object for rendering context */
479
444
  private getThemeLike(): { fg: (color: string, text: string) => string } {
480
445
  // Use a minimal theme-like that applies ANSI codes based on color names
@@ -100,13 +100,3 @@ export function getSeparator(style: SeparatorStyle): SeparatorDef {
100
100
  return getSeparator("powerline-thin");
101
101
  }
102
102
  }
103
-
104
- /**
105
- * Get the visible width of a separator (ANSI-stripped length).
106
- */
107
- export function separatorVisibleWidth(style: SeparatorStyle): number {
108
- const sep = getSeparator(style);
109
- // Strip ANSI codes and measure
110
- const stripped = sep.left.replace(/\x1b\[[0-9;]*m/g, "");
111
- return stripped.length;
112
- }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
15
- import type { ColorScheme, ColorValue, SemanticColor, ThemeLike } from "../types.js";
15
+ import type { ColorScheme, SemanticColor, ThemeLike } from "../types.js";
16
16
 
17
17
  // ─── Color mode detection ──────────────────────────────────────────────────
18
18
 
@@ -244,20 +244,6 @@ export function getDefaultColors(): ColorScheme {
244
244
  return scheme;
245
245
  }
246
246
 
247
- /**
248
- * Resolve a ColorValue to an actual color string using the theme.
249
- * If the value is a theme color name, uses theme.fg().
250
- * If it's a hex string, returns it directly.
251
- */
252
- export function resolveColor(color: ColorValue, theme: ThemeLike): string {
253
- // Check if it's a hex color (starts with #)
254
- if (color.startsWith("#")) {
255
- return color;
256
- }
257
- // It's a ThemeColor — use theme.fg
258
- return theme.fg(color as ThemeColor, "").replace(/\x1b\[0m$/, "");
259
- }
260
-
261
247
  /**
262
248
  * Wrap `text` in an ANSI escape for the given hex value, chosen to match
263
249
  * the active colour mode (truecolor / 256 / none).
@@ -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, cocoindex_status, sandbox_runs, search_queries.
5
+ * tokens_saved, compression_ratio, 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)
@@ -129,12 +129,6 @@ function renderCompressionRatioSegment(ctx: FooterSegmentContext): RenderedSegme
129
129
  return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
130
130
  }
131
131
 
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.
135
- return hidden();
136
- }
137
-
138
132
  function renderSandboxRunsSegment(ctx: FooterSegmentContext): RenderedSegment {
139
133
  // Count sandbox events from session manager branch
140
134
  const events = getSessionEvents(ctx);
@@ -171,11 +165,10 @@ function renderSearchQueriesSegment(ctx: FooterSegmentContext): RenderedSegment
171
165
  }
172
166
 
173
167
  export const COMPACTOR_SEGMENTS: FooterSegment[] = [
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 },
168
+ { id: "session_events", label: "Session Events", shortLabel: "EVT", description: "Number of session events", zone: "center", render: renderSessionEventsSegment, defaultShow: true },
169
+ { id: "compactions", label: "Compactions", shortLabel: "CMP", description: "Number of context compactions", zone: "center", render: renderCompactionsSegment, defaultShow: true },
170
+ { id: "tokens_saved", label: "Tokens Saved", shortLabel: "SVD", description: "Tokens saved by compaction", zone: "center", render: renderTokensSavedSegment, defaultShow: true },
171
+ { id: "compression_ratio", label: "Compression Ratio", shortLabel: "RAT", description: "Last compaction compression ratio", zone: "center", render: renderCompressionRatioSegment, defaultShow: false },
172
+ { id: "sandbox_runs", label: "Sandbox Runs", shortLabel: "SBX", description: "Number of sandbox code runs", zone: "center", render: renderSandboxRunsSegment, defaultShow: false },
173
+ { id: "search_queries", label: "Search Queries", shortLabel: "QRY", description: "Number of search queries", zone: "center", render: renderSearchQueriesSegment, defaultShow: false },
181
174
  ];
@@ -61,45 +61,6 @@ function getUsageStats(piContext: unknown): UsageStats {
61
61
  return { input, output, cacheRead, cacheWrite, cost };
62
62
  }
63
63
 
64
- // ─── Rainbow helpers (kept for potential future use) ─────────────────────────
65
-
66
- /** ANSI 256-color rainbow palette */
67
- const RAINBOW_COLORS = [
68
- "\x1b[38;5;196m", // red
69
- "\x1b[38;5;202m", // orange
70
- "\x1b[38;5;226m", // yellow
71
- "\x1b[38;5;82m", // green
72
- "\x1b[38;5;45m", // blue
73
- "\x1b[38;5;129m", // indigo
74
- "\x1b[38;5;171m", // violet
75
- ];
76
-
77
- const ANSI_RESET = "\x1b[0m";
78
-
79
- /** Apply rainbow coloring to text, cycling through colors per character */
80
- export function rainbowText(text: string): string {
81
- let result = "";
82
- let colorIdx = 0;
83
- for (const char of text) {
84
- if (char === " ") {
85
- result += char;
86
- } else {
87
- result += `${RAINBOW_COLORS[colorIdx % RAINBOW_COLORS.length]}${char}${ANSI_RESET}`;
88
- colorIdx++;
89
- }
90
- }
91
- return result;
92
- }
93
-
94
- /** Render a rainbow border line of the given width */
95
- export function rainbowBorder(width: number): string {
96
- let result = "";
97
- for (let i = 0; i < width; i++) {
98
- result += `${RAINBOW_COLORS[i % RAINBOW_COLORS.length]}─${ANSI_RESET}`;
99
- }
100
- return result;
101
- }
102
-
103
64
  // ─── Segment Renderers ──────────────────────────────────────────────────────
104
65
 
105
66
  function renderModelSegment(ctx: FooterSegmentContext): RenderedSegment {
@@ -324,19 +285,19 @@ function renderThinkingLevelSegment(ctx: FooterSegmentContext): RenderedSegment
324
285
  // ─── Core segments array ────────────────────────────────────────────────────
325
286
 
326
287
  export const CORE_SEGMENTS: FooterSegment[] = [
327
- { id: "model", label: "Model", shortLabel: "MDL", description: "Current model name", zone: "left", icon: "", render: renderModelSegment, defaultShow: true },
328
- { id: "api_state", label: "API", shortLabel: "API", description: "API connection state", zone: "left", icon: "", render: renderApiStateSegment, defaultShow: true },
329
- { id: "tool_count", label: "Tool Count", shortLabel: "TLS", description: "Number of tools available", zone: "left", icon: "", render: renderToolCountSegment, defaultShow: true },
330
- { id: "git", label: "Git", shortLabel: "GIT", description: "Current git branch + dirty/clean status", zone: "left", icon: "", render: renderGitSegment, defaultShow: true },
331
- { id: "tps", label: "TPS", shortLabel: "TPS", description: "Tokens per second \u2014 live during generation", zone: "center", icon: "", render: renderTpsSegment, defaultShow: true },
332
- { id: "context_pct", label: "Context %", shortLabel: "CTX", description: "Context window usage percentage", zone: "center", icon: "", render: renderContextPctSegment, defaultShow: true },
333
- { id: "cost", label: "Cost", shortLabel: "CST", description: "Session cost in USD", zone: "center", icon: "", render: renderCostSegment, defaultShow: true },
334
- { id: "tokens_total", label: "Tokens Total", shortLabel: "TOK", description: "Total tokens used this session", zone: "center", icon: "", render: renderTokensSegment("total"), defaultShow: false },
335
- { id: "tokens_in", label: "Tokens In", shortLabel: "TIN", description: "Input tokens consumed", zone: "center", icon: "", render: renderTokensSegment("in"), defaultShow: false },
336
- { id: "tokens_out", label: "Tokens Out", shortLabel: "TOUT", description: "Output tokens generated", zone: "center", icon: "", render: renderTokensSegment("out"), defaultShow: false },
337
- { id: "session", label: "Session", shortLabel: "SES", description: "Session identifier", zone: "left", icon: "", render: renderSessionSegment, defaultShow: false },
338
- { id: "hostname", label: "Hostname", shortLabel: "HST", description: "Machine hostname", zone: "left", icon: "", render: renderHostnameSegment, defaultShow: false },
339
- { id: "clock", label: "Clock", shortLabel: "CLK", description: "Current wall time (HH:MM:SS)", zone: "right", icon: "", render: renderClockSegment, defaultShow: true },
340
- { id: "duration", label: "Duration", shortLabel: "DUR", description: "Session duration", zone: "right", icon: "", render: renderDurationSegment, defaultShow: true },
341
- { id: "thinking_level", label: "Thinking", shortLabel: "THK", description: "Current model thinking level", zone: "center", icon: "", render: renderThinkingLevelSegment, defaultShow: false },
288
+ { id: "model", label: "Model", shortLabel: "MDL", description: "Current model name", zone: "left", render: renderModelSegment, defaultShow: true },
289
+ { id: "api_state", label: "API", shortLabel: "API", description: "API connection state", zone: "left", render: renderApiStateSegment, defaultShow: true },
290
+ { id: "tool_count", label: "Tool Count", shortLabel: "TLS", description: "Number of tools available", zone: "left", render: renderToolCountSegment, defaultShow: true },
291
+ { id: "git", label: "Git", shortLabel: "GIT", description: "Current git branch + dirty/clean status", zone: "left", render: renderGitSegment, defaultShow: true },
292
+ { id: "tps", label: "TPS", shortLabel: "TPS", description: "Tokens per second \u2014 live during generation", zone: "center", render: renderTpsSegment, defaultShow: true },
293
+ { id: "context_pct", label: "Context %", shortLabel: "CTX", description: "Context window usage percentage", zone: "center", render: renderContextPctSegment, defaultShow: true },
294
+ { id: "cost", label: "Cost", shortLabel: "CST", description: "Session cost in USD", zone: "center", render: renderCostSegment, defaultShow: true },
295
+ { id: "tokens_total", label: "Tokens Total", shortLabel: "TOK", description: "Total tokens used this session", zone: "center", render: renderTokensSegment("total"), defaultShow: false },
296
+ { id: "tokens_in", label: "Tokens In", shortLabel: "TIN", description: "Input tokens consumed", zone: "center", render: renderTokensSegment("in"), defaultShow: false },
297
+ { id: "tokens_out", label: "Tokens Out", shortLabel: "TOUT", description: "Output tokens generated", zone: "center", render: renderTokensSegment("out"), defaultShow: false },
298
+ { id: "session", label: "Session", shortLabel: "SES", description: "Session identifier", zone: "left", render: renderSessionSegment, defaultShow: false },
299
+ { id: "hostname", label: "Hostname", shortLabel: "HST", description: "Machine hostname", zone: "left", render: renderHostnameSegment, defaultShow: false },
300
+ { id: "clock", label: "Clock", shortLabel: "CLK", description: "Current wall time (HH:MM:SS)", zone: "right", render: renderClockSegment, defaultShow: true },
301
+ { id: "duration", label: "Duration", shortLabel: "DUR", description: "Session duration", zone: "right", render: renderDurationSegment, defaultShow: true },
302
+ { id: "thinking_level", label: "Thinking", shortLabel: "THK", description: "Current model thinking level", zone: "center", render: renderThinkingLevelSegment, defaultShow: false },
342
303
  ];
@@ -22,7 +22,7 @@ function withIcon(segmentId: string, text: string): string {
22
22
  */
23
23
  function getKanboardData(): Record<string, unknown> | null {
24
24
  try {
25
- const registry = globalThis.__unipi_kanboard_registry;
25
+ const registry = (globalThis as Record<string, unknown>).__unipi_kanboard_registry;
26
26
  if (!registry || typeof registry !== "object") return null;
27
27
  return registry as Record<string, unknown>;
28
28
  } catch {
@@ -84,8 +84,8 @@ function renderTaskPctSegment(ctx: FooterSegmentContext): RenderedSegment {
84
84
  }
85
85
 
86
86
  export const KANBOARD_SEGMENTS: FooterSegment[] = [
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 },
87
+ { id: "docs_count", label: "Docs", shortLabel: "DOC", description: "Workflow documents count", zone: "center", render: renderDocsCountSegment, defaultShow: true },
88
+ { id: "tasks_done", label: "Done", shortLabel: "DNE", description: "Completed tasks", zone: "center", render: renderTasksDoneSegment, defaultShow: true },
89
+ { id: "tasks_total", label: "Total", shortLabel: "TSK", description: "Total tasks", zone: "center", render: renderTasksTotalSegment, defaultShow: true },
90
+ { id: "task_pct", label: "Progress", shortLabel: "PCT", description: "Task completion percentage", zone: "center", render: renderTaskPctSegment, defaultShow: true },
91
91
  ];
@@ -37,7 +37,7 @@ function withIcon(segmentId: string, text: string): string {
37
37
  */
38
38
  function getMcpStats(ctx: FooterSegmentContext): McpStats {
39
39
  // Source 1: globalThis escape hatch (future: MCP registry may expose this)
40
- const global = globalThis.__unipi_mcp_stats;
40
+ const global = (globalThis as Record<string, unknown>).__unipi_mcp_stats;
41
41
  if (global && typeof global === "object") {
42
42
  return global;
43
43
  }
@@ -102,8 +102,8 @@ function renderServersFailedSegment(ctx: FooterSegmentContext): RenderedSegment
102
102
  }
103
103
 
104
104
  export const MCP_SEGMENTS: FooterSegment[] = [
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 },
105
+ { id: "servers_total", label: "Servers", shortLabel: "SRV", description: "Total MCP servers configured", zone: "center", render: renderServersTotalSegment, defaultShow: true },
106
+ { id: "servers_active", label: "Active", shortLabel: "ACT", description: "Currently connected MCP servers", zone: "center", render: renderServersActiveSegment, defaultShow: true },
107
+ { id: "tools_total", label: "Tools", shortLabel: "TLS", description: "Total MCP tools available", zone: "center", render: renderToolsTotalSegment, defaultShow: true },
108
+ { id: "servers_failed", label: "Failed", shortLabel: "ERR", description: "Failed MCP server connections", zone: "center", render: renderServersFailedSegment, defaultShow: true },
109
109
  ];
@@ -51,7 +51,7 @@ interface InfoRegistryLike {
51
51
  */
52
52
  function getInfoRegistryMemoryData(): InfoMemoryData | null {
53
53
  try {
54
- const registry = globalThis.__unipi_info_registry;
54
+ const registry = (globalThis as Record<string, unknown>).__unipi_info_registry;
55
55
  if (!registry || typeof registry !== "object") return null;
56
56
 
57
57
  // The info registry exposes getCachedData(groupId) synchronously
@@ -139,7 +139,7 @@ function renderConsolidationsSegment(ctx: FooterSegmentContext): RenderedSegment
139
139
  }
140
140
 
141
141
  export const MEMORY_SEGMENTS: FooterSegment[] = [
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
+ { id: "project_count", label: "Project Memory", shortLabel: "MEM", description: "Memory entries for this project", zone: "center", render: renderProjectCountSegment, defaultShow: true },
143
+ { id: "total_count", label: "Total Memory", shortLabel: "TOT", description: "Total memory entries across projects", zone: "center", render: renderTotalCountSegment, defaultShow: true },
144
+ { id: "consolidations", label: "Consolidations", shortLabel: "CNS", description: "Number of memory consolidations", zone: "center", render: renderConsolidationsSegment, defaultShow: false },
145
145
  ];
@@ -56,6 +56,6 @@ function renderLastSentSegment(ctx: FooterSegmentContext): RenderedSegment {
56
56
  }
57
57
 
58
58
  export const NOTIFY_SEGMENTS: FooterSegment[] = [
59
- { id: "platforms_enabled", label: "Platforms", shortLabel: "NTF", description: "Active notification platforms", zone: "center", icon: "", render: renderPlatformsEnabledSegment, defaultShow: true },
60
- { id: "last_sent", label: "Last Sent", shortLabel: "LST", description: "Time of last notification sent", zone: "center", icon: "", render: renderLastSentSegment, defaultShow: true },
59
+ { id: "platforms_enabled", label: "Platforms", shortLabel: "NTF", description: "Active notification platforms", zone: "center", render: renderPlatformsEnabledSegment, defaultShow: true },
60
+ { id: "last_sent", label: "Last Sent", shortLabel: "LST", description: "Time of last notification sent", zone: "center", render: renderLastSentSegment, defaultShow: true },
61
61
  ];
@@ -117,7 +117,7 @@ function renderLoopStatusSegment(ctx: FooterSegmentContext): RenderedSegment {
117
117
  }
118
118
 
119
119
  export const RALPH_SEGMENTS: FooterSegment[] = [
120
- { id: "active_loops", label: "Loops", shortLabel: "RL", description: "Active Ralph loops", zone: "center", icon: "", render: renderActiveLoopsSegment, defaultShow: true },
121
- { id: "total_iterations", label: "Iterations", shortLabel: "ITR", description: "Total Ralph loop iterations", zone: "center", icon: "", render: renderTotalIterationsSegment, defaultShow: true },
122
- { id: "loop_status", label: "Status", shortLabel: "STS", description: "Current loop status", zone: "center", icon: "", render: renderLoopStatusSegment, defaultShow: true },
120
+ { id: "active_loops", label: "Loops", shortLabel: "RL", description: "Active Ralph loops", zone: "center", render: renderActiveLoopsSegment, defaultShow: true },
121
+ { id: "total_iterations", label: "Iterations", shortLabel: "ITR", description: "Total Ralph loop iterations", zone: "center", render: renderTotalIterationsSegment, defaultShow: true },
122
+ { id: "loop_status", label: "Status", shortLabel: "STS", description: "Current loop status", zone: "center", render: renderLoopStatusSegment, defaultShow: true },
123
123
  ];
@@ -120,5 +120,5 @@ function renderExtensionStatusesSegment(ctx: FooterSegmentContext): RenderedSegm
120
120
  }
121
121
 
122
122
  export const STATUS_EXT_SEGMENTS: FooterSegment[] = [
123
- { id: "extension_statuses", label: "Extensions", shortLabel: "EXT", description: "Extension statuses overview", zone: "center", icon: "", render: renderExtensionStatusesSegment, defaultShow: true },
123
+ { id: "extension_statuses", label: "Extensions", shortLabel: "EXT", description: "Extension statuses overview", zone: "center", render: renderExtensionStatusesSegment, defaultShow: true },
124
124
  ];
@@ -118,7 +118,7 @@ function formatDuration(ms: number): string {
118
118
  }
119
119
 
120
120
  export const WORKFLOW_SEGMENTS: FooterSegment[] = [
121
- { id: "current_command", label: "Command", shortLabel: "WRK", description: "Active workflow command", zone: "left", icon: "", render: renderCurrentCommandSegment, defaultShow: true },
122
- { id: "sandbox_level", label: "Sandbox", shortLabel: "SBX", description: "Sandbox permission level", zone: "center", icon: "", render: renderSandboxLevelSegment, defaultShow: false },
123
- { id: "command_duration", label: "Duration", shortLabel: "CDUR", description: "Current command duration", zone: "center", icon: "", render: renderCommandDurationSegment, defaultShow: true },
121
+ { id: "current_command", label: "Command", shortLabel: "WRK", description: "Active workflow command", zone: "left", render: renderCurrentCommandSegment, defaultShow: true },
122
+ { id: "sandbox_level", label: "Sandbox", shortLabel: "SBX", description: "Sandbox permission level", zone: "center", render: renderSandboxLevelSegment, defaultShow: false },
123
+ { id: "command_duration", label: "Duration", shortLabel: "CDUR", description: "Current command duration", zone: "center", render: renderCommandDurationSegment, defaultShow: true },
124
124
  ];
@@ -245,14 +245,6 @@ export class TpsTracker {
245
245
  return last.startedAt > 0 && last.completedAt === 0;
246
246
  }
247
247
 
248
- /**
249
- * Whether the model was recently generating tokens.
250
- * Kept for backward compatibility with renderer.
251
- */
252
- isGenerating(): boolean {
253
- return this.isStreaming();
254
- }
255
-
256
248
  /**
257
249
  * Get total output tokens for the session (estimated).
258
250
  */
@@ -10,12 +10,12 @@
10
10
  */
11
11
 
12
12
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
13
- import { matchesKey, SettingsList, type SettingItem, type SettingsListTheme } from "@earendil-works/pi-tui";
13
+ import { Key, matchesKey, SettingsList, type SettingItem, type SettingsListTheme } from "@earendil-works/pi-tui";
14
14
  import { loadFooterSettings, saveFooterSettings } from "../config.js";
15
15
  import { PRESET_NAMES } from "../presets.js";
16
16
  import { setIconStyle } from "../rendering/icons.js";
17
17
  import type { FooterGroup, FooterSettings, SeparatorStyle, IconStyle } from "../types.js";
18
- import { boxInnerWidth } from "@pi-unipi/core";
18
+ import { boxInnerWidth, OverlayTheme } from "@pi-unipi/core";
19
19
 
20
20
  // ─── Section types ─────────────────────────────────────────────────────
21
21
 
@@ -43,27 +43,10 @@ const THEME: SettingsListTheme = {
43
43
  hint: (text) => `\x1b[2m${text}\x1b[0m`,
44
44
  };
45
45
 
46
- // ─── Helper: frame a line inside box drawing ───────────────────────────
46
+ // ─── Shared box-drawing helper ────────────────────────────────────────
47
47
 
48
- function frameLine(content: string, innerWidth: number): string {
49
- const visLen = visibleWidth(content);
50
- const pad = Math.max(0, innerWidth - visLen);
51
- return `\x1b[90m│\x1b[0m${content}${" ".repeat(pad)}\x1b[90m│\x1b[0m`;
52
- }
53
-
54
- function ruleLine(innerWidth: number): string {
55
- return `\x1b[90m├${"─".repeat(innerWidth)}┤\x1b[0m`;
56
- }
48
+ const overlay = new OverlayTheme();
57
49
 
58
- function borderLine(innerWidth: number, edge: "top" | "bottom"): string {
59
- const left = edge === "top" ? "┌" : "└";
60
- const right = edge === "top" ? "┐" : "┘";
61
- return `\x1b[90m${left}${"─".repeat(innerWidth)}${right}\x1b[0m`;
62
- }
63
-
64
- function visibleWidth(text: string): number {
65
- return text.replace(/\x1b\[[0-9;]*m/g, "").length;
66
- }
67
50
 
68
51
  // ─── Show the footer settings overlay ──────────────────────────────────
69
52
 
@@ -129,7 +112,7 @@ class FooterSettingsOverlay {
129
112
 
130
113
  handleInput(data: string): void {
131
114
  // Tab cycles sections
132
- if (data === "\t" || data === "\x1b[Z") {
115
+ if (data === "\t" || matchesKey(data, Key.shift("tab"))) {
133
116
  const idx = SECTIONS.indexOf(this.section);
134
117
  if (data === "\t") {
135
118
  this.section = SECTIONS[(idx + 1) % SECTIONS.length];
@@ -209,7 +192,7 @@ class FooterSettingsOverlay {
209
192
  }
210
193
 
211
194
  // Left arrow / backspace in segment drill-down — back to groups
212
- if (this.section === "segments" && this.selectedGroupId && (data === "\x1b[D" || data === "\x7f")) {
195
+ if (this.section === "segments" && this.selectedGroupId && (matchesKey(data, Key.left) || data === "\x7f")) {
213
196
  this.backToGroups();
214
197
  return;
215
198
  }
@@ -462,8 +445,8 @@ class FooterSettingsOverlay {
462
445
  const lines: string[] = [];
463
446
 
464
447
  // Header
465
- lines.push(borderLine(innerWidth, "top"));
466
- lines.push(frameLine(`\x1b[1m\x1b[36m⚙ Footer Settings\x1b[0m`, innerWidth));
448
+ lines.push(overlay.borderLine(innerWidth, "top"));
449
+ lines.push(overlay.frameLine(`\x1b[1m\x1b[36m⚙ Footer Settings\x1b[0m`, innerWidth));
467
450
 
468
451
  // Section tabs
469
452
  const tabParts = SECTIONS.map((s) => {
@@ -473,20 +456,20 @@ class FooterSettingsOverlay {
473
456
  }
474
457
  return `\x1b[2m${label}\x1b[0m`;
475
458
  });
476
- lines.push(frameLine(` ${tabParts.join(" ")}`, innerWidth));
477
- lines.push(ruleLine(innerWidth));
459
+ lines.push(overlay.frameLine(` ${tabParts.join(" ")}`, innerWidth));
460
+ lines.push(overlay.ruleLine(innerWidth));
478
461
 
479
462
  // Section content
480
463
  const activeList = this.currentList;
481
464
  if (activeList) {
482
465
  const contentLines = activeList.render(innerWidth - 2);
483
466
  for (const line of contentLines) {
484
- lines.push(frameLine(` ${line}`, innerWidth));
467
+ lines.push(overlay.frameLine(` ${line}`, innerWidth));
485
468
  }
486
469
  }
487
470
 
488
471
  // Footer hints
489
- lines.push(ruleLine(innerWidth));
472
+ lines.push(overlay.ruleLine(innerWidth));
490
473
 
491
474
  let hints: string;
492
475
  if (this.section === "segments" && this.selectedGroupId) {
@@ -496,8 +479,8 @@ class FooterSettingsOverlay {
496
479
  } else {
497
480
  hints = "j/k navigate · Space/Enter change · Tab section · q close";
498
481
  }
499
- lines.push(frameLine(`\x1b[2m${hints}\x1b[0m`, innerWidth));
500
- lines.push(borderLine(innerWidth, "bottom"));
482
+ lines.push(overlay.frameLine(`\x1b[2m${hints}\x1b[0m`, innerWidth));
483
+ lines.push(overlay.borderLine(innerWidth, "bottom"));
501
484
 
502
485
  return lines;
503
486
  }
package/src/types.ts CHANGED
@@ -145,8 +145,6 @@ export interface FooterSegment {
145
145
  description: string;
146
146
  /** Layout zone assignment */
147
147
  zone: SegmentZone;
148
- /** Icon glyph (Nerd Font or ASCII) */
149
- icon: string;
150
148
  /** Render function */
151
149
  render: SegmentRenderFn;
152
150
  /** Whether this segment is shown by default */
@@ -161,8 +159,6 @@ export interface FooterGroup {
161
159
  id: string;
162
160
  /** Display name */
163
161
  name: string;
164
- /** Group icon */
165
- icon: string;
166
162
  /** Segments within this group */
167
163
  segments: FooterSegment[];
168
164
  /** Whether this group is shown by default */
@@ -199,13 +195,6 @@ export interface FooterSettings {
199
195
  groups: Record<string, FooterGroupSettings>;
200
196
  }
201
197
 
202
- /** Full footer config (settings + runtime state) */
203
- export interface FooterConfig {
204
- settings: FooterSettings;
205
- /** Runtime: whether footer is currently active */
206
- active: boolean;
207
- }
208
-
209
198
  // ─── Presets ────────────────────────────────────────────────────────────────
210
199
 
211
200
  /** Preset definition */
@@ -218,10 +207,6 @@ export interface PresetDef {
218
207
  secondarySegments: string[];
219
208
  /** Color scheme for this preset */
220
209
  colors?: ColorScheme;
221
- /** Per-segment options */
222
- segmentOptions?: Record<string, Record<string, unknown>>;
223
- /** Zone order (default: left, center, right) */
224
- zoneOrder?: ("left" | "center" | "right")[];
225
210
  /** Zone separator string (between zones) */
226
211
  zoneSeparator?: string;
227
212
  }