@xynogen/pix-pretty 1.1.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/thinking.test.ts +153 -169
- package/src/thinking.ts +115 -68
- 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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import type {
|
|
3
|
+
AgentToolUpdateCallback,
|
|
4
|
+
EditToolInput,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
ToolRenderResultOptions,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
import { MAX_RENDER_LINES } from "../config.js";
|
|
10
|
+
import { parseDiff } from "../diff.js";
|
|
11
|
+
import {
|
|
12
|
+
diffThemeCacheKey,
|
|
13
|
+
renderSplit,
|
|
14
|
+
resolveDiffColors,
|
|
15
|
+
summarize,
|
|
16
|
+
} from "../diff-render.js";
|
|
17
|
+
import { lang } from "../lang.js";
|
|
18
|
+
import type {
|
|
19
|
+
EditOperation,
|
|
20
|
+
EditParams,
|
|
21
|
+
EditRenderState,
|
|
22
|
+
PiPrettyApi,
|
|
23
|
+
RenderContextLike,
|
|
24
|
+
ThemeLike,
|
|
25
|
+
ToolFactory,
|
|
26
|
+
ToolResultLike,
|
|
27
|
+
} from "../types.js";
|
|
28
|
+
import {
|
|
29
|
+
fillToolBackground,
|
|
30
|
+
getTextContent,
|
|
31
|
+
isTextContent,
|
|
32
|
+
renderToolError,
|
|
33
|
+
setResultDetails,
|
|
34
|
+
termW,
|
|
35
|
+
} from "../utils.js";
|
|
36
|
+
import type { ToolContext } from "./context.js";
|
|
37
|
+
|
|
38
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export function getEditOperations(input: EditParams): EditOperation[] {
|
|
41
|
+
if (Array.isArray(input?.edits)) {
|
|
42
|
+
return input.edits
|
|
43
|
+
.map((e) => ({
|
|
44
|
+
oldText:
|
|
45
|
+
typeof e?.oldText === "string"
|
|
46
|
+
? e.oldText
|
|
47
|
+
: typeof e?.old_text === "string"
|
|
48
|
+
? e.old_text
|
|
49
|
+
: "",
|
|
50
|
+
newText:
|
|
51
|
+
typeof e?.newText === "string"
|
|
52
|
+
? e.newText
|
|
53
|
+
: typeof e?.new_text === "string"
|
|
54
|
+
? e.new_text
|
|
55
|
+
: "",
|
|
56
|
+
}))
|
|
57
|
+
.filter((e) => e.oldText && e.oldText !== e.newText);
|
|
58
|
+
}
|
|
59
|
+
const oldText =
|
|
60
|
+
typeof input?.oldText === "string"
|
|
61
|
+
? input.oldText
|
|
62
|
+
: typeof input?.old_text === "string"
|
|
63
|
+
? input.old_text
|
|
64
|
+
: "";
|
|
65
|
+
const newText =
|
|
66
|
+
typeof input?.newText === "string"
|
|
67
|
+
? input.newText
|
|
68
|
+
: typeof input?.new_text === "string"
|
|
69
|
+
? input.new_text
|
|
70
|
+
: "";
|
|
71
|
+
return oldText && oldText !== newText ? [{ oldText, newText }] : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function summarizeEditOperations(operations: EditOperation[]) {
|
|
75
|
+
const diffs = operations.map((e) => parseDiff(e.oldText, e.newText));
|
|
76
|
+
const totalAdded = diffs.reduce((sum, d) => sum + d.added, 0);
|
|
77
|
+
const totalRemoved = diffs.reduce((sum, d) => sum + d.removed, 0);
|
|
78
|
+
return {
|
|
79
|
+
diffs,
|
|
80
|
+
totalAdded,
|
|
81
|
+
totalRemoved,
|
|
82
|
+
summary: summarize(totalAdded, totalRemoved),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Tool ───────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export function registerEditTool(
|
|
89
|
+
pi: PiPrettyApi,
|
|
90
|
+
createEditTool: ToolFactory<EditToolInput>,
|
|
91
|
+
ctx: ToolContext,
|
|
92
|
+
trackInvalidator: (id: string, inv: () => void) => void,
|
|
93
|
+
): void {
|
|
94
|
+
const { cwd, sp, TextComponent } = ctx;
|
|
95
|
+
const origEdit = createEditTool(cwd);
|
|
96
|
+
|
|
97
|
+
pi.registerTool({
|
|
98
|
+
...origEdit,
|
|
99
|
+
name: "edit",
|
|
100
|
+
|
|
101
|
+
async execute(
|
|
102
|
+
tid: string,
|
|
103
|
+
params: EditParams,
|
|
104
|
+
sig: AbortSignal | undefined,
|
|
105
|
+
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
106
|
+
toolCtx: ExtensionContext,
|
|
107
|
+
) {
|
|
108
|
+
const fp = params.path ?? params.file_path ?? "";
|
|
109
|
+
const operations = getEditOperations(params);
|
|
110
|
+
const fileLang = lang(fp);
|
|
111
|
+
|
|
112
|
+
const result = (await origEdit.execute(
|
|
113
|
+
tid,
|
|
114
|
+
params as unknown as Parameters<typeof origEdit.execute>[1],
|
|
115
|
+
sig,
|
|
116
|
+
upd,
|
|
117
|
+
toolCtx,
|
|
118
|
+
)) as ToolResultLike;
|
|
119
|
+
|
|
120
|
+
if (operations.length === 0) return result;
|
|
121
|
+
|
|
122
|
+
const { diffs, summary } = summarizeEditOperations(operations);
|
|
123
|
+
|
|
124
|
+
if (operations.length === 1) {
|
|
125
|
+
let editLine = 0;
|
|
126
|
+
try {
|
|
127
|
+
if (fp && existsSync(fp)) {
|
|
128
|
+
const f = readFileSync(fp, "utf-8");
|
|
129
|
+
const idx = f.indexOf(operations[0].newText);
|
|
130
|
+
if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
editLine = 0;
|
|
134
|
+
}
|
|
135
|
+
setResultDetails(result, {
|
|
136
|
+
_type: "editInfo",
|
|
137
|
+
summary,
|
|
138
|
+
editLine,
|
|
139
|
+
oldContent: operations[0].oldText,
|
|
140
|
+
newContent: operations[0].newText,
|
|
141
|
+
language: fileLang,
|
|
142
|
+
filePath: fp,
|
|
143
|
+
});
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
setResultDetails(result, {
|
|
148
|
+
_type: "multiEditInfo",
|
|
149
|
+
summary,
|
|
150
|
+
editCount: operations.length,
|
|
151
|
+
diffLineCount: diffs.reduce((sum, d) => sum + d.lines.length, 0),
|
|
152
|
+
ops: operations.map((op) => ({
|
|
153
|
+
oldContent: op.oldText,
|
|
154
|
+
newContent: op.newText,
|
|
155
|
+
language: fileLang,
|
|
156
|
+
filePath: fp,
|
|
157
|
+
})),
|
|
158
|
+
});
|
|
159
|
+
return result;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
renderCall(
|
|
163
|
+
args: EditParams,
|
|
164
|
+
theme: ThemeLike,
|
|
165
|
+
renderCtx: RenderContextLike<EditRenderState>,
|
|
166
|
+
) {
|
|
167
|
+
const fp = args?.path ?? args?.file_path ?? "";
|
|
168
|
+
const operations = getEditOperations(args);
|
|
169
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
170
|
+
const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
|
|
171
|
+
|
|
172
|
+
if (operations.length === 0) {
|
|
173
|
+
text.setText(fillToolBackground(hdr));
|
|
174
|
+
return text;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { summary } = summarizeEditOperations(operations);
|
|
178
|
+
const suffix =
|
|
179
|
+
operations.length === 1
|
|
180
|
+
? summary
|
|
181
|
+
: `${operations.length} edits ${summary}`;
|
|
182
|
+
text.setText(fillToolBackground(`${hdr} ${theme.fg("muted", suffix)}`));
|
|
183
|
+
return text;
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
renderResult(
|
|
187
|
+
result: ToolResultLike,
|
|
188
|
+
_opt: ToolRenderResultOptions,
|
|
189
|
+
theme: ThemeLike,
|
|
190
|
+
renderCtx: RenderContextLike<EditRenderState>,
|
|
191
|
+
) {
|
|
192
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
193
|
+
if (renderCtx.isError) {
|
|
194
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
const d = result.details as Record<string, unknown> | undefined;
|
|
198
|
+
|
|
199
|
+
// Single edit — full split diff
|
|
200
|
+
if (d?._type === "editInfo") {
|
|
201
|
+
const key = `ed:${diffThemeCacheKey(theme)}:${termW()}:${d.summary}:${(d.oldContent as string).length}:${(d.newContent as string).length}:${d.language ?? ""}`;
|
|
202
|
+
if (renderCtx.toolCallId)
|
|
203
|
+
trackInvalidator(renderCtx.toolCallId, renderCtx.invalidate);
|
|
204
|
+
if (renderCtx.state._edk !== key) {
|
|
205
|
+
renderCtx.state._edk = key;
|
|
206
|
+
const loc =
|
|
207
|
+
(d.editLine as number) > 0
|
|
208
|
+
? ` ${theme.fg("muted", `at line ${d.editLine}`)}`
|
|
209
|
+
: "";
|
|
210
|
+
renderCtx.state._edt = ` ${d.summary}${loc}\n${theme.fg("muted", " rendering diff…")}`;
|
|
211
|
+
const dc = resolveDiffColors(theme);
|
|
212
|
+
const diff = parseDiff(
|
|
213
|
+
d.oldContent as string,
|
|
214
|
+
d.newContent as string,
|
|
215
|
+
);
|
|
216
|
+
renderSplit(
|
|
217
|
+
diff,
|
|
218
|
+
d.language as string | undefined,
|
|
219
|
+
MAX_RENDER_LINES,
|
|
220
|
+
dc,
|
|
221
|
+
)
|
|
222
|
+
.then((rendered) => {
|
|
223
|
+
if (renderCtx.state._edk !== key) return;
|
|
224
|
+
const loc2 =
|
|
225
|
+
(d.editLine as number) > 0
|
|
226
|
+
? ` ${theme.fg("muted", `at line ${d.editLine}`)}`
|
|
227
|
+
: "";
|
|
228
|
+
renderCtx.state._edt = ` ${d.summary}${loc2}\n${rendered}`;
|
|
229
|
+
renderCtx.invalidate();
|
|
230
|
+
})
|
|
231
|
+
.catch(() => {
|
|
232
|
+
if (renderCtx.state._edk !== key) return;
|
|
233
|
+
renderCtx.state._edt = ` ${d.summary}`;
|
|
234
|
+
renderCtx.invalidate();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
text.setText(renderCtx.state._edt ?? ` ${d.summary}`);
|
|
238
|
+
return text;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Multi-edit — stacked diffs
|
|
242
|
+
if (d?._type === "multiEditInfo") {
|
|
243
|
+
const key = `med:${diffThemeCacheKey(theme)}:${termW()}:${d.summary}:${d.editCount}:${d.diffLineCount}`;
|
|
244
|
+
if (renderCtx.toolCallId)
|
|
245
|
+
trackInvalidator(renderCtx.toolCallId, renderCtx.invalidate);
|
|
246
|
+
if (renderCtx.state._edk !== key) {
|
|
247
|
+
renderCtx.state._edk = key;
|
|
248
|
+
renderCtx.state._edt = ` ${d.editCount} edits ${d.summary}\n${theme.fg("muted", " rendering diff…")}`;
|
|
249
|
+
const dc = resolveDiffColors(theme);
|
|
250
|
+
Promise.all(
|
|
251
|
+
(
|
|
252
|
+
d.ops as Array<{
|
|
253
|
+
oldContent: string;
|
|
254
|
+
newContent: string;
|
|
255
|
+
language?: string;
|
|
256
|
+
}>
|
|
257
|
+
).map((op) => {
|
|
258
|
+
const diff = parseDiff(op.oldContent, op.newContent);
|
|
259
|
+
return renderSplit(diff, op.language, MAX_RENDER_LINES, dc);
|
|
260
|
+
}),
|
|
261
|
+
)
|
|
262
|
+
.then((rendered) => {
|
|
263
|
+
if (renderCtx.state._edk !== key) return;
|
|
264
|
+
const body = rendered.join(`\n${theme.fg("muted", " ···")}\n`);
|
|
265
|
+
renderCtx.state._edt = ` ${d.editCount} edits ${d.summary}\n${body}`;
|
|
266
|
+
renderCtx.invalidate();
|
|
267
|
+
})
|
|
268
|
+
.catch(() => {
|
|
269
|
+
if (renderCtx.state._edk !== key) return;
|
|
270
|
+
renderCtx.state._edt = ` ${d.editCount} edits ${d.summary}`;
|
|
271
|
+
renderCtx.invalidate();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
text.setText(
|
|
275
|
+
renderCtx.state._edt ?? ` ${d.editCount} edits ${d.summary}`,
|
|
276
|
+
);
|
|
277
|
+
return text;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const fallback = result.content?.[0];
|
|
281
|
+
const fallbackText =
|
|
282
|
+
fallback && isTextContent(fallback) ? fallback.text : "edited";
|
|
283
|
+
text.setText(
|
|
284
|
+
fillToolBackground(
|
|
285
|
+
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
return text;
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionContext,
|
|
3
|
+
FindToolInput,
|
|
4
|
+
ToolRenderResultOptions,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { FG_DIM, RST } from "../ansi.js";
|
|
8
|
+
import { renderFindResults } from "../renderers.js";
|
|
9
|
+
import type {
|
|
10
|
+
FindParams,
|
|
11
|
+
FindResultDetails,
|
|
12
|
+
PiPrettyApi,
|
|
13
|
+
RenderContextLike,
|
|
14
|
+
ThemeLike,
|
|
15
|
+
ToolFactory,
|
|
16
|
+
ToolResultLike,
|
|
17
|
+
} from "../types.js";
|
|
18
|
+
import {
|
|
19
|
+
appendNotices,
|
|
20
|
+
fillToolBackground,
|
|
21
|
+
getTextContent,
|
|
22
|
+
isTextContent,
|
|
23
|
+
makeTextResult,
|
|
24
|
+
renderToolError,
|
|
25
|
+
setResultDetails,
|
|
26
|
+
} from "../utils.js";
|
|
27
|
+
import type { ToolContext } from "./context.js";
|
|
28
|
+
|
|
29
|
+
export function registerFindTool(
|
|
30
|
+
pi: PiPrettyApi,
|
|
31
|
+
createFindTool: ToolFactory<FindToolInput>,
|
|
32
|
+
ctx: ToolContext,
|
|
33
|
+
): void {
|
|
34
|
+
const { cwd, sp, TextComponent, fffState } = ctx;
|
|
35
|
+
const origFind = createFindTool(cwd);
|
|
36
|
+
|
|
37
|
+
pi.registerTool({
|
|
38
|
+
...origFind,
|
|
39
|
+
name: "find",
|
|
40
|
+
|
|
41
|
+
async execute(
|
|
42
|
+
tid: string,
|
|
43
|
+
params: FindParams,
|
|
44
|
+
sig: AbortSignal | undefined,
|
|
45
|
+
upd: unknown,
|
|
46
|
+
toolCtx: ExtensionContext,
|
|
47
|
+
) {
|
|
48
|
+
// Try FFF first (frecency-ranked, SIMD-accelerated)
|
|
49
|
+
if (fffState.finder && !fffState.finder.isDestroyed) {
|
|
50
|
+
try {
|
|
51
|
+
const effectiveLimit = Math.max(1, params.limit ?? 200);
|
|
52
|
+
let query = params.pattern;
|
|
53
|
+
if (params.path) query = `${params.path} ${query}`;
|
|
54
|
+
|
|
55
|
+
const searchResult = fffState.finder.fileSearch(query, {
|
|
56
|
+
pageSize: effectiveLimit,
|
|
57
|
+
});
|
|
58
|
+
if (searchResult.ok) {
|
|
59
|
+
const { items, totalMatched } = searchResult.value;
|
|
60
|
+
const trimmed = items.slice(0, effectiveLimit);
|
|
61
|
+
const notices: string[] = [];
|
|
62
|
+
if (fffState.partialIndex)
|
|
63
|
+
notices.push("Warning: partial file index");
|
|
64
|
+
if (trimmed.length >= effectiveLimit)
|
|
65
|
+
notices.push(`${effectiveLimit} limit reached`);
|
|
66
|
+
if (totalMatched > trimmed.length)
|
|
67
|
+
notices.push(`${totalMatched} total matches`);
|
|
68
|
+
|
|
69
|
+
const textContent = appendNotices(
|
|
70
|
+
trimmed.map((item) => item.relativePath).join("\n"),
|
|
71
|
+
notices,
|
|
72
|
+
);
|
|
73
|
+
return makeTextResult<FindResultDetails>(textContent, {
|
|
74
|
+
_type: "findResult",
|
|
75
|
+
text: textContent,
|
|
76
|
+
pattern: params.pattern,
|
|
77
|
+
matchCount: trimmed.length,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
/* fall through to SDK */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// SDK fallback
|
|
86
|
+
const result = await origFind.execute(
|
|
87
|
+
tid,
|
|
88
|
+
params,
|
|
89
|
+
sig,
|
|
90
|
+
upd as never,
|
|
91
|
+
toolCtx,
|
|
92
|
+
);
|
|
93
|
+
const textContent = getTextContent(result);
|
|
94
|
+
const matchCount = textContent
|
|
95
|
+
? textContent.trim().split("\n").filter(Boolean).length
|
|
96
|
+
: 0;
|
|
97
|
+
|
|
98
|
+
setResultDetails<FindResultDetails>(result, {
|
|
99
|
+
_type: "findResult",
|
|
100
|
+
text: textContent,
|
|
101
|
+
pattern: params.pattern,
|
|
102
|
+
matchCount,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
renderCall(
|
|
109
|
+
args: FindParams,
|
|
110
|
+
theme: ThemeLike,
|
|
111
|
+
renderCtx: RenderContextLike,
|
|
112
|
+
) {
|
|
113
|
+
const pattern = args.pattern ?? "";
|
|
114
|
+
const path = args.path
|
|
115
|
+
? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
|
|
116
|
+
: "";
|
|
117
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
118
|
+
text.setText(
|
|
119
|
+
fillToolBackground(
|
|
120
|
+
`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
return text;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
renderResult(
|
|
127
|
+
result: ToolResultLike<FindResultDetails>,
|
|
128
|
+
_opt: ToolRenderResultOptions,
|
|
129
|
+
theme: ThemeLike,
|
|
130
|
+
renderCtx: RenderContextLike,
|
|
131
|
+
) {
|
|
132
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
133
|
+
|
|
134
|
+
if (renderCtx.isError) {
|
|
135
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
136
|
+
return text;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const d = result.details;
|
|
140
|
+
if (d?._type === "findResult" && d.text) {
|
|
141
|
+
const rendered = renderFindResults(d.text);
|
|
142
|
+
const info = `${FG_DIM}${d.matchCount} files${RST}`;
|
|
143
|
+
text.setText(fillToolBackground(` ${info}\n${rendered}`));
|
|
144
|
+
return text;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const fallback = result.content?.[0];
|
|
148
|
+
const fallbackText =
|
|
149
|
+
fallback && isTextContent(fallback) ? fallback.text : "found";
|
|
150
|
+
text.setText(
|
|
151
|
+
fillToolBackground(
|
|
152
|
+
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
return text;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionContext,
|
|
3
|
+
GrepToolInput,
|
|
4
|
+
ToolRenderResultOptions,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { FG_DIM, RST } from "../ansi.js";
|
|
8
|
+
import { fffFormatGrepText } from "../fff.js";
|
|
9
|
+
import { renderGrepResults } from "../renderers.js";
|
|
10
|
+
import type {
|
|
11
|
+
GrepParams,
|
|
12
|
+
GrepRenderState,
|
|
13
|
+
GrepResultDetails,
|
|
14
|
+
PiPrettyApi,
|
|
15
|
+
RenderContextLike,
|
|
16
|
+
ThemeLike,
|
|
17
|
+
ToolFactory,
|
|
18
|
+
ToolResultLike,
|
|
19
|
+
} from "../types.js";
|
|
20
|
+
import {
|
|
21
|
+
appendNotices,
|
|
22
|
+
countRipgrepMatches,
|
|
23
|
+
fillToolBackground,
|
|
24
|
+
getTextContent,
|
|
25
|
+
isTextContent,
|
|
26
|
+
makeTextResult,
|
|
27
|
+
normalizeLineEndings,
|
|
28
|
+
renderToolError,
|
|
29
|
+
setResultDetails,
|
|
30
|
+
termW,
|
|
31
|
+
} from "../utils.js";
|
|
32
|
+
import type { ToolContext } from "./context.js";
|
|
33
|
+
|
|
34
|
+
export function registerGrepTool(
|
|
35
|
+
pi: PiPrettyApi,
|
|
36
|
+
createGrepTool: ToolFactory<GrepToolInput>,
|
|
37
|
+
ctx: ToolContext,
|
|
38
|
+
): void {
|
|
39
|
+
const { cwd, sp, TextComponent, fffState, cursorStore } = ctx;
|
|
40
|
+
const origGrep = createGrepTool(cwd);
|
|
41
|
+
|
|
42
|
+
pi.registerTool({
|
|
43
|
+
...origGrep,
|
|
44
|
+
name: "grep",
|
|
45
|
+
|
|
46
|
+
async execute(
|
|
47
|
+
tid: string,
|
|
48
|
+
params: GrepParams,
|
|
49
|
+
sig: AbortSignal | undefined,
|
|
50
|
+
upd: unknown,
|
|
51
|
+
toolCtx: ExtensionContext,
|
|
52
|
+
) {
|
|
53
|
+
// Try FFF first (SIMD-accelerated).
|
|
54
|
+
// Constrained searches (path/glob) fall through to SDK — FFF 0.5.2
|
|
55
|
+
// can abort the process on constrained searches with Unicode filenames.
|
|
56
|
+
if (
|
|
57
|
+
fffState.finder &&
|
|
58
|
+
!fffState.finder.isDestroyed &&
|
|
59
|
+
!params.path &&
|
|
60
|
+
!params.glob
|
|
61
|
+
) {
|
|
62
|
+
try {
|
|
63
|
+
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
64
|
+
const grepResult = fffState.finder.grep(params.pattern, {
|
|
65
|
+
mode: params.literal ? "plain" : "regex",
|
|
66
|
+
smartCase: !params.ignoreCase,
|
|
67
|
+
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
68
|
+
cursor: null,
|
|
69
|
+
beforeContext: params.context ?? 0,
|
|
70
|
+
afterContext: params.context ?? 0,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (grepResult.ok) {
|
|
74
|
+
const grep = grepResult.value;
|
|
75
|
+
const notices: string[] = [];
|
|
76
|
+
if (fffState.partialIndex)
|
|
77
|
+
notices.push("Warning: partial file index");
|
|
78
|
+
if (grep.items.length >= effectiveLimit)
|
|
79
|
+
notices.push(`${effectiveLimit} limit reached`);
|
|
80
|
+
if (grep.regexFallbackError)
|
|
81
|
+
notices.push(
|
|
82
|
+
`Regex failed: ${grep.regexFallbackError}, used literal match`,
|
|
83
|
+
);
|
|
84
|
+
if (grep.nextCursor) {
|
|
85
|
+
const cursorId = cursorStore.store(grep.nextCursor);
|
|
86
|
+
notices.push(
|
|
87
|
+
`More results available. Use cursor="${cursorId}" to continue`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const textContent = appendNotices(
|
|
92
|
+
fffFormatGrepText(grep.items, effectiveLimit),
|
|
93
|
+
notices,
|
|
94
|
+
);
|
|
95
|
+
return makeTextResult<GrepResultDetails>(textContent, {
|
|
96
|
+
_type: "grepResult",
|
|
97
|
+
text: textContent,
|
|
98
|
+
pattern: params.pattern,
|
|
99
|
+
matchCount: Math.min(grep.items.length, effectiveLimit),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
/* fall through to SDK */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// SDK fallback
|
|
108
|
+
const result = await origGrep.execute(
|
|
109
|
+
tid,
|
|
110
|
+
params,
|
|
111
|
+
sig,
|
|
112
|
+
upd as never,
|
|
113
|
+
toolCtx,
|
|
114
|
+
);
|
|
115
|
+
const textContent = normalizeLineEndings(getTextContent(result));
|
|
116
|
+
if (result.content) {
|
|
117
|
+
for (const content of result.content) {
|
|
118
|
+
if (isTextContent(content))
|
|
119
|
+
content.text = normalizeLineEndings(content.text || "");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const matchCount = textContent ? countRipgrepMatches(textContent) : 0;
|
|
123
|
+
|
|
124
|
+
setResultDetails<GrepResultDetails>(result, {
|
|
125
|
+
_type: "grepResult",
|
|
126
|
+
text: textContent,
|
|
127
|
+
pattern: params.pattern,
|
|
128
|
+
matchCount,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
renderCall(
|
|
135
|
+
args: GrepParams,
|
|
136
|
+
theme: ThemeLike,
|
|
137
|
+
renderCtx: RenderContextLike,
|
|
138
|
+
) {
|
|
139
|
+
const pattern = args.pattern ?? "";
|
|
140
|
+
const path = args.path
|
|
141
|
+
? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
|
|
142
|
+
: "";
|
|
143
|
+
const glob = args.glob ? ` ${theme.fg("muted", `(${args.glob})`)}` : "";
|
|
144
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
145
|
+
text.setText(
|
|
146
|
+
fillToolBackground(
|
|
147
|
+
`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`,
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
return text;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
renderResult(
|
|
154
|
+
result: ToolResultLike<GrepResultDetails>,
|
|
155
|
+
_opt: ToolRenderResultOptions,
|
|
156
|
+
theme: ThemeLike,
|
|
157
|
+
renderCtx: RenderContextLike<GrepRenderState>,
|
|
158
|
+
) {
|
|
159
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
160
|
+
|
|
161
|
+
if (renderCtx.isError) {
|
|
162
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const d = result.details;
|
|
167
|
+
if (d?._type === "grepResult" && d.text) {
|
|
168
|
+
const key = `grep:${d.pattern}:${d.matchCount}:${termW()}`;
|
|
169
|
+
if (renderCtx.state._gk !== key) {
|
|
170
|
+
renderCtx.state._gk = key;
|
|
171
|
+
const info = `${FG_DIM}${d.matchCount} matches${RST}`;
|
|
172
|
+
renderCtx.state._gt = fillToolBackground(` ${info}`);
|
|
173
|
+
|
|
174
|
+
renderGrepResults(d.text, d.pattern)
|
|
175
|
+
.then((rendered: string) => {
|
|
176
|
+
if (renderCtx.state._gk !== key) return;
|
|
177
|
+
renderCtx.state._gt = fillToolBackground(
|
|
178
|
+
` ${info}\n${rendered}`,
|
|
179
|
+
);
|
|
180
|
+
renderCtx.invalidate();
|
|
181
|
+
})
|
|
182
|
+
.catch(() => {});
|
|
183
|
+
}
|
|
184
|
+
text.setText(
|
|
185
|
+
renderCtx.state._gt ??
|
|
186
|
+
fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}`),
|
|
187
|
+
);
|
|
188
|
+
return text;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const fallback = result.content?.[0];
|
|
192
|
+
const fallbackText =
|
|
193
|
+
fallback && isTextContent(fallback) ? fallback.text : "searched";
|
|
194
|
+
text.setText(
|
|
195
|
+
fillToolBackground(
|
|
196
|
+
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
return text;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|