@pi-unipi/footer 2.12.1 → 2.13.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 CHANGED
@@ -18,6 +18,7 @@ An experimental input surface, on by default and toggleable in `/unipi:footer-se
18
18
  - **Top border:** animated lolcat-gradient UNIPI brand + git branch (turns rainbow-frame animated while thinking is max/xhigh)
19
19
  - **Bottom border:** workspace · context %/window · model · thinking level
20
20
  - **Session strip:** turns/steps, wall + tool wall time, average TTFT, tok/s, cache hit % — colored per stat, honest across restarts (derived from persisted session timestamps when live hooks are unavailable; provider-reported `usage.output` anchors token counts whenever present)
21
+ - **Process line (new in 2.13):** centered one-liner directly above the frame while background work is in flight — `● 3 running ● 1 stopped ● 1 failed ● 2 done` — green ● running, yellow ● stopped (killed), red ● failed, gray ● done. Covers every task type (shell jobs, delegates, fusion workflows) via direct registry reads; zero-count buckets are omitted and the line hides when idle. Counts reset per session.
21
22
  - The classic segment status line is suppressed while glance mode is on; toggle it back for the classic footer
22
23
 
23
24
  ## Commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.12.1",
3
+ "version": "2.13.0",
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,7 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.12.0"
35
+ "@pi-unipi/core": "2.12.0",
36
+ "@pi-unipi/background-tasks": "2.13.0"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ import { STATUS_EXT_SEGMENTS } from "./segments/status-ext.js";
29
29
 
30
30
  import type { FooterGroup, FooterSegment } from "./types.js";
31
31
  import { tpsTracker } from "./tps-tracker.js";
32
+ import { renderProcessLine } from "./process-line.js";
32
33
 
33
34
  /** All segment groups */
34
35
  const ALL_GROUPS: FooterGroup[] = [
@@ -328,9 +329,10 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
328
329
  };
329
330
  });
330
331
 
331
- // Top row widget — classic status line (suppressed in glance mode; the
332
- // glance frame's top border already shows UNIPI branch and the bottom
333
- // border shows context/model/thinking, so a segment row would duplicate it)
332
+ // Top row widget — dual role. Classic mode: status segment line. Glance
333
+ // mode: the bg-process one-liner, rendered directly above the glance frame
334
+ // (the frame replaces the editor, so this aboveEditor slot sits right above
335
+ // the footer); the frame's own borders show branch/context/model/thinking.
334
336
  ctx.ui.setWidget("footer-top", (_tui, theme) => {
335
337
  // Update the renderer's theme-like
336
338
  const themeLike = { fg: (color: string, text: string) => theme.fg(color as any, text) };
@@ -344,8 +346,8 @@ function setupFooterUI(pi: ExtensionAPI, ctx: ExtensionContext, state: FooterSta
344
346
  },
345
347
  render(width: number): string[] {
346
348
  if (!state.enabled || !state.piContext || width <= 0) return [];
347
- // Glance mode replaces the classic segment line entirely.
348
- if (state.glanceMode) return [];
349
+ // Glance mode: this slot becomes the bg-process one-liner.
350
+ if (state.glanceMode) return renderProcessLine(width);
349
351
  const layout = state.renderer.computeLayout(width);
350
352
  if (!layout.topContent) return [];
351
353
 
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @pi-unipi/footer — Background process one-liner
3
+ *
4
+ * Glance-mode strip rendered above the footer frame: one colored dot + count
5
+ * per background-task status. Reads DIRECTLY from the
6
+ * @pi-unipi/background-tasks shared registry (no events, no polling
7
+ * channels); re-renders on the footer's existing 1s refresh timer.
8
+ *
9
+ * Dot → status mapping:
10
+ * green ● running yellow ● stopped (killed) red ● failed gray ● done (completed)
11
+ *
12
+ * Buckets with zero count are omitted; with nothing in flight the line is
13
+ * empty so the footer stays clean.
14
+ */
15
+
16
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
17
+ import { getSharedTaskRegistry } from "@pi-unipi/background-tasks";
18
+
19
+ const GREEN_DOT = "\x1b[38;5;82m●\x1b[0m"; // running — active work
20
+ const YELLOW_DOT = "\x1b[38;5;220m●\x1b[0m"; // stopped (killed) — needs attention
21
+ const RED_DOT = "\x1b[38;5;196m●\x1b[0m"; // failed — needs attention
22
+ const GRAY_DOT = "\x1b[38;5;245m●\x1b[0m"; // done (completed) — idle info
23
+
24
+ export interface BgProcessCounts {
25
+ running: number;
26
+ stopped: number;
27
+ failed: number;
28
+ done: number;
29
+ }
30
+
31
+ /**
32
+ * Count background tasks by display status straight from the registry.
33
+ * Returns null when background-tasks has not published a registry (module
34
+ * disabled, before first load, or after session shutdown).
35
+ */
36
+ export function countBgProcesses(): BgProcessCounts | null {
37
+ try {
38
+ const tasks = getSharedTaskRegistry()?.allTasks();
39
+ if (!tasks) return null;
40
+ const counts: BgProcessCounts = { running: 0, stopped: 0, failed: 0, done: 0 };
41
+ for (const task of tasks) {
42
+ if (task.status === "running") counts.running++;
43
+ else if (task.status === "killed") counts.stopped++;
44
+ else if (task.status === "failed") counts.failed++;
45
+ else if (task.status === "completed") counts.done++;
46
+ }
47
+ return counts;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Render the centered one-liner for the given terminal width.
55
+ * Returns [] when there is nothing to show.
56
+ */
57
+ export function renderProcessLine(width: number): string[] {
58
+ if (width <= 0) return [];
59
+ const counts = countBgProcesses();
60
+ if (!counts) return [];
61
+
62
+ const parts: string[] = [];
63
+ if (counts.running > 0) parts.push(`${GREEN_DOT} ${counts.running} running`);
64
+ if (counts.stopped > 0) parts.push(`${YELLOW_DOT} ${counts.stopped} stopped`);
65
+ if (counts.failed > 0) parts.push(`${RED_DOT} ${counts.failed} failed`);
66
+ if (counts.done > 0) parts.push(`${GRAY_DOT} ${counts.done} done`);
67
+ if (parts.length === 0) return [];
68
+
69
+ const line = parts.join(" ");
70
+ const w = visibleWidth(line);
71
+ if (w >= width) return [truncateToWidth(line, width)];
72
+ const leftPad = Math.floor((width - w) / 2);
73
+ return [" ".repeat(leftPad) + line];
74
+ }