@xynogen/pix-find 0.2.3 → 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 +2 -1
- package/src/find.ts +153 -59
- package/src/find.test.ts +0 -255
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-find",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pi tool — glob file search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
16
|
+
"!src/**/*.test.*",
|
|
16
17
|
"README.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
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;
|
package/src/find.test.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import type { CursorStore, FffState } from "@xynogen/pix-pretty/fff";
|
|
3
|
-
import type {
|
|
4
|
-
PiPrettyApi,
|
|
5
|
-
RenderContextLike,
|
|
6
|
-
TextComponentCtor,
|
|
7
|
-
ThemeLike,
|
|
8
|
-
} from "@xynogen/pix-pretty/types";
|
|
9
|
-
import { applyFindDefaults, DEFAULT_FIND_LIMIT, globHighlight, registerFindTool } from "./find";
|
|
10
|
-
|
|
11
|
-
describe("globHighlight", () => {
|
|
12
|
-
it("keeps literal runs from a glob as case-insensitive alternatives", () => {
|
|
13
|
-
const re = globHighlight("**/*.test.ts");
|
|
14
|
-
expect(re).toBeInstanceOf(RegExp);
|
|
15
|
-
expect((re as RegExp).flags).toBe("gi");
|
|
16
|
-
expect("foo.test.ts".match(re as RegExp)).not.toBeNull();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("highlights the extension of a simple glob", () => {
|
|
20
|
-
expect("app.ts".match(globHighlight("*.ts") as RegExp)?.[0]).toBe(".ts");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("returns undefined when the glob has no meaningful literal run", () => {
|
|
24
|
-
expect(globHighlight("*")).toBeUndefined();
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
class MockTextComponent {
|
|
29
|
-
private text = "";
|
|
30
|
-
setText(v: string) {
|
|
31
|
-
this.text = v;
|
|
32
|
-
}
|
|
33
|
-
getText() {
|
|
34
|
-
return this.text;
|
|
35
|
-
}
|
|
36
|
-
render(_width?: number) {
|
|
37
|
-
return this.text.split("\n");
|
|
38
|
-
}
|
|
39
|
-
invalidate() {}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("applyFindDefaults", () => {
|
|
43
|
-
it("applies a conservative default without overriding an explicit limit", () => {
|
|
44
|
-
expect(applyFindDefaults({ pattern: "**/*.ts" })).toEqual({
|
|
45
|
-
pattern: "**/*.ts",
|
|
46
|
-
limit: DEFAULT_FIND_LIMIT,
|
|
47
|
-
});
|
|
48
|
-
expect(applyFindDefaults({ pattern: "**/*.ts", limit: 8 })).toEqual({
|
|
49
|
-
pattern: "**/*.ts",
|
|
50
|
-
limit: 8,
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe("registerFindTool", () => {
|
|
56
|
-
it("registers a tool named 'find'", () => {
|
|
57
|
-
const tools: string[] = [];
|
|
58
|
-
const mockPi: PiPrettyApi = {
|
|
59
|
-
registerTool(t: unknown) {
|
|
60
|
-
tools.push((t as { name: string }).name);
|
|
61
|
-
},
|
|
62
|
-
registerCommand() {},
|
|
63
|
-
on() {},
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
registerFindTool(
|
|
67
|
-
mockPi,
|
|
68
|
-
() => ({ execute: async () => ({ content: [], details: undefined }) }),
|
|
69
|
-
{
|
|
70
|
-
cwd: process.cwd(),
|
|
71
|
-
sp: (p: string) => p,
|
|
72
|
-
TextComponent: MockTextComponent as unknown as TextComponentCtor,
|
|
73
|
-
fffState: {
|
|
74
|
-
module: null,
|
|
75
|
-
finder: null,
|
|
76
|
-
partialIndex: false,
|
|
77
|
-
dbDir: null,
|
|
78
|
-
} satisfies FffState,
|
|
79
|
-
cursorStore: {
|
|
80
|
-
store: () => "",
|
|
81
|
-
get: () => undefined,
|
|
82
|
-
} as unknown as CursorStore,
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
expect(tools).toEqual(["find"]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("restores result paths when an elapsed card is expanded", () => {
|
|
89
|
-
const registered: { renderResult?: (...args: unknown[]) => MockTextComponent } = {};
|
|
90
|
-
const mockPi: PiPrettyApi = {
|
|
91
|
-
registerTool(tool: unknown) {
|
|
92
|
-
Object.assign(registered, tool);
|
|
93
|
-
},
|
|
94
|
-
registerCommand() {},
|
|
95
|
-
on() {},
|
|
96
|
-
};
|
|
97
|
-
registerFindTool(
|
|
98
|
-
mockPi,
|
|
99
|
-
() => ({ execute: async () => ({ content: [], details: undefined }) }),
|
|
100
|
-
{
|
|
101
|
-
cwd: process.cwd(),
|
|
102
|
-
sp: (p: string) => p,
|
|
103
|
-
TextComponent: MockTextComponent as unknown as TextComponentCtor,
|
|
104
|
-
fffState: { module: null, finder: null, partialIndex: false, dbDir: null },
|
|
105
|
-
cursorStore: { store: () => "", get: () => undefined } as unknown as CursorStore,
|
|
106
|
-
},
|
|
107
|
-
);
|
|
108
|
-
const theme: ThemeLike = {
|
|
109
|
-
fg: (_key: string, value: string) => value,
|
|
110
|
-
bold: (value: string) => value,
|
|
111
|
-
};
|
|
112
|
-
const result = registered.renderResult?.(
|
|
113
|
-
{
|
|
114
|
-
content: [{ type: "text", text: "src/one.ts\nsrc/two.ts" }],
|
|
115
|
-
details: {
|
|
116
|
-
_type: "findResult",
|
|
117
|
-
text: "src/one.ts\nsrc/two.ts",
|
|
118
|
-
pattern: "**/*.ts",
|
|
119
|
-
matchCount: 2,
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
undefined,
|
|
123
|
-
theme,
|
|
124
|
-
{
|
|
125
|
-
expanded: true,
|
|
126
|
-
isError: false,
|
|
127
|
-
invalidate: () => {},
|
|
128
|
-
state: { collapsed: true },
|
|
129
|
-
} as unknown as RenderContextLike,
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const strip = (s: string) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
133
|
-
const shown = strip(result?.getText() ?? "");
|
|
134
|
-
expect(shown).toContain("src/one.ts");
|
|
135
|
-
expect(shown).toContain("src/two.ts");
|
|
136
|
-
// No floating count header in the framed view — the collapsed row carries it.
|
|
137
|
-
expect(shown).not.toContain("2 files");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("frames single-file output like multi-file (no inline row)", () => {
|
|
141
|
-
const registered: { renderResult?: (...args: unknown[]) => MockTextComponent } = {};
|
|
142
|
-
const mockPi: PiPrettyApi = {
|
|
143
|
-
registerTool(tool: unknown) {
|
|
144
|
-
Object.assign(registered, tool);
|
|
145
|
-
},
|
|
146
|
-
registerCommand() {},
|
|
147
|
-
on() {},
|
|
148
|
-
};
|
|
149
|
-
registerFindTool(
|
|
150
|
-
mockPi,
|
|
151
|
-
() => ({ execute: async () => ({ content: [], details: undefined }) }),
|
|
152
|
-
{
|
|
153
|
-
cwd: process.cwd(),
|
|
154
|
-
sp: (p: string) => p,
|
|
155
|
-
TextComponent: MockTextComponent as unknown as TextComponentCtor,
|
|
156
|
-
fffState: {
|
|
157
|
-
module: null,
|
|
158
|
-
finder: null,
|
|
159
|
-
partialIndex: false,
|
|
160
|
-
dbDir: null,
|
|
161
|
-
} satisfies FffState,
|
|
162
|
-
cursorStore: { store: () => "", get: () => undefined } as unknown as CursorStore,
|
|
163
|
-
},
|
|
164
|
-
);
|
|
165
|
-
const theme: ThemeLike = { fg: (_k: string, v: string) => v, bold: (v: string) => v };
|
|
166
|
-
const strip = (s: string) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
167
|
-
const result = {
|
|
168
|
-
content: [{ type: "text", text: "src/a.ts" }],
|
|
169
|
-
details: { _type: "findResult", text: "src/a.ts", pattern: "*.ts", matchCount: 1 } as never,
|
|
170
|
-
};
|
|
171
|
-
const out =
|
|
172
|
-
registered
|
|
173
|
-
.renderResult?.(result, { isPartial: false }, theme, {
|
|
174
|
-
expanded: false,
|
|
175
|
-
isError: false,
|
|
176
|
-
invalidate: () => {},
|
|
177
|
-
state: {},
|
|
178
|
-
} as unknown as RenderContextLike)
|
|
179
|
-
?.getText() ?? "";
|
|
180
|
-
// Single result is framed just like multi — one shape, no inline row.
|
|
181
|
-
expect(out).toContain("─");
|
|
182
|
-
expect(strip(out)).toContain("src/a.ts");
|
|
183
|
-
expect(strip(out)).not.toContain("file");
|
|
184
|
-
const multi = {
|
|
185
|
-
content: [{ type: "text", text: "a.ts\nb.ts\nc.ts" }],
|
|
186
|
-
details: {
|
|
187
|
-
_type: "findResult",
|
|
188
|
-
text: "a.ts\nb.ts\nc.ts",
|
|
189
|
-
pattern: "*.ts",
|
|
190
|
-
matchCount: 3,
|
|
191
|
-
} as never,
|
|
192
|
-
};
|
|
193
|
-
const multiOut =
|
|
194
|
-
registered
|
|
195
|
-
.renderResult?.(multi, { isPartial: false }, theme, {
|
|
196
|
-
expanded: false,
|
|
197
|
-
isError: false,
|
|
198
|
-
invalidate: () => {},
|
|
199
|
-
state: {},
|
|
200
|
-
} as unknown as RenderContextLike)
|
|
201
|
-
?.getText() ?? "";
|
|
202
|
-
expect(multiOut).toContain("─");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("collapses structured errors and restores the exact diagnostic on expansion", () => {
|
|
206
|
-
const registered: { renderResult?: (...args: unknown[]) => MockTextComponent } = {};
|
|
207
|
-
const mockPi: PiPrettyApi = {
|
|
208
|
-
registerTool(tool: unknown) {
|
|
209
|
-
Object.assign(registered, tool);
|
|
210
|
-
},
|
|
211
|
-
registerCommand() {},
|
|
212
|
-
on() {},
|
|
213
|
-
};
|
|
214
|
-
registerFindTool(
|
|
215
|
-
mockPi,
|
|
216
|
-
() => ({ execute: async () => ({ content: [], details: undefined }) }),
|
|
217
|
-
{
|
|
218
|
-
cwd: process.cwd(),
|
|
219
|
-
sp: (p: string) => p,
|
|
220
|
-
TextComponent: MockTextComponent as unknown as TextComponentCtor,
|
|
221
|
-
fffState: { module: null, finder: null, partialIndex: false, dbDir: null },
|
|
222
|
-
cursorStore: { store: () => "", get: () => undefined } as unknown as CursorStore,
|
|
223
|
-
},
|
|
224
|
-
);
|
|
225
|
-
const theme: ThemeLike = {
|
|
226
|
-
fg: (_key: string, value: string) => value,
|
|
227
|
-
bold: (value: string) => value,
|
|
228
|
-
};
|
|
229
|
-
const diagnostic = "Invalid glob pattern: [";
|
|
230
|
-
const result = {
|
|
231
|
-
content: [{ type: "text", text: diagnostic }],
|
|
232
|
-
details: {
|
|
233
|
-
_type: "findResult",
|
|
234
|
-
text: diagnostic,
|
|
235
|
-
pattern: "[",
|
|
236
|
-
path: "src",
|
|
237
|
-
matchCount: 0,
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
const render = (state: Record<string, unknown>, expanded = false) => {
|
|
241
|
-
const component = registered.renderResult?.(result, { isPartial: false }, theme, {
|
|
242
|
-
expanded,
|
|
243
|
-
isError: true,
|
|
244
|
-
invalidate: () => {},
|
|
245
|
-
state,
|
|
246
|
-
} as unknown as RenderContextLike);
|
|
247
|
-
return component?.render(120).join("\n") ?? "";
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
expect(render({ timer: 1 })).toContain(diagnostic);
|
|
251
|
-
expect(render({ timer: 1 })).toContain("─");
|
|
252
|
-
expect(render({ collapsed: true })).toContain("✗ find [ in src · failed");
|
|
253
|
-
expect(render({ collapsed: true }, true)).toContain(diagnostic);
|
|
254
|
-
});
|
|
255
|
-
});
|