@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
@@ -1,339 +0,0 @@
1
- /**
2
- * @pi-unipi/utility — Diff Tool Wrapper
3
- *
4
- * Wraps the default Pi write/edit tools with Shiki-powered diff rendering.
5
- * When enabled, enhanced tools are registered that:
6
- * 1. Read old content before write
7
- * 2. Delegate to the original tool
8
- * 3. Compute diff
9
- * 4. Store diff in result.details for async rendering
10
- */
11
-
12
- import * as fs from "node:fs";
13
- import * as path from "node:path";
14
- import type { ExtensionAPI, ToolDefinition, AgentToolResult } from "@earendil-works/pi-coding-agent";
15
- import { visibleWidth as piVisibleWidth, truncateToWidth as piTruncateToWidth } from "@earendil-works/pi-tui";
16
- import { readDiffSettings } from "./settings.js";
17
- import { parseDiff } from "./parser.js";
18
- import { resolveDiffColors, applyDiffPalette } from "./theme.js";
19
- import { renderSplit, renderUnified, termW, SPLIT_MIN_WIDTH, truncateToTermWidth } from "./renderer.js";
20
- import { detectLanguageFromPath, hlBlock, MAX_HL_CHARS } from "./highlighter.js";
21
-
22
- // ─── Types ──────────────────────────────────────────────────────────────────────
23
-
24
- /** Extended tool result with diff data */
25
- interface DiffToolDetails {
26
- /** Old file content (null for new files) */
27
- oldContent: string | null;
28
- /** New file content */
29
- newContent: string;
30
- /** Parsed diff */
31
- diff: ReturnType<typeof parseDiff>;
32
- /** File path */
33
- filePath: string;
34
- /** Detected language */
35
- language: string;
36
- }
37
-
38
- /** Edit operation from the edit tool */
39
- export interface EditOperation {
40
- oldText: string;
41
- newText: string;
42
- }
43
-
44
- // ─── Helpers ────────────────────────────────────────────────────────────────────
45
-
46
- /**
47
- * Normalize edit tool input to get the list of edit operations.
48
- * Handles both single-edit and multi-edit parameter formats.
49
- */
50
- export function getEditOperations(input: any): EditOperation[] {
51
- if (Array.isArray(input?.edits)) {
52
- return input.edits.map((e: any) => ({
53
- oldText: e.oldText ?? e.old_text ?? "",
54
- newText: e.newText ?? e.new_text ?? "",
55
- }));
56
- }
57
- if (input?.oldText !== undefined || input?.old_text !== undefined) {
58
- return [{
59
- oldText: input.oldText ?? input.old_text ?? "",
60
- newText: input.newText ?? input.new_text ?? "",
61
- }];
62
- }
63
- return [];
64
- }
65
-
66
- /**
67
- * Summarize edit operations into aggregate diff stats.
68
- */
69
- export function summarizeEditOperations(operations: EditOperation[]): {
70
- totalEdits: number;
71
- totalAdditions: number;
72
- totalDeletions: number;
73
- } {
74
- let totalAdditions = 0;
75
- let totalDeletions = 0;
76
-
77
- for (const op of operations) {
78
- const oldLines = op.oldText.split("\n");
79
- const newLines = op.newText.split("\n");
80
- totalDeletions += oldLines.length;
81
- totalAdditions += newLines.length;
82
- }
83
-
84
- return {
85
- totalEdits: operations.length,
86
- totalAdditions,
87
- totalDeletions,
88
- };
89
- }
90
-
91
- /**
92
- * Read file content safely. Returns null if file doesn't exist.
93
- */
94
- function readFileSafe(filePath: string): string | null {
95
- try {
96
- if (fs.existsSync(filePath)) {
97
- return fs.readFileSync(filePath, "utf-8");
98
- }
99
- } catch {
100
- // Ignore read errors
101
- }
102
- return null;
103
- }
104
-
105
- // ─── Tool Registration ──────────────────────────────────────────────────────────
106
-
107
- /**
108
- * Register the enhanced write tool that wraps the default with diff rendering.
109
- */
110
- export function registerEnhancedWriteTool(pi: ExtensionAPI, cwd: string): void {
111
- // We need to re-register a tool with the same name "write" to override it.
112
- // The approach: register our own tool that reads old content, writes the file,
113
- // computes the diff, and stores it for rendering.
114
-
115
- pi.registerTool({
116
- name: "write",
117
- label: "Write File",
118
- description: "Write content to a file at the given path. Creates parent directories if needed. Shows a syntax-highlighted diff of the changes.",
119
- parameters: {
120
- type: "object",
121
- properties: {
122
- path: { type: "string", description: "Path to the file to write" },
123
- content: { type: "string", description: "Content to write to the file" },
124
- },
125
- required: ["path", "content"],
126
- } as any,
127
- async execute(toolCallId: string, params: any, signal: any, _onUpdate: any, _ctx: any): Promise<any> {
128
- const { path: filePath, content } = params;
129
- const absolutePath = path.resolve(cwd, filePath);
130
- const dir = path.dirname(absolutePath);
131
-
132
- // Read old content before write
133
- const oldContent = readFileSafe(absolutePath);
134
-
135
- // Write the file
136
- if (!fs.existsSync(dir)) {
137
- fs.mkdirSync(dir, { recursive: true });
138
- }
139
- fs.writeFileSync(absolutePath, content, "utf-8");
140
-
141
- // Compute diff
142
- const language = detectLanguageFromPath(filePath);
143
- const diff = parseDiff(oldContent ?? "", content, 3, filePath, filePath);
144
-
145
- return {
146
- content: [
147
- { type: "text", text: `Successfully wrote ${content.length} bytes to ${filePath}` },
148
- ],
149
- details: {
150
- oldContent,
151
- newContent: content,
152
- diff,
153
- filePath,
154
- language,
155
- } as DiffToolDetails,
156
- };
157
- },
158
- renderResult(result: any, _options: any, theme: any): any {
159
- const details = result?.details as DiffToolDetails | undefined;
160
- if (!details || !details.diff || !details.diff.lines || details.diff.lines.length === 0) {
161
- // Error or empty-diff case: render the message from result.content so the
162
- // user sees "Could not find text to replace..." etc. Never return null here
163
- // because Container.render() will crash on null child.
164
- const msg = result?.content?.[0]?.text ?? "";
165
- return {
166
- setText: () => {},
167
- text: msg,
168
- render: (width: number) => (width > 0 ? [msg.slice(0, width)] : [msg]),
169
- } as any;
170
- }
171
-
172
- try {
173
- const dc = resolveDiffColors(theme);
174
- const tw = termW();
175
- const max = 60;
176
-
177
- const rendered: string = tw >= SPLIT_MIN_WIDTH
178
- ? renderSplit(details.diff, details.language, max, dc)
179
- : renderUnified(details.diff, details.language, max, dc);
180
-
181
- // Split into lines and cache for width-aware rendering.
182
- // Each line is already truncated to terminal width by
183
- // truncateToTermWidth() in the renderer, but we also
184
- // respect the width parameter from Box.render().
185
- const cachedLines = rendered.split("\n");
186
-
187
- return {
188
- setText: () => {},
189
- text: rendered,
190
- render: (width: number) => {
191
- // If width is provided, re-truncate lines that
192
- // still exceed it (e.g., inside nested Boxes)
193
- const maxW = width > 0 ? width : tw;
194
- return cachedLines.map((line: string) => {
195
- if (piVisibleWidth(line) > maxW) {
196
- return piTruncateToWidth(line, maxW, "…");
197
- }
198
- return line;
199
- });
200
- },
201
- } as any;
202
- } catch {
203
- const fallback = "(diff rendering failed)";
204
- return {
205
- setText: () => {},
206
- text: fallback,
207
- render: (width: number) => (width > 0 ? [fallback.slice(0, width)] : [fallback]),
208
- } as any;
209
- }
210
- },
211
- });
212
- }
213
-
214
- /**
215
- * Register the enhanced edit tool that wraps the default with diff rendering.
216
- */
217
- export function registerEnhancedEditTool(pi: ExtensionAPI, cwd: string): void {
218
- pi.registerTool({
219
- name: "edit",
220
- label: "Edit File",
221
- description: "Edit a file by replacing text. Shows a syntax-highlighted diff of the changes.",
222
- parameters: {
223
- type: "object",
224
- properties: {
225
- path: { type: "string", description: "Path to the file to edit" },
226
- edits: {
227
- type: "array",
228
- items: {
229
- type: "object",
230
- properties: {
231
- oldText: { type: "string", description: "Text to replace" },
232
- newText: { type: "string", description: "Replacement text" },
233
- },
234
- required: ["oldText", "newText"],
235
- },
236
- description: "Array of edit operations",
237
- },
238
- },
239
- required: ["path", "edits"],
240
- } as any,
241
- async execute(toolCallId: string, params: any, signal: any, _onUpdate: any, _ctx: any): Promise<any> {
242
- const { path: filePath, edits } = params;
243
- const absolutePath = path.resolve(cwd, filePath);
244
-
245
- // Read old content
246
- const oldContent = readFileSafe(absolutePath);
247
- if (oldContent === null) {
248
- return {
249
- content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
250
- details: undefined,
251
- isError: true,
252
- };
253
- }
254
-
255
- // Apply edits
256
- let newContent = oldContent;
257
- const operations = getEditOperations(params);
258
- for (const op of operations) {
259
- const idx = newContent.indexOf(op.oldText);
260
- if (idx === -1) {
261
- return {
262
- content: [{ type: "text", text: `Error: Could not find text to replace in ${filePath}` }],
263
- details: undefined,
264
- isError: true,
265
- };
266
- }
267
- newContent = newContent.substring(0, idx) + op.newText + newContent.substring(idx + op.oldText.length);
268
- }
269
-
270
- // Write the modified content
271
- fs.writeFileSync(absolutePath, newContent, "utf-8");
272
-
273
- // Compute diff
274
- const language = detectLanguageFromPath(filePath);
275
- const diff = parseDiff(oldContent, newContent, 3, filePath, filePath);
276
- const summary = summarizeEditOperations(operations);
277
-
278
- return {
279
- content: [
280
- { type: "text", text: `Successfully edited ${filePath} (${summary.totalEdits} edit${summary.totalEdits !== 1 ? "s" : ""})` },
281
- ],
282
- details: {
283
- oldContent,
284
- newContent,
285
- diff,
286
- filePath,
287
- language,
288
- } as DiffToolDetails,
289
- };
290
- },
291
- renderResult(result: any, _options: any, theme: any): any {
292
- const details = result?.details as DiffToolDetails | undefined;
293
- if (!details || !details.diff || !details.diff.lines || details.diff.lines.length === 0) {
294
- // Error or empty-diff case: render the message from result.content so the
295
- // user sees "Could not find text to replace..." etc. Never return null here
296
- // because Container.render() will crash on null child.
297
- const msg = result?.content?.[0]?.text ?? "";
298
- return {
299
- setText: () => {},
300
- text: msg,
301
- render: (width: number) => (width > 0 ? [msg.slice(0, width)] : [msg]),
302
- } as any;
303
- }
304
-
305
- try {
306
- const dc = resolveDiffColors(theme);
307
- const tw = termW();
308
- const max = 60;
309
-
310
- const rendered: string = tw >= SPLIT_MIN_WIDTH
311
- ? renderSplit(details.diff, details.language, max, dc)
312
- : renderUnified(details.diff, details.language, max, dc);
313
-
314
- const cachedLines = rendered.split("\n");
315
-
316
- return {
317
- setText: () => {},
318
- text: rendered,
319
- render: (width: number) => {
320
- const maxW = width > 0 ? width : tw;
321
- return cachedLines.map((line: string) => {
322
- if (piVisibleWidth(line) > maxW) {
323
- return piTruncateToWidth(line, maxW, "…");
324
- }
325
- return line;
326
- });
327
- },
328
- } as any;
329
- } catch {
330
- const fallback = "(diff rendering failed)";
331
- return {
332
- setText: () => {},
333
- text: fallback,
334
- render: (width: number) => (width > 0 ? [fallback.slice(0, width)] : [fallback]),
335
- } as any;
336
- }
337
- },
338
- });
339
- }
@@ -1,214 +0,0 @@
1
- /**
2
- * @pi-unipi/utility — Terminal Capabilities Detection
3
- *
4
- * Detect terminal features for optimal rendering:
5
- * - Color support (basic, 256, truecolor)
6
- * - Nerd Font detection
7
- * - Unicode support
8
- * - Terminal dimensions
9
- */
10
-
11
- import type { TerminalCapabilities } from "../types.js";
12
-
13
- /** Cached capabilities per process */
14
- let cachedCapabilities: TerminalCapabilities | null = null;
15
- let cacheTimestamp = 0;
16
- const CACHE_TTL_MS = 5000; // Re-detect every 5s
17
-
18
- /** Detect color support level */
19
- function detectColorSupport(): { color: boolean; truecolor: boolean } {
20
- const env = process.env;
21
-
22
- // No color
23
- if (env.NO_COLOR || env.NODE_DISABLE_COLORS) {
24
- return { color: false, truecolor: false };
25
- }
26
-
27
- // Force color
28
- if (env.FORCE_COLOR) {
29
- const level = parseInt(env.FORCE_COLOR, 10);
30
- return {
31
- color: level >= 1,
32
- truecolor: level >= 3,
33
- };
34
- }
35
-
36
- // CI environments typically support colors
37
- if (env.CI) {
38
- return { color: true, truecolor: false };
39
- }
40
-
41
- // Terminal emulator detection
42
- const term = env.TERM || "";
43
- const termProgram = env.TERM_PROGRAM || "";
44
-
45
- // Apple Terminal.app does NOT support 24-bit truecolor (longstanding
46
- // limitation as of macOS 26). Force 256-colour even if some wrapper
47
- // leaked COLORTERM=truecolor into the environment.
48
- const isAppleTerminal = termProgram === "Apple_Terminal";
49
-
50
- // Truecolor TERM_PROGRAM allow-list. Note: terminfo names like
51
- // "xterm-256color" / "screen-256color" / "tmux-256color" advertise
52
- // 256-colour, NOT truecolor — they must not appear here.
53
- const truecolorTermPrograms = [
54
- "iTerm.app",
55
- "WezTerm",
56
- "Alacritty",
57
- "vscode",
58
- "Hyper",
59
- "Warp",
60
- "ghostty",
61
- "Ghostty",
62
- "zed",
63
- "Zed",
64
- "cursor",
65
- "Cursor",
66
- ];
67
-
68
- // Truecolor TERM tokens (the rare TERM that *does* advertise truecolor).
69
- const truecolorTermTokens = ["truecolor", "24bit", "alacritty", "kitty", "wezterm"];
70
-
71
- const hasTruecolor = isAppleTerminal
72
- ? false
73
- : env.COLORTERM === "truecolor" ||
74
- env.COLORTERM === "24bit" ||
75
- truecolorTermPrograms.includes(termProgram) ||
76
- truecolorTermTokens.some((t) => term.includes(t));
77
-
78
- // Basic color support
79
- const hasColor =
80
- hasTruecolor ||
81
- term.includes("color") ||
82
- term.includes("ansi") ||
83
- term.includes("xterm") ||
84
- term.includes("screen") ||
85
- term.includes("tmux") ||
86
- termProgram.length > 0;
87
-
88
- return { color: hasColor, truecolor: hasTruecolor };
89
- }
90
-
91
- /** Detect Nerd Font support */
92
- function detectNerdFont(): boolean {
93
- const env = process.env;
94
-
95
- // Explicit override
96
- if (env.NERD_FONT === "1" || env.NERD_FONT === "true") {
97
- return true;
98
- }
99
- if (env.NERD_FONT === "0" || env.NERD_FONT === "false") {
100
- return false;
101
- }
102
-
103
- // Terminal emulator hints
104
- const termProgram = env.TERM_PROGRAM || "";
105
- const knownNerdFontTerminals = [
106
- "iTerm.app",
107
- "WezTerm",
108
- "Alacritty",
109
- "Kitty",
110
- "Ghostty",
111
- "Warp",
112
- ];
113
-
114
- if (knownNerdFontTerminals.some((t) => termProgram.includes(t))) {
115
- return true;
116
- }
117
-
118
- // Default to false for safety
119
- return false;
120
- }
121
-
122
- /** Detect Unicode support level */
123
- function detectUnicode(): "none" | "basic" | "full" {
124
- const env = process.env;
125
-
126
- // Explicit override
127
- if (env.UNICODE === "0" || env.NO_UNICODE) {
128
- return "none";
129
- }
130
-
131
- // LANG/LC_ALL hints
132
- const locale = env.LANG || env.LC_ALL || env.LC_CTYPE || "";
133
- if (locale.includes("UTF-8") || locale.includes("utf8")) {
134
- return "full";
135
- }
136
-
137
- // Windows CMD typically has limited Unicode
138
- if (process.platform === "win32" && !env.WT_SESSION) {
139
- return "basic";
140
- }
141
-
142
- // Default to basic (safe middle ground)
143
- return "basic";
144
- }
145
-
146
- /** Get terminal dimensions */
147
- function getTerminalSize(): { width: number; height: number } {
148
- const stdout = process.stdout;
149
- if (stdout && stdout.isTTY) {
150
- const cols = stdout.columns || 80;
151
- const rows = stdout.rows || 24;
152
- return { width: cols, height: rows };
153
- }
154
- return { width: 80, height: 24 };
155
- }
156
-
157
- /**
158
- * Detect terminal capabilities.
159
- * Results are cached for CACHE_TTL_MS to avoid repeated detection.
160
- */
161
- export function detectCapabilities(): TerminalCapabilities {
162
- const now = Date.now();
163
- if (cachedCapabilities && now - cacheTimestamp < CACHE_TTL_MS) {
164
- // Update dimensions even when cached (they change on resize)
165
- const size = getTerminalSize();
166
- return {
167
- ...cachedCapabilities,
168
- width: size.width,
169
- height: size.height,
170
- };
171
- }
172
-
173
- const colorSupport = detectColorSupport();
174
- const size = getTerminalSize();
175
-
176
- cachedCapabilities = {
177
- color: colorSupport.color,
178
- truecolor: colorSupport.truecolor,
179
- nerdFont: detectNerdFont(),
180
- unicode: detectUnicode(),
181
- width: size.width,
182
- height: size.height,
183
- };
184
-
185
- cacheTimestamp = now;
186
- return cachedCapabilities;
187
- }
188
-
189
- /** Force re-detection of capabilities */
190
- export function refreshCapabilities(): TerminalCapabilities {
191
- cachedCapabilities = null;
192
- cacheTimestamp = 0;
193
- return detectCapabilities();
194
- }
195
-
196
- /** Check if a specific capability is available */
197
- export function hasCapability(
198
- cap: keyof TerminalCapabilities,
199
- ): boolean {
200
- const caps = detectCapabilities();
201
- const value = caps[cap];
202
- if (typeof value === "boolean") {
203
- return value;
204
- }
205
- if (typeof value === "string") {
206
- return value !== "none";
207
- }
208
- return false;
209
- }
210
-
211
- /** Get safe icon based on Nerd Font availability */
212
- export function getIcon(nerdFont: string, fallback: string): string {
213
- return detectCapabilities().nerdFont ? nerdFont : fallback;
214
- }