@heyhuynhgiabuu/pi-pretty 0.6.16 → 0.6.17

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 (76) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +6 -16
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +61 -18
  6. package/dist/config.js.map +1 -1
  7. package/dist/expand.d.ts +0 -6
  8. package/dist/expand.d.ts.map +1 -1
  9. package/dist/expand.js +0 -13
  10. package/dist/expand.js.map +1 -1
  11. package/dist/image.js +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +18 -21
  15. package/dist/index.js.map +1 -1
  16. package/dist/render.d.ts +3 -5
  17. package/dist/render.d.ts.map +1 -1
  18. package/dist/render.js +9 -9
  19. package/dist/render.js.map +1 -1
  20. package/dist/tools/bash.d.ts +1 -1
  21. package/dist/tools/bash.d.ts.map +1 -1
  22. package/dist/tools/bash.js +18 -23
  23. package/dist/tools/bash.js.map +1 -1
  24. package/dist/tools/find.d.ts +2 -2
  25. package/dist/tools/find.d.ts.map +1 -1
  26. package/dist/tools/find.js +12 -8
  27. package/dist/tools/find.js.map +1 -1
  28. package/dist/tools/grep.d.ts +2 -2
  29. package/dist/tools/grep.d.ts.map +1 -1
  30. package/dist/tools/grep.js +5 -5
  31. package/dist/tools/grep.js.map +1 -1
  32. package/dist/tools/labels.d.ts +2 -0
  33. package/dist/tools/labels.d.ts.map +1 -0
  34. package/dist/tools/labels.js +7 -0
  35. package/dist/tools/labels.js.map +1 -0
  36. package/dist/tools/ls.d.ts +1 -1
  37. package/dist/tools/ls.d.ts.map +1 -1
  38. package/dist/tools/ls.js +1 -1
  39. package/dist/tools/ls.js.map +1 -1
  40. package/dist/tools/read.d.ts +1 -1
  41. package/dist/tools/read.d.ts.map +1 -1
  42. package/dist/tools/read.js +22 -40
  43. package/dist/tools/read.js.map +1 -1
  44. package/dist/tui-text.d.ts.map +1 -1
  45. package/dist/tui-text.js +1 -5
  46. package/dist/tui-text.js.map +1 -1
  47. package/dist/types.d.ts +3 -27
  48. package/dist/types.d.ts.map +1 -1
  49. package/package.json +3 -3
  50. package/src/collapsed-hint.ts +1 -1
  51. package/src/config.ts +67 -22
  52. package/src/expand.ts +0 -14
  53. package/src/find-glob.ts +1 -1
  54. package/src/image.ts +1 -1
  55. package/src/index.ts +25 -34
  56. package/src/notices.ts +1 -1
  57. package/src/render.ts +37 -35
  58. package/src/tools/bash.ts +24 -48
  59. package/src/tools/find.ts +22 -36
  60. package/src/tools/grep.ts +12 -15
  61. package/src/tools/labels.ts +3 -0
  62. package/src/tools/ls.ts +16 -10
  63. package/src/tools/metrics.ts +1 -1
  64. package/src/tools/read.ts +39 -59
  65. package/src/tui-text.ts +1 -5
  66. package/src/types.ts +3 -35
  67. package/dist/multi-grep-fallback.d.ts +0 -30
  68. package/dist/multi-grep-fallback.d.ts.map +0 -1
  69. package/dist/multi-grep-fallback.js +0 -205
  70. package/dist/multi-grep-fallback.js.map +0 -1
  71. package/dist/tools/multi-grep.d.ts +0 -7
  72. package/dist/tools/multi-grep.d.ts.map +0 -1
  73. package/dist/tools/multi-grep.js +0 -298
  74. package/dist/tools/multi-grep.js.map +0 -1
  75. package/src/multi-grep-fallback.ts +0 -248
  76. package/src/tools/multi-grep.ts +0 -339
package/src/tools/bash.ts CHANGED
@@ -1,28 +1,12 @@
1
1
  /* pi-pretty: bash tool -- command execution with styled output. */
2
2
 
3
- import {
4
- type ToolDefinition,
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- type AgentToolResult,
8
- } from "@earendil-works/pi-coding-agent";
9
- import type { SdkToolDef, BashDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
10
- import {
11
- TOOL_RESULT_INDENT,
12
- resolveBaseBackground,
13
- termWidth,
14
- MAX_PREVIEW_LINES,
15
- BG_BASE,
16
- BG_ERROR,
17
- FG_DIM,
18
- FG_RULE,
19
- RST,
20
- } from "../config.js";
21
- import { wrapExecuteWithMetrics } from "./metrics.js";
22
- import { collapsedExpandFooter } from "../collapsed-hint.js";
23
- import { renderBashOutput, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
3
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
+ import { BG_BASE, FG_DIM, MAX_PREVIEW_LINES, resolveBaseBackground, TOOL_RESULT_INDENT, termWidth } from "../config.js";
5
+ import { compactErrorLines, inferBashExitCode, stripBashExitStatusLine } from "../helpers.js";
6
+ import { fillToolBackground, renderToolDuration, renderToolError } from "../render.js";
24
7
  import { resolveTextCtor } from "../tui-text.js";
25
- import { stripBashExitStatusLine, inferBashExitCode, compactErrorLines } from "../helpers.js";
8
+ import type { BashDetails, ComponentLike, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
9
+ import { wrapExecuteWithMetrics } from "./metrics.js";
26
10
 
27
11
  type Result = AgentToolResult<Record<string, unknown>>;
28
12
 
@@ -40,8 +24,8 @@ export function registerBashTool(
40
24
  label: "Bash",
41
25
  description: sdkTool.description
42
26
  ? `${sdkTool.description} For text search: \`rg -n\`.`
43
- : "Execute shell commands. For text search: \`rg -n\`.",
44
- promptSnippet: "Execute commands via bash. For text search: \`rg -n\`.",
27
+ : "Execute shell commands. For text search: `rg -n`.",
28
+ promptSnippet: "Execute commands via bash. For text search: `rg -n`.",
45
29
  promptGuidelines: [
46
30
  "For text search: `rg -n`. If no results, try `rg -u` (respects .gitignore by default).",
47
31
  "In rg: | means alternation, \\| means literal pipe. Opposite of GNU grep. Never use \\| for alternation.",
@@ -78,14 +62,11 @@ export function registerBashTool(
78
62
  rawCmd.length === 0
79
63
  ? theme.fg("toolOutput", "...")
80
64
  : !ctx.expanded && rawCmd.length > headerBudget
81
- ? rawCmd.slice(0, Math.max(1, headerBudget)) + "…"
65
+ ? `${rawCmd.slice(0, Math.max(1, headerBudget))}…`
82
66
  : rawCmd;
67
+ const commandLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold(`$ ${cmd}`));
83
68
  text.setText(
84
- fillToolBackground(
85
- `\n${TOOL_RESULT_INDENT}${theme.fg("toolTitle", theme.bold(`$ ${cmd}`))}${t}`,
86
- ctx.isError ? BG_ERROR : undefined,
87
- ctx.expanded ? undefined : tw,
88
- ),
69
+ fillToolBackground(`\n${TOOL_RESULT_INDENT}${commandLabel}${t}`, undefined, ctx.expanded ? undefined : tw),
89
70
  );
90
71
  return text;
91
72
  },
@@ -111,26 +92,22 @@ export function registerBashTool(
111
92
 
112
93
  if (d?._type === "bashResult") {
113
94
  const isErr = ctx.isError || (d.exitCode !== null && d.exitCode !== 0);
114
- const bg = isErr ? BG_ERROR : undefined;
115
95
  const cleaned = stripBashExitStatusLine(d.text);
116
96
  const output = isErr ? compactErrorLines(cleaned).join("\n") : cleaned;
117
- const { summary } = renderBashOutput(output, d.exitCode);
118
97
  const lineCount = output.split("\n").length;
119
- const info =
120
- lineCount > 1
121
- ? `${TOOL_RESULT_INDENT}${FG_DIM}(${lineCount} lines)${RST} ${renderToolMetrics(result)}`
122
- : ` ${renderToolMetrics(result)}`;
123
- const header = `${TOOL_RESULT_INDENT}${summary}${info}`;
98
+ const info = [`${lineCount} lines`, renderToolDuration(result), !ctx.expanded ? "ctrl+o to expand" : ""]
99
+ .filter(Boolean)
100
+ .map((part) => theme.fg("dim", part))
101
+ .join(theme.fg("dim", " · "));
102
+ const header = `${TOOL_RESULT_INDENT}${info}`;
124
103
  const rw = termWidth();
125
104
 
126
105
  const renderFn = (w: number) => {
127
- if (!ctx.expanded) {
128
- return fillToolBackground([header, collapsedExpandFooter()].join("\n"), bg, w);
129
- }
130
- if (!output.trim()) return fillToolBackground(header, bg, w);
106
+ if (!ctx.expanded) return fillToolBackground(`${header}\n`, undefined, w);
107
+ if (!output.trim()) return fillToolBackground(`${header}\n`, undefined, w);
131
108
  const show = output.split("\n");
132
- const out = [header, rule(w), ...show.map((l: string) => `${TOOL_RESULT_INDENT}${l}`)];
133
- return fillToolBackground(out.join("\n"), bg, w);
109
+ const out = [header, "", ...show.map((line: string) => `${TOOL_RESULT_INDENT}${line}`)];
110
+ return fillToolBackground(`${out.join("\n")}\n`, undefined, w);
134
111
  };
135
112
 
136
113
  text.setText(renderFn(rw));
@@ -140,7 +117,7 @@ export function registerBashTool(
140
117
  let key: string | undefined;
141
118
  (text as unknown as Record<string, unknown>).render = (w: number) => {
142
119
  const width = Math.max(1, Math.floor(w || termWidth()));
143
- const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolMetrics(result)}`;
120
+ const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(result)}`;
144
121
  if (key !== k) {
145
122
  text.setText(renderFn(width));
146
123
  key = k;
@@ -157,16 +134,15 @@ export function registerBashTool(
157
134
  }
158
135
  const fc = result.content?.[0];
159
136
  text.setText(
160
- fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
137
+ fillToolBackground(
138
+ `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
139
+ ),
161
140
  );
162
141
  return text;
163
142
  },
164
143
  } as unknown as ToolDefinition<any, any, any>);
165
144
  }
166
145
 
167
- function rule(w: number): string {
168
- return `${FG_RULE}${"\u2500".repeat(w)}${RST}`;
169
- }
170
146
  function getText(result: Result): string {
171
147
  return (
172
148
  ((result.content ?? []) as TextContent[])
package/src/tools/find.ts CHANGED
@@ -1,20 +1,15 @@
1
1
  /* pi-pretty: find tool -- FFF-backed file search with SDK (fd) fallback. */
2
2
 
3
3
  import { isAbsolute, relative } from "node:path";
4
- import {
5
- type ToolDefinition,
6
- type ExtensionAPI,
7
- type ExtensionContext,
8
- type AgentToolResult,
9
- } from "@earendil-works/pi-coding-agent";
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";
4
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
5
+ import { BG_ERROR, FG_DIM, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from "../config.js";
6
+ import { isLikelyGlobPattern, normalizeFindGlobPattern } from "../find-glob.js";
12
7
  import { shortPath } from "../helpers.js";
13
- import { wrapExecuteWithMetrics } from "./metrics.js";
14
- import { renderFindResults, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
15
- import { resolveTextCtor } from "../tui-text.js";
16
8
  import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
17
- import { isLikelyGlobPattern, normalizeFindGlobPattern } from "../find-glob.js";
9
+ import { fillToolBackground, renderFindResults, renderToolDuration, renderToolError } from "../render.js";
10
+ import { resolveTextCtor } from "../tui-text.js";
11
+ import type { FffServiceWithCursor, FindDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
12
+ import { wrapExecuteWithMetrics } from "./metrics.js";
18
13
 
19
14
  type Result = AgentToolResult<Record<string, unknown>>;
20
15
 
@@ -105,7 +100,9 @@ export function registerFindTool(
105
100
  const basePathResult = fff.getBasePath();
106
101
  const basePath = basePathResult.ok ? basePathResult.value : null;
107
102
  const globPattern = buildGlobPattern(pattern, path, basePath);
108
- const searchResult = fff.glob(globPattern, { pageSize: effectiveLimit });
103
+ const searchResult = fff.glob(globPattern, {
104
+ pageSize: effectiveLimit,
105
+ });
109
106
 
110
107
  if (searchResult.ok) {
111
108
  const items = searchResult.value.items.slice(0, effectiveLimit);
@@ -153,22 +150,15 @@ export function registerFindTool(
153
150
  const a = args as { pattern?: unknown; path?: unknown; limit?: unknown };
154
151
  const text = (ctx as RenderCtxLike).lastComponent ?? new TC("", 0, 0);
155
152
  const pattern = a.pattern == null ? "" : String(a.pattern);
156
- const pathArg =
157
- a.path == null ? "<missing>" : shortPath(cwd, home, String(a.path));
153
+ const pathArg = a.path == null ? "<missing>" : shortPath(cwd, home, String(a.path));
158
154
  const limit = a.limit;
159
- const findLabel = theme.fg("toolTitle", theme.bold("find"));
160
- const patternPart = pattern ? theme.fg("accent", pattern) : "";
155
+ const findLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold("✱ find"));
156
+ const patternPart = pattern ? theme.fg("toolTitle", pattern) : "";
161
157
  const inPart = theme.fg("dim", " in ");
162
158
  const pathPart = theme.fg("toolOutput", pathArg);
163
- const limitPart =
164
- limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
159
+ const limitPart = limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
165
160
  const out = `${findLabel} ${patternPart}${inPart}${pathPart}${limitPart}`;
166
- text.setText(
167
- fillToolBackground(
168
- `\n${TOOL_RESULT_INDENT}${out}`,
169
- ctx.isError ? BG_ERROR : undefined,
170
- ),
171
- );
161
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}`, ctx.isError ? BG_ERROR : undefined));
172
162
  return text;
173
163
  },
174
164
 
@@ -186,17 +176,14 @@ export function registerFindTool(
186
176
  const noticeStr = d.notices?.length
187
177
  ? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
188
178
  : "";
189
- text.setText(
190
- fillToolBackground(
191
- `\n${TOOL_RESULT_INDENT}${theme.fg("dim", "0 files")}${noticeStr}\n`,
192
- ),
193
- );
179
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${theme.fg("dim", "0 files")}${noticeStr}\n`));
194
180
  return text;
195
181
  }
196
182
  if (!ctx.expanded) {
183
+ const duration = renderToolDuration(r);
197
184
  text.setText(
198
185
  fillToolBackground(
199
- `${TOOL_RESULT_INDENT}${FG_DIM}${d.matchCount} files — ctrl+o to expand${RST}${renderToolMetrics(r)}\n`,
186
+ `${TOOL_RESULT_INDENT}${FG_DIM}${d.matchCount} files — ctrl+o to expand${RST}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n`,
200
187
  ),
201
188
  );
202
189
  return text;
@@ -208,20 +195,19 @@ export function registerFindTool(
208
195
  const noticeStr = d.notices?.length
209
196
  ? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
210
197
  : "";
198
+ const duration = renderToolDuration(r);
211
199
  text.setText(
212
200
  fillToolBackground(
213
- `\n${TOOL_RESULT_INDENT}${theme.fg("dim", `${d.matchCount} files`)}${renderToolMetrics(r)}\n${rendered}${noticeStr}\n`,
201
+ `\n${TOOL_RESULT_INDENT}${theme.fg("dim", `${d.matchCount} files`)}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n${rendered}${noticeStr}\n`,
214
202
  ),
215
203
  );
216
204
  return text;
217
205
  }
218
206
  const fc = r.content?.[0] as TextContent | undefined;
219
207
  text.setText(
220
- fillToolBackground(
221
- `\n${TOOL_RESULT_INDENT}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}\n`,
222
- ),
208
+ fillToolBackground(`\n${TOOL_RESULT_INDENT}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}\n`),
223
209
  );
224
210
  return text;
225
211
  },
226
212
  } as unknown as ToolDefinition);
227
- }
213
+ }
package/src/tools/grep.ts CHANGED
@@ -1,19 +1,14 @@
1
1
  /* pi-pretty: grep tool -- FFF-backed text search with SDK fallback. */
2
2
 
3
- import {
4
- type ToolDefinition,
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- type AgentToolResult,
8
- } from "@earendil-works/pi-coding-agent";
9
- import type { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
10
- import { TOOL_RESULT_INDENT, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, RST, resolveBaseBackground } from "../config.js";
11
- import { shortPath, normalizeLineEndings } from "../helpers.js";
12
- import { wrapExecuteWithMetrics } from "./metrics.js";
13
- import { renderToolError, fillToolBackground } from "../render.js";
14
- import { resolveTextCtor } from "../tui-text.js";
3
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
+ import { BG_ERROR, FG_DIM, MAX_PREVIEW_LINES, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from "../config.js";
15
5
  import { fffFormatGrepText } from "../fff-helpers.js";
6
+ import { normalizeLineEndings, shortPath } from "../helpers.js";
16
7
  import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
8
+ import { fillToolBackground, renderToolError } from "../render.js";
9
+ import { resolveTextCtor } from "../tui-text.js";
10
+ import type { FffServiceWithCursor, GrepDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
11
+ import { wrapExecuteWithMetrics } from "./metrics.js";
17
12
 
18
13
  const invalidArg = "<missing>";
19
14
 
@@ -111,7 +106,7 @@ export function registerGrepTool(
111
106
  const limit = args.limit;
112
107
  const literal = args.literal === true;
113
108
  const caseInsensitive = args.caseInsensitive === true || args.ignoreCase === true;
114
- let out = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
109
+ let out = `${theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold("✱ grep"))} ${theme.fg("toolTitle", `/${pattern || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
115
110
  if (glob) out += theme.fg("dim", ` (${String(glob)})`);
116
111
  if (limit !== undefined && limit !== null) out += theme.fg("dim", ` limit ${limit}`);
117
112
  if (literal) out += theme.fg("dim", ` (literal)`);
@@ -158,13 +153,15 @@ export function registerGrepTool(
158
153
  if (remaining > 0) {
159
154
  out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, to expand)`));
160
155
  }
161
- const body = out.map((l) => `${TOOL_RESULT_INDENT}${l}`).join("\n") + "\n\n";
156
+ const body = `${out.map((l) => `${TOOL_RESULT_INDENT}${l}`).join("\n")}\n\n`;
162
157
  text.setText(fillToolBackground(body, ctx.isError ? BG_ERROR : undefined));
163
158
  return text;
164
159
  }
165
160
  const fc = result.content?.[0];
166
161
  const fallback = fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches";
167
- text.setText(fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined));
162
+ text.setText(
163
+ fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined),
164
+ );
168
165
  return text;
169
166
  },
170
167
  } as unknown as ToolDefinition<any, any, any>);
@@ -0,0 +1,3 @@
1
+ export function customToolTitle(name: string): string {
2
+ return `⚙ ${name}`;
3
+ }
package/src/tools/ls.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  /* pi-pretty: ls tool -- directory listing with styled output. */
2
2
 
3
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
4
  import {
4
- type ToolDefinition,
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- type AgentToolResult,
8
- } from "@earendil-works/pi-coding-agent";
9
- import type { SdkToolDef, LsDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
10
- import { TOOL_RESULT_INDENT, resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, RST } from "../config.js";
5
+ BG_BASE,
6
+ BG_ERROR,
7
+ FG_DIM,
8
+ MAX_PREVIEW_LINES,
9
+ RST,
10
+ resolveBaseBackground,
11
+ TOOL_RESULT_INDENT,
12
+ termWidth,
13
+ } from "../config.js";
11
14
  import { shortPath } from "../helpers.js";
12
- import { wrapExecuteWithMetrics } from "./metrics.js";
13
- import { renderTree, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
15
+ import { fillToolBackground, renderToolError, renderToolMetrics, renderTree } from "../render.js";
14
16
  import { resolveTextCtor } from "../tui-text.js";
17
+ import type { ComponentLike, LsDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
18
+ import { wrapExecuteWithMetrics } from "./metrics.js";
15
19
 
16
20
  type Result = AgentToolResult<Record<string, unknown>>;
17
21
 
@@ -91,7 +95,9 @@ export function registerLsTool(
91
95
  }
92
96
  const fc = result.content?.[0];
93
97
  text.setText(
94
- fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
98
+ fillToolBackground(
99
+ `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
100
+ ),
95
101
  );
96
102
  return text;
97
103
  },
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
- import { ELAPSED_KEY, CHARS_KEY } from "../helpers.js";
8
+ import { CHARS_KEY, ELAPSED_KEY } from "../helpers.js";
9
9
 
10
10
  type ExecuteFn = (
11
11
  tid: string,
package/src/tools/read.ts CHANGED
@@ -1,40 +1,24 @@
1
1
  /* pi-pretty: read tool -- file reading with syntax highlighting and inline image support. */
2
2
 
3
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
4
+ import { Image as TuiImage } from "@earendil-works/pi-tui";
3
5
  import {
4
- type ToolDefinition,
5
- type ExtensionAPI,
6
- type ExtensionContext,
7
- type AgentToolResult,
8
- } from "@earendil-works/pi-coding-agent";
9
- import type { SdkToolDef, ReadDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
10
- import {
11
- TOOL_RESULT_INDENT,
12
- resolveBaseBackground,
13
- termWidth,
14
- MAX_PREVIEW_LINES,
15
6
  BG_BASE,
16
7
  BG_ERROR,
17
8
  FG_DIM,
18
9
  FG_LNUM,
19
10
  FG_RULE,
11
+ MAX_PREVIEW_LINES,
20
12
  RST,
13
+ resolveBaseBackground,
14
+ TOOL_RESULT_INDENT,
15
+ termWidth,
21
16
  } from "../config.js";
22
- import { shortPath, normalizeLineEndings } from "../helpers.js";
23
- import { wrapExecuteWithMetrics } from "./metrics.js";
24
- import { renderToolError, renderToolMetrics, fillToolBackground, renderFileContent } from "../render.js";
17
+ import { normalizeLineEndings, shortPath } from "../helpers.js";
18
+ import { fillToolBackground, renderFileContent, renderToolError, renderToolMetrics } from "../render.js";
25
19
  import { resolveTextCtor } from "../tui-text.js";
26
-
27
- // Simple terminal image support check
28
- function isImageTerminal(): boolean {
29
- const term = (process.env.TERM_PROGRAM ?? process.env.TERM ?? "").toLowerCase();
30
- const proto = (process.env.PRETTY_IMAGE_PROTOCOL ?? "").toLowerCase();
31
- if (proto === "kitty" || proto === "iterm2") return true;
32
- if (proto === "none") return false;
33
- return (
34
- ["ghostty", "kitty", "iterm.app", "wezterm", "mintty"].some((t) => term.includes(t)) ||
35
- process.env.LC_TERMINAL === "iTerm2"
36
- );
37
- }
20
+ import type { ReadDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
21
+ import { wrapExecuteWithMetrics } from "./metrics.js";
38
22
 
39
23
  type Result = AgentToolResult<Record<string, unknown>>;
40
24
 
@@ -85,7 +69,14 @@ export function registerReadTool(
85
69
  resolveBaseBackground(theme);
86
70
 
87
71
  const text = ctx.lastComponent ?? new TC("", 0, 0);
88
- text.setText("");
72
+ if (!ctx.isError) {
73
+ text.setText("");
74
+ return text;
75
+ }
76
+
77
+ const path = String(args.path ?? "");
78
+ const label = theme.fg("error", theme.bold("→ read"));
79
+ text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}`, BG_ERROR));
89
80
  return text;
90
81
  },
91
82
 
@@ -95,40 +86,25 @@ export function registerReadTool(
95
86
  const text = ctx.lastComponent ?? new TC("", 0, 0);
96
87
 
97
88
  if (ctx.isError) {
98
- text.setText(renderToolError(getText(result) || "Error", theme));
89
+ text.setText(fillToolBackground(renderToolError(getText(result) || "Error", theme), BG_ERROR));
99
90
  return text;
100
91
  }
101
92
 
102
93
  const d = result.details as ReadDetails | undefined;
103
94
 
104
- // Image rendering
95
+ // Image rendering — use pi-tui Image component
105
96
  if (d?._type === "readImage") {
106
- if ((ctx as any).showImages && isImageTerminal()) {
107
- try {
108
- const T = require("@earendil-works/pi-tui").Text as new (
109
- t?: string,
110
- x?: number,
111
- y?: number,
112
- ) => ComponentLike;
113
- const img = new T("", 0, 0);
114
- if (d.mimeType.startsWith("image/svg")) {
115
- img.setText(d.data);
116
- } else {
117
- const pngData = (require("@earendil-works/pi-coding-agent") as any).convertToPng?.(d.data) ?? d.data;
118
- img.setText(`\x1b_Ga=T,f=100,m=${d.mimeType === "image/png" ? "1" : "0"};${pngData}\x1b\\\\`);
119
- }
120
- return img;
121
- } catch {
122
- /* fall through */
123
- }
124
- }
125
- const fc = result.content?.[0];
126
- text.setText(
127
- fillToolBackground(
128
- `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 80) : `[image: ${d.filePath}]`)}`,
129
- ),
97
+ const mimeType = d.mimeType.startsWith("image/svg") ? "image/svg+xml" : d.mimeType;
98
+ return new TuiImage(
99
+ d.data,
100
+ mimeType,
101
+ {
102
+ fallbackColor: (text) => theme.fg("toolTitle", text),
103
+ },
104
+ {
105
+ filename: d.filePath,
106
+ },
130
107
  );
131
- return text;
132
108
  }
133
109
 
134
110
  // File content — line-numbered display
@@ -141,7 +117,8 @@ export function registerReadTool(
141
117
  if (!ctx.expanded) {
142
118
  text.setText(
143
119
  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`,
120
+ `\n${TOOL_RESULT_INDENT}${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}\n${TOOL_RESULT_INDENT}${FG_DIM}${total} lines — ctrl+o to expand${RST}\n`,
121
+ BG_BASE,
145
122
  ),
146
123
  );
147
124
  return text;
@@ -152,7 +129,7 @@ export function registerReadTool(
152
129
  const gw = nw + 3;
153
130
  const cw = Math.max(1, tw - gw);
154
131
 
155
- const header = `${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", p2)}${theme.fg("dim", off2)}`;
132
+ const header = `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}`;
156
133
  const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`];
157
134
  out.push(`${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(tw - 1)}${RST}`);
158
135
  for (let i = 0; i < show.length; i++) {
@@ -169,7 +146,7 @@ export function registerReadTool(
169
146
  }
170
147
  out.push("");
171
148
  const rendered = out.join("\n");
172
- text.setText(fillToolBackground(rendered));
149
+ text.setText(fillToolBackground(rendered, BG_BASE));
173
150
  (ctx as any).state._rt = rendered;
174
151
 
175
152
  // Async syntax highlighting via Shiki
@@ -180,7 +157,7 @@ export function registerReadTool(
180
157
  .map((l) => `${TOOL_RESULT_INDENT}${l}`)
181
158
  .join("\n");
182
159
  const rendered = `\n${TOOL_RESULT_INDENT}${header}\n${padded}\n`;
183
- text.setText(fillToolBackground(rendered));
160
+ text.setText(fillToolBackground(rendered, BG_BASE));
184
161
  (ctx as any).state._rt = rendered;
185
162
  })
186
163
  .catch(() => {});
@@ -190,7 +167,10 @@ export function registerReadTool(
190
167
 
191
168
  const fc = result.content?.[0];
192
169
  text.setText(
193
- fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
170
+ fillToolBackground(
171
+ `${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
172
+ BG_BASE,
173
+ ),
194
174
  );
195
175
  return text;
196
176
  },
package/src/tui-text.ts CHANGED
@@ -13,10 +13,6 @@ type TextCtor = new (t?: string, x?: number, y?: number) => { setText(v: string)
13
13
 
14
14
  /** No-op stub that satisfies the Text interface so rendering doesn't crash. */
15
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
16
  setText(_v: string): void {
21
17
  // no-op — pi-tui not available
22
18
  }
@@ -26,7 +22,7 @@ let _ctor: TextCtor | null = null;
26
22
  let _resolved = false;
27
23
 
28
24
  function resolve(): TextCtor {
29
- if (_resolved) return _ctor!;
25
+ if (_ctor) return _ctor;
30
26
  _resolved = true;
31
27
  try {
32
28
  // eslint-disable-next-line @typescript-eslint/no-var-requires
package/src/types.ts CHANGED
@@ -2,21 +2,21 @@
2
2
  * pi-pretty shared types.
3
3
  */
4
4
 
5
- import type { TextContent, ImageContent } from "@earendil-works/pi-ai";
5
+ import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
6
6
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
7
7
 
8
8
  // ---------------------------------------------------------------------------
9
9
  // Re-export FFF types needed by tools
10
10
  // ---------------------------------------------------------------------------
11
11
 
12
- export type { FileFinder, FileItem, GrepResult, SearchResult, GrepMatch, GrepCursor } from "@ff-labs/fff-node";
12
+ export type { FileFinder, FileItem, GrepCursor, GrepMatch, GrepResult, SearchResult } from "@ff-labs/fff-node";
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // Content / Result types
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
18
  export type ToolContent = TextContent | ImageContent;
19
- export type { TextContent, ImageContent };
19
+ export type { ImageContent, TextContent };
20
20
  // ---------------------------------------------------------------------------
21
21
  // Theme / rendering context types
22
22
  // ---------------------------------------------------------------------------
@@ -120,14 +120,6 @@ export interface GrepInput {
120
120
  ignoreCase?: boolean;
121
121
  }
122
122
 
123
- export interface MultiGrepInput {
124
- patterns: string[];
125
- path?: string;
126
- constraints?: string;
127
- context?: number;
128
- limit?: number;
129
- }
130
-
131
123
  // ---------------------------------------------------------------------------
132
124
  // SDK tool definition shape (DI-friendly — accepts both mock and real SDK)
133
125
  // ---------------------------------------------------------------------------
@@ -153,29 +145,6 @@ export interface SdkTools {
153
145
  getAgentDir?: () => string;
154
146
  }
155
147
 
156
- // ---------------------------------------------------------------------------
157
- // Multi-grep fallback types
158
- // ---------------------------------------------------------------------------
159
-
160
- export interface MultiGrepFallbackParams {
161
- cwd: string;
162
- patterns: string[];
163
- path?: string;
164
- constraints?: string;
165
- context?: number;
166
- limit: number;
167
- ignoreCase: boolean;
168
- signal?: AbortSignal;
169
- }
170
-
171
- export interface MultiGrepFallbackResult {
172
- text: string;
173
- matchCount: number;
174
- limitReached: boolean;
175
- }
176
-
177
- export type MultiGrepFallback = (params: MultiGrepFallbackParams) => Promise<MultiGrepFallbackResult>;
178
-
179
148
  // ---------------------------------------------------------------------------
180
149
  // FFF service interfaces
181
150
  // ---------------------------------------------------------------------------
@@ -224,5 +193,4 @@ export interface PiPrettyDeps {
224
193
  sdk?: SdkTools;
225
194
  TextComponent?: new (text?: string, x?: number, y?: number) => ComponentLike;
226
195
  fffModule?: typeof import("@ff-labs/fff-node");
227
- multiGrepRipgrepFallback?: MultiGrepFallback;
228
196
  }
@@ -1,30 +0,0 @@
1
- export type ConstraintParseResult = {
2
- ok: true;
3
- globs: string[];
4
- tokens: string[];
5
- } | {
6
- ok: false;
7
- error: string;
8
- };
9
- export type MultiGrepRipgrepFallbackParams = {
10
- cwd: string;
11
- patterns: string[];
12
- path?: string;
13
- constraints?: string;
14
- context?: number;
15
- limit: number;
16
- ignoreCase: boolean;
17
- signal?: AbortSignal;
18
- };
19
- export type MultiGrepRipgrepFallbackResult = {
20
- text: string;
21
- matchCount: number;
22
- limitReached: boolean;
23
- };
24
- export type MultiGrepRipgrepFallback = (params: MultiGrepRipgrepFallbackParams) => Promise<MultiGrepRipgrepFallbackResult>;
25
- export declare function parseMultiGrepConstraints(constraints: string | undefined): ConstraintParseResult;
26
- export declare function getMultiGrepRipgrepArgs(params: MultiGrepRipgrepFallbackParams): ConstraintParseResult & {
27
- args?: string[];
28
- };
29
- export declare function runMultiGrepRipgrepFallback(params: MultiGrepRipgrepFallbackParams): Promise<MultiGrepRipgrepFallbackResult>;
30
- //# sourceMappingURL=multi-grep-fallback.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"multi-grep-fallback.d.ts","sourceRoot":"","sources":["../src/multi-grep-fallback.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,qBAAqB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnH,MAAM,MAAM,8BAA8B,GAAG;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,CACtC,MAAM,EAAE,8BAA8B,KAClC,OAAO,CAAC,8BAA8B,CAAC,CAAC;AA8E7C,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,qBAAqB,CAehG;AAqBD,wBAAgB,uBAAuB,CACtC,MAAM,EAAE,8BAA8B,GACpC,qBAAqB,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAI7C;AAED,wBAAsB,2BAA2B,CAChD,MAAM,EAAE,8BAA8B,GACpC,OAAO,CAAC,8BAA8B,CAAC,CAoGzC"}