@oh-my-pi/pi-coding-agent 3.5.1337 → 3.8.1337
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/CHANGELOG.md +29 -0
- package/package.json +5 -4
- package/src/core/bash-executor.ts +115 -154
- package/src/core/index.ts +2 -0
- package/src/core/session-manager.ts +113 -74
- package/src/core/tools/edit-diff.ts +45 -33
- package/src/core/tools/edit.ts +70 -182
- package/src/core/tools/find.ts +141 -160
- package/src/core/tools/index.ts +10 -9
- package/src/core/tools/ls.ts +64 -82
- package/src/core/tools/lsp/client.ts +63 -0
- package/src/core/tools/lsp/edits.ts +13 -4
- package/src/core/tools/lsp/index.ts +191 -85
- package/src/core/tools/notebook.ts +89 -144
- package/src/core/tools/read.ts +110 -158
- package/src/core/tools/write.ts +22 -115
- package/src/core/utils.ts +187 -0
- package/src/modes/interactive/components/tool-execution.ts +14 -14
- package/src/modes/interactive/interactive-mode.ts +23 -54
- package/src/modes/rpc/rpc-mode.ts +8 -7
package/src/core/tools/find.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { globSync } from "glob";
|
|
6
6
|
import { ensureTool } from "../../utils/tools-manager";
|
|
7
|
+
import { untilAborted } from "../utils";
|
|
7
8
|
import { resolveToCwd } from "./path-utils";
|
|
8
9
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
9
10
|
|
|
@@ -67,191 +68,171 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
|
|
|
67
68
|
},
|
|
68
69
|
signal?: AbortSignal,
|
|
69
70
|
) => {
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
return untilAborted(signal, async () => {
|
|
72
|
+
// Ensure fd is available
|
|
73
|
+
const fdPath = await ensureTool("fd", true);
|
|
74
|
+
if (!fdPath) {
|
|
75
|
+
throw new Error("fd is not available and could not be downloaded");
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
+
const searchPath = resolveToCwd(searchDir || ".", cwd);
|
|
79
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
80
|
+
const effectiveType = type ?? "all";
|
|
81
|
+
const includeHidden = hidden ?? false;
|
|
82
|
+
const shouldSortByMtime = sortByMtime ?? false;
|
|
83
|
+
|
|
84
|
+
// Build fd arguments
|
|
85
|
+
const args: string[] = [
|
|
86
|
+
"--glob", // Use glob pattern
|
|
87
|
+
"--color=never", // No ANSI colors
|
|
88
|
+
"--max-results",
|
|
89
|
+
String(effectiveLimit),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
if (includeHidden) {
|
|
93
|
+
args.push("--hidden");
|
|
94
|
+
}
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
96
|
+
// Add type filter
|
|
97
|
+
if (effectiveType === "file") {
|
|
98
|
+
args.push("--type", "f");
|
|
99
|
+
} else if (effectiveType === "dir") {
|
|
100
|
+
args.push("--type", "d");
|
|
101
|
+
}
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Build fd arguments
|
|
95
|
-
const args: string[] = [
|
|
96
|
-
"--glob", // Use glob pattern
|
|
97
|
-
"--color=never", // No ANSI colors
|
|
98
|
-
"--max-results",
|
|
99
|
-
String(effectiveLimit),
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
if (includeHidden) {
|
|
103
|
-
args.push("--hidden");
|
|
104
|
-
}
|
|
103
|
+
// Include .gitignore files (root + nested) so fd respects them even outside git repos
|
|
104
|
+
const gitignoreFiles = new Set<string>();
|
|
105
|
+
const rootGitignore = path.join(searchPath, ".gitignore");
|
|
106
|
+
if (existsSync(rootGitignore)) {
|
|
107
|
+
gitignoreFiles.add(rootGitignore);
|
|
108
|
+
}
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
try {
|
|
111
|
+
const nestedGitignores = globSync("**/.gitignore", {
|
|
112
|
+
cwd: searchPath,
|
|
113
|
+
dot: true,
|
|
114
|
+
absolute: true,
|
|
115
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
116
|
+
});
|
|
117
|
+
for (const file of nestedGitignores) {
|
|
118
|
+
gitignoreFiles.add(file);
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore glob errors
|
|
122
|
+
}
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (existsSync(rootGitignore)) {
|
|
117
|
-
gitignoreFiles.add(rootGitignore);
|
|
118
|
-
}
|
|
124
|
+
for (const gitignorePath of gitignoreFiles) {
|
|
125
|
+
args.push("--ignore-file", gitignorePath);
|
|
126
|
+
}
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
cwd: searchPath,
|
|
123
|
-
dot: true,
|
|
124
|
-
absolute: true,
|
|
125
|
-
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
126
|
-
});
|
|
127
|
-
for (const file of nestedGitignores) {
|
|
128
|
-
gitignoreFiles.add(file);
|
|
129
|
-
}
|
|
130
|
-
} catch {
|
|
131
|
-
// Ignore glob errors
|
|
132
|
-
}
|
|
128
|
+
// Pattern and path
|
|
129
|
+
args.push(pattern, searchPath);
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
// Run fd
|
|
132
|
+
const result = Bun.spawnSync([fdPath, ...args], {
|
|
133
|
+
stdin: "ignore",
|
|
134
|
+
stdout: "pipe",
|
|
135
|
+
stderr: "pipe",
|
|
136
|
+
});
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
args.push(pattern, searchPath);
|
|
138
|
+
const output = result.stdout.toString().trim();
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
if (result.exitCode !== 0) {
|
|
141
|
+
const errorMsg = result.stderr.toString().trim() || `fd exited with code ${result.exitCode}`;
|
|
142
|
+
// fd returns non-zero for some errors but may still have partial output
|
|
143
|
+
if (!output) {
|
|
144
|
+
throw new Error(errorMsg);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
if (!output) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
151
|
+
details: { fileCount: 0, files: [], truncated: false },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
149
154
|
|
|
150
|
-
|
|
155
|
+
const lines = output.split("\n");
|
|
156
|
+
const relativized: string[] = [];
|
|
157
|
+
const mtimes: number[] = [];
|
|
151
158
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
159
|
+
for (const rawLine of lines) {
|
|
160
|
+
const line = rawLine.replace(/\r$/, "").trim();
|
|
161
|
+
if (!line) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
166
|
+
let relativePath = line;
|
|
167
|
+
if (line.startsWith(searchPath)) {
|
|
168
|
+
relativePath = line.slice(searchPath.length + 1); // +1 for the /
|
|
169
|
+
} else {
|
|
170
|
+
relativePath = path.relative(searchPath, line);
|
|
171
|
+
}
|
|
168
172
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const line = rawLine.replace(/\r$/, "").trim();
|
|
175
|
-
if (!line) {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
180
|
-
let relativePath = line;
|
|
181
|
-
if (line.startsWith(searchPath)) {
|
|
182
|
-
relativePath = line.slice(searchPath.length + 1); // +1 for the /
|
|
183
|
-
} else {
|
|
184
|
-
relativePath = path.relative(searchPath, line);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (hadTrailingSlash && !relativePath.endsWith("/")) {
|
|
188
|
-
relativePath += "/";
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
relativized.push(relativePath);
|
|
192
|
-
|
|
193
|
-
// Collect mtime if sorting is requested
|
|
194
|
-
if (shouldSortByMtime) {
|
|
195
|
-
try {
|
|
196
|
-
const fullPath = path.join(searchPath, relativePath);
|
|
197
|
-
const stat: Stats = statSync(fullPath);
|
|
198
|
-
mtimes.push(stat.mtimeMs);
|
|
199
|
-
} catch {
|
|
200
|
-
mtimes.push(0);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
173
|
+
if (hadTrailingSlash && !relativePath.endsWith("/")) {
|
|
174
|
+
relativePath += "/";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
relativized.push(relativePath);
|
|
204
178
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
179
|
+
// Collect mtime if sorting is requested
|
|
180
|
+
if (shouldSortByMtime) {
|
|
181
|
+
try {
|
|
182
|
+
const fullPath = path.join(searchPath, relativePath);
|
|
183
|
+
const stat: Stats = statSync(fullPath);
|
|
184
|
+
mtimes.push(stat.mtimeMs);
|
|
185
|
+
} catch {
|
|
186
|
+
mtimes.push(0);
|
|
211
187
|
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
212
190
|
|
|
213
|
-
|
|
214
|
-
|
|
191
|
+
// Sort by mtime if requested (most recent first)
|
|
192
|
+
if (shouldSortByMtime && relativized.length > 0) {
|
|
193
|
+
const indexed = relativized.map((path, idx) => ({ path, mtime: mtimes[idx] || 0 }));
|
|
194
|
+
indexed.sort((a, b) => b.mtime - a.mtime);
|
|
195
|
+
relativized.length = 0;
|
|
196
|
+
relativized.push(...indexed.map((item) => item.path));
|
|
197
|
+
}
|
|
215
198
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
199
|
+
// Check if we hit the result limit
|
|
200
|
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
219
201
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
files: relativized.slice(0, 50),
|
|
224
|
-
truncated: resultLimitReached || truncation.truncated,
|
|
225
|
-
};
|
|
202
|
+
// Apply byte truncation (no line limit since we already have result limit)
|
|
203
|
+
const rawOutput = relativized.join("\n");
|
|
204
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
226
205
|
|
|
227
|
-
|
|
228
|
-
|
|
206
|
+
let resultOutput = truncation.content;
|
|
207
|
+
const details: FindToolDetails = {
|
|
208
|
+
fileCount: relativized.length,
|
|
209
|
+
files: relativized.slice(0, 50),
|
|
210
|
+
truncated: resultLimitReached || truncation.truncated,
|
|
211
|
+
};
|
|
229
212
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
233
|
-
);
|
|
234
|
-
details.resultLimitReached = effectiveLimit;
|
|
235
|
-
}
|
|
213
|
+
// Build notices
|
|
214
|
+
const notices: string[] = [];
|
|
236
215
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
216
|
+
if (resultLimitReached) {
|
|
217
|
+
notices.push(
|
|
218
|
+
`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
219
|
+
);
|
|
220
|
+
details.resultLimitReached = effectiveLimit;
|
|
221
|
+
}
|
|
241
222
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
223
|
+
if (truncation.truncated) {
|
|
224
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
225
|
+
details.truncation = truncation;
|
|
226
|
+
}
|
|
245
227
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})();
|
|
228
|
+
if (notices.length > 0) {
|
|
229
|
+
resultOutput += `\n\n[${notices.join(". ")}]`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: resultOutput }],
|
|
234
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
235
|
+
};
|
|
255
236
|
});
|
|
256
237
|
},
|
|
257
238
|
};
|
package/src/core/tools/index.ts
CHANGED
|
@@ -11,8 +11,6 @@ export {
|
|
|
11
11
|
createLspTool,
|
|
12
12
|
type FileDiagnosticsResult,
|
|
13
13
|
type FileFormatResult,
|
|
14
|
-
formatFile,
|
|
15
|
-
getDiagnosticsForFile,
|
|
16
14
|
getLspStatus,
|
|
17
15
|
type LspServerStatus,
|
|
18
16
|
type LspToolDetails,
|
|
@@ -61,7 +59,7 @@ import { createEditTool, editTool } from "./edit";
|
|
|
61
59
|
import { createFindTool, findTool } from "./find";
|
|
62
60
|
import { createGrepTool, grepTool } from "./grep";
|
|
63
61
|
import { createLsTool, lsTool } from "./ls";
|
|
64
|
-
import { createLspTool,
|
|
62
|
+
import { createLspTool, createLspWritethrough, lspTool } from "./lsp/index";
|
|
65
63
|
import { createNotebookTool, notebookTool } from "./notebook";
|
|
66
64
|
import { createOutputTool, outputTool } from "./output";
|
|
67
65
|
import { createReadTool, readTool } from "./read";
|
|
@@ -105,10 +103,12 @@ const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
|
|
|
105
103
|
tool: editTool,
|
|
106
104
|
create: (cwd, _ctx, options) => {
|
|
107
105
|
const enableDiagnostics = options?.lspDiagnosticsOnEdit ?? false;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
const enableFormat = options?.lspFormatOnWrite ?? true;
|
|
107
|
+
const writethrough = createLspWritethrough(cwd, {
|
|
108
|
+
enableFormat,
|
|
109
|
+
enableDiagnostics,
|
|
111
110
|
});
|
|
111
|
+
return createEditTool(cwd, { fuzzyMatch: options?.editFuzzyMatch ?? true, writethrough });
|
|
112
112
|
},
|
|
113
113
|
},
|
|
114
114
|
write: {
|
|
@@ -116,10 +116,11 @@ const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
|
|
|
116
116
|
create: (cwd, _ctx, options) => {
|
|
117
117
|
const enableFormat = options?.lspFormatOnWrite ?? true;
|
|
118
118
|
const enableDiagnostics = options?.lspDiagnosticsOnWrite ?? true;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const writethrough = createLspWritethrough(cwd, {
|
|
120
|
+
enableFormat,
|
|
121
|
+
enableDiagnostics,
|
|
122
122
|
});
|
|
123
|
+
return createWriteTool(cwd, { writethrough });
|
|
123
124
|
},
|
|
124
125
|
},
|
|
125
126
|
grep: { tool: grepTool, create: createGrepTool },
|
package/src/core/tools/ls.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, statSync } from "node:fs";
|
|
|
2
2
|
import nodePath from "node:path";
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { untilAborted } from "../utils";
|
|
5
6
|
import { resolveToCwd } from "./path-utils";
|
|
6
7
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
7
8
|
|
|
@@ -28,109 +29,90 @@ export function createLsTool(cwd: string): AgentTool<typeof lsSchema> {
|
|
|
28
29
|
{ path, limit }: { path?: string; limit?: number },
|
|
29
30
|
signal?: AbortSignal,
|
|
30
31
|
) => {
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
return untilAborted(signal, async () => {
|
|
33
|
+
const dirPath = resolveToCwd(path || ".", cwd);
|
|
34
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
35
|
+
|
|
36
|
+
// Check if path exists
|
|
37
|
+
if (!existsSync(dirPath)) {
|
|
38
|
+
throw new Error(`Path not found: ${dirPath}`);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
// Check if path is a directory
|
|
42
|
+
const stat = statSync(dirPath);
|
|
43
|
+
if (!stat.isDirectory()) {
|
|
44
|
+
throw new Error(`Not a directory: ${dirPath}`);
|
|
45
|
+
}
|
|
39
46
|
|
|
47
|
+
// Read directory entries
|
|
48
|
+
let entries: string[];
|
|
40
49
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
entries = readdirSync(dirPath);
|
|
51
|
+
} catch (e: any) {
|
|
52
|
+
throw new Error(`Cannot read directory: ${e.message}`);
|
|
53
|
+
}
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
reject(new Error(`Path not found: ${dirPath}`));
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
55
|
+
// Sort alphabetically (case-insensitive)
|
|
56
|
+
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
reject(new Error(`Not a directory: ${dirPath}`));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
58
|
+
// Format entries with directory indicators
|
|
59
|
+
const results: string[] = [];
|
|
60
|
+
let entryLimitReached = false;
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} catch (e: any) {
|
|
62
|
-
reject(new Error(`Cannot read directory: ${e.message}`));
|
|
63
|
-
return;
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (results.length >= effectiveLimit) {
|
|
64
|
+
entryLimitReached = true;
|
|
65
|
+
break;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Format entries with directory indicators
|
|
70
|
-
const results: string[] = [];
|
|
71
|
-
let entryLimitReached = false;
|
|
72
|
-
|
|
73
|
-
for (const entry of entries) {
|
|
74
|
-
if (results.length >= effectiveLimit) {
|
|
75
|
-
entryLimitReached = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
68
|
+
const fullPath = nodePath.join(dirPath, entry);
|
|
69
|
+
let suffix = "";
|
|
78
70
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const entryStat = statSync(fullPath);
|
|
84
|
-
if (entryStat.isDirectory()) {
|
|
85
|
-
suffix = "/";
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
// Skip entries we can't stat
|
|
89
|
-
continue;
|
|
71
|
+
try {
|
|
72
|
+
const entryStat = statSync(fullPath);
|
|
73
|
+
if (entryStat.isDirectory()) {
|
|
74
|
+
suffix = "/";
|
|
90
75
|
}
|
|
91
|
-
|
|
92
|
-
|
|
76
|
+
} catch {
|
|
77
|
+
// Skip entries we can't stat
|
|
78
|
+
continue;
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (results.length === 0) {
|
|
98
|
-
resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
81
|
+
results.push(entry + suffix);
|
|
82
|
+
}
|
|
101
83
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
84
|
+
if (results.length === 0) {
|
|
85
|
+
return { content: [{ type: "text", text: "(empty directory)" }], details: undefined };
|
|
86
|
+
}
|
|
105
87
|
|
|
106
|
-
|
|
107
|
-
|
|
88
|
+
// Apply byte truncation (no line limit since we already have entry limit)
|
|
89
|
+
const rawOutput = results.join("\n");
|
|
90
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
|
|
92
|
+
let output = truncation.content;
|
|
93
|
+
const details: LsToolDetails = {};
|
|
111
94
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
details.entryLimitReached = effectiveLimit;
|
|
115
|
-
}
|
|
95
|
+
// Build notices
|
|
96
|
+
const notices: string[] = [];
|
|
116
97
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
98
|
+
if (entryLimitReached) {
|
|
99
|
+
notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
|
|
100
|
+
details.entryLimitReached = effectiveLimit;
|
|
101
|
+
}
|
|
121
102
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
if (truncation.truncated) {
|
|
104
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
105
|
+
details.truncation = truncation;
|
|
106
|
+
}
|
|
125
107
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
details: Object.keys(details).length > 0 ? details : undefined,
|
|
129
|
-
});
|
|
130
|
-
} catch (e: any) {
|
|
131
|
-
signal?.removeEventListener("abort", onAbort);
|
|
132
|
-
reject(e);
|
|
108
|
+
if (notices.length > 0) {
|
|
109
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
133
110
|
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: output }],
|
|
114
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
115
|
+
};
|
|
134
116
|
});
|
|
135
117
|
},
|
|
136
118
|
};
|
|
@@ -512,6 +512,69 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Sync in-memory content to the LSP client without reading from disk.
|
|
517
|
+
* Use this to provide instant feedback during edits before the file is saved.
|
|
518
|
+
*/
|
|
519
|
+
export async function syncContent(client: LspClient, filePath: string, content: string): Promise<void> {
|
|
520
|
+
const uri = fileToUri(filePath);
|
|
521
|
+
const lockKey = `${client.name}:${uri}`;
|
|
522
|
+
|
|
523
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
524
|
+
if (existingLock) {
|
|
525
|
+
await existingLock;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const syncPromise = (async () => {
|
|
529
|
+
const info = client.openFiles.get(uri);
|
|
530
|
+
|
|
531
|
+
if (!info) {
|
|
532
|
+
// Open file with provided content instead of reading from disk
|
|
533
|
+
const languageId = detectLanguageId(filePath);
|
|
534
|
+
await sendNotification(client, "textDocument/didOpen", {
|
|
535
|
+
textDocument: {
|
|
536
|
+
uri,
|
|
537
|
+
languageId,
|
|
538
|
+
version: 1,
|
|
539
|
+
text: content,
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
client.openFiles.set(uri, { version: 1, languageId });
|
|
543
|
+
client.lastActivity = Date.now();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const version = ++info.version;
|
|
548
|
+
await sendNotification(client, "textDocument/didChange", {
|
|
549
|
+
textDocument: { uri, version },
|
|
550
|
+
contentChanges: [{ text: content }],
|
|
551
|
+
});
|
|
552
|
+
client.lastActivity = Date.now();
|
|
553
|
+
})();
|
|
554
|
+
|
|
555
|
+
fileOperationLocks.set(lockKey, syncPromise);
|
|
556
|
+
try {
|
|
557
|
+
await syncPromise;
|
|
558
|
+
} finally {
|
|
559
|
+
fileOperationLocks.delete(lockKey);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Notify LSP that a file was saved.
|
|
565
|
+
* Assumes content was already synced via syncContent - just sends didSave.
|
|
566
|
+
*/
|
|
567
|
+
export async function notifySaved(client: LspClient, filePath: string): Promise<void> {
|
|
568
|
+
const uri = fileToUri(filePath);
|
|
569
|
+
const info = client.openFiles.get(uri);
|
|
570
|
+
if (!info) return; // File not open, nothing to notify
|
|
571
|
+
|
|
572
|
+
await sendNotification(client, "textDocument/didSave", {
|
|
573
|
+
textDocument: { uri },
|
|
574
|
+
});
|
|
575
|
+
client.lastActivity = Date.now();
|
|
576
|
+
}
|
|
577
|
+
|
|
515
578
|
/**
|
|
516
579
|
* Refresh a file in the LSP client.
|
|
517
580
|
* Increments version, sends didChange and didSave notifications.
|