@pi-unipi/unipi 2.6.0 → 2.6.2

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.
Files changed (205) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +21 -22
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/ask-user/settings-tui.ts +18 -34
  5. package/packages/autocomplete/package.json +1 -1
  6. package/packages/autocomplete/src/constants.ts +2 -15
  7. package/packages/autocomplete/src/provider.ts +3 -17
  8. package/packages/btw/extensions/btw.ts +18 -45
  9. package/packages/btw/package.json +2 -2
  10. package/packages/compactor/README.md +2 -2
  11. package/packages/compactor/package.json +3 -3
  12. package/packages/compactor/skills/compactor/SKILL.md +1 -1
  13. package/packages/compactor/skills/compactor-detail/SKILL.md +3 -5
  14. package/packages/compactor/skills/compactor-doctor/SKILL.md +1 -1
  15. package/packages/compactor/skills/compactor-stats/SKILL.md +1 -1
  16. package/packages/compactor/src/commands/index.ts +47 -78
  17. package/packages/compactor/src/compaction/brief.ts +161 -90
  18. package/packages/compactor/src/compaction/build-sections.ts +3 -4
  19. package/packages/compactor/src/compaction/compact-args.ts +86 -0
  20. package/packages/compactor/src/compaction/cut.ts +270 -28
  21. package/packages/compactor/src/compaction/drill-down.ts +261 -0
  22. package/packages/compactor/src/compaction/format-recall.ts +96 -0
  23. package/packages/compactor/src/compaction/format.ts +8 -3
  24. package/packages/compactor/src/compaction/hooks.ts +248 -72
  25. package/packages/compactor/src/compaction/merge.ts +34 -4
  26. package/packages/compactor/src/compaction/rank.ts +270 -0
  27. package/packages/compactor/src/compaction/recall-scope.ts +28 -0
  28. package/packages/compactor/src/compaction/search-entries.ts +333 -96
  29. package/packages/compactor/src/compaction/skill-collapse.ts +35 -0
  30. package/packages/compactor/src/compaction/summarize.ts +37 -6
  31. package/packages/compactor/src/compaction/token-estimate.ts +104 -0
  32. package/packages/compactor/src/compaction/touched-files.ts +35 -0
  33. package/packages/compactor/src/config/manager.ts +2 -27
  34. package/packages/compactor/src/config/presets.ts +0 -2
  35. package/packages/compactor/src/config/schema.ts +2 -16
  36. package/packages/compactor/src/executor/executor.ts +6 -15
  37. package/packages/compactor/src/executor/runtime.ts +2 -12
  38. package/packages/compactor/src/index.ts +4 -121
  39. package/packages/compactor/src/info-screen.ts +3 -10
  40. package/packages/compactor/src/security/evaluator.ts +0 -53
  41. package/packages/compactor/src/security/policy.ts +7 -8
  42. package/packages/compactor/src/session/db.ts +0 -6
  43. package/packages/compactor/src/tools/ctx-execute-file.ts +0 -5
  44. package/packages/compactor/src/tools/register.ts +27 -50
  45. package/packages/compactor/src/tools/vcc-recall.ts +86 -48
  46. package/packages/compactor/src/tui/settings-overlay.ts +20 -40
  47. package/packages/compactor/src/types.ts +43 -100
  48. package/packages/core/constants.ts +1 -28
  49. package/packages/core/events.ts +1 -83
  50. package/packages/core/index.ts +1 -0
  51. package/packages/core/model-cache.ts +18 -12
  52. package/packages/core/package.json +3 -1
  53. package/packages/core/tui-overlay.ts +89 -0
  54. package/packages/core/tui-width.ts +18 -0
  55. package/packages/core/utils.ts +43 -0
  56. package/packages/footer/package.json +2 -2
  57. package/packages/footer/src/help.ts +2 -2
  58. package/packages/footer/src/index.ts +9 -10
  59. package/packages/footer/src/presets.ts +1 -27
  60. package/packages/footer/src/registry/index.ts +0 -23
  61. package/packages/footer/src/rendering/icons.ts +0 -13
  62. package/packages/footer/src/rendering/renderer.ts +5 -40
  63. package/packages/footer/src/rendering/separators.ts +0 -10
  64. package/packages/footer/src/rendering/theme.ts +1 -15
  65. package/packages/footer/src/segments/compactor.ts +7 -14
  66. package/packages/footer/src/segments/core.ts +15 -54
  67. package/packages/footer/src/segments/kanboard.ts +5 -5
  68. package/packages/footer/src/segments/mcp.ts +5 -5
  69. package/packages/footer/src/segments/memory.ts +4 -4
  70. package/packages/footer/src/segments/notify.ts +2 -2
  71. package/packages/footer/src/segments/ralph.ts +3 -3
  72. package/packages/footer/src/segments/status-ext.ts +1 -1
  73. package/packages/footer/src/segments/workflow.ts +3 -3
  74. package/packages/footer/src/tps-tracker.ts +0 -8
  75. package/packages/footer/src/tui/settings-tui.ts +14 -31
  76. package/packages/footer/src/types.ts +0 -15
  77. package/packages/image/package.json +2 -2
  78. package/packages/image/src/register-providers.ts +0 -11
  79. package/packages/image/src/tools.ts +0 -3
  80. package/packages/image/src/tui/model-selector.ts +36 -71
  81. package/packages/info-screen/config.ts +0 -16
  82. package/packages/info-screen/core-groups.ts +0 -10
  83. package/packages/info-screen/index.ts +22 -13
  84. package/packages/info-screen/package.json +2 -2
  85. package/packages/info-screen/registry.ts +1 -72
  86. package/packages/info-screen/tui/info-overlay.ts +85 -100
  87. package/packages/info-screen/usage-parser.ts +12 -47
  88. package/packages/input-shortcuts/package.json +2 -2
  89. package/packages/input-shortcuts/src/index.ts +3 -3
  90. package/packages/input-shortcuts/src/registers.ts +0 -8
  91. package/packages/input-shortcuts/src/settings-overlay.ts +4 -13
  92. package/packages/input-shortcuts/src/undo-redo.ts +0 -12
  93. package/packages/kanboard/package.json +2 -2
  94. package/packages/mcp/package.json +2 -2
  95. package/packages/mcp/src/bridge/client.ts +0 -15
  96. package/packages/mcp/src/config/manager.ts +0 -38
  97. package/packages/mcp/src/config/schema.ts +0 -27
  98. package/packages/mcp/src/config/sync.ts +0 -29
  99. package/packages/mcp/src/index.ts +1 -1
  100. package/packages/mcp/src/tui/add-overlay.ts +6 -6
  101. package/packages/mcp/src/types.ts +0 -6
  102. package/packages/memory/embedding.ts +0 -31
  103. package/packages/memory/mempalace.ts +0 -16
  104. package/packages/memory/package.json +4 -8
  105. package/packages/memory/storage.ts +56 -753
  106. package/packages/milestone/hooks.ts +1 -6
  107. package/packages/milestone/milestone.ts +2 -41
  108. package/packages/milestone/package.json +2 -2
  109. package/packages/notify/README.md +1 -0
  110. package/packages/notify/commands.ts +61 -3
  111. package/packages/notify/events.ts +0 -10
  112. package/packages/notify/package.json +2 -2
  113. package/packages/notify/settings.ts +0 -19
  114. package/packages/notify/tui/gotify-setup.ts +108 -143
  115. package/packages/notify/tui/ntfy-setup.ts +137 -172
  116. package/packages/notify/tui/recap-model-selector.ts +107 -101
  117. package/packages/notify/tui/settings-overlay.ts +94 -117
  118. package/packages/notify/tui/telegram-setup.ts +48 -79
  119. package/packages/notify/types.ts +0 -2
  120. package/packages/ralph/index.ts +11 -35
  121. package/packages/ralph/package.json +3 -3
  122. package/packages/ralph/ralph-loop.ts +3 -44
  123. package/packages/subagents/package.json +2 -1
  124. package/packages/subagents/src/agent-manager.ts +0 -51
  125. package/packages/subagents/src/agent-runner.ts +6 -80
  126. package/packages/subagents/src/conversation-viewer.ts +1 -19
  127. package/packages/subagents/src/core-compat.ts +8 -25
  128. package/packages/subagents/src/custom-agents.ts +0 -1
  129. package/packages/subagents/src/index.ts +3 -48
  130. package/packages/subagents/src/types.ts +0 -11
  131. package/packages/subagents/src/widget.ts +5 -5
  132. package/packages/unipi/bundled.js +4468 -7769
  133. package/packages/unipi/index.ts +6 -3
  134. package/packages/updater/package.json +2 -2
  135. package/packages/updater/src/changelog.ts +1 -16
  136. package/packages/updater/src/index.ts +0 -5
  137. package/packages/updater/src/installer.ts +4 -26
  138. package/packages/updater/src/markdown.ts +6 -152
  139. package/packages/updater/src/settings.ts +0 -15
  140. package/packages/updater/src/tui/changelog-overlay.ts +42 -234
  141. package/packages/updater/src/tui/list-detail-overlay.ts +260 -0
  142. package/packages/updater/src/tui/readme-overlay.ts +53 -210
  143. package/packages/updater/src/tui/settings-overlay.ts +13 -26
  144. package/packages/updater/src/version.ts +2 -31
  145. package/packages/utility/package.json +2 -4
  146. package/packages/utility/src/commands.ts +4 -5
  147. package/packages/utility/src/herdr-sync.ts +136 -0
  148. package/packages/utility/src/index.ts +11 -68
  149. package/packages/utility/src/lifecycle/cleanup.ts +2 -16
  150. package/packages/utility/src/{diff/settings.ts → settings.ts} +42 -80
  151. package/packages/utility/src/tools/env.ts +0 -0
  152. package/packages/utility/src/tui/name-badge-state.ts +18 -2
  153. package/packages/utility/src/tui/name-badge.ts +5 -4
  154. package/packages/utility/src/tui/util-settings-tui.ts +41 -224
  155. package/packages/web-api/package.json +2 -6
  156. package/packages/web-api/src/engine/dependencies.ts +13 -134
  157. package/packages/web-api/src/engine/extract.ts +9 -99
  158. package/packages/web-api/src/engine/format.ts +0 -75
  159. package/packages/web-api/src/engine/types.ts +0 -39
  160. package/packages/web-api/src/index.ts +1 -2
  161. package/packages/web-api/src/providers/base.ts +0 -10
  162. package/packages/web-api/src/providers/duckduckgo.ts +0 -1
  163. package/packages/web-api/src/providers/firecrawl.ts +0 -9
  164. package/packages/web-api/src/providers/jina-reader.ts +0 -9
  165. package/packages/web-api/src/providers/jina-search.ts +0 -9
  166. package/packages/web-api/src/providers/perplexity.ts +0 -9
  167. package/packages/web-api/src/providers/registry.ts +0 -80
  168. package/packages/web-api/src/providers/serpapi.ts +0 -9
  169. package/packages/web-api/src/providers/tavily.ts +0 -9
  170. package/packages/web-api/src/providers/wigolo-client.ts +0 -30
  171. package/packages/web-api/src/providers/wigolo.ts +0 -1
  172. package/packages/web-api/src/settings.ts +17 -77
  173. package/packages/web-api/src/tools.ts +21 -24
  174. package/packages/web-api/src/tui/settings-dialog.ts +0 -1
  175. package/packages/workflow/commands.ts +38 -122
  176. package/packages/workflow/index.ts +1 -5
  177. package/packages/workflow/package.json +2 -2
  178. package/packages/cocoindex/README.md +0 -94
  179. package/packages/cocoindex/bridge.ts +0 -842
  180. package/packages/cocoindex/commands.ts +0 -175
  181. package/packages/cocoindex/index.ts +0 -48
  182. package/packages/cocoindex/installer.ts +0 -397
  183. package/packages/cocoindex/package.json +0 -49
  184. package/packages/cocoindex/skills/cocoindex/SKILL.md +0 -88
  185. package/packages/cocoindex/tools.ts +0 -157
  186. package/packages/compactor/src/display/diff-renderer.ts +0 -281
  187. package/packages/compactor/src/display/line-width-safety.ts +0 -28
  188. package/packages/compactor/src/display/render-utils.ts +0 -52
  189. package/packages/compactor/src/display/thinking-label.ts +0 -18
  190. package/packages/compactor/src/display/tool-overrides.ts +0 -136
  191. package/packages/compactor/src/tools/compact.ts +0 -20
  192. package/packages/subagents/src/file-lock.ts +0 -102
  193. package/packages/utility/src/cache/ttl-cache.ts +0 -311
  194. package/packages/utility/src/diff/highlighter.ts +0 -352
  195. package/packages/utility/src/diff/parser.ts +0 -191
  196. package/packages/utility/src/diff/renderer.ts +0 -395
  197. package/packages/utility/src/diff/theme.ts +0 -316
  198. package/packages/utility/src/diff/wrapper.ts +0 -339
  199. package/packages/utility/src/display/capabilities.ts +0 -214
  200. package/packages/utility/src/display/width.ts +0 -226
  201. package/packages/utility/src/tools/batch.ts +0 -229
  202. package/packages/utility/src/tui/badge-settings-tui.ts +0 -388
  203. package/packages/utility/src/tui/badge-settings.ts +0 -67
  204. package/packages/utility/src/tui/settings-inspector.ts +0 -303
  205. package/packages/web-api/src/providers/llm-summarize.ts +0 -71
@@ -29,18 +29,22 @@ import compactor from "@pi-unipi/compactor";
29
29
  import footer from "@pi-unipi/footer";
30
30
  import updater from "@pi-unipi/updater";
31
31
  import inputShortcuts from "@pi-unipi/input-shortcuts";
32
- import cocoindex from "@pi-unipi/cocoindex";
33
32
  import image from "../image/src/index.js";
34
33
 
35
34
  export default function (pi: ExtensionAPI) {
36
35
  workflow(pi);
37
36
  ralph(pi);
38
37
  memory(pi);
38
+ // Utility loads BEFORE info-screen: the name badge overlay must be pushed
39
+ // to the BOTTOM of the overlay stack. hideOverlay() pops the topmost entry,
40
+ // and a capturing overlay's done() callback is one-shot — if the badge were
41
+ // stacked above the boot info-screen, the info-screen's auto-close would pop
42
+ // the badge (spending its done()) and strand the dashboard uncloseable.
43
+ utility(pi);
39
44
  infoScreen(pi);
40
45
  subagents(pi);
41
46
  btw(pi);
42
47
  webApi(pi);
43
- utility(pi);
44
48
  askUser(pi);
45
49
  mcp(pi);
46
50
  notify(pi);
@@ -51,6 +55,5 @@ export default function (pi: ExtensionAPI) {
51
55
  footer(pi);
52
56
  updater(pi);
53
57
  inputShortcuts(pi);
54
- cocoindex(pi);
55
58
  image(pi);
56
59
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/updater",
3
- "version": "2.6.0",
3
+ "version": "2.6.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",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.6.0"
37
+ "@pi-unipi/core": "2.6.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -8,7 +8,7 @@
8
8
  import { existsSync, readFileSync } from "fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
- import { findPackageRoot } from "@pi-unipi/core";
11
+ import { findPackageRoot, compareVersions } from "@pi-unipi/core";
12
12
  import { isNewerVersion } from "./version.js";
13
13
  import type { ChangelogEntry } from "../types.js";
14
14
 
@@ -155,18 +155,3 @@ export function getNewerVersions(
155
155
  }
156
156
  return result;
157
157
  }
158
-
159
- /**
160
- * Compare semver strings (simple lexicographic for x.y.z format).
161
- * Returns positive if a > b, negative if a < b, 0 if equal.
162
- */
163
- export function compareVersions(a: string, b: string): number {
164
- const pa = a.split(".").map(Number);
165
- const pb = b.split(".").map(Number);
166
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
167
- const na = pa[i] ?? 0;
168
- const nb = pb[i] ?? 0;
169
- if (na !== nb) return na - nb;
170
- }
171
- return 0;
172
- }
@@ -165,9 +165,4 @@ export default function updaterExtension(pi: ExtensionAPI): void {
165
165
  // Update check failure — silent, non-critical
166
166
  }
167
167
  });
168
-
169
- // Cleanup on session shutdown
170
- pi.on("session_shutdown", async () => {
171
- // No cleanup needed
172
- });
173
168
  }
@@ -2,12 +2,11 @@
2
2
  * @pi-unipi/updater — Update installer
3
3
  *
4
4
  * Wraps child_process.exec for installing updates via pi CLI.
5
- * Emits UPDATE_APPLIED or UPDATE_ERROR events.
6
5
  */
7
6
 
8
7
  import { exec } from "child_process";
9
8
  import { promisify } from "util";
10
- import { getInstalledPackageVersion, emitEvent, UNIPI_EVENTS } from "@pi-unipi/core";
9
+ import { getInstalledPackageVersion } from "@pi-unipi/core";
11
10
  import type { InstallResult } from "../types.js";
12
11
 
13
12
  const execAsync = promisify(exec);
@@ -20,14 +19,11 @@ const INSTALL_TIMEOUT_MS = 60000;
20
19
  * Uses pi CLI: `pi install npm:@pi-unipi/unipi`
21
20
  * Returns structured result with success/failure info.
22
21
  */
23
- export async function installUpdate(
24
- pi?: { events: { emit: (name: string, payload: unknown) => void } },
25
- ): Promise<InstallResult> {
22
+ export async function installUpdate(): Promise<InstallResult> {
26
23
  const thisDir = new URL("..", import.meta.url).pathname;
27
- const installedBefore = getInstalledPackageVersion(thisDir, "@pi-unipi/unipi");
28
24
 
29
25
  try {
30
- const { stdout, stderr } = await execAsync(
26
+ await execAsync(
31
27
  "pi install npm:@pi-unipi/unipi",
32
28
  {
33
29
  timeout: INSTALL_TIMEOUT_MS,
@@ -38,34 +34,16 @@ export async function installUpdate(
38
34
  // Get new version after install
39
35
  const installedAfter = getInstalledPackageVersion(thisDir, "@pi-unipi/unipi");
40
36
 
41
- const result: InstallResult = {
37
+ return {
42
38
  success: true,
43
39
  version: installedAfter,
44
40
  };
45
-
46
- // Emit success event
47
- if (pi) {
48
- emitEvent(pi, UNIPI_EVENTS.UPDATE_APPLIED, {
49
- previousVersion: installedBefore,
50
- newVersion: installedAfter,
51
- });
52
- }
53
-
54
- return result;
55
41
  } catch (err: unknown) {
56
42
  const errorMessage = (err instanceof Error && 'stderr' in err ? String((err as Error & { stderr?: string }).stderr) : undefined)
57
43
  || (err instanceof Error ? err.message : undefined)
58
44
  || String(err)
59
45
  || "Unknown install error";
60
46
 
61
- // Emit error event
62
- if (pi) {
63
- emitEvent(pi, UNIPI_EVENTS.UPDATE_ERROR, {
64
- error: errorMessage,
65
- phase: "install",
66
- });
67
- }
68
-
69
47
  return {
70
48
  success: false,
71
49
  error: errorMessage,
@@ -1,173 +1,27 @@
1
1
  /**
2
2
  * @pi-unipi/updater — Markdown terminal renderer
3
3
  *
4
- * Renders markdown to terminal-formatted strings.
5
- * When a Theme is provided, uses the full Markdown component from pi-tui
6
- * with theme-aware styling. Falls back to simple ANSI rendering otherwise.
4
+ * Renders markdown to terminal-formatted strings using the full
5
+ * Markdown component from pi-tui with theme-aware styling.
7
6
  */
8
7
 
9
8
  import { Markdown } from "@earendil-works/pi-tui";
10
- import type { MarkdownTheme } from "@earendil-works/pi-tui";
11
9
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
12
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
13
11
 
14
12
  /**
15
13
  * Render markdown text to terminal-formatted lines.
16
14
  *
17
- * When a Theme is provided, uses the full Markdown component from pi-tui
18
- * with syntax highlighting, proper list nesting, tables, etc.
15
+ * Uses pi-tui's Markdown component with syntax highlighting,
16
+ * proper list nesting, tables, etc.
19
17
  *
20
18
  * @param text - Markdown text to render
21
19
  * @param width - Available width for rendering
22
- * @param theme - Optional Theme for styled rendering
20
+ * @param theme - Theme for styled rendering
23
21
  * @returns Array of rendered terminal lines
24
22
  */
25
- export function renderMarkdown(text: string, width: number, theme?: Theme): string[] {
26
- if (theme) {
27
- return renderWithTheme(text, width, theme);
28
- }
29
- return renderSimple(text, width);
30
- }
31
-
32
- /**
33
- * Render using the full Markdown component with theme support.
34
- */
35
- function renderWithTheme(text: string, width: number, theme: Theme): string[] {
23
+ export function renderMarkdown(text: string, width: number, theme: Theme): string[] {
36
24
  const mdTheme = getMarkdownTheme();
37
25
  const md = new Markdown(text, 1, 0, mdTheme);
38
26
  return md.render(width);
39
27
  }
40
-
41
- /**
42
- * Simple fallback renderer using basic ANSI codes.
43
- * Used when no Theme is available.
44
- */
45
-
46
- /** ANSI escape codes */
47
- const ESC = "\x1b";
48
- const BOLD = `${ESC}[1m`;
49
- const DIM = `${ESC}[2m`;
50
- const UNDERLINE = `${ESC}[4m`;
51
- const RESET = `${ESC}[0m`;
52
-
53
- /** Wrap text in ANSI formatting */
54
- function fmt(code: string, text: string): string {
55
- return `${code}${text}${RESET}`;
56
- }
57
-
58
- /** Word-wrap a line to fit within width */
59
- function wordWrap(line: string, width: number): string[] {
60
- // Strip ANSI for length calculation but preserve in output
61
- const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
62
- if (stripped.length <= width) return [line];
63
-
64
- const words = line.split(/(\s+)/);
65
- const result: string[] = [];
66
- let currentLine = "";
67
- let currentWidth = 0;
68
-
69
- for (const word of words) {
70
- const wordWidth = word.replace(/\x1b\[[0-9;]*m/g, "").length;
71
- if (currentWidth + wordWidth > width && currentLine) {
72
- result.push(currentLine);
73
- currentLine = word.trimStart();
74
- currentWidth = currentLine.replace(/\x1b\[[0-9;]*m/g, "").length;
75
- } else {
76
- currentLine += word;
77
- currentWidth += wordWidth;
78
- }
79
- }
80
- if (currentLine) result.push(currentLine);
81
- return result.length > 0 ? result : [""];
82
- }
83
-
84
- /** Apply inline formatting: bold, italic, code, links */
85
- function formatInline(text: string): string {
86
- // Inline code: `code`
87
- text = text.replace(/`([^`]+)`/g, (_, code) => fmt(DIM, code));
88
-
89
- // Bold: **text**
90
- text = text.replace(/\*\*([^*]+)\*\*/g, (_, bold) => fmt(BOLD, bold));
91
-
92
- // Italic: *text*
93
- text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, italic) => fmt(UNDERLINE, italic));
94
-
95
- // Links: [text](url) → underlined text
96
- text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, (_, linkText) => fmt(UNDERLINE, linkText));
97
-
98
- return text;
99
- }
100
-
101
- /**
102
- * Simple markdown renderer (fallback).
103
- */
104
- function renderSimple(text: string, width: number): string[] {
105
- const lines = text.split("\n");
106
- const result: string[] = [];
107
- let inCodeBlock = false;
108
-
109
- for (const line of lines) {
110
- const trimmed = line.trim();
111
-
112
- // Code fence toggle
113
- if (trimmed.startsWith("```")) {
114
- inCodeBlock = !inCodeBlock;
115
- continue;
116
- }
117
-
118
- // Inside code block — dim, no formatting
119
- if (inCodeBlock) {
120
- const formatted = fmt(DIM, line);
121
- result.push(...wordWrap(formatted, width));
122
- continue;
123
- }
124
-
125
- // Heading: #, ##, ###
126
- if (trimmed.startsWith("### ")) {
127
- const heading = trimmed.slice(4);
128
- const formatted = fmt(BOLD + UNDERLINE, heading);
129
- result.push(...wordWrap(formatted, width));
130
- continue;
131
- }
132
- if (trimmed.startsWith("## ")) {
133
- const heading = trimmed.slice(3);
134
- const formatted = fmt(BOLD, heading);
135
- result.push(...wordWrap(formatted, width));
136
- continue;
137
- }
138
- if (trimmed.startsWith("# ")) {
139
- const heading = trimmed.slice(2);
140
- const formatted = fmt(BOLD, heading);
141
- result.push(...wordWrap(formatted, width));
142
- continue;
143
- }
144
-
145
- // Bullet list: - or *
146
- if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
147
- const content = trimmed.slice(2);
148
- const formatted = " • " + formatInline(content);
149
- result.push(...wordWrap(formatted, width));
150
- continue;
151
- }
152
-
153
- // Numbered list: 1. 2. etc.
154
- const numberMatch = trimmed.match(/^(\d+)\.\s+(.+)$/);
155
- if (numberMatch) {
156
- const formatted = ` ${numberMatch[1]}. ${formatInline(numberMatch[2])}`;
157
- result.push(...wordWrap(formatted, width));
158
- continue;
159
- }
160
-
161
- // Empty line — preserve spacing
162
- if (!trimmed) {
163
- result.push("");
164
- continue;
165
- }
166
-
167
- // Regular paragraph — apply inline formatting
168
- const formatted = formatInline(trimmed);
169
- result.push(...wordWrap(formatted, width));
170
- }
171
-
172
- return result;
173
- }
@@ -56,21 +56,6 @@ export function saveConfig(config: UpdaterConfig): void {
56
56
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
57
57
  }
58
58
 
59
- /** Validate config, returning list of error messages */
60
- export function validateConfig(config: UpdaterConfig): string[] {
61
- const errors: string[] = [];
62
-
63
- if (!VALID_MODES.includes(config.autoUpdate)) {
64
- errors.push(`autoUpdate must be one of: ${VALID_MODES.join(", ")}`);
65
- }
66
-
67
- if (config.checkIntervalMs < 60000) {
68
- errors.push("checkIntervalMs must be at least 60000 (1 minute)");
69
- }
70
-
71
- return errors;
72
- }
73
-
74
59
  /** Get human-readable label for an interval */
75
60
  export function getIntervalLabel(ms: number): string {
76
61
  for (const [label, value] of Object.entries(VALID_INTERVALS)) {
@@ -1,257 +1,65 @@
1
1
  /**
2
2
  * @pi-unipi/updater — Changelog TUI Overlay
3
3
  *
4
- * Version list with Current/New labels, Enter opens detail view, Esc/q back.
5
- * Uses ctx.ui.custom() overlay API with component return pattern.
4
+ * Version list with Current/New labels, Enter opens detail view.
6
5
  */
7
6
 
8
7
  import { existsSync } from "fs";
9
- import { join } from "path";
10
- import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
8
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
9
  import { parseChangelog, resolveChangelogPath } from "../changelog.js";
13
10
  import { renderMarkdown } from "../markdown.js";
14
- import { getInstalledPackageVersion, boxInnerWidth } from "@pi-unipi/core";
11
+ import { getInstalledPackageVersion } from "@pi-unipi/core";
12
+ import { createListDetailOverlay } from "./list-detail-overlay.js";
15
13
  import type { ChangelogEntry } from "../../types.js";
16
14
 
17
- type View = "list" | "detail";
18
-
19
- interface ChangelogState {
20
- view: View;
21
- entries: ChangelogEntry[];
22
- listIndex: number;
23
- listScroll: number;
24
- detailScroll: number;
25
- installedVersion: string;
26
- }
27
-
28
- /**
29
- * Pad content to exact visible width.
30
- */
31
- function padVisible(content: string, targetWidth: number): string {
32
- const vw = visibleWidth(content);
33
- const pad = Math.max(0, targetWidth - vw);
34
- return content + " ".repeat(pad);
35
- }
36
-
37
15
  /**
38
16
  * Render the changelog overlay.
39
17
  */
40
18
  export function renderChangelogOverlay() {
41
- return (
42
- tui: import("@earendil-works/pi-tui").TUI,
43
- theme: Theme,
44
- _kb: import("@earendil-works/pi-coding-agent").KeybindingsManager,
45
- done: (result: { viewed: boolean } | null) => void,
46
- ) => {
47
- const installedVersion = getInstalledPackageVersion(
48
- new URL("..", import.meta.url).pathname,
49
- "@pi-unipi/unipi",
50
- );
51
-
52
- const state: ChangelogState = {
53
- view: "list",
54
- entries: [],
55
- listIndex: 0,
56
- listScroll: 0,
57
- detailScroll: 0,
58
- installedVersion,
59
- };
60
-
61
- // Load changelog
62
- let loaded = false;
63
- const ensureLoaded = () => {
64
- if (loaded) return;
19
+ const installedVersion = getInstalledPackageVersion(
20
+ new URL("..", import.meta.url).pathname,
21
+ "@pi-unipi/unipi",
22
+ );
23
+
24
+ return createListDetailOverlay<ChangelogEntry>({
25
+ title: " 📋 Changelog ",
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",
29
+
30
+ loadEntries: () => {
65
31
  const changelogPath = resolveChangelogPath();
66
- if (existsSync(changelogPath)) {
67
- state.entries = parseChangelog(changelogPath);
68
- }
69
- loaded = true;
70
- };
71
-
72
- const render = (width: number): string[] => {
73
- ensureLoaded();
74
-
75
- const innerWidth = boxInnerWidth(width);
76
- const lines: string[] = [];
77
-
78
- // ── Header ──────────────────────────────────────────────────────
79
- lines.push(theme.fg("accent", `╭${"─".repeat(innerWidth)}╮`));
80
- lines.push(
81
- theme.fg("accent", "│") +
82
- padVisible(theme.fg("accent", theme.bold(" 📋 Changelog ")), innerWidth) +
83
- theme.fg("accent", "│"),
84
- );
85
- lines.push(theme.fg("accent", `├${"─".repeat(innerWidth)}┤`));
86
-
87
- // ── Content ─────────────────────────────────────────────────────
88
- if (state.view === "list") {
89
- renderListView(lines, innerWidth);
32
+ return existsSync(changelogPath) ? parseChangelog(changelogPath) : [];
33
+ },
34
+
35
+ renderItem: (entry, selected, theme) => {
36
+ let label: string;
37
+ if (entry.version === "Unreleased") {
38
+ label = theme.fg("muted", "Unreleased");
39
+ } else if (entry.version === installedVersion) {
40
+ label = theme.fg("success", "✓ Current");
90
41
  } else {
91
- renderDetailView(lines, innerWidth);
92
- }
93
-
94
- // ── Footer ──────────────────────────────────────────────────────
95
- lines.push(theme.fg("accent", `├${"─".repeat(innerWidth)}┤`));
96
- const footer =
97
- state.view === "list"
98
- ? ` ${theme.fg("accent", "j/k")} navigate ${theme.fg("success", "Enter")} view details ${theme.fg("error", "q/Esc")} close`
99
- : ` ${theme.fg("accent", "j/k")} scroll ${theme.fg("error", "q/Esc")} back to list`;
100
- lines.push(
101
- theme.fg("accent", "│") +
102
- padVisible(truncateToWidth(footer, innerWidth), innerWidth) +
103
- theme.fg("accent", "│"),
104
- );
105
- lines.push(theme.fg("accent", `╰${"─".repeat(innerWidth)}╯`));
106
-
107
- return lines;
108
- };
109
-
110
- const renderListView = (lines: string[], innerWidth: number) => {
111
- if (state.entries.length === 0) {
112
- lines.push(
113
- theme.fg("accent", "│") +
114
- padVisible(theme.fg("muted", " No changelog available."), innerWidth) +
115
- theme.fg("accent", "│"),
116
- );
117
- return;
42
+ const pa = entry.version.split(".").map(Number);
43
+ const pb = installedVersion.split(".").map(Number);
44
+ const isNewer =
45
+ pa[0]! > pb[0]! ||
46
+ (pa[0] === pb[0] && pa[1]! > pb[1]!) ||
47
+ (pa[0] === pb[0] && pa[1] === pb[1] && pa[2]! > pb[2]!);
48
+ label = isNewer ? theme.fg("warning", "↑ New") : "";
118
49
  }
119
50
 
120
- state.listIndex = Math.min(state.listIndex, state.entries.length - 1);
121
- state.listIndex = Math.max(0, state.listIndex);
122
-
123
- // Show visible entries
124
- const maxLines = 20;
125
- if (state.listIndex < state.listScroll) state.listScroll = state.listIndex;
126
- if (state.listIndex >= state.listScroll + maxLines) {
127
- state.listScroll = state.listIndex - maxLines + 1;
128
- }
51
+ const version = selected ? theme.bold(entry.version) : theme.fg("text", entry.version);
52
+ const date = entry.date ? ` — ${theme.fg("muted", entry.date)}` : "";
53
+ const prefix = selected ? theme.fg("accent", "▸ ") : " ";
54
+ return ` ${prefix}${version}${date} ${label}`;
55
+ },
129
56
 
130
- const visible = state.entries.slice(state.listScroll, state.listScroll + maxLines);
131
-
132
- for (let i = 0; i < visible.length; i++) {
133
- const entry = visible[i]!;
134
- const globalIdx = state.listScroll + i;
135
- const selected = globalIdx === state.listIndex;
136
- const prefix = selected ? theme.fg("accent", "▸ ") : " ";
137
-
138
- let label: string;
139
- if (entry.version === "Unreleased") {
140
- label = theme.fg("muted", "Unreleased");
141
- } else if (entry.version === state.installedVersion) {
142
- label = theme.fg("success", "✓ Current");
143
- } else {
144
- const pa = entry.version.split(".").map(Number);
145
- const pb = state.installedVersion.split(".").map(Number);
146
- const isNewer =
147
- pa[0]! > pb[0]! ||
148
- (pa[0] === pb[0] && pa[1]! > pb[1]!) ||
149
- (pa[0] === pb[0] && pa[1] === pb[1] && pa[2]! > pb[2]!);
150
- label = isNewer ? theme.fg("warning", "↑ New") : "";
151
- }
152
-
153
- const version = selected ? theme.bold(entry.version) : theme.fg("text", entry.version);
154
- const date = entry.date ? ` — ${theme.fg("muted", entry.date)}` : "";
155
- const line = ` ${prefix}${version}${date} ${label}`;
156
- lines.push(
157
- theme.fg("accent", "│") +
158
- padVisible(
159
- selected ? theme.bg("selectedBg", truncateToWidth(line, innerWidth)) : truncateToWidth(line, innerWidth),
160
- innerWidth,
161
- ) +
162
- theme.fg("accent", "│"),
163
- );
164
- }
165
- };
166
-
167
- const renderDetailView = (lines: string[], innerWidth: number) => {
168
- const entry = state.entries[state.listIndex];
169
- if (!entry) {
170
- lines.push(
171
- theme.fg("accent", "│") +
172
- padVisible(theme.fg("muted", " No entry selected."), innerWidth) +
173
- theme.fg("accent", "│"),
174
- );
175
- return;
176
- }
177
-
178
- const title = entry.date
57
+ renderDetailTitle: (entry, theme) =>
58
+ entry.date
179
59
  ? `${theme.bold(entry.version)} — ${theme.fg("muted", entry.date)}`
180
- : `${theme.bold(entry.version)} — ${theme.fg("muted", "Unreleased")}`;
181
- lines.push(
182
- theme.fg("accent", "│") +
183
- padVisible(truncateToWidth(` ${title}`, innerWidth), innerWidth) +
184
- theme.fg("accent", "│"),
185
- );
186
- lines.push(
187
- theme.fg("accent", "│") +
188
- padVisible("", innerWidth) +
189
- theme.fg("accent", "│"),
190
- );
191
-
192
- const bodyLines = renderMarkdown(entry.body, innerWidth - 2, theme);
193
- const maxScroll = Math.max(0, bodyLines.length - 15);
194
- state.detailScroll = Math.min(state.detailScroll, maxScroll);
195
- state.detailScroll = Math.max(0, state.detailScroll);
196
-
197
- const visible = bodyLines.slice(state.detailScroll, state.detailScroll + 15);
198
- for (const line of visible) {
199
- lines.push(
200
- theme.fg("accent", "│") +
201
- padVisible(truncateToWidth(` ${line}`, innerWidth), innerWidth) +
202
- theme.fg("accent", "│"),
203
- );
204
- }
205
- };
206
-
207
- const handleInput = (data: string) => {
208
- ensureLoaded();
209
-
210
- // Close from list view
211
- if ((matchesKey(data, Key.escape) || data === "q") && state.view === "list") {
212
- done({ viewed: true });
213
- return;
214
- }
215
-
216
- // Back from detail view
217
- if ((matchesKey(data, Key.escape) || data === "q") && state.view === "detail") {
218
- state.view = "list";
219
- state.detailScroll = 0;
220
- tui.requestRender();
221
- return;
222
- }
223
-
224
- // Navigation
225
- if (state.view === "list") {
226
- if (matchesKey(data, Key.down) || data === "j") {
227
- state.listIndex = Math.min(state.listIndex + 1, state.entries.length - 1);
228
- } else if (matchesKey(data, Key.up) || data === "k") {
229
- state.listIndex = Math.max(state.listIndex - 1, 0);
230
- } else if (matchesKey(data, Key.enter)) {
231
- if (state.entries.length > 0) {
232
- state.view = "detail";
233
- state.detailScroll = 0;
234
- }
235
- } else if (data === "g") {
236
- state.listIndex = 0;
237
- } else if (data === "G") {
238
- state.listIndex = state.entries.length - 1;
239
- }
240
- } else {
241
- if (matchesKey(data, Key.down) || data === "j") {
242
- state.detailScroll++;
243
- } else if (matchesKey(data, Key.up) || data === "k") {
244
- state.detailScroll = Math.max(0, state.detailScroll - 1);
245
- } else if (data === "g") {
246
- state.detailScroll = 0;
247
- } else if (data === "G") {
248
- state.detailScroll = 999999;
249
- }
250
- }
251
-
252
- tui.requestRender();
253
- };
60
+ : `${theme.bold(entry.version)} — ${theme.fg("muted", "Unreleased")}`,
254
61
 
255
- return { render, handleInput, invalidate: () => {}, focused: true };
256
- };
62
+ renderDetailBody: (entry, innerWidth, theme) =>
63
+ renderMarkdown(entry.body, innerWidth - 2, theme),
64
+ });
257
65
  }