@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
package/src/tools/ls.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolUpdateCallback,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
LsToolInput,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { FG_DIM, RST } from "../ansi.js";
|
|
8
|
+
import { renderTree } from "../renderers.js";
|
|
9
|
+
import type {
|
|
10
|
+
LsParams,
|
|
11
|
+
PiPrettyApi,
|
|
12
|
+
RenderContextLike,
|
|
13
|
+
ThemeLike,
|
|
14
|
+
ToolFactory,
|
|
15
|
+
ToolResultLike,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
import {
|
|
18
|
+
fillToolBackground,
|
|
19
|
+
getTextContent,
|
|
20
|
+
isTextContent,
|
|
21
|
+
renderToolError,
|
|
22
|
+
setResultDetails,
|
|
23
|
+
} from "../utils.js";
|
|
24
|
+
import type { ToolContext } from "./context.js";
|
|
25
|
+
|
|
26
|
+
export function registerLsTool(
|
|
27
|
+
pi: PiPrettyApi,
|
|
28
|
+
createLsTool: ToolFactory<LsToolInput>,
|
|
29
|
+
ctx: ToolContext,
|
|
30
|
+
): void {
|
|
31
|
+
const { cwd, sp, TextComponent } = ctx;
|
|
32
|
+
const origLs = createLsTool(cwd);
|
|
33
|
+
|
|
34
|
+
pi.registerTool({
|
|
35
|
+
...origLs,
|
|
36
|
+
name: "ls",
|
|
37
|
+
|
|
38
|
+
async execute(
|
|
39
|
+
tid: string,
|
|
40
|
+
params: LsParams,
|
|
41
|
+
sig: AbortSignal | undefined,
|
|
42
|
+
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
43
|
+
toolCtx: ExtensionContext,
|
|
44
|
+
) {
|
|
45
|
+
const result = (await origLs.execute(
|
|
46
|
+
tid,
|
|
47
|
+
params,
|
|
48
|
+
sig,
|
|
49
|
+
upd,
|
|
50
|
+
toolCtx,
|
|
51
|
+
)) as ToolResultLike;
|
|
52
|
+
const textContent = getTextContent(result);
|
|
53
|
+
const fp = params.path ?? cwd;
|
|
54
|
+
const entryCount = textContent
|
|
55
|
+
? textContent.trim().split("\n").filter(Boolean).length
|
|
56
|
+
: 0;
|
|
57
|
+
|
|
58
|
+
setResultDetails(result, {
|
|
59
|
+
_type: "lsResult",
|
|
60
|
+
text: textContent ?? "",
|
|
61
|
+
path: fp,
|
|
62
|
+
entryCount,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
renderCall(args: LsParams, theme: ThemeLike, renderCtx: RenderContextLike) {
|
|
69
|
+
const fp = args.path ?? ".";
|
|
70
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
71
|
+
text.setText(
|
|
72
|
+
fillToolBackground(
|
|
73
|
+
`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
return text;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
renderResult(
|
|
80
|
+
result: ToolResultLike,
|
|
81
|
+
_opt: unknown,
|
|
82
|
+
theme: ThemeLike,
|
|
83
|
+
renderCtx: RenderContextLike,
|
|
84
|
+
) {
|
|
85
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
86
|
+
|
|
87
|
+
if (renderCtx.isError) {
|
|
88
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const d = result.details as Record<string, unknown> | undefined;
|
|
93
|
+
if (d?._type === "lsResult" && d.text) {
|
|
94
|
+
const tree = renderTree(d.text as string, d.path as string);
|
|
95
|
+
const info = `${FG_DIM}${d.entryCount} entries${RST}`;
|
|
96
|
+
text.setText(fillToolBackground(` ${info}\n${tree}`));
|
|
97
|
+
return text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const fallback = result.content?.[0];
|
|
101
|
+
const fallbackText =
|
|
102
|
+
fallback && isTextContent(fallback) ? fallback.text : "listed";
|
|
103
|
+
text.setText(
|
|
104
|
+
fillToolBackground(
|
|
105
|
+
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
return text;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
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
|
+
GrepResultDetails,
|
|
12
|
+
MultiGrepParams,
|
|
13
|
+
MultiGrepRenderState,
|
|
14
|
+
PiPrettyApi,
|
|
15
|
+
RenderContextLike,
|
|
16
|
+
ThemeLike,
|
|
17
|
+
ToolFactory,
|
|
18
|
+
ToolResultLike,
|
|
19
|
+
} from "../types.js";
|
|
20
|
+
import {
|
|
21
|
+
appendNotices,
|
|
22
|
+
buildLiteralAlternationPattern,
|
|
23
|
+
countRipgrepMatches,
|
|
24
|
+
getConstraintBackedPath,
|
|
25
|
+
getErrorMessage,
|
|
26
|
+
getTextContent,
|
|
27
|
+
isTextContent,
|
|
28
|
+
makeTextResult,
|
|
29
|
+
normalizeLineEndings,
|
|
30
|
+
shouldIgnoreCaseForPatterns,
|
|
31
|
+
termW,
|
|
32
|
+
trimToUndefined,
|
|
33
|
+
} from "../utils.js";
|
|
34
|
+
import type { ToolContext } from "./context.js";
|
|
35
|
+
|
|
36
|
+
export function registerMultiGrepTool(
|
|
37
|
+
pi: PiPrettyApi,
|
|
38
|
+
createGrepTool: ToolFactory<GrepToolInput> | null,
|
|
39
|
+
ctx: ToolContext,
|
|
40
|
+
): void {
|
|
41
|
+
const {
|
|
42
|
+
cwd,
|
|
43
|
+
sp,
|
|
44
|
+
TextComponent,
|
|
45
|
+
fffState,
|
|
46
|
+
cursorStore,
|
|
47
|
+
multiGrepRipgrepFallback,
|
|
48
|
+
} = ctx;
|
|
49
|
+
const multiGrepFallback = createGrepTool ? createGrepTool(cwd) : null;
|
|
50
|
+
|
|
51
|
+
pi.registerTool({
|
|
52
|
+
name: "multi_grep",
|
|
53
|
+
label: "multi_grep",
|
|
54
|
+
description: [
|
|
55
|
+
"Search file contents for lines matching ANY of multiple patterns (OR logic).",
|
|
56
|
+
"Uses SIMD-accelerated Aho-Corasick multi-pattern matching when FFF is available.",
|
|
57
|
+
"Falls back to ripgrep while preserving literal OR semantics and file constraints when needed.",
|
|
58
|
+
"Patterns are literal text — never escape special characters.",
|
|
59
|
+
"Use path to scope a directory/file and constraints for file filtering ('*.rs', 'src/', '!test/').",
|
|
60
|
+
].join(" "),
|
|
61
|
+
promptSnippet:
|
|
62
|
+
"Multi-pattern OR search across file contents (FFF-accelerated with grep fallback)",
|
|
63
|
+
promptGuidelines: [
|
|
64
|
+
"Use multi_grep when you need to find multiple identifiers at once (OR logic).",
|
|
65
|
+
"Include all naming conventions: snake_case, PascalCase, camelCase variants.",
|
|
66
|
+
"Patterns are literal text. Never escape special characters.",
|
|
67
|
+
"Use path to scope a directory or file when you need fresh on-disk results.",
|
|
68
|
+
"Use the constraints parameter for additional file filtering, not inside patterns.",
|
|
69
|
+
],
|
|
70
|
+
|
|
71
|
+
parameters: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
patterns: {
|
|
75
|
+
type: "array",
|
|
76
|
+
items: { type: "string" },
|
|
77
|
+
description:
|
|
78
|
+
"Patterns to search for (OR logic — matches lines containing ANY pattern).",
|
|
79
|
+
},
|
|
80
|
+
path: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description:
|
|
83
|
+
"Directory or file path to search (default: current directory)",
|
|
84
|
+
},
|
|
85
|
+
constraints: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description:
|
|
88
|
+
"File constraints, e.g. '*.{ts,tsx} !test/' to filter files.",
|
|
89
|
+
},
|
|
90
|
+
context: {
|
|
91
|
+
type: "number",
|
|
92
|
+
description:
|
|
93
|
+
"Number of context lines before and after each match (default: 0)",
|
|
94
|
+
},
|
|
95
|
+
limit: {
|
|
96
|
+
type: "number",
|
|
97
|
+
description: "Maximum number of matches to return (default: 100)",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ["patterns"],
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async execute(
|
|
104
|
+
tid: string,
|
|
105
|
+
params: MultiGrepParams,
|
|
106
|
+
sig: AbortSignal | undefined,
|
|
107
|
+
upd: unknown,
|
|
108
|
+
toolCtx: ExtensionContext,
|
|
109
|
+
) {
|
|
110
|
+
if (sig?.aborted) return makeTextResult("Aborted", {});
|
|
111
|
+
|
|
112
|
+
if (!params.patterns || params.patterns.length === 0) {
|
|
113
|
+
return makeTextResult(
|
|
114
|
+
"Error: patterns array must have at least 1 element",
|
|
115
|
+
{ error: "empty patterns" },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
120
|
+
const pattern = buildLiteralAlternationPattern(params.patterns);
|
|
121
|
+
const requestedPath = trimToUndefined(params.path);
|
|
122
|
+
const requestedConstraints = trimToUndefined(params.constraints);
|
|
123
|
+
const effectivePath =
|
|
124
|
+
requestedPath ?? getConstraintBackedPath(requestedConstraints);
|
|
125
|
+
const hasNativeConstraints = Boolean(
|
|
126
|
+
requestedPath || requestedConstraints,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// FFF path (no constraints — constrained searches use ripgrep)
|
|
130
|
+
if (
|
|
131
|
+
fffState.finder &&
|
|
132
|
+
!fffState.finder.isDestroyed &&
|
|
133
|
+
!hasNativeConstraints
|
|
134
|
+
) {
|
|
135
|
+
try {
|
|
136
|
+
const grepResult = fffState.finder.multiGrep({
|
|
137
|
+
patterns: params.patterns,
|
|
138
|
+
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
139
|
+
smartCase: true,
|
|
140
|
+
cursor: null,
|
|
141
|
+
beforeContext: params.context ?? 0,
|
|
142
|
+
afterContext: params.context ?? 0,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!grepResult.ok) {
|
|
146
|
+
return makeTextResult(`multi_grep error: ${grepResult.error}`, {
|
|
147
|
+
error: grepResult.error,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const grep = grepResult.value;
|
|
152
|
+
const notices: string[] = [];
|
|
153
|
+
if (fffState.partialIndex)
|
|
154
|
+
notices.push("Warning: partial file index");
|
|
155
|
+
if (grep.items.length >= effectiveLimit)
|
|
156
|
+
notices.push(`${effectiveLimit} limit reached`);
|
|
157
|
+
if (grep.nextCursor) {
|
|
158
|
+
const cursorId = cursorStore.store(grep.nextCursor);
|
|
159
|
+
notices.push(`More results: cursor="${cursorId}"`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const textContent = appendNotices(
|
|
163
|
+
fffFormatGrepText(grep.items, effectiveLimit),
|
|
164
|
+
notices,
|
|
165
|
+
);
|
|
166
|
+
return makeTextResult<GrepResultDetails>(textContent, {
|
|
167
|
+
_type: "grepResult",
|
|
168
|
+
text: textContent,
|
|
169
|
+
pattern,
|
|
170
|
+
matchCount: Math.min(grep.items.length, effectiveLimit),
|
|
171
|
+
});
|
|
172
|
+
} catch {
|
|
173
|
+
/* fall through to SDK */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Ripgrep path (constrained, or FFF unavailable)
|
|
178
|
+
if (requestedConstraints || !multiGrepFallback) {
|
|
179
|
+
try {
|
|
180
|
+
const pathBackedConstraint = Boolean(
|
|
181
|
+
requestedConstraints &&
|
|
182
|
+
!requestedPath &&
|
|
183
|
+
requestedConstraints === effectivePath,
|
|
184
|
+
);
|
|
185
|
+
const constraintsForRipgrep = pathBackedConstraint
|
|
186
|
+
? undefined
|
|
187
|
+
: requestedConstraints;
|
|
188
|
+
const notices: string[] = [];
|
|
189
|
+
|
|
190
|
+
if (!fffState.finder || fffState.finder.isDestroyed)
|
|
191
|
+
notices.push("FFF unavailable, used ripgrep fallback");
|
|
192
|
+
else if (hasNativeConstraints)
|
|
193
|
+
notices.push("Used ripgrep fallback for constrained search");
|
|
194
|
+
else notices.push("Used ripgrep fallback");
|
|
195
|
+
|
|
196
|
+
const rgResult = await multiGrepRipgrepFallback({
|
|
197
|
+
cwd,
|
|
198
|
+
patterns: params.patterns,
|
|
199
|
+
path: effectivePath,
|
|
200
|
+
constraints: constraintsForRipgrep,
|
|
201
|
+
ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
|
|
202
|
+
context: params.context,
|
|
203
|
+
limit: effectiveLimit,
|
|
204
|
+
signal: sig,
|
|
205
|
+
});
|
|
206
|
+
const textContent =
|
|
207
|
+
normalizeLineEndings(rgResult.text) || "No matches found";
|
|
208
|
+
if (rgResult.limitReached)
|
|
209
|
+
notices.push(`${effectiveLimit} limit reached`);
|
|
210
|
+
const finalText = appendNotices(textContent, notices);
|
|
211
|
+
|
|
212
|
+
return makeTextResult<GrepResultDetails>(finalText, {
|
|
213
|
+
_type: "grepResult",
|
|
214
|
+
text: finalText,
|
|
215
|
+
pattern,
|
|
216
|
+
matchCount: rgResult.matchCount,
|
|
217
|
+
});
|
|
218
|
+
} catch (error: unknown) {
|
|
219
|
+
const message = getErrorMessage(error);
|
|
220
|
+
return makeTextResult(`multi_grep error: ${message}`, {
|
|
221
|
+
error: message,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// SDK grep fallback
|
|
227
|
+
try {
|
|
228
|
+
const notices: string[] = [];
|
|
229
|
+
if (!fffState.finder || fffState.finder.isDestroyed)
|
|
230
|
+
notices.push("FFF unavailable, used SDK grep fallback");
|
|
231
|
+
|
|
232
|
+
const result = await multiGrepFallback.execute(
|
|
233
|
+
tid,
|
|
234
|
+
{
|
|
235
|
+
pattern,
|
|
236
|
+
path: effectivePath,
|
|
237
|
+
ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
|
|
238
|
+
context: params.context,
|
|
239
|
+
limit: params.limit,
|
|
240
|
+
},
|
|
241
|
+
sig,
|
|
242
|
+
upd as never,
|
|
243
|
+
toolCtx,
|
|
244
|
+
);
|
|
245
|
+
const textContent =
|
|
246
|
+
normalizeLineEndings(getTextContent(result)) || "No matches found";
|
|
247
|
+
const finalText = appendNotices(textContent, notices);
|
|
248
|
+
|
|
249
|
+
return makeTextResult<GrepResultDetails>(finalText, {
|
|
250
|
+
_type: "grepResult",
|
|
251
|
+
text: finalText,
|
|
252
|
+
pattern,
|
|
253
|
+
matchCount: textContent ? countRipgrepMatches(textContent) : 0,
|
|
254
|
+
});
|
|
255
|
+
} catch (error: unknown) {
|
|
256
|
+
const message = getErrorMessage(error);
|
|
257
|
+
return makeTextResult(`multi_grep error: ${message}`, {
|
|
258
|
+
error: message,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
renderCall(
|
|
264
|
+
args: MultiGrepParams,
|
|
265
|
+
theme: ThemeLike,
|
|
266
|
+
renderCtx: RenderContextLike,
|
|
267
|
+
) {
|
|
268
|
+
const patterns = args.patterns ?? [];
|
|
269
|
+
const path = args.path
|
|
270
|
+
? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
|
|
271
|
+
: "";
|
|
272
|
+
const constraints = args.constraints;
|
|
273
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
274
|
+
let content =
|
|
275
|
+
theme.fg("toolTitle", theme.bold("multi_grep")) +
|
|
276
|
+
" " +
|
|
277
|
+
theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "));
|
|
278
|
+
content += path;
|
|
279
|
+
if (constraints) content += theme.fg("muted", ` (${constraints})`);
|
|
280
|
+
text.setText(content);
|
|
281
|
+
return text;
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
renderResult(
|
|
285
|
+
result: ToolResultLike<GrepResultDetails | { error?: string }>,
|
|
286
|
+
_opt: ToolRenderResultOptions,
|
|
287
|
+
theme: ThemeLike,
|
|
288
|
+
renderCtx: RenderContextLike<MultiGrepRenderState>,
|
|
289
|
+
) {
|
|
290
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
291
|
+
|
|
292
|
+
if (renderCtx.isError) {
|
|
293
|
+
text.setText(
|
|
294
|
+
`\n${theme.fg("error", getTextContent(result) || "Error")}`,
|
|
295
|
+
);
|
|
296
|
+
return text;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const d = result.details;
|
|
300
|
+
if (d && "_type" in d && d._type === "grepResult" && d.text) {
|
|
301
|
+
const key = `mgrep:${d.pattern}:${d.matchCount}:${termW()}`;
|
|
302
|
+
if (renderCtx.state._mgk !== key) {
|
|
303
|
+
renderCtx.state._mgk = key;
|
|
304
|
+
const info = `${FG_DIM}${d.matchCount} matches${RST}`;
|
|
305
|
+
renderCtx.state._mgt = ` ${info}`;
|
|
306
|
+
|
|
307
|
+
renderGrepResults(d.text, d.pattern)
|
|
308
|
+
.then((rendered: string) => {
|
|
309
|
+
if (renderCtx.state._mgk !== key) return;
|
|
310
|
+
renderCtx.state._mgt = ` ${info}\n${rendered}`;
|
|
311
|
+
renderCtx.invalidate();
|
|
312
|
+
})
|
|
313
|
+
.catch(() => {});
|
|
314
|
+
}
|
|
315
|
+
text.setText(
|
|
316
|
+
renderCtx.state._mgt ?? ` ${FG_DIM}${d.matchCount} matches${RST}`,
|
|
317
|
+
);
|
|
318
|
+
return text;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const fallback = result.content?.[0];
|
|
322
|
+
const fallbackText =
|
|
323
|
+
fallback && isTextContent(fallback) ? fallback.text : "searched";
|
|
324
|
+
text.setText(` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`);
|
|
325
|
+
return text;
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolUpdateCallback,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ReadToolInput,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { FG_DIM, RST } from "../ansi.js";
|
|
8
|
+
import { MAX_PREVIEW_LINES } from "../config.js";
|
|
9
|
+
import { fileIcon } from "../icons.js";
|
|
10
|
+
import { renderFileContent } from "../renderers.js";
|
|
11
|
+
import type {
|
|
12
|
+
PiPrettyApi,
|
|
13
|
+
ReadParams,
|
|
14
|
+
RenderContextLike,
|
|
15
|
+
ThemeLike,
|
|
16
|
+
ToolFactory,
|
|
17
|
+
ToolResultLike,
|
|
18
|
+
} from "../types.js";
|
|
19
|
+
import {
|
|
20
|
+
fillToolBackground,
|
|
21
|
+
getTextContent,
|
|
22
|
+
humanSize,
|
|
23
|
+
isImageContent,
|
|
24
|
+
isTextContent,
|
|
25
|
+
normalizeLineEndings,
|
|
26
|
+
renderToolError,
|
|
27
|
+
setResultDetails,
|
|
28
|
+
} from "../utils.js";
|
|
29
|
+
import type { ToolContext } from "./context.js";
|
|
30
|
+
|
|
31
|
+
export function registerReadTool(
|
|
32
|
+
pi: PiPrettyApi,
|
|
33
|
+
createReadTool: ToolFactory<ReadToolInput>,
|
|
34
|
+
ctx: ToolContext,
|
|
35
|
+
): void {
|
|
36
|
+
const { cwd, sp, TextComponent } = ctx;
|
|
37
|
+
const origRead = createReadTool(cwd);
|
|
38
|
+
|
|
39
|
+
pi.registerTool({
|
|
40
|
+
...origRead,
|
|
41
|
+
name: "read",
|
|
42
|
+
|
|
43
|
+
async execute(
|
|
44
|
+
tid: string,
|
|
45
|
+
params: ReadParams,
|
|
46
|
+
sig: AbortSignal | undefined,
|
|
47
|
+
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
48
|
+
toolCtx: ExtensionContext,
|
|
49
|
+
) {
|
|
50
|
+
const result = (await origRead.execute(
|
|
51
|
+
tid,
|
|
52
|
+
params,
|
|
53
|
+
sig,
|
|
54
|
+
upd,
|
|
55
|
+
toolCtx,
|
|
56
|
+
)) as ToolResultLike;
|
|
57
|
+
|
|
58
|
+
const fp = params.path ?? "";
|
|
59
|
+
const offset = params.offset ?? 1;
|
|
60
|
+
|
|
61
|
+
const imageBlock = result.content?.find(isImageContent);
|
|
62
|
+
if (imageBlock) {
|
|
63
|
+
setResultDetails(result, {
|
|
64
|
+
_type: "readImage",
|
|
65
|
+
filePath: fp,
|
|
66
|
+
data: imageBlock.data,
|
|
67
|
+
mimeType: imageBlock.mimeType ?? "image/png",
|
|
68
|
+
});
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const textContent = getTextContent(result);
|
|
73
|
+
if (textContent && fp) {
|
|
74
|
+
const normalizedContent = normalizeLineEndings(textContent);
|
|
75
|
+
const lineCount = normalizedContent.split("\n").length;
|
|
76
|
+
setResultDetails(result, {
|
|
77
|
+
_type: "readFile",
|
|
78
|
+
filePath: fp,
|
|
79
|
+
content: normalizedContent,
|
|
80
|
+
offset,
|
|
81
|
+
lineCount,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
renderCall(
|
|
89
|
+
args: ReadParams,
|
|
90
|
+
theme: ThemeLike,
|
|
91
|
+
renderCtx: RenderContextLike,
|
|
92
|
+
) {
|
|
93
|
+
const fp = args.path ?? "";
|
|
94
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
95
|
+
const offset = args.offset
|
|
96
|
+
? ` ${theme.fg("muted", `from line ${args.offset}`)}`
|
|
97
|
+
: "";
|
|
98
|
+
const limit = args.limit
|
|
99
|
+
? ` ${theme.fg("muted", `(${args.limit} lines)`)}`
|
|
100
|
+
: "";
|
|
101
|
+
text.setText(
|
|
102
|
+
fillToolBackground(
|
|
103
|
+
`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
return text;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
renderResult(
|
|
110
|
+
result: ToolResultLike,
|
|
111
|
+
_opt: unknown,
|
|
112
|
+
theme: ThemeLike,
|
|
113
|
+
renderCtx: RenderContextLike,
|
|
114
|
+
) {
|
|
115
|
+
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
116
|
+
|
|
117
|
+
if (renderCtx.isError) {
|
|
118
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const d = result.details as Record<string, unknown> | undefined;
|
|
123
|
+
|
|
124
|
+
if (d?._type === "readImage") {
|
|
125
|
+
const byteSize = Math.ceil(((d.data as string).length * 3) / 4);
|
|
126
|
+
text.setText(
|
|
127
|
+
fillToolBackground(
|
|
128
|
+
` ${fileIcon(d.filePath as string)}${FG_DIM}${d.mimeType ?? "image"} · ${humanSize(byteSize)}${RST}`,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
return text;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (d?._type === "readFile" && d.content) {
|
|
135
|
+
const key = `read:${d.filePath}:${d.offset}:${d.lineCount}:${process.stdout.columns ?? 80}`;
|
|
136
|
+
if (renderCtx.state._rk !== key) {
|
|
137
|
+
renderCtx.state._rk = key;
|
|
138
|
+
const info = `${FG_DIM}${d.lineCount} lines${RST}`;
|
|
139
|
+
renderCtx.state._rt = fillToolBackground(` ${info}`);
|
|
140
|
+
|
|
141
|
+
const maxShow = renderCtx.expanded
|
|
142
|
+
? (d.lineCount as number)
|
|
143
|
+
: MAX_PREVIEW_LINES;
|
|
144
|
+
renderFileContent(
|
|
145
|
+
d.content as string,
|
|
146
|
+
d.filePath as string,
|
|
147
|
+
d.offset as number,
|
|
148
|
+
maxShow,
|
|
149
|
+
)
|
|
150
|
+
.then((rendered: string) => {
|
|
151
|
+
if (renderCtx.state._rk !== key) return;
|
|
152
|
+
renderCtx.state._rt = fillToolBackground(
|
|
153
|
+
` ${info}\n${rendered}`,
|
|
154
|
+
);
|
|
155
|
+
renderCtx.invalidate();
|
|
156
|
+
})
|
|
157
|
+
.catch(() => {});
|
|
158
|
+
}
|
|
159
|
+
text.setText(
|
|
160
|
+
renderCtx.state._rt ??
|
|
161
|
+
fillToolBackground(` ${FG_DIM}${d.lineCount} lines${RST}`),
|
|
162
|
+
);
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fallback = result.content?.[0];
|
|
167
|
+
const fallbackText =
|
|
168
|
+
fallback && isTextContent(fallback) ? fallback.text : "read";
|
|
169
|
+
text.setText(
|
|
170
|
+
fillToolBackground(
|
|
171
|
+
` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
return text;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|