@pi-unipi/footer 3.0.0-alpha.35 → 3.0.0-alpha.36
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 +1 -1
- package/package.json +3 -3
- package/src/config.ts +5 -2
- package/src/index.ts +72 -4
- package/src/process-line.ts +59 -0
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Open `/unipi:settings` → **Footer**. The file is `~/.unipi/config/footer/confi
|
|
|
63
63
|
| `iconStyle` | `nerd` | Icon set: `nerd` (needs a Nerd Font), `emoji` or `text`. |
|
|
64
64
|
| `colorMode` | `auto` | `auto`, `truecolor`, `256` or `none`. The legacy value `mono` loads as `none`. |
|
|
65
65
|
| `rainbow` | `always` | `always` animates the brand (and the whole frame at `xhigh`/`max` thinking); `brand-only` never animates the whole frame; `off` disables the animation. |
|
|
66
|
-
| `processLine` | `
|
|
66
|
+
| `processLine` | `false` | Shows an extra line above the input that counts background tasks by status. Background tasks live in the ↓ work tray. |
|
|
67
67
|
| `strip.turns` | `true` | Turn and step counters. |
|
|
68
68
|
| `strip.time` | `true` | Model time and tool time. |
|
|
69
69
|
| `strip.speed` | `true` | Average TTFT and tokens per second. |
|
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.36",
|
|
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.36",
|
|
36
|
+
"@pi-unipi/background-tasks": "3.0.0-alpha.36"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
package/src/config.ts
CHANGED
|
@@ -32,7 +32,10 @@ export const DEFAULT_FOOTER_SETTINGS: FooterSettings = {
|
|
|
32
32
|
iconStyle: "nerd",
|
|
33
33
|
colorMode: "auto",
|
|
34
34
|
rainbow: "always",
|
|
35
|
-
|
|
35
|
+
// Off by default: bg tasks + subagents live in the ↓ work tray; the only
|
|
36
|
+
// line above the input is the UNI-221 "Working…" one while wake-capable
|
|
37
|
+
// work is pending.
|
|
38
|
+
processLine: false,
|
|
36
39
|
strip: {
|
|
37
40
|
turns: true,
|
|
38
41
|
time: true,
|
|
@@ -118,7 +121,7 @@ registerSettings({
|
|
|
118
121
|
description: "Which frame parts get the animated rainbow.",
|
|
119
122
|
options: ["always", "brand-only", "off"],
|
|
120
123
|
},
|
|
121
|
-
{ key: "processLine", type: "boolean", label: "Background tasks line", description: "
|
|
124
|
+
{ key: "processLine", type: "boolean", label: "Background tasks line", description: "Extra one-liner above the input counting background tasks by status (off by default — they live in the ↓ work tray)." },
|
|
122
125
|
{ key: "strip.turns", type: "boolean", label: "Strip: turns", description: "Turn and step counters in the stats strip." },
|
|
123
126
|
{ key: "strip.time", type: "boolean", label: "Strip: time", description: "Model time and tool time in the stats strip." },
|
|
124
127
|
{ key: "strip.speed", type: "boolean", label: "Strip: speed", description: "Average time to first token and tokens per second." },
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,18 @@ import {
|
|
|
18
18
|
FOOTER_COMMANDS,
|
|
19
19
|
getPackageVersion,
|
|
20
20
|
findPackageRoot,
|
|
21
|
+
pendingWorkLabel,
|
|
22
|
+
workTrayItemCount,
|
|
21
23
|
} from "@pi-unipi/core";
|
|
24
|
+
|
|
25
|
+
/** The work tray has something to open with ↓ (hint on the Working line). */
|
|
26
|
+
function trayHasItems(): boolean {
|
|
27
|
+
try {
|
|
28
|
+
return workTrayItemCount() > 0;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
22
33
|
import { getFooterRegistry, type FooterRegistry } from "./registry/index.js";
|
|
23
34
|
import { loadFooterSettings, saveFooterSettings } from "./config.js";
|
|
24
35
|
import { registerCommands } from "./commands.js";
|
|
@@ -26,7 +37,7 @@ import { GlanceEditor } from "./glance-editor.js";
|
|
|
26
37
|
import type { GlanceStatus } from "./glance-editor.js";
|
|
27
38
|
import { tpsTracker } from "./tps-tracker.js";
|
|
28
39
|
import { publishSharedTps } from "./tps-shared.js";
|
|
29
|
-
import { renderProcessLine, countBgProcesses } from "./process-line.js";
|
|
40
|
+
import { renderProcessLine, countBgProcesses, renderWaitingLine } from "./process-line.js";
|
|
30
41
|
import { lhModeLabel } from "./segments/long-horizon.js";
|
|
31
42
|
import { SessionScanner } from "./session-scan.js";
|
|
32
43
|
import { renderSessionStrip, stripVisibleAtRows } from "./strip.js";
|
|
@@ -232,14 +243,63 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
|
|
|
232
243
|
// glance frame (the frame replaces the editor, so this aboveEditor slot sits
|
|
233
244
|
// right above the footer); the frame's own borders show
|
|
234
245
|
// branch/context/model/thinking.
|
|
235
|
-
ctx.ui.setWidget("footer-top", (tui,
|
|
246
|
+
ctx.ui.setWidget("footer-top", (tui, theme) => {
|
|
236
247
|
state.tuiRef = tui;
|
|
248
|
+
// UNI-221: animation state for the pending-work working line.
|
|
249
|
+
let waitingSince: number | undefined;
|
|
250
|
+
let spinTimer: ReturnType<typeof setInterval> | undefined;
|
|
251
|
+
const stopSpin = () => {
|
|
252
|
+
if (spinTimer) clearInterval(spinTimer);
|
|
253
|
+
spinTimer = undefined;
|
|
254
|
+
};
|
|
237
255
|
return {
|
|
238
|
-
dispose() {
|
|
256
|
+
dispose() {
|
|
257
|
+
stopSpin();
|
|
258
|
+
},
|
|
239
259
|
invalidate() {},
|
|
240
260
|
render(width: number): string[] {
|
|
241
261
|
const settings = loadFooterSettings();
|
|
242
|
-
if (!state.enabled || !
|
|
262
|
+
if (!state.enabled || !state.piContext || width <= 0) {
|
|
263
|
+
stopSpin();
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
// UNI-162 / UNI-221: while pi looks idle but wake-capable work is
|
|
267
|
+
// still pending (a background subagent, a bg wake, a fusion
|
|
268
|
+
// handoff) show a working line shaped like pi's own ("⠋ Working… ·
|
|
269
|
+
// bg: npm test · 12s") — the pane must not read as finished. It is
|
|
270
|
+
// a status, not part of the bg dots, so it ignores processLine/
|
|
271
|
+
// strip-hiding settings. It is the only bg/subagent line outside
|
|
272
|
+
// the ↓ work tray (the tray draws nothing while closed).
|
|
273
|
+
const ctx = state.piContext as { isIdle?: () => boolean } | undefined;
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
const frame = Math.floor(now / 80);
|
|
276
|
+
const fg = (color: string, text: string) => {
|
|
277
|
+
try {
|
|
278
|
+
return (theme as unknown as { fg: (c: string, t: string) => string }).fg(color, text);
|
|
279
|
+
} catch {
|
|
280
|
+
return text;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const waiting = ctx?.isIdle
|
|
284
|
+
? renderWaitingLine(width, () => ctx.isIdle!(), {
|
|
285
|
+
frame,
|
|
286
|
+
elapsedMs: waitingSince !== undefined ? now - waitingSince : 0,
|
|
287
|
+
spinner: (t) => fg("accent", t),
|
|
288
|
+
muted: (t) => fg("muted", t),
|
|
289
|
+
...(trayHasItems() ? { hint: "↓ view" } : {}),
|
|
290
|
+
})
|
|
291
|
+
: undefined;
|
|
292
|
+
if (waiting !== undefined) {
|
|
293
|
+
waitingSince ??= now;
|
|
294
|
+
if (!spinTimer) {
|
|
295
|
+
spinTimer = setInterval(() => tui.requestRender(), 80);
|
|
296
|
+
spinTimer.unref?.();
|
|
297
|
+
}
|
|
298
|
+
return [waiting];
|
|
299
|
+
}
|
|
300
|
+
waitingSince = undefined;
|
|
301
|
+
stopSpin();
|
|
302
|
+
if (!settings.processLine) return [];
|
|
243
303
|
if (!stripVisibleAtRows(terminalRows(tui))) return [];
|
|
244
304
|
return renderProcessLine(width);
|
|
245
305
|
},
|
|
@@ -340,6 +400,14 @@ function renderKey(state: FooterState): string {
|
|
|
340
400
|
state.activeToolCalls,
|
|
341
401
|
bg ? `${bg.running}/${bg.stopped}/${bg.failed}/${bg.done}` : "",
|
|
342
402
|
tpsTracker.getStepCount(),
|
|
403
|
+
// UNI-221: the pending-work working line appears/disappears with it.
|
|
404
|
+
(() => {
|
|
405
|
+
try {
|
|
406
|
+
return pendingWorkLabel() ?? "";
|
|
407
|
+
} catch {
|
|
408
|
+
return "";
|
|
409
|
+
}
|
|
410
|
+
})(),
|
|
343
411
|
// Rainbow brand animates with wall time — keep the 1s shimmer when on.
|
|
344
412
|
loadFooterSettings().rainbow !== "off" ? Math.floor(Date.now() / 1000) : "",
|
|
345
413
|
].join("|");
|
package/src/process-line.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
17
17
|
import { getSharedTaskRegistry } from "@pi-unipi/background-tasks";
|
|
18
|
+
import { pendingWorkLabel, WORKING_TITLE } from "@pi-unipi/core";
|
|
18
19
|
|
|
19
20
|
const GREEN_DOT = "\x1b[38;5;82m●\x1b[0m"; // running — active work
|
|
20
21
|
const YELLOW_DOT = "\x1b[38;5;220m●\x1b[0m"; // stopped (killed) — needs attention
|
|
@@ -76,3 +77,61 @@ export function renderProcessLine(width: number): string[] {
|
|
|
76
77
|
const leftPad = Math.floor((width - w) / 2);
|
|
77
78
|
return [" ".repeat(leftPad) + line];
|
|
78
79
|
}
|
|
80
|
+
|
|
81
|
+
/** pi's own working spinner frames (pi-tui Loader default). */
|
|
82
|
+
export const WORKING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
83
|
+
|
|
84
|
+
export interface WorkingLineStyle {
|
|
85
|
+
/** Colours the spinner frame (accent). */
|
|
86
|
+
spinner?: (text: string) => string;
|
|
87
|
+
/** Colours the detail text (muted). */
|
|
88
|
+
muted?: (text: string) => string;
|
|
89
|
+
/** Spinner frame index (animated by the caller). */
|
|
90
|
+
frame?: number;
|
|
91
|
+
/** Elapsed ms since the pending work was first seen idle. */
|
|
92
|
+
elapsedMs?: number;
|
|
93
|
+
/** Trailing quiet key hint (e.g. "↓ view" when the work tray has items). */
|
|
94
|
+
hint?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatElapsed(ms: number): string {
|
|
98
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
99
|
+
if (s < 60) return `${String(s)}s`;
|
|
100
|
+
const m = Math.floor(s / 60);
|
|
101
|
+
return `${String(m)}m ${String(s % 60)}s`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* UNI-162 / UNI-221: while pi is idle but a wait source still has a reason
|
|
106
|
+
* (a background subagent running, a bg task that will wake the agent, a
|
|
107
|
+
* non-blocking fusion handoff in flight) the pane must not read as finished.
|
|
108
|
+
* Renders a working line shaped like pi's own ("⠋ Working… · bg: npm test ·
|
|
109
|
+
* 12s") read straight off the arbiter's wait sources via core's
|
|
110
|
+
* `pendingWorkLabel()` (whichever package registered the wait source is the
|
|
111
|
+
* source of truth).
|
|
112
|
+
*
|
|
113
|
+
* It is the ONLY bg/subagent line outside the ↓ work tray (the tray draws
|
|
114
|
+
* nothing while closed; the status-dot one-liner is opt-in `processLine`).
|
|
115
|
+
*
|
|
116
|
+
* Returns `undefined` when pi is busy (pi shows its own working line) or
|
|
117
|
+
* nothing is pending (callers show their normal idle state).
|
|
118
|
+
*/
|
|
119
|
+
export function renderWaitingLine(width: number, isIdle: () => boolean, style: WorkingLineStyle = {}): string | undefined {
|
|
120
|
+
if (width <= 1) return undefined;
|
|
121
|
+
let idle: boolean;
|
|
122
|
+
try {
|
|
123
|
+
idle = isIdle();
|
|
124
|
+
} catch {
|
|
125
|
+
idle = true;
|
|
126
|
+
}
|
|
127
|
+
if (!idle) return undefined;
|
|
128
|
+
const label = pendingWorkLabel();
|
|
129
|
+
if (label === null) return undefined;
|
|
130
|
+
const spinner = style.spinner ?? ((t: string) => t);
|
|
131
|
+
const muted = style.muted ?? ((t: string) => t);
|
|
132
|
+
const frame = WORKING_FRAMES[Math.abs(style.frame ?? 0) % WORKING_FRAMES.length] ?? WORKING_FRAMES[0];
|
|
133
|
+
const elapsed = style.elapsedMs !== undefined ? ` · ${formatElapsed(style.elapsedMs)}` : "";
|
|
134
|
+
const hint = style.hint ? ` · ${style.hint}` : "";
|
|
135
|
+
const line = ` ${spinner(frame)} ${spinner(WORKING_TITLE)}${muted(` · ${label}${elapsed}${hint}`)}`;
|
|
136
|
+
return truncateToWidth(line, Math.max(1, width - 1));
|
|
137
|
+
}
|