@heyhuynhgiabuu/pi-pretty 0.6.7 → 0.6.8
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/CHANGELOG.md +14 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +37 -15
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +38 -16
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +2 -5
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +20 -9
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/multi-grep.d.ts.map +1 -1
- package/dist/tools/multi-grep.js +118 -22
- package/dist/tools/multi-grep.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +33 -13
- package/dist/tools/read.js.map +1 -1
- package/dist/tui-text.d.ts +25 -0
- package/dist/tui-text.d.ts.map +1 -0
- package/dist/tui-text.js +55 -0
- package/dist/tui-text.js.map +1 -0
- package/package.json +1 -1
- package/src/tools/bash.ts +77 -22
- package/src/tools/find.ts +91 -40
- package/src/tools/grep.ts +2 -8
- package/src/tools/ls.ts +42 -14
- package/src/tools/multi-grep.ts +162 -32
- package/src/tools/read.ts +79 -25
- package/src/tui-text.ts +56 -0
package/src/tools/multi-grep.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
/* pi-pretty: multi_grep tool -- FFF-backed multi-pattern search with ripgrep/SDK fallback. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ToolDefinition,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
type AgentToolResult,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4
9
|
import { Type } from "typebox";
|
|
5
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
SdkToolDef,
|
|
12
|
+
GrepDetails,
|
|
13
|
+
FffServiceWithCursor,
|
|
14
|
+
TextContent,
|
|
15
|
+
ComponentLike,
|
|
16
|
+
ThemeLike,
|
|
17
|
+
RenderCtxLike,
|
|
18
|
+
} from "../types.js";
|
|
6
19
|
import { resolveBaseBackground, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST } from "../config.js";
|
|
7
20
|
import { shortPath, normalizeLineEndings } from "../helpers.js";
|
|
8
21
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
9
22
|
import { renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
23
|
+
import { resolveTextCtor } from "../tui-text.js";
|
|
10
24
|
import { fffFormatGrepText } from "../fff-helpers.js";
|
|
11
25
|
import { parseMultiGrepConstraints } from "../multi-grep-fallback.js";
|
|
12
26
|
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
@@ -14,7 +28,11 @@ import type { MultiGrepFallback } from "../types.js";
|
|
|
14
28
|
|
|
15
29
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
16
30
|
|
|
17
|
-
const noopFallback: MultiGrepFallback = async () => ({
|
|
31
|
+
const noopFallback: MultiGrepFallback = async () => ({
|
|
32
|
+
text: "",
|
|
33
|
+
matchCount: 0,
|
|
34
|
+
limitReached: false,
|
|
35
|
+
});
|
|
18
36
|
|
|
19
37
|
export function registerMultiGrepTool(
|
|
20
38
|
pi: ExtensionAPI,
|
|
@@ -24,17 +42,14 @@ export function registerMultiGrepTool(
|
|
|
24
42
|
ripgrepFallback: MultiGrepFallback = noopFallback,
|
|
25
43
|
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
26
44
|
): void {
|
|
27
|
-
const TC = TextComp
|
|
28
|
-
const { Text } = require("@earendil-works/pi-tui") as { Text: new (t?: string, x?: number, y?: number) => { setText(v: string): void } };
|
|
29
|
-
return Text;
|
|
30
|
-
})();
|
|
45
|
+
const TC = resolveTextCtor(TextComp);
|
|
31
46
|
const home = process.env.HOME ?? "";
|
|
32
47
|
|
|
33
48
|
pi.registerTool({
|
|
34
49
|
name: "multi_grep",
|
|
35
50
|
label: "Multi Grep",
|
|
36
51
|
description: "Search file contents using multiple patterns (OR logic)",
|
|
37
|
-
|
|
52
|
+
parameters: Type.Object({
|
|
38
53
|
patterns: Type.Array(Type.String()),
|
|
39
54
|
path: Type.Optional(Type.String()),
|
|
40
55
|
constraints: Type.Optional(Type.String()),
|
|
@@ -49,12 +64,18 @@ export function registerMultiGrepTool(
|
|
|
49
64
|
|
|
50
65
|
// Guard: empty patterns
|
|
51
66
|
if (!patterns.length || (patterns.length === 1 && !patterns[0])) {
|
|
52
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ text: "patterns array must have at least 1 element", type: "text" as const }],
|
|
69
|
+
details: { _type: "grepResult" } as GrepDetails,
|
|
70
|
+
};
|
|
53
71
|
}
|
|
54
72
|
|
|
55
73
|
// Guard: aborted signal
|
|
56
74
|
if (sig?.aborted) {
|
|
57
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ text: "Aborted", type: "text" as const }],
|
|
77
|
+
details: { _type: "grepResult" } as GrepDetails,
|
|
78
|
+
};
|
|
58
79
|
}
|
|
59
80
|
const constraintsStr = p.constraints ? String(p.constraints) : undefined;
|
|
60
81
|
const context = typeof p.context === "number" ? p.context : undefined;
|
|
@@ -92,32 +113,83 @@ export function registerMultiGrepTool(
|
|
|
92
113
|
notices.push(`More results available: cursor="${cursorId}"`);
|
|
93
114
|
}
|
|
94
115
|
const text = appendNotices(fffFormatGrepText(items, effectiveLimit), notices);
|
|
95
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text" as const, text }],
|
|
118
|
+
details: {
|
|
119
|
+
_type: "grepResult",
|
|
120
|
+
text,
|
|
121
|
+
pattern: alternationPattern,
|
|
122
|
+
matchCount: items.length,
|
|
123
|
+
} as GrepDetails,
|
|
124
|
+
};
|
|
96
125
|
}
|
|
97
126
|
// FFF failure -> return error directly
|
|
98
|
-
return {
|
|
99
|
-
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text" as const, text: grepResult.error || "multi_grep failed" }],
|
|
129
|
+
details: {
|
|
130
|
+
_type: "grepResult",
|
|
131
|
+
text: "",
|
|
132
|
+
pattern: alternationPattern,
|
|
133
|
+
matchCount: 0,
|
|
134
|
+
} as GrepDetails,
|
|
135
|
+
};
|
|
136
|
+
} catch {
|
|
137
|
+
/* fall through */
|
|
138
|
+
}
|
|
100
139
|
}
|
|
101
140
|
|
|
102
141
|
// 2. Ripgrep fallback
|
|
103
142
|
if (requestedConstraints || !sdkGrepTool) {
|
|
104
143
|
try {
|
|
105
|
-
const pathBacked = Boolean(
|
|
106
|
-
|
|
144
|
+
const pathBacked = Boolean(
|
|
145
|
+
requestedConstraints &&
|
|
146
|
+
requestedPath &&
|
|
147
|
+
!Boolean(p.path) &&
|
|
148
|
+
!requestedConstraints.includes("*") &&
|
|
149
|
+
!requestedConstraints.includes("?"),
|
|
150
|
+
);
|
|
151
|
+
const constraintsForRg = pathBacked ? undefined : requestedConstraints;
|
|
107
152
|
const notices: string[] = [];
|
|
108
153
|
if (!fffService?.isAvailable) notices.push("FFF unavailable, used ripgrep fallback");
|
|
109
154
|
else if (hasNativeConstraints) notices.push("Used ripgrep fallback for constrained search");
|
|
110
155
|
else notices.push("Used ripgrep fallback");
|
|
111
156
|
|
|
112
157
|
const rgResult = await ripgrepFallback({
|
|
113
|
-
cwd,
|
|
114
|
-
|
|
158
|
+
cwd,
|
|
159
|
+
patterns,
|
|
160
|
+
path: effectivePath,
|
|
161
|
+
constraints: constraintsForRg,
|
|
162
|
+
ignoreCase: shouldIgnoreCase(patterns),
|
|
163
|
+
context,
|
|
164
|
+
limit: effectiveLimit,
|
|
165
|
+
signal: sig,
|
|
115
166
|
});
|
|
116
167
|
const text = normalizeLineEndings(rgResult.text) || "No matches found";
|
|
117
168
|
if (rgResult.limitReached) notices.push(`${effectiveLimit} limit reached`);
|
|
118
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: "text" as const, text: appendNotices(text, notices) }],
|
|
171
|
+
details: {
|
|
172
|
+
_type: "grepResult",
|
|
173
|
+
text,
|
|
174
|
+
pattern: alternationPattern,
|
|
175
|
+
matchCount: rgResult.matchCount,
|
|
176
|
+
} as GrepDetails,
|
|
177
|
+
};
|
|
119
178
|
} catch (error: unknown) {
|
|
120
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "text" as const,
|
|
183
|
+
text: `multi_grep error: ${error instanceof Error ? error.message : String(error)}`,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
details: {
|
|
187
|
+
_type: "grepResult",
|
|
188
|
+
text: "",
|
|
189
|
+
pattern: alternationPattern,
|
|
190
|
+
matchCount: 0,
|
|
191
|
+
} as GrepDetails,
|
|
192
|
+
};
|
|
121
193
|
}
|
|
122
194
|
}
|
|
123
195
|
|
|
@@ -125,13 +197,43 @@ export function registerMultiGrepTool(
|
|
|
125
197
|
try {
|
|
126
198
|
const notices: string[] = [];
|
|
127
199
|
if (!fffService?.isAvailable) notices.push("FFF unavailable, used SDK grep fallback");
|
|
128
|
-
const result = await sdkGrepTool.execute(
|
|
200
|
+
const result = (await sdkGrepTool.execute(
|
|
201
|
+
tid,
|
|
202
|
+
{
|
|
203
|
+
pattern: alternationPattern,
|
|
204
|
+
path: effectivePath,
|
|
205
|
+
ignoreCase: shouldIgnoreCase(patterns),
|
|
206
|
+
context,
|
|
207
|
+
limit: effectiveLimit,
|
|
208
|
+
},
|
|
209
|
+
sig,
|
|
210
|
+
null,
|
|
211
|
+
ctx,
|
|
212
|
+
)) as Result;
|
|
129
213
|
const tc = getText(result);
|
|
130
214
|
result.content = [{ type: "text" as const, text: appendNotices(tc, notices) }];
|
|
131
|
-
result.details = {
|
|
215
|
+
result.details = {
|
|
216
|
+
_type: "grepResult",
|
|
217
|
+
text: tc,
|
|
218
|
+
pattern: alternationPattern,
|
|
219
|
+
matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
|
|
220
|
+
} as GrepDetails;
|
|
132
221
|
return result;
|
|
133
222
|
} catch (error: unknown) {
|
|
134
|
-
return {
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: "text" as const,
|
|
227
|
+
text: `multi_grep error: ${error instanceof Error ? error.message : String(error)}`,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
details: {
|
|
231
|
+
_type: "grepResult",
|
|
232
|
+
text: "",
|
|
233
|
+
pattern: alternationPattern,
|
|
234
|
+
matchCount: 0,
|
|
235
|
+
} as GrepDetails,
|
|
236
|
+
};
|
|
135
237
|
}
|
|
136
238
|
}),
|
|
137
239
|
|
|
@@ -140,9 +242,17 @@ export function registerMultiGrepTool(
|
|
|
140
242
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
141
243
|
const patterns: string[] = Array.isArray(args.patterns) ? args.patterns.map((p: unknown) => String(p)) : [];
|
|
142
244
|
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
143
|
-
const path =
|
|
245
|
+
const path =
|
|
246
|
+
args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.path));
|
|
144
247
|
const literal = args.literal === true;
|
|
145
|
-
const patternStr =
|
|
248
|
+
const patternStr =
|
|
249
|
+
patterns.length === 0
|
|
250
|
+
? ""
|
|
251
|
+
: patterns.length === 1
|
|
252
|
+
? patterns[0]!
|
|
253
|
+
: patterns.length === 2
|
|
254
|
+
? `${patterns[0]}|${patterns[1]}`
|
|
255
|
+
: `${patterns[0]}|${patterns[1]}|+${patterns.length - 2}`;
|
|
146
256
|
let out = `${theme.fg("toolTitle", theme.bold("mgrep"))} ${theme.fg("accent", `/${patternStr || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
|
|
147
257
|
if (literal) out += theme.fg("dim", ` (literal)`);
|
|
148
258
|
if (limit !== undefined) out += theme.fg("dim", ` limit ${limit}`);
|
|
@@ -152,9 +262,12 @@ export function registerMultiGrepTool(
|
|
|
152
262
|
|
|
153
263
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
154
264
|
resolveBaseBackground(theme);
|
|
155
|
-
|
|
265
|
+
|
|
156
266
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
157
|
-
if (ctx.isError) {
|
|
267
|
+
if (ctx.isError) {
|
|
268
|
+
text.setText(renderToolError(getText(result) || "Error", theme));
|
|
269
|
+
return text;
|
|
270
|
+
}
|
|
158
271
|
const d = result.details as GrepDetails | undefined;
|
|
159
272
|
if (d?._type === "grepResult" && d.text) {
|
|
160
273
|
const lines = d.text.split("\n");
|
|
@@ -163,7 +276,9 @@ export function registerMultiGrepTool(
|
|
|
163
276
|
const nw = Math.max(3, 5);
|
|
164
277
|
|
|
165
278
|
let hlRe: RegExp | null = null;
|
|
166
|
-
try {
|
|
279
|
+
try {
|
|
280
|
+
hlRe = new RegExp(`(${d.pattern})`, "gi");
|
|
281
|
+
} catch {}
|
|
167
282
|
|
|
168
283
|
const out: string[] = [];
|
|
169
284
|
let currentFile = "";
|
|
@@ -186,16 +301,31 @@ export function registerMultiGrepTool(
|
|
|
186
301
|
}
|
|
187
302
|
const preview = out.join("\n");
|
|
188
303
|
const more = lines.length > maxShow ? `\n${FG_DIM} ... ${lines.length - maxShow} more lines${RST}` : "";
|
|
189
|
-
text.setText(
|
|
304
|
+
text.setText(
|
|
305
|
+
fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}\n${preview}${more}`),
|
|
306
|
+
);
|
|
190
307
|
return text;
|
|
191
308
|
}
|
|
192
309
|
const fc = result.content?.[0];
|
|
193
|
-
text.setText(
|
|
310
|
+
text.setText(
|
|
311
|
+
fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches")}`),
|
|
312
|
+
);
|
|
194
313
|
return text;
|
|
195
314
|
},
|
|
196
315
|
} as unknown as ToolDefinition<any, any, any>);
|
|
197
316
|
}
|
|
198
317
|
|
|
199
|
-
function shouldIgnoreCase(patterns: string[]): boolean {
|
|
200
|
-
|
|
201
|
-
|
|
318
|
+
function shouldIgnoreCase(patterns: string[]): boolean {
|
|
319
|
+
return !patterns.some((p) => /[A-Z]/.test(p));
|
|
320
|
+
}
|
|
321
|
+
function appendNotices(text: string, notices: string[]): string {
|
|
322
|
+
return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text;
|
|
323
|
+
}
|
|
324
|
+
function getText(result: Result): string {
|
|
325
|
+
return (
|
|
326
|
+
((result.content ?? []) as TextContent[])
|
|
327
|
+
.filter((c) => c.type === "text")
|
|
328
|
+
.map((c) => c.text)
|
|
329
|
+
.join("\n") ?? ""
|
|
330
|
+
);
|
|
331
|
+
}
|
package/src/tools/read.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
/* pi-pretty: read tool -- file reading with syntax highlighting and inline image support. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ToolDefinition,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
type AgentToolResult,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4
9
|
import type { SdkToolDef, ReadDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
resolveBaseBackground,
|
|
12
|
+
termWidth,
|
|
13
|
+
MAX_PREVIEW_LINES,
|
|
14
|
+
BG_BASE,
|
|
15
|
+
BG_ERROR,
|
|
16
|
+
FG_DIM,
|
|
17
|
+
FG_LNUM,
|
|
18
|
+
FG_RULE,
|
|
19
|
+
RST,
|
|
20
|
+
} from "../config.js";
|
|
6
21
|
import { shortPath, normalizeLineEndings } from "../helpers.js";
|
|
7
22
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
8
23
|
import { renderToolError, renderToolMetrics, fillToolBackground, renderFileContent } from "../render.js";
|
|
24
|
+
import { resolveTextCtor } from "../tui-text.js";
|
|
9
25
|
|
|
10
26
|
// Simple terminal image support check
|
|
11
27
|
function isImageTerminal(): boolean {
|
|
@@ -13,7 +29,10 @@ function isImageTerminal(): boolean {
|
|
|
13
29
|
const proto = (process.env.PRETTY_IMAGE_PROTOCOL ?? "").toLowerCase();
|
|
14
30
|
if (proto === "kitty" || proto === "iterm2") return true;
|
|
15
31
|
if (proto === "none") return false;
|
|
16
|
-
return
|
|
32
|
+
return (
|
|
33
|
+
["ghostty", "kitty", "iterm.app", "wezterm", "mintty"].some((t) => term.includes(t)) ||
|
|
34
|
+
process.env.LC_TERMINAL === "iTerm2"
|
|
35
|
+
);
|
|
17
36
|
}
|
|
18
37
|
|
|
19
38
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
@@ -25,10 +44,7 @@ export function registerReadTool(
|
|
|
25
44
|
sdkTool: SdkToolDef,
|
|
26
45
|
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
27
46
|
): void {
|
|
28
|
-
const TC = TextComp
|
|
29
|
-
const { Text } = require("@earendil-works/pi-tui") as { Text: new (t?: string, x?: number, y?: number) => { setText(v: string): void } };
|
|
30
|
-
return Text;
|
|
31
|
-
})();
|
|
47
|
+
const TC = resolveTextCtor(TextComp);
|
|
32
48
|
const home = process.env.HOME ?? "";
|
|
33
49
|
|
|
34
50
|
pi.registerTool({
|
|
@@ -40,22 +56,33 @@ export function registerReadTool(
|
|
|
40
56
|
|
|
41
57
|
execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
|
|
42
58
|
const p = params as any;
|
|
43
|
-
const result = await sdkTool.execute(tid, p, sig, undefined, ctx) as Result;
|
|
59
|
+
const result = (await sdkTool.execute(tid, p, sig, undefined, ctx)) as Result;
|
|
44
60
|
|
|
45
61
|
const imageBlock = (result.content as any[])?.find((c: any) => c.type === "image");
|
|
46
62
|
if (imageBlock) {
|
|
47
|
-
result.details = {
|
|
63
|
+
result.details = {
|
|
64
|
+
_type: "readImage",
|
|
65
|
+
filePath: String(p.path ?? ""),
|
|
66
|
+
data: imageBlock.data,
|
|
67
|
+
mimeType: imageBlock.mimeType ?? "image/png",
|
|
68
|
+
} as ReadDetails;
|
|
48
69
|
return result;
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
const tc = normalizeLineEndings(getText(result));
|
|
52
|
-
result.details = {
|
|
73
|
+
result.details = {
|
|
74
|
+
_type: "readFile",
|
|
75
|
+
filePath: String(p.path ?? ""),
|
|
76
|
+
content: tc,
|
|
77
|
+
offset: typeof p.offset === "number" ? p.offset : 0,
|
|
78
|
+
lineCount: tc ? tc.split("\n").length : 0,
|
|
79
|
+
} as ReadDetails;
|
|
53
80
|
return result;
|
|
54
81
|
}),
|
|
55
82
|
|
|
56
83
|
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
57
84
|
resolveBaseBackground(theme);
|
|
58
|
-
|
|
85
|
+
|
|
59
86
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
60
87
|
text.setText("");
|
|
61
88
|
return text;
|
|
@@ -63,10 +90,13 @@ export function registerReadTool(
|
|
|
63
90
|
|
|
64
91
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
65
92
|
resolveBaseBackground(theme);
|
|
66
|
-
|
|
93
|
+
|
|
67
94
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
68
95
|
|
|
69
|
-
if (ctx.isError) {
|
|
96
|
+
if (ctx.isError) {
|
|
97
|
+
text.setText(renderToolError(getText(result) || "Error", theme));
|
|
98
|
+
return text;
|
|
99
|
+
}
|
|
70
100
|
|
|
71
101
|
const d = result.details as ReadDetails | undefined;
|
|
72
102
|
|
|
@@ -74,7 +104,11 @@ export function registerReadTool(
|
|
|
74
104
|
if (d?._type === "readImage") {
|
|
75
105
|
if ((ctx as any).showImages && isImageTerminal()) {
|
|
76
106
|
try {
|
|
77
|
-
const T = require("@earendil-works/pi-tui").Text as new (
|
|
107
|
+
const T = require("@earendil-works/pi-tui").Text as new (
|
|
108
|
+
t?: string,
|
|
109
|
+
x?: number,
|
|
110
|
+
y?: number,
|
|
111
|
+
) => ComponentLike;
|
|
78
112
|
const img = new T("", 0, 0);
|
|
79
113
|
if (d.mimeType.startsWith("image/svg")) {
|
|
80
114
|
img.setText(d.data);
|
|
@@ -83,10 +117,16 @@ export function registerReadTool(
|
|
|
83
117
|
img.setText(`\x1b_Ga=T,f=100,m=${d.mimeType === "image/png" ? "1" : "0"};${pngData}\x1b\\\\`);
|
|
84
118
|
}
|
|
85
119
|
return img;
|
|
86
|
-
} catch {
|
|
120
|
+
} catch {
|
|
121
|
+
/* fall through */
|
|
122
|
+
}
|
|
87
123
|
}
|
|
88
124
|
const fc = result.content?.[0];
|
|
89
|
-
text.setText(
|
|
125
|
+
text.setText(
|
|
126
|
+
fillToolBackground(
|
|
127
|
+
` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 80) : `[image: ${d.filePath}]`)}`,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
90
130
|
return text;
|
|
91
131
|
}
|
|
92
132
|
|
|
@@ -111,7 +151,9 @@ export function registerReadTool(
|
|
|
111
151
|
const code = show[i] ?? "";
|
|
112
152
|
const display = code.length > cw ? code.slice(0, cw) + `${FG_DIM}›${RST}` : code;
|
|
113
153
|
const lineNo = String(ln);
|
|
114
|
-
out.push(
|
|
154
|
+
out.push(
|
|
155
|
+
` ${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
|
|
156
|
+
);
|
|
115
157
|
}
|
|
116
158
|
if (total > maxShow) {
|
|
117
159
|
out.push(` ${FG_DIM} … ${total - maxShow} more lines (${total} total)${RST}`);
|
|
@@ -122,23 +164,35 @@ export function registerReadTool(
|
|
|
122
164
|
(ctx as any).state._rt = rendered;
|
|
123
165
|
|
|
124
166
|
// Async syntax highlighting via Shiki
|
|
125
|
-
renderFileContent(d.content, d.filePath, d.offset || 0, maxShow, tw)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
167
|
+
renderFileContent(d.content, d.filePath, d.offset || 0, maxShow, tw)
|
|
168
|
+
.then((hl) => {
|
|
169
|
+
const padded = hl
|
|
170
|
+
.split("\n")
|
|
171
|
+
.map((l) => ` ${l}`)
|
|
172
|
+
.join("\n");
|
|
173
|
+
const rendered = `\n ${header}\n${padded}\n`;
|
|
174
|
+
text.setText(fillToolBackground(rendered));
|
|
175
|
+
(ctx as any).state._rt = rendered;
|
|
176
|
+
})
|
|
177
|
+
.catch(() => {});
|
|
131
178
|
|
|
132
179
|
return text;
|
|
133
180
|
}
|
|
134
181
|
|
|
135
182
|
const fc = result.content?.[0];
|
|
136
|
-
text.setText(
|
|
183
|
+
text.setText(
|
|
184
|
+
fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
|
|
185
|
+
);
|
|
137
186
|
return text;
|
|
138
187
|
},
|
|
139
188
|
} as unknown as ToolDefinition<any, any, any>);
|
|
140
189
|
}
|
|
141
190
|
|
|
142
191
|
function getText(result: Result): string {
|
|
143
|
-
return (
|
|
192
|
+
return (
|
|
193
|
+
((result.content ?? []) as TextContent[])
|
|
194
|
+
.filter((c) => c.type === "text")
|
|
195
|
+
.map((c) => c.text)
|
|
196
|
+
.join("\n") ?? ""
|
|
197
|
+
);
|
|
144
198
|
}
|
package/src/tui-text.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy resolver for pi-tui Text constructor.
|
|
3
|
+
*
|
|
4
|
+
* Returns the real Text class from @earendil-works/pi-tui, or a stub when
|
|
5
|
+
* pi-tui is unavailable (e.g. not in jiti's alias map). This avoids crashing
|
|
6
|
+
* during tool registration when pi-tui isn't aliased.
|
|
7
|
+
*
|
|
8
|
+
* The resolution is cached — the require() call happens at most once.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
type TextCtor = new (t?: string, x?: number, y?: number) => { setText(v: string): void; [k: string]: any };
|
|
13
|
+
|
|
14
|
+
/** No-op stub that satisfies the Text interface so rendering doesn't crash. */
|
|
15
|
+
class StubText {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
+
constructor(_t?: string, _x?: number, _y?: number) {
|
|
18
|
+
// no-op
|
|
19
|
+
}
|
|
20
|
+
setText(_v: string): void {
|
|
21
|
+
// no-op — pi-tui not available
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let _ctor: TextCtor | null = null;
|
|
26
|
+
let _resolved = false;
|
|
27
|
+
|
|
28
|
+
function resolve(): TextCtor {
|
|
29
|
+
if (_resolved) return _ctor!;
|
|
30
|
+
_resolved = true;
|
|
31
|
+
try {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
33
|
+
const { Text } = require("@earendil-works/pi-tui") as { Text: TextCtor };
|
|
34
|
+
_ctor = Text;
|
|
35
|
+
return _ctor;
|
|
36
|
+
} catch {
|
|
37
|
+
_ctor = StubText;
|
|
38
|
+
return _ctor;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns a Text constructor, always valid. Falls back to StubText if
|
|
44
|
+
* @earendil-works/pi-tui is unavailable (caught and cached).
|
|
45
|
+
*/
|
|
46
|
+
export function getTextCtor(): TextCtor {
|
|
47
|
+
return resolve();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns TextComp if provided, otherwise the lazy-resolved Text constructor.
|
|
52
|
+
* Always returns a valid constructor (never undefined/null).
|
|
53
|
+
*/
|
|
54
|
+
export function resolveTextCtor(TextComp?: TextCtor): TextCtor {
|
|
55
|
+
return TextComp ?? resolve();
|
|
56
|
+
}
|