@abacus-ai/cli 1.106.25007 → 2.0.0-canary.0

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 (200) hide show
  1. package/.oxlintrc.json +8 -0
  2. package/dist/index.mjs +12603 -0
  3. package/package.json +7 -39
  4. package/resources/abacus.ico +0 -0
  5. package/resources/entitlements.plist +9 -0
  6. package/src/__e2e__/README.md +196 -0
  7. package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
  8. package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
  9. package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
  10. package/src/__e2e__/conversation.e2e.test.tsx +56 -0
  11. package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
  12. package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
  13. package/src/__e2e__/helpers/test-helpers.ts +450 -0
  14. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
  15. package/src/__e2e__/llm-models.e2e.test.ts +402 -0
  16. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
  17. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
  18. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
  19. package/src/__e2e__/repl.e2e.test.tsx +78 -0
  20. package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
  21. package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
  22. package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
  23. package/src/args.ts +22 -0
  24. package/src/components/__tests__/react-compiler.test.tsx +78 -0
  25. package/src/components/__tests__/status-indicator.test.tsx +403 -0
  26. package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
  27. package/src/components/composer/agent-mode-indicator.tsx +63 -0
  28. package/src/components/composer/bash-runner.tsx +54 -0
  29. package/src/components/composer/commands/default-commands.tsx +615 -0
  30. package/src/components/composer/commands/handler.tsx +59 -0
  31. package/src/components/composer/commands/picker.tsx +273 -0
  32. package/src/components/composer/commands/registry.ts +233 -0
  33. package/src/components/composer/commands/types.ts +33 -0
  34. package/src/components/composer/context.tsx +88 -0
  35. package/src/components/composer/file-mention-picker.tsx +83 -0
  36. package/src/components/composer/help.tsx +44 -0
  37. package/src/components/composer/index.tsx +1006 -0
  38. package/src/components/composer/mentions.ts +57 -0
  39. package/src/components/composer/message-queue.tsx +70 -0
  40. package/src/components/composer/mode-panel.tsx +35 -0
  41. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
  42. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
  43. package/src/components/composer/modes/bash-handler.tsx +132 -0
  44. package/src/components/composer/modes/bash-renderer.tsx +175 -0
  45. package/src/components/composer/modes/default-handlers.tsx +33 -0
  46. package/src/components/composer/modes/index.ts +41 -0
  47. package/src/components/composer/modes/types.ts +21 -0
  48. package/src/components/composer/persistent-shell.ts +283 -0
  49. package/src/components/composer/process.ts +65 -0
  50. package/src/components/composer/types.ts +9 -0
  51. package/src/components/composer/use-mention-search.ts +68 -0
  52. package/src/components/error-boundry.tsx +60 -0
  53. package/src/components/exit-message.tsx +29 -0
  54. package/src/components/expanded-view.tsx +74 -0
  55. package/src/components/file-completion.tsx +127 -0
  56. package/src/components/header.tsx +47 -0
  57. package/src/components/logo.tsx +37 -0
  58. package/src/components/segments.tsx +356 -0
  59. package/src/components/status-indicator.tsx +306 -0
  60. package/src/components/tool-group-summary.tsx +263 -0
  61. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
  62. package/src/components/tool-permissions/diff-preview.tsx +355 -0
  63. package/src/components/tool-permissions/index.ts +5 -0
  64. package/src/components/tool-permissions/permission-options.tsx +375 -0
  65. package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
  66. package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
  67. package/src/components/tools/agent/ask-user-question.tsx +101 -0
  68. package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
  69. package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
  70. package/src/components/tools/agent/handoff-to-main.tsx +27 -0
  71. package/src/components/tools/agent/subagent.tsx +37 -0
  72. package/src/components/tools/agent/todo-write.tsx +104 -0
  73. package/src/components/tools/browser/close-tab.tsx +58 -0
  74. package/src/components/tools/browser/computer.tsx +70 -0
  75. package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
  76. package/src/components/tools/browser/get-tab-content.tsx +51 -0
  77. package/src/components/tools/browser/navigate-to.tsx +59 -0
  78. package/src/components/tools/browser/new-tab.tsx +60 -0
  79. package/src/components/tools/browser/perform-action.tsx +63 -0
  80. package/src/components/tools/browser/refresh-tab.tsx +43 -0
  81. package/src/components/tools/browser/switch-tab.tsx +58 -0
  82. package/src/components/tools/filesystem/delete-file.tsx +104 -0
  83. package/src/components/tools/filesystem/edit.tsx +220 -0
  84. package/src/components/tools/filesystem/list-dir.tsx +78 -0
  85. package/src/components/tools/filesystem/read-file.tsx +180 -0
  86. package/src/components/tools/filesystem/upload-image.tsx +76 -0
  87. package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
  88. package/src/components/tools/index.ts +91 -0
  89. package/src/components/tools/mcp/mcp-tool.tsx +158 -0
  90. package/src/components/tools/search/fetch-url.tsx +73 -0
  91. package/src/components/tools/search/file-search.tsx +78 -0
  92. package/src/components/tools/search/grep.tsx +90 -0
  93. package/src/components/tools/search/semantic-search.tsx +66 -0
  94. package/src/components/tools/search/web-search.tsx +71 -0
  95. package/src/components/tools/shared/index.tsx +48 -0
  96. package/src/components/tools/shared/zod-coercion.ts +35 -0
  97. package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
  98. package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
  99. package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
  100. package/src/components/tools/types.ts +16 -0
  101. package/src/components/tools.tsx +66 -0
  102. package/src/components/ui/__tests__/divider.test.tsx +61 -0
  103. package/src/components/ui/__tests__/gradient.test.tsx +125 -0
  104. package/src/components/ui/__tests__/input.test.tsx +166 -0
  105. package/src/components/ui/__tests__/select.test.tsx +273 -0
  106. package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
  107. package/src/components/ui/blinking-indicator.tsx +25 -0
  108. package/src/components/ui/divider.tsx +162 -0
  109. package/src/components/ui/gradient.tsx +56 -0
  110. package/src/components/ui/input.tsx +228 -0
  111. package/src/components/ui/select.tsx +151 -0
  112. package/src/components/ui/shimmer.tsx +84 -0
  113. package/src/context/agent-mode.tsx +95 -0
  114. package/src/context/extension-file.tsx +136 -0
  115. package/src/context/network-activity.tsx +45 -0
  116. package/src/context/notification.tsx +62 -0
  117. package/src/context/shell-size.tsx +49 -0
  118. package/src/context/shell-title.tsx +38 -0
  119. package/src/entrypoints/print-mode.ts +312 -0
  120. package/src/entrypoints/repl.tsx +401 -0
  121. package/src/hooks/use-agent.ts +15 -0
  122. package/src/hooks/use-api-client.ts +1 -0
  123. package/src/hooks/use-available-height.ts +8 -0
  124. package/src/hooks/use-cleanup.ts +29 -0
  125. package/src/hooks/use-interrupt-manager.ts +242 -0
  126. package/src/hooks/use-models.ts +22 -0
  127. package/src/index.ts +217 -0
  128. package/src/lib/__tests__/ansi.test.ts +255 -0
  129. package/src/lib/__tests__/cli.test.ts +122 -0
  130. package/src/lib/__tests__/commands.test.ts +325 -0
  131. package/src/lib/__tests__/constants.test.ts +15 -0
  132. package/src/lib/__tests__/focusables.test.ts +25 -0
  133. package/src/lib/__tests__/fs.test.ts +231 -0
  134. package/src/lib/__tests__/markdown.test.tsx +348 -0
  135. package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
  136. package/src/lib/__tests__/mcpManagement.test.ts +38 -0
  137. package/src/lib/__tests__/path-paste.test.ts +144 -0
  138. package/src/lib/__tests__/path.test.ts +300 -0
  139. package/src/lib/__tests__/queries.test.ts +39 -0
  140. package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
  141. package/src/lib/__tests__/text-buffer.test.ts +328 -0
  142. package/src/lib/__tests__/text-utils.test.ts +32 -0
  143. package/src/lib/__tests__/timing.test.ts +78 -0
  144. package/src/lib/__tests__/utils.test.ts +238 -0
  145. package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
  146. package/src/lib/ansi.ts +150 -0
  147. package/src/lib/cli-push-server.ts +112 -0
  148. package/src/lib/cli.ts +44 -0
  149. package/src/lib/clipboard.ts +226 -0
  150. package/src/lib/command-utils.ts +93 -0
  151. package/src/lib/commands.ts +270 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/extension-connection.ts +181 -0
  154. package/src/lib/focusables.ts +7 -0
  155. package/src/lib/fs.ts +533 -0
  156. package/src/lib/markdown/code-block.tsx +63 -0
  157. package/src/lib/markdown/index.ts +4 -0
  158. package/src/lib/markdown/link.tsx +19 -0
  159. package/src/lib/markdown/markdown.tsx +372 -0
  160. package/src/lib/markdown/types.ts +15 -0
  161. package/src/lib/mcpCommandHandler.ts +121 -0
  162. package/src/lib/mcpManagement.ts +44 -0
  163. package/src/lib/path-paste.ts +185 -0
  164. package/src/lib/path.ts +179 -0
  165. package/src/lib/queries.ts +15 -0
  166. package/src/lib/standaloneMcpService.ts +688 -0
  167. package/src/lib/status-utils.ts +237 -0
  168. package/src/lib/test-utils.tsx +72 -0
  169. package/src/lib/text-buffer.ts +2415 -0
  170. package/src/lib/text-utils.ts +272 -0
  171. package/src/lib/timing.ts +63 -0
  172. package/src/lib/types.ts +295 -0
  173. package/src/lib/utils.ts +182 -0
  174. package/src/lib/vim-buffer-actions.ts +732 -0
  175. package/src/providers/agent.tsx +1075 -0
  176. package/src/providers/api-client.tsx +43 -0
  177. package/src/services/logger.ts +85 -0
  178. package/src/terminal/detection.ts +187 -0
  179. package/src/terminal/exit.ts +279 -0
  180. package/src/terminal/notification.ts +83 -0
  181. package/src/terminal/progress.ts +201 -0
  182. package/src/terminal/setup.ts +797 -0
  183. package/src/terminal/suspend.ts +58 -0
  184. package/src/terminal/types.ts +51 -0
  185. package/src/theme/context.tsx +57 -0
  186. package/src/theme/index.ts +4 -0
  187. package/src/theme/themed.tsx +35 -0
  188. package/src/theme/themes.json +546 -0
  189. package/src/theme/types.ts +110 -0
  190. package/src/tools/types.ts +59 -0
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
  192. package/src/tools/utils/tool-ui-components.tsx +631 -0
  193. package/src/tools/utils/zod-coercion.ts +35 -0
  194. package/tsconfig.json +11 -0
  195. package/tsconfig.node.json +29 -0
  196. package/tsconfig.test.json +27 -0
  197. package/tsdown.config.ts +17 -0
  198. package/vitest.config.ts +76 -0
  199. package/README.md +0 -28
  200. package/dist/index.js +0 -26
package/src/lib/fs.ts ADDED
@@ -0,0 +1,533 @@
1
+ import {
2
+ getFileRecency,
3
+ prepareQuery,
4
+ scoreItemFuzzy,
5
+ compareItemsByFuzzyScore,
6
+ type IPreparedQuery,
7
+ type IItemAccessor,
8
+ type FuzzyScorerCache,
9
+ LABEL_SCORE_THRESHOLD,
10
+ } from "@codellm/agent/utils";
11
+ import { rgPath } from "@vscode/ripgrep";
12
+ import { spawn } from "node:child_process";
13
+ import { promises as fs } from "node:fs";
14
+ import path from "node:path";
15
+ import { createInterface } from "node:readline";
16
+
17
+ import { unescapePath } from "./path.js";
18
+
19
+ class TtlCache<K, V> {
20
+ private cache = new Map<K, { value: V; expiresAt: number }>();
21
+ private ttlMs: number;
22
+
23
+ constructor(ttlMs: number) {
24
+ this.ttlMs = ttlMs;
25
+ }
26
+
27
+ get(key: K): V | undefined {
28
+ const entry = this.cache.get(key);
29
+ if (!entry) {
30
+ return undefined;
31
+ }
32
+ if (Date.now() > entry.expiresAt) {
33
+ this.cache.delete(key);
34
+ return undefined;
35
+ }
36
+ return entry.value;
37
+ }
38
+
39
+ set(key: K, value: V): void {
40
+ this.cache.set(key, { value, expiresAt: Date.now() + this.ttlMs });
41
+ }
42
+
43
+ clear(): void {
44
+ this.cache.clear();
45
+ }
46
+
47
+ delete(key: K): void {
48
+ this.cache.delete(key);
49
+ }
50
+ }
51
+
52
+ type SearchOptions = {
53
+ root: string;
54
+ maxResults?: number;
55
+ includeHidden?: boolean;
56
+ globs?: string[];
57
+ ignoreGlobs?: string[];
58
+ useRipgrep?: boolean;
59
+ useGitignore?: boolean;
60
+ };
61
+
62
+ type FileDoc = {
63
+ path: string;
64
+ name: string;
65
+ dir: string;
66
+ ext: string;
67
+ isDirectory?: boolean;
68
+ };
69
+
70
+ type FileIndex = {
71
+ docs: FileDoc[];
72
+ building: boolean;
73
+ builtAt?: number;
74
+ buildError?: Error;
75
+ };
76
+
77
+ type SearchResult = {
78
+ paths: string[];
79
+ isDirectory: boolean[];
80
+ };
81
+
82
+ const SEARCH_RESULT_CACHE_TTL = 5 * 1000;
83
+ const FILE_INDEX_CACHE_TTL = 60 * 1000;
84
+
85
+ const searchResultCache = new TtlCache<string, SearchResult>(SEARCH_RESULT_CACHE_TTL);
86
+ const fileIndexCache = new TtlCache<string, FileIndex>(FILE_INDEX_CACHE_TTL);
87
+
88
+ const RECENCY_BOOST_SCORE = LABEL_SCORE_THRESHOLD / 4;
89
+
90
+ function toDoc(filePath: string, isDirectory = false): FileDoc {
91
+ const normalized = filePath.replace(/\\/g, "/");
92
+ const name = path.posix.basename(normalized);
93
+ const dir = path.posix.dirname(normalized);
94
+ const ext = path.posix.extname(normalized);
95
+ return { path: normalized, name, dir, ext, isDirectory };
96
+ }
97
+
98
+ function getIndexKey(root: string): string {
99
+ return path.resolve(root).replace(/\\/g, "/");
100
+ }
101
+
102
+ function getResultCacheKey(
103
+ query: string,
104
+ options: Required<Pick<SearchOptions, "root" | "maxResults">>,
105
+ ): string {
106
+ return `${getIndexKey(options.root)}::${query}::${options.maxResults}`;
107
+ }
108
+
109
+ const fileDocAccessor: IItemAccessor<FileDoc> = {
110
+ getItemLabel(item: FileDoc): string {
111
+ return item.name;
112
+ },
113
+ getItemDescription(item: FileDoc): string {
114
+ return item.dir;
115
+ },
116
+ getItemPath(item: FileDoc): string {
117
+ return item.path;
118
+ },
119
+ };
120
+
121
+ function searchAndRank(
122
+ docs: ReadonlyArray<FileDoc>,
123
+ query: IPreparedQuery,
124
+ maxResults: number,
125
+ ): SearchResult {
126
+ if (docs.length === 0) {
127
+ return { paths: [], isDirectory: [] };
128
+ }
129
+
130
+ const scorerCache: FuzzyScorerCache = {};
131
+ const queryLower = query.normalizedLowercase;
132
+ const queryLength = query.normalized.length;
133
+
134
+ if (queryLength === 0) {
135
+ return { paths: [], isDirectory: [] };
136
+ }
137
+
138
+ const scoredDocs: Array<{ doc: FileDoc; score: number; recencyBoost: number }> = [];
139
+ const candidateLimit = Math.min(docs.length, maxResults * 15);
140
+
141
+ const candidates: Array<{ doc: FileDoc; recency: number }> = [];
142
+
143
+ for (const doc of docs) {
144
+ const nameLower = doc.name.toLowerCase();
145
+ const pathLower = doc.path.toLowerCase();
146
+
147
+ const nameStarts = nameLower.startsWith(queryLower);
148
+ const nameIncludes = nameLower.includes(queryLower);
149
+ const pathIncludes = pathLower.includes(queryLower);
150
+
151
+ if (nameStarts || nameIncludes || pathIncludes) {
152
+ const recency = getFileRecency(doc.path);
153
+ candidates.push({ doc, recency });
154
+ }
155
+ }
156
+
157
+ candidates.sort((a, b) => b.recency - a.recency);
158
+
159
+ let candidatesChecked = 0;
160
+ for (const { doc, recency } of candidates) {
161
+ if (candidatesChecked >= candidateLimit && scoredDocs.length >= maxResults * 2) {
162
+ break;
163
+ }
164
+
165
+ candidatesChecked++;
166
+
167
+ const recencyBoost = recency > 0 ? RECENCY_BOOST_SCORE : 0;
168
+
169
+ const itemScore = scoreItemFuzzy(doc, query, true, fileDocAccessor, scorerCache);
170
+ if (itemScore.score > 0) {
171
+ scoredDocs.push({
172
+ doc,
173
+ score: itemScore.score + recencyBoost,
174
+ recencyBoost,
175
+ });
176
+ }
177
+ }
178
+
179
+ scoredDocs.sort((a, b) => {
180
+ if (a.score !== b.score) {
181
+ return b.score - a.score;
182
+ }
183
+ if (a.recencyBoost !== b.recencyBoost) {
184
+ return b.recencyBoost - a.recencyBoost;
185
+ }
186
+ return compareItemsByFuzzyScore(a.doc, b.doc, query, true, fileDocAccessor, scorerCache);
187
+ });
188
+
189
+ const topResults = scoredDocs.slice(0, maxResults);
190
+ return {
191
+ paths: topResults.map((item) => item.doc.path),
192
+ isDirectory: topResults.map((item) => item.doc.isDirectory ?? false),
193
+ };
194
+ }
195
+
196
+ function getBaselineResults(docs: ReadonlyArray<FileDoc>, maxResults: number): SearchResult {
197
+ const withRecency = docs.map((doc) => ({
198
+ doc,
199
+ recency: getFileRecency(doc.path),
200
+ }));
201
+
202
+ const sorted = withRecency
203
+ .sort((a, b) => {
204
+ if (a.recency !== b.recency) {
205
+ return b.recency - a.recency;
206
+ }
207
+ const aDepth = a.doc.path.split("/").length;
208
+ const bDepth = b.doc.path.split("/").length;
209
+ return aDepth - bDepth || a.doc.name.localeCompare(b.doc.name);
210
+ })
211
+ .slice(0, maxResults);
212
+
213
+ return {
214
+ paths: sorted.map((item) => item.doc.path),
215
+ isDirectory: sorted.map((item) => item.doc.isDirectory ?? false),
216
+ };
217
+ }
218
+
219
+ async function ensureIndex(
220
+ options: Required<
221
+ Pick<SearchOptions, "root" | "includeHidden" | "globs" | "ignoreGlobs" | "useRipgrep">
222
+ > &
223
+ Pick<SearchOptions, "useGitignore">,
224
+ ): Promise<FileIndex> {
225
+ const key = getIndexKey(options.root);
226
+ const cached = fileIndexCache.get(key);
227
+ if (cached) {
228
+ if (cached.docs.length === 0 && !cached.building && !cached.buildError) {
229
+ fileIndexCache.delete(key);
230
+ } else {
231
+ return cached;
232
+ }
233
+ }
234
+
235
+ const index: FileIndex = { docs: [], building: true };
236
+ fileIndexCache.set(key, index);
237
+
238
+ const seenPaths = new Set<string>();
239
+
240
+ const addDoc = (doc: FileDoc) => {
241
+ if (seenPaths.has(doc.path)) {
242
+ return;
243
+ }
244
+ seenPaths.add(doc.path);
245
+ index.docs.push(doc);
246
+ };
247
+
248
+ if (options.useRipgrep !== false) {
249
+ const args: string[] = ["--files", "--no-messages", "--max-depth", "100"];
250
+
251
+ if (options.includeHidden) {
252
+ args.push("--hidden");
253
+ }
254
+
255
+ if (options.useGitignore === false) {
256
+ args.push("--no-ignore");
257
+ }
258
+
259
+ const include = options.globs ?? [];
260
+ const ignore = options.ignoreGlobs ?? [];
261
+
262
+ for (const g of include) {
263
+ args.push("-g", g);
264
+ }
265
+ for (const g of ignore) {
266
+ args.push("-g", g);
267
+ }
268
+
269
+ const child = spawn(rgPath, args, { cwd: options.root, stdio: ["ignore", "pipe", "pipe"] });
270
+ const rl = createInterface({ input: child.stdout });
271
+
272
+ rl.on("line", (line: string) => {
273
+ const raw = unescapePath(line.trim());
274
+ if (!raw) {
275
+ return;
276
+ }
277
+ const absolute = path.resolve(options.root, raw);
278
+ const doc = toDoc(absolute, false);
279
+ if (!options.includeHidden) {
280
+ if (
281
+ doc.name.startsWith(".") ||
282
+ doc.path.split("/").some((seg) => seg.startsWith(".") && seg !== ".")
283
+ ) {
284
+ return;
285
+ }
286
+ }
287
+ addDoc(doc);
288
+ });
289
+
290
+ const performFallbackWalk = async () => {
291
+ const MAX_DEPTH = 12;
292
+ const MAX_FILES = 15000;
293
+ const SKIP_DIRS = /^(node_modules|\.git|dist|build|out|coverage|\.vscode|\.idea|\.DS_Store)$/;
294
+
295
+ const walk = async (dir: string, depth: number): Promise<void> => {
296
+ if (depth > MAX_DEPTH || index.docs.length >= MAX_FILES) {
297
+ return;
298
+ }
299
+
300
+ try {
301
+ const entries = await fs.readdir(dir, { withFileTypes: true });
302
+ const dirs: string[] = [];
303
+
304
+ for (const entry of entries) {
305
+ if (!options.includeHidden && entry.name.startsWith(".")) {
306
+ continue;
307
+ }
308
+
309
+ const full = path.join(dir, entry.name);
310
+
311
+ if (entry.isDirectory()) {
312
+ if (SKIP_DIRS.test(entry.name)) {
313
+ continue;
314
+ }
315
+ addDoc(toDoc(full, true));
316
+ dirs.push(full);
317
+ } else if (entry.isFile()) {
318
+ addDoc(toDoc(full));
319
+ if (index.docs.length >= MAX_FILES) {
320
+ return;
321
+ }
322
+ }
323
+ }
324
+
325
+ await Promise.all(dirs.map((d) => walk(d, depth + 1)));
326
+ } catch {
327
+ // Skip directories we can't read
328
+ }
329
+ };
330
+
331
+ await walk(options.root, 0);
332
+ };
333
+
334
+ const done = new Promise<void>((resolve) => {
335
+ child.on("close", (code) => {
336
+ if (code !== 0 && code !== null) {
337
+ void performFallbackWalk()
338
+ .then(() => {
339
+ index.building = false;
340
+ index.builtAt = Date.now();
341
+ resolve();
342
+ })
343
+ .catch((err: unknown) => {
344
+ index.building = false;
345
+ index.buildError = err instanceof Error ? err : new Error("Indexing failed");
346
+ resolve();
347
+ });
348
+ } else {
349
+ // Collect directories after ripgrep finishes successfully
350
+ void (async () => {
351
+ const MAX_DEPTH = 12;
352
+ const SKIP_DIRS =
353
+ /^(node_modules|\.git|dist|build|out|coverage|\.vscode|\.idea|\.DS_Store)$/;
354
+ const walk = async (dir: string, depth: number): Promise<void> => {
355
+ if (depth > MAX_DEPTH) {
356
+ return;
357
+ }
358
+ try {
359
+ const entries = await fs.readdir(dir, { withFileTypes: true });
360
+ await Promise.all(
361
+ entries
362
+ .filter(
363
+ (e) =>
364
+ e.isDirectory() &&
365
+ (options.includeHidden || !e.name.startsWith(".")) &&
366
+ !SKIP_DIRS.test(e.name),
367
+ )
368
+ .map(async (e) => {
369
+ const full = path.join(dir, e.name);
370
+ addDoc(toDoc(full, true));
371
+ await walk(full, depth + 1);
372
+ }),
373
+ );
374
+ } catch {}
375
+ };
376
+ await walk(options.root, 0);
377
+ })().finally(() => {
378
+ index.building = false;
379
+ index.builtAt = Date.now();
380
+ resolve();
381
+ });
382
+ }
383
+ });
384
+ child.on("error", () => {
385
+ void performFallbackWalk()
386
+ .then(() => {
387
+ index.building = false;
388
+ index.builtAt = Date.now();
389
+ resolve();
390
+ })
391
+ .catch((err: unknown) => {
392
+ index.building = false;
393
+ index.buildError = err instanceof Error ? err : new Error("Indexing failed");
394
+ resolve();
395
+ });
396
+ });
397
+ });
398
+
399
+ void done.then(() => {
400
+ if (index.building) {
401
+ index.building = false;
402
+ index.builtAt = Date.now();
403
+ }
404
+ });
405
+ } else {
406
+ const MAX_DEPTH = 12;
407
+ const MAX_FILES = 15000;
408
+ const SKIP_DIRS = /^(node_modules|\.git|dist|build|out|coverage|\.vscode|\.idea|\.DS_Store)$/;
409
+
410
+ const walk = async (dir: string, depth: number): Promise<void> => {
411
+ if (depth > MAX_DEPTH || index.docs.length >= MAX_FILES) {
412
+ return;
413
+ }
414
+
415
+ try {
416
+ const entries = await fs.readdir(dir, { withFileTypes: true });
417
+ const dirs: string[] = [];
418
+
419
+ for (const entry of entries) {
420
+ if (!options.includeHidden && entry.name.startsWith(".")) {
421
+ continue;
422
+ }
423
+
424
+ const full = path.join(dir, entry.name);
425
+
426
+ if (entry.isDirectory()) {
427
+ if (SKIP_DIRS.test(entry.name)) {
428
+ continue;
429
+ }
430
+ addDoc(toDoc(full, true));
431
+ dirs.push(full);
432
+ } else if (entry.isFile()) {
433
+ addDoc(toDoc(full));
434
+ if (index.docs.length >= MAX_FILES) {
435
+ return;
436
+ }
437
+ }
438
+ }
439
+
440
+ await Promise.all(dirs.map((d) => walk(d, depth + 1)));
441
+ } catch {}
442
+ };
443
+
444
+ void walk(options.root, 0)
445
+ .then(() => {
446
+ index.building = false;
447
+ index.builtAt = Date.now();
448
+ })
449
+ .catch((err: unknown) => {
450
+ index.building = false;
451
+ index.buildError = err instanceof Error ? err : new Error("Indexing failed");
452
+ });
453
+ }
454
+
455
+ return index;
456
+ }
457
+
458
+ export async function* searchFiles(
459
+ query: string,
460
+ options: SearchOptions,
461
+ ): AsyncGenerator<SearchResult, void, void> {
462
+ const root = options.root;
463
+ const maxResults = options.maxResults ?? 100;
464
+ const includeHidden = options.includeHidden ?? false;
465
+ const globs = options.globs ?? [];
466
+ const ignoreGlobs = options.ignoreGlobs ?? [];
467
+ const useRipgrep = options.useRipgrep ?? true;
468
+ const useGitignore = options.useGitignore ?? true;
469
+
470
+ const stat = await fs.stat(root).catch(() => null);
471
+ if (!stat || !stat.isDirectory()) {
472
+ return;
473
+ }
474
+
475
+ const index = await ensureIndex({
476
+ root,
477
+ includeHidden,
478
+ globs,
479
+ ignoreGlobs,
480
+ useRipgrep,
481
+ useGitignore,
482
+ });
483
+
484
+ const trimmedQuery = query.trim();
485
+ const cachedKey = getResultCacheKey(trimmedQuery, { root, maxResults });
486
+ const cached = searchResultCache.get(cachedKey);
487
+ if (cached && cached.paths.length > 0) {
488
+ yield cached;
489
+ if (!index.building) {
490
+ return;
491
+ }
492
+ }
493
+
494
+ if (trimmedQuery.length === 0) {
495
+ const baseline = getBaselineResults(index.docs, maxResults);
496
+ if (baseline.paths.length > 0) {
497
+ searchResultCache.set(cachedKey, baseline);
498
+ yield baseline;
499
+ }
500
+ return;
501
+ }
502
+
503
+ const preparedQuery = prepareQuery(trimmedQuery);
504
+ const results = searchAndRank(index.docs, preparedQuery, maxResults);
505
+
506
+ if (results.paths.length > 0) {
507
+ searchResultCache.set(cachedKey, results);
508
+ yield results;
509
+ }
510
+
511
+ if (index.building) {
512
+ await new Promise<void>((resolve) => {
513
+ const checkInterval = setInterval(() => {
514
+ if (!index.building) {
515
+ clearTimeout(timeout);
516
+ clearInterval(checkInterval);
517
+ resolve();
518
+ }
519
+ }, 50);
520
+ const timeout = setTimeout(() => {
521
+ clearInterval(checkInterval);
522
+ resolve();
523
+ }, 5000);
524
+ });
525
+
526
+ const finalResults = searchAndRank(index.docs, preparedQuery, maxResults);
527
+ if (finalResults.paths.length > 0) {
528
+ const finalKey = getResultCacheKey(trimmedQuery, { root, maxResults });
529
+ searchResultCache.set(finalKey, finalResults);
530
+ yield finalResults;
531
+ }
532
+ }
533
+ }
@@ -0,0 +1,63 @@
1
+ import { View, Text } from "@codellm/jar";
2
+ import { renderMermaidAscii } from "beautiful-mermaid";
3
+ import { highlight, supportsLanguage } from "cli-highlight";
4
+
5
+ import type { CodeBlockProps } from "./types.js";
6
+
7
+ /**
8
+ * Check if the language is mermaid
9
+ */
10
+ function isMermaidLanguage(language: string | undefined): boolean {
11
+ if (!language) return false;
12
+ const normalizedLang = language.toLowerCase().trim();
13
+ return normalizedLang === "mermaid" || normalizedLang === "mmd";
14
+ }
15
+
16
+ /**
17
+ * Try to render mermaid diagram as ASCII art
18
+ * Returns null if parsing fails
19
+ */
20
+ function tryRenderMermaid(content: string): string | null {
21
+ try {
22
+ return renderMermaidAscii(content);
23
+ } catch {
24
+ // Fallback to regular code block rendering
25
+ return null;
26
+ }
27
+ }
28
+
29
+ export function CodeBlock({ children, language }: CodeBlockProps) {
30
+ // Try to render mermaid diagrams as ASCII art
31
+ if (isMermaidLanguage(language)) {
32
+ const mermaidOutput = tryRenderMermaid(children || "");
33
+ if (mermaidOutput !== null) {
34
+ return (
35
+ <View paddingX={1} flexDirection="row" alignSelf="flex-start">
36
+ <Text>{mermaidOutput}</Text>
37
+ </View>
38
+ );
39
+ }
40
+ // If mermaid parsing fails, fall through to regular code block rendering
41
+ }
42
+
43
+ let highlighted = children;
44
+
45
+ if (language && supportsLanguage(language)) {
46
+ try {
47
+ // Use cli-highlight's default theme which works well with terminal colors
48
+ highlighted = highlight(children || "", {
49
+ language,
50
+ });
51
+ } catch {
52
+ // Fallback to plain text if highlighting fails
53
+ }
54
+ }
55
+
56
+ return (
57
+ <View paddingX={1} flexDirection="row" alignSelf="flex-start">
58
+ <Text>{highlighted}</Text>
59
+ </View>
60
+ );
61
+ }
62
+
63
+ CodeBlock.displayName = "CodeBlock";
@@ -0,0 +1,4 @@
1
+ export { Markdown } from "./markdown.js";
2
+ export { CodeBlock } from "./code-block.js";
3
+ export { Link } from "./link.js";
4
+ export type { MarkdownProps, CodeBlockProps, LinkProps } from "./types.js";
@@ -0,0 +1,19 @@
1
+ import { Text } from "@codellm/jar";
2
+
3
+ import type { LinkProps } from "./types.js";
4
+
5
+ import { useTheme } from "../../theme/index.js";
6
+
7
+ export function Link({ href, children }: LinkProps) {
8
+ const { colors } = useTheme();
9
+ // ANSI hyperlink: ESC]8;;url ST text ESC]8;; ST
10
+ const open = `\x1b]8;;${href}\x07`;
11
+ const close = `\x1b]8;;\x07`;
12
+ return (
13
+ <Text color={colors.link} underline transform={(s) => `${open}${s}${close}`}>
14
+ {children}
15
+ </Text>
16
+ );
17
+ }
18
+
19
+ Link.displayName = "Link";