@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.
- package/.oxlintrc.json +8 -0
- package/dist/index.mjs +12603 -0
- package/package.json +7 -39
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +9 -0
- package/src/__e2e__/README.md +196 -0
- package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
- package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
- package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
- package/src/__e2e__/conversation.e2e.test.tsx +56 -0
- package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
- package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
- package/src/__e2e__/helpers/test-helpers.ts +450 -0
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
- package/src/__e2e__/llm-models.e2e.test.ts +402 -0
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
- package/src/__e2e__/repl.e2e.test.tsx +78 -0
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
- package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
- package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
- package/src/args.ts +22 -0
- package/src/components/__tests__/react-compiler.test.tsx +78 -0
- package/src/components/__tests__/status-indicator.test.tsx +403 -0
- package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
- package/src/components/composer/agent-mode-indicator.tsx +63 -0
- package/src/components/composer/bash-runner.tsx +54 -0
- package/src/components/composer/commands/default-commands.tsx +615 -0
- package/src/components/composer/commands/handler.tsx +59 -0
- package/src/components/composer/commands/picker.tsx +273 -0
- package/src/components/composer/commands/registry.ts +233 -0
- package/src/components/composer/commands/types.ts +33 -0
- package/src/components/composer/context.tsx +88 -0
- package/src/components/composer/file-mention-picker.tsx +83 -0
- package/src/components/composer/help.tsx +44 -0
- package/src/components/composer/index.tsx +1006 -0
- package/src/components/composer/mentions.ts +57 -0
- package/src/components/composer/message-queue.tsx +70 -0
- package/src/components/composer/mode-panel.tsx +35 -0
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
- package/src/components/composer/modes/bash-handler.tsx +132 -0
- package/src/components/composer/modes/bash-renderer.tsx +175 -0
- package/src/components/composer/modes/default-handlers.tsx +33 -0
- package/src/components/composer/modes/index.ts +41 -0
- package/src/components/composer/modes/types.ts +21 -0
- package/src/components/composer/persistent-shell.ts +283 -0
- package/src/components/composer/process.ts +65 -0
- package/src/components/composer/types.ts +9 -0
- package/src/components/composer/use-mention-search.ts +68 -0
- package/src/components/error-boundry.tsx +60 -0
- package/src/components/exit-message.tsx +29 -0
- package/src/components/expanded-view.tsx +74 -0
- package/src/components/file-completion.tsx +127 -0
- package/src/components/header.tsx +47 -0
- package/src/components/logo.tsx +37 -0
- package/src/components/segments.tsx +356 -0
- package/src/components/status-indicator.tsx +306 -0
- package/src/components/tool-group-summary.tsx +263 -0
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
- package/src/components/tool-permissions/diff-preview.tsx +355 -0
- package/src/components/tool-permissions/index.ts +5 -0
- package/src/components/tool-permissions/permission-options.tsx +375 -0
- package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
- package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
- package/src/components/tools/agent/ask-user-question.tsx +101 -0
- package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
- package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
- package/src/components/tools/agent/handoff-to-main.tsx +27 -0
- package/src/components/tools/agent/subagent.tsx +37 -0
- package/src/components/tools/agent/todo-write.tsx +104 -0
- package/src/components/tools/browser/close-tab.tsx +58 -0
- package/src/components/tools/browser/computer.tsx +70 -0
- package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
- package/src/components/tools/browser/get-tab-content.tsx +51 -0
- package/src/components/tools/browser/navigate-to.tsx +59 -0
- package/src/components/tools/browser/new-tab.tsx +60 -0
- package/src/components/tools/browser/perform-action.tsx +63 -0
- package/src/components/tools/browser/refresh-tab.tsx +43 -0
- package/src/components/tools/browser/switch-tab.tsx +58 -0
- package/src/components/tools/filesystem/delete-file.tsx +104 -0
- package/src/components/tools/filesystem/edit.tsx +220 -0
- package/src/components/tools/filesystem/list-dir.tsx +78 -0
- package/src/components/tools/filesystem/read-file.tsx +180 -0
- package/src/components/tools/filesystem/upload-image.tsx +76 -0
- package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
- package/src/components/tools/index.ts +91 -0
- package/src/components/tools/mcp/mcp-tool.tsx +158 -0
- package/src/components/tools/search/fetch-url.tsx +73 -0
- package/src/components/tools/search/file-search.tsx +78 -0
- package/src/components/tools/search/grep.tsx +90 -0
- package/src/components/tools/search/semantic-search.tsx +66 -0
- package/src/components/tools/search/web-search.tsx +71 -0
- package/src/components/tools/shared/index.tsx +48 -0
- package/src/components/tools/shared/zod-coercion.ts +35 -0
- package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
- package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
- package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
- package/src/components/tools/types.ts +16 -0
- package/src/components/tools.tsx +66 -0
- package/src/components/ui/__tests__/divider.test.tsx +61 -0
- package/src/components/ui/__tests__/gradient.test.tsx +125 -0
- package/src/components/ui/__tests__/input.test.tsx +166 -0
- package/src/components/ui/__tests__/select.test.tsx +273 -0
- package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
- package/src/components/ui/blinking-indicator.tsx +25 -0
- package/src/components/ui/divider.tsx +162 -0
- package/src/components/ui/gradient.tsx +56 -0
- package/src/components/ui/input.tsx +228 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/shimmer.tsx +84 -0
- package/src/context/agent-mode.tsx +95 -0
- package/src/context/extension-file.tsx +136 -0
- package/src/context/network-activity.tsx +45 -0
- package/src/context/notification.tsx +62 -0
- package/src/context/shell-size.tsx +49 -0
- package/src/context/shell-title.tsx +38 -0
- package/src/entrypoints/print-mode.ts +312 -0
- package/src/entrypoints/repl.tsx +401 -0
- package/src/hooks/use-agent.ts +15 -0
- package/src/hooks/use-api-client.ts +1 -0
- package/src/hooks/use-available-height.ts +8 -0
- package/src/hooks/use-cleanup.ts +29 -0
- package/src/hooks/use-interrupt-manager.ts +242 -0
- package/src/hooks/use-models.ts +22 -0
- package/src/index.ts +217 -0
- package/src/lib/__tests__/ansi.test.ts +255 -0
- package/src/lib/__tests__/cli.test.ts +122 -0
- package/src/lib/__tests__/commands.test.ts +325 -0
- package/src/lib/__tests__/constants.test.ts +15 -0
- package/src/lib/__tests__/focusables.test.ts +25 -0
- package/src/lib/__tests__/fs.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.tsx +348 -0
- package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
- package/src/lib/__tests__/mcpManagement.test.ts +38 -0
- package/src/lib/__tests__/path-paste.test.ts +144 -0
- package/src/lib/__tests__/path.test.ts +300 -0
- package/src/lib/__tests__/queries.test.ts +39 -0
- package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
- package/src/lib/__tests__/text-buffer.test.ts +328 -0
- package/src/lib/__tests__/text-utils.test.ts +32 -0
- package/src/lib/__tests__/timing.test.ts +78 -0
- package/src/lib/__tests__/utils.test.ts +238 -0
- package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
- package/src/lib/ansi.ts +150 -0
- package/src/lib/cli-push-server.ts +112 -0
- package/src/lib/cli.ts +44 -0
- package/src/lib/clipboard.ts +226 -0
- package/src/lib/command-utils.ts +93 -0
- package/src/lib/commands.ts +270 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/extension-connection.ts +181 -0
- package/src/lib/focusables.ts +7 -0
- package/src/lib/fs.ts +533 -0
- package/src/lib/markdown/code-block.tsx +63 -0
- package/src/lib/markdown/index.ts +4 -0
- package/src/lib/markdown/link.tsx +19 -0
- package/src/lib/markdown/markdown.tsx +372 -0
- package/src/lib/markdown/types.ts +15 -0
- package/src/lib/mcpCommandHandler.ts +121 -0
- package/src/lib/mcpManagement.ts +44 -0
- package/src/lib/path-paste.ts +185 -0
- package/src/lib/path.ts +179 -0
- package/src/lib/queries.ts +15 -0
- package/src/lib/standaloneMcpService.ts +688 -0
- package/src/lib/status-utils.ts +237 -0
- package/src/lib/test-utils.tsx +72 -0
- package/src/lib/text-buffer.ts +2415 -0
- package/src/lib/text-utils.ts +272 -0
- package/src/lib/timing.ts +63 -0
- package/src/lib/types.ts +295 -0
- package/src/lib/utils.ts +182 -0
- package/src/lib/vim-buffer-actions.ts +732 -0
- package/src/providers/agent.tsx +1075 -0
- package/src/providers/api-client.tsx +43 -0
- package/src/services/logger.ts +85 -0
- package/src/terminal/detection.ts +187 -0
- package/src/terminal/exit.ts +279 -0
- package/src/terminal/notification.ts +83 -0
- package/src/terminal/progress.ts +201 -0
- package/src/terminal/setup.ts +797 -0
- package/src/terminal/suspend.ts +58 -0
- package/src/terminal/types.ts +51 -0
- package/src/theme/context.tsx +57 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themed.tsx +35 -0
- package/src/theme/themes.json +546 -0
- package/src/theme/types.ts +110 -0
- package/src/tools/types.ts +59 -0
- package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
- package/src/tools/utils/tool-ui-components.tsx +631 -0
- package/src/tools/utils/zod-coercion.ts +35 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +29 -0
- package/tsconfig.test.json +27 -0
- package/tsdown.config.ts +17 -0
- package/vitest.config.ts +76 -0
- package/README.md +0 -28
- 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,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";
|