@pi-unipi/footer 3.0.0-alpha.2 → 3.0.0-alpha.20
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 +4 -6
- package/package.json +3 -3
- package/src/glance-editor.ts +19 -11
- package/src/help.ts +1 -1
- package/src/index.ts +36 -3
- package/src/presets.ts +3 -4
- package/src/rendering/icons.ts +0 -20
- package/src/segments/compactor.ts +52 -142
- package/src/segments/status-ext.ts +0 -1
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
|
|
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
|
-
"
|
|
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 |
|
|
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 |
|
|
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
|
+
"version": "3.0.0-alpha.20",
|
|
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.
|
|
36
|
-
"@pi-unipi/background-tasks": "3.0.0-alpha.
|
|
35
|
+
"@pi-unipi/core": "3.0.0-alpha.20",
|
|
36
|
+
"@pi-unipi/background-tasks": "3.0.0-alpha.20"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
package/src/glance-editor.ts
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* subclass CustomEditor, use super.render() ONLY to obtain editor content
|
|
6
6
|
* lines, then compose our own rounded frame. Layout per the v3 design:
|
|
7
7
|
*
|
|
8
|
-
* ╭─UNIPI │ feat/footer-default-v2
|
|
9
|
-
*
|
|
8
|
+
* ╭─UNIPI │ feat/footer-default-v2 ─────────────────────╮
|
|
9
|
+
* Type your prompt here...
|
|
10
10
|
* ╰─ unipi ────────── 42%/1.0M │ Claude Opus 4.5 │ thinking:high ╯
|
|
11
11
|
*
|
|
12
12
|
* Top border carries the UNIPI brand + git branch. Bottom border right side
|
|
13
13
|
* carries workspace · context%/window · model · thinking level; left filler.
|
|
14
|
+
* The body is open-sided: no vertical rails — the rounded corners are the
|
|
15
|
+
* only frame marks (content keeps its column via a one-space indent).
|
|
14
16
|
* All keybindings/autocomplete/history/paste behavior inherited.
|
|
15
17
|
*/
|
|
16
18
|
|
|
@@ -19,6 +21,7 @@ import type { EditorOptions, EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
|
19
21
|
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
20
22
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
21
23
|
import { getIcon, getResolvedIconStyle } from "./rendering/icons.js";
|
|
24
|
+
import { kanboardGlanceLabel } from "@pi-unipi/core";
|
|
22
25
|
import { lolcatRainbow, paintLolcatGradient } from "./rendering/lolcat.js";
|
|
23
26
|
|
|
24
27
|
/** Live status injected by the footer extension. */
|
|
@@ -41,12 +44,13 @@ export interface GlanceStatus {
|
|
|
41
44
|
modelName: string;
|
|
42
45
|
/** Current thinking level (already "off"-filtered by the provider). */
|
|
43
46
|
thinkingLevel: string | null;
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* and the plain thinking slot is suppressed (efforts are inline).
|
|
47
|
+
/** Active Fusion pair, when selected. When set, the model slot renders
|
|
48
|
+
* `Fusion · <lead> <effort> ◆ <sidekick>` — lead lit, sidekick muted —
|
|
49
|
+
* and the plain thinking slot is suppressed (efforts are inline).
|
|
48
50
|
*/
|
|
49
51
|
fusion: { leadName: string; leadEffort: string; sidekickName: string; sidekickEffort: string; savedUsd?: number; busy?: boolean; leadToolCalls?: number; sidekickToolCalls?: number } | null;
|
|
52
|
+
/** Kanboard claims/autowork snapshot (null → no `▣` segment). */
|
|
53
|
+
kanboard: { claims: string[]; autowork: boolean } | null;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
const BORDER = {
|
|
@@ -196,17 +200,21 @@ export function composeGlanceTitles(
|
|
|
196
200
|
branch: string | null,
|
|
197
201
|
workspace: string,
|
|
198
202
|
lhMode: string | null = null,
|
|
203
|
+
kanboard: { claims: string[]; autowork: boolean } | null = null,
|
|
199
204
|
): { titleParts: string[]; leftTitle: string } {
|
|
205
|
+
const kanboardLabel = kanboardGlanceLabel(kanboard);
|
|
200
206
|
const titleParts: string[] = [];
|
|
201
207
|
if (getResolvedIconStyle() === "text") {
|
|
202
208
|
titleParts.push(brand);
|
|
203
209
|
if (lhMode) titleParts.push(`mode:${lhMode}`);
|
|
210
|
+
if (kanboardLabel) titleParts.push(kanboardLabel.replace(/^▣ /, "kanboard:"));
|
|
204
211
|
if (branch) titleParts.push(`branch:${branch}`);
|
|
205
212
|
return { titleParts, leftTitle: ` workspace:${workspace} ` };
|
|
206
213
|
}
|
|
207
214
|
const brandIcon = getIcon("model");
|
|
208
215
|
titleParts.push(`${brandIcon ? brandIcon + " " : ""}${brand}`);
|
|
209
216
|
if (lhMode) titleParts.push(lhMode);
|
|
217
|
+
if (kanboardLabel) titleParts.push(kanboardLabel);
|
|
210
218
|
if (branch) {
|
|
211
219
|
const gitIcon = getIcon("git");
|
|
212
220
|
titleParts.push(`${gitIcon ? gitIcon + " " : ""}${branch}`);
|
|
@@ -276,7 +284,7 @@ export class GlanceEditor extends CustomEditor {
|
|
|
276
284
|
// Brand rendered as an animated lolcat gradient (phase from wall time;
|
|
277
285
|
// the footer's 1s refresh timer re-renders, so it shimmers each tick).
|
|
278
286
|
const brand = lolcatRainbow("UNIPI", Date.now() / 1000);
|
|
279
|
-
const { titleParts, leftTitle } = composeGlanceTitles(brand, st.branch, st.workspace, st.lhMode);
|
|
287
|
+
const { titleParts, leftTitle } = composeGlanceTitles(brand, st.branch, st.workspace, st.lhMode, st.kanboard);
|
|
280
288
|
const title = titleParts.join(SEP);
|
|
281
289
|
const leadRule = `${BORDER.horizontal} `;
|
|
282
290
|
const titleText = ` ${title}${SEP}`;
|
|
@@ -296,10 +304,10 @@ export class GlanceEditor extends CustomEditor {
|
|
|
296
304
|
topCluster +
|
|
297
305
|
border(BORDER.topRight);
|
|
298
306
|
|
|
299
|
-
// ── Body rows:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
);
|
|
307
|
+
// ── Body rows: open sides — the corner curves are the only frame marks.
|
|
308
|
+
// One-space indent keeps content in the column it had between the old
|
|
309
|
+
// rails; no trailing padding (rows may end short of the frame width).
|
|
310
|
+
const bodyRows = contentLines.map(row => " " + truncateToWidth(row, inner, ""));
|
|
303
311
|
|
|
304
312
|
// ── Bottom frame: ╰─ unipi ─────── [RIGHT CLUSTER] ─╯ ──
|
|
305
313
|
// Right cluster: workspace · pct%/window │ model │ thinking:level
|
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 = ["
|
|
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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI, Theme, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
-
import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS, getSharedFusionStatus, getSharedLongHorizonMode, getSharedPlanPermissionStatus } from "@pi-unipi/core";
|
|
10
|
+
import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS, getSharedFusionStatus, getSharedKanboardStatus, getSharedLongHorizonMode, getSharedPlanPermissionStatus } from "@pi-unipi/core";
|
|
11
11
|
import { FooterRegistry, getFooterRegistry } from "./registry/index.js";
|
|
12
12
|
import { FooterRenderer } from "./rendering/renderer.js";
|
|
13
13
|
import { subscribeToEvents } from "./events.js";
|
|
@@ -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";
|
|
@@ -448,6 +448,7 @@ function installGlanceEditor(
|
|
|
448
448
|
const lhMode = typeof lhModeRaw === "string" && lhModeRaw.length > 0
|
|
449
449
|
? MODE_LABELS[lhModeRaw] ?? lhModeRaw
|
|
450
450
|
: null;
|
|
451
|
+
const kanboard = getSharedKanboardStatus() ?? null;
|
|
451
452
|
// Plan/permission state rides the same CORE registry data, written by
|
|
452
453
|
// the workflow module's events (also re-emitted on session_start).
|
|
453
454
|
const coreData = st.registry.getGroupData("core") as
|
|
@@ -470,6 +471,7 @@ function installGlanceEditor(
|
|
|
470
471
|
modelName,
|
|
471
472
|
thinkingLevel: typeof p?.thinkingLevel === "string" ? p.thinkingLevel : null,
|
|
472
473
|
fusion,
|
|
474
|
+
kanboard,
|
|
473
475
|
};
|
|
474
476
|
}),
|
|
475
477
|
);
|
|
@@ -546,14 +548,30 @@ const STRIP_COLOR = {
|
|
|
546
548
|
psMid: "\x1b[97m", // white — in between
|
|
547
549
|
cacheHit: "\x1b[92m", // green — high hit
|
|
548
550
|
cacheWarn: "\x1b[93m", // amber — lowish hit
|
|
551
|
+
compact: "\x1b[94m", // blue — compactions
|
|
549
552
|
reset: "\x1b[39m",
|
|
550
553
|
} as const;
|
|
551
554
|
|
|
552
555
|
const c = (code: string, text: string) => `${code}${text}${STRIP_COLOR.reset}`;
|
|
553
556
|
|
|
557
|
+
function fmtTokensShort(n: number): string {
|
|
558
|
+
if (n < 1000) return String(n);
|
|
559
|
+
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
560
|
+
if (n < 1000000) return `${Math.round(n / 1000)}k`;
|
|
561
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function agoShort(ms: number): string {
|
|
565
|
+
const sec = Math.max(0, Math.round(ms / 1000));
|
|
566
|
+
if (sec < 60) return "just now";
|
|
567
|
+
if (sec < 3600) return `${Math.round(sec / 60)}m ago`;
|
|
568
|
+
if (sec < 86400) return `${Math.round(sec / 3600)}h ago`;
|
|
569
|
+
return `${Math.round(sec / 86400)}d ago`;
|
|
570
|
+
}
|
|
571
|
+
|
|
554
572
|
/**
|
|
555
573
|
* Glance-style centered stats strip under the input:
|
|
556
|
-
* n Turn · n Steps | wall · tool wall | avg TTFT · n tok/s | cache n%
|
|
574
|
+
* n Turn · n Steps | wall · tool wall | avg TTFT · n tok/s | n compactions · 39k→13k · 3m ago | cache n%
|
|
557
575
|
*/
|
|
558
576
|
function renderSessionStrip(piContext: unknown): string | null {
|
|
559
577
|
const parts: string[] = [];
|
|
@@ -588,6 +606,21 @@ function renderSessionStrip(piContext: unknown): string | null {
|
|
|
588
606
|
if (seg) parts.push(seg);
|
|
589
607
|
}
|
|
590
608
|
|
|
609
|
+
// Compactions: hidden until the first one.
|
|
610
|
+
const branch = (() => {
|
|
611
|
+
try {
|
|
612
|
+
return ((piContext as any)?.sessionManager?.getBranch?.() ?? []) as unknown[];
|
|
613
|
+
} catch {
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
})();
|
|
617
|
+
const cmp = compactionSummary(branch);
|
|
618
|
+
if (cmp.count > 0) {
|
|
619
|
+
const sizes = cmp.before > 0 ? ` \u00b7 ${c(STRIP_COLOR.compact, `${fmtTokensShort(cmp.before)}\u2192${fmtTokensShort(cmp.after)}`)}` : "";
|
|
620
|
+
const age = cmp.lastAt != null ? ` \u00b7 ${agoShort(Date.now() - cmp.lastAt)}` : "";
|
|
621
|
+
parts.push(`${c(STRIP_COLOR.compact, String(cmp.count))} compaction${cmp.count === 1 ? "" : "s"}${sizes}${age}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
591
624
|
const hit = cacheHitPct(piContext);
|
|
592
625
|
if (hit !== null) {
|
|
593
626
|
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", "
|
|
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
|
-
"
|
|
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
|
],
|
package/src/rendering/icons.ts
CHANGED
|
@@ -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
|
|
2
|
+
* @pi-unipi/footer — Compactor segment
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* tokens_saved, compression_ratio, sandbox_runs, search_queries.
|
|
4
|
+
* One dense, icon-free segment, shown once the session has compacted:
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
/**
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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" },
|