@xynogen/pix-find 0.2.4 → 0.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/package.json +1 -1
- package/src/find.ts +153 -59
package/package.json
CHANGED
package/src/find.ts
CHANGED
|
@@ -4,6 +4,17 @@ import type {
|
|
|
4
4
|
ToolRenderResultOptions,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { resolveBaseBackground } from "@xynogen/pix-pretty/ansi";
|
|
7
|
+
import {
|
|
8
|
+
BATCH_MAX_BYTES,
|
|
9
|
+
type BatchSection,
|
|
10
|
+
capSections,
|
|
11
|
+
formatBatchIndex,
|
|
12
|
+
formatCallTargets,
|
|
13
|
+
joinSectionBodies,
|
|
14
|
+
resolveBatchStrings,
|
|
15
|
+
sliceBatchTargets,
|
|
16
|
+
withOptionalStringArray,
|
|
17
|
+
} from "@xynogen/pix-pretty/batch";
|
|
7
18
|
import type { ToolContext } from "@xynogen/pix-pretty/context";
|
|
8
19
|
import type {
|
|
9
20
|
FindParams,
|
|
@@ -32,6 +43,11 @@ import { type CollapseState, tickCollapse } from "@xynogen/pix-runtime/collapse"
|
|
|
32
43
|
|
|
33
44
|
export const DEFAULT_FIND_LIMIT = 200;
|
|
34
45
|
|
|
46
|
+
const FILE_NOUNS = ["file", "files"] as const;
|
|
47
|
+
|
|
48
|
+
/** FindParams plus the optional batch `patterns` array (added to the schema at runtime). */
|
|
49
|
+
type FindBatchParams = FindParams & { patterns?: string[] };
|
|
50
|
+
|
|
35
51
|
export function applyFindDefaults(params: FindParams): FindParams {
|
|
36
52
|
return params.limit === undefined ? { ...params, limit: DEFAULT_FIND_LIMIT } : params;
|
|
37
53
|
}
|
|
@@ -67,88 +83,165 @@ export function registerFindTool(
|
|
|
67
83
|
...origFind,
|
|
68
84
|
name: "find",
|
|
69
85
|
description:
|
|
70
|
-
"Find files by glob pattern. Defaults to 200 paths; use limit to request more. Respects .gitignore and remains capped by Pi's 50KB hard limit.",
|
|
86
|
+
"Find files by glob pattern. Defaults to 200 paths; use limit to request more. Respects .gitignore and remains capped by Pi's 50KB hard limit. Pass `patterns` to search several known globs in one call.",
|
|
87
|
+
parameters: withOptionalStringArray(
|
|
88
|
+
origFind.parameters,
|
|
89
|
+
"patterns",
|
|
90
|
+
"Known glob patterns to search in one call (each capped, one combined result). Use instead of `pattern` for multiple known globs.",
|
|
91
|
+
["pattern"],
|
|
92
|
+
),
|
|
71
93
|
renderShell: "self",
|
|
72
94
|
|
|
73
95
|
async execute(
|
|
74
96
|
tid: string,
|
|
75
|
-
params:
|
|
97
|
+
params: FindBatchParams,
|
|
76
98
|
sig: AbortSignal | undefined,
|
|
77
99
|
upd: unknown,
|
|
78
100
|
toolCtx: ExtensionContext,
|
|
79
101
|
) {
|
|
80
|
-
|
|
102
|
+
// Search one glob (FFF-accelerated, SDK fallback) → structured find result.
|
|
103
|
+
const runOne = async (
|
|
104
|
+
pattern: string,
|
|
105
|
+
callId: string,
|
|
106
|
+
): Promise<ToolResultLike<FindResultDetails>> => {
|
|
107
|
+
const { patterns: _patterns, ...rest } = params;
|
|
108
|
+
const effectiveParams = applyFindDefaults({ ...rest, pattern });
|
|
81
109
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
if (searchResult.ok) {
|
|
93
|
-
const { items, totalMatched } = searchResult.value;
|
|
94
|
-
const trimmed = items.slice(0, effectiveLimit);
|
|
95
|
-
const notices: string[] = [];
|
|
96
|
-
if (fffState.partialIndex) notices.push("Warning: partial file index");
|
|
97
|
-
if (trimmed.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
98
|
-
if (totalMatched > trimmed.length) notices.push(`${totalMatched} total matches`);
|
|
99
|
-
|
|
100
|
-
const textContent = appendNotices(
|
|
101
|
-
trimmed.map((item) => item.relativePath).join("\n"),
|
|
102
|
-
notices,
|
|
103
|
-
);
|
|
104
|
-
return makeTextResult<FindResultDetails>(textContent, {
|
|
105
|
-
_type: "findResult",
|
|
106
|
-
text: textContent,
|
|
107
|
-
pattern: effectiveParams.pattern,
|
|
108
|
-
path: effectiveParams.path,
|
|
109
|
-
matchCount: trimmed.length,
|
|
110
|
+
// Try FFF first (frecency-ranked, SIMD-accelerated)
|
|
111
|
+
if (fffState.finder && !fffState.finder.isDestroyed) {
|
|
112
|
+
try {
|
|
113
|
+
const effectiveLimit = Math.max(1, effectiveParams.limit ?? DEFAULT_FIND_LIMIT);
|
|
114
|
+
let query = effectiveParams.pattern;
|
|
115
|
+
if (effectiveParams.path) query = `${effectiveParams.path} ${query}`;
|
|
116
|
+
|
|
117
|
+
const searchResult = fffState.finder.fileSearch(query, {
|
|
118
|
+
pageSize: effectiveLimit,
|
|
110
119
|
});
|
|
120
|
+
if (searchResult.ok) {
|
|
121
|
+
const { items, totalMatched } = searchResult.value;
|
|
122
|
+
const trimmed = items.slice(0, effectiveLimit);
|
|
123
|
+
const notices: string[] = [];
|
|
124
|
+
if (fffState.partialIndex) notices.push("Warning: partial file index");
|
|
125
|
+
if (trimmed.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
126
|
+
if (totalMatched > trimmed.length) notices.push(`${totalMatched} total matches`);
|
|
127
|
+
|
|
128
|
+
const textContent = appendNotices(
|
|
129
|
+
trimmed.map((item) => item.relativePath).join("\n"),
|
|
130
|
+
notices,
|
|
131
|
+
);
|
|
132
|
+
return makeTextResult<FindResultDetails>(textContent, {
|
|
133
|
+
_type: "findResult",
|
|
134
|
+
text: textContent,
|
|
135
|
+
pattern: effectiveParams.pattern,
|
|
136
|
+
path: effectiveParams.path,
|
|
137
|
+
matchCount: trimmed.length,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
/* fall through to SDK */
|
|
111
142
|
}
|
|
112
|
-
} catch {
|
|
113
|
-
/* fall through to SDK */
|
|
114
143
|
}
|
|
115
|
-
}
|
|
116
144
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const result = await origFind.execute(tid, effectiveParams, sig, upd as never, toolCtx);
|
|
145
|
+
// SDK fallback
|
|
146
|
+
const result = await origFind.execute(callId, effectiveParams, sig, upd as never, toolCtx);
|
|
120
147
|
const textContent = getTextContent(result);
|
|
121
|
-
const matchCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
|
|
122
|
-
|
|
123
148
|
setResultDetails<FindResultDetails>(result, {
|
|
124
149
|
_type: "findResult",
|
|
125
150
|
text: textContent,
|
|
126
|
-
pattern
|
|
151
|
+
pattern,
|
|
152
|
+
path: effectiveParams.path,
|
|
153
|
+
matchCount: textContent ? textContent.trim().split("\n").filter(Boolean).length : 0,
|
|
154
|
+
});
|
|
155
|
+
return result as ToolResultLike<FindResultDetails>;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const { targets, omitted } = sliceBatchTargets(
|
|
159
|
+
resolveBatchStrings(params.pattern, params.patterns),
|
|
160
|
+
);
|
|
161
|
+
if (targets.length === 0) {
|
|
162
|
+
return makeTextResult<FindResultDetails>("pattern or patterns required", {
|
|
163
|
+
_type: "findResult",
|
|
164
|
+
text: "pattern or patterns required",
|
|
165
|
+
pattern: "",
|
|
127
166
|
path: params.path,
|
|
128
|
-
matchCount,
|
|
167
|
+
matchCount: 0,
|
|
129
168
|
});
|
|
169
|
+
}
|
|
130
170
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
text,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
171
|
+
// Single glob → preserve the original single-search result shape.
|
|
172
|
+
if (targets.length === 1 && omitted === 0) {
|
|
173
|
+
try {
|
|
174
|
+
return await runOne(targets[0] ?? "", tid);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
177
|
+
if (sig?.aborted || /aborted/i.test(text)) throw error;
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text" as const, text }],
|
|
180
|
+
details: {
|
|
181
|
+
_type: "findResult" as const,
|
|
182
|
+
text,
|
|
183
|
+
pattern: params.pattern ?? "",
|
|
184
|
+
path: params.path,
|
|
185
|
+
matchCount: 0,
|
|
186
|
+
},
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
146
190
|
}
|
|
191
|
+
|
|
192
|
+
// Batch: search every glob in parallel, cap the combined output.
|
|
193
|
+
const settled = await Promise.all(
|
|
194
|
+
targets.map(async (pattern, i) => {
|
|
195
|
+
try {
|
|
196
|
+
return { pattern, result: await runOne(pattern, `${tid}:${i}`) };
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (sig?.aborted) throw error;
|
|
199
|
+
return { pattern, error: error instanceof Error ? error.message : String(error) };
|
|
200
|
+
}
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
const sections: BatchSection[] = settled.map((entry) => {
|
|
204
|
+
if ("error" in entry && entry.error) {
|
|
205
|
+
return { id: entry.pattern, body: "", units: 0, nouns: FILE_NOUNS, error: entry.error };
|
|
206
|
+
}
|
|
207
|
+
const details = (entry as { result: ToolResultLike<FindResultDetails> }).result.details;
|
|
208
|
+
const body =
|
|
209
|
+
details?._type === "findResult"
|
|
210
|
+
? details.text
|
|
211
|
+
: getTextContent((entry as { result: ToolResultLike }).result);
|
|
212
|
+
const units =
|
|
213
|
+
details?._type === "findResult"
|
|
214
|
+
? details.matchCount
|
|
215
|
+
: body.trim().split("\n").filter(Boolean).length;
|
|
216
|
+
return { id: entry.pattern, body, units, nouns: FILE_NOUNS };
|
|
217
|
+
});
|
|
218
|
+
const { text } = capSections(
|
|
219
|
+
sections,
|
|
220
|
+
BATCH_MAX_BYTES,
|
|
221
|
+
params.limit ?? DEFAULT_FIND_LIMIT,
|
|
222
|
+
omitted,
|
|
223
|
+
);
|
|
224
|
+
const matchCount = sections.reduce((sum, s) => sum + (s.error ? 0 : s.units), 0);
|
|
225
|
+
const full = [formatBatchIndex(sections, omitted), joinSectionBodies(sections)]
|
|
226
|
+
.filter(Boolean)
|
|
227
|
+
.join("\n\n");
|
|
228
|
+
return makeTextResult<FindResultDetails>(text, {
|
|
229
|
+
_type: "findResult",
|
|
230
|
+
text: full,
|
|
231
|
+
pattern: targets.join(", "),
|
|
232
|
+
patterns: targets,
|
|
233
|
+
path: params.path,
|
|
234
|
+
matchCount,
|
|
235
|
+
});
|
|
147
236
|
},
|
|
148
237
|
|
|
149
|
-
renderCall(args:
|
|
238
|
+
renderCall(args: FindBatchParams, theme: ThemeLike, renderCtx: RenderContextLike) {
|
|
150
239
|
resolveBaseBackground(theme);
|
|
151
|
-
const
|
|
240
|
+
const batchPatterns = resolveBatchStrings(args.pattern, args.patterns);
|
|
241
|
+
const pattern =
|
|
242
|
+
batchPatterns.length > 1
|
|
243
|
+
? formatCallTargets(batchPatterns, 3, "patterns")
|
|
244
|
+
: (args.pattern ?? "");
|
|
152
245
|
const path = args.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
|
|
153
246
|
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
154
247
|
if (
|
|
@@ -215,7 +308,8 @@ export function registerFindTool(
|
|
|
215
308
|
renderDimPreview(output, theme, {
|
|
216
309
|
frame: !isPartial,
|
|
217
310
|
paint: (s: string) => theme.fg(renderCtx.isError ? "error" : "success", s),
|
|
218
|
-
highlight:
|
|
311
|
+
highlight:
|
|
312
|
+
d?._type === "findResult" && !d.patterns ? globHighlight(d.pattern) : undefined,
|
|
219
313
|
}),
|
|
220
314
|
);
|
|
221
315
|
return text;
|