@pi-unipi/footer 3.0.0-alpha.3 → 3.0.0-alpha.5

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/README.md CHANGED
@@ -37,7 +37,7 @@ Footer subscribes to events from every Unipi package:
37
37
  | Group | Events | Segments |
38
38
  |-------|--------|----------|
39
39
  | core | Pi SDK | model, thinking, path, git, context_pct, cost, tokens, session |
40
- | compactor | Pi session data + `COMPACTOR_COMPACTED` | session_events, compactions, tokens_saved, compression_ratio |
40
+ | compactor | Pi session data | compactions — `cmp 4× 39k→13k · 3m` (count, tokens before → after, time since the last; hidden until the first) |
41
41
  | memory | `MEMORY_STORED`/`DELETED`/`CONSOLIDATED` | project_count, total_count, consolidations |
42
42
  | mcp | `MCP_SERVER_STARTED`/`STOPPED`/`ERROR` | servers_total, servers_active, tools_total |
43
43
  | ralph | `RALPH_LOOP_START`/`END`/`ITERATION_DONE` | active_loops, total_iterations, loop_status |
@@ -90,9 +90,7 @@ Settings in `~/.pi/agent/settings.json` under `unipi.footer`:
90
90
  "compactor": {
91
91
  "show": true,
92
92
  "segments": {
93
- "session_events": true,
94
- "compactions": true,
95
- "tokens_saved": true
93
+ "compactions": true
96
94
  }
97
95
  }
98
96
  }
@@ -137,11 +135,11 @@ When `iconStyle` is not set, footer auto-detects Nerd Font support and defaults
137
135
 
138
136
  ```
139
137
  Wide terminal (>120 cols):
140
- model | thinking | path | git | context | cost | compactions | tokens_saved | project_count
138
+ model | thinking | path | git | context | compactions | cost | project_count
141
139
 
142
140
  Narrow terminal (<120 cols):
143
141
  Row 1: model | thinking | path | git | context | cost
144
- Row 2: compactions | tokens_saved | project_count | ralph | workflow
142
+ Row 2: compactions | project_count | ralph | workflow
145
143
  ```
146
144
 
147
145
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "3.0.0-alpha.3",
3
+ "version": "3.0.0-alpha.5",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.3",
36
- "@pi-unipi/background-tasks": "3.0.0-alpha.3"
35
+ "@pi-unipi/core": "3.0.0-alpha.5",
36
+ "@pi-unipi/background-tasks": "3.0.0-alpha.5"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
package/src/help.ts CHANGED
@@ -75,7 +75,7 @@ function buildHelpLines(
75
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", "uni", "directory"];
77
77
  if (coreIds.includes(segId)) return "core";
78
- const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "sandbox_runs", "search_queries"];
78
+ const compactorIds = ["compactions"];
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
@@ -18,7 +18,7 @@ import { GlanceEditor } from "./glance-editor.js";
18
18
 
19
19
  // Import segment groups
20
20
  import { CORE_SEGMENTS } from "./segments/core.js";
21
- import { COMPACTOR_SEGMENTS } from "./segments/compactor.js";
21
+ import { COMPACTOR_SEGMENTS, compactionSummary } from "./segments/compactor.js";
22
22
  import { MEMORY_SEGMENTS } from "./segments/memory.js";
23
23
  import { MCP_SEGMENTS } from "./segments/mcp.js";
24
24
  import { RALPH_SEGMENTS } from "./segments/ralph.js";
@@ -546,14 +546,30 @@ const STRIP_COLOR = {
546
546
  psMid: "\x1b[97m", // white — in between
547
547
  cacheHit: "\x1b[92m", // green — high hit
548
548
  cacheWarn: "\x1b[93m", // amber — lowish hit
549
+ compact: "\x1b[94m", // blue — compactions
549
550
  reset: "\x1b[39m",
550
551
  } as const;
551
552
 
552
553
  const c = (code: string, text: string) => `${code}${text}${STRIP_COLOR.reset}`;
553
554
 
555
+ function fmtTokensShort(n: number): string {
556
+ if (n < 1000) return String(n);
557
+ if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
558
+ if (n < 1000000) return `${Math.round(n / 1000)}k`;
559
+ return `${(n / 1000000).toFixed(1)}M`;
560
+ }
561
+
562
+ function agoShort(ms: number): string {
563
+ const sec = Math.max(0, Math.round(ms / 1000));
564
+ if (sec < 60) return "just now";
565
+ if (sec < 3600) return `${Math.round(sec / 60)}m ago`;
566
+ if (sec < 86400) return `${Math.round(sec / 3600)}h ago`;
567
+ return `${Math.round(sec / 86400)}d ago`;
568
+ }
569
+
554
570
  /**
555
571
  * Glance-style centered stats strip under the input:
556
- * n Turn · n Steps | wall · tool wall | avg TTFT · n tok/s | cache n%
572
+ * n Turn · n Steps | wall · tool wall | avg TTFT · n tok/s | n compactions · 39k→13k · 3m ago | cache n%
557
573
  */
558
574
  function renderSessionStrip(piContext: unknown): string | null {
559
575
  const parts: string[] = [];
@@ -588,6 +604,21 @@ function renderSessionStrip(piContext: unknown): string | null {
588
604
  if (seg) parts.push(seg);
589
605
  }
590
606
 
607
+ // Compactions: hidden until the first one.
608
+ const branch = (() => {
609
+ try {
610
+ return ((piContext as any)?.sessionManager?.getBranch?.() ?? []) as unknown[];
611
+ } catch {
612
+ return [];
613
+ }
614
+ })();
615
+ const cmp = compactionSummary(branch);
616
+ if (cmp.count > 0) {
617
+ const sizes = cmp.before > 0 ? ` \u00b7 ${c(STRIP_COLOR.compact, `${fmtTokensShort(cmp.before)}\u2192${fmtTokensShort(cmp.after)}`)}` : "";
618
+ const age = cmp.lastAt != null ? ` \u00b7 ${agoShort(Date.now() - cmp.lastAt)}` : "";
619
+ parts.push(`${c(STRIP_COLOR.compact, String(cmp.count))} compaction${cmp.count === 1 ? "" : "s"}${sizes}${age}`);
620
+ }
621
+
591
622
  const hit = cacheHitPct(piContext);
592
623
  if (hit !== null) {
593
624
  const hitColor = hit >= 70 ? STRIP_COLOR.cacheHit : STRIP_COLOR.cacheWarn;
package/src/presets.ts CHANGED
@@ -18,7 +18,7 @@ const DEFAULT_PRESET: PresetDef = {
18
18
  "uni", "lh_mode", "model", "thinking_level", "directory", "git",
19
19
  ],
20
20
  rightSegments: [
21
- "context_pct", "tokens_total",
21
+ "context_pct", "compactions", "tokens_total",
22
22
  "tps", "cost",
23
23
  "clock", "duration",
24
24
  ],
@@ -33,7 +33,7 @@ const CLASSIC_PRESET: PresetDef = {
33
33
  ],
34
34
  rightSegments: [
35
35
  "tps", "context_pct", "cost",
36
- "compactions", "tokens_saved", "project_count",
36
+ "compactions", "project_count",
37
37
  "current_command", "loop_status", "extension_statuses",
38
38
  "clock", "duration",
39
39
  ],
@@ -76,7 +76,7 @@ const FULL_PRESET: PresetDef = {
76
76
  ],
77
77
  rightSegments: [
78
78
  "tps", "context_pct", "cost", "tokens_total",
79
- "session_events", "compactions", "tokens_saved",
79
+ "compactions",
80
80
  "project_count", "total_count",
81
81
  "servers_total", "servers_active", "tools_total",
82
82
  "active_loops", "loop_status",
@@ -87,7 +87,6 @@ const FULL_PRESET: PresetDef = {
87
87
  secondarySegments: [
88
88
  "hostname",
89
89
  "tokens_in", "tokens_out",
90
- "compression_ratio",
91
90
  "platforms_enabled", "last_sent",
92
91
  "thinking_level",
93
92
  ],
@@ -35,13 +35,8 @@ export interface IconSet {
35
35
  directory: string;
36
36
 
37
37
  // Compactor segments
38
- sessionEvents: string;
39
- compactions: string;
40
- tokensSaved: string;
41
- compressionRatio: string;
42
38
  indexedDocs: string;
43
39
  sandboxRuns: string;
44
- searchQueries: string;
45
40
 
46
41
  // Memory segments
47
42
  projectCount: string;
@@ -106,13 +101,8 @@ export const NERD_ICONS: IconSet = {
106
101
  thinkingLevel: "\uF400", //
107
102
 
108
103
  // Compactor
109
- sessionEvents: "\uEA86", //
110
- compactions: "\u{F0C8F}", // 󰲏
111
- tokensSaved: "\uF155", // (kept — missing from customization)
112
- compressionRatio:"\u{F0C8F}", // 󰲏
113
104
  indexedDocs: "\u{F0219}", // 󰈙
114
105
  sandboxRuns: "\uF233", //
115
- searchQueries: "\uF002", //
116
106
 
117
107
  // Memory
118
108
  projectCount: "\uEE9C", //
@@ -177,13 +167,8 @@ export const EMOJI_ICONS: IconSet = {
177
167
  directory: "📁",
178
168
 
179
169
  // Compactor
180
- sessionEvents: "📈",
181
- compactions: "🗜️",
182
- tokensSaved: "💲",
183
- compressionRatio:"📐",
184
170
  indexedDocs: "📑",
185
171
  sandboxRuns: "▶️",
186
- searchQueries: "🔍",
187
172
 
188
173
  // Memory
189
174
  projectCount: "🧠",
@@ -248,13 +233,8 @@ export const TEXT_ICONS: IconSet = {
248
233
  directory: "DIR",
249
234
 
250
235
  // Compactor
251
- sessionEvents: "EVT",
252
- compactions: "CMP",
253
- tokensSaved: "SVD",
254
- compressionRatio:"RAT",
255
236
  indexedDocs: "IDX",
256
237
  sandboxRuns: "SBX",
257
- searchQueries: "QRY",
258
238
 
259
239
  // Memory
260
240
  projectCount: "MEM",
@@ -1,23 +1,16 @@
1
1
  /**
2
- * @pi-unipi/footer — Compactor segments
2
+ * @pi-unipi/footer — Compactor segment
3
3
  *
4
- * Segment renderers for the compactor group: session_events, compactions,
5
- * tokens_saved, compression_ratio, sandbox_runs, search_queries.
4
+ * One dense, icon-free segment, shown once the session has compacted:
6
5
  *
7
- * Data sourced from piContext.sessionManager (live session data).
8
- * Segments without a reliable data source are hidden (visible: false)
9
- * rather than showing a placeholder like "—".
6
+ * cmp 4× 39k→13k · 3m
7
+ *
8
+ * count, tokens before → after across all compactions, time since the last.
9
+ * Read from the session branch (Pi's compaction entries).
10
10
  */
11
11
 
12
12
  import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
13
- import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
14
- import { getIcon } from "../rendering/icons.js";
15
- import { isSegmentEnabled } from "../config.js";
16
-
17
- function withIcon(segmentId: string, text: string): string {
18
- const icon = getIcon(segmentId);
19
- return icon ? `${icon} ${text}` : text;
20
- }
13
+ import { applyColor } from "../rendering/theme.js";
21
14
 
22
15
  function formatTokens(n: number): string {
23
16
  if (n < 1000) return n.toString();
@@ -26,149 +19,66 @@ function formatTokens(n: number): string {
26
19
  return `${(n / 1000000).toFixed(1)}M`;
27
20
  }
28
21
 
29
- /** Hidden segment — no reliable data source available */
30
- function hidden(): RenderedSegment {
31
- return { content: "", visible: false };
22
+ function ago(ms: number): string {
23
+ const s = Math.max(0, Math.round(ms / 1000));
24
+ if (s < 60) return "now";
25
+ if (s < 3600) return `${Math.round(s / 60)}m`;
26
+ if (s < 86400) return `${Math.round(s / 3600)}h`;
27
+ return `${Math.round(s / 86400)}d`;
32
28
  }
33
29
 
34
- /** Safely extract sessionManager from piContext */
35
- function getSessionManager(ctx: FooterSegmentContext): any {
36
- const piCtx = ctx.piContext as Record<string, unknown> | undefined;
37
- return piCtx?.sessionManager as any | undefined;
30
+ /** Tokens left after a compaction: our recorded estimate, else the summary size. */
31
+ function tokensAfterOf(entry: any, before: number): number {
32
+ const recorded = Number(entry?.details?.tokensAfter ?? 0);
33
+ if (recorded > 0) return Math.min(recorded, before);
34
+ const summaryChars = typeof entry?.summary === "string" ? entry.summary.length : 0;
35
+ return Math.min(before, Math.ceil(summaryChars / 4));
38
36
  }
39
37
 
40
- /** Get all session events from sessionManager branch */
41
- function getSessionEvents(ctx: FooterSegmentContext): any[] {
42
- const sm = getSessionManager(ctx);
43
- if (!sm || typeof sm.getBranch !== "function") return [];
38
+ function branchOf(ctx: FooterSegmentContext): any[] {
39
+ const sm = (ctx.piContext as Record<string, unknown> | undefined)?.sessionManager as any;
44
40
  try {
45
- return sm.getBranch() ?? [];
41
+ return typeof sm?.getBranch === "function" ? (sm.getBranch() ?? []) : [];
46
42
  } catch {
47
43
  return [];
48
44
  }
49
45
  }
50
46
 
51
- function renderSessionEventsSegment(ctx: FooterSegmentContext): RenderedSegment {
52
- const events = getSessionEvents(ctx);
53
- const count = events.length;
54
- if (count === 0) {
55
- if (isSegmentEnabled("compactor", "session_events")) {
56
- return { content: mutedPlaceholder(withIcon("sessionEvents", "0")), visible: true };
57
- }
58
- return hidden();
47
+ export function compactionSummary(branch: readonly any[], now = Date.now()): { count: number; before: number; after: number; lastAt?: number } {
48
+ let count = 0;
49
+ let before = 0;
50
+ let after = 0;
51
+ let lastAt: number | undefined;
52
+ for (const e of branch) {
53
+ if (e?.type !== "compaction") continue;
54
+ count++;
55
+ const b = Number(e.tokensBefore ?? 0);
56
+ before += b;
57
+ after += tokensAfterOf(e, b);
58
+ const at = Date.parse(e.timestamp ?? "");
59
+ if (Number.isFinite(at) && at <= now) lastAt = at;
59
60
  }
60
-
61
- const content = withIcon("sessionEvents", `${count}`);
62
- return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
61
+ return { count, before, after, ...(lastAt != null ? { lastAt } : {}) };
63
62
  }
64
63
 
65
64
  function renderCompactionsSegment(ctx: FooterSegmentContext): RenderedSegment {
66
- // Count compaction entries in the session events
67
- const events = getSessionEvents(ctx);
68
- let compactionCount = 0;
69
- for (const e of events) {
70
- if (!e || typeof e !== "object") continue;
71
- if (e.type === "compaction" || e.type === "compacted") {
72
- compactionCount++;
73
- }
74
- }
75
- if (compactionCount === 0) {
76
- if (isSegmentEnabled("compactor", "compactions")) {
77
- return { content: mutedPlaceholder(withIcon("compactions", "0")), visible: true };
78
- }
79
- return hidden();
80
- }
81
-
82
- const content = withIcon("compactions", `${compactionCount}`);
83
- return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
84
- }
85
-
86
- function renderTokensSavedSegment(ctx: FooterSegmentContext): RenderedSegment {
87
- // Sum tokens saved from compaction entries.
88
- // Pi's CompactionEntry has tokensBefore (total tokens before compaction).
89
- // Compaction keeps ~10-15% of context, so tokens saved ≈ tokensBefore × 0.85.
90
- const events = getSessionEvents(ctx);
91
- let tokensSaved = 0;
92
- let hasCompaction = false;
93
- for (const e of events) {
94
- if (!e || typeof e !== "object") continue;
95
- if (e.type === "compaction") {
96
- hasCompaction = true;
97
- const tokensBefore = Number(e.tokensBefore ?? 0);
98
- // Estimate tokens kept at ~12% (compaction summary + recent messages)
99
- const tokensAfter = Math.round(tokensBefore * 0.12);
100
- tokensSaved += Math.max(0, tokensBefore - tokensAfter);
101
- }
102
- }
103
- if (!hasCompaction || tokensSaved === 0) return hidden();
104
-
105
- const content = withIcon("tokensSaved", formatTokens(tokensSaved));
106
- return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
107
- }
108
-
109
- function renderCompressionRatioSegment(ctx: FooterSegmentContext): RenderedSegment {
110
- // Calculate compression ratio from Pi's CompactionEntry.tokensBefore.
111
- // Compaction keeps ~12% of context, giving ~8:1 compression.
112
- const events = getSessionEvents(ctx);
113
- let totalBefore = 0;
114
- let totalAfter = 0;
115
- for (const e of events) {
116
- if (!e || typeof e !== "object") continue;
117
- if (e.type === "compaction") {
118
- const before = Number(e.tokensBefore ?? 0);
119
- if (before > 0) {
120
- totalBefore += before;
121
- totalAfter += Math.round(before * 0.12);
122
- }
123
- }
124
- }
125
- if (totalBefore === 0 || totalAfter === 0) return hidden();
126
-
127
- const ratio = totalBefore / totalAfter;
128
- const content = withIcon("compressionRatio", `${ratio.toFixed(1)}x`);
129
- return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
130
- }
131
-
132
- function renderSandboxRunsSegment(ctx: FooterSegmentContext): RenderedSegment {
133
- // Count sandbox events from session manager branch
134
- const events = getSessionEvents(ctx);
135
- let sandboxCount = 0;
136
- for (const e of events) {
137
- if (!e || typeof e !== "object") continue;
138
- // Count tool calls that are sandbox/execute tools
139
- const name = String((e as any).name ?? "").toLowerCase();
140
- if (name.includes("sandbox") || name.includes("ctx_execute") || name === "execute") {
141
- sandboxCount++;
142
- }
143
- }
144
- if (sandboxCount === 0) return hidden();
145
-
146
- const content = withIcon("sandboxRuns", `${sandboxCount}`);
147
- return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
148
- }
149
-
150
- function renderSearchQueriesSegment(ctx: FooterSegmentContext): RenderedSegment {
151
- // Count search events from session manager branch
152
- const events = getSessionEvents(ctx);
153
- let searchCount = 0;
154
- for (const e of events) {
155
- if (!e || typeof e !== "object") continue;
156
- const name = String((e as any).name ?? "").toLowerCase();
157
- if (name.includes("search") || name.includes("ctx_search")) {
158
- searchCount++;
159
- }
160
- }
161
- if (searchCount === 0) return hidden();
162
-
163
- const content = withIcon("searchQueries", `${searchCount}`);
164
- return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
65
+ const s = compactionSummary(branchOf(ctx));
66
+ if (s.count === 0) return { content: "", visible: false };
67
+ const dim = (t: string) => `\x1b[2m${t}\x1b[22m`;
68
+ const sizes = s.before > 0 ? ` ${formatTokens(s.before)}→${formatTokens(s.after)}` : "";
69
+ const age = s.lastAt != null ? dim(` · ${ago(Date.now() - s.lastAt)}`) : "";
70
+ const content = `${dim("cmp")} ${applyColor("compactor", `${s.count}×${sizes}`, ctx.theme, ctx.colors)}${age}`;
71
+ return { content, visible: true };
165
72
  }
166
73
 
167
74
  export const COMPACTOR_SEGMENTS: FooterSegment[] = [
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 },
75
+ {
76
+ id: "compactions",
77
+ label: "Compactions",
78
+ shortLabel: "CMP",
79
+ description: "Compactions this session: count, tokens before → after, time since the last (hidden until the first)",
80
+ zone: "center",
81
+ render: renderCompactionsSegment,
82
+ defaultShow: true,
83
+ },
174
84
  ];
@@ -23,7 +23,6 @@ const STATUS_DISPLAY: Record<string, { short: string; segmentId: string }> = {
23
23
  workflow: { short: "WF", segmentId: "currentCommand" },
24
24
  ralph: { short: "RL", segmentId: "activeLoops" },
25
25
  memory: { short: "MEM", segmentId: "projectCount" },
26
- compactor: { short: "CMP", segmentId: "compactions" },
27
26
  mcp: { short: "MCP", segmentId: "serversTotal" },
28
27
  notify: { short: "NTF", segmentId: "platformsEnabled" },
29
28
  kanboard: { short: "KB", segmentId: "docsCount" },