@heyhuynhgiabuu/pi-pretty 0.6.15 → 0.6.16

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +11 -122
  2. package/README.md +2 -0
  3. package/dist/collapsed-hint.d.ts +7 -0
  4. package/dist/collapsed-hint.d.ts.map +1 -0
  5. package/dist/collapsed-hint.js +17 -0
  6. package/dist/collapsed-hint.js.map +1 -0
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +3 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/expand.d.ts +13 -0
  12. package/dist/expand.d.ts.map +1 -0
  13. package/dist/expand.js +24 -0
  14. package/dist/expand.js.map +1 -0
  15. package/dist/find-glob.d.ts +9 -0
  16. package/dist/find-glob.d.ts.map +1 -0
  17. package/dist/find-glob.js +29 -0
  18. package/dist/find-glob.js.map +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/render.d.ts.map +1 -1
  23. package/dist/render.js +15 -10
  24. package/dist/render.js.map +1 -1
  25. package/dist/tools/bash.d.ts.map +1 -1
  26. package/dist/tools/bash.js +13 -12
  27. package/dist/tools/bash.js.map +1 -1
  28. package/dist/tools/find.d.ts +2 -2
  29. package/dist/tools/find.d.ts.map +1 -1
  30. package/dist/tools/find.js +90 -45
  31. package/dist/tools/find.js.map +1 -1
  32. package/dist/tools/grep.d.ts.map +1 -1
  33. package/dist/tools/grep.js +8 -4
  34. package/dist/tools/grep.js.map +1 -1
  35. package/dist/tools/ls.d.ts.map +1 -1
  36. package/dist/tools/ls.js +8 -4
  37. package/dist/tools/ls.js.map +1 -1
  38. package/dist/tools/multi-grep.d.ts.map +1 -1
  39. package/dist/tools/multi-grep.js +12 -8
  40. package/dist/tools/multi-grep.js.map +1 -1
  41. package/dist/tools/read.d.ts.map +1 -1
  42. package/dist/tools/read.js +15 -11
  43. package/dist/tools/read.js.map +1 -1
  44. package/dist/types.d.ts +1 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +5 -4
  47. package/src/collapsed-hint.ts +14 -0
  48. package/src/config.ts +3 -0
  49. package/src/expand.ts +24 -0
  50. package/src/find-glob.ts +21 -0
  51. package/src/index.ts +2 -0
  52. package/src/render.ts +17 -10
  53. package/src/tools/bash.ts +15 -12
  54. package/src/tools/find.ts +132 -73
  55. package/src/tools/grep.ts +14 -5
  56. package/src/tools/ls.ts +13 -5
  57. package/src/tools/multi-grep.ts +17 -9
  58. package/src/tools/read.ts +20 -11
  59. package/src/types.ts +1 -0
package/src/tools/find.ts CHANGED
@@ -1,35 +1,85 @@
1
- /* pi-pretty: find tool -- FFF-backed file search with SDK fallback. */
1
+ /* pi-pretty: find tool -- FFF-backed file search with SDK (fd) fallback. */
2
2
 
3
+ import { isAbsolute, relative } from "node:path";
3
4
  import {
4
5
  type ToolDefinition,
5
6
  type ExtensionAPI,
6
7
  type ExtensionContext,
7
8
  type AgentToolResult,
8
9
  } from "@earendil-works/pi-coding-agent";
9
- import { isAbsolute, relative as toRelative } from "node:path";
10
- import type {
11
- SdkToolDef,
12
- FindDetails,
13
- FffServiceLike,
14
- FileItem,
15
- TextContent,
16
- ThemeLike,
17
- RenderCtxLike,
18
- ComponentLike,
19
- } from "../types.js";
20
- import { resolveBaseBackground, BG_ERROR, FG_DIM, RST } from "../config.js";
10
+ import type { SdkToolDef, FindDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
11
+ import { TOOL_RESULT_INDENT, BG_ERROR, FG_DIM, RST, resolveBaseBackground } from "../config.js";
21
12
  import { shortPath } from "../helpers.js";
22
13
  import { wrapExecuteWithMetrics } from "./metrics.js";
23
14
  import { renderFindResults, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
24
15
  import { resolveTextCtor } from "../tui-text.js";
25
16
  import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
17
+ import { isLikelyGlobPattern, normalizeFindGlobPattern } from "../find-glob.js";
26
18
 
27
19
  type Result = AgentToolResult<Record<string, unknown>>;
28
20
 
21
+ function getText(result: Result): string {
22
+ return (result.content ?? [])
23
+ .filter((c) => c.type === "text")
24
+ .map((c) => (c as TextContent).text ?? "")
25
+ .join("\n");
26
+ }
27
+
28
+ function buildGlobPattern(pattern: string, path: string | undefined, basePath: string | null): string {
29
+ const raw = pattern.startsWith("/") ? pattern.slice(1) : pattern;
30
+ const normalized = normalizeFindGlobPattern(raw);
31
+ let cleanPath = path ?? "";
32
+ if (cleanPath && isAbsolute(cleanPath) && basePath) {
33
+ cleanPath = relative(basePath, cleanPath) || "";
34
+ }
35
+ cleanPath = cleanPath.replace(/\/$/, "");
36
+ if (cleanPath) {
37
+ if (normalized.startsWith("**/")) {
38
+ return `${cleanPath}/${normalized}`;
39
+ }
40
+ if (normalized.includes("/")) {
41
+ return `${cleanPath}/${normalized}`;
42
+ }
43
+ return `${cleanPath}/**/${normalized}`;
44
+ }
45
+ return normalized.startsWith("**/") || normalized.includes("/") ? normalized : `**/${normalized}`;
46
+ }
47
+
48
+ function appendFindNotices(result: Result, extra: string[]): Result {
49
+ if (extra.length === 0) return result;
50
+ const d = result.details;
51
+ if (!d || d._type !== "findResult") return result;
52
+ const prev = Array.isArray(d.notices) ? (d.notices as string[]) : [];
53
+ return { ...result, details: { ...d, notices: [...prev, ...extra] } };
54
+ }
55
+
56
+ async function sdkFindAsFindResult(
57
+ sdkTool: SdkToolDef,
58
+ tid: string,
59
+ params: Record<string, unknown>,
60
+ sig: AbortSignal | undefined,
61
+ ctx: ExtensionContext,
62
+ pattern: string,
63
+ extraNotices: string[],
64
+ ): Promise<Result> {
65
+ const result = (await sdkTool.execute(tid, params, sig, undefined, ctx)) as Result;
66
+ const tc = getText(result);
67
+ const prev = (result.details as FindDetails | undefined)?.notices ?? [];
68
+ const notices = [...(Array.isArray(prev) ? prev : []), ...extraNotices];
69
+ result.details = {
70
+ _type: "findResult",
71
+ text: tc,
72
+ pattern,
73
+ matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
74
+ notices,
75
+ };
76
+ return result;
77
+ }
78
+
29
79
  export function registerFindTool(
30
80
  pi: ExtensionAPI,
31
81
  cwd: string,
32
- fffService: FffServiceLike | null | undefined,
82
+ fffService: FffServiceWithCursor | null | undefined,
33
83
  sdkTool: SdkToolDef,
34
84
  TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
35
85
  ): void {
@@ -44,32 +94,38 @@ export function registerFindTool(
44
94
  renderShell: "self",
45
95
 
46
96
  execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
47
- const pattern = String((params as any).pattern ?? "");
48
- const path = (params as any).path ? String((params as any).path) : undefined;
49
- const limit = (params as any).limit;
50
- const glob = (params as any).glob;
51
-
97
+ const pattern = String(params.pattern ?? "");
98
+ const path = params.path ? String(params.path) : undefined;
99
+ const limit = params.limit;
52
100
  const fff = fffService?.isAvailable ? fffService.getFinder() : null;
101
+
53
102
  if (fff) {
54
103
  try {
55
104
  const effectiveLimit = Math.max(1, typeof limit === "number" ? limit : 100);
56
105
  const basePathResult = fff.getBasePath();
57
106
  const basePath = basePathResult.ok ? basePathResult.value : null;
58
- let cleanPath = path ?? "";
59
- if (cleanPath && isAbsolute(cleanPath) && basePath) {
60
- cleanPath = toRelative(basePath, cleanPath) || "";
61
- }
62
- cleanPath = cleanPath.replace(/\/$/, "");
63
- const cleanPattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
64
- const globPattern = cleanPath ? `${cleanPath}/**/${cleanPattern}` : `**/${cleanPattern}`;
107
+ const globPattern = buildGlobPattern(pattern, path, basePath);
65
108
  const searchResult = fff.glob(globPattern, { pageSize: effectiveLimit });
109
+
66
110
  if (searchResult.ok) {
67
- const items: FileItem[] = searchResult.value.items.slice(0, effectiveLimit);
111
+ const items = searchResult.value.items.slice(0, effectiveLimit);
68
112
  const notices: string[] = [];
69
113
  if (fffService?.partialIndex) notices.push(NOTICE_PARTIAL_FILE_INDEX);
70
114
  if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
71
- if (searchResult.value.totalMatched > items.length)
115
+ if (searchResult.value.totalMatched > items.length) {
72
116
  notices.push(`${searchResult.value.totalMatched} total matches`);
117
+ }
118
+
119
+ if (items.length === 0 && isLikelyGlobPattern(pattern)) {
120
+ return sdkFindAsFindResult(sdkTool, tid, params, sig, ctx, pattern, [
121
+ "FFF glob returned no matches; results from SDK find (fd).",
122
+ ...notices,
123
+ ]);
124
+ }
125
+
126
+ if (items.length > 0) notices.push("Search engine: FFF glob.");
127
+ else if (notices.length === 0) notices.push("Search engine: FFF glob (no matches).");
128
+
73
129
  const paths = items.map((i) => i.relativePath).join("\n");
74
130
  return {
75
131
  content: [{ type: "text" as const, text: paths }],
@@ -79,7 +135,7 @@ export function registerFindTool(
79
135
  pattern,
80
136
  matchCount: items.length,
81
137
  notices,
82
- } as FindDetails,
138
+ },
83
139
  };
84
140
  }
85
141
  } catch {
@@ -87,82 +143,85 @@ export function registerFindTool(
87
143
  }
88
144
  }
89
145
 
90
- const result = (await sdkTool.execute(tid, params, sig, undefined, ctx)) as Result;
91
- const tc = getText(result);
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;
98
- return result;
146
+ return sdkFindAsFindResult(sdkTool, tid, params, sig, ctx, pattern, [
147
+ fff ? "FFF find unavailable; results from SDK find (fd)." : "Search engine: SDK find (fd).",
148
+ ]);
99
149
  }),
100
150
 
101
151
  renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
102
152
  resolveBaseBackground(theme);
103
- const text = ctx.lastComponent ?? new TC("", 0, 0);
104
- const pattern = args.pattern === null || args.pattern === undefined ? "" : String(args.pattern);
105
- const path =
106
- args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.path));
107
- const limit = args.limit;
108
- const glob = args.glob;
153
+ const a = args as { pattern?: unknown; path?: unknown; limit?: unknown };
154
+ const text = (ctx as RenderCtxLike).lastComponent ?? new TC("", 0, 0);
155
+ const pattern = a.pattern == null ? "" : String(a.pattern);
156
+ const pathArg =
157
+ a.path == null ? "<missing>" : shortPath(cwd, home, String(a.path));
158
+ const limit = a.limit;
109
159
  const findLabel = theme.fg("toolTitle", theme.bold("find"));
110
160
  const patternPart = pattern ? theme.fg("accent", pattern) : "";
111
161
  const inPart = theme.fg("dim", " in ");
112
- const pathPart = theme.fg("toolOutput", path);
113
- const limitPart = limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
162
+ const pathPart = theme.fg("toolOutput", pathArg);
163
+ const limitPart =
164
+ limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
114
165
  const out = `${findLabel} ${patternPart}${inPart}${pathPart}${limitPart}`;
115
- text.setText(fillToolBackground(` \n ${out}`, ctx.isError ? BG_ERROR : undefined));
166
+ text.setText(
167
+ fillToolBackground(
168
+ `\n${TOOL_RESULT_INDENT}${out}`,
169
+ ctx.isError ? BG_ERROR : undefined,
170
+ ),
171
+ );
116
172
  return text;
117
173
  },
118
174
 
119
175
  renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
120
176
  resolveBaseBackground(theme);
121
- const text = ctx.lastComponent ?? new TC("", 0, 0);
177
+ const r = result;
178
+ const text = (ctx as RenderCtxLike).lastComponent ?? new TC("", 0, 0);
122
179
  if (ctx.isError) {
123
- text.setText(renderToolError(getText(result) || "Error", theme));
180
+ text.setText(renderToolError(getText(r) || "Error", theme));
124
181
  return text;
125
182
  }
126
- const d = result.details as FindDetails | undefined;
183
+ const d = r.details as FindDetails | undefined;
127
184
  if (d?._type === "findResult") {
128
185
  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 `));
186
+ const noticeStr = d.notices?.length
187
+ ? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
188
+ : "";
189
+ text.setText(
190
+ fillToolBackground(
191
+ `\n${TOOL_RESULT_INDENT}${theme.fg("dim", "0 files")}${noticeStr}\n`,
192
+ ),
193
+ );
194
+ return text;
195
+ }
196
+ if (!ctx.expanded) {
197
+ text.setText(
198
+ fillToolBackground(
199
+ `${TOOL_RESULT_INDENT}${FG_DIM}${d.matchCount} files — ctrl+o to expand${RST}${renderToolMetrics(r)}\n`,
200
+ ),
201
+ );
132
202
  return text;
133
203
  }
134
204
  const rendered = renderFindResults(d.text, theme)
135
205
  .split("\n")
136
- .map((l) => ` ${l}`)
206
+ .map((l) => `${TOOL_RESULT_INDENT}${l}`)
137
207
  .join("\n");
138
- const notices = (d as any).notices as string[] | undefined;
139
- const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
208
+ const noticeStr = d.notices?.length
209
+ ? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
210
+ : "";
140
211
  text.setText(
141
212
  fillToolBackground(
142
- ` \n ${theme.fg("dim", `${d.matchCount} files`)}${renderToolMetrics(result)}\n${rendered}${noticeStr}\n `,
213
+ `\n${TOOL_RESULT_INDENT}${theme.fg("dim", `${d.matchCount} files`)}${renderToolMetrics(r)}\n${rendered}${noticeStr}\n`,
143
214
  ),
144
215
  );
145
216
  return text;
146
217
  }
147
- const fc = result.content?.[0];
218
+ const fc = r.content?.[0] as TextContent | undefined;
148
219
  text.setText(
149
220
  fillToolBackground(
150
- ` \n ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "0 files")}\n `,
221
+ `\n${TOOL_RESULT_INDENT}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}\n`,
151
222
  ),
152
223
  );
153
224
  return text;
154
225
  },
155
- } as unknown as ToolDefinition<any, any, any>);
156
- }
157
-
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
- }
226
+ } as unknown as ToolDefinition);
227
+ }
package/src/tools/grep.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  type AgentToolResult,
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import type { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
10
- import { MAX_PREVIEW_LINES, BG_ERROR, resolveBaseBackground } from "../config.js";
10
+ import { TOOL_RESULT_INDENT, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, RST, resolveBaseBackground } from "../config.js";
11
11
  import { shortPath, normalizeLineEndings } from "../helpers.js";
12
12
  import { wrapExecuteWithMetrics } from "./metrics.js";
13
13
  import { renderToolError, fillToolBackground } from "../render.js";
@@ -116,7 +116,7 @@ export function registerGrepTool(
116
116
  if (limit !== undefined && limit !== null) out += theme.fg("dim", ` limit ${limit}`);
117
117
  if (literal) out += theme.fg("dim", ` (literal)`);
118
118
  if (caseInsensitive) out += theme.fg("dim", ` (case-insensitive)`);
119
- text.setText(fillToolBackground(`\n ${out}`, ctx.isError ? BG_ERROR : undefined));
119
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}`, ctx.isError ? BG_ERROR : undefined));
120
120
  return text;
121
121
  },
122
122
 
@@ -138,7 +138,16 @@ export function registerGrepTool(
138
138
  const d = result.details as GrepDetails | undefined;
139
139
  if (d?._type === "grepResult" && d.text) {
140
140
  const lines = d.text.split("\n");
141
- const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
141
+ if (!ctx.expanded) {
142
+ text.setText(
143
+ fillToolBackground(
144
+ `${TOOL_RESULT_INDENT}${FG_DIM}${lines.length} lines — ctrl+o to expand${RST}\n`,
145
+ ctx.isError ? BG_ERROR : undefined,
146
+ ),
147
+ );
148
+ return text;
149
+ }
150
+ const maxShow = lines.length;
142
151
  const show = lines.slice(0, maxShow);
143
152
  const remaining = lines.length - maxShow;
144
153
  const out: string[] = [];
@@ -149,13 +158,13 @@ export function registerGrepTool(
149
158
  if (remaining > 0) {
150
159
  out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, to expand)`));
151
160
  }
152
- const body = out.map((l) => ` ${l}`).join("\n") + "\n\n";
161
+ const body = out.map((l) => `${TOOL_RESULT_INDENT}${l}`).join("\n") + "\n\n";
153
162
  text.setText(fillToolBackground(body, ctx.isError ? BG_ERROR : undefined));
154
163
  return text;
155
164
  }
156
165
  const fc = result.content?.[0];
157
166
  const fallback = fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches";
158
- text.setText(fillToolBackground(` ${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined));
167
+ text.setText(fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined));
159
168
  return text;
160
169
  },
161
170
  } as unknown as ToolDefinition<any, any, any>);
package/src/tools/ls.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  type AgentToolResult,
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import type { SdkToolDef, LsDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
10
- import { resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, RST } from "../config.js";
10
+ import { TOOL_RESULT_INDENT, resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, RST } from "../config.js";
11
11
  import { shortPath } from "../helpers.js";
12
12
  import { wrapExecuteWithMetrics } from "./metrics.js";
13
13
  import { renderTree, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
@@ -56,7 +56,7 @@ export function registerLsTool(
56
56
  let out = theme.fg("toolTitle", theme.bold("ls"));
57
57
  if (path) out += ` ${theme.fg("accent", path)}`;
58
58
  if (limit !== undefined && limit !== null) out += theme.fg("toolOutput", ` (limit ${limit})`);
59
- text.setText(fillToolBackground(` \n ${out}`, ctx.isError ? BG_ERROR : undefined));
59
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}`, ctx.isError ? BG_ERROR : undefined));
60
60
  return text;
61
61
  },
62
62
 
@@ -70,20 +70,28 @@ export function registerLsTool(
70
70
  }
71
71
  const d = result.details as LsDetails | undefined;
72
72
  if (d?._type === "lsResult" && d.text) {
73
+ if (!ctx.expanded) {
74
+ text.setText(
75
+ fillToolBackground(
76
+ `${TOOL_RESULT_INDENT}${FG_DIM}${d.entryCount} entries — ctrl+o to expand${RST}${renderToolMetrics(result)}\n`,
77
+ ),
78
+ );
79
+ return text;
80
+ }
73
81
  const rendered = renderTree(d.text, d.path)
74
82
  .split("\n")
75
- .map((l) => ` ${l}`)
83
+ .map((l) => `${TOOL_RESULT_INDENT}${l}`)
76
84
  .join("\n");
77
85
  text.setText(
78
86
  fillToolBackground(
79
- ` \n ${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}\n${rendered}\n `,
87
+ `\n${TOOL_RESULT_INDENT}${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}\n${rendered}\n`,
80
88
  ),
81
89
  );
82
90
  return text;
83
91
  }
84
92
  const fc = result.content?.[0];
85
93
  text.setText(
86
- fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
94
+ fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
87
95
  );
88
96
  return text;
89
97
  },
@@ -16,7 +16,7 @@ import type {
16
16
  ThemeLike,
17
17
  RenderCtxLike,
18
18
  } from "../types.js";
19
- import { resolveBaseBackground, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST } from "../config.js";
19
+ import { TOOL_RESULT_INDENT, resolveBaseBackground, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST } from "../config.js";
20
20
  import { shortPath, normalizeLineEndings } from "../helpers.js";
21
21
  import { wrapExecuteWithMetrics } from "./metrics.js";
22
22
  import { renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
@@ -256,7 +256,7 @@ export function registerMultiGrepTool(
256
256
  let out = `${theme.fg("toolTitle", theme.bold("mgrep"))} ${theme.fg("accent", `/${patternStr || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
257
257
  if (literal) out += theme.fg("dim", ` (literal)`);
258
258
  if (limit !== undefined) out += theme.fg("dim", ` limit ${limit}`);
259
- text.setText(fillToolBackground(`\n ${out}`, ctx.isError ? BG_ERROR : undefined));
259
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}`, ctx.isError ? BG_ERROR : undefined));
260
260
  return text;
261
261
  },
262
262
 
@@ -271,7 +271,15 @@ export function registerMultiGrepTool(
271
271
  const d = result.details as GrepDetails | undefined;
272
272
  if (d?._type === "grepResult" && d.text) {
273
273
  const lines = d.text.split("\n");
274
- const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
274
+ if (!ctx.expanded) {
275
+ text.setText(
276
+ fillToolBackground(
277
+ `${TOOL_RESULT_INDENT}${FG_DIM}${lines.length} lines — ctrl+o to expand${RST}${renderToolMetrics(result)}\n`,
278
+ ),
279
+ );
280
+ return text;
281
+ }
282
+ const maxShow = lines.length;
275
283
  const show = lines.slice(0, maxShow);
276
284
  const nw = Math.max(3, 5);
277
285
 
@@ -288,27 +296,27 @@ export function registerMultiGrepTool(
288
296
  const [, file, lineNo, content] = fileMatch;
289
297
  if (file !== currentFile) {
290
298
  if (currentFile) out.push("");
291
- out.push(` ${theme.fg("accent", theme.bold(file))}`);
299
+ out.push(`${TOOL_RESULT_INDENT}${theme.fg("accent", theme.bold(file))}`);
292
300
  currentFile = file;
293
301
  }
294
302
  let display = content;
295
303
  if (hlRe) display = content.replace(hlRe, (m) => `${RST}${theme.fg("warning", theme.bold(m))}${RST}`);
296
304
  const padded = `${FG_LNUM}${String(lineNo).padStart(nw)}${RST} ${FG_RULE}│${RST} ${display}${RST}`;
297
- out.push(` ${padded}`);
305
+ out.push(`${TOOL_RESULT_INDENT}${padded}`);
298
306
  } else if (line.trim()) {
299
- out.push(` ${FG_DIM} ${line.trim()}${RST}`);
307
+ out.push(`${TOOL_RESULT_INDENT}${FG_DIM}${line.trim()}${RST}`);
300
308
  }
301
309
  }
302
310
  const preview = out.join("\n");
303
- const more = lines.length > maxShow ? `\n${FG_DIM} ... ${lines.length - maxShow} more lines${RST}` : "";
311
+ const more = lines.length > maxShow ? `\n${TOOL_RESULT_INDENT}${FG_DIM}... ${lines.length - maxShow} more lines${RST}` : "";
304
312
  text.setText(
305
- fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}\n${preview}${more}`),
313
+ fillToolBackground(`${TOOL_RESULT_INDENT}${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}\n${preview}${more}`),
306
314
  );
307
315
  return text;
308
316
  }
309
317
  const fc = result.content?.[0];
310
318
  text.setText(
311
- fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches")}`),
319
+ fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches")}`),
312
320
  );
313
321
  return text;
314
322
  },
package/src/tools/read.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import type { SdkToolDef, ReadDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
10
10
  import {
11
+ TOOL_RESULT_INDENT,
11
12
  resolveBaseBackground,
12
13
  termWidth,
13
14
  MAX_PREVIEW_LINES,
@@ -124,7 +125,7 @@ export function registerReadTool(
124
125
  const fc = result.content?.[0];
125
126
  text.setText(
126
127
  fillToolBackground(
127
- ` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 80) : `[image: ${d.filePath}]`)}`,
128
+ `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 80) : `[image: ${d.filePath}]`)}`,
128
129
  ),
129
130
  );
130
131
  return text;
@@ -135,28 +136,36 @@ export function registerReadTool(
135
136
  const tw = termWidth();
136
137
  const lines = d.content.split("\n");
137
138
  const total = lines.length;
138
- const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
139
+ const p2 = shortPath(cwd, home, String(d.filePath ?? ""));
140
+ const off2 = typeof d.offset === "number" ? `:${d.offset}` : "";
141
+ if (!ctx.expanded) {
142
+ text.setText(
143
+ fillToolBackground(
144
+ `\n${TOOL_RESULT_INDENT}${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", p2)}${theme.fg("dim", off2)}\n${TOOL_RESULT_INDENT}${FG_DIM}${total} lines — ctrl+o to expand${RST}\n`,
145
+ ),
146
+ );
147
+ return text;
148
+ }
149
+ const maxShow = lines.length;
139
150
  const show = lines.slice(0, maxShow);
140
151
  const nw = Math.max(3, String(total).length);
141
152
  const gw = nw + 3;
142
153
  const cw = Math.max(1, tw - gw);
143
154
 
144
- const p2 = shortPath(cwd, home, String(d.filePath ?? ""));
145
- const off2 = typeof d.offset === "number" ? `:${d.offset}` : "";
146
155
  const header = `${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", p2)}${theme.fg("dim", off2)}`;
147
- const out: string[] = ["", ` ${header}`];
148
- out.push(` ${FG_RULE}${"─".repeat(tw - 2)}${RST}`);
156
+ const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`];
157
+ out.push(`${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(tw - 1)}${RST}`);
149
158
  for (let i = 0; i < show.length; i++) {
150
159
  const ln = (d.offset || 0) + i + 1;
151
160
  const code = show[i] ?? "";
152
161
  const display = code.length > cw ? code.slice(0, cw) + `${FG_DIM}›${RST}` : code;
153
162
  const lineNo = String(ln);
154
163
  out.push(
155
- ` ${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
164
+ `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
156
165
  );
157
166
  }
158
167
  if (total > maxShow) {
159
- out.push(` ${FG_DIM} … ${total - maxShow} more lines (${total} total)${RST}`);
168
+ out.push(`${TOOL_RESULT_INDENT}${FG_DIM}… ${total - maxShow} more lines (${total} total)${RST}`);
160
169
  }
161
170
  out.push("");
162
171
  const rendered = out.join("\n");
@@ -168,9 +177,9 @@ export function registerReadTool(
168
177
  .then((hl) => {
169
178
  const padded = hl
170
179
  .split("\n")
171
- .map((l) => ` ${l}`)
180
+ .map((l) => `${TOOL_RESULT_INDENT}${l}`)
172
181
  .join("\n");
173
- const rendered = `\n ${header}\n${padded}\n`;
182
+ const rendered = `\n${TOOL_RESULT_INDENT}${header}\n${padded}\n`;
174
183
  text.setText(fillToolBackground(rendered));
175
184
  (ctx as any).state._rt = rendered;
176
185
  })
@@ -181,7 +190,7 @@ export function registerReadTool(
181
190
 
182
191
  const fc = result.content?.[0];
183
192
  text.setText(
184
- fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
193
+ fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
185
194
  );
186
195
  return text;
187
196
  },
package/src/types.ts CHANGED
@@ -73,6 +73,7 @@ export interface FindDetails extends Record<string, unknown> {
73
73
  text: string;
74
74
  pattern: string;
75
75
  matchCount: number;
76
+ notices?: string[];
76
77
  }
77
78
 
78
79
  export interface GrepDetails extends Record<string, unknown> {