@pi-unipi/updater 2.18.0 → 2.18.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/updater",
3
- "version": "2.18.0",
3
+ "version": "2.18.1",
4
4
  "description": "Auto-updater, changelog browser, and readme browser for Unipi — checks npm registry, renders CHANGELOG.md and README.md files in TUI overlays",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -24,8 +24,8 @@ export function renderChangelogOverlay() {
24
24
  return createListDetailOverlay<ChangelogEntry>({
25
25
  title: " 📋 Changelog ",
26
26
  emptyMessage: "No changelog available.",
27
- listFooter: " j/k navigate Enter view details q/Esc close",
28
- detailFooter: " j/k scroll q/Esc back to list",
27
+ listFooter: " ↑/↓ j/k navigate Enter view details q/Esc close",
28
+ detailFooter: " ↑/↓ j/k scroll q/Esc back to list",
29
29
 
30
30
  loadEntries: () => {
31
31
  const changelogPath = resolveChangelogPath();
@@ -5,7 +5,7 @@
5
5
  * Used by both the changelog and readme browsers.
6
6
  */
7
7
 
8
- import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
+ import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
9
9
  import type { Theme } from "@earendil-works/pi-coding-agent";
10
10
  import { boxInnerWidth } from "@pi-unipi/core";
11
11
 
@@ -168,18 +168,22 @@ export function createListDetailOverlay<T>(
168
168
  }
169
169
 
170
170
  const title = config.renderDetailTitle(entry, theme);
171
- lines.push(
172
- theme.fg("accent", "│") +
173
- padVisible(truncateToWidth(` ${title}`, innerWidth), innerWidth) +
174
- theme.fg("accent", "│"),
175
- );
171
+ const wrapLine = (line: string) =>
172
+ visibleWidth(line) > innerWidth ? wrapTextWithAnsi(line, innerWidth) : [line];
173
+ for (const titleLine of wrapLine(` ${title}`)) {
174
+ lines.push(
175
+ theme.fg("accent", "│") +
176
+ padVisible(titleLine, innerWidth) +
177
+ theme.fg("accent", "│"),
178
+ );
179
+ }
176
180
  lines.push(
177
181
  theme.fg("accent", "│") +
178
182
  padVisible("", innerWidth) +
179
183
  theme.fg("accent", "│"),
180
184
  );
181
185
 
182
- const bodyLines = config.renderDetailBody(entry, innerWidth, theme);
186
+ const bodyLines = config.renderDetailBody(entry, innerWidth, theme).flatMap((line) => wrapLine(` ${line}`));
183
187
  const maxScroll = Math.max(0, bodyLines.length - 15);
184
188
  state.detailScroll = Math.min(state.detailScroll, maxScroll);
185
189
  state.detailScroll = Math.max(0, state.detailScroll);
@@ -188,7 +192,7 @@ export function createListDetailOverlay<T>(
188
192
  for (const line of visible) {
189
193
  lines.push(
190
194
  theme.fg("accent", "│") +
191
- padVisible(truncateToWidth(` ${line}`, innerWidth), innerWidth) +
195
+ padVisible(line, innerWidth) +
192
196
  theme.fg("accent", "│"),
193
197
  );
194
198
  }
@@ -28,8 +28,8 @@ export function renderReadmeOverlay(params?: ReadmeParams) {
28
28
  return createListDetailOverlay<ReadmeEntry>({
29
29
  title: " 📖 README Browser ",
30
30
  emptyMessage: "No README files found.",
31
- listFooter: " j/k navigate Enter read q/Esc close",
32
- detailFooter: " j/k scroll q/Esc back to list",
31
+ listFooter: " ↑/↓ j/k navigate Enter read q/Esc close",
32
+ detailFooter: " ↑/↓ j/k scroll q/Esc back to list",
33
33
 
34
34
  loadEntries: () => {
35
35
  const entries = discoverReadmes();
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { join } from "path";
10
- import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
12
  import { parseChangelog, getNewerVersions, resolveChangelogPath } from "../changelog.js";
13
13
  import { renderMarkdown } from "../markdown.js";
@@ -28,7 +28,7 @@ function padVisible(content: string, targetWidth: number): string {
28
28
 
29
29
  interface UpdateState {
30
30
  result: UpdateCheckResult;
31
- contentLines: string[];
31
+ contentLines: { width: number; lines: string[] } | null;
32
32
  scroll: number;
33
33
  installing: boolean;
34
34
  installError: string | null;
@@ -61,27 +61,9 @@ export function renderUpdateOverlay(checkResult: UpdateCheckResult, providedNewe
61
61
  }
62
62
  }
63
63
 
64
- // Build content lines from changelog using markdown renderer
65
- const contentLines: string[] = [];
66
- for (const entry of newerVersions) {
67
- const title = entry.date
68
- ? `${theme.bold(entry.version)} — ${theme.fg("muted", entry.date)}`
69
- : `${theme.bold(entry.version)} — ${theme.fg("muted", "Unreleased")}`;
70
- contentLines.push(` ${title}`);
71
- // Use markdown renderer for the body content
72
- const bodyLines = renderMarkdown(entry.body, (tui.terminal?.columns ?? 80) - 6, theme);
73
- for (const line of bodyLines) {
74
- contentLines.push(` ${line}`);
75
- }
76
- contentLines.push("");
77
- }
78
- if (contentLines.length === 0) {
79
- contentLines.push(` ${theme.fg("muted", `No changelog available for ${checkResult.latestVersion} (offline?).`)}`);
80
- }
81
-
82
64
  const state: UpdateState = {
83
65
  result: checkResult,
84
- contentLines,
66
+ contentLines: null,
85
67
  scroll: 0,
86
68
  installing: false,
87
69
  installError: null,
@@ -125,6 +107,29 @@ export function renderUpdateOverlay(checkResult: UpdateCheckResult, providedNewe
125
107
  const innerWidth = boxInnerWidth(width);
126
108
  const lines: string[] = [];
127
109
 
110
+ if (state.contentLines?.width !== innerWidth) {
111
+ const contentLines: string[] = [];
112
+ const wrapLine = (line: string) =>
113
+ visibleWidth(line) > innerWidth ? wrapTextWithAnsi(line, innerWidth) : [line];
114
+ for (const entry of newerVersions) {
115
+ const title = entry.date
116
+ ? `${theme.bold(entry.version)} — ${theme.fg("muted", entry.date)}`
117
+ : `${theme.bold(entry.version)} — ${theme.fg("muted", "Unreleased")}`;
118
+ contentLines.push(...wrapLine(` ${title}`));
119
+ const bodyLines = renderMarkdown(entry.body, innerWidth - 2, theme);
120
+ for (const line of bodyLines) {
121
+ contentLines.push(...wrapLine(` ${line}`));
122
+ }
123
+ contentLines.push("");
124
+ }
125
+ if (contentLines.length === 0) {
126
+ contentLines.push(...wrapLine(` ${theme.fg("muted", `No changelog available for ${checkResult.latestVersion} (offline?).`)}`));
127
+ }
128
+ state.contentLines = { width: innerWidth, lines: contentLines };
129
+ }
130
+
131
+ const contentLines = state.contentLines.lines;
132
+
128
133
  // ── Header ──────────────────────────────────────────────────────
129
134
  lines.push(theme.fg("accent", `╭${"─".repeat(innerWidth)}╮`));
130
135
  lines.push(
@@ -150,16 +155,16 @@ export function renderUpdateOverlay(checkResult: UpdateCheckResult, providedNewe
150
155
 
151
156
  // ── Changelog content (scrollable) ──────────────────────────────
152
157
  const contentHeight = 12;
153
- const maxScroll = Math.max(0, state.contentLines.length - contentHeight);
158
+ const maxScroll = Math.max(0, contentLines.length - contentHeight);
154
159
  state.scroll = Math.min(state.scroll, maxScroll);
155
160
  state.scroll = Math.max(0, state.scroll);
156
161
 
157
- const visible = state.contentLines.slice(state.scroll, state.scroll + contentHeight);
162
+ const visible = contentLines.slice(state.scroll, state.scroll + contentHeight);
158
163
  for (let i = 0; i < contentHeight; i++) {
159
164
  const line = visible[i] ?? "";
160
165
  lines.push(
161
166
  theme.fg("accent", "│") +
162
- padVisible(truncateToWidth(line, innerWidth), innerWidth) +
167
+ padVisible(line, innerWidth) +
163
168
  theme.fg("accent", "│"),
164
169
  );
165
170
  }
@@ -192,7 +197,7 @@ export function renderUpdateOverlay(checkResult: UpdateCheckResult, providedNewe
192
197
  theme.fg("accent", "│"),
193
198
  );
194
199
  } else {
195
- const actionLine = ` ${theme.fg("success", "[Y]")} Update now ${theme.fg("muted", "[n]")} Skip ${theme.fg("accent", "j/k")}: scroll`;
200
+ const actionLine = ` ${theme.fg("success", "[Y]")} Update now ${theme.fg("muted", "[n]")} Skip ${theme.fg("accent", "↑/↓ j/k")}: scroll`;
196
201
  lines.push(
197
202
  theme.fg("accent", "│") +
198
203
  padVisible(truncateToWidth(actionLine, innerWidth), innerWidth) +