@pi-unipi/footer 0.1.3 → 2.0.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/README.md +73 -158
- package/package.json +1 -1
- package/src/commands.ts +38 -123
- package/src/config.ts +16 -0
- package/src/help.ts +160 -0
- package/src/index.ts +48 -15
- package/src/presets.ts +41 -32
- package/src/registry/index.ts +1 -1
- package/src/rendering/icons.ts +77 -59
- package/src/rendering/renderer.ts +246 -80
- package/src/rendering/theme.ts +56 -29
- package/src/segments/compactor.ts +76 -30
- package/src/segments/core.ts +124 -15
- package/src/segments/kanboard.ts +25 -9
- package/src/segments/mcp.ts +25 -16
- package/src/segments/memory.ts +9 -6
- package/src/segments/notify.ts +16 -5
- package/src/segments/ralph.ts +23 -8
- package/src/segments/status-ext.ts +1 -1
- package/src/segments/workflow.ts +41 -18
- package/src/tps-tracker.ts +204 -0
- package/src/tui/settings-tui.ts +253 -63
- package/src/types.ts +51 -12
package/src/rendering/theme.ts
CHANGED
|
@@ -8,39 +8,66 @@
|
|
|
8
8
|
import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import type { ColorScheme, ColorValue, SemanticColor, ThemeLike } from "../types.js";
|
|
10
10
|
|
|
11
|
+
/** Wrap text in dim ANSI codes for muted placeholder display */
|
|
12
|
+
export function mutedPlaceholder(text: string): string {
|
|
13
|
+
return `\x1b[2m${text}\x1b[0m`;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
/** Default semantic-to-theme-color mapping */
|
|
12
|
-
const DEFAULT_COLOR_MAP: Record<SemanticColor, ThemeColor
|
|
13
|
-
|
|
17
|
+
const DEFAULT_COLOR_MAP: Record<SemanticColor, ThemeColor | `#${string}`> = {
|
|
18
|
+
// ── Model & Identity (Left zone) ──
|
|
19
|
+
model: "#c792ea", // Soft purple — model name
|
|
14
20
|
path: "text",
|
|
15
|
-
git: "
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
workflowBrainstorm: "
|
|
24
|
-
workflowPlan: "
|
|
25
|
-
workflowWork: "
|
|
26
|
-
workflowReview: "
|
|
27
|
-
workflowAuto: "
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
git: "#82cc6f", // Green (clean default)
|
|
22
|
+
gitClean: "#82cc6f", // Green — clean branch
|
|
23
|
+
gitDirty: "#e5c07b", // Amber — dirty branch
|
|
24
|
+
session: "#61afef", // Blue — session name
|
|
25
|
+
worktree: "#61afef", // Blue — worktree indicator
|
|
26
|
+
// ── Workflow (Left zone) ──
|
|
27
|
+
workflow: "#c792ea", // Purple (default)
|
|
28
|
+
workflowNone: "#4a6a7a", // Muted teal — idle
|
|
29
|
+
workflowBrainstorm: "#e06c75", // Red
|
|
30
|
+
workflowPlan: "#d19a66", // Orange
|
|
31
|
+
workflowWork: "#e5c07b", // Yellow
|
|
32
|
+
workflowReview: "#82cc6f", // Green
|
|
33
|
+
workflowAuto: "#c792ea", // Purple
|
|
34
|
+
workflowDebug: "#e06c75", // Red
|
|
35
|
+
workflowChoreExec: "#d19a66", // Orange
|
|
36
|
+
workflowOther: "#c792ea", // Purple
|
|
37
|
+
// ── TPS tiers (Center zone) ──
|
|
38
|
+
tpsSlow: "#e06c75", // Red — < 30 t/s
|
|
39
|
+
tpsModerate: "#e5c07b", // Amber — 30-50 t/s
|
|
40
|
+
tpsGood: "#56d4bc", // Teal — 50-100 t/s
|
|
41
|
+
tpsFast: "#82cc6f", // Green — 100-200 t/s
|
|
42
|
+
tpsBlazing: "#c792ea", // Purple — > 200 t/s
|
|
43
|
+
tpsIdle: "#4a6a7a", // Muted teal — session avg when idle
|
|
44
|
+
// ── Metrics (Center zone) ──
|
|
45
|
+
compactor: "#56b6c2", // Cyan — compaction stats
|
|
46
|
+
memory: "#61afef", // Blue — memory count
|
|
47
|
+
mcp: "#82cc6f", // Green — MCP status
|
|
48
|
+
ralph: "#e5c07b", // Amber — ralph loops
|
|
49
|
+
ralphOn: "#82cc6c", // Green — ralph active
|
|
50
|
+
ralphOff: "#e06c75", // Red — ralph inactive
|
|
51
|
+
kanboard: "#c678dd", // Purple — kanboard
|
|
52
|
+
notify: "#56b6c2", // Cyan — notifications
|
|
53
|
+
context: "muted", // Theme token for OK context
|
|
54
|
+
contextWarn: "#e5c07b", // Amber — context 70-90%
|
|
55
|
+
contextError: "#e06c75", // Red — context > 90%
|
|
56
|
+
cost: "#d19a66", // Gold — cost
|
|
57
|
+
tokens: "#abb2bf", // Silver — token counts
|
|
58
|
+
// ── Time (Right zone) ──
|
|
59
|
+
clock: "#abb2bf", // Silver — wall clock
|
|
60
|
+
duration: "#61afef", // Blue — session duration
|
|
61
|
+
// ── Thinking levels ──
|
|
62
|
+
thinking: "#61afef",
|
|
63
|
+
thinkingMinimal: "#56b6c2", // Cyan
|
|
64
|
+
thinkingLow: "#61afef", // Blue
|
|
65
|
+
thinkingMedium: "#c792ea", // Purple
|
|
66
|
+
thinkingHigh: "#d19a66", // Gold
|
|
67
|
+
thinkingXhigh: "#e06c75", // Red
|
|
68
|
+
// ── UI chrome ──
|
|
31
69
|
separator: "dim",
|
|
32
70
|
border: "dim",
|
|
33
|
-
context: "muted",
|
|
34
|
-
contextWarn: "warning",
|
|
35
|
-
contextError: "error",
|
|
36
|
-
cost: "text",
|
|
37
|
-
tokens: "muted",
|
|
38
|
-
thinking: "accent",
|
|
39
|
-
thinkingMinimal: "thinkingMinimal",
|
|
40
|
-
thinkingLow: "thinkingLow",
|
|
41
|
-
thinkingMedium: "thinkingMedium",
|
|
42
|
-
thinkingHigh: "thinkingHigh",
|
|
43
|
-
thinkingXhigh: "thinkingXhigh",
|
|
44
71
|
};
|
|
45
72
|
|
|
46
73
|
/**
|
|
@@ -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,
|
|
5
|
+
* tokens_saved, compression_ratio, cocoindex_status, 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)
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
|
|
13
|
-
import { applyColor } from "../rendering/theme.js";
|
|
13
|
+
import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
|
|
14
14
|
import { getIcon } from "../rendering/icons.js";
|
|
15
|
+
import { isSegmentEnabled } from "../config.js";
|
|
15
16
|
|
|
16
17
|
function withIcon(segmentId: string, text: string): string {
|
|
17
18
|
const icon = getIcon(segmentId);
|
|
@@ -50,7 +51,12 @@ function getSessionEvents(ctx: FooterSegmentContext): any[] {
|
|
|
50
51
|
function renderSessionEventsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
51
52
|
const events = getSessionEvents(ctx);
|
|
52
53
|
const count = events.length;
|
|
53
|
-
if (count === 0)
|
|
54
|
+
if (count === 0) {
|
|
55
|
+
if (isSegmentEnabled("compactor", "session_events")) {
|
|
56
|
+
return { content: mutedPlaceholder(withIcon("sessionEvents", "0")), visible: true };
|
|
57
|
+
}
|
|
58
|
+
return hidden();
|
|
59
|
+
}
|
|
54
60
|
|
|
55
61
|
const content = withIcon("sessionEvents", `${count}`);
|
|
56
62
|
return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
|
|
@@ -66,22 +72,32 @@ function renderCompactionsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
66
72
|
compactionCount++;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
|
-
if (compactionCount === 0)
|
|
75
|
+
if (compactionCount === 0) {
|
|
76
|
+
if (isSegmentEnabled("compactor", "compactions")) {
|
|
77
|
+
return { content: mutedPlaceholder(withIcon("compactions", "0")), visible: true };
|
|
78
|
+
}
|
|
79
|
+
return hidden();
|
|
80
|
+
}
|
|
70
81
|
|
|
71
82
|
const content = withIcon("compactions", `${compactionCount}`);
|
|
72
83
|
return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
function renderTokensSavedSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
76
|
-
// Sum tokens saved from compaction
|
|
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.
|
|
77
90
|
const events = getSessionEvents(ctx);
|
|
78
91
|
let tokensSaved = 0;
|
|
79
92
|
let hasCompaction = false;
|
|
80
93
|
for (const e of events) {
|
|
81
94
|
if (!e || typeof e !== "object") continue;
|
|
82
|
-
if (e.type === "compaction"
|
|
95
|
+
if (e.type === "compaction") {
|
|
83
96
|
hasCompaction = true;
|
|
84
|
-
|
|
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);
|
|
85
101
|
}
|
|
86
102
|
}
|
|
87
103
|
if (!hasCompaction || tokensSaved === 0) return hidden();
|
|
@@ -91,45 +107,75 @@ function renderTokensSavedSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
function renderCompressionRatioSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
94
|
-
//
|
|
110
|
+
// Calculate compression ratio from Pi's CompactionEntry.tokensBefore.
|
|
111
|
+
// Compaction keeps ~12% of context, giving ~8:1 compression.
|
|
95
112
|
const events = getSessionEvents(ctx);
|
|
96
|
-
let
|
|
113
|
+
let totalBefore = 0;
|
|
114
|
+
let totalAfter = 0;
|
|
97
115
|
for (const e of events) {
|
|
98
116
|
if (!e || typeof e !== "object") continue;
|
|
99
|
-
if (e.type === "compaction"
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
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);
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
|
-
if (
|
|
125
|
+
if (totalBefore === 0 || totalAfter === 0) return hidden();
|
|
107
126
|
|
|
108
|
-
const
|
|
127
|
+
const ratio = totalBefore / totalAfter;
|
|
128
|
+
const content = withIcon("compressionRatio", `${ratio.toFixed(1)}x`);
|
|
109
129
|
return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
function
|
|
113
|
-
//
|
|
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.
|
|
114
135
|
return hidden();
|
|
115
136
|
}
|
|
116
137
|
|
|
117
|
-
function renderSandboxRunsSegment(
|
|
118
|
-
//
|
|
119
|
-
|
|
138
|
+
function renderSandboxRunsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
139
|
+
// Count sandbox events from session manager branch
|
|
140
|
+
const events = getSessionEvents(ctx);
|
|
141
|
+
let sandboxCount = 0;
|
|
142
|
+
for (const e of events) {
|
|
143
|
+
if (!e || typeof e !== "object") continue;
|
|
144
|
+
// Count tool calls that are sandbox/execute tools
|
|
145
|
+
const name = String((e as any).name ?? "").toLowerCase();
|
|
146
|
+
if (name.includes("sandbox") || name.includes("ctx_execute") || name === "execute") {
|
|
147
|
+
sandboxCount++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (sandboxCount === 0) return hidden();
|
|
151
|
+
|
|
152
|
+
const content = withIcon("sandboxRuns", `${sandboxCount}`);
|
|
153
|
+
return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
|
|
120
154
|
}
|
|
121
155
|
|
|
122
|
-
function renderSearchQueriesSegment(
|
|
123
|
-
//
|
|
124
|
-
|
|
156
|
+
function renderSearchQueriesSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
157
|
+
// Count search events from session manager branch
|
|
158
|
+
const events = getSessionEvents(ctx);
|
|
159
|
+
let searchCount = 0;
|
|
160
|
+
for (const e of events) {
|
|
161
|
+
if (!e || typeof e !== "object") continue;
|
|
162
|
+
const name = String((e as any).name ?? "").toLowerCase();
|
|
163
|
+
if (name.includes("search") || name.includes("ctx_search")) {
|
|
164
|
+
searchCount++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (searchCount === 0) return hidden();
|
|
168
|
+
|
|
169
|
+
const content = withIcon("searchQueries", `${searchCount}`);
|
|
170
|
+
return { content: applyColor("compactor", content, ctx.theme, ctx.colors), visible: true };
|
|
125
171
|
}
|
|
126
172
|
|
|
127
173
|
export const COMPACTOR_SEGMENTS: FooterSegment[] = [
|
|
128
|
-
{ id: "session_events", label: "Session Events", icon: "", render: renderSessionEventsSegment, defaultShow: true },
|
|
129
|
-
{ id: "compactions", label: "Compactions", icon: "", render: renderCompactionsSegment, defaultShow: true },
|
|
130
|
-
{ id: "tokens_saved", label: "Tokens Saved", icon: "", render: renderTokensSavedSegment, defaultShow: true },
|
|
131
|
-
{ id: "compression_ratio", label: "Compression Ratio", icon: "", render: renderCompressionRatioSegment, defaultShow: false },
|
|
132
|
-
{ id: "
|
|
133
|
-
{ id: "sandbox_runs", label: "Sandbox Runs", icon: "", render: renderSandboxRunsSegment, defaultShow: false },
|
|
134
|
-
{ id: "search_queries", label: "Search Queries", icon: "", render: renderSearchQueriesSegment, defaultShow: false },
|
|
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 },
|
|
135
181
|
];
|
package/src/segments/core.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { hostname as osHostname } from "node:os";
|
|
|
10
10
|
import type { FooterSegment, FooterSegmentContext, RenderedSegment, SemanticColor } from "../types.js";
|
|
11
11
|
import { applyColor } from "../rendering/theme.js";
|
|
12
12
|
import { getIcon } from "../rendering/icons.js";
|
|
13
|
+
import { tpsTracker } from "../tps-tracker.js";
|
|
13
14
|
|
|
14
15
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
15
16
|
|
|
@@ -130,8 +131,10 @@ function renderGitSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
130
131
|
const branch = footerData?.getGitBranch?.() ?? null;
|
|
131
132
|
if (!branch) return { content: "", visible: false };
|
|
132
133
|
|
|
134
|
+
const isDirty = footerData?.getGitDirty?.() ?? false;
|
|
135
|
+
const semanticColor: SemanticColor = isDirty ? "gitDirty" : "gitClean";
|
|
133
136
|
const content = withIcon("git", branch);
|
|
134
|
-
return { content: color(ctx,
|
|
137
|
+
return { content: color(ctx, semanticColor, content), visible: true };
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
function renderContextPctSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
@@ -173,7 +176,8 @@ function renderCostSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
173
176
|
if (!stats.cost && !usingSubscription) return { content: "", visible: false };
|
|
174
177
|
|
|
175
178
|
const costDisplay = usingSubscription ? "(sub)" : `$${stats.cost.toFixed(2)}`;
|
|
176
|
-
|
|
179
|
+
const content = withIcon("cost", costDisplay);
|
|
180
|
+
return { content: color(ctx, "cost", content), visible: true };
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
function renderTokensSegment(variant: "total" | "in" | "out"): (ctx: FooterSegmentContext) => RenderedSegment {
|
|
@@ -206,7 +210,7 @@ function renderSessionSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
206
210
|
const sessionId = (piCtx?.sessionManager as any)?.getSessionId?.();
|
|
207
211
|
const display = sessionId?.slice(0, 8) || "new";
|
|
208
212
|
const content = withIcon("session", display);
|
|
209
|
-
return { content: color(ctx, "
|
|
213
|
+
return { content: color(ctx, "session", content), visible: true };
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
function renderHostnameSegment(_ctx: FooterSegmentContext): RenderedSegment {
|
|
@@ -224,19 +228,124 @@ function renderTimeSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
224
228
|
return { content, visible: true };
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
// ─── TPS tier color function ────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
function getTpsSemanticColor(tps: number): SemanticColor {
|
|
234
|
+
if (tps > 200) return "tpsBlazing";
|
|
235
|
+
if (tps > 100) return "tpsFast";
|
|
236
|
+
if (tps > 50) return "tpsGood";
|
|
237
|
+
if (tps > 30) return "tpsModerate";
|
|
238
|
+
return "tpsSlow";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function renderTpsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
242
|
+
const streaming = tpsTracker.isStreaming();
|
|
243
|
+
const liveTps = tpsTracker.getLiveTps();
|
|
244
|
+
const avgTps = tpsTracker.getSessionAvgTps();
|
|
245
|
+
|
|
246
|
+
// No data yet — hide
|
|
247
|
+
if (!tpsTracker.getTotalOutput()) return { content: "", visible: false };
|
|
248
|
+
|
|
249
|
+
const icon = getIcon("tps");
|
|
250
|
+
|
|
251
|
+
if (streaming && liveTps > 0) {
|
|
252
|
+
// Active generation: show live rate + avg
|
|
253
|
+
const liveDisplay = Math.round(liveTps);
|
|
254
|
+
const avgDisplay = Math.round(avgTps);
|
|
255
|
+
const liveText = `\u2191 ${liveDisplay} T/S`;
|
|
256
|
+
const avgText = `AVG ${avgDisplay}`;
|
|
257
|
+
const liveColored = applyColor(getTpsSemanticColor(liveTps), liveText, ctx.theme, ctx.colors);
|
|
258
|
+
const avgColored = applyColor("tpsIdle", avgText, ctx.theme, ctx.colors);
|
|
259
|
+
const content = icon ? `${icon} ${liveColored} \u00b7 ${avgColored}` : `${liveColored} \u00b7 ${avgColored}`;
|
|
260
|
+
return { content, visible: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Idle: show session average
|
|
264
|
+
const avgDisplay = Math.round(avgTps);
|
|
265
|
+
const avgText = `AVG ${avgDisplay} T/S`;
|
|
266
|
+
const avgColored = applyColor("tpsIdle", avgText, ctx.theme, ctx.colors);
|
|
267
|
+
const content = icon ? `${icon} ${avgColored}` : avgColored;
|
|
268
|
+
return { content, visible: true };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function renderClockSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
272
|
+
const now = new Date();
|
|
273
|
+
const h = now.getHours().toString().padStart(2, "0");
|
|
274
|
+
const m = now.getMinutes().toString().padStart(2, "0");
|
|
275
|
+
const s = now.getSeconds().toString().padStart(2, "0");
|
|
276
|
+
const timeStr = `${h}:${m}:${s}`;
|
|
277
|
+
const content = withIcon("clock", timeStr);
|
|
278
|
+
return { content: color(ctx, "clock", content), visible: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function renderDurationSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
282
|
+
// Derive session duration from sessionManager
|
|
283
|
+
const piCtx = ctx.piContext as Record<string, unknown> | undefined;
|
|
284
|
+
const sessionStart = (piCtx?.sessionManager as any)?.getSessionStartTime?.();
|
|
285
|
+
if (!sessionStart) {
|
|
286
|
+
// Fallback: show current time segment style
|
|
287
|
+
return { content: "", visible: false };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const elapsedMs = Date.now() - sessionStart;
|
|
291
|
+
const totalSeconds = Math.floor(elapsedMs / 1000);
|
|
292
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
293
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
294
|
+
const seconds = totalSeconds % 60;
|
|
295
|
+
|
|
296
|
+
let display: string;
|
|
297
|
+
if (hours > 0) {
|
|
298
|
+
display = `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
299
|
+
} else {
|
|
300
|
+
display = `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const content = withIcon("duration", display);
|
|
304
|
+
return { content: color(ctx, "duration", content), visible: true };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ─── Thinking level ──────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
/** Map thinking level to semantic color */
|
|
310
|
+
function getThinkingSemanticColor(level: string | undefined): SemanticColor {
|
|
311
|
+
switch (level) {
|
|
312
|
+
case "minimal": return "thinkingMinimal";
|
|
313
|
+
case "low": return "thinkingLow";
|
|
314
|
+
case "medium": return "thinkingMedium";
|
|
315
|
+
case "high": return "thinkingHigh";
|
|
316
|
+
case "xhigh": return "thinkingXhigh";
|
|
317
|
+
default: return "thinking";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function renderThinkingLevelSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
322
|
+
const piCtx = ctx.piContext as Record<string, unknown> | undefined;
|
|
323
|
+
const model = piCtx?.model as Record<string, unknown> | undefined;
|
|
324
|
+
const thinkingLevel = model?.thinkingLevel as string | undefined;
|
|
325
|
+
|
|
326
|
+
if (!thinkingLevel || thinkingLevel === "off") return { content: "", visible: false };
|
|
327
|
+
|
|
328
|
+
const semanticColor = getThinkingSemanticColor(thinkingLevel);
|
|
329
|
+
const content = withIcon("thinkingLevel", thinkingLevel);
|
|
330
|
+
return { content: color(ctx, semanticColor, content), visible: true };
|
|
331
|
+
}
|
|
332
|
+
|
|
227
333
|
// ─── Core segments array ────────────────────────────────────────────────────
|
|
228
334
|
|
|
229
335
|
export const CORE_SEGMENTS: FooterSegment[] = [
|
|
230
|
-
{ id: "model", label: "Model", icon: "", render: renderModelSegment, defaultShow: true },
|
|
231
|
-
{ id: "api_state", label: "
|
|
232
|
-
{ id: "tool_count", label: "Tool Count", icon: "", render: renderToolCountSegment, defaultShow: true },
|
|
233
|
-
{ id: "git", label: "Git", icon: "", render: renderGitSegment, defaultShow: true },
|
|
234
|
-
{ id: "
|
|
235
|
-
{ id: "
|
|
236
|
-
{ id: "
|
|
237
|
-
{ id: "
|
|
238
|
-
{ id: "
|
|
239
|
-
{ id: "
|
|
240
|
-
{ id: "
|
|
241
|
-
{ id: "
|
|
336
|
+
{ id: "model", label: "Model", shortLabel: "MDL", description: "Current model name", zone: "left", icon: "", render: renderModelSegment, defaultShow: true },
|
|
337
|
+
{ id: "api_state", label: "API", shortLabel: "API", description: "API connection state", zone: "left", icon: "", render: renderApiStateSegment, defaultShow: true },
|
|
338
|
+
{ id: "tool_count", label: "Tool Count", shortLabel: "TLS", description: "Number of tools available", zone: "left", icon: "", render: renderToolCountSegment, defaultShow: true },
|
|
339
|
+
{ id: "git", label: "Git", shortLabel: "GIT", description: "Current git branch + dirty/clean status", zone: "left", icon: "", render: renderGitSegment, defaultShow: true },
|
|
340
|
+
{ id: "tps", label: "TPS", shortLabel: "TPS", description: "Tokens per second \u2014 live during generation", zone: "center", icon: "", render: renderTpsSegment, defaultShow: true },
|
|
341
|
+
{ id: "context_pct", label: "Context %", shortLabel: "CTX", description: "Context window usage percentage", zone: "center", icon: "", render: renderContextPctSegment, defaultShow: true },
|
|
342
|
+
{ id: "cost", label: "Cost", shortLabel: "CST", description: "Session cost in USD", zone: "center", icon: "", render: renderCostSegment, defaultShow: true },
|
|
343
|
+
{ id: "tokens_total", label: "Tokens Total", shortLabel: "TOK", description: "Total tokens used this session", zone: "center", icon: "", render: renderTokensSegment("total"), defaultShow: false },
|
|
344
|
+
{ id: "tokens_in", label: "Tokens In", shortLabel: "TIN", description: "Input tokens consumed", zone: "center", icon: "", render: renderTokensSegment("in"), defaultShow: false },
|
|
345
|
+
{ id: "tokens_out", label: "Tokens Out", shortLabel: "TOUT", description: "Output tokens generated", zone: "center", icon: "", render: renderTokensSegment("out"), defaultShow: false },
|
|
346
|
+
{ id: "session", label: "Session", shortLabel: "SES", description: "Session identifier", zone: "left", icon: "", render: renderSessionSegment, defaultShow: false },
|
|
347
|
+
{ id: "hostname", label: "Hostname", shortLabel: "HST", description: "Machine hostname", zone: "left", icon: "", render: renderHostnameSegment, defaultShow: false },
|
|
348
|
+
{ id: "clock", label: "Clock", shortLabel: "CLK", description: "Current wall time (HH:MM:SS)", zone: "right", icon: "", render: renderClockSegment, defaultShow: true },
|
|
349
|
+
{ id: "duration", label: "Duration", shortLabel: "DUR", description: "Session duration", zone: "right", icon: "", render: renderDurationSegment, defaultShow: true },
|
|
350
|
+
{ id: "thinking_level", label: "Thinking", shortLabel: "THK", description: "Current model thinking level", zone: "center", icon: "", render: renderThinkingLevelSegment, defaultShow: false },
|
|
242
351
|
];
|
package/src/segments/kanboard.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
|
|
10
|
-
import { applyColor } from "../rendering/theme.js";
|
|
10
|
+
import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
|
|
11
11
|
import { getIcon } from "../rendering/icons.js";
|
|
12
|
+
import { isSegmentEnabled } from "../config.js";
|
|
12
13
|
|
|
13
14
|
function withIcon(segmentId: string, text: string): string {
|
|
14
15
|
const icon = getIcon(segmentId);
|
|
@@ -21,7 +22,7 @@ function withIcon(segmentId: string, text: string): string {
|
|
|
21
22
|
*/
|
|
22
23
|
function getKanboardData(): Record<string, unknown> | null {
|
|
23
24
|
try {
|
|
24
|
-
const registry =
|
|
25
|
+
const registry = globalThis.__unipi_kanboard_registry;
|
|
25
26
|
if (!registry || typeof registry !== "object") return null;
|
|
26
27
|
return registry as Record<string, unknown>;
|
|
27
28
|
} catch {
|
|
@@ -32,7 +33,12 @@ function getKanboardData(): Record<string, unknown> | null {
|
|
|
32
33
|
function renderDocsCountSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
33
34
|
const kb = getKanboardData();
|
|
34
35
|
const value = kb?.docsCount;
|
|
35
|
-
if (value === undefined || value === null)
|
|
36
|
+
if (value === undefined || value === null) {
|
|
37
|
+
if (isSegmentEnabled("kanboard", "docs_count")) {
|
|
38
|
+
return { content: mutedPlaceholder(withIcon("docsCount", "0")), visible: true };
|
|
39
|
+
}
|
|
40
|
+
return { content: "", visible: false };
|
|
41
|
+
}
|
|
36
42
|
const content = withIcon("docsCount", `${value}`);
|
|
37
43
|
return { content: applyColor("kanboard", content, ctx.theme, ctx.colors), visible: true };
|
|
38
44
|
}
|
|
@@ -40,7 +46,12 @@ function renderDocsCountSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
40
46
|
function renderTasksDoneSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
41
47
|
const kb = getKanboardData();
|
|
42
48
|
const value = kb?.tasksDone;
|
|
43
|
-
if (value === undefined || value === null)
|
|
49
|
+
if (value === undefined || value === null) {
|
|
50
|
+
if (isSegmentEnabled("kanboard", "tasks_done")) {
|
|
51
|
+
return { content: mutedPlaceholder(withIcon("tasksDone", "0")), visible: true };
|
|
52
|
+
}
|
|
53
|
+
return { content: "", visible: false };
|
|
54
|
+
}
|
|
44
55
|
const content = withIcon("tasksDone", `${value}`);
|
|
45
56
|
return { content: applyColor("kanboard", content, ctx.theme, ctx.colors), visible: true };
|
|
46
57
|
}
|
|
@@ -48,7 +59,12 @@ function renderTasksDoneSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
48
59
|
function renderTasksTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
49
60
|
const kb = getKanboardData();
|
|
50
61
|
const value = kb?.tasksTotal;
|
|
51
|
-
if (value === undefined || value === null)
|
|
62
|
+
if (value === undefined || value === null) {
|
|
63
|
+
if (isSegmentEnabled("kanboard", "tasks_total")) {
|
|
64
|
+
return { content: mutedPlaceholder(withIcon("tasksTotal", "0")), visible: true };
|
|
65
|
+
}
|
|
66
|
+
return { content: "", visible: false };
|
|
67
|
+
}
|
|
52
68
|
const content = withIcon("tasksTotal", `${value}`);
|
|
53
69
|
return { content: applyColor("kanboard", content, ctx.theme, ctx.colors), visible: true };
|
|
54
70
|
}
|
|
@@ -68,8 +84,8 @@ function renderTaskPctSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
export const KANBOARD_SEGMENTS: FooterSegment[] = [
|
|
71
|
-
{ id: "docs_count", label: "Docs
|
|
72
|
-
{ id: "tasks_done", label: "
|
|
73
|
-
{ id: "tasks_total", label: "
|
|
74
|
-
{ id: "task_pct", label: "Task
|
|
87
|
+
{ id: "docs_count", label: "Docs", shortLabel: "DOC", description: "Workflow documents count", zone: "center", icon: "", render: renderDocsCountSegment, defaultShow: true },
|
|
88
|
+
{ id: "tasks_done", label: "Done", shortLabel: "DNE", description: "Completed tasks", zone: "center", icon: "", render: renderTasksDoneSegment, defaultShow: true },
|
|
89
|
+
{ id: "tasks_total", label: "Total", shortLabel: "TSK", description: "Total tasks", zone: "center", icon: "", render: renderTasksTotalSegment, defaultShow: true },
|
|
90
|
+
{ id: "task_pct", label: "Progress", shortLabel: "PCT", description: "Task completion percentage", zone: "center", icon: "", render: renderTaskPctSegment, defaultShow: true },
|
|
75
91
|
];
|
package/src/segments/mcp.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
|
|
16
|
-
import { applyColor } from "../rendering/theme.js";
|
|
16
|
+
import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
|
|
17
17
|
import { getIcon } from "../rendering/icons.js";
|
|
18
|
+
import { isSegmentEnabled } from "../config.js";
|
|
18
19
|
|
|
19
20
|
/** Shape of aggregate MCP stats from globalThis or registry */
|
|
20
21
|
interface McpStats {
|
|
@@ -24,14 +25,6 @@ interface McpStats {
|
|
|
24
25
|
toolsTotal?: number;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/** Shape of the global escape-hatch object */
|
|
28
|
-
interface GlobalMcpStats extends McpStats {}
|
|
29
|
-
|
|
30
|
-
declare global {
|
|
31
|
-
// eslint-disable-next-line no-var
|
|
32
|
-
var __unipi_mcp_stats: GlobalMcpStats | undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
28
|
function withIcon(segmentId: string, text: string): string {
|
|
36
29
|
const icon = getIcon(segmentId);
|
|
37
30
|
return icon ? `${icon} ${text}` : text;
|
|
@@ -64,21 +57,37 @@ function hasUsefulValue(value: unknown): value is number {
|
|
|
64
57
|
|
|
65
58
|
function renderServersTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
66
59
|
const stats = getMcpStats(ctx);
|
|
67
|
-
if (!hasUsefulValue(stats.serversTotal))
|
|
60
|
+
if (!hasUsefulValue(stats.serversTotal)) {
|
|
61
|
+
if (isSegmentEnabled("mcp", "servers_total")) {
|
|
62
|
+
return { content: mutedPlaceholder(withIcon("serversTotal", "0")), visible: true };
|
|
63
|
+
}
|
|
64
|
+
return { content: "", visible: false };
|
|
65
|
+
}
|
|
68
66
|
const content = withIcon("serversTotal", `${stats.serversTotal}`);
|
|
69
67
|
return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
function renderServersActiveSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
73
71
|
const stats = getMcpStats(ctx);
|
|
74
|
-
if (!hasUsefulValue(stats.serversActive))
|
|
72
|
+
if (!hasUsefulValue(stats.serversActive)) {
|
|
73
|
+
if (isSegmentEnabled("mcp", "servers_active")) {
|
|
74
|
+
const total = stats.serversTotal ?? 0;
|
|
75
|
+
return { content: mutedPlaceholder(withIcon("serversActive", `${total}/0`)), visible: true };
|
|
76
|
+
}
|
|
77
|
+
return { content: "", visible: false };
|
|
78
|
+
}
|
|
75
79
|
const content = withIcon("serversActive", `${stats.serversActive}`);
|
|
76
80
|
return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
function renderToolsTotalSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
80
84
|
const stats = getMcpStats(ctx);
|
|
81
|
-
if (!hasUsefulValue(stats.toolsTotal))
|
|
85
|
+
if (!hasUsefulValue(stats.toolsTotal)) {
|
|
86
|
+
if (isSegmentEnabled("mcp", "tools_total")) {
|
|
87
|
+
return { content: mutedPlaceholder(withIcon("toolsTotal", "0")), visible: true };
|
|
88
|
+
}
|
|
89
|
+
return { content: "", visible: false };
|
|
90
|
+
}
|
|
82
91
|
const content = withIcon("toolsTotal", `${stats.toolsTotal}`);
|
|
83
92
|
return { content: applyColor("mcp", content, ctx.theme, ctx.colors), visible: true };
|
|
84
93
|
}
|
|
@@ -93,8 +102,8 @@ function renderServersFailedSegment(ctx: FooterSegmentContext): RenderedSegment
|
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
export const MCP_SEGMENTS: FooterSegment[] = [
|
|
96
|
-
{ id: "servers_total", label: "Servers Total", icon: "", render: renderServersTotalSegment, defaultShow: true },
|
|
97
|
-
{ id: "servers_active", label: "
|
|
98
|
-
{ id: "tools_total", label: "Tools Total", icon: "", render: renderToolsTotalSegment, defaultShow: true },
|
|
99
|
-
{ id: "servers_failed", label: "
|
|
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 },
|
|
100
109
|
];
|
package/src/segments/memory.ts
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import type { FooterSegment, FooterSegmentContext, RenderedSegment } from "../types.js";
|
|
21
|
-
import { applyColor } from "../rendering/theme.js";
|
|
21
|
+
import { applyColor, mutedPlaceholder } from "../rendering/theme.js";
|
|
22
22
|
import { getIcon } from "../rendering/icons.js";
|
|
23
|
+
import { isSegmentEnabled } from "../config.js";
|
|
23
24
|
|
|
24
25
|
function withIcon(segmentId: string, text: string): string {
|
|
25
26
|
const icon = getIcon(segmentId);
|
|
@@ -50,8 +51,7 @@ interface InfoRegistryLike {
|
|
|
50
51
|
*/
|
|
51
52
|
function getInfoRegistryMemoryData(): InfoMemoryData | null {
|
|
52
53
|
try {
|
|
53
|
-
const
|
|
54
|
-
const registry = g.__unipi_info_registry;
|
|
54
|
+
const registry = globalThis.__unipi_info_registry;
|
|
55
55
|
if (!registry || typeof registry !== "object") return null;
|
|
56
56
|
|
|
57
57
|
// The info registry exposes getCachedData(groupId) synchronously
|
|
@@ -81,6 +81,9 @@ function getMemoryCounts(): { project: number | null; total: number | null } {
|
|
|
81
81
|
function renderProjectCountSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
82
82
|
const counts = getMemoryCounts();
|
|
83
83
|
if (counts.project === null) {
|
|
84
|
+
if (isSegmentEnabled("memory", "project_count")) {
|
|
85
|
+
return { content: mutedPlaceholder(withIcon("projectCount", "0")), visible: true };
|
|
86
|
+
}
|
|
84
87
|
return { content: "", visible: false };
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -136,7 +139,7 @@ function renderConsolidationsSegment(ctx: FooterSegmentContext): RenderedSegment
|
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
export const MEMORY_SEGMENTS: FooterSegment[] = [
|
|
139
|
-
{ id: "project_count", label: "Project
|
|
140
|
-
{ id: "total_count", label: "Total
|
|
141
|
-
{ id: "consolidations", label: "Consolidations", icon: "", render: renderConsolidationsSegment, defaultShow: false },
|
|
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
145
|
];
|