@pi-unipi/unipi 2.6.1 → 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 (186) hide show
  1. package/package.json +1 -2
  2. package/packages/ask-user/settings-tui.ts +18 -34
  3. package/packages/autocomplete/src/constants.ts +0 -15
  4. package/packages/autocomplete/src/provider.ts +3 -17
  5. package/packages/btw/extensions/btw.ts +18 -45
  6. package/packages/compactor/README.md +2 -2
  7. package/packages/compactor/skills/compactor/SKILL.md +1 -1
  8. package/packages/compactor/skills/compactor-detail/SKILL.md +3 -5
  9. package/packages/compactor/skills/compactor-doctor/SKILL.md +1 -1
  10. package/packages/compactor/skills/compactor-stats/SKILL.md +1 -1
  11. package/packages/compactor/src/commands/index.ts +47 -78
  12. package/packages/compactor/src/compaction/brief.ts +161 -90
  13. package/packages/compactor/src/compaction/build-sections.ts +3 -4
  14. package/packages/compactor/src/compaction/compact-args.ts +86 -0
  15. package/packages/compactor/src/compaction/cut.ts +270 -28
  16. package/packages/compactor/src/compaction/drill-down.ts +261 -0
  17. package/packages/compactor/src/compaction/format-recall.ts +96 -0
  18. package/packages/compactor/src/compaction/format.ts +8 -3
  19. package/packages/compactor/src/compaction/hooks.ts +248 -72
  20. package/packages/compactor/src/compaction/merge.ts +34 -4
  21. package/packages/compactor/src/compaction/rank.ts +270 -0
  22. package/packages/compactor/src/compaction/recall-scope.ts +28 -0
  23. package/packages/compactor/src/compaction/search-entries.ts +333 -96
  24. package/packages/compactor/src/compaction/skill-collapse.ts +35 -0
  25. package/packages/compactor/src/compaction/summarize.ts +37 -6
  26. package/packages/compactor/src/compaction/token-estimate.ts +104 -0
  27. package/packages/compactor/src/compaction/touched-files.ts +35 -0
  28. package/packages/compactor/src/config/manager.ts +2 -27
  29. package/packages/compactor/src/config/presets.ts +0 -2
  30. package/packages/compactor/src/config/schema.ts +2 -16
  31. package/packages/compactor/src/executor/executor.ts +6 -15
  32. package/packages/compactor/src/executor/runtime.ts +2 -12
  33. package/packages/compactor/src/index.ts +4 -121
  34. package/packages/compactor/src/info-screen.ts +3 -10
  35. package/packages/compactor/src/security/evaluator.ts +0 -53
  36. package/packages/compactor/src/security/policy.ts +7 -8
  37. package/packages/compactor/src/session/db.ts +0 -6
  38. package/packages/compactor/src/tools/ctx-execute-file.ts +0 -5
  39. package/packages/compactor/src/tools/register.ts +27 -50
  40. package/packages/compactor/src/tools/vcc-recall.ts +86 -48
  41. package/packages/compactor/src/tui/settings-overlay.ts +20 -40
  42. package/packages/compactor/src/types.ts +43 -100
  43. package/packages/core/constants.ts +0 -28
  44. package/packages/core/events.ts +1 -83
  45. package/packages/core/index.ts +1 -0
  46. package/packages/core/package.json +2 -0
  47. package/packages/core/tui-overlay.ts +89 -0
  48. package/packages/core/tui-width.ts +18 -0
  49. package/packages/core/utils.ts +43 -0
  50. package/packages/footer/src/help.ts +2 -2
  51. package/packages/footer/src/index.ts +9 -10
  52. package/packages/footer/src/presets.ts +1 -27
  53. package/packages/footer/src/registry/index.ts +0 -23
  54. package/packages/footer/src/rendering/icons.ts +0 -13
  55. package/packages/footer/src/rendering/renderer.ts +5 -40
  56. package/packages/footer/src/rendering/separators.ts +0 -10
  57. package/packages/footer/src/rendering/theme.ts +1 -15
  58. package/packages/footer/src/segments/compactor.ts +7 -14
  59. package/packages/footer/src/segments/core.ts +15 -54
  60. package/packages/footer/src/segments/kanboard.ts +5 -5
  61. package/packages/footer/src/segments/mcp.ts +5 -5
  62. package/packages/footer/src/segments/memory.ts +4 -4
  63. package/packages/footer/src/segments/notify.ts +2 -2
  64. package/packages/footer/src/segments/ralph.ts +3 -3
  65. package/packages/footer/src/segments/status-ext.ts +1 -1
  66. package/packages/footer/src/segments/workflow.ts +3 -3
  67. package/packages/footer/src/tps-tracker.ts +0 -8
  68. package/packages/footer/src/tui/settings-tui.ts +14 -31
  69. package/packages/footer/src/types.ts +0 -15
  70. package/packages/image/src/register-providers.ts +0 -11
  71. package/packages/image/src/tools.ts +0 -3
  72. package/packages/image/src/tui/model-selector.ts +36 -71
  73. package/packages/info-screen/config.ts +0 -16
  74. package/packages/info-screen/core-groups.ts +0 -10
  75. package/packages/info-screen/index.ts +22 -13
  76. package/packages/info-screen/registry.ts +1 -72
  77. package/packages/info-screen/tui/info-overlay.ts +85 -100
  78. package/packages/info-screen/usage-parser.ts +12 -47
  79. package/packages/input-shortcuts/src/index.ts +3 -3
  80. package/packages/input-shortcuts/src/registers.ts +0 -8
  81. package/packages/input-shortcuts/src/settings-overlay.ts +4 -13
  82. package/packages/input-shortcuts/src/undo-redo.ts +0 -12
  83. package/packages/mcp/src/bridge/client.ts +0 -15
  84. package/packages/mcp/src/config/manager.ts +0 -38
  85. package/packages/mcp/src/config/schema.ts +0 -27
  86. package/packages/mcp/src/config/sync.ts +0 -29
  87. package/packages/mcp/src/index.ts +1 -1
  88. package/packages/mcp/src/tui/add-overlay.ts +6 -6
  89. package/packages/mcp/src/types.ts +0 -6
  90. package/packages/memory/embedding.ts +0 -31
  91. package/packages/memory/mempalace.ts +0 -16
  92. package/packages/memory/package.json +2 -6
  93. package/packages/memory/storage.ts +56 -753
  94. package/packages/milestone/hooks.ts +1 -6
  95. package/packages/milestone/milestone.ts +2 -41
  96. package/packages/notify/events.ts +0 -10
  97. package/packages/notify/settings.ts +0 -19
  98. package/packages/notify/tui/gotify-setup.ts +108 -143
  99. package/packages/notify/tui/ntfy-setup.ts +137 -172
  100. package/packages/notify/tui/recap-model-selector.ts +36 -71
  101. package/packages/notify/tui/settings-overlay.ts +50 -86
  102. package/packages/notify/tui/telegram-setup.ts +48 -79
  103. package/packages/notify/types.ts +0 -2
  104. package/packages/ralph/index.ts +11 -35
  105. package/packages/ralph/ralph-loop.ts +3 -44
  106. package/packages/subagents/package.json +1 -0
  107. package/packages/subagents/src/agent-manager.ts +0 -51
  108. package/packages/subagents/src/agent-runner.ts +6 -80
  109. package/packages/subagents/src/conversation-viewer.ts +1 -19
  110. package/packages/subagents/src/core-compat.ts +8 -25
  111. package/packages/subagents/src/custom-agents.ts +0 -1
  112. package/packages/subagents/src/index.ts +3 -48
  113. package/packages/subagents/src/types.ts +0 -11
  114. package/packages/subagents/src/widget.ts +5 -5
  115. package/packages/unipi/bundled.js +4330 -7707
  116. package/packages/unipi/index.ts +6 -3
  117. package/packages/updater/src/changelog.ts +1 -16
  118. package/packages/updater/src/index.ts +0 -5
  119. package/packages/updater/src/installer.ts +4 -26
  120. package/packages/updater/src/markdown.ts +6 -152
  121. package/packages/updater/src/settings.ts +0 -15
  122. package/packages/updater/src/tui/changelog-overlay.ts +42 -234
  123. package/packages/updater/src/tui/list-detail-overlay.ts +260 -0
  124. package/packages/updater/src/tui/readme-overlay.ts +53 -210
  125. package/packages/updater/src/tui/settings-overlay.ts +13 -26
  126. package/packages/updater/src/version.ts +2 -31
  127. package/packages/utility/package.json +1 -3
  128. package/packages/utility/src/commands.ts +4 -5
  129. package/packages/utility/src/herdr-sync.ts +136 -0
  130. package/packages/utility/src/index.ts +11 -68
  131. package/packages/utility/src/lifecycle/cleanup.ts +2 -16
  132. package/packages/utility/src/{diff/settings.ts → settings.ts} +42 -80
  133. package/packages/utility/src/tools/env.ts +0 -0
  134. package/packages/utility/src/tui/name-badge-state.ts +18 -2
  135. package/packages/utility/src/tui/name-badge.ts +5 -4
  136. package/packages/utility/src/tui/util-settings-tui.ts +41 -224
  137. package/packages/web-api/package.json +0 -4
  138. package/packages/web-api/src/engine/dependencies.ts +13 -134
  139. package/packages/web-api/src/engine/extract.ts +9 -99
  140. package/packages/web-api/src/engine/format.ts +0 -75
  141. package/packages/web-api/src/engine/types.ts +0 -39
  142. package/packages/web-api/src/index.ts +1 -2
  143. package/packages/web-api/src/providers/base.ts +0 -10
  144. package/packages/web-api/src/providers/duckduckgo.ts +0 -1
  145. package/packages/web-api/src/providers/firecrawl.ts +0 -9
  146. package/packages/web-api/src/providers/jina-reader.ts +0 -9
  147. package/packages/web-api/src/providers/jina-search.ts +0 -9
  148. package/packages/web-api/src/providers/perplexity.ts +0 -9
  149. package/packages/web-api/src/providers/registry.ts +0 -80
  150. package/packages/web-api/src/providers/serpapi.ts +0 -9
  151. package/packages/web-api/src/providers/tavily.ts +0 -9
  152. package/packages/web-api/src/providers/wigolo-client.ts +0 -30
  153. package/packages/web-api/src/providers/wigolo.ts +0 -1
  154. package/packages/web-api/src/settings.ts +17 -77
  155. package/packages/web-api/src/tools.ts +21 -24
  156. package/packages/web-api/src/tui/settings-dialog.ts +0 -1
  157. package/packages/workflow/commands.ts +38 -122
  158. package/packages/workflow/index.ts +1 -5
  159. package/packages/cocoindex/README.md +0 -94
  160. package/packages/cocoindex/bridge.ts +0 -842
  161. package/packages/cocoindex/commands.ts +0 -175
  162. package/packages/cocoindex/index.ts +0 -48
  163. package/packages/cocoindex/installer.ts +0 -397
  164. package/packages/cocoindex/package.json +0 -49
  165. package/packages/cocoindex/skills/cocoindex/SKILL.md +0 -88
  166. package/packages/cocoindex/tools.ts +0 -157
  167. package/packages/compactor/src/display/diff-renderer.ts +0 -281
  168. package/packages/compactor/src/display/line-width-safety.ts +0 -28
  169. package/packages/compactor/src/display/render-utils.ts +0 -52
  170. package/packages/compactor/src/display/thinking-label.ts +0 -18
  171. package/packages/compactor/src/display/tool-overrides.ts +0 -136
  172. package/packages/compactor/src/tools/compact.ts +0 -20
  173. package/packages/subagents/src/file-lock.ts +0 -102
  174. package/packages/utility/src/cache/ttl-cache.ts +0 -311
  175. package/packages/utility/src/diff/highlighter.ts +0 -352
  176. package/packages/utility/src/diff/parser.ts +0 -191
  177. package/packages/utility/src/diff/renderer.ts +0 -395
  178. package/packages/utility/src/diff/theme.ts +0 -316
  179. package/packages/utility/src/diff/wrapper.ts +0 -339
  180. package/packages/utility/src/display/capabilities.ts +0 -214
  181. package/packages/utility/src/display/width.ts +0 -226
  182. package/packages/utility/src/tools/batch.ts +0 -229
  183. package/packages/utility/src/tui/badge-settings-tui.ts +0 -388
  184. package/packages/utility/src/tui/badge-settings.ts +0 -67
  185. package/packages/utility/src/tui/settings-inspector.ts +0 -303
  186. package/packages/web-api/src/providers/llm-summarize.ts +0 -71
@@ -1,395 +0,0 @@
1
- /**
2
- * @pi-unipi/utility — Diff Renderer
3
- *
4
- * ANSI diff rendering: split (side-by-side) and unified (stacked) views.
5
- * Includes ANSI utilities, background injection, and adaptive wrapping.
6
- *
7
- * IMPORTANT: All output lines must be truncated to terminal width
8
- * to prevent TUI crashes. Use fit() or truncateToTermWidth()
9
- * before pushing lines.
10
- */
11
-
12
- import type { ParsedDiff, DiffLine } from "./parser.js";
13
- import type { DiffColors } from "./theme.js";
14
- import { hexToBgAnsi, hexToFgAnsi } from "./theme.js";
15
- import { hlBlock, detectLanguage } from "./highlighter.js";
16
- import { truncateToWidth as piTruncateToWidth, visibleWidth as piVisibleWidth } from "@earendil-works/pi-tui";
17
-
18
- // ─── Constants ──────────────────────────────────────────────────────────────────
19
-
20
- /** Maximum preview lines shown inline */
21
- export const MAX_PREVIEW_LINES = 60;
22
-
23
- /** Maximum total render lines */
24
- export const MAX_RENDER_LINES = 150;
25
-
26
- /** Minimum terminal width for split view */
27
- export const SPLIT_MIN_WIDTH = 150;
28
-
29
- /** Minimum code column width in split view */
30
- export const SPLIT_MIN_CODE_WIDTH = 60;
31
-
32
- // ─── ANSI Utilities ─────────────────────────────────────────────────────────────
33
-
34
- /**
35
- * Strip all ANSI escape sequences from a string.
36
- */
37
- export function strip(s: string): string {
38
- // eslint-disable-next-line no-control-regex
39
- return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
40
- }
41
-
42
- /**
43
- * Get the visible width of a string (excluding ANSI escapes).
44
- * Delegates to pi-tui's ANSI-aware implementation for correctness.
45
- */
46
- export function visibleWidth(s: string): number {
47
- return piVisibleWidth(s);
48
- }
49
-
50
- /**
51
- * Fit a string to a target width, padding or truncating as needed.
52
- * Uses pi-tui's ANSI-aware truncation for correctness.
53
- */
54
- export function fit(s: string, targetWidth: number): string {
55
- const vw = visibleWidth(s);
56
- if (vw === targetWidth) return s;
57
- if (vw > targetWidth) {
58
- return piTruncateToWidth(s, targetWidth, "");
59
- }
60
- // Pad
61
- return s + " ".repeat(targetWidth - vw);
62
- }
63
-
64
- /**
65
- * Wrap an ANSI string to a maximum width.
66
- * Returns an array of wrapped lines.
67
- */
68
- export function wrapAnsi(s: string, maxWidth: number): string[] {
69
- if (visibleWidth(s) <= maxWidth) return [s];
70
-
71
- const lines: string[] = [];
72
- let current = "";
73
- let currentWidth = 0;
74
- let ansiState = "";
75
-
76
- // Process character by character
77
- let i = 0;
78
- while (i < s.length) {
79
- if (s[i] === "\x1b") {
80
- // Collect ANSI sequence
81
- let seq = "\x1b";
82
- i++;
83
- while (i < s.length && s[i] !== "m") {
84
- seq += s[i];
85
- i++;
86
- }
87
- if (i < s.length) {
88
- seq += "m";
89
- i++;
90
- }
91
- ansiState = seq;
92
- current += seq;
93
- } else {
94
- const char = s[i];
95
- const code = char.codePointAt(0)!;
96
- const charWidth = (code >= 0x4e00 && code <= 0x9fff) || code > 0xffff ? 2 : 1;
97
-
98
- if (currentWidth + charWidth > maxWidth) {
99
- lines.push(current + "\x1b[0m");
100
- current = ansiState;
101
- currentWidth = 0;
102
- }
103
- current += char;
104
- currentWidth += charWidth;
105
- i++;
106
- }
107
- }
108
-
109
- if (strip(current).length > 0) {
110
- lines.push(current);
111
- }
112
-
113
- return lines.length > 0 ? lines : [""];
114
- }
115
-
116
- /**
117
- * Get the current ANSI state (active escape sequences) at the end of a string.
118
- */
119
- export function ansiState(s: string): string {
120
- // Find the last reset or the last escape sequence
121
- const lastReset = s.lastIndexOf("\x1b[0m");
122
- const lastEsc = s.lastIndexOf("\x1b[");
123
-
124
- if (lastEsc < 0) return "";
125
- // If reset is at the end (or after last escape), state is cleared
126
- if (lastReset >= lastEsc) return "";
127
-
128
- // Extract the sequence
129
- const match = s.substring(lastEsc).match(/^\x1b\[[0-9;]*[a-zA-Z]/);
130
- return match ? match[0] : "";
131
- }
132
-
133
- /**
134
- * Format a line number with fixed width.
135
- */
136
- export function lnum(n: number | null, width: number = 4): string {
137
- if (n === null) return " ".repeat(width);
138
- return n.toString().padStart(width, " ");
139
- }
140
-
141
- /**
142
- * Generate a stripe pattern for alternating rows.
143
- */
144
- export function stripes(line: string, even: boolean): string {
145
- const dim = "\x1b[2m";
146
- const reset = "\x1b[0m";
147
- return even ? `${dim}·${reset} ${line}` : ` ${line}`;
148
- }
149
-
150
- // ─── Terminal Width ─────────────────────────────────────────────────────────────
151
-
152
- /**
153
- * Get terminal width, with fallback.
154
- */
155
- export function termW(): number {
156
- try {
157
- return process.stdout.columns || 80;
158
- } catch {
159
- return 80;
160
- }
161
- }
162
-
163
- /**
164
- * Truncate a line to terminal width using pi-tui's ANSI-aware truncation.
165
- * Prevents TUI crash when rendered lines exceed terminal width.
166
- * Accounts for rendering overhead from Box nesting (~6 chars).
167
- */
168
- export function truncateToTermWidth(line: string): string {
169
- const maxW = Math.max(20, termW() - 6);
170
- if (piVisibleWidth(line) <= maxW) return line;
171
- return piTruncateToWidth(line, maxW, "…");
172
- }
173
-
174
- // ─── Background Injection ───────────────────────────────────────────────────────
175
-
176
- /**
177
- * Composite diff background colors under Shiki syntax foregrounds.
178
- *
179
- * Takes an ANSI-highlighted line and injects a background color
180
- * at the specified character ranges.
181
- *
182
- * @param ansiLine - ANSI-highlighted line from Shiki
183
- * @param ranges - Character ranges to apply background to [{start, end}]
184
- * @param baseBg - Base background color for the line
185
- * @param hlBg - Highlight background color for the ranges
186
- */
187
- export function injectBg(
188
- ansiLine: string,
189
- ranges: Array<{ start: number; end: number }>,
190
- baseBg: string,
191
- hlBg: string,
192
- ): string {
193
- if (ranges.length === 0) {
194
- return `${hexToBgAnsi(baseBg)}${ansiLine}\x1b[0m`;
195
- }
196
-
197
- // For simplicity, apply the highlight background to the whole line
198
- // if any ranges are specified. Character-level injection is complex
199
- // and would require parsing the ANSI stream.
200
- return `${hexToBgAnsi(hlBg)}${ansiLine}\x1b[0m`;
201
- }
202
-
203
- // ─── Adaptive Wrap ──────────────────────────────────────────────────────────────
204
-
205
- /**
206
- * Wrap rows adaptively, preserving line structure.
207
- * Returns the wrapped rows with line continuation indicators.
208
- */
209
- export function adaptiveWrapRows(rows: string[], maxWidth: number): string[] {
210
- const result: string[] = [];
211
- for (const row of rows) {
212
- if (visibleWidth(row) <= maxWidth) {
213
- result.push(row);
214
- } else {
215
- const wrapped = wrapAnsi(row, maxWidth);
216
- for (let i = 0; i < wrapped.length; i++) {
217
- result.push(i === 0 ? wrapped[i] : ` ${wrapped[i]}`);
218
- }
219
- }
220
- }
221
- return result;
222
- }
223
-
224
- // ─── Split View Heuristic ───────────────────────────────────────────────────────
225
-
226
- /**
227
- * Determine whether to use split (side-by-side) view.
228
- * Falls back to unified on narrow terminals or when wrap ratio is too high.
229
- *
230
- * @param diff - The parsed diff
231
- * @param tw - Terminal width
232
- * @param max - Maximum render lines
233
- */
234
- export function shouldUseSplit(diff: ParsedDiff, tw: number, max: number): boolean {
235
- // Too narrow for split
236
- if (tw < SPLIT_MIN_WIDTH) return false;
237
-
238
- // Calculate code column widths
239
- const codeWidth = Math.floor((tw - 6) / 2); // 6 chars for padding/line numbers
240
- if (codeWidth < SPLIT_MIN_CODE_WIDTH) return false;
241
-
242
- // Check if any lines would wrap in split mode
243
- const maxContentWidth = Math.max(
244
- ...diff.lines
245
- .filter((l) => l.type !== "hunk")
246
- .map((l) => l.content.length),
247
- 0,
248
- );
249
-
250
- // If lines are too long, unified is better
251
- if (maxContentWidth > codeWidth * 1.5) return false;
252
-
253
- return true;
254
- }
255
-
256
- // ─── Renderers ──────────────────────────────────────────────────────────────────
257
-
258
- /**
259
- * Render a diff in unified (stacked single-column) view.
260
- *
261
- * @param diff - Parsed diff
262
- * @param language - Shiki language for syntax highlighting
263
- * @param max - Maximum lines to render
264
- * @param dc - Diff colors
265
- */
266
- export function renderUnified(
267
- diff: ParsedDiff,
268
- language: string,
269
- max: number,
270
- dc: DiffColors,
271
- ): string {
272
- const lines: string[] = [];
273
- const totalLines = Math.min(diff.lines.length, max);
274
- const truncated = diff.lines.length > max;
275
-
276
- const addBg = hexToBgAnsi(dc.addBg);
277
- const remBg = hexToBgAnsi(dc.remBg);
278
- const addFg = hexToFgAnsi(dc.addFg);
279
- const remFg = hexToFgAnsi(dc.remFg);
280
- const hunkFg = hexToFgAnsi(dc.hunkFg);
281
- const headerFg = hexToFgAnsi(dc.headerFg);
282
- const reset = "\x1b[0m";
283
-
284
- for (let i = 0; i < totalLines; i++) {
285
- const line = diff.lines[i];
286
-
287
- switch (line.type) {
288
- case "hunk":
289
- lines.push(truncateToTermWidth(`${hunkFg}${line.content}${reset}`));
290
- break;
291
- case "add":
292
- lines.push(truncateToTermWidth(`${addBg}${addFg}+${reset}${addBg} ${lnum(null, 4)} ${lnum(line.newLine, 4)} │ ${line.content}${reset}`));
293
- break;
294
- case "remove":
295
- lines.push(truncateToTermWidth(`${remBg}${remFg}-${reset}${remBg} ${lnum(line.oldLine, 4)} ${lnum(null, 4)} │ ${line.content}${reset}`));
296
- break;
297
- case "context":
298
- lines.push(truncateToTermWidth(` ${headerFg}${lnum(line.oldLine, 4)} ${lnum(line.newLine, 4)}${reset} │ ${line.content}`));
299
- break;
300
- }
301
- }
302
-
303
- if (truncated) {
304
- lines.push(`${headerFg}... (${diff.lines.length - max} more lines)${reset}`);
305
- }
306
-
307
- return lines.join("\n");
308
- }
309
-
310
- /**
311
- * Render a diff in split (side-by-side) view.
312
- * Auto-falls back to unified on narrow terminals.
313
- *
314
- * @param diff - Parsed diff
315
- * @param language - Shiki language for syntax highlighting
316
- * @param max - Maximum lines to render
317
- * @param dc - Diff colors
318
- */
319
- export function renderSplit(
320
- diff: ParsedDiff,
321
- language: string,
322
- max: number,
323
- dc: DiffColors,
324
- ): string {
325
- const tw = termW();
326
-
327
- // Auto-fallback to unified
328
- if (!shouldUseSplit(diff, tw, max)) {
329
- return renderUnified(diff, language, max, dc);
330
- }
331
-
332
- const lines: string[] = [];
333
- const codeWidth = Math.floor((tw - 12) / 2); // 12 chars for line nums, separators, padding
334
- const totalLines = Math.min(diff.lines.length, max);
335
- const truncated = diff.lines.length > max;
336
-
337
- const addBg = hexToBgAnsi(dc.addBg);
338
- const remBg = hexToBgAnsi(dc.remBg);
339
- const addFg = hexToFgAnsi(dc.addFg);
340
- const remFg = hexToFgAnsi(dc.remFg);
341
- const hunkFg = hexToFgAnsi(dc.hunkFg);
342
- const headerFg = hexToFgAnsi(dc.headerFg);
343
- const reset = "\x1b[0m";
344
-
345
- // Group lines into left (old) and right (new) columns
346
- // Build the left and right columns for each visual line
347
- const leftLines: string[] = [];
348
- const rightLines: string[] = [];
349
-
350
- for (let i = 0; i < totalLines; i++) {
351
- const line = diff.lines[i];
352
-
353
- switch (line.type) {
354
- case "hunk": {
355
- // Hunk header spans both columns
356
- const padded = ` ${hunkFg}${line.content}${reset}`;
357
- const halfWidth = Math.floor(tw / 2);
358
- leftLines.push(fit(padded, halfWidth));
359
- rightLines.push("");
360
- break;
361
- }
362
- case "add": {
363
- // Left: empty; Right: added line
364
- leftLines.push(`${headerFg}${" ".repeat(codeWidth)}${reset}`);
365
- rightLines.push(`${addBg}${addFg}+${reset}${addBg} ${lnum(line.newLine, 4)} │ ${fit(line.content, codeWidth - 8)}${reset}`);
366
- break;
367
- }
368
- case "remove": {
369
- // Left: removed line; Right: empty
370
- leftLines.push(`${remBg}${remFg}-${reset}${remBg} ${lnum(line.oldLine, 4)} │ ${fit(line.content, codeWidth - 8)}${reset}`);
371
- rightLines.push(`${headerFg}${" ".repeat(codeWidth)}${reset}`);
372
- break;
373
- }
374
- case "context": {
375
- // Both columns show the same context line
376
- leftLines.push(`${headerFg}${lnum(line.oldLine, 4)}${reset} │ ${fit(line.content, codeWidth - 7)}`);
377
- rightLines.push(`${headerFg}${lnum(line.newLine, 4)}${reset} │ ${fit(line.content, codeWidth - 7)}`);
378
- break;
379
- }
380
- }
381
- }
382
-
383
- // Combine left and right columns
384
- for (let i = 0; i < leftLines.length; i++) {
385
- const left = leftLines[i];
386
- const right = rightLines[i];
387
- lines.push(truncateToTermWidth(`${left} │ ${right}`));
388
- }
389
-
390
- if (truncated) {
391
- lines.push(`${headerFg}... (${diff.lines.length - max} more lines)${reset}`);
392
- }
393
-
394
- return lines.join("\n");
395
- }
@@ -1,316 +0,0 @@
1
- /**
2
- * @pi-unipi/utility — Diff Theme System
3
- *
4
- * Color presets, resolution chain, and hex ↔ ANSI conversion for diff rendering.
5
- *
6
- * Resolution chain: env vars → per-color overrides → preset → auto-derive → hardcoded
7
- */
8
-
9
- import { readDiffSettings, type DiffSettings } from "./settings.js";
10
-
11
- // ─── Types ──────────────────────────────────────────────────────────────────────
12
-
13
- /** Diff color configuration */
14
- export interface DiffColors {
15
- /** Background for added lines */
16
- addBg: string;
17
- /** Foreground for added line content */
18
- addFg: string;
19
- /** Background for removed lines */
20
- remBg: string;
21
- /** Foreground for removed line content */
22
- remFg: string;
23
- /** Background for added word-level highlights */
24
- addWordBg: string;
25
- /** Background for removed word-level highlights */
26
- remWordBg: string;
27
- /** Hunk header foreground */
28
- hunkFg: string;
29
- /** Header info foreground */
30
- headerFg: string;
31
- }
32
-
33
- /** Diff theme preset */
34
- export interface DiffPreset {
35
- name: string;
36
- description: string;
37
- colors: DiffColors;
38
- }
39
-
40
- /** An ANSI color code (e.g. "\x1b[38;2;255;0;0m") */
41
- type AnsiColor = string;
42
-
43
- // ─── Built-in Presets ───────────────────────────────────────────────────────────
44
-
45
- const PRESETS: Record<string, DiffPreset> = {
46
- default: {
47
- name: "default",
48
- description: "Classic green/red diff colors",
49
- colors: {
50
- addBg: "#1a3a1a",
51
- addFg: "#b5e8b5",
52
- remBg: "#3a1a1a",
53
- remFg: "#e8b5b5",
54
- addWordBg: "#2d5a2d",
55
- remWordBg: "#5a2d2d",
56
- hunkFg: "#8888ff",
57
- headerFg: "#888888",
58
- },
59
- },
60
- midnight: {
61
- name: "midnight",
62
- description: "Deep blue-tinted diff colors",
63
- colors: {
64
- addBg: "#0a2a3a",
65
- addFg: "#a5d8e8",
66
- remBg: "#3a0a1a",
67
- remFg: "#e8a5c5",
68
- addWordBg: "#1a4a5a",
69
- remWordBg: "#5a1a3a",
70
- hunkFg: "#6688cc",
71
- headerFg: "#666688",
72
- },
73
- },
74
- subtle: {
75
- name: "subtle",
76
- description: "Muted, low-contrast diff colors",
77
- colors: {
78
- addBg: "#1e2e1e",
79
- addFg: "#a0b8a0",
80
- remBg: "#2e1e1e",
81
- remFg: "#b8a0a0",
82
- addWordBg: "#2a3a2a",
83
- remWordBg: "#3a2a2a",
84
- hunkFg: "#7777aa",
85
- headerFg: "#777777",
86
- },
87
- },
88
- neon: {
89
- name: "neon",
90
- description: "High-contrast vivid diff colors",
91
- colors: {
92
- addBg: "#003300",
93
- addFg: "#66ff66",
94
- remBg: "#330000",
95
- remFg: "#ff6666",
96
- addWordBg: "#005500",
97
- remWordBg: "#550000",
98
- hunkFg: "#6666ff",
99
- headerFg: "#999999",
100
- },
101
- },
102
- };
103
-
104
- // ─── Preset Access ──────────────────────────────────────────────────────────────
105
-
106
- /**
107
- * Get a diff preset by name. Falls back to "default" if not found.
108
- */
109
- export function getPreset(name: string): DiffPreset {
110
- return PRESETS[name] ?? PRESETS.default;
111
- }
112
-
113
- /**
114
- * Get all available preset names.
115
- */
116
- export function getPresetNames(): string[] {
117
- return Object.keys(PRESETS);
118
- }
119
-
120
- /**
121
- * Get all presets with their metadata.
122
- */
123
- export function getAllPresets(): DiffPreset[] {
124
- return Object.values(PRESETS);
125
- }
126
-
127
- // ─── Hex ↔ ANSI Conversion ──────────────────────────────────────────────────────
128
-
129
- /**
130
- * Parse a hex color string (#RRGGBB or #RGB) to [r, g, b].
131
- */
132
- export function hexToRgb(hex: string): [number, number, number] {
133
- let h = hex.replace(/^#/, "");
134
- if (h.length === 3) {
135
- h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
136
- }
137
- const r = parseInt(h.substring(0, 2), 16);
138
- const g = parseInt(h.substring(2, 4), 16);
139
- const b = parseInt(h.substring(4, 6), 16);
140
- return [r, g, b];
141
- }
142
-
143
- /**
144
- * Convert [r, g, b] to hex string (#RRGGBB).
145
- */
146
- export function rgbToHex(r: number, g: number, b: number): string {
147
- const toHex = (n: number) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
148
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
149
- }
150
-
151
- /**
152
- * Convert a hex color to ANSI 24-bit foreground escape.
153
- */
154
- export function hexToFgAnsi(hex: string): AnsiColor {
155
- const [r, g, b] = hexToRgb(hex);
156
- return `\x1b[38;2;${r};${g};${b}m`;
157
- }
158
-
159
- /**
160
- * Convert a hex color to ANSI 24-bit background escape.
161
- */
162
- export function hexToBgAnsi(hex: string): AnsiColor {
163
- const [r, g, b] = hexToRgb(hex);
164
- return `\x1b[48;2;${r};${g};${b}m`;
165
- }
166
-
167
- /**
168
- * Extract hex color from an ANSI 24-bit foreground escape sequence.
169
- * Returns null if not a 24-bit color.
170
- */
171
- export function ansiFgToHex(ansi: string): string | null {
172
- const match = ansi.match(/\x1b\[38;2;(\d+);(\d+);(\d+)m/);
173
- if (!match) return null;
174
- return rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3]));
175
- }
176
-
177
- /**
178
- * Extract hex color from an ANSI 24-bit background escape sequence.
179
- * Returns null if not a 24-bit color.
180
- */
181
- export function ansiBgToHex(ansi: string): string | null {
182
- const match = ansi.match(/\x1b\[48;2;(\d+);(\d+);(\d+)m/);
183
- if (!match) return null;
184
- return rgbToHex(parseInt(match[1]), parseInt(match[2]), parseInt(match[3]));
185
- }
186
-
187
- // ─── Color Resolution ───────────────────────────────────────────────────────────
188
-
189
- /**
190
- * Load the diff configuration from settings.
191
- */
192
- export function loadDiffConfig(): DiffSettings {
193
- return readDiffSettings();
194
- }
195
-
196
- /**
197
- * Mix a foreground color with a background color at a given ratio.
198
- * Used for auto-deriving diff backgrounds from pi theme accents.
199
- */
200
- export function mixColors(fg: string, bg: string, ratio: number): string {
201
- const [fr, fg_, fb] = hexToRgb(fg);
202
- const [br, bg_, bb] = hexToRgb(bg);
203
- const r = Math.round(fr * ratio + br * (1 - ratio));
204
- const g = Math.round(fg_ * ratio + bg_ * (1 - ratio));
205
- const b = Math.round(fb * ratio + bb * (1 - ratio));
206
- return rgbToHex(r, g, b);
207
- }
208
-
209
- /**
210
- * Auto-derive diff background colors from a pi theme.
211
- * Mixes accent/success/error colors with a base background.
212
- */
213
- export function autoDeriveBgFromTheme(theme: any): DiffColors | null {
214
- try {
215
- // Try to get theme colors from pi's Theme object
216
- const baseBg = theme?.colors?.customMessageBg || theme?.colors?.background || "#1a1a2e";
217
- const successColor = theme?.colors?.toolSuccess || theme?.colors?.success || "#22c55e";
218
- const errorColor = theme?.colors?.toolError || theme?.colors?.error || "#ef4444";
219
-
220
- return {
221
- addBg: mixColors(successColor, baseBg, 0.15),
222
- addFg: mixColors(successColor, "#ffffff", 0.7),
223
- remBg: mixColors(errorColor, baseBg, 0.15),
224
- remFg: mixColors(errorColor, "#ffffff", 0.7),
225
- addWordBg: mixColors(successColor, baseBg, 0.25),
226
- remWordBg: mixColors(errorColor, baseBg, 0.25),
227
- hunkFg: "#8888ff",
228
- headerFg: "#888888",
229
- };
230
- } catch {
231
- return null;
232
- }
233
- }
234
-
235
- /**
236
- * Check if a value looks like a hex color.
237
- */
238
- function isHexColor(v: string): boolean {
239
- return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(v);
240
- }
241
-
242
- /**
243
- * Resolve diff colors using the full chain:
244
- * env vars → per-color overrides → preset → auto-derive → hardcoded
245
- *
246
- * @param theme - Optional pi Theme object for auto-derivation
247
- */
248
- export function resolveDiffColors(theme?: any): DiffColors {
249
- const config = loadDiffConfig();
250
- const preset = getPreset(config.theme);
251
-
252
- // Start with preset colors
253
- let colors = { ...preset.colors };
254
-
255
- // Layer: auto-derive from pi theme (if available)
256
- if (theme) {
257
- const derived = autoDeriveBgFromTheme(theme);
258
- if (derived) {
259
- // Auto-derived colors fill in where preset has defaults
260
- colors = {
261
- addBg: colors.addBg || derived.addBg,
262
- addFg: colors.addFg || derived.addFg,
263
- remBg: colors.remBg || derived.remBg,
264
- remFg: colors.remFg || derived.remFg,
265
- addWordBg: colors.addWordBg || derived.addWordBg,
266
- remWordBg: colors.remWordBg || derived.remWordBg,
267
- hunkFg: colors.hunkFg || derived.hunkFg,
268
- headerFg: colors.headerFg || derived.headerFg,
269
- };
270
- }
271
- }
272
-
273
- // Layer: environment variable overrides
274
- const envMap: Record<string, keyof DiffColors> = {
275
- DIFF_ADD_BG: "addBg",
276
- DIFF_ADD_FG: "addFg",
277
- DIFF_REM_BG: "remBg",
278
- DIFF_REM_FG: "remFg",
279
- DIFF_ADD_WORD_BG: "addWordBg",
280
- DIFF_REM_WORD_BG: "remWordBg",
281
- DIFF_HUNK_FG: "hunkFg",
282
- DIFF_HEADER_FG: "headerFg",
283
- };
284
-
285
- for (const [envKey, colorKey] of Object.entries(envMap)) {
286
- const envVal = process.env[envKey];
287
- if (envVal && isHexColor(envVal)) {
288
- colors[colorKey] = envVal;
289
- }
290
- }
291
-
292
- // Layer: per-color overrides from settings (if we add diffColors to settings later)
293
- // Currently deferred — spec says "Out of Scope"
294
-
295
- return colors;
296
- }
297
-
298
- /**
299
- * Apply the diff palette to create ANSI escape functions.
300
- * Returns an object with helper functions for coloring diff output.
301
- */
302
- export function applyDiffPalette(theme?: any) {
303
- const colors = resolveDiffColors(theme);
304
-
305
- return {
306
- colors,
307
- addBg: (s: string) => `${hexToBgAnsi(colors.addBg)}${s}\x1b[0m`,
308
- addFg: (s: string) => `${hexToFgAnsi(colors.addFg)}${s}\x1b[0m`,
309
- remBg: (s: string) => `${hexToBgAnsi(colors.remBg)}${s}\x1b[0m`,
310
- remFg: (s: string) => `${hexToFgAnsi(colors.remFg)}${s}\x1b[0m`,
311
- addWordBg: (s: string) => `${hexToBgAnsi(colors.addWordBg)}${s}\x1b[0m`,
312
- remWordBg: (s: string) => `${hexToBgAnsi(colors.remWordBg)}${s}\x1b[0m`,
313
- hunkFg: (s: string) => `${hexToFgAnsi(colors.hunkFg)}${s}\x1b[0m`,
314
- headerFg: (s: string) => `${hexToFgAnsi(colors.headerFg)}${s}\x1b[0m`,
315
- };
316
- }