@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,88 +0,0 @@
1
- ---
2
- name: cocoindex
3
- description: "CocoIndex content indexing and search — use when you need to search indexed project content or trigger re-indexing"
4
- ---
5
-
6
- # CocoIndex Skill
7
-
8
- ## When to Use CocoIndex
9
-
10
- Use CocoIndex tools and commands when you need to:
11
-
12
- 1. **Search indexed project content** — use `cocoindex_search` instead of `content_search` or `ctx_search`
13
- 2. **Trigger re-indexing** — use `/unipi:cocoindex-update` after significant code changes
14
- 3. **Initialize indexing** — use `/unipi:cocoindex-init` for new projects
15
-
16
- ## Key Differences from Old FTS5 Content Store
17
-
18
- | Aspect | Old (FTS5) | New (CocoIndex) |
19
- |--------|-----------|-----------------|
20
- | Chunking | Heading/paragraph split | AST-aware code, recursive text |
21
- | Search | BM25 + trigram fuzzy | Semantic vector + full-text |
22
- | Incremental | Full re-index every time | Delta-only (changed files) |
23
- | Scale | Small corpus | Multi-GiB, parallel |
24
- | Store | SQLite FTS5 | LanceDB |
25
-
26
- ## Tools
27
-
28
- ### `cocoindex_search`
29
- Search indexed content. It uses semantic vector search when the LanceDB table has vectors, LanceDB full-text search when an inverted index exists, and a lexical fallback for older text-only indexes.
30
-
31
- ```
32
- cocoindex_search({ query: "how authentication works", limit: 10 })
33
- ```
34
-
35
- Returns results with `title`, `content`, `source`, `rank`, `contentType`, and `matchLayer`.
36
-
37
- ### `cocoindex_status`
38
- Check indexing status — pipeline configured, last run, doc count, CLI availability.
39
-
40
- ```
41
- cocoindex_status({})
42
- ```
43
-
44
- ## Commands
45
-
46
- | Command | Description |
47
- |---------|-------------|
48
- | `/unipi:cocoindex-update` | Run `cocoindex update` on current project |
49
- | `/unipi:cocoindex-status` | Show indexing status |
50
- | `/unipi:cocoindex-init` | Scaffold a default pipeline |
51
- | `/unipi:cocoindex-search <query>` | Search indexed codebase semantically |
52
- | `/unipi:cocoindex-settings` | Show configuration |
53
-
54
- ## Prerequisites
55
-
56
- 1. **Python 3.10+** with `cocoindex` installed:
57
- ```bash
58
- pip install cocoindex
59
- pip install 'cocoindex[lancedb]'
60
- ```
61
-
62
- 2. **Pipeline initialized** — run `/unipi:cocoindex-init` once per project
63
-
64
- 3. **Embedding config** — CocoIndex reuses the memory package's embedding settings:
65
- - API key from `~/.unipi/memory/config.json`
66
- - Same model and dimensions for cross-system search
67
-
68
- ## Architecture
69
-
70
- ```
71
- Project files → LocalFile source → SplitRecursively → EmbedText → LanceDB
72
- ↓
73
- Agent query ──────────────────────────────→ vector search → ranked results
74
- ```
75
-
76
- - Pipeline defined in `.unipi/cocoindex/main.py` (auto-generated, customizable)
77
- - Data stored in `.unipi/cocoindex/.lancedb/`
78
- - Search queries LanceDB directly via Node.js SDK (no CLI round-trip)
79
-
80
- ## Troubleshooting
81
-
82
- | Issue | Fix |
83
- |-------|-----|
84
- | "CLI not found" | `pip install cocoindex` |
85
- | "Pipeline not initialized" | `/unipi:cocoindex-init` |
86
- | "Search unavailable" | `npm install @lancedb/lancedb` |
87
- | "No results" | Run `/unipi:cocoindex-update` first |
88
- | Embedding errors | Check `~/.unipi/memory/config.json` API key |
@@ -1,157 +0,0 @@
1
- /**
2
- * tools.ts — CocoIndex tool registration
3
- *
4
- * Exposes cocoindex operations as Pi agent tools:
5
- * - cocoindex_search: Search indexed content
6
- * - cocoindex_status: Show indexing status
7
- */
8
-
9
- import { Type } from "typebox";
10
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
- import { COCOINDEX_PACKAGE_SPEC, COCOINDEX_TOOLS } from "@pi-unipi/core";
12
- import * as bridge from "./bridge.js";
13
-
14
- export interface CocoindexToolDeps {
15
- getProjectDir(ctx: ExtensionContext): string;
16
- }
17
-
18
- // ─────────────────────────────────────────────────────────
19
- // Schemas
20
- // ─────────────────────────────────────────────────────────
21
-
22
- export const MAX_SEARCH_RESULTS = 50;
23
-
24
- export function normalizeSearchPage(limit?: number, offset?: number): { limit: number; offset: number } {
25
- const requestedLimit = Number.isFinite(limit) ? Math.floor(limit!) : 10;
26
- const requestedOffset = Number.isFinite(offset) ? Math.floor(offset!) : 0;
27
- return {
28
- limit: Math.min(MAX_SEARCH_RESULTS, Math.max(1, requestedLimit)),
29
- offset: Math.max(0, requestedOffset),
30
- };
31
- }
32
-
33
- const SearchParams = Type.Object({
34
- query: Type.String({ description: "Search query against indexed content" }),
35
- limit: Type.Optional(Type.Number({
36
- description: `Max results (default 10, hard cap ${MAX_SEARCH_RESULTS})`,
37
- minimum: 1,
38
- maximum: MAX_SEARCH_RESULTS,
39
- })),
40
- offset: Type.Optional(Type.Number({ description: "Pagination offset", minimum: 0 })),
41
- });
42
-
43
- const StatusParams = Type.Object({});
44
-
45
- // ─────────────────────────────────────────────────────────
46
- // Helpers
47
- // ─────────────────────────────────────────────────────────
48
-
49
- function textResult(text: string, details?: Record<string, unknown>): any {
50
- return {
51
- content: [{ type: "text", text }],
52
- details,
53
- };
54
- }
55
-
56
- // ─────────────────────────────────────────────────────────
57
- // Registration
58
- // ─────────────────────────────────────────────────────────
59
-
60
- export function registerCocoindexTools(pi: ExtensionAPI, deps: CocoindexToolDeps): void {
61
- // cocoindex_search — search indexed content via LanceDB
62
- pi.registerTool({
63
- name: COCOINDEX_TOOLS.SEARCH,
64
- label: "CocoIndex Search",
65
- description:
66
- "Search indexed content using semantic vector search when available, with full-text/lexical fallbacks. " +
67
- "Diagnostic/search only: this tool never installs CocoIndex. " +
68
- "Use /unipi:cocoindex-init to set up/install, then /unipi:cocoindex-update to index.",
69
- parameters: SearchParams,
70
- async execute(
71
- _toolCallId: string,
72
- params: any,
73
- _signal: AbortSignal | undefined,
74
- _onUpdate: unknown,
75
- ctx: ExtensionContext,
76
- ): Promise<any> {
77
- try {
78
- const available = await bridge.isAvailable();
79
- if (!available) {
80
- return textResult(
81
- "Search Unavailable: CocoIndex CLI is not installed. " +
82
- "Run /unipi:cocoindex-init for guided install, or manually run " +
83
- `uv tool install '${COCOINDEX_PACKAGE_SPEC}'.`,
84
- { cliAvailable: false },
85
- );
86
- }
87
-
88
- // Clamp in execution as well as schema validation: Pi extensions may
89
- // mutate tool inputs after validation and custom hosts may skip it.
90
- const { limit, offset } = normalizeSearchPage(params.limit, params.offset);
91
- const results = await bridge.search(deps.getProjectDir(ctx), params.query, {
92
- limit,
93
- offset,
94
- });
95
-
96
- if (results.length === 0) {
97
- return textResult(
98
- `No results for "${params.query}" in the current CocoIndex data. ` +
99
- "If this seems wrong, run /unipi:cocoindex-status to confirm the pipeline has data, then /unipi:cocoindex-update to refresh it.",
100
- );
101
- }
102
-
103
- const lines = results.map(
104
- (r, i) =>
105
- `[${i + 1}] ${r.title} (${r.matchLayer}, rank: ${r.rank.toFixed(3)})\n${r.content.slice(0, 300)}`,
106
- );
107
- return textResult(
108
- `Found ${results.length} results for "${params.query}":\n\n${lines.join("\n\n")}`,
109
- // Avoid duplicating full result content in model-visible details.
110
- { count: results.length, query: params.query, limit, offset },
111
- );
112
- } catch (err) {
113
- return textResult(`CocoIndex search error: ${err}`, { error: true });
114
- }
115
- },
116
- } as any);
117
-
118
- // cocoindex_status — show indexing status
119
- pi.registerTool({
120
- name: COCOINDEX_TOOLS.STATUS,
121
- label: "CocoIndex Status",
122
- description: "Check CocoIndex indexing status. Diagnostic only; use commands for interactive install/update.",
123
- parameters: StatusParams,
124
- async execute(
125
- _toolCallId: string,
126
- _params: Record<string, never>,
127
- _signal: AbortSignal | undefined,
128
- _onUpdate: unknown,
129
- ctx: ExtensionContext,
130
- ): Promise<any> {
131
- try {
132
- const info = await bridge.status(deps.getProjectDir(ctx));
133
- const lines = [
134
- `📦 CocoIndex Status`,
135
- `CLI available: ${info.cliAvailable ? "✅" : "❌"}`,
136
- `Pipeline configured: ${info.pipelineConfigured ? "✅" : "❌"}`,
137
- `Target store: ${info.targetStore}`,
138
- `Indexed: ${info.indexed ? "✅" : "— (no data)"}`,
139
- `Doc count: ${info.docCount}`,
140
- `Last run: ${info.lastRun ?? "never"}`,
141
- ];
142
- if (!info.cliAvailable) {
143
- lines.push(
144
- "",
145
- "Install guidance:",
146
- " • Run /unipi:cocoindex-init for guided install.",
147
- ` • Manual: uv tool install '${COCOINDEX_PACKAGE_SPEC}'`,
148
- " • If uv is missing and mise is available: mise use -g uv@latest",
149
- );
150
- }
151
- return textResult(lines.join("\n"), info as unknown as Record<string, unknown>);
152
- } catch (err) {
153
- return textResult(`CocoIndex status error: ${err}`, { error: true });
154
- }
155
- },
156
- } as any);
157
- }
@@ -1,281 +0,0 @@
1
- /**
2
- * Diff rendering engine — LCS-based diff with 3 layouts, 3 indicators,
3
- * syntax highlighting, and Nerd Font detection
4
- */
5
-
6
- import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
7
-
8
- export type DiffLayout = "auto" | "split" | "unified";
9
- export type DiffIndicator = "bars" | "classic" | "nerd" | "none";
10
-
11
- // --- Nerd Font Detection ---
12
-
13
- let nerdFontDetected: boolean | null = null;
14
-
15
- /** Detect if terminal supports Nerd Font icons */
16
- export function detectNerdFont(): boolean {
17
- if (nerdFontDetected !== null) return nerdFontDetected;
18
- // Check common Nerd Font env vars or terminal emulators
19
- const term = process.env.TERM_PROGRAM ?? "";
20
- const termFont = process.env.TERM_FONT ?? "";
21
- nerdFontDetected =
22
- termFont.toLowerCase().includes("nerd") ||
23
- process.env.NERD_FONT === "1" ||
24
- term === "WezTerm" ||
25
- term === "iTerm.app" ||
26
- (process.env.TERMINAL_EMULATOR ?? "").includes("JetBrains") ||
27
- false;
28
- return nerdFontDetected;
29
- }
30
-
31
- /** Nerd Font indicator chars */
32
- const NERD_INDICATORS = {
33
- add: "\uf055 ", // nf-fa-plus_circle
34
- remove: "\uf056 ", // nf-fa-minus_circle
35
- same: " ",
36
- } as const;
37
-
38
- // --- Syntax Highlighting ---
39
-
40
- const KEYWORDS: Record<string, Set<string>> = {
41
- js: new Set(["const", "let", "var", "function", "return", "if", "else", "for", "while", "class", "import", "export", "from", "async", "await", "try", "catch", "throw", "new", "this", "typeof", "instanceof", "in", "of", "switch", "case", "break", "continue", "default", "yield", "void", "delete", "null", "undefined", "true", "false"]),
42
- py: new Set(["def", "class", "import", "from", "return", "if", "elif", "else", "for", "while", "try", "except", "finally", "raise", "with", "as", "yield", "lambda", "pass", "break", "continue", "and", "or", "not", "in", "is", "None", "True", "False", "self", "async", "await"]),
43
- ts: new Set(["const", "let", "var", "function", "return", "if", "else", "for", "while", "class", "import", "export", "from", "async", "await", "try", "catch", "throw", "new", "this", "typeof", "instanceof", "interface", "type", "enum", "namespace", "module", "declare", "implements", "extends", "public", "private", "protected", "readonly", "static", "abstract", "override", "keyof", "infer", "never", "unknown", "any", "void", "string", "number", "boolean", "null", "undefined", "true", "false"]),
44
- sh: new Set(["if", "then", "else", "elif", "fi", "for", "while", "do", "done", "case", "esac", "function", "return", "local", "export", "readonly", "declare", "unset", "shift", "source", "exit", "echo", "printf", "read", "test", "true", "false"]),
45
- };
46
-
47
- /** Apply basic syntax highlighting to a line */
48
- function highlightLine(line: string, lang?: string): string {
49
- if (!lang) return line;
50
- const keywords = KEYWORDS[lang] ?? KEYWORDS.js;
51
- // Highlight keywords (simple word boundary match)
52
- return line.replace(/\b([a-zA-Z_]\w*)\b/g, (match) => {
53
- if (keywords.has(match)) return `\x1b[34m${match}\x1b[0m`; // blue
54
- return match;
55
- });
56
- }
57
-
58
- /** Detect language from file extension or content */
59
- function detectLanguage(filePath?: string, content?: string): string | undefined {
60
- if (filePath) {
61
- const ext = filePath.split(".").pop()?.toLowerCase();
62
- const extMap: Record<string, string> = {
63
- js: "js", mjs: "js", cjs: "js", jsx: "js",
64
- ts: "ts", mts: "ts", cts: "ts", tsx: "ts",
65
- py: "py", pyw: "py",
66
- sh: "sh", bash: "sh", zsh: "sh",
67
- };
68
- if (ext && extMap[ext]) return extMap[ext];
69
- }
70
- if (content) {
71
- if (content.includes("def ") || content.includes("import ")) return "py";
72
- if (content.includes("function ") || content.includes("const ")) return "js";
73
- if (content.includes("#!/bin/")) return "sh";
74
- }
75
- return undefined;
76
- }
77
-
78
- interface DiffLine {
79
- type: "same" | "add" | "remove";
80
- text: string;
81
- }
82
-
83
- function lcs<T>(a: T[], b: T[]): T[] {
84
- const m = a.length;
85
- const n = b.length;
86
- const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
87
-
88
- for (let i = 1; i <= m; i++) {
89
- for (let j = 1; j <= n; j++) {
90
- if (a[i - 1] === b[j - 1]) {
91
- dp[i][j] = dp[i - 1][j - 1] + 1;
92
- } else {
93
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
94
- }
95
- }
96
- }
97
-
98
- const result: T[] = [];
99
- let i = m, j = n;
100
- while (i > 0 && j > 0) {
101
- if (a[i - 1] === b[j - 1]) {
102
- result.unshift(a[i - 1]);
103
- i--;
104
- j--;
105
- } else if (dp[i - 1][j] > dp[i][j - 1]) {
106
- i--;
107
- } else {
108
- j--;
109
- }
110
- }
111
-
112
- return result;
113
- }
114
-
115
- function computeDiff(before: string[], after: string[]): DiffLine[] {
116
- const common = lcs(before, after);
117
- const result: DiffLine[] = [];
118
- let i = 0, j = 0, k = 0;
119
-
120
- while (i < before.length || j < after.length) {
121
- if (k < common.length && before[i] === common[k] && after[j] === common[k]) {
122
- result.push({ type: "same", text: after[j] });
123
- i++;
124
- j++;
125
- k++;
126
- } else if (i < before.length && (k >= common.length || before[i] !== common[k])) {
127
- result.push({ type: "remove", text: before[i] });
128
- i++;
129
- } else if (j < after.length && (k >= common.length || after[j] !== common[k])) {
130
- result.push({ type: "add", text: after[j] });
131
- j++;
132
- } else {
133
- break;
134
- }
135
- }
136
-
137
- return result;
138
- }
139
-
140
- function indicatorChar(type: DiffLine["type"], style: DiffIndicator): string {
141
- if (style === "none") return " ";
142
- if (style === "nerd") {
143
- if (type === "add") return `\x1b[32m${NERD_INDICATORS.add}\x1b[0m`;
144
- if (type === "remove") return `\x1b[31m${NERD_INDICATORS.remove}\x1b[0m`;
145
- return NERD_INDICATORS.same;
146
- }
147
- if (style === "bars") {
148
- if (type === "add") return `\x1b[32m│ \x1b[0m`;
149
- if (type === "remove") return `\x1b[31m│ \x1b[0m`;
150
- return " ";
151
- }
152
- return type === "add" ? "+ " : type === "remove" ? "- " : " ";
153
- }
154
-
155
- function renderUnified(
156
- diff: DiffLine[],
157
- indicator: DiffIndicator,
158
- maxWidth?: number,
159
- ): string {
160
- return diff.map((line) => {
161
- const prefix = indicator === "bars"
162
- ? (line.type === "add" ? "│ " : line.type === "remove" ? "│ " : " ")
163
- : indicatorChar(line.type, indicator);
164
- const rendered = prefix + line.text;
165
- if (maxWidth && visibleWidth(rendered) > maxWidth) {
166
- return truncateToWidth(rendered, maxWidth, "…");
167
- }
168
- return rendered;
169
- }).join("\n");
170
- }
171
-
172
- function renderSplit(
173
- diff: DiffLine[],
174
- indicator: DiffIndicator,
175
- maxW?: number,
176
- ): string {
177
- const left: string[] = [];
178
- const right: string[] = [];
179
-
180
- for (const line of diff) {
181
- if (line.type === "same") {
182
- left.push(" " + line.text);
183
- right.push(" " + line.text);
184
- } else if (line.type === "remove") {
185
- left.push(indicatorChar("remove", indicator) + line.text);
186
- right.push("");
187
- } else if (line.type === "add") {
188
- left.push("");
189
- right.push(indicatorChar("add", indicator) + line.text);
190
- }
191
- }
192
-
193
- const halfW = maxW ? Math.floor(maxW / 2) - 2 : 40;
194
- const colW = Math.max(
195
- ...left.map((l) => visibleWidth(l)),
196
- ...right.map((l) => visibleWidth(l)),
197
- Math.min(halfW, 40),
198
- );
199
- const result: string[] = [];
200
- for (let i = 0; i < left.length; i++) {
201
- const lTrunc = visibleWidth(left[i]) > colW
202
- ? truncateToWidth(left[i], colW, "…")
203
- : left[i].padEnd(colW);
204
- const sep = left[i] && right[i] ? " │ " : " ";
205
- let rLine = right[i];
206
- if (maxW && visibleWidth(lTrunc + sep + rLine) > maxW) {
207
- const rBudget = maxW - visibleWidth(lTrunc + sep);
208
- rLine = truncateToWidth(rLine, Math.max(1, rBudget), "…");
209
- }
210
- result.push(lTrunc + sep + rLine);
211
- }
212
-
213
- return result.join("\n");
214
- }
215
-
216
- export function renderDiff(
217
- before: string,
218
- after: string,
219
- opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number; filePath?: string; highlight?: boolean },
220
- ): string {
221
- const layout = opts?.layout ?? "auto";
222
- let indicator = opts?.indicator ?? "bars";
223
- const maxWidth = opts?.maxWidth ?? 120;
224
- const doHighlight = opts?.highlight ?? true;
225
-
226
- // Auto-detect Nerd Font for indicator selection
227
- if (indicator === "bars" && detectNerdFont()) {
228
- indicator = "nerd";
229
- }
230
-
231
- const beforeLines = before.split("\n");
232
- const afterLines = after.split("\n");
233
- const diff = computeDiff(beforeLines, afterLines);
234
-
235
- const effectiveLayout = layout === "auto"
236
- ? (maxWidth >= 100 ? "split" : "unified")
237
- : layout;
238
-
239
- let effectiveIndicator = indicator;
240
- // Auto mode: use classic indicator for unified to keep output clean
241
- if (layout === "auto" && effectiveLayout === "unified" && indicator !== "nerd") {
242
- effectiveIndicator = "classic";
243
- }
244
-
245
- // Apply syntax highlighting if enabled
246
- let highlightedDiff = diff;
247
- if (doHighlight) {
248
- const lang = detectLanguage(opts?.filePath, after);
249
- if (lang) {
250
- highlightedDiff = diff.map((line) => ({
251
- ...line,
252
- text: highlightLine(line.text, lang),
253
- }));
254
- }
255
- }
256
-
257
- if (effectiveLayout === "split") {
258
- return renderSplit(highlightedDiff, effectiveIndicator, maxWidth);
259
- }
260
-
261
- return renderUnified(highlightedDiff, effectiveIndicator, maxWidth);
262
- }
263
-
264
- export function renderEditDiffResult(
265
- previousContent: string,
266
- newContent: string,
267
- opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number },
268
- ): string {
269
- return renderDiff(previousContent, newContent, opts);
270
- }
271
-
272
- export function renderWriteDiffResult(
273
- previousContent: string | undefined,
274
- newContent: string,
275
- opts?: { layout?: DiffLayout; indicator?: DiffIndicator; maxWidth?: number },
276
- ): string {
277
- if (!previousContent) {
278
- return "[New file created]\n" + newContent.split("\n").map((l) => "+ " + l).join("\n");
279
- }
280
- return renderDiff(previousContent, newContent, opts);
281
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Line width safety — width clamping with collapsed hints
3
- *
4
- * Uses ANSI-aware visibleWidth measurement from pi-tui to properly
5
- * handle lines containing escape codes. Falls back to raw-length
6
- * measurement when pi-tui is unavailable.
7
- */
8
-
9
- import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
10
-
11
- /**
12
- * Clamp each line to maxWidth visible columns.
13
- * Uses pi-tui's visibleWidth() for ANSI-aware measurement and
14
- * truncateToWidth() for ANSI-safe truncation.
15
- */
16
- export function clampLineWidth(lines: string[], maxWidth: number): string[] {
17
- return lines.map((line) => {
18
- const vw = visibleWidth(line);
19
- if (vw <= maxWidth) return line;
20
- return truncateToWidth(line, maxWidth, "…");
21
- });
22
- }
23
-
24
- export function collapseHint(originalCount: number, shownCount: number): string {
25
- const omitted = originalCount - shownCount;
26
- if (omitted <= 0) return "";
27
- return `...(${omitted} more lines)...`;
28
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Shared display rendering utilities
3
- */
4
-
5
- export function splitLines(text: string): string[] {
6
- return text.split(/\r?\n/);
7
- }
8
-
9
- export function countNonEmptyLines(text: string): number {
10
- return splitLines(text).filter((l) => l.trim()).length;
11
- }
12
-
13
- export function compactOutputLines(text: string, maxLines: number): string {
14
- const lines = splitLines(text);
15
- if (lines.length <= maxLines) return text;
16
- const omitted = lines.length - maxLines;
17
- return `...(${omitted} lines omitted)...\n${lines.slice(-maxLines).join("\n")}`;
18
- }
19
-
20
- export function previewLines(text: string, lines: number): string {
21
- const all = splitLines(text);
22
- if (all.length <= lines) return text;
23
- return all.slice(0, lines).join("\n") + `\n...(${all.length - lines} more lines)`;
24
- }
25
-
26
- export function pluralize(count: number, singular: string, plural?: string): string {
27
- return count === 1 ? `${count} ${singular}` : `${count} ${plural ?? singular + "s"}`;
28
- }
29
-
30
- export function shortenPath(path: string, maxLen: number = 60): string {
31
- if (path.length <= maxLen) return path;
32
- const parts = path.split("/");
33
- if (parts.length <= 2) return "..." + path.slice(-(maxLen - 3));
34
- return parts[0] + "/.../" + parts.slice(-2).join("/");
35
- }
36
-
37
- export function extractTextOutput(result: any): string {
38
- if (typeof result === "string") return result;
39
- if (result?.output) return String(result.output);
40
- if (result?.stdout) return String(result.stdout);
41
- return "";
42
- }
43
-
44
- export function isLikelyQuietCommand(command: string): boolean {
45
- const quietPatterns = [/^\s*cd\s/, /^\s*mkdir\s+-p/, /^\s*touch\s/, /^\s*rm\s+/];
46
- return quietPatterns.some((re) => re.test(command));
47
- }
48
-
49
- export function sanitizeAnsiForThemedOutput(text: string): string {
50
- // Strip ANSI escape sequences for clean themed rendering
51
- return text.replace(/\x1b\[[0-9;]*m/g, "");
52
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * Thinking labels during streaming
3
- */
4
-
5
- export function formatThinkingLabel(text: string, opts?: { prefix?: string }): string {
6
- const prefix = opts?.prefix ?? "🤔";
7
- const lines = text.split("\n");
8
- if (lines.length === 1) return `${prefix} ${lines[0]}`;
9
- return `${prefix} Thinking...\n${text}`;
10
- }
11
-
12
- export function sanitizeThinkingArtifacts(text: string): string {
13
- // Remove thinking blocks from context before LLM turn
14
- return text
15
- .replace(/<thinking>[\s\S]*?<\/thinking>/g, "")
16
- .replace(/\[thinking\][\s\S]*?\[\/thinking\]/g, "")
17
- .trim();
18
- }