@xynogen/pix-pretty 1.2.0 → 1.3.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/README.md +9 -4
- package/package.json +2 -3
- package/src/ansi.ts +0 -10
- package/src/commands/fff.ts +60 -0
- package/src/diff-render.ts +92 -16
- package/src/image.ts +0 -3
- package/src/index.ts +88 -1452
- package/src/tools/bash.ts +154 -0
- package/src/tools/context.ts +19 -0
- package/src/tools/edit.ts +291 -0
- package/src/tools/find.ts +158 -0
- package/src/tools/grep.ts +202 -0
- package/src/tools/ls.ts +111 -0
- package/src/tools/multi-grep.ts +328 -0
- package/src/tools/read.ts +177 -0
- package/src/tools/write.ts +231 -0
- package/src/tsconfig.json +1 -1
- package/src/types.ts +30 -1
- package/src/utils.ts +45 -2
package/src/index.ts
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-pretty — Pretty terminal output for pi built-in tools.
|
|
3
3
|
*
|
|
4
|
-
* Entry point:
|
|
5
|
-
* delegates execute() unchanged, and attaches custom renderCall/renderResult.
|
|
4
|
+
* Entry point: boots shared state, registers all tool overrides and commands.
|
|
6
5
|
*
|
|
7
6
|
* Modules:
|
|
8
|
-
* types.ts
|
|
9
|
-
* config.ts
|
|
10
|
-
* ansi.ts
|
|
11
|
-
* utils.ts
|
|
12
|
-
* lang.ts
|
|
13
|
-
* image.ts
|
|
14
|
-
* icons.ts
|
|
15
|
-
* highlight.ts
|
|
16
|
-
* renderers.ts
|
|
17
|
-
* fff.ts
|
|
7
|
+
* types.ts shared interfaces/types
|
|
8
|
+
* config.ts theme + thresholds
|
|
9
|
+
* ansi.ts ANSI codes, low-contrast fix
|
|
10
|
+
* utils.ts helpers + renderToolError
|
|
11
|
+
* lang.ts language detection
|
|
12
|
+
* image.ts terminal image protocols
|
|
13
|
+
* icons.ts Nerd Font file-type icons
|
|
14
|
+
* highlight.ts cli-highlight engine + ANSI cache
|
|
15
|
+
* renderers.ts renderFileContent/Bash/Tree/Find/Grep
|
|
16
|
+
* fff.ts Fast File Finder + cursor store + multi-grep fallback
|
|
17
|
+
* diff.ts unified diff parser
|
|
18
|
+
* diff-render.ts split/word-level diff renderer
|
|
19
|
+
* tools/ per-tool registrars (read/bash/ls/find/grep/multi-grep/edit/write)
|
|
20
|
+
* commands/ slash command registrars (fff)
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
|
-
import {
|
|
23
|
+
import { mkdirSync } from "node:fs";
|
|
21
24
|
import { join } from "node:path";
|
|
22
25
|
import type {
|
|
23
|
-
AgentToolUpdateCallback,
|
|
24
26
|
BashToolInput,
|
|
25
27
|
EditToolInput,
|
|
26
28
|
ExtensionContext,
|
|
@@ -28,97 +30,64 @@ import type {
|
|
|
28
30
|
GrepToolInput,
|
|
29
31
|
LsToolInput,
|
|
30
32
|
ReadToolInput,
|
|
31
|
-
ToolRenderResultOptions,
|
|
32
33
|
WriteToolInput,
|
|
33
34
|
} from "@earendil-works/pi-coding-agent";
|
|
34
|
-
import
|
|
35
|
-
|
|
36
|
-
import { FG_DIM, RST, resolveBaseBackground } from "./ansi.js";
|
|
37
|
-
import {
|
|
38
|
-
getDefaultAgentDir,
|
|
39
|
-
MAX_PREVIEW_LINES,
|
|
40
|
-
MAX_RENDER_LINES,
|
|
41
|
-
setPrettyTheme,
|
|
42
|
-
} from "./config.js";
|
|
43
|
-
import { parseDiff } from "./diff.js";
|
|
44
|
-
import {
|
|
45
|
-
diffThemeCacheKey,
|
|
46
|
-
renderSplit,
|
|
47
|
-
resolveDiffColors,
|
|
48
|
-
summarize,
|
|
49
|
-
} from "./diff-render.js";
|
|
35
|
+
import { registerFffCommands } from "./commands/fff.js";
|
|
36
|
+
import { getDefaultAgentDir, setPrettyTheme } from "./config.js";
|
|
50
37
|
import {
|
|
51
38
|
CursorStore,
|
|
52
39
|
fffDestroy,
|
|
53
40
|
fffEnsureFinder,
|
|
54
|
-
fffFormatGrepText,
|
|
55
41
|
fffState,
|
|
56
42
|
getPiPrettyFffDir,
|
|
57
43
|
runMultiGrepRipgrepFallback,
|
|
58
44
|
} from "./fff.js";
|
|
59
|
-
import { clearHighlightCache
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} from "./
|
|
45
|
+
import { clearHighlightCache } from "./highlight.js";
|
|
46
|
+
import { registerBashTool } from "./tools/bash.js";
|
|
47
|
+
import type { ToolContext } from "./tools/context.js";
|
|
48
|
+
import { registerEditTool } from "./tools/edit.js";
|
|
49
|
+
import { registerFindTool } from "./tools/find.js";
|
|
50
|
+
import { registerGrepTool } from "./tools/grep.js";
|
|
51
|
+
import { registerLsTool } from "./tools/ls.js";
|
|
52
|
+
import { registerMultiGrepTool } from "./tools/multi-grep.js";
|
|
53
|
+
import { registerReadTool } from "./tools/read.js";
|
|
54
|
+
import { registerWriteTool } from "./tools/write.js";
|
|
69
55
|
import type {
|
|
70
|
-
BashParams,
|
|
71
|
-
CommandContextLike,
|
|
72
|
-
EditOperation,
|
|
73
|
-
EditParams,
|
|
74
|
-
EditRenderState,
|
|
75
|
-
FindParams,
|
|
76
|
-
FindResultDetails,
|
|
77
|
-
GrepParams,
|
|
78
|
-
GrepRenderState,
|
|
79
|
-
GrepResultDetails,
|
|
80
|
-
LsParams,
|
|
81
|
-
MultiGrepParams,
|
|
82
|
-
MultiGrepRenderState,
|
|
83
56
|
PiPrettyApi,
|
|
84
57
|
PiPrettyDeps,
|
|
85
58
|
PiPrettySdk,
|
|
86
|
-
ReadParams,
|
|
87
|
-
RenderContextLike,
|
|
88
|
-
RenderDetails,
|
|
89
59
|
TextComponentCtor,
|
|
90
|
-
ThemeLike,
|
|
91
60
|
ToolFactory,
|
|
92
|
-
ToolResultLike,
|
|
93
|
-
WriteParams,
|
|
94
|
-
WriteRenderState,
|
|
95
61
|
} from "./types.js";
|
|
96
|
-
import {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
62
|
+
import { getErrorMessage, shortPath } from "./utils.js";
|
|
63
|
+
|
|
64
|
+
// ── Resize invalidation registry ───────────────────────────────────────
|
|
65
|
+
// Diff/write renderResults register their ctx.invalidate keyed by toolCallId
|
|
66
|
+
// so terminal resize triggers re-render at the correct width.
|
|
67
|
+
|
|
68
|
+
const _resizeInvalidators = new Map<string, () => void>();
|
|
69
|
+
let _resizeListenerAttached = false;
|
|
70
|
+
|
|
71
|
+
function attachResizeListener(): void {
|
|
72
|
+
if (_resizeListenerAttached) return;
|
|
73
|
+
_resizeListenerAttached = true;
|
|
74
|
+
process.stdout.on("resize", () => {
|
|
75
|
+
for (const inv of _resizeInvalidators.values()) inv();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function trackInvalidator(toolCallId: string, inv: () => void): void {
|
|
80
|
+
_resizeInvalidators.set(toolCallId, inv);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Extension entry point ──────────────────────────────────────────────
|
|
117
84
|
|
|
118
85
|
export default function piPrettyExtension(
|
|
119
86
|
pi: PiPrettyApi,
|
|
120
87
|
deps?: PiPrettyDeps,
|
|
121
88
|
): void {
|
|
89
|
+
attachResizeListener();
|
|
90
|
+
|
|
122
91
|
let createReadTool: ToolFactory<ReadToolInput> | undefined;
|
|
123
92
|
let createBashTool: ToolFactory<BashToolInput> | undefined;
|
|
124
93
|
let createLsTool: ToolFactory<LsToolInput> | undefined;
|
|
@@ -127,13 +96,11 @@ export default function piPrettyExtension(
|
|
|
127
96
|
let createEditTool: ToolFactory<EditToolInput> | undefined;
|
|
128
97
|
let createWriteTool: ToolFactory<WriteToolInput> | undefined;
|
|
129
98
|
let TextComponent: TextComponentCtor;
|
|
130
|
-
|
|
131
99
|
let sdk: PiPrettySdk;
|
|
132
100
|
|
|
133
|
-
const
|
|
101
|
+
const cursorStore = new CursorStore();
|
|
134
102
|
|
|
135
103
|
if (deps) {
|
|
136
|
-
// Test path: use injected dependencies, reset module state
|
|
137
104
|
sdk = deps.sdk;
|
|
138
105
|
createReadTool = sdk.createReadToolDefinition ?? sdk.createReadTool;
|
|
139
106
|
createBashTool = sdk.createBashToolDefinition ?? sdk.createBashTool;
|
|
@@ -162,7 +129,7 @@ export default function piPrettyExtension(
|
|
|
162
129
|
return;
|
|
163
130
|
}
|
|
164
131
|
}
|
|
165
|
-
if (!createReadTool || !TextComponent) return;
|
|
132
|
+
if (!createReadTool || !TextComponent!) return;
|
|
166
133
|
|
|
167
134
|
const cwd = process.cwd();
|
|
168
135
|
const home = process.env.HOME ?? "";
|
|
@@ -170,20 +137,17 @@ export default function piPrettyExtension(
|
|
|
170
137
|
const multiGrepRipgrepFallback =
|
|
171
138
|
deps?.multiGrepRipgrepFallback ?? runMultiGrepRipgrepFallback;
|
|
172
139
|
|
|
173
|
-
//
|
|
140
|
+
// Respect PRETTY_DISABLE_TOOLS env var
|
|
174
141
|
const disabledTools = new Set(
|
|
175
142
|
(process.env.PRETTY_DISABLE_TOOLS ?? "")
|
|
176
143
|
.split(",")
|
|
177
144
|
.map((s) => s.trim().toLowerCase())
|
|
178
145
|
.filter(Boolean),
|
|
179
146
|
);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
147
|
+
const isToolEnabled = (name: string) =>
|
|
148
|
+
!disabledTools.has(name.toLowerCase());
|
|
183
149
|
|
|
184
|
-
//
|
|
185
|
-
// FFF initialization (optional — graceful fallback to SDK)
|
|
186
|
-
// ===================================================================
|
|
150
|
+
// ── Theme + FFF init ────────────────────────────────────────────────
|
|
187
151
|
|
|
188
152
|
const getAgentDir = sdk.getAgentDir;
|
|
189
153
|
setPrettyTheme(
|
|
@@ -196,8 +160,8 @@ export default function piPrettyExtension(
|
|
|
196
160
|
})(),
|
|
197
161
|
);
|
|
198
162
|
clearHighlightCache();
|
|
163
|
+
|
|
199
164
|
if (!deps) {
|
|
200
|
-
// Only try require() in production — tests inject fffModule via deps
|
|
201
165
|
try {
|
|
202
166
|
fffState.module = require("@ff-labs/fff-node");
|
|
203
167
|
if (getAgentDir) {
|
|
@@ -216,8 +180,7 @@ export default function piPrettyExtension(
|
|
|
216
180
|
} catch {}
|
|
217
181
|
}
|
|
218
182
|
|
|
219
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
220
|
-
// Try dynamic import if sync require failed (ESM-only package)
|
|
183
|
+
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
221
184
|
if (!fffState.module) {
|
|
222
185
|
try {
|
|
223
186
|
const imported = await import("@ff-labs/fff-node");
|
|
@@ -242,9 +205,6 @@ export default function piPrettyExtension(
|
|
|
242
205
|
"warning",
|
|
243
206
|
);
|
|
244
207
|
} else {
|
|
245
|
-
// Confirm indexing via a transient toast instead of a footer status
|
|
246
|
-
// segment — the footer sorts extension statuses by key, and "fff"
|
|
247
|
-
// sorting ahead of other extensions shifted their indicators.
|
|
248
208
|
ctx.ui?.notify?.("FFF indexed", "info");
|
|
249
209
|
}
|
|
250
210
|
} catch (error: unknown) {
|
|
@@ -256,1371 +216,47 @@ export default function piPrettyExtension(
|
|
|
256
216
|
fffDestroy();
|
|
257
217
|
});
|
|
258
218
|
|
|
259
|
-
//
|
|
260
|
-
// read — syntax-highlighted file content
|
|
261
|
-
// ===================================================================
|
|
262
|
-
|
|
263
|
-
const origRead = createReadTool(cwd);
|
|
264
|
-
|
|
265
|
-
if (isToolEnabled("read")) {
|
|
266
|
-
pi.registerTool({
|
|
267
|
-
...origRead,
|
|
268
|
-
name: "read",
|
|
269
|
-
|
|
270
|
-
async execute(
|
|
271
|
-
tid: string,
|
|
272
|
-
params: ReadParams,
|
|
273
|
-
sig: AbortSignal | undefined,
|
|
274
|
-
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
275
|
-
ctx: ExtensionContext,
|
|
276
|
-
) {
|
|
277
|
-
const result = (await origRead.execute(
|
|
278
|
-
tid,
|
|
279
|
-
params,
|
|
280
|
-
sig,
|
|
281
|
-
upd,
|
|
282
|
-
ctx,
|
|
283
|
-
)) as ToolResultLike;
|
|
284
|
-
|
|
285
|
-
const fp = params.path ?? "";
|
|
286
|
-
const offset = params.offset ?? 1;
|
|
219
|
+
// ── Build shared tool context ───────────────────────────────────────
|
|
287
220
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
221
|
+
const toolCtx: ToolContext = {
|
|
222
|
+
cwd,
|
|
223
|
+
sp,
|
|
224
|
+
TextComponent: TextComponent!,
|
|
225
|
+
fffState,
|
|
226
|
+
cursorStore,
|
|
227
|
+
multiGrepRipgrepFallback,
|
|
228
|
+
};
|
|
298
229
|
|
|
299
|
-
|
|
300
|
-
if (textContent && fp) {
|
|
301
|
-
const normalizedContent = normalizeLineEndings(textContent);
|
|
302
|
-
const lineCount = normalizedContent.split("\n").length;
|
|
303
|
-
setResultDetails(result, {
|
|
304
|
-
_type: "readFile",
|
|
305
|
-
filePath: fp,
|
|
306
|
-
content: normalizedContent,
|
|
307
|
-
offset,
|
|
308
|
-
lineCount,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
230
|
+
// ── Register tools ──────────────────────────────────────────────────
|
|
311
231
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
renderCall(args: ReadParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
316
|
-
resolveBaseBackground(theme);
|
|
317
|
-
const fp = args.path ?? "";
|
|
318
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
319
|
-
const offset = args.offset
|
|
320
|
-
? ` ${theme.fg("muted", `from line ${args.offset}`)}`
|
|
321
|
-
: "";
|
|
322
|
-
const limit = args.limit
|
|
323
|
-
? ` ${theme.fg("muted", `(${args.limit} lines)`)}`
|
|
324
|
-
: "";
|
|
325
|
-
text.setText(
|
|
326
|
-
fillToolBackground(
|
|
327
|
-
`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`,
|
|
328
|
-
),
|
|
329
|
-
);
|
|
330
|
-
return text;
|
|
331
|
-
},
|
|
332
|
-
|
|
333
|
-
renderResult(
|
|
334
|
-
result: ToolResultLike,
|
|
335
|
-
_opt: unknown,
|
|
336
|
-
theme: ThemeLike,
|
|
337
|
-
ctx: RenderContextLike,
|
|
338
|
-
) {
|
|
339
|
-
resolveBaseBackground(theme);
|
|
340
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
341
|
-
|
|
342
|
-
if (ctx.isError) {
|
|
343
|
-
text.setText(
|
|
344
|
-
renderToolError(getTextContent(result) || "Error", theme),
|
|
345
|
-
);
|
|
346
|
-
return text;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const d = result.details as RenderDetails | undefined;
|
|
350
|
-
|
|
351
|
-
// Image reads keep the original image content so Pi's native TUI renderer
|
|
352
|
-
// can display it exactly once. pi-pretty only renders metadata here;
|
|
353
|
-
// rendering another inline image caused duplicate previews.
|
|
354
|
-
if (d?._type === "readImage") {
|
|
355
|
-
const byteSize = Math.ceil(((d.data as string).length * 3) / 4);
|
|
356
|
-
const sizeStr = humanSize(byteSize);
|
|
357
|
-
const mimeStr = d.mimeType ?? "image";
|
|
358
|
-
|
|
359
|
-
text.setText(
|
|
360
|
-
fillToolBackground(
|
|
361
|
-
` ${fileIcon(d.filePath)}${FG_DIM}${mimeStr} · ${sizeStr}${RST}`,
|
|
362
|
-
),
|
|
363
|
-
);
|
|
364
|
-
return text;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (d?._type === "readFile" && d.content) {
|
|
368
|
-
const key = `read:${d.filePath}:${d.offset}:${d.lineCount}:${termW()}`;
|
|
369
|
-
if (ctx.state._rk !== key) {
|
|
370
|
-
ctx.state._rk = key;
|
|
371
|
-
const info = `${FG_DIM}${d.lineCount} lines${RST}`;
|
|
372
|
-
ctx.state._rt = fillToolBackground(` ${info}`);
|
|
373
|
-
|
|
374
|
-
const maxShow = ctx.expanded ? d.lineCount : MAX_PREVIEW_LINES;
|
|
375
|
-
renderFileContent(d.content, d.filePath, d.offset, maxShow)
|
|
376
|
-
.then((rendered: string) => {
|
|
377
|
-
if (ctx.state._rk !== key) return;
|
|
378
|
-
ctx.state._rt = fillToolBackground(` ${info}\n${rendered}`);
|
|
379
|
-
ctx.invalidate();
|
|
380
|
-
})
|
|
381
|
-
.catch(() => {});
|
|
382
|
-
}
|
|
383
|
-
text.setText(
|
|
384
|
-
ctx.state._rt ??
|
|
385
|
-
fillToolBackground(` ${FG_DIM}${d.lineCount} lines${RST}`),
|
|
386
|
-
);
|
|
387
|
-
return text;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Fallback
|
|
391
|
-
const fallback = result.content?.[0];
|
|
392
|
-
const fallbackText =
|
|
393
|
-
fallback && isTextContent(fallback) ? fallback.text : "read";
|
|
394
|
-
text.setText(
|
|
395
|
-
fillToolBackground(
|
|
396
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
397
|
-
),
|
|
398
|
-
);
|
|
399
|
-
return text;
|
|
400
|
-
},
|
|
401
|
-
});
|
|
232
|
+
if (isToolEnabled("read") && createReadTool) {
|
|
233
|
+
registerReadTool(pi, createReadTool, toolCtx);
|
|
402
234
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// bash — colored exit status
|
|
406
|
-
// ===================================================================
|
|
407
|
-
|
|
408
|
-
if (createBashTool) {
|
|
409
|
-
const origBash = createBashTool(cwd);
|
|
410
|
-
|
|
411
|
-
pi.registerTool({
|
|
412
|
-
...origBash,
|
|
413
|
-
name: "bash",
|
|
414
|
-
|
|
415
|
-
async execute(
|
|
416
|
-
tid: string,
|
|
417
|
-
params: BashParams,
|
|
418
|
-
sig: AbortSignal | undefined,
|
|
419
|
-
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
420
|
-
ctx: ExtensionContext,
|
|
421
|
-
) {
|
|
422
|
-
const result = (await origBash.execute(
|
|
423
|
-
tid,
|
|
424
|
-
params,
|
|
425
|
-
sig,
|
|
426
|
-
upd,
|
|
427
|
-
ctx,
|
|
428
|
-
)) as ToolResultLike;
|
|
429
|
-
const textContent = getTextContent(result);
|
|
430
|
-
|
|
431
|
-
let exitCode: number | null = 0;
|
|
432
|
-
if (textContent) {
|
|
433
|
-
const exitMatch = textContent.match(
|
|
434
|
-
/(?:exit code|exited with|exit status)[:\s]*(\d+)/i,
|
|
435
|
-
);
|
|
436
|
-
if (exitMatch) exitCode = Number(exitMatch[1]);
|
|
437
|
-
if (
|
|
438
|
-
textContent.includes("command not found") ||
|
|
439
|
-
textContent.includes("No such file")
|
|
440
|
-
) {
|
|
441
|
-
exitCode = 1;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
setResultDetails(result, {
|
|
446
|
-
_type: "bashResult",
|
|
447
|
-
text: textContent ?? "",
|
|
448
|
-
exitCode,
|
|
449
|
-
command: params.command ?? "",
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
return result;
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
renderCall(args: BashParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
456
|
-
resolveBaseBackground(theme);
|
|
457
|
-
const cmd = args.command ?? "";
|
|
458
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
459
|
-
const timeout = args.timeout
|
|
460
|
-
? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}`
|
|
461
|
-
: "";
|
|
462
|
-
const displayCmd =
|
|
463
|
-
ctx.expanded || cmd.length <= 80 ? cmd : `${cmd.slice(0, 77)}…`;
|
|
464
|
-
text.setText(
|
|
465
|
-
fillToolBackground(
|
|
466
|
-
`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", displayCmd)}${timeout}`,
|
|
467
|
-
),
|
|
468
|
-
);
|
|
469
|
-
return text;
|
|
470
|
-
},
|
|
471
|
-
|
|
472
|
-
renderResult(
|
|
473
|
-
result: ToolResultLike,
|
|
474
|
-
_opt: unknown,
|
|
475
|
-
theme: ThemeLike,
|
|
476
|
-
ctx: RenderContextLike,
|
|
477
|
-
) {
|
|
478
|
-
resolveBaseBackground(theme);
|
|
479
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
480
|
-
|
|
481
|
-
if (ctx.isError) {
|
|
482
|
-
text.setText(
|
|
483
|
-
renderToolError(getTextContent(result) || "Error", theme),
|
|
484
|
-
);
|
|
485
|
-
return text;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const d = result.details as RenderDetails | undefined;
|
|
489
|
-
if (d?._type === "bashResult") {
|
|
490
|
-
const { summary } = renderBashOutput(d.text, d.exitCode);
|
|
491
|
-
const lines = d.text.split("\n");
|
|
492
|
-
const lineCount = lines.length;
|
|
493
|
-
const lineInfo =
|
|
494
|
-
lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST}` : "";
|
|
495
|
-
const header = ` ${summary}${lineInfo}`;
|
|
496
|
-
|
|
497
|
-
if (d.text.trim()) {
|
|
498
|
-
const maxShow = ctx.expanded ? lineCount : MAX_PREVIEW_LINES;
|
|
499
|
-
const show = lines.slice(0, maxShow);
|
|
500
|
-
const tw = termW();
|
|
501
|
-
const out: string[] = [header, rule(tw)];
|
|
502
|
-
for (const line of show) {
|
|
503
|
-
out.push(` ${line}`);
|
|
504
|
-
}
|
|
505
|
-
out.push(rule(tw));
|
|
506
|
-
if (lineCount > maxShow) {
|
|
507
|
-
out.push(`${FG_DIM} … ${lineCount - maxShow} more lines${RST}`);
|
|
508
|
-
}
|
|
509
|
-
text.setText(fillToolBackground(out.join("\n")));
|
|
510
|
-
} else {
|
|
511
|
-
text.setText(fillToolBackground(header));
|
|
512
|
-
}
|
|
513
|
-
return text;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const fallback = result.content?.[0];
|
|
517
|
-
const fallbackText =
|
|
518
|
-
fallback && isTextContent(fallback) ? fallback.text : "done";
|
|
519
|
-
text.setText(
|
|
520
|
-
fillToolBackground(
|
|
521
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
522
|
-
),
|
|
523
|
-
);
|
|
524
|
-
return text;
|
|
525
|
-
},
|
|
526
|
-
});
|
|
235
|
+
if (isToolEnabled("bash") && createBashTool) {
|
|
236
|
+
registerBashTool(pi, createBashTool, toolCtx);
|
|
527
237
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// ls — tree view with icons
|
|
531
|
-
// ===================================================================
|
|
532
|
-
|
|
533
|
-
if (createLsTool) {
|
|
534
|
-
const origLs = createLsTool(cwd);
|
|
535
|
-
|
|
536
|
-
pi.registerTool({
|
|
537
|
-
...origLs,
|
|
538
|
-
name: "ls",
|
|
539
|
-
|
|
540
|
-
async execute(
|
|
541
|
-
tid: string,
|
|
542
|
-
params: LsParams,
|
|
543
|
-
sig: AbortSignal | undefined,
|
|
544
|
-
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
545
|
-
ctx: ExtensionContext,
|
|
546
|
-
) {
|
|
547
|
-
const result = (await origLs.execute(
|
|
548
|
-
tid,
|
|
549
|
-
params,
|
|
550
|
-
sig,
|
|
551
|
-
upd,
|
|
552
|
-
ctx,
|
|
553
|
-
)) as ToolResultLike;
|
|
554
|
-
const textContent = getTextContent(result);
|
|
555
|
-
const fp = params.path ?? cwd;
|
|
556
|
-
const entryCount = textContent
|
|
557
|
-
? textContent.trim().split("\n").filter(Boolean).length
|
|
558
|
-
: 0;
|
|
559
|
-
|
|
560
|
-
setResultDetails(result, {
|
|
561
|
-
_type: "lsResult",
|
|
562
|
-
text: textContent ?? "",
|
|
563
|
-
path: fp,
|
|
564
|
-
entryCount,
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
return result;
|
|
568
|
-
},
|
|
569
|
-
|
|
570
|
-
renderCall(args: LsParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
571
|
-
resolveBaseBackground(theme);
|
|
572
|
-
const fp = args.path ?? ".";
|
|
573
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
574
|
-
text.setText(
|
|
575
|
-
fillToolBackground(
|
|
576
|
-
`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`,
|
|
577
|
-
),
|
|
578
|
-
);
|
|
579
|
-
return text;
|
|
580
|
-
},
|
|
581
|
-
|
|
582
|
-
renderResult(
|
|
583
|
-
result: ToolResultLike,
|
|
584
|
-
_opt: unknown,
|
|
585
|
-
theme: ThemeLike,
|
|
586
|
-
ctx: RenderContextLike,
|
|
587
|
-
) {
|
|
588
|
-
resolveBaseBackground(theme);
|
|
589
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
590
|
-
|
|
591
|
-
if (ctx.isError) {
|
|
592
|
-
text.setText(
|
|
593
|
-
renderToolError(getTextContent(result) || "Error", theme),
|
|
594
|
-
);
|
|
595
|
-
return text;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const d = result.details as RenderDetails | undefined;
|
|
599
|
-
if (d?._type === "lsResult" && d.text) {
|
|
600
|
-
const tree = renderTree(d.text, d.path);
|
|
601
|
-
const info = `${FG_DIM}${d.entryCount} entries${RST}`;
|
|
602
|
-
text.setText(fillToolBackground(` ${info}\n${tree}`));
|
|
603
|
-
return text;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const fallback = result.content?.[0];
|
|
607
|
-
const fallbackText =
|
|
608
|
-
fallback && isTextContent(fallback) ? fallback.text : "listed";
|
|
609
|
-
text.setText(
|
|
610
|
-
fillToolBackground(
|
|
611
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
612
|
-
),
|
|
613
|
-
);
|
|
614
|
-
return text;
|
|
615
|
-
},
|
|
616
|
-
});
|
|
238
|
+
if (isToolEnabled("ls") && createLsTool) {
|
|
239
|
+
registerLsTool(pi, createLsTool, toolCtx);
|
|
617
240
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
// find — grouped file list with icons
|
|
621
|
-
// ===================================================================
|
|
622
|
-
|
|
623
|
-
if (createFindTool) {
|
|
624
|
-
const origFind = createFindTool(cwd);
|
|
625
|
-
|
|
626
|
-
pi.registerTool({
|
|
627
|
-
...origFind,
|
|
628
|
-
name: "find",
|
|
629
|
-
|
|
630
|
-
async execute(
|
|
631
|
-
tid: string,
|
|
632
|
-
params: FindParams,
|
|
633
|
-
sig: AbortSignal | undefined,
|
|
634
|
-
upd: unknown,
|
|
635
|
-
ctx: ExtensionContext,
|
|
636
|
-
) {
|
|
637
|
-
// Try FFF first (frecency-ranked, SIMD-accelerated)
|
|
638
|
-
if (fffState.finder && !fffState.finder.isDestroyed) {
|
|
639
|
-
try {
|
|
640
|
-
const effectiveLimit = Math.max(1, params.limit ?? 200);
|
|
641
|
-
let query = params.pattern;
|
|
642
|
-
if (params.path) query = `${params.path} ${query}`;
|
|
643
|
-
|
|
644
|
-
const searchResult = fffState.finder.fileSearch(query, {
|
|
645
|
-
pageSize: effectiveLimit,
|
|
646
|
-
});
|
|
647
|
-
if (searchResult.ok) {
|
|
648
|
-
const search: SearchResult = searchResult.value;
|
|
649
|
-
const items: FileItem[] = search.items.slice(0, effectiveLimit);
|
|
650
|
-
const notices: string[] = [];
|
|
651
|
-
if (fffState.partialIndex)
|
|
652
|
-
notices.push("Warning: partial file index");
|
|
653
|
-
if (items.length >= effectiveLimit)
|
|
654
|
-
notices.push(`${effectiveLimit} limit reached`);
|
|
655
|
-
if (search.totalMatched > items.length)
|
|
656
|
-
notices.push(`${search.totalMatched} total matches`);
|
|
657
|
-
|
|
658
|
-
const textContent = appendNotices(
|
|
659
|
-
items.map((item) => item.relativePath).join("\n"),
|
|
660
|
-
notices,
|
|
661
|
-
);
|
|
662
|
-
return makeTextResult<FindResultDetails>(textContent, {
|
|
663
|
-
_type: "findResult",
|
|
664
|
-
text: textContent,
|
|
665
|
-
pattern: params.pattern,
|
|
666
|
-
matchCount: items.length,
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
} catch {
|
|
670
|
-
/* fall through to SDK */
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// SDK fallback
|
|
675
|
-
const result = await origFind.execute(
|
|
676
|
-
tid,
|
|
677
|
-
params,
|
|
678
|
-
sig,
|
|
679
|
-
upd as never,
|
|
680
|
-
ctx,
|
|
681
|
-
);
|
|
682
|
-
const textContent = getTextContent(result);
|
|
683
|
-
const matchCount = textContent
|
|
684
|
-
? textContent.trim().split("\n").filter(Boolean).length
|
|
685
|
-
: 0;
|
|
686
|
-
|
|
687
|
-
setResultDetails<FindResultDetails>(result, {
|
|
688
|
-
_type: "findResult",
|
|
689
|
-
text: textContent,
|
|
690
|
-
pattern: params.pattern,
|
|
691
|
-
matchCount,
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
return result;
|
|
695
|
-
},
|
|
696
|
-
|
|
697
|
-
renderCall(args: FindParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
698
|
-
resolveBaseBackground(theme);
|
|
699
|
-
const pattern = args.pattern ?? "";
|
|
700
|
-
const path = args.path
|
|
701
|
-
? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
|
|
702
|
-
: "";
|
|
703
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
704
|
-
text.setText(
|
|
705
|
-
fillToolBackground(
|
|
706
|
-
`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`,
|
|
707
|
-
),
|
|
708
|
-
);
|
|
709
|
-
return text;
|
|
710
|
-
},
|
|
711
|
-
|
|
712
|
-
renderResult(
|
|
713
|
-
result: ToolResultLike<FindResultDetails>,
|
|
714
|
-
_opt: ToolRenderResultOptions,
|
|
715
|
-
theme: ThemeLike,
|
|
716
|
-
ctx: RenderContextLike,
|
|
717
|
-
) {
|
|
718
|
-
resolveBaseBackground(theme);
|
|
719
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
720
|
-
|
|
721
|
-
if (ctx.isError) {
|
|
722
|
-
text.setText(
|
|
723
|
-
renderToolError(getTextContent(result) || "Error", theme),
|
|
724
|
-
);
|
|
725
|
-
return text;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const d = result.details;
|
|
729
|
-
if (d?._type === "findResult" && d.text) {
|
|
730
|
-
const rendered = renderFindResults(d.text);
|
|
731
|
-
const info = `${FG_DIM}${d.matchCount} files${RST}`;
|
|
732
|
-
text.setText(fillToolBackground(` ${info}\n${rendered}`));
|
|
733
|
-
return text;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
const fallback = result.content?.[0];
|
|
737
|
-
const fallbackText =
|
|
738
|
-
fallback && isTextContent(fallback) ? fallback.text : "found";
|
|
739
|
-
text.setText(
|
|
740
|
-
fillToolBackground(
|
|
741
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
742
|
-
),
|
|
743
|
-
);
|
|
744
|
-
return text;
|
|
745
|
-
},
|
|
746
|
-
});
|
|
241
|
+
if (isToolEnabled("find") && createFindTool) {
|
|
242
|
+
registerFindTool(pi, createFindTool, toolCtx);
|
|
747
243
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
// grep — highlighted matches with line numbers
|
|
751
|
-
// ===================================================================
|
|
752
|
-
|
|
753
|
-
if (createGrepTool) {
|
|
754
|
-
const origGrep = createGrepTool(cwd);
|
|
755
|
-
|
|
756
|
-
pi.registerTool({
|
|
757
|
-
...origGrep,
|
|
758
|
-
name: "grep",
|
|
759
|
-
|
|
760
|
-
async execute(
|
|
761
|
-
tid: string,
|
|
762
|
-
params: GrepParams,
|
|
763
|
-
sig: AbortSignal | undefined,
|
|
764
|
-
upd: unknown,
|
|
765
|
-
ctx: ExtensionContext,
|
|
766
|
-
) {
|
|
767
|
-
// Try FFF first (SIMD-accelerated, frecency-ranked).
|
|
768
|
-
// FFF 0.5.2 can abort the process when path/glob constraints meet
|
|
769
|
-
// Unicode filenames, so constrained searches use the SDK fallback.
|
|
770
|
-
if (
|
|
771
|
-
fffState.finder &&
|
|
772
|
-
!fffState.finder.isDestroyed &&
|
|
773
|
-
!params.path &&
|
|
774
|
-
!params.glob
|
|
775
|
-
) {
|
|
776
|
-
try {
|
|
777
|
-
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
778
|
-
const query = params.pattern;
|
|
779
|
-
|
|
780
|
-
const grepResult = fffState.finder.grep(query, {
|
|
781
|
-
mode: params.literal ? "plain" : "regex",
|
|
782
|
-
smartCase: !params.ignoreCase,
|
|
783
|
-
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
784
|
-
cursor: null,
|
|
785
|
-
beforeContext: params.context ?? 0,
|
|
786
|
-
afterContext: params.context ?? 0,
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
if (grepResult.ok) {
|
|
790
|
-
const grep: GrepResult = grepResult.value;
|
|
791
|
-
const notices: string[] = [];
|
|
792
|
-
if (fffState.partialIndex)
|
|
793
|
-
notices.push("Warning: partial file index");
|
|
794
|
-
if (grep.items.length >= effectiveLimit)
|
|
795
|
-
notices.push(`${effectiveLimit} limit reached`);
|
|
796
|
-
if (grep.regexFallbackError)
|
|
797
|
-
notices.push(
|
|
798
|
-
`Regex failed: ${grep.regexFallbackError}, used literal match`,
|
|
799
|
-
);
|
|
800
|
-
if (grep.nextCursor) {
|
|
801
|
-
const cursorId = _cursorStore.store(grep.nextCursor);
|
|
802
|
-
notices.push(
|
|
803
|
-
`More results available. Use cursor="${cursorId}" to continue`,
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
const textContent = appendNotices(
|
|
808
|
-
fffFormatGrepText(grep.items, effectiveLimit),
|
|
809
|
-
notices,
|
|
810
|
-
);
|
|
811
|
-
return makeTextResult<GrepResultDetails>(textContent, {
|
|
812
|
-
_type: "grepResult",
|
|
813
|
-
text: textContent,
|
|
814
|
-
pattern: params.pattern,
|
|
815
|
-
matchCount: Math.min(grep.items.length, effectiveLimit),
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
} catch {
|
|
819
|
-
/* fall through to SDK */
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// SDK fallback
|
|
824
|
-
const result = await origGrep.execute(
|
|
825
|
-
tid,
|
|
826
|
-
params,
|
|
827
|
-
sig,
|
|
828
|
-
upd as never,
|
|
829
|
-
ctx,
|
|
830
|
-
);
|
|
831
|
-
const textContent = normalizeLineEndings(getTextContent(result));
|
|
832
|
-
if (result.content) {
|
|
833
|
-
for (const content of result.content) {
|
|
834
|
-
if (isTextContent(content))
|
|
835
|
-
content.text = normalizeLineEndings(content.text || "");
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
const matchCount = textContent ? countRipgrepMatches(textContent) : 0;
|
|
839
|
-
|
|
840
|
-
setResultDetails<GrepResultDetails>(result, {
|
|
841
|
-
_type: "grepResult",
|
|
842
|
-
text: textContent,
|
|
843
|
-
pattern: params.pattern,
|
|
844
|
-
matchCount,
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
return result;
|
|
848
|
-
},
|
|
849
|
-
|
|
850
|
-
renderCall(args: GrepParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
851
|
-
resolveBaseBackground(theme);
|
|
852
|
-
const pattern = args.pattern ?? "";
|
|
853
|
-
const path = args.path
|
|
854
|
-
? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
|
|
855
|
-
: "";
|
|
856
|
-
const glob = args.glob ? ` ${theme.fg("muted", `(${args.glob})`)}` : "";
|
|
857
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
858
|
-
text.setText(
|
|
859
|
-
fillToolBackground(
|
|
860
|
-
`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`,
|
|
861
|
-
),
|
|
862
|
-
);
|
|
863
|
-
return text;
|
|
864
|
-
},
|
|
865
|
-
|
|
866
|
-
renderResult(
|
|
867
|
-
result: ToolResultLike<GrepResultDetails>,
|
|
868
|
-
_opt: ToolRenderResultOptions,
|
|
869
|
-
theme: ThemeLike,
|
|
870
|
-
ctx: RenderContextLike<GrepRenderState>,
|
|
871
|
-
) {
|
|
872
|
-
resolveBaseBackground(theme);
|
|
873
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
874
|
-
|
|
875
|
-
if (ctx.isError) {
|
|
876
|
-
text.setText(
|
|
877
|
-
renderToolError(getTextContent(result) || "Error", theme),
|
|
878
|
-
);
|
|
879
|
-
return text;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const d = result.details;
|
|
883
|
-
if (d?._type === "grepResult" && d.text) {
|
|
884
|
-
const key = `grep:${d.pattern}:${d.matchCount}:${termW()}`;
|
|
885
|
-
if (ctx.state._gk !== key) {
|
|
886
|
-
ctx.state._gk = key;
|
|
887
|
-
const info = `${FG_DIM}${d.matchCount} matches${RST}`;
|
|
888
|
-
ctx.state._gt = fillToolBackground(` ${info}`);
|
|
889
|
-
|
|
890
|
-
renderGrepResults(d.text, d.pattern)
|
|
891
|
-
.then((rendered: string) => {
|
|
892
|
-
if (ctx.state._gk !== key) return;
|
|
893
|
-
ctx.state._gt = fillToolBackground(` ${info}\n${rendered}`);
|
|
894
|
-
ctx.invalidate();
|
|
895
|
-
})
|
|
896
|
-
.catch(() => {});
|
|
897
|
-
}
|
|
898
|
-
text.setText(
|
|
899
|
-
ctx.state._gt ??
|
|
900
|
-
fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}`),
|
|
901
|
-
);
|
|
902
|
-
return text;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const fallback = result.content?.[0];
|
|
906
|
-
const fallbackText =
|
|
907
|
-
fallback && isTextContent(fallback) ? fallback.text : "searched";
|
|
908
|
-
text.setText(
|
|
909
|
-
fillToolBackground(
|
|
910
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
911
|
-
),
|
|
912
|
-
);
|
|
913
|
-
return text;
|
|
914
|
-
},
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// ===================================================================
|
|
919
|
-
// multi_grep — OR-logic multi-pattern search (FFF when available,
|
|
920
|
-
// SDK grep fallback otherwise)
|
|
921
|
-
// ===================================================================
|
|
922
|
-
|
|
923
|
-
if (fffState.module || createGrepTool) {
|
|
924
|
-
const multiGrepFallback = createGrepTool ? createGrepTool(cwd) : null;
|
|
925
|
-
|
|
926
|
-
pi.registerTool({
|
|
927
|
-
name: "multi_grep",
|
|
928
|
-
label: "multi_grep",
|
|
929
|
-
description: [
|
|
930
|
-
"Search file contents for lines matching ANY of multiple patterns (OR logic).",
|
|
931
|
-
"Uses SIMD-accelerated Aho-Corasick multi-pattern matching when FFF is available.",
|
|
932
|
-
"Falls back to ripgrep while preserving literal OR semantics and file constraints when needed.",
|
|
933
|
-
"Patterns are literal text — never escape special characters.",
|
|
934
|
-
"Use path to scope a directory/file and constraints for file filtering ('*.rs', 'src/', '!test/').",
|
|
935
|
-
].join(" "),
|
|
936
|
-
promptSnippet:
|
|
937
|
-
"Multi-pattern OR search across file contents (FFF-accelerated with grep fallback)",
|
|
938
|
-
promptGuidelines: [
|
|
939
|
-
"Use multi_grep when you need to find multiple identifiers at once (OR logic).",
|
|
940
|
-
"Include all naming conventions: snake_case, PascalCase, camelCase variants.",
|
|
941
|
-
"Patterns are literal text. Never escape special characters.",
|
|
942
|
-
"Use path to scope a directory or file when you need fresh on-disk results.",
|
|
943
|
-
"Use the constraints parameter for additional file filtering, not inside patterns.",
|
|
944
|
-
],
|
|
945
|
-
|
|
946
|
-
parameters: {
|
|
947
|
-
type: "object",
|
|
948
|
-
properties: {
|
|
949
|
-
patterns: {
|
|
950
|
-
type: "array",
|
|
951
|
-
items: { type: "string" },
|
|
952
|
-
description:
|
|
953
|
-
"Patterns to search for (OR logic — matches lines containing ANY pattern).",
|
|
954
|
-
},
|
|
955
|
-
path: {
|
|
956
|
-
type: "string",
|
|
957
|
-
description:
|
|
958
|
-
"Directory or file path to search (default: current directory)",
|
|
959
|
-
},
|
|
960
|
-
constraints: {
|
|
961
|
-
type: "string",
|
|
962
|
-
description:
|
|
963
|
-
"File constraints, e.g. '*.{ts,tsx} !test/' to filter files.",
|
|
964
|
-
},
|
|
965
|
-
context: {
|
|
966
|
-
type: "number",
|
|
967
|
-
description:
|
|
968
|
-
"Number of context lines before and after each match (default: 0)",
|
|
969
|
-
},
|
|
970
|
-
limit: {
|
|
971
|
-
type: "number",
|
|
972
|
-
description: "Maximum number of matches to return (default: 100)",
|
|
973
|
-
},
|
|
974
|
-
},
|
|
975
|
-
required: ["patterns"],
|
|
976
|
-
},
|
|
977
|
-
|
|
978
|
-
async execute(
|
|
979
|
-
tid: string,
|
|
980
|
-
params: MultiGrepParams,
|
|
981
|
-
sig: AbortSignal | undefined,
|
|
982
|
-
upd: unknown,
|
|
983
|
-
ctx: ExtensionContext,
|
|
984
|
-
) {
|
|
985
|
-
if (sig?.aborted) return makeTextResult("Aborted", {});
|
|
986
|
-
|
|
987
|
-
if (!params.patterns || params.patterns.length === 0) {
|
|
988
|
-
return makeTextResult(
|
|
989
|
-
"Error: patterns array must have at least 1 element",
|
|
990
|
-
{ error: "empty patterns" },
|
|
991
|
-
);
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
995
|
-
const pattern = buildLiteralAlternationPattern(params.patterns);
|
|
996
|
-
const requestedPath = trimToUndefined(params.path);
|
|
997
|
-
const requestedConstraints = trimToUndefined(params.constraints);
|
|
998
|
-
const effectivePath =
|
|
999
|
-
requestedPath ?? getConstraintBackedPath(requestedConstraints);
|
|
1000
|
-
const hasNativeConstraints = Boolean(
|
|
1001
|
-
requestedPath || requestedConstraints,
|
|
1002
|
-
);
|
|
1003
|
-
|
|
1004
|
-
if (
|
|
1005
|
-
fffState.finder &&
|
|
1006
|
-
!fffState.finder.isDestroyed &&
|
|
1007
|
-
!hasNativeConstraints
|
|
1008
|
-
) {
|
|
1009
|
-
try {
|
|
1010
|
-
const grepResult = fffState.finder.multiGrep({
|
|
1011
|
-
patterns: params.patterns,
|
|
1012
|
-
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
1013
|
-
smartCase: true,
|
|
1014
|
-
cursor: null,
|
|
1015
|
-
beforeContext: params.context ?? 0,
|
|
1016
|
-
afterContext: params.context ?? 0,
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
if (!grepResult.ok) {
|
|
1020
|
-
return makeTextResult(`multi_grep error: ${grepResult.error}`, {
|
|
1021
|
-
error: grepResult.error,
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
const grep: GrepResult = grepResult.value;
|
|
1026
|
-
const notices: string[] = [];
|
|
1027
|
-
if (fffState.partialIndex)
|
|
1028
|
-
notices.push("Warning: partial file index");
|
|
1029
|
-
if (grep.items.length >= effectiveLimit)
|
|
1030
|
-
notices.push(`${effectiveLimit} limit reached`);
|
|
1031
|
-
if (grep.nextCursor) {
|
|
1032
|
-
const cursorId = _cursorStore.store(grep.nextCursor);
|
|
1033
|
-
notices.push(`More results: cursor="${cursorId}"`);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
const textContent = appendNotices(
|
|
1037
|
-
fffFormatGrepText(grep.items, effectiveLimit),
|
|
1038
|
-
notices,
|
|
1039
|
-
);
|
|
1040
|
-
return makeTextResult<GrepResultDetails>(textContent, {
|
|
1041
|
-
_type: "grepResult",
|
|
1042
|
-
text: textContent,
|
|
1043
|
-
pattern,
|
|
1044
|
-
matchCount: Math.min(grep.items.length, effectiveLimit),
|
|
1045
|
-
});
|
|
1046
|
-
} catch {
|
|
1047
|
-
/* fall through to SDK */
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (requestedConstraints || !multiGrepFallback) {
|
|
1052
|
-
try {
|
|
1053
|
-
const pathBackedConstraint = Boolean(
|
|
1054
|
-
requestedConstraints &&
|
|
1055
|
-
!requestedPath &&
|
|
1056
|
-
requestedConstraints === effectivePath,
|
|
1057
|
-
);
|
|
1058
|
-
const constraintsForRipgrep = pathBackedConstraint
|
|
1059
|
-
? undefined
|
|
1060
|
-
: requestedConstraints;
|
|
1061
|
-
const notices: string[] = [];
|
|
1062
|
-
|
|
1063
|
-
if (!fffState.finder || fffState.finder.isDestroyed)
|
|
1064
|
-
notices.push("FFF unavailable, used ripgrep fallback");
|
|
1065
|
-
else if (hasNativeConstraints)
|
|
1066
|
-
notices.push("Used ripgrep fallback for constrained search");
|
|
1067
|
-
else notices.push("Used ripgrep fallback");
|
|
1068
|
-
|
|
1069
|
-
const rgResult = await multiGrepRipgrepFallback({
|
|
1070
|
-
cwd,
|
|
1071
|
-
patterns: params.patterns,
|
|
1072
|
-
path: effectivePath,
|
|
1073
|
-
constraints: constraintsForRipgrep,
|
|
1074
|
-
ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
|
|
1075
|
-
context: params.context,
|
|
1076
|
-
limit: effectiveLimit,
|
|
1077
|
-
signal: sig,
|
|
1078
|
-
});
|
|
1079
|
-
const textContent =
|
|
1080
|
-
normalizeLineEndings(rgResult.text) || "No matches found";
|
|
1081
|
-
if (rgResult.limitReached)
|
|
1082
|
-
notices.push(`${effectiveLimit} limit reached`);
|
|
1083
|
-
const finalText = appendNotices(textContent, notices);
|
|
1084
|
-
|
|
1085
|
-
return makeTextResult<GrepResultDetails>(finalText, {
|
|
1086
|
-
_type: "grepResult",
|
|
1087
|
-
text: finalText,
|
|
1088
|
-
pattern,
|
|
1089
|
-
matchCount: rgResult.matchCount,
|
|
1090
|
-
});
|
|
1091
|
-
} catch (error: unknown) {
|
|
1092
|
-
const message = getErrorMessage(error);
|
|
1093
|
-
return makeTextResult(`multi_grep error: ${message}`, {
|
|
1094
|
-
error: message,
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
try {
|
|
1100
|
-
const notices: string[] = [];
|
|
1101
|
-
if (!fffState.finder || fffState.finder.isDestroyed)
|
|
1102
|
-
notices.push("FFF unavailable, used SDK grep fallback");
|
|
1103
|
-
|
|
1104
|
-
const result = await multiGrepFallback.execute(
|
|
1105
|
-
tid,
|
|
1106
|
-
{
|
|
1107
|
-
pattern,
|
|
1108
|
-
path: effectivePath,
|
|
1109
|
-
ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
|
|
1110
|
-
context: params.context,
|
|
1111
|
-
limit: params.limit,
|
|
1112
|
-
},
|
|
1113
|
-
sig,
|
|
1114
|
-
upd as never,
|
|
1115
|
-
ctx,
|
|
1116
|
-
);
|
|
1117
|
-
const textContent =
|
|
1118
|
-
normalizeLineEndings(getTextContent(result)) || "No matches found";
|
|
1119
|
-
const finalText = appendNotices(textContent, notices);
|
|
1120
|
-
|
|
1121
|
-
return makeTextResult<GrepResultDetails>(finalText, {
|
|
1122
|
-
_type: "grepResult",
|
|
1123
|
-
text: finalText,
|
|
1124
|
-
pattern,
|
|
1125
|
-
matchCount: textContent ? countRipgrepMatches(textContent) : 0,
|
|
1126
|
-
});
|
|
1127
|
-
} catch (error: unknown) {
|
|
1128
|
-
const message = getErrorMessage(error);
|
|
1129
|
-
return makeTextResult(`multi_grep error: ${message}`, {
|
|
1130
|
-
error: message,
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
},
|
|
1134
|
-
|
|
1135
|
-
renderCall(
|
|
1136
|
-
args: MultiGrepParams,
|
|
1137
|
-
theme: ThemeLike,
|
|
1138
|
-
ctx: RenderContextLike,
|
|
1139
|
-
) {
|
|
1140
|
-
resolveBaseBackground(theme);
|
|
1141
|
-
const patterns = args.patterns ?? [];
|
|
1142
|
-
const path = args.path
|
|
1143
|
-
? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
|
|
1144
|
-
: "";
|
|
1145
|
-
const constraints = args.constraints;
|
|
1146
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1147
|
-
let content =
|
|
1148
|
-
theme.fg("toolTitle", theme.bold("multi_grep")) +
|
|
1149
|
-
" " +
|
|
1150
|
-
theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "));
|
|
1151
|
-
content += path;
|
|
1152
|
-
if (constraints) content += theme.fg("muted", ` (${constraints})`);
|
|
1153
|
-
text.setText(fillToolBackground(content));
|
|
1154
|
-
return text;
|
|
1155
|
-
},
|
|
1156
|
-
|
|
1157
|
-
renderResult(
|
|
1158
|
-
result: ToolResultLike<GrepResultDetails | { error?: string }>,
|
|
1159
|
-
_opt: ToolRenderResultOptions,
|
|
1160
|
-
theme: ThemeLike,
|
|
1161
|
-
ctx: RenderContextLike<MultiGrepRenderState>,
|
|
1162
|
-
) {
|
|
1163
|
-
resolveBaseBackground(theme);
|
|
1164
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1165
|
-
|
|
1166
|
-
if (ctx.isError) {
|
|
1167
|
-
text.setText(
|
|
1168
|
-
`\n${theme.fg("error", getTextContent(result) || "Error")}`,
|
|
1169
|
-
);
|
|
1170
|
-
return text;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
const d = result.details;
|
|
1174
|
-
if (d && "_type" in d && d._type === "grepResult" && d.text) {
|
|
1175
|
-
const key = `mgrep:${d.pattern}:${d.matchCount}:${termW()}`;
|
|
1176
|
-
if (ctx.state._mgk !== key) {
|
|
1177
|
-
ctx.state._mgk = key;
|
|
1178
|
-
const info = `${FG_DIM}${d.matchCount} matches${RST}`;
|
|
1179
|
-
ctx.state._mgt = ` ${info}`;
|
|
1180
|
-
|
|
1181
|
-
renderGrepResults(d.text, d.pattern)
|
|
1182
|
-
.then((rendered: string) => {
|
|
1183
|
-
if (ctx.state._mgk !== key) return;
|
|
1184
|
-
ctx.state._mgt = ` ${info}\n${rendered}`;
|
|
1185
|
-
ctx.invalidate();
|
|
1186
|
-
})
|
|
1187
|
-
.catch(() => {});
|
|
1188
|
-
}
|
|
1189
|
-
text.setText(
|
|
1190
|
-
ctx.state._mgt ?? ` ${FG_DIM}${d.matchCount} matches${RST}`,
|
|
1191
|
-
);
|
|
1192
|
-
return text;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const fallback = result.content?.[0];
|
|
1196
|
-
const fallbackText =
|
|
1197
|
-
fallback && isTextContent(fallback) ? fallback.text : "searched";
|
|
1198
|
-
text.setText(
|
|
1199
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
1200
|
-
);
|
|
1201
|
-
return text;
|
|
1202
|
-
},
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
// ===================================================================
|
|
1207
|
-
// edit — split/unified/word-level diff preview
|
|
1208
|
-
// ===================================================================
|
|
1209
|
-
|
|
1210
|
-
function getEditOperations(input: EditParams): EditOperation[] {
|
|
1211
|
-
if (Array.isArray(input?.edits)) {
|
|
1212
|
-
return input.edits
|
|
1213
|
-
.map((e) => ({
|
|
1214
|
-
oldText:
|
|
1215
|
-
typeof e?.oldText === "string"
|
|
1216
|
-
? e.oldText
|
|
1217
|
-
: typeof e?.old_text === "string"
|
|
1218
|
-
? e.old_text
|
|
1219
|
-
: "",
|
|
1220
|
-
newText:
|
|
1221
|
-
typeof e?.newText === "string"
|
|
1222
|
-
? e.newText
|
|
1223
|
-
: typeof e?.new_text === "string"
|
|
1224
|
-
? e.new_text
|
|
1225
|
-
: "",
|
|
1226
|
-
}))
|
|
1227
|
-
.filter((e) => e.oldText && e.oldText !== e.newText);
|
|
1228
|
-
}
|
|
1229
|
-
const oldText =
|
|
1230
|
-
typeof input?.oldText === "string"
|
|
1231
|
-
? input.oldText
|
|
1232
|
-
: typeof input?.old_text === "string"
|
|
1233
|
-
? input.old_text
|
|
1234
|
-
: "";
|
|
1235
|
-
const newText =
|
|
1236
|
-
typeof input?.newText === "string"
|
|
1237
|
-
? input.newText
|
|
1238
|
-
: typeof input?.new_text === "string"
|
|
1239
|
-
? input.new_text
|
|
1240
|
-
: "";
|
|
1241
|
-
return oldText && oldText !== newText ? [{ oldText, newText }] : [];
|
|
244
|
+
if (isToolEnabled("grep") && createGrepTool) {
|
|
245
|
+
registerGrepTool(pi, createGrepTool, toolCtx);
|
|
1242
246
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
const diffs = operations.map((e) => parseDiff(e.oldText, e.newText));
|
|
1246
|
-
const totalAdded = diffs.reduce((sum, d) => sum + d.added, 0);
|
|
1247
|
-
const totalRemoved = diffs.reduce((sum, d) => sum + d.removed, 0);
|
|
1248
|
-
return {
|
|
1249
|
-
diffs,
|
|
1250
|
-
totalAdded,
|
|
1251
|
-
totalRemoved,
|
|
1252
|
-
summary: summarize(totalAdded, totalRemoved),
|
|
1253
|
-
};
|
|
247
|
+
if (isToolEnabled("multi_grep") && (fffState.module || createGrepTool)) {
|
|
248
|
+
registerMultiGrepTool(pi, createGrepTool ?? null, toolCtx);
|
|
1254
249
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
const origEdit = createEditTool(cwd);
|
|
1258
|
-
|
|
1259
|
-
pi.registerTool({
|
|
1260
|
-
...origEdit,
|
|
1261
|
-
name: "edit",
|
|
1262
|
-
|
|
1263
|
-
async execute(
|
|
1264
|
-
tid: string,
|
|
1265
|
-
params: EditParams,
|
|
1266
|
-
sig: AbortSignal | undefined,
|
|
1267
|
-
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
1268
|
-
ctx: ExtensionContext,
|
|
1269
|
-
) {
|
|
1270
|
-
const fp = params.path ?? params.file_path ?? "";
|
|
1271
|
-
const operations = getEditOperations(params);
|
|
1272
|
-
// params is the live tool input (upstream EditToolInput shape); we
|
|
1273
|
-
// type it loosely as EditParams for defensive legacy-field reads, so
|
|
1274
|
-
// cast back to the upstream input when delegating to the real tool.
|
|
1275
|
-
const result = (await origEdit.execute(
|
|
1276
|
-
tid,
|
|
1277
|
-
params as unknown as Parameters<typeof origEdit.execute>[1],
|
|
1278
|
-
sig,
|
|
1279
|
-
upd,
|
|
1280
|
-
ctx,
|
|
1281
|
-
)) as ToolResultLike;
|
|
1282
|
-
|
|
1283
|
-
if (operations.length === 0) return result;
|
|
1284
|
-
|
|
1285
|
-
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1286
|
-
if (operations.length === 1) {
|
|
1287
|
-
let editLine = 0;
|
|
1288
|
-
try {
|
|
1289
|
-
if (fp && existsSync(fp)) {
|
|
1290
|
-
const f = readFileSync(fp, "utf-8");
|
|
1291
|
-
const idx = f.indexOf(operations[0].newText);
|
|
1292
|
-
if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
|
|
1293
|
-
}
|
|
1294
|
-
} catch {
|
|
1295
|
-
editLine = 0;
|
|
1296
|
-
}
|
|
1297
|
-
setResultDetails(result, {
|
|
1298
|
-
_type: "editInfo",
|
|
1299
|
-
summary,
|
|
1300
|
-
editLine,
|
|
1301
|
-
});
|
|
1302
|
-
return result;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
setResultDetails(result, {
|
|
1306
|
-
_type: "multiEditInfo",
|
|
1307
|
-
summary,
|
|
1308
|
-
editCount: operations.length,
|
|
1309
|
-
diffLineCount: diffs.reduce((sum, d) => sum + d.lines.length, 0),
|
|
1310
|
-
});
|
|
1311
|
-
return result;
|
|
1312
|
-
},
|
|
1313
|
-
|
|
1314
|
-
renderCall(
|
|
1315
|
-
args: EditParams,
|
|
1316
|
-
theme: ThemeLike,
|
|
1317
|
-
ctx: RenderContextLike<EditRenderState>,
|
|
1318
|
-
) {
|
|
1319
|
-
resolveBaseBackground(theme);
|
|
1320
|
-
const fp = args?.path ?? args?.file_path ?? "";
|
|
1321
|
-
const operations = getEditOperations(args);
|
|
1322
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1323
|
-
const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
|
|
1324
|
-
|
|
1325
|
-
if (operations.length === 0) {
|
|
1326
|
-
text.setText(fillToolBackground(hdr));
|
|
1327
|
-
return text;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
const { summary } = summarizeEditOperations(operations);
|
|
1331
|
-
const suffix =
|
|
1332
|
-
operations.length === 1
|
|
1333
|
-
? summary
|
|
1334
|
-
: `${operations.length} edits ${summary}`;
|
|
1335
|
-
text.setText(
|
|
1336
|
-
fillToolBackground(`${hdr} ${theme.fg("muted", suffix)}`),
|
|
1337
|
-
);
|
|
1338
|
-
return text;
|
|
1339
|
-
},
|
|
1340
|
-
|
|
1341
|
-
renderResult(
|
|
1342
|
-
result: ToolResultLike,
|
|
1343
|
-
_opt: ToolRenderResultOptions,
|
|
1344
|
-
theme: ThemeLike,
|
|
1345
|
-
ctx: RenderContextLike<EditRenderState>,
|
|
1346
|
-
) {
|
|
1347
|
-
resolveBaseBackground(theme);
|
|
1348
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1349
|
-
if (ctx.isError) {
|
|
1350
|
-
text.setText(
|
|
1351
|
-
renderToolError(getTextContent(result) || "Error", theme),
|
|
1352
|
-
);
|
|
1353
|
-
return text;
|
|
1354
|
-
}
|
|
1355
|
-
const d = result.details as RenderDetails | undefined;
|
|
1356
|
-
if (d?._type === "editInfo") {
|
|
1357
|
-
const loc =
|
|
1358
|
-
d.editLine > 0
|
|
1359
|
-
? ` ${theme.fg("muted", `at line ${d.editLine}`)}`
|
|
1360
|
-
: "";
|
|
1361
|
-
text.setText(fillToolBackground(` ${d.summary}${loc}`));
|
|
1362
|
-
return text;
|
|
1363
|
-
}
|
|
1364
|
-
if (d?._type === "multiEditInfo") {
|
|
1365
|
-
const extra =
|
|
1366
|
-
typeof d.diffLineCount === "number"
|
|
1367
|
-
? ` ${theme.fg("muted", `(${d.diffLineCount} diff lines)`)}`
|
|
1368
|
-
: "";
|
|
1369
|
-
text.setText(
|
|
1370
|
-
fillToolBackground(` ${d.editCount} edits ${d.summary}${extra}`),
|
|
1371
|
-
);
|
|
1372
|
-
return text;
|
|
1373
|
-
}
|
|
1374
|
-
const fallback = result.content?.[0];
|
|
1375
|
-
const fallbackText =
|
|
1376
|
-
fallback && isTextContent(fallback) ? fallback.text : "edited";
|
|
1377
|
-
text.setText(
|
|
1378
|
-
fillToolBackground(
|
|
1379
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
1380
|
-
),
|
|
1381
|
-
);
|
|
1382
|
-
return text;
|
|
1383
|
-
},
|
|
1384
|
-
});
|
|
250
|
+
if (isToolEnabled("edit") && createEditTool) {
|
|
251
|
+
registerEditTool(pi, createEditTool, toolCtx, trackInvalidator);
|
|
1385
252
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
// write — new-file preview + overwrite diff
|
|
1389
|
-
// ===================================================================
|
|
1390
|
-
|
|
1391
|
-
if (createWriteTool) {
|
|
1392
|
-
const origWrite = createWriteTool(cwd);
|
|
1393
|
-
|
|
1394
|
-
pi.registerTool({
|
|
1395
|
-
...origWrite,
|
|
1396
|
-
name: "write",
|
|
1397
|
-
|
|
1398
|
-
async execute(
|
|
1399
|
-
tid: string,
|
|
1400
|
-
params: WriteParams,
|
|
1401
|
-
sig: AbortSignal | undefined,
|
|
1402
|
-
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
1403
|
-
ctx: ExtensionContext,
|
|
1404
|
-
) {
|
|
1405
|
-
const fp = params.path ?? params.file_path ?? "";
|
|
1406
|
-
let old: string | null = null;
|
|
1407
|
-
try {
|
|
1408
|
-
if (fp && existsSync(fp)) old = readFileSync(fp, "utf-8");
|
|
1409
|
-
} catch {
|
|
1410
|
-
old = null;
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
const result = (await origWrite.execute(
|
|
1414
|
-
tid,
|
|
1415
|
-
params as unknown as Parameters<typeof origWrite.execute>[1],
|
|
1416
|
-
sig,
|
|
1417
|
-
upd,
|
|
1418
|
-
ctx,
|
|
1419
|
-
)) as ToolResultLike;
|
|
1420
|
-
const content = params.content ?? "";
|
|
1421
|
-
|
|
1422
|
-
if (old !== null && old !== content) {
|
|
1423
|
-
const diff = parseDiff(old, content);
|
|
1424
|
-
setResultDetails(result, {
|
|
1425
|
-
_type: "diff",
|
|
1426
|
-
summary: summarize(diff.added, diff.removed),
|
|
1427
|
-
oldContent: old,
|
|
1428
|
-
newContent: content,
|
|
1429
|
-
language: lang(fp),
|
|
1430
|
-
});
|
|
1431
|
-
} else if (old === null) {
|
|
1432
|
-
setResultDetails(result, {
|
|
1433
|
-
_type: "new",
|
|
1434
|
-
lines: content ? content.split("\n").length : 0,
|
|
1435
|
-
content,
|
|
1436
|
-
filePath: fp,
|
|
1437
|
-
});
|
|
1438
|
-
} else {
|
|
1439
|
-
setResultDetails(result, { _type: "noChange" });
|
|
1440
|
-
}
|
|
1441
|
-
return result;
|
|
1442
|
-
},
|
|
1443
|
-
|
|
1444
|
-
renderCall(
|
|
1445
|
-
args: WriteParams,
|
|
1446
|
-
theme: ThemeLike,
|
|
1447
|
-
ctx: RenderContextLike<WriteRenderState>,
|
|
1448
|
-
) {
|
|
1449
|
-
resolveBaseBackground(theme);
|
|
1450
|
-
const fp = args?.path ?? args?.file_path ?? "";
|
|
1451
|
-
const isNew = !fp || !existsSync(fp);
|
|
1452
|
-
const label = isNew ? "create" : "write";
|
|
1453
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1454
|
-
const hdr = `${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("accent", sp(fp))}`;
|
|
1455
|
-
|
|
1456
|
-
if (args?.content && isNew) {
|
|
1457
|
-
const previewKey = `create:${diffThemeCacheKey(theme)}:${fp}:${String(args.content).length}`;
|
|
1458
|
-
if (ctx.state._previewKey !== previewKey) {
|
|
1459
|
-
ctx.state._previewKey = previewKey;
|
|
1460
|
-
ctx.state._previewText = hdr;
|
|
1461
|
-
const lg = lang(fp);
|
|
1462
|
-
hlBlock(String(args.content), lg)
|
|
1463
|
-
.then((lines) => {
|
|
1464
|
-
if (ctx.state._previewKey !== previewKey) return;
|
|
1465
|
-
const maxShow = ctx.expanded ? lines.length : 16;
|
|
1466
|
-
const preview = lines.slice(0, maxShow).join("\n");
|
|
1467
|
-
const rem = lines.length - maxShow;
|
|
1468
|
-
let out = `${hdr}\n\n${preview}`;
|
|
1469
|
-
if (rem > 0)
|
|
1470
|
-
out += `\n${theme.fg("muted", `… (${rem} more lines, ${lines.length} total)`)}`;
|
|
1471
|
-
ctx.state._previewText = out;
|
|
1472
|
-
ctx.invalidate();
|
|
1473
|
-
})
|
|
1474
|
-
.catch(() => {});
|
|
1475
|
-
}
|
|
1476
|
-
text.setText(ctx.state._previewText ?? hdr);
|
|
1477
|
-
return text;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
text.setText(fillToolBackground(hdr));
|
|
1481
|
-
return text;
|
|
1482
|
-
},
|
|
1483
|
-
|
|
1484
|
-
renderResult(
|
|
1485
|
-
result: ToolResultLike,
|
|
1486
|
-
_opt: ToolRenderResultOptions,
|
|
1487
|
-
theme: ThemeLike,
|
|
1488
|
-
ctx: RenderContextLike<WriteRenderState>,
|
|
1489
|
-
) {
|
|
1490
|
-
resolveBaseBackground(theme);
|
|
1491
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1492
|
-
if (ctx.isError) {
|
|
1493
|
-
text.setText(
|
|
1494
|
-
renderToolError(getTextContent(result) || "Error", theme),
|
|
1495
|
-
);
|
|
1496
|
-
return text;
|
|
1497
|
-
}
|
|
1498
|
-
const d = result.details as RenderDetails | undefined;
|
|
1499
|
-
|
|
1500
|
-
if (d?._type === "diff") {
|
|
1501
|
-
const key = `wd:${diffThemeCacheKey(theme)}:${termW()}:${d.summary}:${d.newContent.length}:${d.language ?? ""}`;
|
|
1502
|
-
if (ctx.state._wdk !== key) {
|
|
1503
|
-
ctx.state._wdk = key;
|
|
1504
|
-
ctx.state._wdt = ` ${d.summary}\n${theme.fg("muted", " rendering diff…")}`;
|
|
1505
|
-
const dc = resolveDiffColors(theme);
|
|
1506
|
-
const diff = parseDiff(d.oldContent, d.newContent);
|
|
1507
|
-
renderSplit(diff, d.language, MAX_RENDER_LINES, dc)
|
|
1508
|
-
.then((rendered) => {
|
|
1509
|
-
if (ctx.state._wdk !== key) return;
|
|
1510
|
-
ctx.state._wdt = ` ${d.summary}\n${rendered}`;
|
|
1511
|
-
ctx.invalidate();
|
|
1512
|
-
})
|
|
1513
|
-
.catch(() => {
|
|
1514
|
-
if (ctx.state._wdk !== key) return;
|
|
1515
|
-
ctx.state._wdt = ` ${d.summary}`;
|
|
1516
|
-
ctx.invalidate();
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
text.setText(ctx.state._wdt ?? ` ${d.summary}`);
|
|
1520
|
-
return text;
|
|
1521
|
-
}
|
|
1522
|
-
if (d?._type === "noChange") {
|
|
1523
|
-
text.setText(
|
|
1524
|
-
fillToolBackground(` ${theme.fg("muted", "✓ no changes")}`),
|
|
1525
|
-
);
|
|
1526
|
-
return text;
|
|
1527
|
-
}
|
|
1528
|
-
if (d?._type === "new") {
|
|
1529
|
-
const { lines: lineCount, content: rawContent, filePath: fp } = d;
|
|
1530
|
-
const base = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`;
|
|
1531
|
-
const pk = `nf:${diffThemeCacheKey(theme)}:${fp}:${lineCount}`;
|
|
1532
|
-
if (ctx.state._nfk !== pk) {
|
|
1533
|
-
ctx.state._nfk = pk;
|
|
1534
|
-
ctx.state._nft = base;
|
|
1535
|
-
if (rawContent) {
|
|
1536
|
-
hlBlock(rawContent, lang(fp))
|
|
1537
|
-
.then((hlLines) => {
|
|
1538
|
-
if (ctx.state._nfk !== pk) return;
|
|
1539
|
-
const maxShow = ctx.expanded ? hlLines.length : 12;
|
|
1540
|
-
const preview = hlLines.slice(0, maxShow).join("\n");
|
|
1541
|
-
const rem = hlLines.length - maxShow;
|
|
1542
|
-
let out = `${base}\n${preview}`;
|
|
1543
|
-
if (rem > 0)
|
|
1544
|
-
out += `\n${theme.fg("muted", ` … ${rem} more lines`)}`;
|
|
1545
|
-
ctx.state._nft = out;
|
|
1546
|
-
ctx.invalidate();
|
|
1547
|
-
})
|
|
1548
|
-
.catch(() => {});
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
text.setText(ctx.state._nft ?? base);
|
|
1552
|
-
return text;
|
|
1553
|
-
}
|
|
1554
|
-
const fallback = result.content?.[0];
|
|
1555
|
-
const fallbackText =
|
|
1556
|
-
fallback && isTextContent(fallback) ? fallback.text : "written";
|
|
1557
|
-
text.setText(
|
|
1558
|
-
fillToolBackground(
|
|
1559
|
-
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
1560
|
-
),
|
|
1561
|
-
);
|
|
1562
|
-
return text;
|
|
1563
|
-
},
|
|
1564
|
-
});
|
|
253
|
+
if (isToolEnabled("write") && createWriteTool) {
|
|
254
|
+
registerWriteTool(pi, createWriteTool, toolCtx, trackInvalidator);
|
|
1565
255
|
}
|
|
1566
256
|
|
|
1567
|
-
//
|
|
1568
|
-
// FFF commands
|
|
1569
|
-
// ===================================================================
|
|
257
|
+
// ── Register FFF commands ───────────────────────────────────────────
|
|
1570
258
|
|
|
1571
259
|
if (fffState.module) {
|
|
1572
|
-
pi
|
|
1573
|
-
description: "Show FFF file finder health and indexer status",
|
|
1574
|
-
handler: async (_args: string, ctx: CommandContextLike) => {
|
|
1575
|
-
if (!fffState.finder || fffState.finder.isDestroyed) {
|
|
1576
|
-
ctx.ui?.notify?.("FFF not initialized", "warning");
|
|
1577
|
-
return;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
const health = fffState.finder.healthCheck();
|
|
1581
|
-
if (!health.ok) {
|
|
1582
|
-
ctx.ui?.notify?.(`Health check failed: ${health.error}`, "error");
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
const h = health.value;
|
|
1587
|
-
const lines = [
|
|
1588
|
-
`FFF v${h.version}`,
|
|
1589
|
-
`Git: ${h.git.repositoryFound ? `yes (${h.git.workdir ?? "unknown"})` : "no"}`,
|
|
1590
|
-
`Picker: ${h.filePicker.initialized ? `${h.filePicker.indexedFiles ?? 0} files` : "not initialized"}`,
|
|
1591
|
-
`Frecency: ${h.frecency.initialized ? "active" : "disabled"}`,
|
|
1592
|
-
`Query tracker: ${h.queryTracker.initialized ? "active" : "disabled"}`,
|
|
1593
|
-
`Partial index: ${fffState.partialIndex ? "yes (scan timed out)" : "no"}`,
|
|
1594
|
-
];
|
|
1595
|
-
|
|
1596
|
-
const progress = fffState.finder.getScanProgress();
|
|
1597
|
-
if (progress.ok) {
|
|
1598
|
-
lines.push(
|
|
1599
|
-
`Scanning: ${progress.value.isScanning ? "yes" : "no"} (${progress.value.scannedFilesCount} files)`,
|
|
1600
|
-
);
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
ctx.ui?.notify?.(lines.join("\n"), "info");
|
|
1604
|
-
},
|
|
1605
|
-
});
|
|
1606
|
-
|
|
1607
|
-
pi.registerCommand("fff-rescan", {
|
|
1608
|
-
description: "Trigger FFF to rescan files",
|
|
1609
|
-
handler: async (_args: string, ctx: CommandContextLike) => {
|
|
1610
|
-
if (!fffState.finder || fffState.finder.isDestroyed) {
|
|
1611
|
-
ctx.ui?.notify?.("FFF not initialized", "warning");
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
const result = fffState.finder.scanFiles();
|
|
1616
|
-
if (!result.ok) {
|
|
1617
|
-
ctx.ui?.notify?.(`Rescan failed: ${result.error}`, "error");
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
fffState.partialIndex = false;
|
|
1622
|
-
ctx.ui?.notify?.("FFF rescan triggered", "info");
|
|
1623
|
-
},
|
|
1624
|
-
});
|
|
260
|
+
registerFffCommands(pi, fffState);
|
|
1625
261
|
}
|
|
1626
262
|
}
|