@pi-unipi/footer 2.10.2 → 2.12.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 +20 -3
- package/package.json +2 -2
- package/src/commands.ts +9 -15
- package/src/config.ts +2 -0
- package/src/glance-editor.ts +285 -0
- package/src/help.ts +1 -1
- package/src/index.ts +360 -27
- package/src/presets.ts +16 -1
- package/src/rendering/icons.ts +8 -0
- package/src/rendering/theme.ts +2 -0
- package/src/segments/core.ts +114 -7
- package/src/tps-tracker.ts +529 -240
- package/src/tui/settings-tui.ts +18 -2
- package/src/types.ts +4 -0
package/src/segments/core.ts
CHANGED
|
@@ -87,14 +87,91 @@ function renderToolCountSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
|
87
87
|
return { content: color(ctx, "model", content), visible: true };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// ─── Git metadata (dirty / ahead / behind) ──────────────────────────────
|
|
91
|
+
//
|
|
92
|
+
// pi's ReadonlyFooterDataProvider only exposes getGitBranch(). For glance-style
|
|
93
|
+
// adornments we probe git ourselves: fire-and-forget async refresh, cached,
|
|
94
|
+
// at most one probe every GIT_PROBE_MS. Renders read the last known values.
|
|
95
|
+
|
|
96
|
+
const GIT_PROBE_MS = 2000;
|
|
97
|
+
|
|
98
|
+
interface GitMeta {
|
|
99
|
+
cwd: string;
|
|
100
|
+
at: number;
|
|
101
|
+
inFlight: boolean;
|
|
102
|
+
dirty: boolean | null;
|
|
103
|
+
ahead: number | null;
|
|
104
|
+
behind: number | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const gitMetaCache: GitMeta = {
|
|
108
|
+
cwd: "", at: 0, inFlight: false, dirty: null, ahead: null, behind: null,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function probeGitMeta(cwd: string): void {
|
|
112
|
+
if (gitMetaCache.inFlight) return;
|
|
113
|
+
gitMetaCache.inFlight = true;
|
|
114
|
+
void import("node:child_process").then(({ execFile }) => {
|
|
115
|
+
execFile(
|
|
116
|
+
"git",
|
|
117
|
+
["--no-optional-locks", "status", "--porcelain=v1", "--branch"],
|
|
118
|
+
{ cwd, timeout: 1500 },
|
|
119
|
+
(err, stdout) => {
|
|
120
|
+
gitMetaCache.inFlight = false;
|
|
121
|
+
gitMetaCache.at = Date.now();
|
|
122
|
+
if (err || !stdout) {
|
|
123
|
+
gitMetaCache.dirty = null;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let dirty = false;
|
|
127
|
+
let ahead = 0;
|
|
128
|
+
let behind = 0;
|
|
129
|
+
for (const line of stdout.split("\n")) {
|
|
130
|
+
if (line.startsWith("##")) {
|
|
131
|
+
const ab = line.match(/\[ahead (\d+)(?:,\s*behind (\d+))?\]|\[behind (\d+)\]/);
|
|
132
|
+
if (ab) {
|
|
133
|
+
ahead = Number(ab[1] ?? 0);
|
|
134
|
+
behind = Number(ab[2] ?? ab[3] ?? 0);
|
|
135
|
+
}
|
|
136
|
+
} else if (line.trim()) {
|
|
137
|
+
dirty = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
gitMetaCache.dirty = dirty;
|
|
141
|
+
gitMetaCache.ahead = ahead;
|
|
142
|
+
gitMetaCache.behind = behind;
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getGitMeta(cwd: string): { dirty: boolean | null; ahead: number | null; behind: number | null } {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
if (gitMetaCache.cwd !== cwd || now - gitMetaCache.at > GIT_PROBE_MS) {
|
|
151
|
+
gitMetaCache.cwd = cwd;
|
|
152
|
+
probeGitMeta(cwd);
|
|
153
|
+
}
|
|
154
|
+
return { dirty: gitMetaCache.dirty, ahead: gitMetaCache.ahead, behind: gitMetaCache.behind };
|
|
155
|
+
}
|
|
156
|
+
|
|
90
157
|
function renderGitSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
91
158
|
const footerData = ctx.footerData as any;
|
|
92
159
|
const branch = footerData?.getGitBranch?.() ?? null;
|
|
93
160
|
if (!branch) return { content: "", visible: false };
|
|
94
161
|
|
|
95
|
-
const
|
|
162
|
+
const piCtx = ctx.piContext as Record<string, unknown> | undefined;
|
|
163
|
+
const cwd = (piCtx?.sessionManager as any)?.getCwd?.() ?? (piCtx as any)?.cwd ?? process.cwd();
|
|
164
|
+
const meta = getGitMeta(String(cwd));
|
|
165
|
+
const isDirty = meta.dirty === true || (meta.dirty === null && (footerData?.getGitDirty?.() ?? false));
|
|
96
166
|
const semanticColor: SemanticColor = isDirty ? "gitDirty" : "gitClean";
|
|
97
|
-
|
|
167
|
+
|
|
168
|
+
// Glance-style adornments: * dirty, ↑N ahead, ↓N behind
|
|
169
|
+
let marks = "";
|
|
170
|
+
if (isDirty) marks += "*";
|
|
171
|
+
if ((meta.ahead ?? 0) > 0) marks += `↑${meta.ahead}`;
|
|
172
|
+
if ((meta.behind ?? 0) > 0) marks += `↓${meta.behind}`;
|
|
173
|
+
|
|
174
|
+
const content = withIcon("git", `${branch}${marks}`);
|
|
98
175
|
return { content: color(ctx, semanticColor, content), visible: true };
|
|
99
176
|
}
|
|
100
177
|
|
|
@@ -180,6 +257,18 @@ function renderHostnameSegment(_ctx: FooterSegmentContext): RenderedSegment {
|
|
|
180
257
|
return { content, visible: true };
|
|
181
258
|
}
|
|
182
259
|
|
|
260
|
+
function renderUniBrandSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
261
|
+
return { content: color(ctx, "brand", "UNI"), visible: true };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderDirectorySegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
265
|
+
const piCtx = ctx.piContext as Record<string, unknown> | undefined;
|
|
266
|
+
const cwd = (piCtx?.sessionManager as any)?.getCwd?.() ?? (piCtx as any)?.cwd ?? process.cwd();
|
|
267
|
+
const dir = String(cwd).split("/").filter(Boolean).pop() ?? "~";
|
|
268
|
+
const content = withIcon("directory", dir);
|
|
269
|
+
return { content: color(ctx, "directory", content), visible: true };
|
|
270
|
+
}
|
|
271
|
+
|
|
183
272
|
// ─── TPS tier color function ────────────────────────────────────────────────
|
|
184
273
|
|
|
185
274
|
function getTpsSemanticColor(tps: number): SemanticColor {
|
|
@@ -190,32 +279,48 @@ function getTpsSemanticColor(tps: number): SemanticColor {
|
|
|
190
279
|
return "tpsSlow";
|
|
191
280
|
}
|
|
192
281
|
|
|
282
|
+
/** Format a TTFT duration for display: "1.2s" or "350ms". */
|
|
283
|
+
function formatTtft(ms: number): string {
|
|
284
|
+
if (ms >= 10000) return `${Math.round(ms / 1000)}s`;
|
|
285
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
|
|
286
|
+
return `${ms}ms`;
|
|
287
|
+
}
|
|
288
|
+
|
|
193
289
|
function renderTpsSegment(ctx: FooterSegmentContext): RenderedSegment {
|
|
194
290
|
const streaming = tpsTracker.isStreaming();
|
|
195
291
|
const liveTps = tpsTracker.getLiveTps();
|
|
196
292
|
const avgTps = tpsTracker.getSessionAvgTps();
|
|
293
|
+
// Harness-style TTFT average; null until a full turn→first-word sample exists.
|
|
294
|
+
const avgTtft = tpsTracker.getAvgTtftMs();
|
|
197
295
|
|
|
198
296
|
// No data yet — hide
|
|
199
|
-
if (!tpsTracker.getTotalOutput()) return { content: "", visible: false };
|
|
297
|
+
if (!tpsTracker.getTotalOutput() && avgTtft === null) return { content: "", visible: false };
|
|
200
298
|
|
|
201
299
|
const icon = getIcon("tps");
|
|
202
300
|
|
|
301
|
+
const ttftPart = avgTtft !== null ? ` TTFT ${formatTtft(avgTtft)} \u00b7` : "";
|
|
302
|
+
|
|
203
303
|
if (streaming && liveTps > 0) {
|
|
204
|
-
// Active generation: show live rate + avg
|
|
304
|
+
// Active generation: show live rate + avg + ttft
|
|
205
305
|
const liveDisplay = Math.round(liveTps);
|
|
206
306
|
const avgDisplay = Math.round(avgTps);
|
|
207
307
|
const liveText = `\u2191 ${liveDisplay} T/S`;
|
|
208
308
|
const avgText = `AVG ${avgDisplay}`;
|
|
209
309
|
const liveColored = applyColor(getTpsSemanticColor(liveTps), liveText, ctx.theme, ctx.colors);
|
|
210
|
-
const avgColored = applyColor("tpsIdle", avgText, ctx.theme, ctx.colors);
|
|
310
|
+
const avgColored = applyColor("tpsIdle", `${ttftPart} ${avgText}`.trimStart(), ctx.theme, ctx.colors);
|
|
211
311
|
const content = icon ? `${icon} ${liveColored} \u00b7 ${avgColored}` : `${liveColored} \u00b7 ${avgColored}`;
|
|
212
312
|
return { content, visible: true };
|
|
213
313
|
}
|
|
214
314
|
|
|
215
|
-
// Idle: show session average
|
|
315
|
+
// Idle: show session average (or just TTFT when nothing else yet)
|
|
316
|
+
if (!tpsTracker.getTotalOutput()) {
|
|
317
|
+
const text = `TTFT ${formatTtft(avgTtft ?? 0)}`;
|
|
318
|
+
const colored = applyColor("tpsIdle", text, ctx.theme, ctx.colors);
|
|
319
|
+
return { content: icon ? `${icon} ${colored}` : colored, visible: true };
|
|
320
|
+
}
|
|
216
321
|
const avgDisplay = Math.round(avgTps);
|
|
217
322
|
const avgText = `AVG ${avgDisplay} T/S`;
|
|
218
|
-
const avgColored = applyColor("tpsIdle", avgText, ctx.theme, ctx.colors);
|
|
323
|
+
const avgColored = applyColor("tpsIdle", `${ttftPart} ${avgText}`.trimStart(), ctx.theme, ctx.colors);
|
|
219
324
|
const content = icon ? `${icon} ${avgColored}` : avgColored;
|
|
220
325
|
return { content, visible: true };
|
|
221
326
|
}
|
|
@@ -297,6 +402,8 @@ export const CORE_SEGMENTS: FooterSegment[] = [
|
|
|
297
402
|
{ id: "tokens_out", label: "Tokens Out", shortLabel: "TOUT", description: "Output tokens generated", zone: "center", render: renderTokensSegment("out"), defaultShow: false },
|
|
298
403
|
{ id: "session", label: "Session", shortLabel: "SES", description: "Session identifier", zone: "left", render: renderSessionSegment, defaultShow: false },
|
|
299
404
|
{ id: "hostname", label: "Hostname", shortLabel: "HST", description: "Machine hostname", zone: "left", render: renderHostnameSegment, defaultShow: false },
|
|
405
|
+
{ id: "uni", label: "Unipi", shortLabel: "UNI", description: "Unipi brand mark", zone: "left", render: renderUniBrandSegment, defaultShow: true },
|
|
406
|
+
{ id: "directory", label: "Directory", shortLabel: "DIR", description: "Current directory name", zone: "left", render: renderDirectorySegment, defaultShow: true },
|
|
300
407
|
{ id: "clock", label: "Clock", shortLabel: "CLK", description: "Current wall time (HH:MM:SS)", zone: "right", render: renderClockSegment, defaultShow: true },
|
|
301
408
|
{ id: "duration", label: "Duration", shortLabel: "DUR", description: "Session duration", zone: "right", render: renderDurationSegment, defaultShow: true },
|
|
302
409
|
{ id: "thinking_level", label: "Thinking", shortLabel: "THK", description: "Current model thinking level", zone: "center", render: renderThinkingLevelSegment, defaultShow: false },
|