@heyhuynhgiabuu/pi-pretty 0.6.6 → 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/src/render.ts CHANGED
@@ -7,22 +7,48 @@
7
7
 
8
8
  import type { BundledLanguage } from "shiki";
9
9
  import { codeToANSI } from "@shikijs/cli";
10
- import { truncateToWidth } from "@earendil-works/pi-tui";
11
10
  import { basename, dirname } from "node:path";
12
11
 
13
12
  import {
14
- RST, FG_LNUM, FG_DIM, FG_RULE, FG_GREEN, FG_RED, FG_YELLOW, FG_BLUE,
15
- BG_BASE, BG_ERROR,
16
- dirIcon, detectLang, termWidth, MAX_PREVIEW_LINES, MAX_HL_CHARS, CACHE_LIMIT,
13
+ RST,
14
+ FG_LNUM,
15
+ FG_DIM,
16
+ FG_RULE,
17
+ FG_GREEN,
18
+ FG_RED,
19
+ FG_YELLOW,
20
+ FG_BLUE,
21
+ BG_BASE,
22
+ BG_ERROR,
23
+ dirIcon,
24
+ detectLang,
25
+ termWidth,
26
+ MAX_PREVIEW_LINES,
27
+ MAX_HL_CHARS,
28
+ CACHE_LIMIT,
17
29
  resolveBaseBackground,
18
30
  } from "./config.js";
19
31
  import {
20
- normalizeLineEndings, humanSize, formatElapsedMs, formatCharCount,
21
- ELAPSED_KEY, CHARS_KEY, compactErrorLines,
32
+ normalizeLineEndings,
33
+ humanSize,
34
+ formatElapsedMs,
35
+ formatCharCount,
36
+ ELAPSED_KEY,
37
+ CHARS_KEY,
38
+ compactErrorLines,
22
39
  } from "./helpers.js";
23
40
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
24
41
  import type { ThemeLike, RenderCtxLike as RenderContext } from "./types.js";
25
42
 
43
+ // ---------------------------------------------------------------------------
44
+ // Lazy imports — avoid top-level require() that blocks module loading
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /** Lazy accessor to pi-tui's truncateToWidth */
48
+ function _truncateToWidth(text: string, maxWidth: number, ellipsis?: string, pad?: boolean): string {
49
+ return require("@earendil-works/pi-tui").truncateToWidth(text, maxWidth, ellipsis, pad);
50
+ }
51
+
26
52
  // ---------------------------------------------------------------------------
27
53
  // Shiki ANSI cache
28
54
  // ---------------------------------------------------------------------------
@@ -113,9 +139,15 @@ export function preserveBoxBackground(ansi: string): string {
113
139
  if (code === 38) {
114
140
  // Foreground extended — keep entire sequence
115
141
  kept.push(parts[i]);
116
- if (parts[i + 1] === "5") { kept.push(parts[i + 1]); i += 2; }
117
- else if (parts[i + 1] === "2") { kept.push(parts[i + 1], parts[i + 2], parts[i + 3], parts[i + 4]); i += 5; }
118
- else { i++; }
142
+ if (parts[i + 1] === "5") {
143
+ kept.push(parts[i + 1]);
144
+ i += 2;
145
+ } else if (parts[i + 1] === "2") {
146
+ kept.push(parts[i + 1], parts[i + 2], parts[i + 3], parts[i + 4]);
147
+ i += 5;
148
+ } else {
149
+ i++;
150
+ }
119
151
  } else if (code === 48) {
120
152
  // Background extended — skip entirely
121
153
  if (parts[i + 1] === "5") i += 3;
@@ -136,7 +168,7 @@ export function fillToolBackground(text: string, bg = BG_BASE, width?: number):
136
168
  return text
137
169
  .split("\n")
138
170
  .map((line) => {
139
- const fitted = width ? truncateToWidth(line, width, "") : line;
171
+ const fitted = width ? _truncateToWidth(line, width, "") : line;
140
172
  const stripped = preserveBoxBackground(fitted);
141
173
  // Apply background to the entire line
142
174
  return bg ? bg + stripped : stripped;
@@ -198,7 +230,7 @@ export async function renderFileContent(
198
230
 
199
231
  const out: string[] = [];
200
232
  for (const line of hl) {
201
- out.push(truncateToWidth(line ?? "", Math.max(1, tw), `${FG_DIM}›`));
233
+ out.push(_truncateToWidth(line ?? "", Math.max(1, tw), `${FG_DIM}›`));
202
234
  }
203
235
  return out.join("\n");
204
236
  }
@@ -210,9 +242,10 @@ export async function renderFileContent(
210
242
  export function renderBashOutput(text: string, exitCode: number | null): { summary: string; body: string } {
211
243
  const isOk = exitCode === 0;
212
244
  const statusIcon = isOk ? "✓" : "✗";
213
- const codeStr = exitCode !== null
214
- ? `${isOk ? FG_GREEN : FG_RED}${statusIcon} exit ${exitCode}${RST}`
215
- : `${FG_YELLOW} killed${RST}`;
245
+ const codeStr =
246
+ exitCode !== null
247
+ ? `${isOk ? FG_GREEN : FG_RED}${statusIcon} exit ${exitCode}${RST}`
248
+ : `${FG_YELLOW}⚡ killed${RST}`;
216
249
 
217
250
  const lines = text.split("\n");
218
251
  const maxShow = MAX_PREVIEW_LINES;
@@ -255,44 +288,46 @@ export function renderTree(text: string, _basePath: string): string {
255
288
  return out.join("\n");
256
289
  }
257
290
 
258
- // ---------------------------------------------------------------------------
259
- // Find — grouped file list (plain, no tree characters or icons)
260
- // ---------------------------------------------------------------------------
261
-
262
- export function renderFindResults(text: string, theme?: ThemeLike): string {
263
- const lines = text.trim().split("\n").filter(Boolean);
264
- if (!lines.length) return theme ? theme.fg("dim", "(no matches)") : `${FG_DIM}(no matches)${RST}`;
265
-
266
- const groups = new Map<string, string[]>();
267
- for (const line of lines) {
268
- const trimmed = line.trim();
269
- const dir = dirname(trimmed) || ".";
270
- const file = basename(trimmed);
271
- if (!groups.has(dir)) groups.set(dir, []);
272
- const bucket = groups.get(dir);
273
- if (bucket) bucket.push(file);
274
- }
275
-
276
- const out: string[] = [];
277
- let count = 0;
278
-
279
- for (const [dir, files] of groups) {
280
- if (count > 0) out.push("");
281
- const dirColored = theme ? theme.fg("accent", theme.bold(`${dir}/`)) : `${FG_BLUE}\x1b[1m${dir}/${RST}`;
282
- out.push(dirColored);
283
- for (let i = 0; i < files.length; i++) {
284
- if (count >= MAX_PREVIEW_LINES) {
285
- const more = theme ? theme.fg("dim", `… ${lines.length - count} more files`) : `${FG_DIM}… ${lines.length - count} more files${RST}`;
286
- out.push(` ${more}`);
287
- return out.join("\n");
288
- }
289
- out.push(` ${files[i]}`);
290
- count++;
291
- }
292
- }
293
-
294
- return out.join("\n");
295
- }
291
+ // ---------------------------------------------------------------------------
292
+ // Find — grouped file list (plain, no tree characters or icons)
293
+ // ---------------------------------------------------------------------------
294
+
295
+ export function renderFindResults(text: string, theme?: ThemeLike): string {
296
+ const lines = text.trim().split("\n").filter(Boolean);
297
+ if (!lines.length) return theme ? theme.fg("dim", "(no matches)") : `${FG_DIM}(no matches)${RST}`;
298
+
299
+ const groups = new Map<string, string[]>();
300
+ for (const line of lines) {
301
+ const trimmed = line.trim();
302
+ const dir = dirname(trimmed) || ".";
303
+ const file = basename(trimmed);
304
+ if (!groups.has(dir)) groups.set(dir, []);
305
+ const bucket = groups.get(dir);
306
+ if (bucket) bucket.push(file);
307
+ }
308
+
309
+ const out: string[] = [];
310
+ let count = 0;
311
+
312
+ for (const [dir, files] of groups) {
313
+ if (count > 0) out.push("");
314
+ const dirColored = theme ? theme.fg("accent", theme.bold(`${dir}/`)) : `${FG_BLUE}\x1b[1m${dir}/${RST}`;
315
+ out.push(dirColored);
316
+ for (let i = 0; i < files.length; i++) {
317
+ if (count >= MAX_PREVIEW_LINES) {
318
+ const more = theme
319
+ ? theme.fg("dim", `… ${lines.length - count} more files`)
320
+ : `${FG_DIM}… ${lines.length - count} more files${RST}`;
321
+ out.push(` ${more}`);
322
+ return out.join("\n");
323
+ }
324
+ out.push(` ${files[i]}`);
325
+ count++;
326
+ }
327
+ }
328
+
329
+ return out.join("\n");
330
+ }
296
331
 
297
332
  // ---------------------------------------------------------------------------
298
333
  // Grep — highlighted matches with line numbers
@@ -309,7 +344,9 @@ export async function renderGrepResults(text: string, pattern: string): Promise<
309
344
  let re: RegExp | null = null;
310
345
  try {
311
346
  re = new RegExp(`(${pattern})`, "gi");
312
- } catch { /* skip highlighting */ }
347
+ } catch {
348
+ /* skip highlighting */
349
+ }
313
350
 
314
351
  for (const line of lines) {
315
352
  if (count >= MAX_PREVIEW_LINES) {
package/src/tools/bash.ts CHANGED
@@ -1,10 +1,25 @@
1
1
  /* pi-pretty: bash tool -- command execution with styled output. */
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, BashDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
5
- import { resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, 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_RULE,
18
+ RST,
19
+ } from "../config.js";
6
20
  import { wrapExecuteWithMetrics } from "./metrics.js";
7
21
  import { renderBashOutput, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
22
+ import { resolveTextCtor } from "../tui-text.js";
8
23
  import { stripBashExitStatusLine, inferBashExitCode, compactErrorLines } from "../helpers.js";
9
24
 
10
25
  type Result = AgentToolResult<Record<string, unknown>>;
@@ -16,10 +31,7 @@ export function registerBashTool(
16
31
  sdkTool: SdkToolDef,
17
32
  TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
18
33
  ): void {
19
- const TC = TextComp ?? (() => {
20
- const { Text } = require("@earendil-works/pi-tui") as { Text: new (t?: string, x?: number, y?: number) => { setText(v: string): void } };
21
- return Text;
22
- })();
34
+ const TC = resolveTextCtor(TextComp);
23
35
 
24
36
  pi.registerTool({
25
37
  name: "bash",
@@ -30,10 +42,19 @@ export function registerBashTool(
30
42
 
31
43
  execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
32
44
  try {
33
- return await sdkTool.execute(tid, params, sig, undefined, ctx) as Result;
45
+ return (await sdkTool.execute(tid, params, sig, undefined, ctx)) as Result;
34
46
  } catch (error: unknown) {
35
47
  const msg = error instanceof Error ? error.message : String(error);
36
- return { content: [{ type: "text" as const, text: msg }], isError: true, details: { _type: "bashResult", text: msg, exitCode: 1, command: String((params as any).command ?? "") } as BashDetails };
48
+ return {
49
+ content: [{ type: "text" as const, text: msg }],
50
+ isError: true,
51
+ details: {
52
+ _type: "bashResult",
53
+ text: msg,
54
+ exitCode: 1,
55
+ command: String((params as any).command ?? ""),
56
+ } as BashDetails,
57
+ };
37
58
  }
38
59
  }),
39
60
 
@@ -43,23 +64,40 @@ export function registerBashTool(
43
64
  const t = typeof args.timeout === "number" ? ` ${theme.fg("muted", `(timeout ${args.timeout}s)`)}` : "";
44
65
  const tw = termWidth() || 80;
45
66
  const rawCmd = String(args.command ?? "");
46
- const cmd = rawCmd.length === 0 ? theme.fg("toolOutput", "...") : (rawCmd.length > tw - 20 ? rawCmd.slice(0, Math.max(1, tw - 20)) + "…" : rawCmd);
67
+ const cmd =
68
+ rawCmd.length === 0
69
+ ? theme.fg("toolOutput", "...")
70
+ : rawCmd.length > tw - 20
71
+ ? rawCmd.slice(0, Math.max(1, tw - 20)) + "…"
72
+ : rawCmd;
47
73
  const toolWidth = ctx.expanded ? undefined : tw;
48
- text.setText(fillToolBackground(`\n ${theme.fg("toolTitle", theme.bold(`$ ${cmd}`))}${t}`, ctx.isError ? BG_ERROR : undefined, toolWidth));
74
+ text.setText(
75
+ fillToolBackground(
76
+ `\n ${theme.fg("toolTitle", theme.bold(`$ ${cmd}`))}${t}`,
77
+ ctx.isError ? BG_ERROR : undefined,
78
+ toolWidth,
79
+ ),
80
+ );
49
81
  return text;
50
82
  },
51
83
 
52
84
  renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
53
85
  resolveBaseBackground(theme);
54
-
86
+
55
87
  const text = ctx.lastComponent ?? new TC("", 0, 0);
56
88
 
57
89
  const details = result.details;
58
90
  const tc = getText(result);
59
91
  const d: BashDetails | undefined =
60
- (details as BashDetails)?._type === "bashResult" ? details as BashDetails
92
+ (details as BashDetails)?._type === "bashResult"
93
+ ? (details as BashDetails)
61
94
  : tc || ctx.isError
62
- ? { _type: "bashResult", text: tc || "Error", exitCode: inferBashExitCode(tc, ctx.isError ? 1 : 0), command: "" }
95
+ ? {
96
+ _type: "bashResult",
97
+ text: tc || "Error",
98
+ exitCode: inferBashExitCode(tc, ctx.isError ? 1 : 0),
99
+ command: "",
100
+ }
63
101
  : undefined;
64
102
 
65
103
  if (d?._type === "bashResult") {
@@ -69,7 +107,10 @@ export function registerBashTool(
69
107
  const output = isErr ? compactErrorLines(cleaned).join("\n") : cleaned;
70
108
  const { summary } = renderBashOutput(output, d.exitCode);
71
109
  const lineCount = output.split("\n").length;
72
- const info = lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST} ${renderToolMetrics(result)}` : ` ${renderToolMetrics(result)}`;
110
+ const info =
111
+ lineCount > 1
112
+ ? ` ${FG_DIM}(${lineCount} lines)${RST} ${renderToolMetrics(result)}`
113
+ : ` ${renderToolMetrics(result)}`;
73
114
  const header = ` ${summary}${info}`;
74
115
  const rw = termWidth();
75
116
 
@@ -83,30 +124,44 @@ export function registerBashTool(
83
124
  };
84
125
 
85
126
  text.setText(renderFn(rw));
86
- const baseRender = typeof (text as ComponentLike).render === "function"
87
- ? (text as ComponentLike).render.bind(text)
88
- : null;
127
+ const baseRender =
128
+ typeof (text as ComponentLike).render === "function" ? (text as ComponentLike).render.bind(text) : null;
89
129
  if (baseRender) {
90
130
  let key: string | undefined;
91
131
  (text as unknown as Record<string, unknown>).render = (w: number) => {
92
132
  const width = Math.max(1, Math.floor(w || termWidth()));
93
133
  const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolMetrics(result)}`;
94
- if (key !== k) { text.setText(renderFn(width)); key = k; }
134
+ if (key !== k) {
135
+ text.setText(renderFn(width));
136
+ key = k;
137
+ }
95
138
  return baseRender(width);
96
139
  };
97
140
  }
98
141
  return text;
99
142
  }
100
143
 
101
- if (ctx.isError) { text.setText(renderToolError(tc || "Error", theme)); return text; }
144
+ if (ctx.isError) {
145
+ text.setText(renderToolError(tc || "Error", theme));
146
+ return text;
147
+ }
102
148
  const fc = result.content?.[0];
103
- text.setText(fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`));
149
+ text.setText(
150
+ fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
151
+ );
104
152
  return text;
105
153
  },
106
154
  } as unknown as ToolDefinition<any, any, any>);
107
155
  }
108
156
 
109
- function rule(w: number): string { return `${FG_RULE}${"\u2500".repeat(w)}${RST}`; }
157
+ function rule(w: number): string {
158
+ return `${FG_RULE}${"\u2500".repeat(w)}${RST}`;
159
+ }
110
160
  function getText(result: Result): string {
111
- return ((result.content ?? []) as TextContent[]).filter((c) => c.type === "text").map((c) => c.text).join("\n") ?? "";
161
+ return (
162
+ ((result.content ?? []) as TextContent[])
163
+ .filter((c) => c.type === "text")
164
+ .map((c) => c.text)
165
+ .join("\n") ?? ""
166
+ );
112
167
  }
package/src/tools/find.ts CHANGED
@@ -1,12 +1,27 @@
1
1
  /* pi-pretty: find tool -- FFF-backed file search with 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 { isAbsolute, relative as toRelative } from "node:path";
5
- import type { SdkToolDef, FindDetails, FffServiceLike, FileItem, TextContent, ThemeLike, RenderCtxLike, ComponentLike } from "../types.js";
10
+ import type {
11
+ SdkToolDef,
12
+ FindDetails,
13
+ FffServiceLike,
14
+ FileItem,
15
+ TextContent,
16
+ ThemeLike,
17
+ RenderCtxLike,
18
+ ComponentLike,
19
+ } from "../types.js";
6
20
  import { resolveBaseBackground, BG_ERROR, FG_DIM, RST } from "../config.js";
7
21
  import { shortPath } from "../helpers.js";
8
22
  import { wrapExecuteWithMetrics } from "./metrics.js";
9
23
  import { renderFindResults, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
24
+ import { resolveTextCtor } from "../tui-text.js";
10
25
  import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
11
26
 
12
27
  type Result = AgentToolResult<Record<string, unknown>>;
@@ -18,10 +33,7 @@ export function registerFindTool(
18
33
  sdkTool: SdkToolDef,
19
34
  TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
20
35
  ): void {
21
- if (!TextComp) {
22
- const { Text } = require("@earendil-works/pi-tui") as { Text: new (t?: string, x?: number, y?: number) => { setText(v: string): void; render(w: number): string[]; invalidate(): void } };
23
- TextComp = Text;
24
- }
36
+ const TC = resolveTextCtor(TextComp);
25
37
  const home = process.env.HOME ?? "";
26
38
 
27
39
  pi.registerTool({
@@ -49,33 +61,49 @@ export function registerFindTool(
49
61
  }
50
62
  cleanPath = cleanPath.replace(/\/$/, "");
51
63
  const cleanPattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
52
- const globPattern = cleanPath
53
- ? `${cleanPath}/**/${cleanPattern}`
54
- : `**/${cleanPattern}`;
64
+ const globPattern = cleanPath ? `${cleanPath}/**/${cleanPattern}` : `**/${cleanPattern}`;
55
65
  const searchResult = fff.glob(globPattern, { pageSize: effectiveLimit });
56
- if (searchResult.ok) {
57
- const items: FileItem[] = searchResult.value.items.slice(0, effectiveLimit);
58
- const notices: string[] = [];
59
- if (fffService?.partialIndex) notices.push(NOTICE_PARTIAL_FILE_INDEX);
60
- if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
61
- if (searchResult.value.totalMatched > items.length) notices.push(`${searchResult.value.totalMatched} total matches`);
62
- const paths = items.map((i) => i.relativePath).join("\n");
63
- return { content: [{ type: "text" as const, text: paths }], details: { _type: "findResult", text: paths, pattern, matchCount: items.length, notices } as FindDetails };
64
- }
65
- } catch { /* fall through to SDK */ }
66
+ if (searchResult.ok) {
67
+ const items: FileItem[] = searchResult.value.items.slice(0, effectiveLimit);
68
+ const notices: string[] = [];
69
+ if (fffService?.partialIndex) notices.push(NOTICE_PARTIAL_FILE_INDEX);
70
+ if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
71
+ if (searchResult.value.totalMatched > items.length)
72
+ notices.push(`${searchResult.value.totalMatched} total matches`);
73
+ const paths = items.map((i) => i.relativePath).join("\n");
74
+ return {
75
+ content: [{ type: "text" as const, text: paths }],
76
+ details: {
77
+ _type: "findResult",
78
+ text: paths,
79
+ pattern,
80
+ matchCount: items.length,
81
+ notices,
82
+ } as FindDetails,
83
+ };
84
+ }
85
+ } catch {
86
+ /* fall through to SDK */
87
+ }
66
88
  }
67
89
 
68
- const result = await sdkTool.execute(tid, params, sig, undefined, ctx) as Result;
90
+ const result = (await sdkTool.execute(tid, params, sig, undefined, ctx)) as Result;
69
91
  const tc = getText(result);
70
- result.details = { _type: "findResult", text: tc, pattern, matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0 } as FindDetails;
92
+ result.details = {
93
+ _type: "findResult",
94
+ text: tc,
95
+ pattern,
96
+ matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
97
+ } as FindDetails;
71
98
  return result;
72
99
  }),
73
100
 
74
- renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
101
+ renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
75
102
  resolveBaseBackground(theme);
76
- const text = ctx.lastComponent ?? new TextComp!("", 0, 0);
103
+ const text = ctx.lastComponent ?? new TC("", 0, 0);
77
104
  const pattern = args.pattern === null || args.pattern === undefined ? "" : String(args.pattern);
78
- const path = args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.path));
105
+ const path =
106
+ args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.path));
79
107
  const limit = args.limit;
80
108
  const glob = args.glob;
81
109
  const findLabel = theme.fg("toolTitle", theme.bold("find"));
@@ -90,28 +118,51 @@ export function registerFindTool(
90
118
 
91
119
  renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
92
120
  resolveBaseBackground(theme);
93
- const text = ctx.lastComponent ?? new TextComp!("", 0, 0);
94
- if (ctx.isError) { text.setText(renderToolError(getText(result) || "Error", theme)); return text; }
121
+ const text = ctx.lastComponent ?? new TC("", 0, 0);
122
+ if (ctx.isError) {
123
+ text.setText(renderToolError(getText(result) || "Error", theme));
124
+ return text;
125
+ }
95
126
  const d = result.details as FindDetails | undefined;
96
127
  if (d?._type === "findResult") {
97
- if (!d.text.trim()) {
98
- const notices = (d as any).notices as string[] | undefined;
99
- const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
100
- text.setText(fillToolBackground(` \n ${theme.fg("dim", "0 files")}${noticeStr}\n `));
101
- return text;
102
- }
103
- const rendered = renderFindResults(d.text, theme).split("\n").map(l => ` ${l}`).join("\n");
104
- const notices = (d as any).notices as string[] | undefined;
105
- const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
106
- text.setText(fillToolBackground(` \n ${theme.fg("dim", `${d.matchCount} files`)}${renderToolMetrics(result)}\n${rendered}${noticeStr}\n `));
107
- return text;
128
+ if (!d.text.trim()) {
129
+ const notices = (d as any).notices as string[] | undefined;
130
+ const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
131
+ text.setText(fillToolBackground(` \n ${theme.fg("dim", "0 files")}${noticeStr}\n `));
132
+ return text;
133
+ }
134
+ const rendered = renderFindResults(d.text, theme)
135
+ .split("\n")
136
+ .map((l) => ` ${l}`)
137
+ .join("\n");
138
+ const notices = (d as any).notices as string[] | undefined;
139
+ const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
140
+ text.setText(
141
+ fillToolBackground(
142
+ ` \n ${theme.fg("dim", `${d.matchCount} files`)}${renderToolMetrics(result)}\n${rendered}${noticeStr}\n `,
143
+ ),
144
+ );
145
+ return text;
108
146
  }
109
147
  const fc = result.content?.[0];
110
- text.setText(fillToolBackground(` \n ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "0 files")}\n `));
148
+ text.setText(
149
+ fillToolBackground(
150
+ ` \n ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "0 files")}\n `,
151
+ ),
152
+ );
111
153
  return text;
112
154
  },
113
155
  } as unknown as ToolDefinition<any, any, any>);
114
156
  }
115
157
 
116
- function appendNotices(text: string, notices: string[]): string { return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text; }
117
- function getText(result: Result): string { return ((result.content ?? []) as TextContent[]).filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n") ?? ""; }
158
+ function appendNotices(text: string, notices: string[]): string {
159
+ return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text;
160
+ }
161
+ function getText(result: Result): string {
162
+ return (
163
+ ((result.content ?? []) as TextContent[])
164
+ .filter((c) => c.type === "text")
165
+ .map((c) => c.text ?? "")
166
+ .join("\n") ?? ""
167
+ );
168
+ }
package/src/tools/grep.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  /* pi-pretty: grep tool -- FFF-backed text search with 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 { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
5
- import { keyHint } from "@earendil-works/pi-coding-agent";
6
10
  import { MAX_PREVIEW_LINES, BG_ERROR, resolveBaseBackground } from "../config.js";
7
11
  import { shortPath, normalizeLineEndings } from "../helpers.js";
8
12
  import { wrapExecuteWithMetrics } from "./metrics.js";
9
13
  import { renderToolError, fillToolBackground } from "../render.js";
14
+ import { resolveTextCtor } from "../tui-text.js";
10
15
  import { fffFormatGrepText } from "../fff-helpers.js";
11
16
  import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
12
17
 
@@ -21,7 +26,7 @@ export function registerGrepTool(
21
26
  sdkTool: SdkToolDef,
22
27
  TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
23
28
  ): void {
24
- const T = TextComp ?? (() => { const m = require("@earendil-works/pi-tui") as { Text: new (t?: string, x?: number, y?: number) => { setText(v: string): void } }; return m.Text; })();
29
+ const T = resolveTextCtor(TextComp);
25
30
  const home = process.env.HOME ?? "";
26
31
 
27
32
  pi.registerTool({
@@ -45,7 +50,12 @@ export function registerGrepTool(
45
50
  const fff = fffService.getFinder();
46
51
  if (!fff) throw new Error("FFF finder not available");
47
52
  const effectiveLimit = Math.max(1, limit);
48
- const grepResult = fff.grep(pattern, { pageSize: effectiveLimit, mode: literal ? "plain" : "regex", beforeContext: context, afterContext: context });
53
+ const grepResult = fff.grep(pattern, {
54
+ pageSize: effectiveLimit,
55
+ mode: literal ? "plain" : "regex",
56
+ beforeContext: context,
57
+ afterContext: context,
58
+ });
49
59
  if (grepResult.ok) {
50
60
  const grep = grepResult.value;
51
61
  const items = grep.items.slice(0, effectiveLimit);
@@ -59,15 +69,36 @@ export function registerGrepTool(
59
69
  notices.push(`More results available: cursor="${cursorId}"`);
60
70
  }
61
71
  const text = appendNotices(fffFormatGrepText(items, effectiveLimit), notices);
62
- return { content: [{ type: "text" as const, text }], details: { _type: "grepResult", text, pattern, matchCount: items.length } as GrepDetails };
72
+ return {
73
+ content: [{ type: "text" as const, text }],
74
+ details: {
75
+ _type: "grepResult",
76
+ text,
77
+ pattern,
78
+ matchCount: items.length,
79
+ } as GrepDetails,
80
+ };
63
81
  }
64
- } catch { /* fall through */ }
82
+ } catch {
83
+ /* fall through */
84
+ }
65
85
  }
66
86
 
67
- const result = await sdkTool.execute(tid, p, sig, undefined, ctx) as Result;
68
- for (const c of (result.content ?? []) as any[]) { if (c.type === "text") c.text = normalizeLineEndings(c.text); }
69
- const tc = ((result.content ?? []) as TextContent[]).filter((c) => c.type === "text").map((c) => c.text).join("\n") ?? "";
70
- result.details = { _type: "grepResult", text: tc, pattern, matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0 } as GrepDetails;
87
+ const result = (await sdkTool.execute(tid, p, sig, undefined, ctx)) as Result;
88
+ for (const c of (result.content ?? []) as any[]) {
89
+ if (c.type === "text") c.text = normalizeLineEndings(c.text);
90
+ }
91
+ const tc =
92
+ ((result.content ?? []) as TextContent[])
93
+ .filter((c) => c.type === "text")
94
+ .map((c) => c.text)
95
+ .join("\n") ?? "";
96
+ result.details = {
97
+ _type: "grepResult",
98
+ text: tc,
99
+ pattern,
100
+ matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
101
+ } as GrepDetails;
71
102
  return result;
72
103
  }),
73
104
 
@@ -92,7 +123,18 @@ export function registerGrepTool(
92
123
  renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
93
124
  resolveBaseBackground(theme);
94
125
  const text = ctx.lastComponent ?? new T("", 0, 0);
95
- if (ctx.isError) { text.setText(renderToolError(((result.content ?? []) as TextContent[]).filter((c) => c.type === "text").map((c) => c.text).join("\n") || "Error", theme)); return text; }
126
+ if (ctx.isError) {
127
+ text.setText(
128
+ renderToolError(
129
+ ((result.content ?? []) as TextContent[])
130
+ .filter((c) => c.type === "text")
131
+ .map((c) => c.text)
132
+ .join("\n") || "Error",
133
+ theme,
134
+ ),
135
+ );
136
+ return text;
137
+ }
96
138
  const d = result.details as GrepDetails | undefined;
97
139
  if (d?._type === "grepResult" && d.text) {
98
140
  const lines = d.text.split("\n");
@@ -105,7 +147,7 @@ export function registerGrepTool(
105
147
  out.push(theme.fg("toolOutput", line));
106
148
  }
107
149
  if (remaining > 0) {
108
- out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, ${keyHint("app.tools.expand", "to expand")})`));
150
+ out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, to expand)`));
109
151
  }
110
152
  const body = out.map((l) => ` ${l}`).join("\n") + "\n\n";
111
153
  text.setText(fillToolBackground(body, ctx.isError ? BG_ERROR : undefined));
@@ -119,4 +161,6 @@ export function registerGrepTool(
119
161
  } as unknown as ToolDefinition<any, any, any>);
120
162
  }
121
163
 
122
- function appendNotices(text: string, notices: string[]): string { return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text; }
164
+ function appendNotices(text: string, notices: string[]): string {
165
+ return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text;
166
+ }