@heyhuynhgiabuu/pi-pretty 0.6.7 → 0.6.9

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.
@@ -1,12 +1,26 @@
1
1
  /* pi-pretty: multi_grep tool -- FFF-backed multi-pattern search with ripgrep/SDK fallback. */
2
2
 
3
- import { type ToolDefinition, type ExtensionAPI, type ExtensionContext, type AgentToolResult } from "@earendil-works/pi-coding-agent";
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 { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
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 () => ({ text: "", matchCount: 0, limitReached: false });
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
- parameters: Type.Object({
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 { content: [{ text: "patterns array must have at least 1 element", type: "text" as const }], details: { _type: "grepResult" } as GrepDetails };
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 { content: [{ text: "Aborted", type: "text" as const }], details: { _type: "grepResult" } as GrepDetails };
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 { content: [{ type: "text" as const, text }], details: { _type: "grepResult", text, pattern: alternationPattern, matchCount: items.length } as GrepDetails };
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 { content: [{ type: "text" as const, text: grepResult.error || "multi_grep failed" }], details: { _type: "grepResult", text: "", pattern: alternationPattern, matchCount: 0 } as GrepDetails };
99
- } catch { /* fall through */ }
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(requestedConstraints && requestedPath && !Boolean(p.path) && !requestedConstraints.includes("*") && !requestedConstraints.includes("?"));
106
- const constraintsForRg = pathBacked ? undefined : requestedConstraints;
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, patterns, path: effectivePath, constraints: constraintsForRg,
114
- ignoreCase: shouldIgnoreCase(patterns), context, limit: effectiveLimit, signal: sig,
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 { content: [{ type: "text" as const, text: appendNotices(text, notices) }], details: { _type: "grepResult", text, pattern: alternationPattern, matchCount: rgResult.matchCount } as GrepDetails };
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 { content: [{ type: "text" as const, text: `multi_grep error: ${error instanceof Error ? error.message : String(error)}` }], details: { _type: "grepResult", text: "", pattern: alternationPattern, matchCount: 0 } as GrepDetails };
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(tid, { pattern: alternationPattern, path: effectivePath, ignoreCase: shouldIgnoreCase(patterns), context, limit: effectiveLimit }, sig, null, ctx) as Result;
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 = { _type: "grepResult", text: tc, pattern: alternationPattern, matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0 } as GrepDetails;
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 { content: [{ type: "text" as const, text: `multi_grep error: ${error instanceof Error ? error.message : String(error)}` }], details: { _type: "grepResult", text: "", pattern: alternationPattern, matchCount: 0 } as GrepDetails };
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 = args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.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 = patterns.length === 0 ? "" : patterns.length === 1 ? patterns[0]! : patterns.length === 2 ? `${patterns[0]}|${patterns[1]}` : `${patterns[0]}|${patterns[1]}|+${patterns.length - 2}`;
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) { text.setText(renderToolError(getText(result) || "Error", theme)); return text; }
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 { hlRe = new RegExp(`(${d.pattern})`, "gi"); } catch {}
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(fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}\n${preview}${more}`));
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(fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches")}`));
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 { return !patterns.some((p) => /[A-Z]/.test(p)); }
200
- function appendNotices(text: string, notices: string[]): string { return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text; }
201
- function getText(result: Result): string { return ((result.content ?? []) as TextContent[]).filter((c) => c.type === "text").map((c) => c.text).join("\n") ?? ""; }
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 { type ToolDefinition, type ExtensionAPI, type ExtensionContext, type AgentToolResult } from "@earendil-works/pi-coding-agent";
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 { resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST } from "../config.js";
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 ["ghostty", "kitty", "iterm.app", "wezterm", "mintty"].some((t) => term.includes(t)) || process.env.LC_TERMINAL === "iTerm2";
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 = { _type: "readImage", filePath: String(p.path ?? ""), data: imageBlock.data, mimeType: imageBlock.mimeType ?? "image/png" } as ReadDetails;
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 = { _type: "readFile", filePath: String(p.path ?? ""), content: tc, offset: typeof p.offset === "number" ? p.offset : 0, lineCount: tc ? tc.split("\n").length : 0 } as ReadDetails;
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) { text.setText(renderToolError(getText(result) || "Error", theme)); return text; }
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 (t?: string, x?: number, y?: number) => ComponentLike;
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 { /* fall through */ }
120
+ } catch {
121
+ /* fall through */
122
+ }
87
123
  }
88
124
  const fc = result.content?.[0];
89
- text.setText(fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 80) : `[image: ${d.filePath}]`)}`));
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(` ${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`);
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).then(hl => {
126
- const padded = hl.split("\n").map(l => ` ${l}`).join("\n");
127
- const rendered = `\n ${header}\n${padded}\n`;
128
- text.setText(fillToolBackground(rendered));
129
- (ctx as any).state._rt = rendered;
130
- }).catch(() => {});
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(fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`));
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 ((result.content ?? []) as TextContent[]).filter((c) => c.type === "text").map((c) => c.text).join("\n") ?? "";
192
+ return (
193
+ ((result.content ?? []) as TextContent[])
194
+ .filter((c) => c.type === "text")
195
+ .map((c) => c.text)
196
+ .join("\n") ?? ""
197
+ );
144
198
  }
@@ -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
+ }