@pi-unipi/unipi 2.12.2 → 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/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.13.0] — 2026-08-28
10
+
11
+ ### Added
12
+
13
+ - `footer`: **bg-process one-liner above the glance footer** — a centered strip directly above the frame showing live background-task counts with status dots: green ● running, yellow ● stopped (killed), red ● failed, gray ● done. Zero-count buckets are omitted and the line hides entirely when idle. Reads `BackgroundTaskRegistry.allTasks()` directly (no events, no polling channels) and re-renders on the footer's 1s refresh timer; counts reset per session.
14
+ - `background-tasks`: **shared registry accessor** (`getSharedTaskRegistry` / `setSharedTaskRegistry` / `clearSharedTaskRegistry` in `src/registry-shared.ts`) — lets sibling extensions read the live task registry synchronously. Stored on `globalThis` under a `Symbol.for` key so the singleton survives duplicate module instances; published at extension init and `session_start`, cleared on `session_shutdown`.
15
+
16
+ ### Changed
17
+
18
+ - `footer`: new dependency on `@pi-unipi/background-tasks` for the process one-liner's direct registry reads.
19
+
9
20
  ## [2.12.0] — 2026-08-27
10
21
 
11
22
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.12.2",
3
+ "version": "2.13.0",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -83,12 +83,12 @@
83
83
  },
84
84
  "dependencies": {
85
85
  "@pi-unipi/ask-user": "2.12.0",
86
- "@pi-unipi/background-tasks": "2.12.0",
86
+ "@pi-unipi/background-tasks": "2.13.0",
87
87
  "@pi-unipi/btw": "2.12.0",
88
88
  "@pi-unipi/command-enchantment": "2.12.0",
89
89
  "@pi-unipi/compactor": "2.12.0",
90
90
  "@pi-unipi/core": "2.12.0",
91
- "@pi-unipi/footer": "2.12.1",
91
+ "@pi-unipi/footer": "2.13.0",
92
92
  "@pi-unipi/image": "2.12.0",
93
93
  "@pi-unipi/info-screen": "2.12.0",
94
94
  "@pi-unipi/input-shortcuts": "2.12.0",
@@ -74,6 +74,22 @@ subscription OAuth attribution, exact-match system-prompt sanitization, and
74
74
  cache-retention policy for Anthropic routes. Duplicate installed copies resolve
75
75
  ownership through an EventBus claim; later copies go inert.
76
76
 
77
+ ## Shared registry (for sibling extensions)
78
+
79
+ Other packages can read the live task registry synchronously — no events, no polling:
80
+
81
+ ```ts
82
+ import { getSharedTaskRegistry } from "@pi-unipi/background-tasks";
83
+
84
+ const tasks = getSharedTaskRegistry()?.allTasks() ?? [];
85
+ const running = tasks.filter((t) => t.status === "running").length;
86
+ ```
87
+
88
+ The registry is published on `globalThis` under a `Symbol.for` key at extension
89
+ init and `session_start`, and cleared on `session_shutdown` (counts are
90
+ per-session). Returns `undefined` when the module is disabled — callers must
91
+ treat that as "no data", e.g. the footer's glance process line does.
92
+
77
93
  ## Differences from the reference
78
94
 
79
95
  - Commands live in the `/unipi:*` namespace; env prefix is `UNIPI_BG_*`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "Background tasks for UniPi — durable shell jobs, delegated agents, attested Pi runs, and fixed-purpose Fusion workflows",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -19,10 +19,14 @@ import {
19
19
  import { registerToolsAndCommands } from "./tools.js";
20
20
  import { registerFusionExtension } from "./fusion-extension.js";
21
21
  import { registerDelegateExtension } from "./delegate-extension.js";
22
+ import { setSharedTaskRegistry, clearSharedTaskRegistry } from "./registry-shared.js";
22
23
  import { taskDisplayName, type BgTask, type StartAttestedPiTaskOptions, type StartTaskOptions } from "./types.js";
23
24
 
24
25
  const STATUS_INTERVAL_MS = 1000;
25
26
 
27
+ // Direct synchronous access for sibling extensions (footer process one-liner).
28
+ export { getSharedTaskRegistry } from "./registry-shared.js";
29
+
26
30
  export default function backgroundTasksExtension(pi: ExtensionAPI): void {
27
31
  const { config, warnings } = loadBackgroundTasksConfig(process.cwd());
28
32
 
@@ -53,6 +57,8 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
53
57
  eventService.publishTerminal(task);
54
58
  },
55
59
  });
60
+ setSharedTaskRegistry(registry);
61
+
56
62
  const eventService: BackgroundTaskExtensionService = installBackgroundTaskExtensionApi({
57
63
  events: pi.events,
58
64
  registry,
@@ -253,6 +259,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
253
259
 
254
260
  pi.on("session_start", async (_event, ctx) => {
255
261
  registry.setShuttingDown(false);
262
+ setSharedTaskRegistry(registry);
256
263
  currentCtx = ctx;
257
264
  await registry.ensureRuntimeDir(ctx);
258
265
  updateUi(ctx);
@@ -264,6 +271,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
264
271
 
265
272
  pi.on("session_shutdown", async (_event, ctx) => {
266
273
  registry.setShuttingDown(true);
274
+ clearSharedTaskRegistry();
267
275
  currentCtx = undefined;
268
276
  if (statusInterval) {
269
277
  clearInterval(statusInterval);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared BackgroundTaskRegistry accessor
3
+ *
4
+ * Exposes the live registry to sibling extensions (e.g. @pi-unipi/footer)
5
+ * without events or request/response channels — direct synchronous reads of
6
+ * `allTasks()`.
7
+ *
8
+ * Stored on globalThis under a `Symbol.for` key so the singleton is shared
9
+ * even if this package ends up instantiated more than once (duplicate
10
+ * node_modules copies would otherwise each hold their own module state).
11
+ */
12
+
13
+ import type { BackgroundTaskRegistry } from "./registry.js";
14
+
15
+ const SHARED_REGISTRY_KEY = Symbol.for("unipi.background-tasks.shared-registry");
16
+
17
+ /** Publish the live registry (idempotent; later calls overwrite). */
18
+ export function setSharedTaskRegistry(registry: BackgroundTaskRegistry): void {
19
+ (globalThis as unknown as Record<symbol, unknown>)[SHARED_REGISTRY_KEY] = registry;
20
+ }
21
+
22
+ /** Read the live registry, or undefined when background-tasks is not loaded. */
23
+ export function getSharedTaskRegistry(): BackgroundTaskRegistry | undefined {
24
+ return (globalThis as unknown as Record<symbol, unknown>)[SHARED_REGISTRY_KEY] as
25
+ | BackgroundTaskRegistry
26
+ | undefined;
27
+ }
28
+
29
+ /** Drop the shared reference (used on session shutdown so readers see a clean slate). */
30
+ export function clearSharedTaskRegistry(): void {
31
+ delete (globalThis as unknown as Record<symbol, unknown>)[SHARED_REGISTRY_KEY];
32
+ }
@@ -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
@@ -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",
@@ -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
+ }
@@ -34743,6 +34743,18 @@ ${verified.answer}`), details };
34743
34743
  });
34744
34744
  }
34745
34745
 
34746
+ // packages/background-tasks/src/registry-shared.ts
34747
+ var SHARED_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("unipi.background-tasks.shared-registry");
34748
+ function setSharedTaskRegistry(registry3) {
34749
+ globalThis[SHARED_REGISTRY_KEY] = registry3;
34750
+ }
34751
+ function getSharedTaskRegistry() {
34752
+ return globalThis[SHARED_REGISTRY_KEY];
34753
+ }
34754
+ function clearSharedTaskRegistry() {
34755
+ delete globalThis[SHARED_REGISTRY_KEY];
34756
+ }
34757
+
34746
34758
  // packages/background-tasks/src/index.ts
34747
34759
  init_types();
34748
34760
  var STATUS_INTERVAL_MS2 = 1e3;
@@ -34771,6 +34783,7 @@ function backgroundTasksExtension(pi) {
34771
34783
  eventService.publishTerminal(task);
34772
34784
  }
34773
34785
  });
34786
+ setSharedTaskRegistry(registry3);
34774
34787
  const eventService = installBackgroundTaskExtensionApi({
34775
34788
  events: pi.events,
34776
34789
  registry: registry3,
@@ -34951,6 +34964,7 @@ ${task.outputPath}`, "info");
34951
34964
  });
34952
34965
  pi.on("session_start", async (_event, ctx) => {
34953
34966
  registry3.setShuttingDown(false);
34967
+ setSharedTaskRegistry(registry3);
34954
34968
  currentCtx = ctx;
34955
34969
  await registry3.ensureRuntimeDir(ctx);
34956
34970
  updateUi(ctx);
@@ -34961,6 +34975,7 @@ ${task.outputPath}`, "info");
34961
34975
  });
34962
34976
  pi.on("session_shutdown", async (_event, ctx) => {
34963
34977
  registry3.setShuttingDown(true);
34978
+ clearSharedTaskRegistry();
34964
34979
  currentCtx = void 0;
34965
34980
  if (statusInterval) {
34966
34981
  clearInterval(statusInterval);
@@ -51419,7 +51434,7 @@ function compactorExtension(pi) {
51419
51434
 
51420
51435
  // packages/footer/src/index.ts
51421
51436
  init_core();
51422
- import { truncateToWidth as truncateToWidth22, visibleWidth as visibleWidth19 } from "@earendil-works/pi-tui";
51437
+ import { truncateToWidth as truncateToWidth23, visibleWidth as visibleWidth20 } from "@earendil-works/pi-tui";
51423
51438
 
51424
51439
  // packages/footer/src/registry/index.ts
51425
51440
  var FooterRegistry = class {
@@ -54880,6 +54895,45 @@ var STATUS_EXT_SEGMENTS = [
54880
54895
  { id: "extension_statuses", label: "Extensions", shortLabel: "EXT", description: "Extension statuses overview", zone: "center", render: renderExtensionStatusesSegment, defaultShow: true }
54881
54896
  ];
54882
54897
 
54898
+ // packages/footer/src/process-line.ts
54899
+ import { truncateToWidth as truncateToWidth22, visibleWidth as visibleWidth19 } from "@earendil-works/pi-tui";
54900
+ var GREEN_DOT2 = "\x1B[38;5;82m\u25CF\x1B[0m";
54901
+ var YELLOW_DOT = "\x1B[38;5;220m\u25CF\x1B[0m";
54902
+ var RED_DOT2 = "\x1B[38;5;196m\u25CF\x1B[0m";
54903
+ var GRAY_DOT = "\x1B[38;5;245m\u25CF\x1B[0m";
54904
+ function countBgProcesses() {
54905
+ try {
54906
+ const tasks = getSharedTaskRegistry()?.allTasks();
54907
+ if (!tasks) return null;
54908
+ const counts = { running: 0, stopped: 0, failed: 0, done: 0 };
54909
+ for (const task of tasks) {
54910
+ if (task.status === "running") counts.running++;
54911
+ else if (task.status === "killed") counts.stopped++;
54912
+ else if (task.status === "failed") counts.failed++;
54913
+ else if (task.status === "completed") counts.done++;
54914
+ }
54915
+ return counts;
54916
+ } catch {
54917
+ return null;
54918
+ }
54919
+ }
54920
+ function renderProcessLine(width) {
54921
+ if (width <= 0) return [];
54922
+ const counts = countBgProcesses();
54923
+ if (!counts) return [];
54924
+ const parts = [];
54925
+ if (counts.running > 0) parts.push(`${GREEN_DOT2} ${counts.running} running`);
54926
+ if (counts.stopped > 0) parts.push(`${YELLOW_DOT} ${counts.stopped} stopped`);
54927
+ if (counts.failed > 0) parts.push(`${RED_DOT2} ${counts.failed} failed`);
54928
+ if (counts.done > 0) parts.push(`${GRAY_DOT} ${counts.done} done`);
54929
+ if (parts.length === 0) return [];
54930
+ const line = parts.join(" ");
54931
+ const w = visibleWidth19(line);
54932
+ if (w >= width) return [truncateToWidth22(line, width)];
54933
+ const leftPad = Math.floor((width - w) / 2);
54934
+ return [" ".repeat(leftPad) + line];
54935
+ }
54936
+
54883
54937
  // packages/footer/src/index.ts
54884
54938
  var ALL_GROUPS = [
54885
54939
  { id: "core", name: "Core", segments: CORE_SEGMENTS, defaultShow: true },
@@ -55095,11 +55149,11 @@ function setupFooterUI(pi, ctx, state2) {
55095
55149
  },
55096
55150
  render(width) {
55097
55151
  if (!state2.enabled || !state2.piContext || width <= 0) return [];
55098
- if (state2.glanceMode) return [];
55152
+ if (state2.glanceMode) return renderProcessLine(width);
55099
55153
  const layout = state2.renderer.computeLayout(width);
55100
55154
  if (!layout.topContent) return [];
55101
55155
  const line = layout.topContent;
55102
- return [visibleWidth19(line) > width ? truncateToWidth22(line, width) : line];
55156
+ return [visibleWidth20(line) > width ? truncateToWidth23(line, width) : line];
55103
55157
  }
55104
55158
  };
55105
55159
  }, { placement: "aboveEditor" });
@@ -55114,8 +55168,8 @@ function setupFooterUI(pi, ctx, state2) {
55114
55168
  if (!state2.enabled || !state2.glanceMode || !state2.piContext || width <= 0) return [];
55115
55169
  const strip = renderSessionStrip(state2.piContext);
55116
55170
  if (!strip) return [];
55117
- const w = visibleWidth19(strip);
55118
- if (w >= width) return [truncateToWidth22(strip, width)];
55171
+ const w = visibleWidth20(strip);
55172
+ if (w >= width) return [truncateToWidth23(strip, width)];
55119
55173
  const leftPad = Math.floor((width - w) / 2);
55120
55174
  return [" ".repeat(leftPad) + strip];
55121
55175
  }
@@ -55420,9 +55474,9 @@ function renderMarkdown(text, width, theme) {
55420
55474
 
55421
55475
  // packages/updater/src/tui/list-detail-overlay.ts
55422
55476
  init_core();
55423
- import { Key as Key16, matchesKey as matchesKey22, truncateToWidth as truncateToWidth23, visibleWidth as visibleWidth20 } from "@earendil-works/pi-tui";
55477
+ import { Key as Key16, matchesKey as matchesKey22, truncateToWidth as truncateToWidth24, visibleWidth as visibleWidth21 } from "@earendil-works/pi-tui";
55424
55478
  function padVisible2(content, targetWidth) {
55425
- const vw = visibleWidth20(content);
55479
+ const vw = visibleWidth21(content);
55426
55480
  const pad = Math.max(0, targetWidth - vw);
55427
55481
  return content + " ".repeat(pad);
55428
55482
  }
@@ -55463,7 +55517,7 @@ function createListDetailOverlay(config) {
55463
55517
  lines.push(theme.fg("accent", `\u251C${"\u2500".repeat(innerWidth)}\u2524`));
55464
55518
  const footer = state2.view === "list" ? config.listFooter : config.detailFooter;
55465
55519
  lines.push(
55466
- theme.fg("accent", "\u2502") + padVisible2(truncateToWidth23(footer, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
55520
+ theme.fg("accent", "\u2502") + padVisible2(truncateToWidth24(footer, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
55467
55521
  );
55468
55522
  lines.push(theme.fg("accent", `\u2570${"\u2500".repeat(innerWidth)}\u256F`));
55469
55523
  return lines;
@@ -55490,7 +55544,7 @@ function createListDetailOverlay(config) {
55490
55544
  const line = config.renderItem(entry, selected, theme);
55491
55545
  lines.push(
55492
55546
  theme.fg("accent", "\u2502") + padVisible2(
55493
- selected ? theme.bg("selectedBg", truncateToWidth23(line, innerWidth)) : truncateToWidth23(line, innerWidth),
55547
+ selected ? theme.bg("selectedBg", truncateToWidth24(line, innerWidth)) : truncateToWidth24(line, innerWidth),
55494
55548
  innerWidth
55495
55549
  ) + theme.fg("accent", "\u2502")
55496
55550
  );
@@ -55506,7 +55560,7 @@ function createListDetailOverlay(config) {
55506
55560
  }
55507
55561
  const title = config.renderDetailTitle(entry, theme);
55508
55562
  lines.push(
55509
- theme.fg("accent", "\u2502") + padVisible2(truncateToWidth23(` ${title}`, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
55563
+ theme.fg("accent", "\u2502") + padVisible2(truncateToWidth24(` ${title}`, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
55510
55564
  );
55511
55565
  lines.push(
55512
55566
  theme.fg("accent", "\u2502") + padVisible2("", innerWidth) + theme.fg("accent", "\u2502")
@@ -55518,7 +55572,7 @@ function createListDetailOverlay(config) {
55518
55572
  const visible = bodyLines.slice(state2.detailScroll, state2.detailScroll + 15);
55519
55573
  for (const line of visible) {
55520
55574
  lines.push(
55521
- theme.fg("accent", "\u2502") + padVisible2(truncateToWidth23(` ${line}`, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
55575
+ theme.fg("accent", "\u2502") + padVisible2(truncateToWidth24(` ${line}`, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
55522
55576
  );
55523
55577
  }
55524
55578
  };
@@ -55777,7 +55831,7 @@ function renderChangelogOverlay() {
55777
55831
  }
55778
55832
 
55779
55833
  // packages/updater/src/tui/settings-overlay.ts
55780
- import { Key as Key17, matchesKey as matchesKey23, truncateToWidth as truncateToWidth24 } from "@earendil-works/pi-tui";
55834
+ import { Key as Key17, matchesKey as matchesKey23, truncateToWidth as truncateToWidth25 } from "@earendil-works/pi-tui";
55781
55835
 
55782
55836
  // packages/updater/src/settings.ts
55783
55837
  init_core();
@@ -55854,14 +55908,14 @@ function renderSettingsOverlay2() {
55854
55908
  const modeOptions = getAutoUpdateOptions();
55855
55909
  const render = (width) => {
55856
55910
  const lines = [];
55857
- lines.push(truncateToWidth24(` ${BOLD}\u2699 Updater Settings${RESET2}`, width));
55911
+ lines.push(truncateToWidth25(` ${BOLD}\u2699 Updater Settings${RESET2}`, width));
55858
55912
  lines.push("\u2500".repeat(width));
55859
55913
  lines.push("");
55860
55914
  const intervalLabel = getIntervalLabel(state2.config.checkIntervalMs);
55861
55915
  const row0Selected = state2.row === 0;
55862
55916
  const row0Prefix = row0Selected ? `${TEAL}\u25B8${RESET2} ` : " ";
55863
55917
  lines.push(
55864
- truncateToWidth24(
55918
+ truncateToWidth25(
55865
55919
  ` ${row0Prefix}${BOLD}Check Interval${RESET2} ${DIM}${intervalLabel}${RESET2}`,
55866
55920
  width
55867
55921
  )
@@ -55870,13 +55924,13 @@ function renderSettingsOverlay2() {
55870
55924
  const active = opt.ms === state2.config.checkIntervalMs;
55871
55925
  return active ? `${GREEN}\u25CF ${opt.label}${RESET2}` : `${DIM}\u25CB ${opt.label}${RESET2}`;
55872
55926
  }).join(" ");
55873
- lines.push(truncateToWidth24(` ${intervalLine}`, width));
55927
+ lines.push(truncateToWidth25(` ${intervalLine}`, width));
55874
55928
  lines.push("");
55875
55929
  const modeLabel = state2.config.autoUpdate;
55876
55930
  const row1Selected = state2.row === 1;
55877
55931
  const row1Prefix = row1Selected ? `${TEAL}\u25B8${RESET2} ` : " ";
55878
55932
  lines.push(
55879
- truncateToWidth24(
55933
+ truncateToWidth25(
55880
55934
  ` ${row1Prefix}${BOLD}Auto Update${RESET2} ${DIM}${modeLabel}${RESET2}`,
55881
55935
  width
55882
55936
  )
@@ -55885,11 +55939,11 @@ function renderSettingsOverlay2() {
55885
55939
  const active = mode === state2.config.autoUpdate;
55886
55940
  return active ? `${GREEN}\u25CF ${mode}${RESET2}` : `${DIM}\u25CB ${mode}${RESET2}`;
55887
55941
  }).join(" ");
55888
- lines.push(truncateToWidth24(` ${modeLine}`, width));
55942
+ lines.push(truncateToWidth25(` ${modeLine}`, width));
55889
55943
  lines.push("");
55890
55944
  lines.push("\u2500".repeat(width));
55891
55945
  lines.push(
55892
- truncateToWidth24(
55946
+ truncateToWidth25(
55893
55947
  ` j/k: navigate Space: cycle ${GREEN}Enter: save${RESET2} ${DIM}Esc: cancel${RESET2}`,
55894
55948
  width
55895
55949
  )
@@ -56127,7 +56181,7 @@ async function checkForUpdates() {
56127
56181
  }
56128
56182
 
56129
56183
  // packages/updater/src/tui/update-overlay.ts
56130
- import { Key as Key18, matchesKey as matchesKey24, truncateToWidth as truncateToWidth25, visibleWidth as visibleWidth21 } from "@earendil-works/pi-tui";
56184
+ import { Key as Key18, matchesKey as matchesKey24, truncateToWidth as truncateToWidth26, visibleWidth as visibleWidth22 } from "@earendil-works/pi-tui";
56131
56185
 
56132
56186
  // packages/updater/src/installer.ts
56133
56187
  init_core();
@@ -56162,7 +56216,7 @@ async function installUpdate() {
56162
56216
  // packages/updater/src/tui/update-overlay.ts
56163
56217
  init_core();
56164
56218
  function padVisible3(content, targetWidth) {
56165
- const vw = visibleWidth21(content);
56219
+ const vw = visibleWidth22(content);
56166
56220
  const pad = Math.max(0, targetWidth - vw);
56167
56221
  return content + " ".repeat(pad);
56168
56222
  }
@@ -56237,7 +56291,7 @@ function renderUpdateOverlay(checkResult) {
56237
56291
  const current = theme.fg("muted", state2.result.currentVersion);
56238
56292
  const latest = theme.fg("success", theme.bold(state2.result.latestVersion));
56239
56293
  lines.push(
56240
- theme.fg("accent", "\u2502") + padVisible3(truncateToWidth25(` ${current} \u2192 ${latest}`, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
56294
+ theme.fg("accent", "\u2502") + padVisible3(truncateToWidth26(` ${current} \u2192 ${latest}`, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
56241
56295
  );
56242
56296
  lines.push(
56243
56297
  theme.fg("accent", "\u2502") + padVisible3("", innerWidth) + theme.fg("accent", "\u2502")
@@ -56250,7 +56304,7 @@ function renderUpdateOverlay(checkResult) {
56250
56304
  for (let i = 0; i < contentHeight; i++) {
56251
56305
  const line = visible[i] ?? "";
56252
56306
  lines.push(
56253
- theme.fg("accent", "\u2502") + padVisible3(truncateToWidth25(line, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
56307
+ theme.fg("accent", "\u2502") + padVisible3(truncateToWidth26(line, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
56254
56308
  );
56255
56309
  }
56256
56310
  lines.push(theme.fg("accent", `\u251C${"\u2500".repeat(innerWidth)}\u2524`));
@@ -56268,12 +56322,12 @@ function renderUpdateOverlay(checkResult) {
56268
56322
  } else if (config.autoUpdate === "auto" && !state2.autoCancelled) {
56269
56323
  const actionLine = ` ${theme.fg("success", "[Y]")} Update now ${theme.fg("muted", "[n]")} Cancel Auto-updating in ${theme.fg("warning", String(state2.autoCountdown))}...`;
56270
56324
  lines.push(
56271
- theme.fg("accent", "\u2502") + padVisible3(truncateToWidth25(actionLine, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
56325
+ theme.fg("accent", "\u2502") + padVisible3(truncateToWidth26(actionLine, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
56272
56326
  );
56273
56327
  } else {
56274
56328
  const actionLine = ` ${theme.fg("success", "[Y]")} Update now ${theme.fg("muted", "[n]")} Skip ${theme.fg("accent", "j/k")}: scroll`;
56275
56329
  lines.push(
56276
- theme.fg("accent", "\u2502") + padVisible3(truncateToWidth25(actionLine, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
56330
+ theme.fg("accent", "\u2502") + padVisible3(truncateToWidth26(actionLine, innerWidth), innerWidth) + theme.fg("accent", "\u2502")
56277
56331
  );
56278
56332
  }
56279
56333
  lines.push(theme.fg("accent", `\u2570${"\u2500".repeat(innerWidth)}\u256F`));